diff --git a/pom.xml b/pom.xml index df49023265..e480d1527b 100644 --- a/pom.xml +++ b/pom.xml @@ -485,6 +485,7 @@ see https://blogs.oracle.com/darcy/entry/bootclasspath_older_source --> -Xlint:unchecked,-options + -parameters @@ -740,7 +741,7 @@ - 2.10.0 + 2.23.0 1.3 5.2.0 @@ -757,7 +758,7 @@ 4.0 - 1.3.0 + 1.4.2 9.2.10.v20150310 diff --git a/scm-annotations/src/main/java/sonia/scm/security/AllowAnonymousAccess.java b/scm-annotations/src/main/java/sonia/scm/security/AllowAnonymousAccess.java new file mode 100644 index 0000000000..b770914d69 --- /dev/null +++ b/scm-annotations/src/main/java/sonia/scm/security/AllowAnonymousAccess.java @@ -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 without 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 { +} diff --git a/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java index 4c6a52b878..3ca8335346 100644 --- a/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java +++ b/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java @@ -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 context) { + super(context, createMessage(context)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(List context) { + return context.stream() + .map(c -> c.getType().toLowerCase() + " with id " + c.getId()) + .collect(joining(" in ", "", " already exists")); } } diff --git a/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java b/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java index f0340a6a59..d566859b4c 100644 --- a/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java +++ b/scm-core/src/main/java/sonia/scm/ConcurrentModificationException.java @@ -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 context) { + super(context, createMessage(context)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(List context) { + return context.stream() + .map(c -> c.getType().toLowerCase() + " with id " + c.getId()) + .collect(joining(" in ", "", " has been modified concurrently")); + } } diff --git a/scm-core/src/main/java/sonia/scm/ContextEntry.java b/scm-core/src/main/java/sonia/scm/ContextEntry.java new file mode 100644 index 0000000000..bbd2fadc5d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ContextEntry.java @@ -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 context = new LinkedList<>(); + + public static List noContext() { + return new ContextBuilder().build(); + } + + public static List 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 build() { + return Collections.unmodifiableList(context); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java new file mode 100644 index 0000000000..dd87b77210 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java @@ -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 context; + + public ExceptionWithContext(List context, String message) { + super(message); + this.context = context; + } + + public ExceptionWithContext(List context, String message, Exception cause) { + super(message, cause); + this.context = context; + } + + public List getContext() { + return unmodifiableList(context); + } + + public abstract String getCode(); +} diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index bbe78c8cf7..a621f4f697 100644 --- a/scm-core/src/main/java/sonia/scm/HandlerBase.java +++ b/scm-core/src/main/java/sonia/scm/HandlerBase.java @@ -54,25 +54,21 @@ public interface HandlerBase * * @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); } diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 20b3a16a8e..390777958d 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -58,7 +58,7 @@ public interface Manager * * @throws NotFoundException */ - void refresh(T object) throws NotFoundException; + void refresh(T object); //~--- get methods ---------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index ef20a374cb..f6e91aeced 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -66,7 +66,7 @@ public class ManagerDecorator implements Manager { } @Override - public T create(T object) throws AlreadyExistsException { + public T create(T object) { return decorated.create(object); } diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java index 37546be0b8..69b9617e93 100644 --- a/scm-core/src/main/java/sonia/scm/NotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java @@ -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 context) { + super(context, createMessage(context)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(List context) { + return context.stream() + .map(c -> c.getType().toLowerCase() + " with id " + c.getId()) + .collect(joining(" in ", "could not find ", "")); } } diff --git a/scm-core/src/main/java/sonia/scm/NotSupportedFeatuerException.java b/scm-core/src/main/java/sonia/scm/NotSupportedFeatureException.java similarity index 79% rename from scm-core/src/main/java/sonia/scm/NotSupportedFeatuerException.java rename to scm-core/src/main/java/sonia/scm/NotSupportedFeatureException.java index e6fea6cfa7..daf996ee6c 100644 --- a/scm-core/src/main/java/sonia/scm/NotSupportedFeatuerException.java +++ b/scm-core/src/main/java/sonia/scm/NotSupportedFeatureException.java @@ -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"; } } diff --git a/scm-core/src/main/java/sonia/scm/filter/Filters.java b/scm-core/src/main/java/sonia/scm/filter/Filters.java index b1f5ea47cf..d3ac708296 100644 --- a/scm-core/src/main/java/sonia/scm/filter/Filters.java +++ b/scm-core/src/main/java/sonia/scm/filter/Filters.java @@ -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; diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java index 33b6cb8030..42c8f22a0f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java @@ -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 * * @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"); } /** diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index ac6879a2b9..3e975b862a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -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 context, String message, Exception cause) { + super(context, message, cause); + } + + @Override + public String getCode() { + return null; } } diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java index 7b71078f67..fd0a72ad7c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java @@ -25,9 +25,13 @@ public class NamespaceAndName implements Comparable { return name; } + public String logString() { + return getNamespace() + "/" + getName(); + } + @Override public String toString() { - return getNamespace() + "/" + getName(); + return logString(); } @Override diff --git a/scm-core/src/main/java/sonia/scm/repository/PathNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/PathNotFoundException.java deleted file mode 100644 index ed62a5967c..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/PathNotFoundException.java +++ /dev/null @@ -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; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 19c5b31349..fd8f07df8d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -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 permissions; + private final Set 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 getPermissions() { - if (permissions == null) { - permissions = Lists.newArrayList(); - } - - return permissions; + public Collection getPermissions() { + return Collections.unmodifiableCollection(permissions); } /** @@ -300,8 +297,17 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per this.name = name; } - public void setPermissions(List permissions) { - this.permissions = permissions; + public void setPermissions(Collection 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) { diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java index 445fc22ab8..79c06d03f9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java @@ -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. diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index 1e2fdccf42..7f4880c258 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -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 ---------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java index 87df960da7..b7a819e5f9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java @@ -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); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java deleted file mode 100644 index 9dd866daa4..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java +++ /dev/null @@ -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()); - } -} diff --git a/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java deleted file mode 100644 index 4185e3223d..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java +++ /dev/null @@ -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; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index d2db6856a7..d482c04ea4 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -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) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java index bf896efed8..d1e0cbc5f1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java @@ -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"); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index 2f844cfbfb..fa09cea2cb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -66,6 +66,10 @@ public enum Command /** * @since 2.0 */ - MODIFICATIONS + MODIFICATIONS, + /** + * @since 2.0 + */ + MERGE } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java index c0e9e3f622..7217d0e97a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java @@ -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"); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java index 9c782a781b..73062a0244 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java @@ -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 -------------------------------------------------------- /** diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java new file mode 100644 index 0000000000..881a374864 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java @@ -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 feature_branch into integration_branch do this: + *

+ *     repositoryService.gerMergeCommand()
+ *       .setBranchToMerge("feature_branch")
+ *       .setTargetBranch("integration_branch")
+ *       .executeMerge();
+ * 
+ * + * If the merge is successful, the result will look like this: + *

+ *                            O    <- Merge result (new head of integration_branch)
+ *                            |\
+ *                            | \
+ *  old integration_branch -> O  O <- feature_branch
+ *                            |  |
+ *                            O  O
+ * 
+ * + * To check whether they can be merged without conflicts beforehand do this: + *

+ *     repositoryService.gerMergeCommand()
+ *       .setBranchToMerge("feature_branch")
+ *       .setTargetBranch("integration_branch")
+ *       .dryRun()
+ *       .isMergeable();
+ * 
+ * + * Keep in mind that you should always 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. + * + * This is mandatory. + * + * @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. + * + * This is mandatory. + * + * @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 {0} for the branch to be merged and {1} for the target + * branch, eg.: + * + *

+   * ...setMessageTemplate("Merge of {0} into {1}")...
+   * 
+ * + * 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 false. + * + * @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 true. + * + * @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); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java new file mode 100644 index 0000000000..53f712cddc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java @@ -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 false 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 filesWithConflict; + + private MergeCommandResult(Collection filesWithConflict) { + this.filesWithConflict = filesWithConflict; + } + + public static MergeCommandResult success() { + return new MergeCommandResult(emptyList()); + } + + public static MergeCommandResult failure(Collection filesWithConflict) { + return new MergeCommandResult(new HashSet<>(filesWithConflict)); + } + + /** + * If this returns true, the merge was successfull. If this returns false 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 false) this will give you a list of + * file paths that could not be merged automatically. + */ + public Collection getFilesWithConflict() { + return unmodifiableCollection(filesWithConflict); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java new file mode 100644 index 0000000000..6ebb330aae --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java @@ -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 true, when an automatic merge is possible at the moment; false + * otherwise. + */ + public boolean isMergeable() { + return mergeable; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java index 6459b47cd5..498746cc60 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java @@ -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); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index bdd6e4b320..fe0529e6b5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -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. * diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index fbb1ee6b58..8db3ab7546 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -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); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java index be679f9df1..ee37d6243e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java index 06f242783b..81600230db 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java index 105f4e48e1..bba42cd86d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java index 5cfd57f71b..e26b2eb5aa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java b/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java index 52ef68f272..4bbe61ea41 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java @@ -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(), diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java index e4a7f3437b..f4babcee72 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java index 9356bb212a..92cd41662b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java new file mode 100644 index 0000000000..0a3680f6b3 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java @@ -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); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java new file mode 100644 index 0000000000..baf03a0aef --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java @@ -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(); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java index e9b40e8a17..322468f827 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index c66c56c0f1..77201d1a72 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -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); + } } diff --git a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java index fda5e69323..caa35e0b88 100644 --- a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java +++ b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java @@ -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; + } } diff --git a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java index 870430a1bb..93a6a7c1d1 100644 --- a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java +++ b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java @@ -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; } } diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 7b6d5cb039..e2a2218d34 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -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() { } diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java index b88dda8a97..ffe6ecc787 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java @@ -301,7 +301,7 @@ public class AuthenticationFilter extends HttpFilter } } - chain.doFilter(new SecurityHttpServletRequestWrapper(request, username), + chain.doFilter(new PropagatePrincipleServletRequestWrapper(request, username), response); } diff --git a/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java b/scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java similarity index 78% rename from scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java rename to scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java index 2cd78ce807..2b40b0e73f 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java @@ -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; } diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java index 93b0752766..50bde53ae1 100644 --- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java @@ -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); diff --git a/scm-it/pom.xml b/scm-it/pom.xml index 251fe4d957..3f518dd9fe 100644 --- a/scm-it/pom.xml +++ b/scm-it/pom.xml @@ -80,13 +80,6 @@ test - - javax - javaee-api - 7.0 - test - - org.glassfish javax.json diff --git a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java new file mode 100644 index 0000000000..0a329b8e1e --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java @@ -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()); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/DiffITCase.java b/scm-it/src/test/java/sonia/scm/it/DiffITCase.java new file mode 100644 index 0000000000..acd2816422 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/DiffITCase.java @@ -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 svnRepositoryResponse; + private ScmRequests.RepositoryResponse hgRepositoryResponse; + private ScmRequests.RepositoryResponse 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 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 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 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 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 addedFiles = new HashMap() {{ + put(addedFile, "content"); + }}; + Map modifiedFiles = new HashMap() {{ + put(fileToBeUpdated, "the updated content"); + }}; + ArrayList removedFiles = Lists.newArrayList(fileToBeDeleted); + return RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index 8785f1d8ce..aa91e67022 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -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 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 userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType); - assertEquals(userPermissions.size(), createdPermissions); + List userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType); + Assertions.assertThat(userPermissions).extracting(e -> e.get("name")).containsAll(createdPermissions); } @Test diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index 66ebc57c90..83baa89463 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -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 addedFiles = new HashMap() - {{ + Map addedFiles = new HashMap() {{ put("a.txt", "bla bla"); }}; - Map modifiedFiles = new HashMap() - {{ + Map modifiedFiles = new HashMap() {{ put("b.txt", "new content"); }}; ArrayList 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) diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java index 11db0200f1..427d98f245 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java @@ -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)) { diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java index 14caa57beb..bde3892773 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java @@ -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 requestDiff(String revision) { - return new DiffResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"), this); + public DiffResponse requestDiffInGitFormat(String revision) { + return new DiffResponse<>(applyGETRequestFromLinkWithParams(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href", "?format=GIT"), this); } public ModificationsResponse requestModifications(String revision) { @@ -362,6 +366,10 @@ public class ScmRequests { this.previousResponse = previousResponse; } + public Response getResponse(){ + return response; + } + public PREV returnToPrevious() { return previousResponse; } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index a164fc6649..48605437c6 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -99,10 +99,10 @@ public class TestData { ; } - public static List getUserPermissions(String username, String password, String repositoryType) { + public static List getUserPermissions(String username, String password, String repositoryType) { return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) .extract() - .body().jsonPath().getList("_embedded.permissions"); + .body().jsonPath().getList("_embedded.permissions"); } public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { diff --git a/scm-it/src/test/resources/diff/binaryfile/echo b/scm-it/src/test/resources/diff/binaryfile/echo new file mode 100755 index 0000000000..11bc2152e4 Binary files /dev/null and b/scm-it/src/test/resources/diff/binaryfile/echo differ diff --git a/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest new file mode 100644 index 0000000000..f714a955c7 --- /dev/null +++ b/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest @@ -0,0 +1,1230 @@ +package sonia.scm.repository.spi; + +import de.regnis.q.sequence.line.diff.QDiffGenerator; +import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; +import de.regnis.q.sequence.line.diff.QDiffManager; +import de.regnis.q.sequence.line.diff.QDiffUniGenerator; +import org.tmatesoft.svn.core.SVNErrorCode; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNMergeRangeList; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.SVNPropertyValue; +import org.tmatesoft.svn.core.internal.util.SVNHashMap; +import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; +import org.tmatesoft.svn.core.internal.util.SVNPathUtil; +import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; +import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; +import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; +import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNDiffOptions; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import org.tmatesoft.svn.util.SVNLogType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public class SCMSvnDiffGenerator implements ISvnDiffGenerator { + + protected static final String WC_REVISION_LABEL = "(working copy)"; + protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; + protected static final String HEADER_SEPARATOR = "==================================================================="; + protected static final String HEADER_ENCODING = "UTF-8"; + + private SvnTarget originalTarget1; + private SvnTarget originalTarget2; + private SvnTarget baseTarget; + private SvnTarget relativeToTarget; + private SvnTarget repositoryRoot; + private String encoding; + private byte[] eol; + private boolean useGitFormat; + private boolean forcedBinaryDiff; + + private boolean diffDeleted; + private boolean diffAdded; + private List rawDiffOptions; + private boolean forceEmpty; + + private Set visitedPaths; + private String externalDiffCommand; + private SVNDiffOptions diffOptions; + private boolean fallbackToAbsolutePath; + private ISVNOptions options; + private boolean propertiesOnly; + private boolean ignoreProperties; + + private String getDisplayPath(SvnTarget target) { + String relativePath; + if (baseTarget == null) { + relativePath = null; + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = baseTarget.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) { + String relativePath; + if (repositoryRoot == null) { + relativePath = null; + } else { + if (repositoryRoot.isFile() == target.isFile()) { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = repositoryRoot.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = new File("").getAbsolutePath(); + relativePath = getRelativePath(targetString, baseTargetString); + } + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativePath(String targetString, String baseTargetString) { + if (targetString != null) { + targetString = targetString.replace(File.separatorChar, '/'); + } + if (baseTargetString != null) { + baseTargetString = baseTargetString.replace(File.separatorChar, '/'); + } + + final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString); + if (pathAsChild != null) { + return pathAsChild; + } + if (targetString.equals(baseTargetString)) { + return ""; + } + return null; + } + + private String getChildPath(String path, String relativeToPath) { + if (relativeToTarget == null) { + return null; + } + + String relativePath = getRelativePath(path, relativeToPath); + if (relativePath == null) { + return path; + } + + if (relativePath.length() > 0) { + return relativePath; + } + + if (relativeToPath.equals(path)) { + return "."; + } + + return null; + } + + public SCMSvnDiffGenerator() { + this.originalTarget1 = null; + this.originalTarget2 = null; + this.visitedPaths = new HashSet(); + this.diffDeleted = true; + this.diffAdded = true; + } + + public void setBaseTarget(SvnTarget baseTarget) { + this.baseTarget = baseTarget; + } + + public void setUseGitFormat(boolean useGitFormat) { + this.useGitFormat = useGitFormat; + } + + public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) { + this.originalTarget1 = originalTarget1; + this.originalTarget2 = originalTarget2; + } + + public void setRelativeToTarget(SvnTarget relativeToTarget) { + this.relativeToTarget = relativeToTarget; + } + + public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) { + //anchors are not used + } + + public void setRepositoryRoot(SvnTarget repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public void setForceEmpty(boolean forceEmpty) { + this.forceEmpty = forceEmpty; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + public String getGlobalEncoding() { + ISVNOptions options = getOptions(); + + if (options != null && options instanceof DefaultSVNOptions) { + DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options; + return defaultOptions.getGlobalCharset(); + } + return null; + } + + public void setEOL(byte[] eol) { + this.eol = eol; + } + + public byte[] getEOL() { + return eol; + } + + public boolean isForcedBinaryDiff() { + return forcedBinaryDiff; + } + + public void setForcedBinaryDiff(boolean forcedBinaryDiff) { + this.forcedBinaryDiff = forcedBinaryDiff; + } + + public boolean isPropertiesOnly() { + return propertiesOnly; + } + + public void setPropertiesOnly(boolean propertiesOnly) { + this.propertiesOnly = propertiesOnly; + } + + public boolean isIgnoreProperties() { + return ignoreProperties; + } + + public void setIgnoreProperties(boolean ignoreProperties) { + this.ignoreProperties = ignoreProperties; + } + + public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isIgnoreProperties()) { + return; + } + if (dirWasAdded && !isDiffAdded()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (displayPath == null || displayPath.length() == 0) { + displayPath = "."; + } + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + boolean showDiffHeader = !visitedPaths.contains(displayPath); + if (showDiffHeader) { + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified); + visitedPaths.add(displayPath); + if (useGitFormat) { + displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + if (shouldStopDisplaying) { + return; + } + +// if (useGitFormat) { +// String copyFromPath = null; +// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified; +// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1); +// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2); +// displayGitDiffHeader(outputStream, operationKind, +// getRelativeToRootPath(target, originalTarget1), +// getRelativeToRootPath(target, originalTarget2), +// copyFromPath); +// } + + if (useGitFormat) { + displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null); + } else { + displayHeaderFields(outputStream, label1, label2); + } + } + + displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream); + + displayPropDiffValues(outputStream, propChanges, originalProps); + } + + private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''", + displayPath, relativeToPath); + SVNErrorManager.error(errorMessage, SVNLogType.CLIENT); + } + + private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + + try { + displayString(outputStream, "--- "); + displayFirstGitLabelPath(outputStream, path1, revision1, operation); + displayEOL(outputStream); + displayString(outputStream, "+++ "); + displaySecondGitLabelPath(outputStream, path2, revision2, operation); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private String adjustRelativeToReposRoot(String targetString) { + if (repositoryRoot != null) { + String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString(); + String relativePath = getRelativePath(targetString, repositoryRootString); + return relativePath == null ? "" : relativePath; + } + return targetString; + } + + private String computeLabel(String targetString, String originalTargetString) { + if (originalTargetString.length() == 0) { + return targetString; + } else if (originalTargetString.charAt(0) == '/') { + return targetString + "\t(..." + originalTargetString + ")"; + } else { + return targetString + "\t(.../" + originalTargetString + ")"; + } + } + + public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isPropertiesOnly()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean leftIsBinary = false; + boolean rightIsBinary = false; + + if (mimeType1 != null) { + leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1); + } + if (mimeType2 != null) { + rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2); + } + + if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + + displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + + return; + } + + final String diffCommand = getExternalDiffCommand(); + if (diffCommand != null) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2); + } else { + internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2); + } + } + + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { + displayCannotDisplayFileMarkedBinary(outputStream); + + if (leftIsBinary && !rightIsBinary) { + displayMimeType(outputStream, mimeType1); + } else if (!leftIsBinary && rightIsBinary) { + displayMimeType(outputStream, mimeType2); + } else if (leftIsBinary && rightIsBinary) { + if (mimeType1.equals(mimeType2)) { + displayMimeType(outputStream, mimeType1); + } else { + displayMimeTypes(outputStream, mimeType1, mimeType2); + } + } + } + + private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException { + String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath); + if (file2 == null && !isDiffDeleted()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + if (file1 == null && !isDiffAdded()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath); + + RandomAccessFile is1 = null; + RandomAccessFile is2 = null; + try { + is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); + is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + final String diffHeader; + if (forceEmpty || useGitFormat) { + displayString(outputStream, header); + diffHeader = headerFields; + + visitedPaths.add(displayPath); + } else { + diffHeader = header + headerFields; + } + QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader); + EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream); + QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator); + if (emptyDetectionOutputStream.isSomethingWritten()) { + visitedPaths.add(displayPath); + } + emptyDetectionOutputStream.flush(); + } catch (IOException e) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); + SVNErrorManager.error(err, e, SVNLogType.DEFAULT); + } finally { + SVNFileUtil.closeFile(is1); + SVNFileUtil.closeFile(is2); + } + } + + private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (useGitFormat) { + displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath); + } else { + displayHeaderFields(byteArrayOutputStream, label1, label2); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + boolean stopDisplaying = displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation); + if (useGitFormat) { + displayGitDiffHeader(byteArrayOutputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + copyFromPath); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException { + final List args = new ArrayList(); + args.add(diffCommand); + if (rawDiffOptions != null) { + args.addAll(rawDiffOptions); + } else { + Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection(); + args.addAll(svnDiffOptionsCollection); + args.add("-u"); + } + + if (label1 != null) { + args.add("-L"); + args.add(label1); + } + + if (label2 != null) { + args.add("-L"); + args.add(label2); + } + + boolean tmpFile1 = false; + boolean tmpFile2 = false; + if (file1 == null) { + file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile1 = true; + } + if (file2 == null) { + file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile2 = true; + } + + String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); + String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); + + args.add(file1Path); + args.add(file2Path); + try { + final Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + + SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true, + new ISVNReturnValueCallback() { + + public void handleReturnValue(int returnValue) throws SVNException { + if (returnValue != 0 && returnValue != 1) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, + "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)}); + SVNErrorManager.error(err, SVNLogType.DEFAULT); + } + } + + public void handleChar(char ch) throws SVNException { + try { + writer.write(ch); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } + } + + public boolean isHandleProgramOutput() { + return true; + } + }); + + writer.flush(); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } finally { + try { + if (tmpFile1) { + SVNFileUtil.deleteFile(file1); + } + if (tmpFile2) { + SVNFileUtil.deleteFile(file2); + } + } catch (SVNException e) { + // skip + } + } + } + + private String getExternalDiffCommand() { + return externalDiffCommand; + } + + private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = "); + displayString(outputStream, mimeType); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = ("); + displayString(outputStream, mimeType1); + displayString(outputStream, ", "); + displayString(outputStream, mimeType2); + displayString(outputStream, ")"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException { + try { + displayString(outputStream, "Cannot display: file marked as a binary type."); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void ensureEncodingAndEOLSet() { + if (getEOL() == null) { + setEOL(SVNProperty.EOL_LF_BYTES); + } + if (getEncoding() == null) { + final ISVNOptions options = getOptions(); + if (options != null && options.getNativeCharset() != null) { + setEncoding(options.getNativeCharset()); + } else { + setEncoding("UTF-8"); + } + } + } + + private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException { + for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) { + String name = (String) changedPropNames.next(); + SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; + SVNPropertyValue newValue = diff.getSVNPropertyValue(name); + String headerFormat = null; + + if (originalValue == null) { + headerFormat = "Added: "; + } else if (newValue == null) { + headerFormat = "Deleted: "; + } else { + headerFormat = "Modified: "; + } + + try { + displayString(outputStream, (headerFormat + name)); + displayEOL(outputStream); + if (SVNProperty.MERGE_INFO.equals(name)) { + displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); + continue; + } + + byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding()); + byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding()); + + if (originalValueBytes == null) { + originalValueBytes = new byte[0]; + } else { + originalValueBytes = maybeAppendEOL(originalValueBytes); + } + + boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 && + (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] || + newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]); + + if (newValueBytes == null) { + newValueBytes = new byte[0]; + } else { + newValueBytes = maybeAppendEOL(newValueBytes); + } + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##"); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + QDiffGenerator generator = new QDiffUniGenerator(properties, ""); + Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes), + null, writer, generator); + writer.flush(); + if (!newValueHadEol) { + displayString(outputStream, "\\ No newline at end of property"); + displayEOL(outputStream); + } + } catch (IOException e) { + wrapException(e); + } + } + + } + + private byte[] maybeAppendEOL(byte[] buffer) { + if (buffer.length == 0) { + return buffer; + } + + byte lastByte = buffer[buffer.length - 1]; + if (lastByte == SVNProperty.EOL_CR_BYTES[0]) { + return buffer; + } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) { + final byte[] newBuffer = new byte[buffer.length + getEOL().length]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length); + return newBuffer; + } else { + return buffer; + } + } + + private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("a/" + copyFromPath, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("a/" + copyFromPath, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("b/" + path2, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath); + } + } + + private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "new file mode 10644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "deleted file mode 10644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "copy from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "copy to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "rename from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "rename to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException { + displayGitPath(outputStream, path1, "a/", false); + } + + private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException { + displayGitPath(outputStream, path2, "b/", false); + } + + private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "a/"; + if (operation == SvnDiffCallback.OperationKind.Added) { + path1 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true); + } + + private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "b/"; + if (operation == SvnDiffCallback.OperationKind.Deleted) { + path2 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true); + } + + private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException { +// if (!label && path1.length() == 0) { +// displayString(outputStream, "."); +// } else { + displayString(outputStream, pathPrefix); + displayString(outputStream, path1); +// } + } + + private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) { + String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor); + return getLabel(adjustedPath, revision); + } + + private String getAdjustedPath(String displayPath, String path1, String commonAncestor) { + String adjustedPath = getRelativePath(path1, commonAncestor); + + if (adjustedPath == null || adjustedPath.length() == 0) { + adjustedPath = displayPath; + } else if (adjustedPath.charAt(0) == '/') { + adjustedPath = displayPath + "\t(..." + adjustedPath + ")"; + } else { + adjustedPath = displayPath + "\t(.../" + adjustedPath + ")"; + } + return adjustedPath; + //TODO: respect relativeToDir + } + + protected String getLabel(String path, String revToken) { + revToken = revToken == null ? WC_REVISION_LABEL : revToken; + return path + "\t" + revToken; + } + + protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException { + try { + if (deleted && !isDiffDeleted()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (deleted)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + if (added && !isDiffAdded()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (added)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + displayString(os, "Index: "); + displayString(os, path); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return false; + } catch (IOException e) { + wrapException(e); + } + return false; + } + + protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException { + try { + displayString(os, "--- "); + displayString(os, label1); + displayEOL(os); + displayString(os, "+++ "); + displayString(os, label2); + displayEOL(os); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException { + try { + displayEOL(outputStream); + displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path))); + displayEOL(outputStream); + displayString(outputStream, PROPERTIES_SEPARATOR); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) { + if (value == null) { + return null; + } + if (value.isString()) { + try { + return value.getString().getBytes(encoding); + } catch (UnsupportedEncodingException e) { + return value.getString().getBytes(); + } + } + return value.getBytes(); + } + + private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException { + Map oldMergeInfo = null; + Map newMergeInfo = null; + if (oldValue != null) { + oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); + } + if (newValue != null) { + newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); + } + + Map deleted = new TreeMap(); + Map added = new TreeMap(); + SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); + + for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); + displayString(outputStream, (" Reverse-merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + + for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); + displayString(outputStream, (" Merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + } + + private boolean useLocalFileSeparatorChar() { + return true; + } + + public boolean isDiffDeleted() { + return diffDeleted; + } + + public boolean isDiffAdded() { + return diffAdded; + } + + private void wrapException(IOException e) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e); + SVNErrorManager.error(errorMessage, e, SVNLogType.WC); + } + + private void displayString(OutputStream outputStream, String s) throws IOException { + outputStream.write(s.getBytes(HEADER_ENCODING)); + } + + private void displayEOL(OutputStream os) throws IOException { + os.write(getEOL()); + } + + public SVNDiffOptions getDiffOptions() { + if (diffOptions == null) { + diffOptions = new SVNDiffOptions(); + } + return diffOptions; + } + + public void setExternalDiffCommand(String externalDiffCommand) { + this.externalDiffCommand = externalDiffCommand; + } + + public void setRawDiffOptions(List rawDiffOptions) { + this.rawDiffOptions = rawDiffOptions; + } + + public void setDiffOptions(SVNDiffOptions diffOptions) { + this.diffOptions = diffOptions; + } + + public void setDiffDeleted(boolean diffDeleted) { + this.diffDeleted = diffDeleted; + } + + public void setDiffAdded(boolean diffAdded) { + this.diffAdded = diffAdded; + } + + public void setBasePath(File absoluteFile) { + setBaseTarget(SvnTarget.fromFile(absoluteFile)); + } + + public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) { + this.fallbackToAbsolutePath = fallbackToAbsolutePath; + } + + public void setOptions(ISVNOptions options) { + this.options = options; + } + + public ISVNOptions getOptions() { + return options; + } + + private class EmptyDetectionOutputStream extends OutputStream { + + private final OutputStream outputStream; + private boolean somethingWritten; + + public EmptyDetectionOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.somethingWritten = false; + } + + public boolean isSomethingWritten() { + return somethingWritten; + } + + @Override + public void write(int c) throws IOException { + somethingWritten = true; + outputStream.write(c); + } + + @Override + public void write(byte[] bytes) throws IOException { + somethingWritten = bytes.length > 0; + outputStream.write(bytes); + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + somethingWritten = length > 0; + outputStream.write(bytes, offset, length); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + } +} diff --git a/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest new file mode 100644 index 0000000000..c46ce5f534 --- /dev/null +++ b/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest @@ -0,0 +1,1234 @@ +package sonia.scm.repository.spi; + +import de.regnis.q.sequence.line.diff.QDiffGenerator; +import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; +import de.regnis.q.sequence.line.diff.QDiffManager; +import de.regnis.q.sequence.line.diff.QDiffUniGenerator; +import org.tmatesoft.svn.core.SVNErrorCode; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNMergeRangeList; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.SVNPropertyValue; +import org.tmatesoft.svn.core.internal.util.SVNHashMap; +import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; +import org.tmatesoft.svn.core.internal.util.SVNPathUtil; +import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; +import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; +import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; +import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNDiffOptions; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import org.tmatesoft.svn.util.SVNLogType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public class SCMSvnDiffGenerator implements ISvnDiffGenerator { + + protected static final String WC_REVISION_LABEL = "(working copy)"; + protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; + protected static final String HEADER_SEPARATOR = "==================================================================="; + protected static final String HEADER_ENCODING = "UTF-8"; + + private SvnTarget originalTarget1; + private SvnTarget originalTarget2; + private SvnTarget baseTarget; + private SvnTarget relativeToTarget; + private SvnTarget repositoryRoot; + private String encoding; + private byte[] eol; + private boolean useGitFormat; + private boolean forcedBinaryDiff; + + private boolean diffDeleted; + private boolean diffAdded; + private List rawDiffOptions; + private boolean forceEmpty; + + private Set visitedPaths; + private String externalDiffCommand; + private SVNDiffOptions diffOptions; + private boolean fallbackToAbsolutePath; + private ISVNOptions options; + private boolean propertiesOnly; + private boolean ignoreProperties; + + private String getDisplayPath(SvnTarget target) { + String relativePath; + if (baseTarget == null) { + relativePath = null; + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = baseTarget.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) { + String relativePath; + if (repositoryRoot == null) { + relativePath = null; + } else { + if (repositoryRoot.isFile() == target.isFile()) { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = repositoryRoot.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = new File("").getAbsolutePath(); + relativePath = getRelativePath(targetString, baseTargetString); + } + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativePath(String targetString, String baseTargetString) { + if (targetString != null) { + targetString = targetString.replace(File.separatorChar, '/'); + } + if (baseTargetString != null) { + baseTargetString = baseTargetString.replace(File.separatorChar, '/'); + } + + final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString); + if (pathAsChild != null) { + return pathAsChild; + } + if (targetString.equals(baseTargetString)) { + return ""; + } + return null; + } + + private String getChildPath(String path, String relativeToPath) { + if (relativeToTarget == null) { + return null; + } + + String relativePath = getRelativePath(path, relativeToPath); + if (relativePath == null) { + return path; + } + + if (relativePath.length() > 0) { + return relativePath; + } + + if (relativeToPath.equals(path)) { + return "."; + } + + return null; + } + + public SCMSvnDiffGenerator() { + this.originalTarget1 = null; + this.originalTarget2 = null; + this.visitedPaths = new HashSet(); + this.diffDeleted = true; + this.diffAdded = true; + } + + public void setBaseTarget(SvnTarget baseTarget) { + this.baseTarget = baseTarget; + } + + public void setUseGitFormat(boolean useGitFormat) { + this.useGitFormat = useGitFormat; + } + + public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) { + this.originalTarget1 = originalTarget1; + this.originalTarget2 = originalTarget2; + } + + public void setRelativeToTarget(SvnTarget relativeToTarget) { + this.relativeToTarget = relativeToTarget; + } + + public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) { + //anchors are not used + } + + public void setRepositoryRoot(SvnTarget repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public void setForceEmpty(boolean forceEmpty) { + this.forceEmpty = forceEmpty; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + public String getGlobalEncoding() { + ISVNOptions options = getOptions(); + + if (options != null && options instanceof DefaultSVNOptions) { + DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options; + return defaultOptions.getGlobalCharset(); + } + return null; + } + + public void setEOL(byte[] eol) { + this.eol = eol; + } + + public byte[] getEOL() { + return eol; + } + + public boolean isForcedBinaryDiff() { + return forcedBinaryDiff; + } + + public void setForcedBinaryDiff(boolean forcedBinaryDiff) { + this.forcedBinaryDiff = forcedBinaryDiff; + } + + public boolean isPropertiesOnly() { + return propertiesOnly; + } + + public void setPropertiesOnly(boolean propertiesOnly) { + this.propertiesOnly = propertiesOnly; + } + + public boolean isIgnoreProperties() { + return ignoreProperties; + } + + public void setIgnoreProperties(boolean ignoreProperties) { + this.ignoreProperties = ignoreProperties; + } + + public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isIgnoreProperties()) { + return; + } + if (dirWasAdded && !isDiffAdded()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (displayPath == null || displayPath.length() == 0) { + displayPath = "."; + } + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + boolean showDiffHeader = !visitedPaths.contains(displayPath); + if (showDiffHeader) { + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified); + visitedPaths.add(displayPath); + if (useGitFormat) { + displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + if (shouldStopDisplaying) { + return; + } + +// if (useGitFormat) { +// String copyFromPath = null; +// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified; +// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1); +// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2); +// displayGitDiffHeader(outputStream, operationKind, +// getRelativeToRootPath(target, originalTarget1), +// getRelativeToRootPath(target, originalTarget2), +// copyFromPath); +// } + + if (useGitFormat) { + displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null); + } else { + displayHeaderFields(outputStream, label1, label2); + } + } + + displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream); + + displayPropDiffValues(outputStream, propChanges, originalProps); + } + + private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''", + displayPath, relativeToPath); + SVNErrorManager.error(errorMessage, SVNLogType.CLIENT); + } + + private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + + try { + displayString(outputStream, "--- "); + displayFirstGitLabelPath(outputStream, path1, revision1, operation); + displayEOL(outputStream); + displayString(outputStream, "+++ "); + displaySecondGitLabelPath(outputStream, path2, revision2, operation); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private String adjustRelativeToReposRoot(String targetString) { + if (repositoryRoot != null) { + String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString(); + String relativePath = getRelativePath(targetString, repositoryRootString); + return relativePath == null ? "" : relativePath; + } + return targetString; + } + + private String computeLabel(String targetString, String originalTargetString) { + if (originalTargetString.length() == 0) { + return targetString; + } else if (originalTargetString.charAt(0) == '/') { + return targetString + "\t(..." + originalTargetString + ")"; + } else { + return targetString + "\t(.../" + originalTargetString + ")"; + } + } + + public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isPropertiesOnly()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean leftIsBinary = false; + boolean rightIsBinary = false; + + if (mimeType1 != null) { + leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1); + } + if (mimeType2 != null) { + rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2); + } + + if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + + displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + + return; + } + + final String diffCommand = getExternalDiffCommand(); + if (diffCommand != null) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2); + } else { + internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2); + } + } + + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { + displayCannotDisplayFileMarkedBinary(outputStream); + + if (leftIsBinary && !rightIsBinary) { + displayMimeType(outputStream, mimeType1); + } else if (!leftIsBinary && rightIsBinary) { + displayMimeType(outputStream, mimeType2); + } else if (leftIsBinary && rightIsBinary) { + if (mimeType1.equals(mimeType2)) { + displayMimeType(outputStream, mimeType1); + } else { + displayMimeTypes(outputStream, mimeType1, mimeType2); + } + } + } + + private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException { + String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath); + if (file2 == null && !isDiffDeleted()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + if (file1 == null && !isDiffAdded()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath); + + RandomAccessFile is1 = null; + RandomAccessFile is2 = null; + try { + is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); + is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + final String diffHeader; + if (forceEmpty || useGitFormat) { + displayString(outputStream, header); + diffHeader = headerFields; + + visitedPaths.add(displayPath); + } else { + diffHeader = header + headerFields; + } + QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader); + EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream); + QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator); + if (emptyDetectionOutputStream.isSomethingWritten()) { + visitedPaths.add(displayPath); + } + emptyDetectionOutputStream.flush(); + } catch (IOException e) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); + SVNErrorManager.error(err, e, SVNLogType.DEFAULT); + } finally { + SVNFileUtil.closeFile(is1); + SVNFileUtil.closeFile(is2); + } + } + + private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (useGitFormat) { + displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath); + } else { + displayHeaderFields(byteArrayOutputStream, label1, label2); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (!useGitFormat) { + displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation); + } else { + displayGitDiffHeader(byteArrayOutputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + copyFromPath); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException { + final List args = new ArrayList(); + args.add(diffCommand); + if (rawDiffOptions != null) { + args.addAll(rawDiffOptions); + } else { + Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection(); + args.addAll(svnDiffOptionsCollection); + args.add("-u"); + } + + if (label1 != null) { + args.add("-L"); + args.add(label1); + } + + if (label2 != null) { + args.add("-L"); + args.add(label2); + } + + boolean tmpFile1 = false; + boolean tmpFile2 = false; + if (file1 == null) { + file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile1 = true; + } + if (file2 == null) { + file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile2 = true; + } + + String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); + String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); + + args.add(file1Path); + args.add(file2Path); + try { + final Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + + SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true, + new ISVNReturnValueCallback() { + + public void handleReturnValue(int returnValue) throws SVNException { + if (returnValue != 0 && returnValue != 1) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, + "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)}); + SVNErrorManager.error(err, SVNLogType.DEFAULT); + } + } + + public void handleChar(char ch) throws SVNException { + try { + writer.write(ch); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } + } + + public boolean isHandleProgramOutput() { + return true; + } + }); + + writer.flush(); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } finally { + try { + if (tmpFile1) { + SVNFileUtil.deleteFile(file1); + } + if (tmpFile2) { + SVNFileUtil.deleteFile(file2); + } + } catch (SVNException e) { + // skip + } + } + } + + private String getExternalDiffCommand() { + return externalDiffCommand; + } + + private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = "); + displayString(outputStream, mimeType); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = ("); + displayString(outputStream, mimeType1); + displayString(outputStream, ", "); + displayString(outputStream, mimeType2); + displayString(outputStream, ")"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException { + try { + displayString(outputStream, "Cannot display: file marked as a binary type."); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void ensureEncodingAndEOLSet() { + if (getEOL() == null) { + setEOL(SVNProperty.EOL_LF_BYTES); + } + if (getEncoding() == null) { + final ISVNOptions options = getOptions(); + if (options != null && options.getNativeCharset() != null) { + setEncoding(options.getNativeCharset()); + } else { + setEncoding("UTF-8"); + } + } + } + + private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException { + for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) { + String name = (String) changedPropNames.next(); + SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; + SVNPropertyValue newValue = diff.getSVNPropertyValue(name); + String headerFormat = null; + + if (originalValue == null) { + headerFormat = "Added: "; + } else if (newValue == null) { + headerFormat = "Deleted: "; + } else { + headerFormat = "Modified: "; + } + + try { + displayString(outputStream, (headerFormat + name)); + displayEOL(outputStream); + if (SVNProperty.MERGE_INFO.equals(name)) { + displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); + continue; + } + + byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding()); + byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding()); + + if (originalValueBytes == null) { + originalValueBytes = new byte[0]; + } else { + originalValueBytes = maybeAppendEOL(originalValueBytes); + } + + boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 && + (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] || + newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]); + + if (newValueBytes == null) { + newValueBytes = new byte[0]; + } else { + newValueBytes = maybeAppendEOL(newValueBytes); + } + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##"); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + QDiffGenerator generator = new QDiffUniGenerator(properties, ""); + Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes), + null, writer, generator); + writer.flush(); + if (!newValueHadEol) { + displayString(outputStream, "\\ No newline at end of property"); + displayEOL(outputStream); + } + } catch (IOException e) { + wrapException(e); + } + } + + } + + private byte[] maybeAppendEOL(byte[] buffer) { + if (buffer.length == 0) { + return buffer; + } + + byte lastByte = buffer[buffer.length - 1]; + if (lastByte == SVNProperty.EOL_CR_BYTES[0]) { + return buffer; + } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) { + final byte[] newBuffer = new byte[buffer.length + getEOL().length]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length); + return newBuffer; + } else { + return buffer; + } + } + + private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("a/" + copyFromPath, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("a/" + copyFromPath, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("b/" + path2, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath); + } + } + + private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "new file mode 100644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "deleted file mode 100644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "copy from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "copy to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "rename from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "rename to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException { + displayGitPath(outputStream, path1, "a/", false); + } + + private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException { + displayGitPath(outputStream, path2, "b/", false); + } + + private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "a/"; + if (operation == SvnDiffCallback.OperationKind.Added) { + path1 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true); + } + + private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "b/"; + if (operation == SvnDiffCallback.OperationKind.Deleted) { + path2 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true); + } + + private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException { +// if (!label && path1.length() == 0) { +// displayString(outputStream, "."); +// } else { + displayString(outputStream, pathPrefix); + displayString(outputStream, path1); +// } + } + + private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) { + String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor); + return getLabel(adjustedPath, revision); + } + + private String getAdjustedPath(String displayPath, String path1, String commonAncestor) { + String adjustedPath = getRelativePath(path1, commonAncestor); + + if (adjustedPath == null || adjustedPath.length() == 0) { + adjustedPath = displayPath; + } else if (adjustedPath.charAt(0) == '/') { + adjustedPath = displayPath + "\t(..." + adjustedPath + ")"; + } else { + adjustedPath = displayPath + "\t(.../" + adjustedPath + ")"; + } + return adjustedPath; + //TODO: respect relativeToDir + } + + protected String getLabel(String path, String revToken) { + if (useGitFormat){ + return path; + } + revToken = revToken == null ? WC_REVISION_LABEL : revToken; + return path + "\t" + revToken; + } + + protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException { + try { + if (deleted && !isDiffDeleted()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (deleted)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + if (added && !isDiffAdded()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (added)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + displayString(os, "Index: "); + displayString(os, path); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return false; + } catch (IOException e) { + wrapException(e); + } + return false; + } + + protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException { + try { + displayString(os, "--- "); + displayString(os, label1); + displayEOL(os); + displayString(os, "+++ "); + displayString(os, label2); + displayEOL(os); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException { + try { + displayEOL(outputStream); + displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path))); + displayEOL(outputStream); + displayString(outputStream, PROPERTIES_SEPARATOR); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) { + if (value == null) { + return null; + } + if (value.isString()) { + try { + return value.getString().getBytes(encoding); + } catch (UnsupportedEncodingException e) { + return value.getString().getBytes(); + } + } + return value.getBytes(); + } + + private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException { + Map oldMergeInfo = null; + Map newMergeInfo = null; + if (oldValue != null) { + oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); + } + if (newValue != null) { + newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); + } + + Map deleted = new TreeMap(); + Map added = new TreeMap(); + SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); + + for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); + displayString(outputStream, (" Reverse-merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + + for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); + displayString(outputStream, (" Merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + } + + private boolean useLocalFileSeparatorChar() { + return true; + } + + public boolean isDiffDeleted() { + return diffDeleted; + } + + public boolean isDiffAdded() { + return diffAdded; + } + + private void wrapException(IOException e) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e); + SVNErrorManager.error(errorMessage, e, SVNLogType.WC); + } + + private void displayString(OutputStream outputStream, String s) throws IOException { + outputStream.write(s.getBytes(HEADER_ENCODING)); + } + + private void displayEOL(OutputStream os) throws IOException { + os.write(getEOL()); + } + + public SVNDiffOptions getDiffOptions() { + if (diffOptions == null) { + diffOptions = new SVNDiffOptions(); + } + return diffOptions; + } + + public void setExternalDiffCommand(String externalDiffCommand) { + this.externalDiffCommand = externalDiffCommand; + } + + public void setRawDiffOptions(List rawDiffOptions) { + this.rawDiffOptions = rawDiffOptions; + } + + public void setDiffOptions(SVNDiffOptions diffOptions) { + this.diffOptions = diffOptions; + } + + public void setDiffDeleted(boolean diffDeleted) { + this.diffDeleted = diffDeleted; + } + + public void setDiffAdded(boolean diffAdded) { + this.diffAdded = diffAdded; + } + + public void setBasePath(File absoluteFile) { + setBaseTarget(SvnTarget.fromFile(absoluteFile)); + } + + public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) { + this.fallbackToAbsolutePath = fallbackToAbsolutePath; + } + + public void setOptions(ISVNOptions options) { + this.options = options; + } + + public ISVNOptions getOptions() { + return options; + } + + private class EmptyDetectionOutputStream extends OutputStream { + + private final OutputStream outputStream; + private boolean somethingWritten; + + public EmptyDetectionOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.somethingWritten = false; + } + + public boolean isSomethingWritten() { + return somethingWritten; + } + + @Override + public void write(int c) throws IOException { + somethingWritten = true; + outputStream.write(c); + } + + @Override + public void write(byte[] bytes) throws IOException { + somethingWritten = bytes.length > 0; + outputStream.write(bytes); + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + somethingWritten = length > 0; + outputStream.write(bytes, offset, length); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + } +} diff --git a/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest new file mode 100644 index 0000000000..d0cf749024 --- /dev/null +++ b/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest @@ -0,0 +1,1240 @@ +package sonia.scm.repository.spi; + +import de.regnis.q.sequence.line.diff.QDiffGenerator; +import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; +import de.regnis.q.sequence.line.diff.QDiffManager; +import de.regnis.q.sequence.line.diff.QDiffUniGenerator; +import org.tmatesoft.svn.core.SVNErrorCode; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNMergeRangeList; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.SVNPropertyValue; +import org.tmatesoft.svn.core.internal.util.SVNHashMap; +import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; +import org.tmatesoft.svn.core.internal.util.SVNPathUtil; +import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; +import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; +import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; +import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNDiffOptions; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import org.tmatesoft.svn.util.SVNLogType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public class SCMSvnDiffGenerator implements ISvnDiffGenerator { + + protected static final String WC_REVISION_LABEL = "(working copy)"; + protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; + protected static final String HEADER_SEPARATOR = "==================================================================="; + protected static final String HEADER_ENCODING = "UTF-8"; + + private SvnTarget originalTarget1; + private SvnTarget originalTarget2; + private SvnTarget baseTarget; + private SvnTarget relativeToTarget; + private SvnTarget repositoryRoot; + private String encoding; + private byte[] eol; + private boolean useGitFormat; + private boolean forcedBinaryDiff; + + private boolean diffDeleted; + private boolean diffAdded; + private List rawDiffOptions; + private boolean forceEmpty; + + private Set visitedPaths; + private String externalDiffCommand; + private SVNDiffOptions diffOptions; + private boolean fallbackToAbsolutePath; + private ISVNOptions options; + private boolean propertiesOnly; + private boolean ignoreProperties; + + private String getDisplayPath(SvnTarget target) { + String relativePath; + if (baseTarget == null) { + relativePath = null; + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = baseTarget.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) { + String relativePath; + if (repositoryRoot == null) + { + relativePath = null; + } + else + { + if (repositoryRoot.isFile() == target.isFile()) + { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = repositoryRoot.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + else + { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = new File("").getAbsolutePath(); + relativePath = getRelativePath(targetString, baseTargetString); + } + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativePath(String targetString, String baseTargetString) { + if (targetString != null) + { + targetString = targetString.replace(File.separatorChar, '/'); + } + if (baseTargetString != null) + { + baseTargetString = baseTargetString.replace(File.separatorChar, '/'); + } + + final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString); + if (pathAsChild != null) + { + return pathAsChild; + } + if (targetString.equals(baseTargetString)) + { + return ""; + } + return null; + } + + private String getChildPath(String path, String relativeToPath) { + if (relativeToTarget == null) { + return null; + } + + String relativePath = getRelativePath(path, relativeToPath); + if (relativePath == null) { + return path; + } + + if (relativePath.length() > 0) { + return relativePath; + } + + if (relativeToPath.equals(path)) { + return "."; + } + + return null; + } + + public SCMSvnDiffGenerator() { + this.originalTarget1 = null; + this.originalTarget2 = null; + this.visitedPaths = new HashSet(); + this.diffDeleted = true; + this.diffAdded = true; + } + + public void setBaseTarget(SvnTarget baseTarget) { + this.baseTarget = baseTarget; + } + + public void setUseGitFormat(boolean useGitFormat) { + this.useGitFormat = useGitFormat; + } + + public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) { + this.originalTarget1 = originalTarget1; + this.originalTarget2 = originalTarget2; + } + + public void setRelativeToTarget(SvnTarget relativeToTarget) { + this.relativeToTarget = relativeToTarget; + } + + public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) { + //anchors are not used + } + + public void setRepositoryRoot(SvnTarget repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public void setForceEmpty(boolean forceEmpty) { + this.forceEmpty = forceEmpty; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + public String getGlobalEncoding() { + ISVNOptions options = getOptions(); + + if (options != null && options instanceof DefaultSVNOptions) { + DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options; + return defaultOptions.getGlobalCharset(); + } + return null; + } + + public void setEOL(byte[] eol) { + this.eol = eol; + } + + public byte[] getEOL() { + return eol; + } + + public boolean isForcedBinaryDiff() { + return forcedBinaryDiff; + } + + public void setForcedBinaryDiff(boolean forcedBinaryDiff) { + this.forcedBinaryDiff = forcedBinaryDiff; + } + + public boolean isPropertiesOnly() { + return propertiesOnly; + } + + public void setPropertiesOnly(boolean propertiesOnly) { + this.propertiesOnly = propertiesOnly; + } + + public boolean isIgnoreProperties() { + return ignoreProperties; + } + + public void setIgnoreProperties(boolean ignoreProperties) { + this.ignoreProperties = ignoreProperties; + } + + public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isIgnoreProperties()) { + return; + } + if (dirWasAdded && !isDiffAdded()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (displayPath == null || displayPath.length() == 0) { + displayPath = "."; + } + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + boolean showDiffHeader = !visitedPaths.contains(displayPath); + if (showDiffHeader) { + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified); + visitedPaths.add(displayPath); + if (useGitFormat) { + displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + if (shouldStopDisplaying) { + return; + } + +// if (useGitFormat) { +// String copyFromPath = null; +// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified; +// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1); +// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2); +// displayGitDiffHeader(outputStream, operationKind, +// getRelativeToRootPath(target, originalTarget1), +// getRelativeToRootPath(target, originalTarget2), +// copyFromPath); +// } + + if (useGitFormat) { + displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null); + } else { + displayHeaderFields(outputStream, label1, label2); + } + } + + displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream); + + displayPropDiffValues(outputStream, propChanges, originalProps); + } + + private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''", + displayPath, relativeToPath); + SVNErrorManager.error(errorMessage, SVNLogType.CLIENT); + } + + private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + + try { + displayString(outputStream, "--- "); + displayFirstGitLabelPath(outputStream, path1, revision1, operation); + displayEOL(outputStream); + displayString(outputStream, "+++ "); + displaySecondGitLabelPath(outputStream, path2, revision2, operation); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private String adjustRelativeToReposRoot(String targetString) { + if (repositoryRoot != null) { + String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString(); + String relativePath = getRelativePath(targetString, repositoryRootString); + return relativePath == null ? "" : relativePath; + } + return targetString; + } + + private String computeLabel(String targetString, String originalTargetString) { + if (originalTargetString.length() == 0) { + return targetString; + } else if (originalTargetString.charAt(0) == '/') { + return targetString + "\t(..." + originalTargetString + ")"; + } else { + return targetString + "\t(.../" + originalTargetString + ")"; + } + } + + public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isPropertiesOnly()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean leftIsBinary = false; + boolean rightIsBinary = false; + + if (mimeType1 != null) { + leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1); + } + if (mimeType2 != null) { + rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2); + } + + if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + + displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + + return; + } + + final String diffCommand = getExternalDiffCommand(); + if (diffCommand != null) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2); + } else { + internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2); + } + } + + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { + displayCannotDisplayFileMarkedBinary(outputStream); + + if (leftIsBinary && !rightIsBinary) { + displayMimeType(outputStream, mimeType1); + } else if (!leftIsBinary && rightIsBinary) { + displayMimeType(outputStream, mimeType2); + } else if (leftIsBinary && rightIsBinary) { + if (mimeType1.equals(mimeType2)) { + displayMimeType(outputStream, mimeType1); + } else { + displayMimeTypes(outputStream, mimeType1, mimeType2); + } + } + } + + private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException { + String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath); + if (file2 == null && !isDiffDeleted()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + if (file1 == null && !isDiffAdded()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath); + + RandomAccessFile is1 = null; + RandomAccessFile is2 = null; + try { + is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); + is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + final String diffHeader; + if (forceEmpty || useGitFormat) { + displayString(outputStream, header); + diffHeader = headerFields; + + visitedPaths.add(displayPath); + } else { + diffHeader = header + headerFields; + } + QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader); + EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream); + QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator); + if (emptyDetectionOutputStream.isSomethingWritten()) { + visitedPaths.add(displayPath); + } + emptyDetectionOutputStream.flush(); + } catch (IOException e) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); + SVNErrorManager.error(err, e, SVNLogType.DEFAULT); + } finally { + SVNFileUtil.closeFile(is1); + SVNFileUtil.closeFile(is2); + } + } + + private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (useGitFormat) { + displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath); + } else { + displayHeaderFields(byteArrayOutputStream, label1, label2); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + boolean stopDisplaying = displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation); + if (useGitFormat) { + displayGitDiffHeader(byteArrayOutputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + copyFromPath); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException { + final List args = new ArrayList(); + args.add(diffCommand); + if (rawDiffOptions != null) { + args.addAll(rawDiffOptions); + } else { + Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection(); + args.addAll(svnDiffOptionsCollection); + args.add("-u"); + } + + if (label1 != null) { + args.add("-L"); + args.add(label1); + } + + if (label2 != null) { + args.add("-L"); + args.add(label2); + } + + boolean tmpFile1 = false; + boolean tmpFile2 = false; + if (file1 == null) { + file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile1 = true; + } + if (file2 == null) { + file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile2 = true; + } + + String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); + String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); + + args.add(file1Path); + args.add(file2Path); + try { + final Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + + SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true, + new ISVNReturnValueCallback() { + + public void handleReturnValue(int returnValue) throws SVNException { + if (returnValue != 0 && returnValue != 1) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, + "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)}); + SVNErrorManager.error(err, SVNLogType.DEFAULT); + } + } + + public void handleChar(char ch) throws SVNException { + try { + writer.write(ch); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } + } + + public boolean isHandleProgramOutput() { + return true; + } + }); + + writer.flush(); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } finally { + try { + if (tmpFile1) { + SVNFileUtil.deleteFile(file1); + } + if (tmpFile2) { + SVNFileUtil.deleteFile(file2); + } + } catch (SVNException e) { + // skip + } + } + } + + private String getExternalDiffCommand() { + return externalDiffCommand; + } + + private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = "); + displayString(outputStream, mimeType); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = ("); + displayString(outputStream, mimeType1); + displayString(outputStream, ", "); + displayString(outputStream, mimeType2); + displayString(outputStream, ")"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException { + try { + displayString(outputStream, "Cannot display: file marked as a binary type."); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void ensureEncodingAndEOLSet() { + if (getEOL() == null) { + setEOL(SVNProperty.EOL_LF_BYTES); + } + if (getEncoding() == null) { + final ISVNOptions options = getOptions(); + if (options != null && options.getNativeCharset() != null) { + setEncoding(options.getNativeCharset()); + } else { + setEncoding("UTF-8"); + } + } + } + + private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException { + for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) { + String name = (String) changedPropNames.next(); + SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; + SVNPropertyValue newValue = diff.getSVNPropertyValue(name); + String headerFormat = null; + + if (originalValue == null) { + headerFormat = "Added: "; + } else if (newValue == null) { + headerFormat = "Deleted: "; + } else { + headerFormat = "Modified: "; + } + + try { + displayString(outputStream, (headerFormat + name)); + displayEOL(outputStream); + if (SVNProperty.MERGE_INFO.equals(name)) { + displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); + continue; + } + + byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding()); + byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding()); + + if (originalValueBytes == null) { + originalValueBytes = new byte[0]; + } else { + originalValueBytes = maybeAppendEOL(originalValueBytes); + } + + boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 && + (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] || + newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]); + + if (newValueBytes == null) { + newValueBytes = new byte[0]; + } else { + newValueBytes = maybeAppendEOL(newValueBytes); + } + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##"); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + QDiffGenerator generator = new QDiffUniGenerator(properties, ""); + Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes), + null, writer, generator); + writer.flush(); + if (!newValueHadEol) { + displayString(outputStream, "\\ No newline at end of property"); + displayEOL(outputStream); + } + } catch (IOException e) { + wrapException(e); + } + } + + } + + private byte[] maybeAppendEOL(byte[] buffer) { + if (buffer.length == 0) { + return buffer; + } + + byte lastByte = buffer[buffer.length - 1]; + if (lastByte == SVNProperty.EOL_CR_BYTES[0]) { + return buffer; + } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) { + final byte[] newBuffer = new byte[buffer.length + getEOL().length]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length); + return newBuffer; + } else { + return buffer; + } + } + + private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("a/" + copyFromPath, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("a/" + copyFromPath, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("b/" + path2, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath); + } + } + + private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "new file mode 10644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "deleted file mode 10644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "copy from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "copy to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "rename from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "rename to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException { + displayGitPath(outputStream, path1, "a/", false); + } + + private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException { + displayGitPath(outputStream, path2, "b/", false); + } + + private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "a/"; + if (operation == SvnDiffCallback.OperationKind.Added) { + path1 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true); + } + + private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "b/"; + if (operation == SvnDiffCallback.OperationKind.Deleted) { + path2 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true); + } + + private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException { +// if (!label && path1.length() == 0) { +// displayString(outputStream, "."); +// } else { + displayString(outputStream, pathPrefix); + displayString(outputStream, path1); +// } + } + + private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) { + String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor); + return getLabel(adjustedPath, revision); + } + + private String getAdjustedPath(String displayPath, String path1, String commonAncestor) { + String adjustedPath = getRelativePath(path1, commonAncestor); + + if (adjustedPath == null || adjustedPath.length() == 0) { + adjustedPath = displayPath; + } else if (adjustedPath.charAt(0) == '/') { + adjustedPath = displayPath + "\t(..." + adjustedPath + ")"; + } else { + adjustedPath = displayPath + "\t(.../" + adjustedPath + ")"; + } + return adjustedPath; + //TODO: respect relativeToDir + } + + protected String getLabel(String path, String revToken) { + revToken = revToken == null ? WC_REVISION_LABEL : revToken; + return path + "\t" + revToken; + } + + protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException { + try { + if (deleted && !isDiffDeleted()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (deleted)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + if (added && !isDiffAdded()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (added)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + displayString(os, "Index: "); + displayString(os, path); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return false; + } catch (IOException e) { + wrapException(e); + } + return false; + } + + protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException { + try { + displayString(os, "--- "); + displayString(os, label1); + displayEOL(os); + displayString(os, "+++ "); + displayString(os, label2); + displayEOL(os); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException { + try { + displayEOL(outputStream); + displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path))); + displayEOL(outputStream); + displayString(outputStream, PROPERTIES_SEPARATOR); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) { + if (value == null) { + return null; + } + if (value.isString()) { + try { + return value.getString().getBytes(encoding); + } catch (UnsupportedEncodingException e) { + return value.getString().getBytes(); + } + } + return value.getBytes(); + } + + private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException { + Map oldMergeInfo = null; + Map newMergeInfo = null; + if (oldValue != null) { + oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); + } + if (newValue != null) { + newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); + } + + Map deleted = new TreeMap(); + Map added = new TreeMap(); + SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); + + for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); + displayString(outputStream, (" Reverse-merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + + for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); + displayString(outputStream, (" Merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + } + + private boolean useLocalFileSeparatorChar() { + return true; + } + + public boolean isDiffDeleted() { + return diffDeleted; + } + + public boolean isDiffAdded() { + return diffAdded; + } + + private void wrapException(IOException e) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e); + SVNErrorManager.error(errorMessage, e, SVNLogType.WC); + } + + private void displayString(OutputStream outputStream, String s) throws IOException { + outputStream.write(s.getBytes(HEADER_ENCODING)); + } + + private void displayEOL(OutputStream os) throws IOException { + os.write(getEOL()); + } + + public SVNDiffOptions getDiffOptions() { + if (diffOptions == null) { + diffOptions = new SVNDiffOptions(); + } + return diffOptions; + } + + public void setExternalDiffCommand(String externalDiffCommand) { + this.externalDiffCommand = externalDiffCommand; + } + + public void setRawDiffOptions(List rawDiffOptions) { + this.rawDiffOptions = rawDiffOptions; + } + + public void setDiffOptions(SVNDiffOptions diffOptions) { + this.diffOptions = diffOptions; + } + + public void setDiffDeleted(boolean diffDeleted) { + this.diffDeleted = diffDeleted; + } + + public void setDiffAdded(boolean diffAdded) { + this.diffAdded = diffAdded; + } + + public void setBasePath(File absoluteFile) { + setBaseTarget(SvnTarget.fromFile(absoluteFile)); + } + + public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) { + this.fallbackToAbsolutePath = fallbackToAbsolutePath; + } + + public void setOptions(ISVNOptions options) { + this.options = options; + } + + public ISVNOptions getOptions() { + return options; + } + + private class EmptyDetectionOutputStream extends OutputStream { + + private final OutputStream outputStream; + private boolean somethingWritten; + + public EmptyDetectionOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.somethingWritten = false; + } + + public boolean isSomethingWritten() { + return somethingWritten; + } + + @Override + public void write(int c) throws IOException { + somethingWritten = true; + outputStream.write(c); + } + + @Override + public void write(byte[] bytes) throws IOException { + somethingWritten = bytes.length > 0; + outputStream.write(bytes); + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + somethingWritten = length > 0; + outputStream.write(bytes, offset, length); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + } +} diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index 16a74549e5..db0173d1df 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -113,92 +113,29 @@ sonia.scm.maven smp-maven-plugin - 1.0.0-alpha-2 + 1.0.0-alpha-3 true - - true - - - - fix-descriptor - process-resources - - fix-descriptor - - - - append-dependencies - process-classes - - append-dependencies - - - - - com.github.sdorra - buildfrontend-maven-plugin - - - ${nodejs.version} - - - YARN - ${yarn.version} - - false - - - - - install - process-resources - - install - - - - build - compile - - run - - - - + + com.github.sdorra + buildfrontend-maven-plugin + + + ${nodejs.version} + + + YARN + ${yarn.version} + + false + + - - release - - - - - sonia.maven - web-compressor - 1.4 - - - compile - - compress-directory - - - - - true - ${project.build.directory}/classes - - - - - - - doc diff --git a/scm-plugins/scm-git-plugin/.flowconfig b/scm-plugins/scm-git-plugin/.flowconfig index 7ede008602..b05e157358 100644 --- a/scm-plugins/scm-git-plugin/.flowconfig +++ b/scm-plugins/scm-git-plugin/.flowconfig @@ -4,5 +4,6 @@ [include] [libs] +./node_modules/@scm-manager/ui-components/flow-typed [options] diff --git a/scm-plugins/scm-git-plugin/flow-typed/npm/classnames_v2.x.x.js b/scm-plugins/scm-git-plugin/flow-typed/npm/classnames_v2.x.x.js deleted file mode 100644 index 2307243eeb..0000000000 --- a/scm-plugins/scm-git-plugin/flow-typed/npm/classnames_v2.x.x.js +++ /dev/null @@ -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">; -} diff --git a/scm-plugins/scm-git-plugin/flow-typed/npm/jest_v23.x.x.js b/scm-plugins/scm-git-plugin/flow-typed/npm/jest_v23.x.x.js deleted file mode 100644 index 23b66b07e5..0000000000 --- a/scm-plugins/scm-git-plugin/flow-typed/npm/jest_v23.x.x.js +++ /dev/null @@ -1,1108 +0,0 @@ -// flow-typed signature: f5a484315a3dea13d273645306e4076a -// flow-typed version: 7c5d14b3d4/jest_v23.x.x/flow_>=v0.39.x - -type JestMockFn, TReturn> = { - (...args: TArguments): TReturn, - /** - * An object for introspecting mock calls - */ - mock: { - /** - * An array that represents all calls that have been made into this mock - * function. Each call is represented by an array of arguments that were - * passed during the call. - */ - calls: Array, - /** - * An array that contains all the object instances that have been - * instantiated from this mock function. - */ - instances: Array - }, - /** - * Resets all information stored in the mockFn.mock.calls and - * mockFn.mock.instances arrays. Often this is useful when you want to clean - * up a mock's usage data between two assertions. - */ - mockClear(): void, - /** - * Resets all information stored in the mock. This is useful when you want to - * completely restore a mock back to its initial state. - */ - mockReset(): void, - /** - * Removes the mock and restores the initial implementation. This is useful - * when you want to mock functions in certain test cases and restore the - * original implementation in others. Beware that mockFn.mockRestore only - * works when mock was created with jest.spyOn. Thus you have to take care of - * restoration yourself when manually assigning jest.fn(). - */ - mockRestore(): void, - /** - * Accepts a function that should be used as the implementation of the mock. - * The mock itself will still record all calls that go into and instances - * that come from itself -- the only difference is that the implementation - * will also be executed when the mock is called. - */ - mockImplementation( - fn: (...args: TArguments) => TReturn - ): JestMockFn, - /** - * Accepts a function that will be used as an implementation of the mock for - * one call to the mocked function. Can be chained so that multiple function - * calls produce different results. - */ - mockImplementationOnce( - fn: (...args: TArguments) => TReturn - ): JestMockFn, - /** - * Accepts a string to use in test result output in place of "jest.fn()" to - * indicate which mock function is being referenced. - */ - mockName(name: string): JestMockFn, - /** - * Just a simple sugar function for returning `this` - */ - mockReturnThis(): void, - /** - * Accepts a value that will be returned whenever the mock function is called. - */ - mockReturnValue(value: TReturn): JestMockFn, - /** - * Sugar for only returning a value once inside your mock - */ - mockReturnValueOnce(value: TReturn): JestMockFn, - /** - * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) - */ - mockResolvedValue(value: TReturn): JestMockFn>, - /** - * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) - */ - mockResolvedValueOnce(value: TReturn): JestMockFn>, - /** - * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) - */ - mockRejectedValue(value: TReturn): JestMockFn>, - /** - * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) - */ - mockRejectedValueOnce(value: TReturn): JestMockFn> -}; - -type JestAsymmetricEqualityType = { - /** - * A custom Jasmine equality tester - */ - asymmetricMatch(value: mixed): boolean -}; - -type JestCallsType = { - allArgs(): mixed, - all(): mixed, - any(): boolean, - count(): number, - first(): mixed, - mostRecent(): mixed, - reset(): void -}; - -type JestClockType = { - install(): void, - mockDate(date: Date): void, - tick(milliseconds?: number): void, - uninstall(): void -}; - -type JestMatcherResult = { - message?: string | (() => string), - pass: boolean -}; - -type JestMatcher = (actual: any, expected: any) => JestMatcherResult; - -type JestPromiseType = { - /** - * Use rejects to unwrap the reason of a rejected promise so any other - * matcher can be chained. If the promise is fulfilled the assertion fails. - */ - rejects: JestExpectType, - /** - * Use resolves to unwrap the value of a fulfilled promise so any other - * matcher can be chained. If the promise is rejected the assertion fails. - */ - resolves: JestExpectType -}; - -/** - * Jest allows functions and classes to be used as test names in test() and - * describe() - */ -type JestTestName = string | Function; - -/** - * Plugin: jest-styled-components - */ - -type JestStyledComponentsMatcherValue = - | string - | JestAsymmetricEqualityType - | RegExp - | typeof undefined; - -type JestStyledComponentsMatcherOptions = { - media?: string; - modifier?: string; - supports?: string; -} - -type JestStyledComponentsMatchersType = { - toHaveStyleRule( - property: string, - value: JestStyledComponentsMatcherValue, - options?: JestStyledComponentsMatcherOptions - ): void, -}; - -/** - * Plugin: jest-enzyme - */ -type EnzymeMatchersType = { - toBeChecked(): void, - toBeDisabled(): void, - toBeEmpty(): void, - toBeEmptyRender(): void, - toBePresent(): void, - toContainReact(element: React$Element): void, - toExist(): void, - toHaveClassName(className: string): void, - toHaveHTML(html: string): void, - toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void), - toHaveRef(refName: string): void, - toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void), - toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void), - toHaveTagName(tagName: string): void, - toHaveText(text: string): void, - toIncludeText(text: string): void, - toHaveValue(value: any): void, - toMatchElement(element: React$Element): void, - toMatchSelector(selector: string): void -}; - -// DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers -type DomTestingLibraryType = { - toBeInTheDOM(): void, - toHaveTextContent(content: string): void, - toHaveAttribute(name: string, expectedValue?: string): void -}; - -// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers -type JestJQueryMatchersType = { - toExist(): void, - toHaveLength(len: number): void, - toHaveId(id: string): void, - toHaveClass(className: string): void, - toHaveTag(tag: string): void, - toHaveAttr(key: string, val?: any): void, - toHaveProp(key: string, val?: any): void, - toHaveText(text: string | RegExp): void, - toHaveData(key: string, val?: any): void, - toHaveValue(val: any): void, - toHaveCss(css: {[key: string]: any}): void, - toBeChecked(): void, - toBeDisabled(): void, - toBeEmpty(): void, - toBeHidden(): void, - toBeSelected(): void, - toBeVisible(): void, - toBeFocused(): void, - toBeInDom(): void, - toBeMatchedBy(sel: string): void, - toHaveDescendant(sel: string): void, - toHaveDescendantWithText(sel: string, text: string | RegExp): void -}; - - -// Jest Extended Matchers: https://github.com/jest-community/jest-extended -type JestExtendedMatchersType = { - /** - * Note: Currently unimplemented - * Passing assertion - * - * @param {String} message - */ - // pass(message: string): void; - - /** - * Note: Currently unimplemented - * Failing assertion - * - * @param {String} message - */ - // fail(message: string): void; - - /** - * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. - */ - toBeEmpty(): void; - - /** - * Use .toBeOneOf when checking if a value is a member of a given Array. - * @param {Array.<*>} members - */ - toBeOneOf(members: any[]): void; - - /** - * Use `.toBeNil` when checking a value is `null` or `undefined`. - */ - toBeNil(): void; - - /** - * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. - * @param {Function} predicate - */ - toSatisfy(predicate: (n: any) => boolean): void; - - /** - * Use `.toBeArray` when checking if a value is an `Array`. - */ - toBeArray(): void; - - /** - * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. - * @param {Number} x - */ - toBeArrayOfSize(x: number): void; - - /** - * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. - * @param {Array.<*>} members - */ - toIncludeAllMembers(members: any[]): void; - - /** - * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. - * @param {Array.<*>} members - */ - toIncludeAnyMembers(members: any[]): void; - - /** - * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. - * @param {Function} predicate - */ - toSatisfyAll(predicate: (n: any) => boolean): void; - - /** - * Use `.toBeBoolean` when checking if a value is a `Boolean`. - */ - toBeBoolean(): void; - - /** - * Use `.toBeTrue` when checking a value is equal (===) to `true`. - */ - toBeTrue(): void; - - /** - * Use `.toBeFalse` when checking a value is equal (===) to `false`. - */ - toBeFalse(): void; - - /** - * Use .toBeDate when checking if a value is a Date. - */ - toBeDate(): void; - - /** - * Use `.toBeFunction` when checking if a value is a `Function`. - */ - toBeFunction(): void; - - /** - * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. - * - * Note: Required Jest version >22 - * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same - * - * @param {Mock} mock - */ - toHaveBeenCalledBefore(mock: JestMockFn): void; - - /** - * Use `.toBeNumber` when checking if a value is a `Number`. - */ - toBeNumber(): void; - - /** - * Use `.toBeNaN` when checking a value is `NaN`. - */ - toBeNaN(): void; - - /** - * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. - */ - toBeFinite(): void; - - /** - * Use `.toBePositive` when checking if a value is a positive `Number`. - */ - toBePositive(): void; - - /** - * Use `.toBeNegative` when checking if a value is a negative `Number`. - */ - toBeNegative(): void; - - /** - * Use `.toBeEven` when checking if a value is an even `Number`. - */ - toBeEven(): void; - - /** - * Use `.toBeOdd` when checking if a value is an odd `Number`. - */ - toBeOdd(): void; - - /** - * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). - * - * @param {Number} start - * @param {Number} end - */ - toBeWithin(start: number, end: number): void; - - /** - * Use `.toBeObject` when checking if a value is an `Object`. - */ - toBeObject(): void; - - /** - * Use `.toContainKey` when checking if an object contains the provided key. - * - * @param {String} key - */ - toContainKey(key: string): void; - - /** - * Use `.toContainKeys` when checking if an object has all of the provided keys. - * - * @param {Array.} keys - */ - toContainKeys(keys: string[]): void; - - /** - * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. - * - * @param {Array.} keys - */ - toContainAllKeys(keys: string[]): void; - - /** - * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. - * - * @param {Array.} keys - */ - toContainAnyKeys(keys: string[]): void; - - /** - * Use `.toContainValue` when checking if an object contains the provided value. - * - * @param {*} value - */ - toContainValue(value: any): void; - - /** - * Use `.toContainValues` when checking if an object contains all of the provided values. - * - * @param {Array.<*>} values - */ - toContainValues(values: any[]): void; - - /** - * Use `.toContainAllValues` when checking if an object only contains all of the provided values. - * - * @param {Array.<*>} values - */ - toContainAllValues(values: any[]): void; - - /** - * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. - * - * @param {Array.<*>} values - */ - toContainAnyValues(values: any[]): void; - - /** - * Use `.toContainEntry` when checking if an object contains the provided entry. - * - * @param {Array.} entry - */ - toContainEntry(entry: [string, string]): void; - - /** - * Use `.toContainEntries` when checking if an object contains all of the provided entries. - * - * @param {Array.>} entries - */ - toContainEntries(entries: [string, string][]): void; - - /** - * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. - * - * @param {Array.>} entries - */ - toContainAllEntries(entries: [string, string][]): void; - - /** - * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. - * - * @param {Array.>} entries - */ - toContainAnyEntries(entries: [string, string][]): void; - - /** - * Use `.toBeExtensible` when checking if an object is extensible. - */ - toBeExtensible(): void; - - /** - * Use `.toBeFrozen` when checking if an object is frozen. - */ - toBeFrozen(): void; - - /** - * Use `.toBeSealed` when checking if an object is sealed. - */ - toBeSealed(): void; - - /** - * Use `.toBeString` when checking if a value is a `String`. - */ - toBeString(): void; - - /** - * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. - * - * @param {String} string - */ - toEqualCaseInsensitive(string: string): void; - - /** - * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. - * - * @param {String} prefix - */ - toStartWith(prefix: string): void; - - /** - * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. - * - * @param {String} suffix - */ - toEndWith(suffix: string): void; - - /** - * Use `.toInclude` when checking if a `String` includes the given `String` substring. - * - * @param {String} substring - */ - toInclude(substring: string): void; - - /** - * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. - * - * @param {String} substring - * @param {Number} times - */ - toIncludeRepeated(substring: string, times: number): void; - - /** - * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. - * - * @param {Array.} substring - */ - toIncludeMultiple(substring: string[]): void; -}; - -interface JestExpectType { - not: - & JestExpectType - & EnzymeMatchersType - & DomTestingLibraryType - & JestJQueryMatchersType - & JestStyledComponentsMatchersType - & JestExtendedMatchersType, - /** - * If you have a mock function, you can use .lastCalledWith to test what - * arguments it was last called with. - */ - lastCalledWith(...args: Array): void, - /** - * toBe just checks that a value is what you expect. It uses === to check - * strict equality. - */ - toBe(value: any): void, - /** - * Use .toBeCalledWith to ensure that a mock function was called with - * specific arguments. - */ - toBeCalledWith(...args: Array): void, - /** - * Using exact equality with floating point numbers is a bad idea. Rounding - * means that intuitive things fail. - */ - toBeCloseTo(num: number, delta: any): void, - /** - * Use .toBeDefined to check that a variable is not undefined. - */ - toBeDefined(): void, - /** - * Use .toBeFalsy when you don't care what a value is, you just want to - * ensure a value is false in a boolean context. - */ - toBeFalsy(): void, - /** - * To compare floating point numbers, you can use toBeGreaterThan. - */ - toBeGreaterThan(number: number): void, - /** - * To compare floating point numbers, you can use toBeGreaterThanOrEqual. - */ - toBeGreaterThanOrEqual(number: number): void, - /** - * To compare floating point numbers, you can use toBeLessThan. - */ - toBeLessThan(number: number): void, - /** - * To compare floating point numbers, you can use toBeLessThanOrEqual. - */ - toBeLessThanOrEqual(number: number): void, - /** - * Use .toBeInstanceOf(Class) to check that an object is an instance of a - * class. - */ - toBeInstanceOf(cls: Class<*>): void, - /** - * .toBeNull() is the same as .toBe(null) but the error messages are a bit - * nicer. - */ - toBeNull(): void, - /** - * Use .toBeTruthy when you don't care what a value is, you just want to - * ensure a value is true in a boolean context. - */ - toBeTruthy(): void, - /** - * Use .toBeUndefined to check that a variable is undefined. - */ - toBeUndefined(): void, - /** - * Use .toContain when you want to check that an item is in a list. For - * testing the items in the list, this uses ===, a strict equality check. - */ - toContain(item: any): void, - /** - * Use .toContainEqual when you want to check that an item is in a list. For - * testing the items in the list, this matcher recursively checks the - * equality of all fields, rather than checking for object identity. - */ - toContainEqual(item: any): void, - /** - * Use .toEqual when you want to check that two objects have the same value. - * This matcher recursively checks the equality of all fields, rather than - * checking for object identity. - */ - toEqual(value: any): void, - /** - * Use .toHaveBeenCalled to ensure that a mock function got called. - */ - toHaveBeenCalled(): void, - toBeCalled(): void; - /** - * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact - * number of times. - */ - toHaveBeenCalledTimes(number: number): void, - toBeCalledTimes(number: number): void; - /** - * - */ - toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void; - nthCalledWith(nthCall: number, ...args: Array): void; - /** - * - */ - toHaveReturned(): void; - toReturn(): void; - /** - * - */ - toHaveReturnedTimes(number: number): void; - toReturnTimes(number: number): void; - /** - * - */ - toHaveReturnedWith(value: any): void; - toReturnWith(value: any): void; - /** - * - */ - toHaveLastReturnedWith(value: any): void; - lastReturnedWith(value: any): void; - /** - * - */ - toHaveNthReturnedWith(nthCall: number, value: any): void; - nthReturnedWith(nthCall: number, value: any): void; - /** - * Use .toHaveBeenCalledWith to ensure that a mock function was called with - * specific arguments. - */ - toHaveBeenCalledWith(...args: Array): void, - toBeCalledWith(...args: Array): void, - /** - * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called - * with specific arguments. - */ - toHaveBeenLastCalledWith(...args: Array): void, - lastCalledWith(...args: Array): void, - /** - * Check that an object has a .length property and it is set to a certain - * numeric value. - */ - toHaveLength(number: number): void, - /** - * - */ - toHaveProperty(propPath: string, value?: any): void, - /** - * Use .toMatch to check that a string matches a regular expression or string. - */ - toMatch(regexpOrString: RegExp | string): void, - /** - * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. - */ - toMatchObject(object: Object | Array): void, - /** - * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. - */ - toStrictEqual(value: any): void, - /** - * This ensures that an Object matches the most recent snapshot. - */ - toMatchSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, name?: string): void, - /** - * This ensures that an Object matches the most recent snapshot. - */ - toMatchSnapshot(name: string): void, - - toMatchInlineSnapshot(snapshot?: string): void, - toMatchInlineSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, snapshot?: string): void, - /** - * Use .toThrow to test that a function throws when it is called. - * If you want to test that a specific error gets thrown, you can provide an - * argument to toThrow. The argument can be a string for the error message, - * a class for the error, or a regex that should match the error. - * - * Alias: .toThrowError - */ - toThrow(message?: string | Error | Class | RegExp): void, - toThrowError(message?: string | Error | Class | RegExp): void, - /** - * Use .toThrowErrorMatchingSnapshot to test that a function throws a error - * matching the most recent snapshot when it is called. - */ - toThrowErrorMatchingSnapshot(): void, - toThrowErrorMatchingInlineSnapshot(snapshot?: string): void, -} - -type JestObjectType = { - /** - * Disables automatic mocking in the module loader. - * - * After this method is called, all `require()`s will return the real - * versions of each module (rather than a mocked version). - */ - disableAutomock(): JestObjectType, - /** - * An un-hoisted version of disableAutomock - */ - autoMockOff(): JestObjectType, - /** - * Enables automatic mocking in the module loader. - */ - enableAutomock(): JestObjectType, - /** - * An un-hoisted version of enableAutomock - */ - autoMockOn(): JestObjectType, - /** - * Clears the mock.calls and mock.instances properties of all mocks. - * Equivalent to calling .mockClear() on every mocked function. - */ - clearAllMocks(): JestObjectType, - /** - * Resets the state of all mocks. Equivalent to calling .mockReset() on every - * mocked function. - */ - resetAllMocks(): JestObjectType, - /** - * Restores all mocks back to their original value. - */ - restoreAllMocks(): JestObjectType, - /** - * Removes any pending timers from the timer system. - */ - clearAllTimers(): void, - /** - * The same as `mock` but not moved to the top of the expectation by - * babel-jest. - */ - doMock(moduleName: string, moduleFactory?: any): JestObjectType, - /** - * The same as `unmock` but not moved to the top of the expectation by - * babel-jest. - */ - dontMock(moduleName: string): JestObjectType, - /** - * Returns a new, unused mock function. Optionally takes a mock - * implementation. - */ - fn, TReturn>( - implementation?: (...args: TArguments) => TReturn - ): JestMockFn, - /** - * Determines if the given function is a mocked function. - */ - isMockFunction(fn: Function): boolean, - /** - * Given the name of a module, use the automatic mocking system to generate a - * mocked version of the module for you. - */ - genMockFromModule(moduleName: string): any, - /** - * Mocks a module with an auto-mocked version when it is being required. - * - * The second argument can be used to specify an explicit module factory that - * is being run instead of using Jest's automocking feature. - * - * The third argument can be used to create virtual mocks -- mocks of modules - * that don't exist anywhere in the system. - */ - mock( - moduleName: string, - moduleFactory?: any, - options?: Object - ): JestObjectType, - /** - * Returns the actual module instead of a mock, bypassing all checks on - * whether the module should receive a mock implementation or not. - */ - requireActual(moduleName: string): any, - /** - * Returns a mock module instead of the actual module, bypassing all checks - * on whether the module should be required normally or not. - */ - requireMock(moduleName: string): any, - /** - * Resets the module registry - the cache of all required modules. This is - * useful to isolate modules where local state might conflict between tests. - */ - resetModules(): JestObjectType, - /** - * Exhausts the micro-task queue (usually interfaced in node via - * process.nextTick). - */ - runAllTicks(): void, - /** - * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), - * setInterval(), and setImmediate()). - */ - runAllTimers(): void, - /** - * Exhausts all tasks queued by setImmediate(). - */ - runAllImmediates(): void, - /** - * Executes only the macro task queue (i.e. all tasks queued by setTimeout() - * or setInterval() and setImmediate()). - */ - advanceTimersByTime(msToRun: number): void, - /** - * Executes only the macro task queue (i.e. all tasks queued by setTimeout() - * or setInterval() and setImmediate()). - * - * Renamed to `advanceTimersByTime`. - */ - runTimersToTime(msToRun: number): void, - /** - * Executes only the macro-tasks that are currently pending (i.e., only the - * tasks that have been queued by setTimeout() or setInterval() up to this - * point) - */ - runOnlyPendingTimers(): void, - /** - * Explicitly supplies the mock object that the module system should return - * for the specified module. Note: It is recommended to use jest.mock() - * instead. - */ - setMock(moduleName: string, moduleExports: any): JestObjectType, - /** - * Indicates that the module system should never return a mocked version of - * the specified module from require() (e.g. that it should always return the - * real module). - */ - unmock(moduleName: string): JestObjectType, - /** - * Instructs Jest to use fake versions of the standard timer functions - * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, - * setImmediate and clearImmediate). - */ - useFakeTimers(): JestObjectType, - /** - * Instructs Jest to use the real versions of the standard timer functions. - */ - useRealTimers(): JestObjectType, - /** - * Creates a mock function similar to jest.fn but also tracks calls to - * object[methodName]. - */ - spyOn(object: Object, methodName: string, accessType?: "get" | "set"): JestMockFn, - /** - * Set the default timeout interval for tests and before/after hooks in milliseconds. - * Note: The default timeout interval is 5 seconds if this method is not called. - */ - setTimeout(timeout: number): JestObjectType -}; - -type JestSpyType = { - calls: JestCallsType -}; - -/** Runs this function after every test inside this context */ -declare function afterEach( - fn: (done: () => void) => ?Promise, - timeout?: number -): void; -/** Runs this function before every test inside this context */ -declare function beforeEach( - fn: (done: () => void) => ?Promise, - timeout?: number -): void; -/** Runs this function after all tests have finished inside this context */ -declare function afterAll( - fn: (done: () => void) => ?Promise, - timeout?: number -): void; -/** Runs this function before any tests have started inside this context */ -declare function beforeAll( - fn: (done: () => void) => ?Promise, - timeout?: number -): void; - -/** A context for grouping tests together */ -declare var describe: { - /** - * Creates a block that groups together several related tests in one "test suite" - */ - (name: JestTestName, fn: () => void): void, - - /** - * Only run this describe block - */ - only(name: JestTestName, fn: () => void): void, - - /** - * Skip running this describe block - */ - skip(name: JestTestName, fn: () => void): void -}; - -/** An individual test unit */ -declare var it: { - /** - * An individual test unit - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - ( - name: JestTestName, - fn?: (done: () => void) => ?Promise, - timeout?: number - ): void, - /** - * each runs this test against array of argument arrays per each run - * - * @param {table} table of Test - */ - each( - table: Array> - ): ( - name: JestTestName, - fn?: (...args: Array) => ?Promise - ) => void, - /** - * Only run this test - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - only( - name: JestTestName, - fn?: (done: () => void) => ?Promise, - timeout?: number - ): { - each( - table: Array> - ): ( - name: JestTestName, - fn?: (...args: Array) => ?Promise - ) => void, - }, - /** - * Skip running this test - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - skip( - name: JestTestName, - fn?: (done: () => void) => ?Promise, - timeout?: number - ): void, - /** - * Run the test concurrently - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - concurrent( - name: JestTestName, - fn?: (done: () => void) => ?Promise, - timeout?: number - ): void -}; -declare function fit( - name: JestTestName, - fn: (done: () => void) => ?Promise, - timeout?: number -): void; -/** An individual test unit */ -declare var test: typeof it; -/** A disabled group of tests */ -declare var xdescribe: typeof describe; -/** A focused group of tests */ -declare var fdescribe: typeof describe; -/** A disabled individual test */ -declare var xit: typeof it; -/** A disabled individual test */ -declare var xtest: typeof it; - -type JestPrettyFormatColors = { - comment: { close: string, open: string }, - content: { close: string, open: string }, - prop: { close: string, open: string }, - tag: { close: string, open: string }, - value: { close: string, open: string }, -}; - -type JestPrettyFormatIndent = string => string; -type JestPrettyFormatRefs = Array; -type JestPrettyFormatPrint = any => string; -type JestPrettyFormatStringOrNull = string | null; - -type JestPrettyFormatOptions = {| - callToJSON: boolean, - edgeSpacing: string, - escapeRegex: boolean, - highlight: boolean, - indent: number, - maxDepth: number, - min: boolean, - plugins: JestPrettyFormatPlugins, - printFunctionName: boolean, - spacing: string, - theme: {| - comment: string, - content: string, - prop: string, - tag: string, - value: string, - |}, -|}; - -type JestPrettyFormatPlugin = { - print: ( - val: any, - serialize: JestPrettyFormatPrint, - indent: JestPrettyFormatIndent, - opts: JestPrettyFormatOptions, - colors: JestPrettyFormatColors, - ) => string, - test: any => boolean, -}; - -type JestPrettyFormatPlugins = Array; - -/** The expect function is used every time you want to test a value */ -declare var expect: { - /** The object that you want to make assertions against */ - (value: any): - & JestExpectType - & JestPromiseType - & EnzymeMatchersType - & DomTestingLibraryType - & JestJQueryMatchersType - & JestStyledComponentsMatchersType - & JestExtendedMatchersType, - - /** Add additional Jasmine matchers to Jest's roster */ - extend(matchers: { [name: string]: JestMatcher }): void, - /** Add a module that formats application-specific data structures. */ - addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, - assertions(expectedAssertions: number): void, - hasAssertions(): void, - any(value: mixed): JestAsymmetricEqualityType, - anything(): any, - arrayContaining(value: Array): Array, - objectContaining(value: Object): Object, - /** Matches any received string that contains the exact expected string. */ - stringContaining(value: string): string, - stringMatching(value: string | RegExp): string, - not: { - arrayContaining: (value: $ReadOnlyArray) => Array, - objectContaining: (value: {}) => Object, - stringContaining: (value: string) => string, - stringMatching: (value: string | RegExp) => string, - }, -}; - -// TODO handle return type -// http://jasmine.github.io/2.4/introduction.html#section-Spies -declare function spyOn(value: mixed, method: string): Object; - -/** Holds all functions related to manipulating test runner */ -declare var jest: JestObjectType; - -/** - * The global Jasmine object, this is generally not exposed as the public API, - * using features inside here could break in later versions of Jest. - */ -declare var jasmine: { - DEFAULT_TIMEOUT_INTERVAL: number, - any(value: mixed): JestAsymmetricEqualityType, - anything(): any, - arrayContaining(value: Array): Array, - clock(): JestClockType, - createSpy(name: string): JestSpyType, - createSpyObj( - baseName: string, - methodNames: Array - ): { [methodName: string]: JestSpyType }, - objectContaining(value: Object): Object, - stringMatching(value: string): string -}; diff --git a/scm-plugins/scm-git-plugin/flow-typed/npm/moment_v2.3.x.js b/scm-plugins/scm-git-plugin/flow-typed/npm/moment_v2.3.x.js deleted file mode 100644 index c2786e87fd..0000000000 --- a/scm-plugins/scm-git-plugin/flow-typed/npm/moment_v2.3.x.js +++ /dev/null @@ -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, - strict?: boolean - ): moment$Moment, - static ( - string?: string, - format?: string | Array, - locale?: string, - strict?: boolean - ): moment$Moment, - static ( - initDate: ?Object | number | Date | Array | moment$Moment | string - ): moment$Moment, - static unix(seconds: number): moment$Moment, - static utc(): moment$Moment, - static utc(number: number | Array): moment$Moment, - static utc( - str: string, - str2?: string | Array, - 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 - ): 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, - static max(dates: Array): moment$Moment, - static min(...dates: Array): moment$Moment, - static min(dates: Array): 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, - removePrefix?: boolean - ): string, - toNow(removePrefix?: boolean): string, - to( - value: moment$Moment | string | number | Date | Array, - removePrefix?: boolean - ): string, - calendar(refTime?: any, formats?: moment$CalendarFormats): string, - diff( - date: moment$Moment | string | number | Date | Array, - format?: string, - floating?: boolean - ): number, - valueOf(): number, - unix(): number, - daysInMonth(): number, - toDate(): Date, - toArray(): Array, - toJSON(): string, - toISOString( - keepOffset?: boolean - ): string, - toObject(): moment$MomentObject, - isBefore( - date?: moment$Moment | string | number | Date | Array, - units?: ?string - ): boolean, - isSame( - date?: moment$Moment | string | number | Date | Array, - units?: ?string - ): boolean, - isAfter( - date?: moment$Moment | string | number | Date | Array, - units?: ?string - ): boolean, - isSameOrBefore( - date?: moment$Moment | string | number | Date | Array, - units?: ?string - ): boolean, - isSameOrAfter( - date?: moment$Moment | string | number | Date | Array, - units?: ?string - ): boolean, - isBetween( - fromDate: moment$Moment | string | number | Date | Array, - toDate?: ?moment$Moment | string | number | Date | Array, - 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, - locale(locale: string, customization?: Object | null): moment$Moment, - locale(): string, - static months(): Array, - static monthsShort(): Array, - static weekdays(): Array, - static weekdaysShort(): Array, - static weekdaysMin(): Array, - 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; -} diff --git a/scm-plugins/scm-git-plugin/flow-typed/npm/react-jss_vx.x.x.js b/scm-plugins/scm-git-plugin/flow-typed/npm/react-jss_vx.x.x.js deleted file mode 100644 index cf8abae155..0000000000 --- a/scm-plugins/scm-git-plugin/flow-typed/npm/react-jss_vx.x.x.js +++ /dev/null @@ -1,137 +0,0 @@ -// flow-typed signature: ba35d02d668b0d0a3e04a63a6847974e -// flow-typed version: <>/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'>; -} diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index 8f0ab90d34..6377574498 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -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" } } diff --git a/scm-plugins/scm-git-plugin/pom.xml b/scm-plugins/scm-git-plugin/pom.xml index 11e2a40bd0..a838e2f146 100644 --- a/scm-plugins/scm-git-plugin/pom.xml +++ b/scm-plugins/scm-git-plugin/pom.xml @@ -43,11 +43,24 @@ - - + + sonia.scm.maven + smp-maven-plugin + true + + true + + @scm-manager/ui-types + @scm-manager/ui-components + + + + + + org.apache.maven.plugins maven-jar-plugin @@ -61,33 +74,6 @@ - - com.github.sdorra - buildfrontend-maven-plugin - - - link-ui-types - process-sources - - install-link - - - @scm-manager/ui-types - - - - link-ui-components - process-sources - - install-link - - - @scm-manager/ui-components - - - - - diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java deleted file mode 100644 index bace7d0b73..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java +++ /dev/null @@ -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; -} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java new file mode 100644 index 0000000000..553f0f5a00 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java @@ -0,0 +1,25 @@ +package sonia.scm.repository; + +import java.util.function.Consumer; + +public class CloseableWrapper implements AutoCloseable { + + private final C wrapped; + private final Consumer cleanup; + + public CloseableWrapper(C wrapped, Consumer cleanup) { + this.wrapped = wrapped; + this.cleanup = cleanup; + } + + public C get() { return wrapped; } + + @Override + public void close() { + try { + cleanup.accept(wrapped); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index f5c1857a89..d190eae567 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -90,7 +90,9 @@ public class GitRepositoryHandler private static final Object LOCK = new Object(); 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; + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 13340a20e7..f490b7ea4f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -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 -------------------------------------------------------------- /** diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java new file mode 100644 index 0000000000..f93713a221 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java @@ -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); +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java index e90a1c11ed..3cf72166ea 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java @@ -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; } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java index 75050c26ea..e4e37d6fed 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java index f50f245963..2ad38648da 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java @@ -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 blameLines = new ArrayList(); @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java index 0cc47100de..4922752b6f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index ab1b0ae420..9186572858 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -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 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 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 subRepositories = subrepositoryCache.get(revId); if (subRepositories == null) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index 4b05098d03..7477e0aee3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -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)); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java index 2175846d5a..b0dd8f1fd6 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java @@ -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; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 83977ef290..2d56c8e786 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -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); + } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java index 4e9261f517..2ea25126cf 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java @@ -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 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(); + } + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java new file mode 100644 index 0000000000..5e9eac5230 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -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; + } + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java index 2b35ba74f6..5040069c12 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java @@ -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)); } } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java index 2a1f805cf6..6bdd8b793a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java @@ -77,7 +77,6 @@ public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand * @return * * @throws IOException - * @throws RepositoryException */ @Override public ChangesetPagingResult getOutgoingChangesets( diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java index a7b341ff5d..7a829355bf 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java index 193d68d7a5..3963366aa7 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java @@ -85,7 +85,6 @@ public class GitPushCommand extends AbstractGitPushOrPullCommand * @return * * @throws IOException - * @throws RepositoryException */ @Override public PushResponse push(PushCommandRequest request) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index d60abd424d..bda0d87b21 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java index 02fee3cef0..807ec807e6 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java @@ -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 { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java new file mode 100644 index 0000000000..22fce5f330 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java @@ -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); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java index 5081a29d21..85119a3e9f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java @@ -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); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java new file mode 100644 index 0000000000..fd0cba510b --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java @@ -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 { + WorkingCopy(Repository wrapped, Consumer cleanup) { + super(wrapped, cleanup); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryViewer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryViewer.java index 773e09aada..82e19c77b7 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryViewer.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryViewer.java @@ -151,7 +151,6 @@ public class GitRepositoryViewer * @return * * @throws IOException - * @throws RepositoryException */ private BranchesModel createBranchesModel(Repository repository) throws IOException diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java index e731e01a62..a3dac0e7d1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java @@ -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); } } diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js new file mode 100644 index 0000000000..630984ad87 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js @@ -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 { + + 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 ( + <> + + + + + ); + } + +} + +export default translate("plugins")(GitConfigurationForm); diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.js b/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.js new file mode 100644 index 0000000000..3718cc2900 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.js @@ -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 { + + constructor(props: Props) { + super(props); + } + + render() { + const { link, t } = this.props; + + return ( +
+ + <GlobalConfiguration link={link} render={props => <GitConfigurationForm {...props} />}/> + </div> + ); + } + +} + +export default translate("plugins")(GitGlobalConfiguration); diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.js b/scm-plugins/scm-git-plugin/src/main/js/index.js index be40135f8d..3f91405509 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/index.js +++ b/scm-plugins/scm-git-plugin/src/main/js/index.js @@ -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); diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json index 65594bae19..8cb801ac2c 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json @@ -4,6 +4,17 @@ "clone" : "Clone the repository", "create" : "Create a new repository", "replace" : "Push an existing repository" + }, + "config": { + "link": "Git", + "title": "Git Configuration", + "directory": "Repository Directory", + "directoryHelpText": "Location of the Git repositories.", + "gcExpression": "GC Cron Expression", + "gcExpressionHelpText": "Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.", + "disabled": "Disabled", + "disabledHelpText": "Enable or disable the Git plugin", + "submit": "Submit" } } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java new file mode 100644 index 0000000000..e92ee7abb5 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java @@ -0,0 +1,29 @@ +package sonia.scm.repository; + +import org.junit.Test; + +import java.util.function.Consumer; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class CloseableWrapperTest { + + @Test + public void shouldExecuteGivenMethodAtClose() { + Consumer<String> wrapped = new Consumer<String>() { + // no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy + @Override + public void accept(String s) { + } + }; + + Consumer<String> closer = spy(wrapped); + + try (CloseableWrapper<String> wrapper = new CloseableWrapper<>("test", closer)) { + // nothing to do here + } + + verify(closer).accept("test"); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java index 90627f98e3..ace6756cad 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java @@ -65,6 +65,9 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Mock private ConfigurationStoreFactory factory; + @Mock + private GitWorkdirFactory gitWorkdirFactory; + RepositoryLocationResolver repositoryLocationResolver ; private Path repoDir; @@ -95,7 +98,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(contextProvider,fileSystem); repositoryLocationResolver = new RepositoryLocationResolver(repoDao, initialRepositoryLocationResolver); GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - fileSystem, scheduler, repositoryLocationResolver); + fileSystem, scheduler, repositoryLocationResolver, gitWorkdirFactory); repoDir = directory.toPath(); when(repoDao.getPath(any())).thenReturn(repoDir); @@ -112,7 +115,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - new DefaultFileSystem(), scheduler, repositoryLocationResolver); + new DefaultFileSystem(), scheduler, repositoryLocationResolver, gitWorkdirFactory); Repository repository = new Repository("id", "git", "Space", "Name"); GitConfig config = new GitConfig(); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java index 496b71e656..0b3c1d6e9d 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java @@ -50,7 +50,9 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase @After public void close() { - context.close(); + if (context != null) { + context.close(); + } } /** @@ -63,7 +65,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase { if (context == null) { - context = new GitContext(repositoryDirectory); + context = new GitContext(repositoryDirectory, repository); } return context; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java index d0fd627046..5757cd5d5e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java @@ -85,7 +85,6 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase * * * @throws IOException - * @throws RepositoryException */ @Test public void testGetBlameResult() throws IOException @@ -119,7 +118,6 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase * * * @throws IOException - * @throws RepositoryException */ @Test public void testGetBlameResultWithRevision() diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 92b7ff69a9..5e63adfb70 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -32,7 +32,6 @@ package sonia.scm.repository.spi; import org.junit.Test; -import sonia.scm.NotFoundException; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.GitConstants; @@ -54,7 +53,7 @@ import static org.junit.Assert.assertTrue; public class GitBrowseCommandTest extends AbstractGitCommandTestBase { @Test - public void testGetFile() throws IOException, NotFoundException { + public void testDefaultBranch() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setPath("a.txt"); BrowserResult result = createCommand().getBrowserResult(request); @@ -63,7 +62,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } @Test - public void testDefaultDefaultBranch() throws IOException, NotFoundException { + public void testDefaultDefaultBranch() throws IOException { // without default branch, the repository head should be used FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); assertNotNull(root); @@ -78,7 +77,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } @Test - public void testExplicitDefaultBranch() throws IOException, NotFoundException { + public void testExplicitDefaultBranch() throws IOException { repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); @@ -91,7 +90,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } @Test - public void testBrowse() throws IOException, NotFoundException { + public void testBrowse() throws IOException { FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); assertNotNull(root); @@ -113,7 +112,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } @Test - public void testBrowseSubDirectory() throws IOException, NotFoundException { + public void testBrowseSubDirectory() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setPath("c"); @@ -143,7 +142,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } @Test - public void testRecursive() throws IOException, NotFoundException { + public void testRecusive() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setRecursive(true); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java index 3611c9c636..079fcac1da 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java @@ -32,10 +32,13 @@ package sonia.scm.repository.spi; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import sonia.scm.NotFoundException; import sonia.scm.repository.GitConstants; -import sonia.scm.repository.PathNotFoundException; -import sonia.scm.repository.RevisionNotFoundException; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -51,9 +54,12 @@ import static org.junit.Assert.assertEquals; * @author Sebastian Sdorra */ public class GitCatCommandTest extends AbstractGitCommandTestBase { - + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + @Test - public void testDefaultBranch() throws IOException, PathNotFoundException, RevisionNotFoundException { + public void testDefaultBranch() throws IOException { // without default branch, the repository head should be used CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -66,7 +72,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase { } @Test - public void testCat() throws IOException, PathNotFoundException, RevisionNotFoundException { + public void testCat() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -75,32 +81,58 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase { } @Test - public void testSimpleCat() throws IOException, PathNotFoundException, RevisionNotFoundException { + public void testSimpleCat() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("b.txt"); assertEquals("b", execute(request)); } - @Test(expected = PathNotFoundException.class) - public void testUnknownFile() throws IOException, PathNotFoundException, RevisionNotFoundException { + @Test + public void testUnknownFile() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("unknown"); - execute(request); - } - @Test(expected = RevisionNotFoundException.class) - public void testUnknownRevision() throws IOException, PathNotFoundException, RevisionNotFoundException { - CatCommandRequest request = new CatCommandRequest(); + expectedException.expect(new BaseMatcher<Object>() { + @Override + public void describeTo(Description description) { + description.appendText("expected NotFoundException for path"); + } + + @Override + public boolean matches(Object item) { + return "Path".equals(((NotFoundException)item).getContext().get(0).getType()); + } + }); - request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - request.setPath("a.txt"); execute(request); } @Test - public void testSimpleStream() throws IOException, PathNotFoundException, RevisionNotFoundException { + public void testUnknownRevision() throws IOException { + CatCommandRequest request = new CatCommandRequest(); + + request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + request.setPath("a.txt"); + + expectedException.expect(new BaseMatcher<Object>() { + @Override + public void describeTo(Description description) { + description.appendText("expected NotFoundException for revision"); + } + + @Override + public boolean matches(Object item) { + return "Revision".equals(((NotFoundException)item).getContext().get(0).getType()); + } + }); + + execute(request); + } + + @Test + public void testSimpleStream() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("b.txt"); @@ -113,7 +145,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase { catResultStream.close(); } - private String execute(CatCommandRequest request) throws IOException, PathNotFoundException, RevisionNotFoundException { + private String execute(CatCommandRequest request) throws IOException { String content = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java new file mode 100644 index 0000000000..f6e462f968 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -0,0 +1,93 @@ +package sonia.scm.repository.spi; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; + +import static org.junit.Assert.assertEquals; + +public class GitDiffCommandTest extends AbstractGitCommandTestBase { + + public static final String DIFF_FILE_A = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..1dc60c7 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1 @@\n" + + "-a\n" + + "+a and b\n"; + public static final String DIFF_FILE_B = "diff --git a/b.txt b/b.txt\n" + + "deleted file mode 100644\n" + + "index 6178079..0000000\n" + + "--- a/b.txt\n" + + "+++ /dev/null\n" + + "@@ -1 +0,0 @@\n" + + "-b\n"; + public static final String DIFF_FILE_A_MULTIPLE_REVISIONS = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1,2 @@\n" + + " a\n" + + "+line for blame\n"; + public static final String DIFF_FILE_F_MULTIPLE_REVISIONS = "diff --git a/f.txt b/f.txt\n" + + "new file mode 100644\n" + + "index 0000000..6a69f92\n" + + "--- /dev/null\n" + + "+++ b/f.txt\n" + + "@@ -0,0 +1 @@\n" + + "+f\n"; + + @Test + public void diffForOneRevisionShouldCreateDiff() { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest, output); + assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); + } + + @Test + public void diffForOneBranchShouldCreateDiff() { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("test-branch"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest, output); + assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); + } + + @Test + public void diffForPathShouldCreateLimitedDiff() { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("test-branch"); + diffCommandRequest.setPath("a.txt"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest, output); + assertEquals(DIFF_FILE_A, output.toString()); + } + + @Test + public void diffBetweenTwoBranchesShouldCreateDiff() { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("master"); + diffCommandRequest.setAncestorChangeset("test-branch"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest, output); + assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS + DIFF_FILE_F_MULTIPLE_REVISIONS, output.toString()); + } + + @Test + public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("master"); + diffCommandRequest.setAncestorChangeset("test-branch"); + diffCommandRequest.setPath("a.txt"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest, output); + assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java index e3d36601e7..acf0b0f820 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java @@ -61,7 +61,6 @@ public class GitIncomingCommandTest * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testGetIncomingChangesets() @@ -95,7 +94,6 @@ public class GitIncomingCommandTest * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testGetIncomingChangesetsWithAllreadyPullChangesets() @@ -105,7 +103,7 @@ public class GitIncomingCommandTest commit(outgoing, "added a"); - GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomingRepository); + GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, null), incomingRepository); PullCommandRequest req = new PullCommandRequest(); req.setRemoteRepository(outgoingRepository); pull.pull(req); @@ -132,7 +130,6 @@ public class GitIncomingCommandTest * * * @throws IOException - * @throws RepositoryException */ @Test public void testGetIncomingChangesetsWithEmptyRepository() @@ -156,7 +153,6 @@ public class GitIncomingCommandTest * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test @Ignore @@ -191,7 +187,7 @@ public class GitIncomingCommandTest */ private GitIncomingCommand createCommand() { - return new GitIncomingCommand(handler, new GitContext(incomingDirectory), + return new GitIncomingCommand(handler, new GitContext(incomingDirectory, null), incomingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index 78db8ae686..4afaf09c67 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -64,7 +64,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase * Tests log command with the usage of a default branch. */ @Test - public void testGetDefaultBranch() throws Exception { + public void testGetDefaultBranch() { // without default branch, the repository head should be used ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); @@ -92,7 +92,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase } @Test - public void testGetAll() throws Exception + public void testGetAll() { ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); @@ -103,7 +103,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase } @Test - public void testGetAllByPath() throws Exception + public void testGetAllByPath() { LogCommandRequest request = new LogCommandRequest(); @@ -119,7 +119,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase } @Test - public void testGetAllWithLimit() throws Exception + public void testGetAllWithLimit() { LogCommandRequest request = new LogCommandRequest(); @@ -143,7 +143,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase } @Test - public void testGetAllWithPaging() throws Exception + public void testGetAllWithPaging() { LogCommandRequest request = new LogCommandRequest(); @@ -194,7 +194,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase } @Test - public void testGetRange() throws Exception + public void testGetRange() { LogCommandRequest request = new LogCommandRequest(); @@ -216,6 +216,26 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId()); } + @Test + public void testGetAncestor() + { + LogCommandRequest request = new LogCommandRequest(); + + request.setBranch("test-branch"); + request.setAncestorChangeset("master"); + + ChangesetPagingResult result = createCommand().getChangesets(request); + + assertNotNull(result); + assertEquals(1, result.getTotal()); + assertEquals(1, result.getChangesets().size()); + + Changeset c = result.getChangesets().get(0); + + assertNotNull(c); + assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", c.getId()); + } + @Test public void shouldFindDefaultBranchFromHEAD() throws Exception { setRepositoryHeadReference("ref: refs/heads/test-branch"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java new file mode 100644 index 0000000000..1fca7814ed --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -0,0 +1,139 @@ +package sonia.scm.repository.spi; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.repository.Person; +import sonia.scm.repository.api.MergeCommandResult; +import sonia.scm.user.User; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class GitMergeCommandTest extends AbstractGitCommandTestBase { + + private static final String REALM = "AdminRealm"; + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Test + public void shouldDetectMergeableBranches() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("mergeable"); + request.setTargetBranch("master"); + + boolean mergeable = command.dryRun(request).isMergeable(); + + assertThat(mergeable).isTrue(); + } + + @Test + public void shouldDetectNotMergeableBranches() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("test-branch"); + request.setTargetBranch("master"); + + boolean mergeable = command.dryRun(request).isMergeable(); + + assertThat(mergeable).isFalse(); + } + + @Test + public void shouldMergeMergeableBranches() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det"); + assertThat(message).contains("master", "mergeable"); + // We expect the merge result of file b.txt here by looking up the sha hash of its content. + // If the file is missing (aka not merged correctly) this will throw a MissingObjectException: + byte[] contentOfFileB = repository.open(repository.resolve("9513e9c76e73f3e562fd8e4c909d0607113c77c6")).getBytes(); + assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n"); + } + + @Test + public void shouldUseConfiguredCommitMessageTemplate() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setMessageTemplate("simple"); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + String message = mergeCommit.getFullMessage(); + assertThat(message).isEqualTo("simple"); + } + + @Test + public void shouldNotMergeConflictingBranches() { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("test-branch"); + request.setTargetBranch("master"); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isFalse(); + assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt"); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException { + shiro.setSubject( + new Subject.Builder() + .principals(new SimplePrincipalCollection(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM)) + .buildSubject()); + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable<RevCommit> mergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + PersonIdent mergeAuthor = mergeCommit.iterator().next().getAuthorIdent(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det"); + } + + private GitMergeCommand createCommand() { + return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java index fb982f6f0c..41a516a124 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java @@ -18,8 +18,8 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase { @Before public void init() { - incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory), incomingRepository); - outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory), outgoingRepository); + incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, null), incomingRepository); + outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, null), outgoingRepository); } @Test @@ -63,12 +63,12 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase { } void pushOutgoingAndPullIncoming() throws IOException { - GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory), + GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, null), outgoingRepository); PushCommandRequest request = new PushCommandRequest(); request.setRemoteRepository(incomingRepository); cmd.push(request); - GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory), + GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, null), incomingRepository); PullCommandRequest pullRequest = new PullCommandRequest(); pullRequest.setRemoteRepository(incomingRepository); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java index 3510650fb4..65592cf7e4 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java @@ -61,7 +61,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testGetOutgoingChangesets() @@ -95,7 +94,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testGetOutgoingChangesetsWithAlreadyPushedChanges() @@ -106,7 +104,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase commit(outgoing, "added a"); GitPushCommand push = new GitPushCommand(handler, - new GitContext(outgoingDirectory), + new GitContext(outgoingDirectory, null), outgoingRepository); PushCommandRequest req = new PushCommandRequest(); @@ -135,7 +133,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase * * * @throws IOException - * @throws RepositoryException */ @Test public void testGetOutgoingChangesetsWithEmptyRepository() @@ -161,7 +158,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase */ private GitOutgoingCommand createCommand() { - return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory), + return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, null), outgoingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java index 4f3d7e933d..70212ba233 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java @@ -61,7 +61,6 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase * * @throws GitAPIException * @throws IOException - * @throws RepositoryException */ @Test public void testPush() @@ -99,7 +98,7 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase */ private GitPushCommand createCommand() { - return new GitPushCommand(handler, new GitContext(outgoingDirectory), + return new GitPushCommand(handler, new GitContext(outgoingDirectory, null), outgoingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java new file mode 100644 index 0000000000..0c39a1deb0 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java @@ -0,0 +1,87 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void emptyPoolShouldCreateNewWorkdir() throws IOException { + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); + File masterRepo = createRepositoryDirectory(); + + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { + + assertThat(workingCopy.get().getDirectory()) + .exists() + .isNotEqualTo(masterRepo) + .isDirectory(); + assertThat(new File(workingCopy.get().getWorkTree(), "a.txt")) + .exists() + .isFile() + .hasContent("a\nline for blame"); + } + } + + @Test + public void cloneFromPoolShouldBeClosed() throws IOException { + PoolWithSpy factory = new PoolWithSpy(temporaryFolder.newFolder()); + + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { + assertThat(workingCopy).isNotNull(); + } + verify(factory.createdClone).close(); + } + + @Test + public void cloneFromPoolShouldNotBeReused() throws IOException { + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); + + File firstDirectory; + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { + firstDirectory = workingCopy.get().getDirectory(); + } + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { + File secondDirectory = workingCopy.get().getDirectory(); + assertThat(secondDirectory).isNotEqualTo(firstDirectory); + } + } + + @Test + public void cloneFromPoolShouldBeDeletedOnClose() throws IOException { + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); + + File directory; + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { + directory = workingCopy.get().getWorkTree(); + } + assertThat(directory).doesNotExist(); + } + + private static class PoolWithSpy extends SimpleGitWorkdirFactory { + PoolWithSpy(File poolDirectory) { + super(poolDirectory); + } + + Repository createdClone; + + @Override + protected Repository cloneRepository(File bareRepository, File destination) throws GitAPIException { + createdClone = spy(super.cloneRepository(bareRepository, destination)); + return createdClone; + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip index 3fbab0be38..8f689e9664 100644 Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip differ diff --git a/scm-plugins/scm-git-plugin/yarn.lock b/scm-plugins/scm-git-plugin/yarn.lock index c1abfd6640..3514ed3f2c 100644 --- a/scm-plugins/scm-git-plugin/yarn.lock +++ b/scm-plugins/scm-git-plugin/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb" +"@scm-manager/ui-bundler@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -747,9 +747,9 @@ vinyl-source-stream "^2.0.0" watchify "^3.11.0" -"@scm-manager/ui-extensions@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e" +"@scm-manager/ui-extensions@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45" dependencies: react "^16.4.2" react-dom "^16.4.2" diff --git a/scm-plugins/scm-hg-plugin/.flowconfig b/scm-plugins/scm-hg-plugin/.flowconfig index 7ede008602..b05e157358 100644 --- a/scm-plugins/scm-hg-plugin/.flowconfig +++ b/scm-plugins/scm-hg-plugin/.flowconfig @@ -4,5 +4,6 @@ [include] [libs] +./node_modules/@scm-manager/ui-components/flow-typed [options] diff --git a/scm-plugins/scm-hg-plugin/flow-typed/npm/classnames_v2.x.x.js b/scm-plugins/scm-hg-plugin/flow-typed/npm/classnames_v2.x.x.js deleted file mode 100644 index 2307243eeb..0000000000 --- a/scm-plugins/scm-hg-plugin/flow-typed/npm/classnames_v2.x.x.js +++ /dev/null @@ -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">; -} diff --git a/scm-plugins/scm-hg-plugin/flow-typed/npm/jest_v23.x.x.js b/scm-plugins/scm-hg-plugin/flow-typed/npm/jest_v23.x.x.js deleted file mode 100644 index 23b66b07e5..0000000000 --- a/scm-plugins/scm-hg-plugin/flow-typed/npm/jest_v23.x.x.js +++ /dev/null @@ -1,1108 +0,0 @@ -// flow-typed signature: f5a484315a3dea13d273645306e4076a -// flow-typed version: 7c5d14b3d4/jest_v23.x.x/flow_>=v0.39.x - -type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = { - (...args: TArguments): TReturn, - /** - * An object for introspecting mock calls - */ - mock: { - /** - * An array that represents all calls that have been made into this mock - * function. Each call is represented by an array of arguments that were - * passed during the call. - */ - calls: Array<TArguments>, - /** - * An array that contains all the object instances that have been - * instantiated from this mock function. - */ - instances: Array<TReturn> - }, - /** - * Resets all information stored in the mockFn.mock.calls and - * mockFn.mock.instances arrays. Often this is useful when you want to clean - * up a mock's usage data between two assertions. - */ - mockClear(): void, - /** - * Resets all information stored in the mock. This is useful when you want to - * completely restore a mock back to its initial state. - */ - mockReset(): void, - /** - * Removes the mock and restores the initial implementation. This is useful - * when you want to mock functions in certain test cases and restore the - * original implementation in others. Beware that mockFn.mockRestore only - * works when mock was created with jest.spyOn. Thus you have to take care of - * restoration yourself when manually assigning jest.fn(). - */ - mockRestore(): void, - /** - * Accepts a function that should be used as the implementation of the mock. - * The mock itself will still record all calls that go into and instances - * that come from itself -- the only difference is that the implementation - * will also be executed when the mock is called. - */ - mockImplementation( - fn: (...args: TArguments) => TReturn - ): JestMockFn<TArguments, TReturn>, - /** - * Accepts a function that will be used as an implementation of the mock for - * one call to the mocked function. Can be chained so that multiple function - * calls produce different results. - */ - mockImplementationOnce( - fn: (...args: TArguments) => TReturn - ): JestMockFn<TArguments, TReturn>, - /** - * Accepts a string to use in test result output in place of "jest.fn()" to - * indicate which mock function is being referenced. - */ - mockName(name: string): JestMockFn<TArguments, TReturn>, - /** - * Just a simple sugar function for returning `this` - */ - mockReturnThis(): void, - /** - * Accepts a value that will be returned whenever the mock function is called. - */ - mockReturnValue(value: TReturn): JestMockFn<TArguments, TReturn>, - /** - * Sugar for only returning a value once inside your mock - */ - mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>, - /** - * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) - */ - mockResolvedValue(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>, - /** - * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) - */ - mockResolvedValueOnce(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>, - /** - * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) - */ - mockRejectedValue(value: TReturn): JestMockFn<TArguments, Promise<any>>, - /** - * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) - */ - mockRejectedValueOnce(value: TReturn): JestMockFn<TArguments, Promise<any>> -}; - -type JestAsymmetricEqualityType = { - /** - * A custom Jasmine equality tester - */ - asymmetricMatch(value: mixed): boolean -}; - -type JestCallsType = { - allArgs(): mixed, - all(): mixed, - any(): boolean, - count(): number, - first(): mixed, - mostRecent(): mixed, - reset(): void -}; - -type JestClockType = { - install(): void, - mockDate(date: Date): void, - tick(milliseconds?: number): void, - uninstall(): void -}; - -type JestMatcherResult = { - message?: string | (() => string), - pass: boolean -}; - -type JestMatcher = (actual: any, expected: any) => JestMatcherResult; - -type JestPromiseType = { - /** - * Use rejects to unwrap the reason of a rejected promise so any other - * matcher can be chained. If the promise is fulfilled the assertion fails. - */ - rejects: JestExpectType, - /** - * Use resolves to unwrap the value of a fulfilled promise so any other - * matcher can be chained. If the promise is rejected the assertion fails. - */ - resolves: JestExpectType -}; - -/** - * Jest allows functions and classes to be used as test names in test() and - * describe() - */ -type JestTestName = string | Function; - -/** - * Plugin: jest-styled-components - */ - -type JestStyledComponentsMatcherValue = - | string - | JestAsymmetricEqualityType - | RegExp - | typeof undefined; - -type JestStyledComponentsMatcherOptions = { - media?: string; - modifier?: string; - supports?: string; -} - -type JestStyledComponentsMatchersType = { - toHaveStyleRule( - property: string, - value: JestStyledComponentsMatcherValue, - options?: JestStyledComponentsMatcherOptions - ): void, -}; - -/** - * Plugin: jest-enzyme - */ -type EnzymeMatchersType = { - toBeChecked(): void, - toBeDisabled(): void, - toBeEmpty(): void, - toBeEmptyRender(): void, - toBePresent(): void, - toContainReact(element: React$Element<any>): void, - toExist(): void, - toHaveClassName(className: string): void, - toHaveHTML(html: string): void, - toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void), - toHaveRef(refName: string): void, - toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void), - toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void), - toHaveTagName(tagName: string): void, - toHaveText(text: string): void, - toIncludeText(text: string): void, - toHaveValue(value: any): void, - toMatchElement(element: React$Element<any>): void, - toMatchSelector(selector: string): void -}; - -// DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers -type DomTestingLibraryType = { - toBeInTheDOM(): void, - toHaveTextContent(content: string): void, - toHaveAttribute(name: string, expectedValue?: string): void -}; - -// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers -type JestJQueryMatchersType = { - toExist(): void, - toHaveLength(len: number): void, - toHaveId(id: string): void, - toHaveClass(className: string): void, - toHaveTag(tag: string): void, - toHaveAttr(key: string, val?: any): void, - toHaveProp(key: string, val?: any): void, - toHaveText(text: string | RegExp): void, - toHaveData(key: string, val?: any): void, - toHaveValue(val: any): void, - toHaveCss(css: {[key: string]: any}): void, - toBeChecked(): void, - toBeDisabled(): void, - toBeEmpty(): void, - toBeHidden(): void, - toBeSelected(): void, - toBeVisible(): void, - toBeFocused(): void, - toBeInDom(): void, - toBeMatchedBy(sel: string): void, - toHaveDescendant(sel: string): void, - toHaveDescendantWithText(sel: string, text: string | RegExp): void -}; - - -// Jest Extended Matchers: https://github.com/jest-community/jest-extended -type JestExtendedMatchersType = { - /** - * Note: Currently unimplemented - * Passing assertion - * - * @param {String} message - */ - // pass(message: string): void; - - /** - * Note: Currently unimplemented - * Failing assertion - * - * @param {String} message - */ - // fail(message: string): void; - - /** - * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. - */ - toBeEmpty(): void; - - /** - * Use .toBeOneOf when checking if a value is a member of a given Array. - * @param {Array.<*>} members - */ - toBeOneOf(members: any[]): void; - - /** - * Use `.toBeNil` when checking a value is `null` or `undefined`. - */ - toBeNil(): void; - - /** - * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. - * @param {Function} predicate - */ - toSatisfy(predicate: (n: any) => boolean): void; - - /** - * Use `.toBeArray` when checking if a value is an `Array`. - */ - toBeArray(): void; - - /** - * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. - * @param {Number} x - */ - toBeArrayOfSize(x: number): void; - - /** - * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. - * @param {Array.<*>} members - */ - toIncludeAllMembers(members: any[]): void; - - /** - * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. - * @param {Array.<*>} members - */ - toIncludeAnyMembers(members: any[]): void; - - /** - * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. - * @param {Function} predicate - */ - toSatisfyAll(predicate: (n: any) => boolean): void; - - /** - * Use `.toBeBoolean` when checking if a value is a `Boolean`. - */ - toBeBoolean(): void; - - /** - * Use `.toBeTrue` when checking a value is equal (===) to `true`. - */ - toBeTrue(): void; - - /** - * Use `.toBeFalse` when checking a value is equal (===) to `false`. - */ - toBeFalse(): void; - - /** - * Use .toBeDate when checking if a value is a Date. - */ - toBeDate(): void; - - /** - * Use `.toBeFunction` when checking if a value is a `Function`. - */ - toBeFunction(): void; - - /** - * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. - * - * Note: Required Jest version >22 - * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same - * - * @param {Mock} mock - */ - toHaveBeenCalledBefore(mock: JestMockFn<any, any>): void; - - /** - * Use `.toBeNumber` when checking if a value is a `Number`. - */ - toBeNumber(): void; - - /** - * Use `.toBeNaN` when checking a value is `NaN`. - */ - toBeNaN(): void; - - /** - * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. - */ - toBeFinite(): void; - - /** - * Use `.toBePositive` when checking if a value is a positive `Number`. - */ - toBePositive(): void; - - /** - * Use `.toBeNegative` when checking if a value is a negative `Number`. - */ - toBeNegative(): void; - - /** - * Use `.toBeEven` when checking if a value is an even `Number`. - */ - toBeEven(): void; - - /** - * Use `.toBeOdd` when checking if a value is an odd `Number`. - */ - toBeOdd(): void; - - /** - * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). - * - * @param {Number} start - * @param {Number} end - */ - toBeWithin(start: number, end: number): void; - - /** - * Use `.toBeObject` when checking if a value is an `Object`. - */ - toBeObject(): void; - - /** - * Use `.toContainKey` when checking if an object contains the provided key. - * - * @param {String} key - */ - toContainKey(key: string): void; - - /** - * Use `.toContainKeys` when checking if an object has all of the provided keys. - * - * @param {Array.<String>} keys - */ - toContainKeys(keys: string[]): void; - - /** - * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. - * - * @param {Array.<String>} keys - */ - toContainAllKeys(keys: string[]): void; - - /** - * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. - * - * @param {Array.<String>} keys - */ - toContainAnyKeys(keys: string[]): void; - - /** - * Use `.toContainValue` when checking if an object contains the provided value. - * - * @param {*} value - */ - toContainValue(value: any): void; - - /** - * Use `.toContainValues` when checking if an object contains all of the provided values. - * - * @param {Array.<*>} values - */ - toContainValues(values: any[]): void; - - /** - * Use `.toContainAllValues` when checking if an object only contains all of the provided values. - * - * @param {Array.<*>} values - */ - toContainAllValues(values: any[]): void; - - /** - * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. - * - * @param {Array.<*>} values - */ - toContainAnyValues(values: any[]): void; - - /** - * Use `.toContainEntry` when checking if an object contains the provided entry. - * - * @param {Array.<String, String>} entry - */ - toContainEntry(entry: [string, string]): void; - - /** - * Use `.toContainEntries` when checking if an object contains all of the provided entries. - * - * @param {Array.<Array.<String, String>>} entries - */ - toContainEntries(entries: [string, string][]): void; - - /** - * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. - * - * @param {Array.<Array.<String, String>>} entries - */ - toContainAllEntries(entries: [string, string][]): void; - - /** - * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. - * - * @param {Array.<Array.<String, String>>} entries - */ - toContainAnyEntries(entries: [string, string][]): void; - - /** - * Use `.toBeExtensible` when checking if an object is extensible. - */ - toBeExtensible(): void; - - /** - * Use `.toBeFrozen` when checking if an object is frozen. - */ - toBeFrozen(): void; - - /** - * Use `.toBeSealed` when checking if an object is sealed. - */ - toBeSealed(): void; - - /** - * Use `.toBeString` when checking if a value is a `String`. - */ - toBeString(): void; - - /** - * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. - * - * @param {String} string - */ - toEqualCaseInsensitive(string: string): void; - - /** - * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. - * - * @param {String} prefix - */ - toStartWith(prefix: string): void; - - /** - * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. - * - * @param {String} suffix - */ - toEndWith(suffix: string): void; - - /** - * Use `.toInclude` when checking if a `String` includes the given `String` substring. - * - * @param {String} substring - */ - toInclude(substring: string): void; - - /** - * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. - * - * @param {String} substring - * @param {Number} times - */ - toIncludeRepeated(substring: string, times: number): void; - - /** - * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. - * - * @param {Array.<String>} substring - */ - toIncludeMultiple(substring: string[]): void; -}; - -interface JestExpectType { - not: - & JestExpectType - & EnzymeMatchersType - & DomTestingLibraryType - & JestJQueryMatchersType - & JestStyledComponentsMatchersType - & JestExtendedMatchersType, - /** - * If you have a mock function, you can use .lastCalledWith to test what - * arguments it was last called with. - */ - lastCalledWith(...args: Array<any>): void, - /** - * toBe just checks that a value is what you expect. It uses === to check - * strict equality. - */ - toBe(value: any): void, - /** - * Use .toBeCalledWith to ensure that a mock function was called with - * specific arguments. - */ - toBeCalledWith(...args: Array<any>): void, - /** - * Using exact equality with floating point numbers is a bad idea. Rounding - * means that intuitive things fail. - */ - toBeCloseTo(num: number, delta: any): void, - /** - * Use .toBeDefined to check that a variable is not undefined. - */ - toBeDefined(): void, - /** - * Use .toBeFalsy when you don't care what a value is, you just want to - * ensure a value is false in a boolean context. - */ - toBeFalsy(): void, - /** - * To compare floating point numbers, you can use toBeGreaterThan. - */ - toBeGreaterThan(number: number): void, - /** - * To compare floating point numbers, you can use toBeGreaterThanOrEqual. - */ - toBeGreaterThanOrEqual(number: number): void, - /** - * To compare floating point numbers, you can use toBeLessThan. - */ - toBeLessThan(number: number): void, - /** - * To compare floating point numbers, you can use toBeLessThanOrEqual. - */ - toBeLessThanOrEqual(number: number): void, - /** - * Use .toBeInstanceOf(Class) to check that an object is an instance of a - * class. - */ - toBeInstanceOf(cls: Class<*>): void, - /** - * .toBeNull() is the same as .toBe(null) but the error messages are a bit - * nicer. - */ - toBeNull(): void, - /** - * Use .toBeTruthy when you don't care what a value is, you just want to - * ensure a value is true in a boolean context. - */ - toBeTruthy(): void, - /** - * Use .toBeUndefined to check that a variable is undefined. - */ - toBeUndefined(): void, - /** - * Use .toContain when you want to check that an item is in a list. For - * testing the items in the list, this uses ===, a strict equality check. - */ - toContain(item: any): void, - /** - * Use .toContainEqual when you want to check that an item is in a list. For - * testing the items in the list, this matcher recursively checks the - * equality of all fields, rather than checking for object identity. - */ - toContainEqual(item: any): void, - /** - * Use .toEqual when you want to check that two objects have the same value. - * This matcher recursively checks the equality of all fields, rather than - * checking for object identity. - */ - toEqual(value: any): void, - /** - * Use .toHaveBeenCalled to ensure that a mock function got called. - */ - toHaveBeenCalled(): void, - toBeCalled(): void; - /** - * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact - * number of times. - */ - toHaveBeenCalledTimes(number: number): void, - toBeCalledTimes(number: number): void; - /** - * - */ - toHaveBeenNthCalledWith(nthCall: number, ...args: Array<any>): void; - nthCalledWith(nthCall: number, ...args: Array<any>): void; - /** - * - */ - toHaveReturned(): void; - toReturn(): void; - /** - * - */ - toHaveReturnedTimes(number: number): void; - toReturnTimes(number: number): void; - /** - * - */ - toHaveReturnedWith(value: any): void; - toReturnWith(value: any): void; - /** - * - */ - toHaveLastReturnedWith(value: any): void; - lastReturnedWith(value: any): void; - /** - * - */ - toHaveNthReturnedWith(nthCall: number, value: any): void; - nthReturnedWith(nthCall: number, value: any): void; - /** - * Use .toHaveBeenCalledWith to ensure that a mock function was called with - * specific arguments. - */ - toHaveBeenCalledWith(...args: Array<any>): void, - toBeCalledWith(...args: Array<any>): void, - /** - * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called - * with specific arguments. - */ - toHaveBeenLastCalledWith(...args: Array<any>): void, - lastCalledWith(...args: Array<any>): void, - /** - * Check that an object has a .length property and it is set to a certain - * numeric value. - */ - toHaveLength(number: number): void, - /** - * - */ - toHaveProperty(propPath: string, value?: any): void, - /** - * Use .toMatch to check that a string matches a regular expression or string. - */ - toMatch(regexpOrString: RegExp | string): void, - /** - * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. - */ - toMatchObject(object: Object | Array<Object>): void, - /** - * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. - */ - toStrictEqual(value: any): void, - /** - * This ensures that an Object matches the most recent snapshot. - */ - toMatchSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, name?: string): void, - /** - * This ensures that an Object matches the most recent snapshot. - */ - toMatchSnapshot(name: string): void, - - toMatchInlineSnapshot(snapshot?: string): void, - toMatchInlineSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, snapshot?: string): void, - /** - * Use .toThrow to test that a function throws when it is called. - * If you want to test that a specific error gets thrown, you can provide an - * argument to toThrow. The argument can be a string for the error message, - * a class for the error, or a regex that should match the error. - * - * Alias: .toThrowError - */ - toThrow(message?: string | Error | Class<Error> | RegExp): void, - toThrowError(message?: string | Error | Class<Error> | RegExp): void, - /** - * Use .toThrowErrorMatchingSnapshot to test that a function throws a error - * matching the most recent snapshot when it is called. - */ - toThrowErrorMatchingSnapshot(): void, - toThrowErrorMatchingInlineSnapshot(snapshot?: string): void, -} - -type JestObjectType = { - /** - * Disables automatic mocking in the module loader. - * - * After this method is called, all `require()`s will return the real - * versions of each module (rather than a mocked version). - */ - disableAutomock(): JestObjectType, - /** - * An un-hoisted version of disableAutomock - */ - autoMockOff(): JestObjectType, - /** - * Enables automatic mocking in the module loader. - */ - enableAutomock(): JestObjectType, - /** - * An un-hoisted version of enableAutomock - */ - autoMockOn(): JestObjectType, - /** - * Clears the mock.calls and mock.instances properties of all mocks. - * Equivalent to calling .mockClear() on every mocked function. - */ - clearAllMocks(): JestObjectType, - /** - * Resets the state of all mocks. Equivalent to calling .mockReset() on every - * mocked function. - */ - resetAllMocks(): JestObjectType, - /** - * Restores all mocks back to their original value. - */ - restoreAllMocks(): JestObjectType, - /** - * Removes any pending timers from the timer system. - */ - clearAllTimers(): void, - /** - * The same as `mock` but not moved to the top of the expectation by - * babel-jest. - */ - doMock(moduleName: string, moduleFactory?: any): JestObjectType, - /** - * The same as `unmock` but not moved to the top of the expectation by - * babel-jest. - */ - dontMock(moduleName: string): JestObjectType, - /** - * Returns a new, unused mock function. Optionally takes a mock - * implementation. - */ - fn<TArguments: $ReadOnlyArray<*>, TReturn>( - implementation?: (...args: TArguments) => TReturn - ): JestMockFn<TArguments, TReturn>, - /** - * Determines if the given function is a mocked function. - */ - isMockFunction(fn: Function): boolean, - /** - * Given the name of a module, use the automatic mocking system to generate a - * mocked version of the module for you. - */ - genMockFromModule(moduleName: string): any, - /** - * Mocks a module with an auto-mocked version when it is being required. - * - * The second argument can be used to specify an explicit module factory that - * is being run instead of using Jest's automocking feature. - * - * The third argument can be used to create virtual mocks -- mocks of modules - * that don't exist anywhere in the system. - */ - mock( - moduleName: string, - moduleFactory?: any, - options?: Object - ): JestObjectType, - /** - * Returns the actual module instead of a mock, bypassing all checks on - * whether the module should receive a mock implementation or not. - */ - requireActual(moduleName: string): any, - /** - * Returns a mock module instead of the actual module, bypassing all checks - * on whether the module should be required normally or not. - */ - requireMock(moduleName: string): any, - /** - * Resets the module registry - the cache of all required modules. This is - * useful to isolate modules where local state might conflict between tests. - */ - resetModules(): JestObjectType, - /** - * Exhausts the micro-task queue (usually interfaced in node via - * process.nextTick). - */ - runAllTicks(): void, - /** - * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), - * setInterval(), and setImmediate()). - */ - runAllTimers(): void, - /** - * Exhausts all tasks queued by setImmediate(). - */ - runAllImmediates(): void, - /** - * Executes only the macro task queue (i.e. all tasks queued by setTimeout() - * or setInterval() and setImmediate()). - */ - advanceTimersByTime(msToRun: number): void, - /** - * Executes only the macro task queue (i.e. all tasks queued by setTimeout() - * or setInterval() and setImmediate()). - * - * Renamed to `advanceTimersByTime`. - */ - runTimersToTime(msToRun: number): void, - /** - * Executes only the macro-tasks that are currently pending (i.e., only the - * tasks that have been queued by setTimeout() or setInterval() up to this - * point) - */ - runOnlyPendingTimers(): void, - /** - * Explicitly supplies the mock object that the module system should return - * for the specified module. Note: It is recommended to use jest.mock() - * instead. - */ - setMock(moduleName: string, moduleExports: any): JestObjectType, - /** - * Indicates that the module system should never return a mocked version of - * the specified module from require() (e.g. that it should always return the - * real module). - */ - unmock(moduleName: string): JestObjectType, - /** - * Instructs Jest to use fake versions of the standard timer functions - * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, - * setImmediate and clearImmediate). - */ - useFakeTimers(): JestObjectType, - /** - * Instructs Jest to use the real versions of the standard timer functions. - */ - useRealTimers(): JestObjectType, - /** - * Creates a mock function similar to jest.fn but also tracks calls to - * object[methodName]. - */ - spyOn(object: Object, methodName: string, accessType?: "get" | "set"): JestMockFn<any, any>, - /** - * Set the default timeout interval for tests and before/after hooks in milliseconds. - * Note: The default timeout interval is 5 seconds if this method is not called. - */ - setTimeout(timeout: number): JestObjectType -}; - -type JestSpyType = { - calls: JestCallsType -}; - -/** Runs this function after every test inside this context */ -declare function afterEach( - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; -/** Runs this function before every test inside this context */ -declare function beforeEach( - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; -/** Runs this function after all tests have finished inside this context */ -declare function afterAll( - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; -/** Runs this function before any tests have started inside this context */ -declare function beforeAll( - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; - -/** A context for grouping tests together */ -declare var describe: { - /** - * Creates a block that groups together several related tests in one "test suite" - */ - (name: JestTestName, fn: () => void): void, - - /** - * Only run this describe block - */ - only(name: JestTestName, fn: () => void): void, - - /** - * Skip running this describe block - */ - skip(name: JestTestName, fn: () => void): void -}; - -/** An individual test unit */ -declare var it: { - /** - * An individual test unit - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - ( - name: JestTestName, - fn?: (done: () => void) => ?Promise<mixed>, - timeout?: number - ): void, - /** - * each runs this test against array of argument arrays per each run - * - * @param {table} table of Test - */ - each( - table: Array<Array<mixed>> - ): ( - name: JestTestName, - fn?: (...args: Array<any>) => ?Promise<mixed> - ) => void, - /** - * Only run this test - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - only( - name: JestTestName, - fn?: (done: () => void) => ?Promise<mixed>, - timeout?: number - ): { - each( - table: Array<Array<mixed>> - ): ( - name: JestTestName, - fn?: (...args: Array<any>) => ?Promise<mixed> - ) => void, - }, - /** - * Skip running this test - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - skip( - name: JestTestName, - fn?: (done: () => void) => ?Promise<mixed>, - timeout?: number - ): void, - /** - * Run the test concurrently - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - concurrent( - name: JestTestName, - fn?: (done: () => void) => ?Promise<mixed>, - timeout?: number - ): void -}; -declare function fit( - name: JestTestName, - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; -/** An individual test unit */ -declare var test: typeof it; -/** A disabled group of tests */ -declare var xdescribe: typeof describe; -/** A focused group of tests */ -declare var fdescribe: typeof describe; -/** A disabled individual test */ -declare var xit: typeof it; -/** A disabled individual test */ -declare var xtest: typeof it; - -type JestPrettyFormatColors = { - comment: { close: string, open: string }, - content: { close: string, open: string }, - prop: { close: string, open: string }, - tag: { close: string, open: string }, - value: { close: string, open: string }, -}; - -type JestPrettyFormatIndent = string => string; -type JestPrettyFormatRefs = Array<any>; -type JestPrettyFormatPrint = any => string; -type JestPrettyFormatStringOrNull = string | null; - -type JestPrettyFormatOptions = {| - callToJSON: boolean, - edgeSpacing: string, - escapeRegex: boolean, - highlight: boolean, - indent: number, - maxDepth: number, - min: boolean, - plugins: JestPrettyFormatPlugins, - printFunctionName: boolean, - spacing: string, - theme: {| - comment: string, - content: string, - prop: string, - tag: string, - value: string, - |}, -|}; - -type JestPrettyFormatPlugin = { - print: ( - val: any, - serialize: JestPrettyFormatPrint, - indent: JestPrettyFormatIndent, - opts: JestPrettyFormatOptions, - colors: JestPrettyFormatColors, - ) => string, - test: any => boolean, -}; - -type JestPrettyFormatPlugins = Array<JestPrettyFormatPlugin>; - -/** The expect function is used every time you want to test a value */ -declare var expect: { - /** The object that you want to make assertions against */ - (value: any): - & JestExpectType - & JestPromiseType - & EnzymeMatchersType - & DomTestingLibraryType - & JestJQueryMatchersType - & JestStyledComponentsMatchersType - & JestExtendedMatchersType, - - /** Add additional Jasmine matchers to Jest's roster */ - extend(matchers: { [name: string]: JestMatcher }): void, - /** Add a module that formats application-specific data structures. */ - addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, - assertions(expectedAssertions: number): void, - hasAssertions(): void, - any(value: mixed): JestAsymmetricEqualityType, - anything(): any, - arrayContaining(value: Array<mixed>): Array<mixed>, - objectContaining(value: Object): Object, - /** Matches any received string that contains the exact expected string. */ - stringContaining(value: string): string, - stringMatching(value: string | RegExp): string, - not: { - arrayContaining: (value: $ReadOnlyArray<mixed>) => Array<mixed>, - objectContaining: (value: {}) => Object, - stringContaining: (value: string) => string, - stringMatching: (value: string | RegExp) => string, - }, -}; - -// TODO handle return type -// http://jasmine.github.io/2.4/introduction.html#section-Spies -declare function spyOn(value: mixed, method: string): Object; - -/** Holds all functions related to manipulating test runner */ -declare var jest: JestObjectType; - -/** - * The global Jasmine object, this is generally not exposed as the public API, - * using features inside here could break in later versions of Jest. - */ -declare var jasmine: { - DEFAULT_TIMEOUT_INTERVAL: number, - any(value: mixed): JestAsymmetricEqualityType, - anything(): any, - arrayContaining(value: Array<mixed>): Array<mixed>, - clock(): JestClockType, - createSpy(name: string): JestSpyType, - createSpyObj( - baseName: string, - methodNames: Array<string> - ): { [methodName: string]: JestSpyType }, - objectContaining(value: Object): Object, - stringMatching(value: string): string -}; diff --git a/scm-plugins/scm-hg-plugin/flow-typed/npm/moment_v2.3.x.js b/scm-plugins/scm-hg-plugin/flow-typed/npm/moment_v2.3.x.js deleted file mode 100644 index c2786e87fd..0000000000 --- a/scm-plugins/scm-hg-plugin/flow-typed/npm/moment_v2.3.x.js +++ /dev/null @@ -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>; -} diff --git a/scm-plugins/scm-hg-plugin/flow-typed/npm/react-jss_vx.x.x.js b/scm-plugins/scm-hg-plugin/flow-typed/npm/react-jss_vx.x.x.js deleted file mode 100644 index cf8abae155..0000000000 --- a/scm-plugins/scm-hg-plugin/flow-typed/npm/react-jss_vx.x.x.js +++ /dev/null @@ -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'>; -} diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index a2ebd0e6ad..dbca702070 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -6,9 +6,9 @@ "build": "ui-bundler plugin" }, "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" } } diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index 3da97d09e8..6b7664c140 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - + <modelVersion>4.0.0</modelVersion> <parent> @@ -16,7 +16,7 @@ <description>Plugin for the version control system Mercurial</description> <dependencies> - + <dependency> <groupId>com.aragost.javahg</groupId> <artifactId>javahg</artifactId> @@ -30,12 +30,10 @@ </dependency> </dependencies> - - <!-- create test jar --> - + <build> <plugins> - + <plugin> <groupId>com.mycila.maven-license-plugin</groupId> <artifactId>maven-license-plugin</artifactId> @@ -54,7 +52,22 @@ <strictCheck>true</strictCheck> </configuration> </plugin> - + + <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> @@ -68,45 +81,18 @@ </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> - + <repositories> - + <repository> <id>maven.scm-manager.org</id> <name>scm-manager release repository</name> <url>http://maven.scm-manager.org/nexus/content/groups/public</url> </repository> - + <repository> <releases> <enabled>false</enabled> @@ -119,7 +105,7 @@ <layout>default</layout> <url>https://oss.sonatype.org/content/groups/public/</url> </repository> - + </repositories> - + </project> diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java deleted file mode 100644 index 6fc4f4e01a..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/rest/resources/HgConfigResource.java +++ /dev/null @@ -1,348 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import sonia.scm.SCMContext; -import sonia.scm.installer.HgInstallerFactory; -import sonia.scm.installer.HgPackage; -import sonia.scm.installer.HgPackageReader; -import sonia.scm.installer.HgPackages; -import sonia.scm.net.ahc.AdvancedHttpClient; -import sonia.scm.repository.HgConfig; -import sonia.scm.repository.HgRepositoryHandler; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.List; - -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("config/repositories/hg") -public class HgConfigResource -{ - - /** - * Constructs ... - * - * - * - * - * @param client - * @param handler - * @param pkgReader - */ - @Inject - public HgConfigResource(AdvancedHttpClient client, - HgRepositoryHandler handler, HgPackageReader pkgReader) - { - this.client = client; - this.handler = handler; - this.pkgReader = pkgReader; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param uriInfo - * - * @return - */ - @POST - @Path("auto-configuration") - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public HgConfig autoConfiguration(@Context UriInfo uriInfo) - { - return autoConfiguration(uriInfo, null); - } - - /** - * Method description - * - * - * @param uriInfo - * @param config - * - * @return - */ - @POST - @Path("auto-configuration") - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public HgConfig autoConfiguration(@Context UriInfo uriInfo, HgConfig config) - { - if (config == null) - { - config = new HgConfig(); - } - - handler.doAutoConfiguration(config); - - return handler.getConfig(); - } - - /** - * Method description - * - * - * - * @param id - * @return - */ - @POST - @Path("packages/{pkgId}") - public Response installPackage(@PathParam("pkgId") String id) - { - Response response = null; - HgPackage pkg = pkgReader.getPackage(id); - - if (pkg != null) - { - if (HgInstallerFactory.createInstaller().installPackage(client, handler, - SCMContext.getContext().getBaseDirectory(), pkg)) - { - response = Response.noContent().build(); - } - else - { - response = - Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } - } - else - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - - return response; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @GET - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public HgConfig getConfig() - { - HgConfig config = handler.getConfig(); - - if (config == null) - { - config = new HgConfig(); - } - - return config; - } - - /** - * Method description - * - * - * @return - */ - @GET - @Path("installations/hg") - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public InstallationsResponse getHgInstallations() - { - List<String> installations = - HgInstallerFactory.createInstaller().getHgInstallations(); - - return new InstallationsResponse(installations); - } - - /** - * Method description - * - * - * @return - */ - @GET - @Path("packages") - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public HgPackages getPackages() - { - return pkgReader.getPackages(); - } - - /** - * Method description - * - * - * @return - */ - @GET - @Path("installations/python") - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public InstallationsResponse getPythonInstallations() - { - List<String> installations = - HgInstallerFactory.createInstaller().getPythonInstallations(); - - return new InstallationsResponse(installations); - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param uriInfo - * @param config - * - * @return - * - * @throws IOException - */ - @POST - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response setConfig(@Context UriInfo uriInfo, HgConfig config) - throws IOException - { - handler.setConfig(config); - handler.storeConfig(); - - return Response.created(uriInfo.getRequestUri()).build(); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 11/04/25 - * @author Enter your name here... - */ - @XmlAccessorType(XmlAccessType.FIELD) - @XmlRootElement(name = "installations") - public static class InstallationsResponse - { - - /** - * Constructs ... - * - */ - public InstallationsResponse() {} - - /** - * Constructs ... - * - * - * @param paths - */ - public InstallationsResponse(List<String> paths) - { - this.paths = paths; - } - - //~--- get methods -------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public List<String> getPaths() - { - return paths; - } - - //~--- set methods -------------------------------------------------------- - - /** - * Method description - * - * - * @param paths - */ - public void setPaths(List<String> paths) - { - this.paths = paths; - } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - @XmlElement(name = "path") - private List<String> paths; - } - - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private AdvancedHttpClient client; - - /** Field description */ - private HgRepositoryHandler handler; - - /** Field description */ - private HgPackageReader pkgReader; -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java index 19cfd37665..4115a514a2 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java @@ -272,7 +272,7 @@ public class AbstractHgHandler } catch (JAXBException ex) { logger.error("could not parse result", ex); - throw new InternalRepositoryException("could not parse result", ex); + throw new InternalRepositoryException(repository, "could not parse result", ex); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java index 554d6aaf05..6309720f75 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java @@ -36,6 +36,9 @@ package sonia.scm.repository.spi; import com.aragost.javahg.commands.ExecutionException; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.web.HgUtil; @@ -46,6 +49,8 @@ import java.io.OutputStream; public class HgCatCommand extends AbstractCommand implements CatCommand { + private static final Logger log = LoggerFactory.getLogger(HgCatCommand.class); + HgCatCommand(HgCommandContext context, Repository repository) { super(context, repository); } @@ -70,7 +75,8 @@ public class HgCatCommand extends AbstractCommand implements CatCommand { try { return cmd.execute(request.getPath()); } catch (ExecutionException e) { - throw new InternalRepositoryException(e); + log.error("could not execute cat command", e); + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity(getRepository()), "could not execute cat command", e); } } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java index 55cf0fbe01..c60f2c5712 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java @@ -103,7 +103,7 @@ public class HgIncomingCommand extends AbstractCommand } else { - throw new InternalRepositoryException("could not execute incoming command", ex); + throw new InternalRepositoryException(getRepository(), "could not execute incoming command", ex); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java index b476b6c2ab..81bee6b9ff 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java @@ -103,7 +103,7 @@ public class HgOutgoingCommand extends AbstractCommand } else { - throw new InternalRepositoryException("could not execute outgoing command", ex); + throw new InternalRepositoryException(getRepository(), "could not execute outgoing command", ex); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java index 0eb23ef4e7..1d130a0f79 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java @@ -97,7 +97,7 @@ public class HgPullCommand extends AbstractHgPushOrPullCommand } catch (ExecutionException ex) { - throw new InternalRepositoryException("could not execute push command", ex); + throw new InternalRepositoryException(getRepository(), "could not execute push command", ex); } return new PullResponse(result.size()); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java index fb0a1a1c73..b1c12037db 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java @@ -97,7 +97,7 @@ public class HgPushCommand extends AbstractHgPushOrPullCommand } catch (ExecutionException ex) { - throw new InternalRepositoryException("could not execute push command", ex); + throw new InternalRepositoryException(getRepository(), "could not execute push command", ex); } return new PushResponse(result.size()); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java index 2734996686..25a368d25b 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java @@ -44,11 +44,11 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.NotFoundException; import sonia.scm.repository.HgContext; import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.RepositoryHookType; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryUtil; import sonia.scm.repository.api.HgHookMessage; import sonia.scm.repository.api.HgHookMessage.Severity; @@ -275,17 +275,11 @@ public class HgHookCallbackServlet extends HttpServlet printMessages(response, context); } - catch (RepositoryNotFoundException ex) + catch (NotFoundException ex) { - if (logger.isErrorEnabled()) - { - logger.error("could not find repository with id {}", id); + logger.error(ex.getMessage()); - if (logger.isTraceEnabled()) - { - logger.trace("repository not found", ex); - } - } + logger.trace("repository not found", ex); response.sendError(HttpServletResponse.SC_NOT_FOUND); } diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js new file mode 100644 index 0000000000..2b6fc130bc --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js @@ -0,0 +1,113 @@ +//@flow +import React from "react"; +import type { Links } from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import { InputField, Checkbox } from "@scm-manager/ui-components"; + +type Configuration = { + "hgBinary": string, + "pythonBinary": string, + "pythonPath"?: string, + "repositoryDirectory": string, + "encoding": string, + "useOptimizedBytecode": boolean, + "showRevisionInId": boolean, + "disabled": boolean, + "_links": Links +}; + +type Props = { + initialConfiguration: Configuration, + readOnly: boolean, + + onConfigurationChange: (Configuration, boolean) => void, + + // context props + t: (string) => string +} + +type State = Configuration & { + validationErrors: string[] +}; + +class HgConfigurationForm extends React.Component<Props, State> { + + constructor(props: Props) { + super(props); + this.state = { ...props.initialConfiguration, validationErrors: [] }; + } + + updateValidationStatus = () => { + const requiredFields = [ + "hgBinary", "pythonBinary", "repositoryDirectory", "encoding" + ]; + + const validationErrors = []; + for (let field of requiredFields) { + if (!this.state[field]) { + validationErrors.push( field ); + } + } + + this.setState({ + validationErrors + }); + + return validationErrors.length === 0; + }; + + + hasValidationError = (name: string) => { + return this.state.validationErrors.indexOf(name) >= 0; + }; + + handleChange = (value: any, name: string) => { + this.setState({ + [name]: value + }, () => this.props.onConfigurationChange(this.state, this.updateValidationStatus())); + }; + + inputField = (name: string) => { + const { readOnly, t } = this.props; + return <InputField + name={ name } + label={t("scm-hg-plugin.config." + name)} + helpText={t("scm-hg-plugin.config." + name + "HelpText")} + value={this.state[name]} + onChange={this.handleChange} + validationError={this.hasValidationError(name)} + errorMessage={t("scm-hg-plugin.config.required")} + disabled={readOnly} + />; + }; + + checkbox = (name: string) => { + const { readOnly, t } = this.props; + return <Checkbox + name={ name } + label={t("scm-hg-plugin.config." + name)} + helpText={t("scm-hg-plugin.config." + name + "HelpText")} + checked={this.state[name]} + onChange={this.handleChange} + disabled={readOnly} + />; + }; + + render() { + return ( + <> + {this.inputField("hgBinary")} + {this.inputField("pythonBinary")} + {this.inputField("pythonPath")} + {this.inputField("repositoryDirectory")} + {this.inputField("encoding")} + {this.checkbox("useOptimizedBytecode")} + {this.checkbox("showRevisionInId")} + {this.checkbox("disabled")} + </> + ); + } + +} + +export default translate("plugins")(HgConfigurationForm); diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgGlobalConfiguration.js b/scm-plugins/scm-hg-plugin/src/main/js/HgGlobalConfiguration.js new file mode 100644 index 0000000000..e92672a282 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgGlobalConfiguration.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import { Title, GlobalConfiguration } from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; +import HgConfigurationForm from "./HgConfigurationForm"; + +type Props = { + link: string, + + // context props + t: (string) => string +} + +class HgGlobalConfiguration extends React.Component<Props> { + + render() { + const { link, t } = this.props; + return ( + <div> + <Title title={t("scm-hg-plugin.config.title")}/> + <GlobalConfiguration link={link} render={props => <HgConfigurationForm {...props} />}/> + </div> + ); + } + +} + +export default translate("plugins")(HgGlobalConfiguration); diff --git a/scm-plugins/scm-hg-plugin/src/main/js/index.js b/scm-plugins/scm-hg-plugin/src/main/js/index.js index f2ebff97c3..a1fa72f5bd 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/index.js +++ b/scm-plugins/scm-hg-plugin/src/main/js/index.js @@ -2,6 +2,8 @@ import { binder } from "@scm-manager/ui-extensions"; import ProtocolInformation from "./ProtocolInformation"; import HgAvatar from "./HgAvatar"; +import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components"; +import HgGlobalConfiguration from "./HgGlobalConfiguration"; const hgPredicate = (props: Object) => { return props.repository && props.repository.type === "hg"; @@ -9,3 +11,7 @@ const hgPredicate = (props: Object) => { binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate); binder.bind("repos.repository-avatar", HgAvatar, hgPredicate); + +// bind global configuration + +cfgBinder.bindGlobal("/hg", "scm-hg-plugin.config.link", "hgConfig", HgGlobalConfiguration); diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json index 4ec1d4e4d2..903f906c7e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json @@ -4,6 +4,27 @@ "clone" : "Clone the repository", "create" : "Create a new repository", "replace" : "Push an existing repository" + }, + "config": { + "link": "Mercurial", + "title": "Mercurial Configuration", + "hgBinary": "HG Binary", + "hgBinaryHelpText": "Location of Mercurial binary.", + "pythonBinary": "Python Binary", + "pythonBinaryHelpText": "Location of Python binary.", + "pythonPath": "Python Module Search Path", + "pythonPathHelpText": "Python Module Search Path (PYTHONPATH).", + "repositoryDirectory": "Repository directory", + "repositoryDirectoryHelpText": "Location of Mercurial repositories.", + "encoding": "Encoding", + "encodingHelpText": "Repository Encoding.", + "useOptimizedBytecode": "Optimized Bytecode (.pyo)", + "useOptimizedBytecodeHelpText": "Use the Python '-O' switch.", + "showRevisionInId": "Show Revision", + "showRevisionInIdHelpText": "Show revision as part of the node id.", + "disabled": "Disabled", + "disabledHelpText": "Enable or disable the Mercurial plugin.", + "required": "This configuration value is required" } } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java index 29fc46ed57..1daa395e07 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java @@ -39,7 +39,6 @@ import org.junit.Test; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Modifications; -import sonia.scm.repository.RevisionNotFoundException; import java.io.IOException; @@ -151,7 +150,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase } @Test - public void testGetCommit() throws IOException, RevisionNotFoundException { + public void testGetCommit() throws IOException { HgLogCommand command = createComamnd(); String revision = "a9bacaf1b7fa0cebfca71fed4e59ed69a6319427"; Changeset c = diff --git a/scm-plugins/scm-hg-plugin/yarn.lock b/scm-plugins/scm-hg-plugin/yarn.lock index c3e8cc476f..a211aa0ca1 100644 --- a/scm-plugins/scm-hg-plugin/yarn.lock +++ b/scm-plugins/scm-hg-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb" +"@scm-manager/ui-bundler@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -681,9 +681,9 @@ vinyl-source-stream "^2.0.0" watchify "^3.11.0" -"@scm-manager/ui-extensions@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e" +"@scm-manager/ui-extensions@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45" dependencies: react "^16.4.2" react-dom "^16.4.2" diff --git a/scm-plugins/scm-legacy-plugin/pom.xml b/scm-plugins/scm-legacy-plugin/pom.xml index 31d8422fbf..52aff51dd2 100644 --- a/scm-plugins/scm-legacy-plugin/pom.xml +++ b/scm-plugins/scm-legacy-plugin/pom.xml @@ -23,4 +23,19 @@ </dependency> </dependencies> -</project> \ No newline at end of file + + <build> + <plugins> + + <plugin> + <groupId>sonia.scm.maven</groupId> + <artifactId>smp-maven-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <corePlugin>true</corePlugin> + </configuration> + </plugin> + + </plugins> + </build> +</project> diff --git a/scm-plugins/scm-svn-plugin/.flowconfig b/scm-plugins/scm-svn-plugin/.flowconfig index 7ede008602..b05e157358 100644 --- a/scm-plugins/scm-svn-plugin/.flowconfig +++ b/scm-plugins/scm-svn-plugin/.flowconfig @@ -4,5 +4,6 @@ [include] [libs] +./node_modules/@scm-manager/ui-components/flow-typed [options] diff --git a/scm-plugins/scm-svn-plugin/flow-typed/npm/classnames_v2.x.x.js b/scm-plugins/scm-svn-plugin/flow-typed/npm/classnames_v2.x.x.js deleted file mode 100644 index 2307243eeb..0000000000 --- a/scm-plugins/scm-svn-plugin/flow-typed/npm/classnames_v2.x.x.js +++ /dev/null @@ -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">; -} diff --git a/scm-plugins/scm-svn-plugin/flow-typed/npm/jest_v23.x.x.js b/scm-plugins/scm-svn-plugin/flow-typed/npm/jest_v23.x.x.js deleted file mode 100644 index 23b66b07e5..0000000000 --- a/scm-plugins/scm-svn-plugin/flow-typed/npm/jest_v23.x.x.js +++ /dev/null @@ -1,1108 +0,0 @@ -// flow-typed signature: f5a484315a3dea13d273645306e4076a -// flow-typed version: 7c5d14b3d4/jest_v23.x.x/flow_>=v0.39.x - -type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = { - (...args: TArguments): TReturn, - /** - * An object for introspecting mock calls - */ - mock: { - /** - * An array that represents all calls that have been made into this mock - * function. Each call is represented by an array of arguments that were - * passed during the call. - */ - calls: Array<TArguments>, - /** - * An array that contains all the object instances that have been - * instantiated from this mock function. - */ - instances: Array<TReturn> - }, - /** - * Resets all information stored in the mockFn.mock.calls and - * mockFn.mock.instances arrays. Often this is useful when you want to clean - * up a mock's usage data between two assertions. - */ - mockClear(): void, - /** - * Resets all information stored in the mock. This is useful when you want to - * completely restore a mock back to its initial state. - */ - mockReset(): void, - /** - * Removes the mock and restores the initial implementation. This is useful - * when you want to mock functions in certain test cases and restore the - * original implementation in others. Beware that mockFn.mockRestore only - * works when mock was created with jest.spyOn. Thus you have to take care of - * restoration yourself when manually assigning jest.fn(). - */ - mockRestore(): void, - /** - * Accepts a function that should be used as the implementation of the mock. - * The mock itself will still record all calls that go into and instances - * that come from itself -- the only difference is that the implementation - * will also be executed when the mock is called. - */ - mockImplementation( - fn: (...args: TArguments) => TReturn - ): JestMockFn<TArguments, TReturn>, - /** - * Accepts a function that will be used as an implementation of the mock for - * one call to the mocked function. Can be chained so that multiple function - * calls produce different results. - */ - mockImplementationOnce( - fn: (...args: TArguments) => TReturn - ): JestMockFn<TArguments, TReturn>, - /** - * Accepts a string to use in test result output in place of "jest.fn()" to - * indicate which mock function is being referenced. - */ - mockName(name: string): JestMockFn<TArguments, TReturn>, - /** - * Just a simple sugar function for returning `this` - */ - mockReturnThis(): void, - /** - * Accepts a value that will be returned whenever the mock function is called. - */ - mockReturnValue(value: TReturn): JestMockFn<TArguments, TReturn>, - /** - * Sugar for only returning a value once inside your mock - */ - mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>, - /** - * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) - */ - mockResolvedValue(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>, - /** - * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) - */ - mockResolvedValueOnce(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>, - /** - * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) - */ - mockRejectedValue(value: TReturn): JestMockFn<TArguments, Promise<any>>, - /** - * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) - */ - mockRejectedValueOnce(value: TReturn): JestMockFn<TArguments, Promise<any>> -}; - -type JestAsymmetricEqualityType = { - /** - * A custom Jasmine equality tester - */ - asymmetricMatch(value: mixed): boolean -}; - -type JestCallsType = { - allArgs(): mixed, - all(): mixed, - any(): boolean, - count(): number, - first(): mixed, - mostRecent(): mixed, - reset(): void -}; - -type JestClockType = { - install(): void, - mockDate(date: Date): void, - tick(milliseconds?: number): void, - uninstall(): void -}; - -type JestMatcherResult = { - message?: string | (() => string), - pass: boolean -}; - -type JestMatcher = (actual: any, expected: any) => JestMatcherResult; - -type JestPromiseType = { - /** - * Use rejects to unwrap the reason of a rejected promise so any other - * matcher can be chained. If the promise is fulfilled the assertion fails. - */ - rejects: JestExpectType, - /** - * Use resolves to unwrap the value of a fulfilled promise so any other - * matcher can be chained. If the promise is rejected the assertion fails. - */ - resolves: JestExpectType -}; - -/** - * Jest allows functions and classes to be used as test names in test() and - * describe() - */ -type JestTestName = string | Function; - -/** - * Plugin: jest-styled-components - */ - -type JestStyledComponentsMatcherValue = - | string - | JestAsymmetricEqualityType - | RegExp - | typeof undefined; - -type JestStyledComponentsMatcherOptions = { - media?: string; - modifier?: string; - supports?: string; -} - -type JestStyledComponentsMatchersType = { - toHaveStyleRule( - property: string, - value: JestStyledComponentsMatcherValue, - options?: JestStyledComponentsMatcherOptions - ): void, -}; - -/** - * Plugin: jest-enzyme - */ -type EnzymeMatchersType = { - toBeChecked(): void, - toBeDisabled(): void, - toBeEmpty(): void, - toBeEmptyRender(): void, - toBePresent(): void, - toContainReact(element: React$Element<any>): void, - toExist(): void, - toHaveClassName(className: string): void, - toHaveHTML(html: string): void, - toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void), - toHaveRef(refName: string): void, - toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void), - toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void), - toHaveTagName(tagName: string): void, - toHaveText(text: string): void, - toIncludeText(text: string): void, - toHaveValue(value: any): void, - toMatchElement(element: React$Element<any>): void, - toMatchSelector(selector: string): void -}; - -// DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers -type DomTestingLibraryType = { - toBeInTheDOM(): void, - toHaveTextContent(content: string): void, - toHaveAttribute(name: string, expectedValue?: string): void -}; - -// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers -type JestJQueryMatchersType = { - toExist(): void, - toHaveLength(len: number): void, - toHaveId(id: string): void, - toHaveClass(className: string): void, - toHaveTag(tag: string): void, - toHaveAttr(key: string, val?: any): void, - toHaveProp(key: string, val?: any): void, - toHaveText(text: string | RegExp): void, - toHaveData(key: string, val?: any): void, - toHaveValue(val: any): void, - toHaveCss(css: {[key: string]: any}): void, - toBeChecked(): void, - toBeDisabled(): void, - toBeEmpty(): void, - toBeHidden(): void, - toBeSelected(): void, - toBeVisible(): void, - toBeFocused(): void, - toBeInDom(): void, - toBeMatchedBy(sel: string): void, - toHaveDescendant(sel: string): void, - toHaveDescendantWithText(sel: string, text: string | RegExp): void -}; - - -// Jest Extended Matchers: https://github.com/jest-community/jest-extended -type JestExtendedMatchersType = { - /** - * Note: Currently unimplemented - * Passing assertion - * - * @param {String} message - */ - // pass(message: string): void; - - /** - * Note: Currently unimplemented - * Failing assertion - * - * @param {String} message - */ - // fail(message: string): void; - - /** - * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. - */ - toBeEmpty(): void; - - /** - * Use .toBeOneOf when checking if a value is a member of a given Array. - * @param {Array.<*>} members - */ - toBeOneOf(members: any[]): void; - - /** - * Use `.toBeNil` when checking a value is `null` or `undefined`. - */ - toBeNil(): void; - - /** - * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. - * @param {Function} predicate - */ - toSatisfy(predicate: (n: any) => boolean): void; - - /** - * Use `.toBeArray` when checking if a value is an `Array`. - */ - toBeArray(): void; - - /** - * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. - * @param {Number} x - */ - toBeArrayOfSize(x: number): void; - - /** - * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. - * @param {Array.<*>} members - */ - toIncludeAllMembers(members: any[]): void; - - /** - * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. - * @param {Array.<*>} members - */ - toIncludeAnyMembers(members: any[]): void; - - /** - * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. - * @param {Function} predicate - */ - toSatisfyAll(predicate: (n: any) => boolean): void; - - /** - * Use `.toBeBoolean` when checking if a value is a `Boolean`. - */ - toBeBoolean(): void; - - /** - * Use `.toBeTrue` when checking a value is equal (===) to `true`. - */ - toBeTrue(): void; - - /** - * Use `.toBeFalse` when checking a value is equal (===) to `false`. - */ - toBeFalse(): void; - - /** - * Use .toBeDate when checking if a value is a Date. - */ - toBeDate(): void; - - /** - * Use `.toBeFunction` when checking if a value is a `Function`. - */ - toBeFunction(): void; - - /** - * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. - * - * Note: Required Jest version >22 - * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same - * - * @param {Mock} mock - */ - toHaveBeenCalledBefore(mock: JestMockFn<any, any>): void; - - /** - * Use `.toBeNumber` when checking if a value is a `Number`. - */ - toBeNumber(): void; - - /** - * Use `.toBeNaN` when checking a value is `NaN`. - */ - toBeNaN(): void; - - /** - * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. - */ - toBeFinite(): void; - - /** - * Use `.toBePositive` when checking if a value is a positive `Number`. - */ - toBePositive(): void; - - /** - * Use `.toBeNegative` when checking if a value is a negative `Number`. - */ - toBeNegative(): void; - - /** - * Use `.toBeEven` when checking if a value is an even `Number`. - */ - toBeEven(): void; - - /** - * Use `.toBeOdd` when checking if a value is an odd `Number`. - */ - toBeOdd(): void; - - /** - * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). - * - * @param {Number} start - * @param {Number} end - */ - toBeWithin(start: number, end: number): void; - - /** - * Use `.toBeObject` when checking if a value is an `Object`. - */ - toBeObject(): void; - - /** - * Use `.toContainKey` when checking if an object contains the provided key. - * - * @param {String} key - */ - toContainKey(key: string): void; - - /** - * Use `.toContainKeys` when checking if an object has all of the provided keys. - * - * @param {Array.<String>} keys - */ - toContainKeys(keys: string[]): void; - - /** - * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. - * - * @param {Array.<String>} keys - */ - toContainAllKeys(keys: string[]): void; - - /** - * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. - * - * @param {Array.<String>} keys - */ - toContainAnyKeys(keys: string[]): void; - - /** - * Use `.toContainValue` when checking if an object contains the provided value. - * - * @param {*} value - */ - toContainValue(value: any): void; - - /** - * Use `.toContainValues` when checking if an object contains all of the provided values. - * - * @param {Array.<*>} values - */ - toContainValues(values: any[]): void; - - /** - * Use `.toContainAllValues` when checking if an object only contains all of the provided values. - * - * @param {Array.<*>} values - */ - toContainAllValues(values: any[]): void; - - /** - * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. - * - * @param {Array.<*>} values - */ - toContainAnyValues(values: any[]): void; - - /** - * Use `.toContainEntry` when checking if an object contains the provided entry. - * - * @param {Array.<String, String>} entry - */ - toContainEntry(entry: [string, string]): void; - - /** - * Use `.toContainEntries` when checking if an object contains all of the provided entries. - * - * @param {Array.<Array.<String, String>>} entries - */ - toContainEntries(entries: [string, string][]): void; - - /** - * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. - * - * @param {Array.<Array.<String, String>>} entries - */ - toContainAllEntries(entries: [string, string][]): void; - - /** - * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. - * - * @param {Array.<Array.<String, String>>} entries - */ - toContainAnyEntries(entries: [string, string][]): void; - - /** - * Use `.toBeExtensible` when checking if an object is extensible. - */ - toBeExtensible(): void; - - /** - * Use `.toBeFrozen` when checking if an object is frozen. - */ - toBeFrozen(): void; - - /** - * Use `.toBeSealed` when checking if an object is sealed. - */ - toBeSealed(): void; - - /** - * Use `.toBeString` when checking if a value is a `String`. - */ - toBeString(): void; - - /** - * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. - * - * @param {String} string - */ - toEqualCaseInsensitive(string: string): void; - - /** - * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. - * - * @param {String} prefix - */ - toStartWith(prefix: string): void; - - /** - * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. - * - * @param {String} suffix - */ - toEndWith(suffix: string): void; - - /** - * Use `.toInclude` when checking if a `String` includes the given `String` substring. - * - * @param {String} substring - */ - toInclude(substring: string): void; - - /** - * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. - * - * @param {String} substring - * @param {Number} times - */ - toIncludeRepeated(substring: string, times: number): void; - - /** - * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. - * - * @param {Array.<String>} substring - */ - toIncludeMultiple(substring: string[]): void; -}; - -interface JestExpectType { - not: - & JestExpectType - & EnzymeMatchersType - & DomTestingLibraryType - & JestJQueryMatchersType - & JestStyledComponentsMatchersType - & JestExtendedMatchersType, - /** - * If you have a mock function, you can use .lastCalledWith to test what - * arguments it was last called with. - */ - lastCalledWith(...args: Array<any>): void, - /** - * toBe just checks that a value is what you expect. It uses === to check - * strict equality. - */ - toBe(value: any): void, - /** - * Use .toBeCalledWith to ensure that a mock function was called with - * specific arguments. - */ - toBeCalledWith(...args: Array<any>): void, - /** - * Using exact equality with floating point numbers is a bad idea. Rounding - * means that intuitive things fail. - */ - toBeCloseTo(num: number, delta: any): void, - /** - * Use .toBeDefined to check that a variable is not undefined. - */ - toBeDefined(): void, - /** - * Use .toBeFalsy when you don't care what a value is, you just want to - * ensure a value is false in a boolean context. - */ - toBeFalsy(): void, - /** - * To compare floating point numbers, you can use toBeGreaterThan. - */ - toBeGreaterThan(number: number): void, - /** - * To compare floating point numbers, you can use toBeGreaterThanOrEqual. - */ - toBeGreaterThanOrEqual(number: number): void, - /** - * To compare floating point numbers, you can use toBeLessThan. - */ - toBeLessThan(number: number): void, - /** - * To compare floating point numbers, you can use toBeLessThanOrEqual. - */ - toBeLessThanOrEqual(number: number): void, - /** - * Use .toBeInstanceOf(Class) to check that an object is an instance of a - * class. - */ - toBeInstanceOf(cls: Class<*>): void, - /** - * .toBeNull() is the same as .toBe(null) but the error messages are a bit - * nicer. - */ - toBeNull(): void, - /** - * Use .toBeTruthy when you don't care what a value is, you just want to - * ensure a value is true in a boolean context. - */ - toBeTruthy(): void, - /** - * Use .toBeUndefined to check that a variable is undefined. - */ - toBeUndefined(): void, - /** - * Use .toContain when you want to check that an item is in a list. For - * testing the items in the list, this uses ===, a strict equality check. - */ - toContain(item: any): void, - /** - * Use .toContainEqual when you want to check that an item is in a list. For - * testing the items in the list, this matcher recursively checks the - * equality of all fields, rather than checking for object identity. - */ - toContainEqual(item: any): void, - /** - * Use .toEqual when you want to check that two objects have the same value. - * This matcher recursively checks the equality of all fields, rather than - * checking for object identity. - */ - toEqual(value: any): void, - /** - * Use .toHaveBeenCalled to ensure that a mock function got called. - */ - toHaveBeenCalled(): void, - toBeCalled(): void; - /** - * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact - * number of times. - */ - toHaveBeenCalledTimes(number: number): void, - toBeCalledTimes(number: number): void; - /** - * - */ - toHaveBeenNthCalledWith(nthCall: number, ...args: Array<any>): void; - nthCalledWith(nthCall: number, ...args: Array<any>): void; - /** - * - */ - toHaveReturned(): void; - toReturn(): void; - /** - * - */ - toHaveReturnedTimes(number: number): void; - toReturnTimes(number: number): void; - /** - * - */ - toHaveReturnedWith(value: any): void; - toReturnWith(value: any): void; - /** - * - */ - toHaveLastReturnedWith(value: any): void; - lastReturnedWith(value: any): void; - /** - * - */ - toHaveNthReturnedWith(nthCall: number, value: any): void; - nthReturnedWith(nthCall: number, value: any): void; - /** - * Use .toHaveBeenCalledWith to ensure that a mock function was called with - * specific arguments. - */ - toHaveBeenCalledWith(...args: Array<any>): void, - toBeCalledWith(...args: Array<any>): void, - /** - * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called - * with specific arguments. - */ - toHaveBeenLastCalledWith(...args: Array<any>): void, - lastCalledWith(...args: Array<any>): void, - /** - * Check that an object has a .length property and it is set to a certain - * numeric value. - */ - toHaveLength(number: number): void, - /** - * - */ - toHaveProperty(propPath: string, value?: any): void, - /** - * Use .toMatch to check that a string matches a regular expression or string. - */ - toMatch(regexpOrString: RegExp | string): void, - /** - * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. - */ - toMatchObject(object: Object | Array<Object>): void, - /** - * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. - */ - toStrictEqual(value: any): void, - /** - * This ensures that an Object matches the most recent snapshot. - */ - toMatchSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, name?: string): void, - /** - * This ensures that an Object matches the most recent snapshot. - */ - toMatchSnapshot(name: string): void, - - toMatchInlineSnapshot(snapshot?: string): void, - toMatchInlineSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, snapshot?: string): void, - /** - * Use .toThrow to test that a function throws when it is called. - * If you want to test that a specific error gets thrown, you can provide an - * argument to toThrow. The argument can be a string for the error message, - * a class for the error, or a regex that should match the error. - * - * Alias: .toThrowError - */ - toThrow(message?: string | Error | Class<Error> | RegExp): void, - toThrowError(message?: string | Error | Class<Error> | RegExp): void, - /** - * Use .toThrowErrorMatchingSnapshot to test that a function throws a error - * matching the most recent snapshot when it is called. - */ - toThrowErrorMatchingSnapshot(): void, - toThrowErrorMatchingInlineSnapshot(snapshot?: string): void, -} - -type JestObjectType = { - /** - * Disables automatic mocking in the module loader. - * - * After this method is called, all `require()`s will return the real - * versions of each module (rather than a mocked version). - */ - disableAutomock(): JestObjectType, - /** - * An un-hoisted version of disableAutomock - */ - autoMockOff(): JestObjectType, - /** - * Enables automatic mocking in the module loader. - */ - enableAutomock(): JestObjectType, - /** - * An un-hoisted version of enableAutomock - */ - autoMockOn(): JestObjectType, - /** - * Clears the mock.calls and mock.instances properties of all mocks. - * Equivalent to calling .mockClear() on every mocked function. - */ - clearAllMocks(): JestObjectType, - /** - * Resets the state of all mocks. Equivalent to calling .mockReset() on every - * mocked function. - */ - resetAllMocks(): JestObjectType, - /** - * Restores all mocks back to their original value. - */ - restoreAllMocks(): JestObjectType, - /** - * Removes any pending timers from the timer system. - */ - clearAllTimers(): void, - /** - * The same as `mock` but not moved to the top of the expectation by - * babel-jest. - */ - doMock(moduleName: string, moduleFactory?: any): JestObjectType, - /** - * The same as `unmock` but not moved to the top of the expectation by - * babel-jest. - */ - dontMock(moduleName: string): JestObjectType, - /** - * Returns a new, unused mock function. Optionally takes a mock - * implementation. - */ - fn<TArguments: $ReadOnlyArray<*>, TReturn>( - implementation?: (...args: TArguments) => TReturn - ): JestMockFn<TArguments, TReturn>, - /** - * Determines if the given function is a mocked function. - */ - isMockFunction(fn: Function): boolean, - /** - * Given the name of a module, use the automatic mocking system to generate a - * mocked version of the module for you. - */ - genMockFromModule(moduleName: string): any, - /** - * Mocks a module with an auto-mocked version when it is being required. - * - * The second argument can be used to specify an explicit module factory that - * is being run instead of using Jest's automocking feature. - * - * The third argument can be used to create virtual mocks -- mocks of modules - * that don't exist anywhere in the system. - */ - mock( - moduleName: string, - moduleFactory?: any, - options?: Object - ): JestObjectType, - /** - * Returns the actual module instead of a mock, bypassing all checks on - * whether the module should receive a mock implementation or not. - */ - requireActual(moduleName: string): any, - /** - * Returns a mock module instead of the actual module, bypassing all checks - * on whether the module should be required normally or not. - */ - requireMock(moduleName: string): any, - /** - * Resets the module registry - the cache of all required modules. This is - * useful to isolate modules where local state might conflict between tests. - */ - resetModules(): JestObjectType, - /** - * Exhausts the micro-task queue (usually interfaced in node via - * process.nextTick). - */ - runAllTicks(): void, - /** - * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), - * setInterval(), and setImmediate()). - */ - runAllTimers(): void, - /** - * Exhausts all tasks queued by setImmediate(). - */ - runAllImmediates(): void, - /** - * Executes only the macro task queue (i.e. all tasks queued by setTimeout() - * or setInterval() and setImmediate()). - */ - advanceTimersByTime(msToRun: number): void, - /** - * Executes only the macro task queue (i.e. all tasks queued by setTimeout() - * or setInterval() and setImmediate()). - * - * Renamed to `advanceTimersByTime`. - */ - runTimersToTime(msToRun: number): void, - /** - * Executes only the macro-tasks that are currently pending (i.e., only the - * tasks that have been queued by setTimeout() or setInterval() up to this - * point) - */ - runOnlyPendingTimers(): void, - /** - * Explicitly supplies the mock object that the module system should return - * for the specified module. Note: It is recommended to use jest.mock() - * instead. - */ - setMock(moduleName: string, moduleExports: any): JestObjectType, - /** - * Indicates that the module system should never return a mocked version of - * the specified module from require() (e.g. that it should always return the - * real module). - */ - unmock(moduleName: string): JestObjectType, - /** - * Instructs Jest to use fake versions of the standard timer functions - * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, - * setImmediate and clearImmediate). - */ - useFakeTimers(): JestObjectType, - /** - * Instructs Jest to use the real versions of the standard timer functions. - */ - useRealTimers(): JestObjectType, - /** - * Creates a mock function similar to jest.fn but also tracks calls to - * object[methodName]. - */ - spyOn(object: Object, methodName: string, accessType?: "get" | "set"): JestMockFn<any, any>, - /** - * Set the default timeout interval for tests and before/after hooks in milliseconds. - * Note: The default timeout interval is 5 seconds if this method is not called. - */ - setTimeout(timeout: number): JestObjectType -}; - -type JestSpyType = { - calls: JestCallsType -}; - -/** Runs this function after every test inside this context */ -declare function afterEach( - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; -/** Runs this function before every test inside this context */ -declare function beforeEach( - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; -/** Runs this function after all tests have finished inside this context */ -declare function afterAll( - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; -/** Runs this function before any tests have started inside this context */ -declare function beforeAll( - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; - -/** A context for grouping tests together */ -declare var describe: { - /** - * Creates a block that groups together several related tests in one "test suite" - */ - (name: JestTestName, fn: () => void): void, - - /** - * Only run this describe block - */ - only(name: JestTestName, fn: () => void): void, - - /** - * Skip running this describe block - */ - skip(name: JestTestName, fn: () => void): void -}; - -/** An individual test unit */ -declare var it: { - /** - * An individual test unit - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - ( - name: JestTestName, - fn?: (done: () => void) => ?Promise<mixed>, - timeout?: number - ): void, - /** - * each runs this test against array of argument arrays per each run - * - * @param {table} table of Test - */ - each( - table: Array<Array<mixed>> - ): ( - name: JestTestName, - fn?: (...args: Array<any>) => ?Promise<mixed> - ) => void, - /** - * Only run this test - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - only( - name: JestTestName, - fn?: (done: () => void) => ?Promise<mixed>, - timeout?: number - ): { - each( - table: Array<Array<mixed>> - ): ( - name: JestTestName, - fn?: (...args: Array<any>) => ?Promise<mixed> - ) => void, - }, - /** - * Skip running this test - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - skip( - name: JestTestName, - fn?: (done: () => void) => ?Promise<mixed>, - timeout?: number - ): void, - /** - * Run the test concurrently - * - * @param {JestTestName} Name of Test - * @param {Function} Test - * @param {number} Timeout for the test, in milliseconds. - */ - concurrent( - name: JestTestName, - fn?: (done: () => void) => ?Promise<mixed>, - timeout?: number - ): void -}; -declare function fit( - name: JestTestName, - fn: (done: () => void) => ?Promise<mixed>, - timeout?: number -): void; -/** An individual test unit */ -declare var test: typeof it; -/** A disabled group of tests */ -declare var xdescribe: typeof describe; -/** A focused group of tests */ -declare var fdescribe: typeof describe; -/** A disabled individual test */ -declare var xit: typeof it; -/** A disabled individual test */ -declare var xtest: typeof it; - -type JestPrettyFormatColors = { - comment: { close: string, open: string }, - content: { close: string, open: string }, - prop: { close: string, open: string }, - tag: { close: string, open: string }, - value: { close: string, open: string }, -}; - -type JestPrettyFormatIndent = string => string; -type JestPrettyFormatRefs = Array<any>; -type JestPrettyFormatPrint = any => string; -type JestPrettyFormatStringOrNull = string | null; - -type JestPrettyFormatOptions = {| - callToJSON: boolean, - edgeSpacing: string, - escapeRegex: boolean, - highlight: boolean, - indent: number, - maxDepth: number, - min: boolean, - plugins: JestPrettyFormatPlugins, - printFunctionName: boolean, - spacing: string, - theme: {| - comment: string, - content: string, - prop: string, - tag: string, - value: string, - |}, -|}; - -type JestPrettyFormatPlugin = { - print: ( - val: any, - serialize: JestPrettyFormatPrint, - indent: JestPrettyFormatIndent, - opts: JestPrettyFormatOptions, - colors: JestPrettyFormatColors, - ) => string, - test: any => boolean, -}; - -type JestPrettyFormatPlugins = Array<JestPrettyFormatPlugin>; - -/** The expect function is used every time you want to test a value */ -declare var expect: { - /** The object that you want to make assertions against */ - (value: any): - & JestExpectType - & JestPromiseType - & EnzymeMatchersType - & DomTestingLibraryType - & JestJQueryMatchersType - & JestStyledComponentsMatchersType - & JestExtendedMatchersType, - - /** Add additional Jasmine matchers to Jest's roster */ - extend(matchers: { [name: string]: JestMatcher }): void, - /** Add a module that formats application-specific data structures. */ - addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, - assertions(expectedAssertions: number): void, - hasAssertions(): void, - any(value: mixed): JestAsymmetricEqualityType, - anything(): any, - arrayContaining(value: Array<mixed>): Array<mixed>, - objectContaining(value: Object): Object, - /** Matches any received string that contains the exact expected string. */ - stringContaining(value: string): string, - stringMatching(value: string | RegExp): string, - not: { - arrayContaining: (value: $ReadOnlyArray<mixed>) => Array<mixed>, - objectContaining: (value: {}) => Object, - stringContaining: (value: string) => string, - stringMatching: (value: string | RegExp) => string, - }, -}; - -// TODO handle return type -// http://jasmine.github.io/2.4/introduction.html#section-Spies -declare function spyOn(value: mixed, method: string): Object; - -/** Holds all functions related to manipulating test runner */ -declare var jest: JestObjectType; - -/** - * The global Jasmine object, this is generally not exposed as the public API, - * using features inside here could break in later versions of Jest. - */ -declare var jasmine: { - DEFAULT_TIMEOUT_INTERVAL: number, - any(value: mixed): JestAsymmetricEqualityType, - anything(): any, - arrayContaining(value: Array<mixed>): Array<mixed>, - clock(): JestClockType, - createSpy(name: string): JestSpyType, - createSpyObj( - baseName: string, - methodNames: Array<string> - ): { [methodName: string]: JestSpyType }, - objectContaining(value: Object): Object, - stringMatching(value: string): string -}; diff --git a/scm-plugins/scm-svn-plugin/flow-typed/npm/moment_v2.3.x.js b/scm-plugins/scm-svn-plugin/flow-typed/npm/moment_v2.3.x.js deleted file mode 100644 index c2786e87fd..0000000000 --- a/scm-plugins/scm-svn-plugin/flow-typed/npm/moment_v2.3.x.js +++ /dev/null @@ -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>; -} diff --git a/scm-plugins/scm-svn-plugin/flow-typed/npm/react-jss_vx.x.x.js b/scm-plugins/scm-svn-plugin/flow-typed/npm/react-jss_vx.x.x.js deleted file mode 100644 index cf8abae155..0000000000 --- a/scm-plugins/scm-svn-plugin/flow-typed/npm/react-jss_vx.x.x.js +++ /dev/null @@ -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'>; -} diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index b05332e6ca..41f1c88a18 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -6,9 +6,9 @@ "build": "ui-bundler plugin" }, "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" } } diff --git a/scm-plugins/scm-svn-plugin/pom.xml b/scm-plugins/scm-svn-plugin/pom.xml index 6d99ff713c..4386efde5b 100644 --- a/scm-plugins/scm-svn-plugin/pom.xml +++ b/scm-plugins/scm-svn-plugin/pom.xml @@ -2,7 +2,7 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> - + <parent> <artifactId>scm-plugins</artifactId> <groupId>sonia.scm.plugins</groupId> @@ -28,7 +28,7 @@ </exclusion> </exclusions> </dependency> - + <dependency> <groupId>sonia.svnkit</groupId> <artifactId>svnkit-dav</artifactId> @@ -36,12 +36,24 @@ </dependency> </dependencies> - - <!-- create test jar --> - + <build> <plugins> - + + <plugin> + <groupId>sonia.scm.maven</groupId> + <artifactId>smp-maven-plugin</artifactId> + <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> @@ -55,51 +67,23 @@ </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> <repositories> - + <repository> <id>maven.tmatesoft.com</id> <name>tmatesoft release repository</name> <url>https://maven.tmatesoft.com/content/repositories/releases</url> </repository> - + <repository> <id>maven.scm-manager.org</id> <name>scm-manager release repository</name> <url>http://maven.scm-manager.org/nexus/content/groups/public</url> </repository> - + </repositories> </project> diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/rest/resources/SvnConfigResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/rest/resources/SvnConfigResource.java deleted file mode 100644 index a1266ab4a4..0000000000 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/rest/resources/SvnConfigResource.java +++ /dev/null @@ -1,125 +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.RepositoryManager; -import sonia.scm.repository.SvnConfig; -import sonia.scm.repository.SvnRepositoryHandler; - -//~--- 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/svn") -@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) -public class SvnConfigResource -{ - - /** - * Constructs ... - * - * - * @param repositoryManager - */ - @Inject - public SvnConfigResource(RepositoryManager repositoryManager) - { - repositoryHandler = (SvnRepositoryHandler) repositoryManager.getHandler( - SvnRepositoryHandler.TYPE_NAME); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @GET - public SvnConfig getConfig() - { - SvnConfig config = repositoryHandler.getConfig(); - - if (config == null) - { - config = new SvnConfig(); - 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, SvnConfig config) - { - repositoryHandler.setConfig(config); - repositoryHandler.storeConfig(); - - return Response.created(uriInfo.getRequestUri()).build(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private SvnRepositoryHandler repositoryHandler; -} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index 64a11034b6..35fd59f715 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -56,6 +56,8 @@ import sonia.scm.util.Util; import java.io.File; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + //~--- JDK imports ------------------------------------------------------------ /** @@ -173,7 +175,8 @@ public class SvnRepositoryHandler } catch (SVNException ex) { - throw new InternalRepositoryException(ex); + logger.error("could not create svn repository", ex); + throw new InternalRepositoryException(entity(repository), "could not create repository", ex); } finally { diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java index 480026c27a..b4be68a51f 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java @@ -59,6 +59,9 @@ import java.io.PrintWriter; import java.util.List; import java.util.Map; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + //~--- JDK imports ------------------------------------------------------------ /** @@ -102,7 +105,7 @@ public final class SvnUtil //~--- methods -------------------------------------------------------------- - public static long parseRevision(String v) throws RevisionNotFoundException { + public static long parseRevision(String v, Repository repository) { long result = -1l; if (!Strings.isNullOrEmpty(v)) @@ -113,7 +116,7 @@ public final class SvnUtil } catch (NumberFormatException ex) { - throw new RevisionNotFoundException(v); + throw notFound(entity("Revision", v).in(repository)); } } @@ -339,7 +342,7 @@ public final class SvnUtil } } - public static long getRevisionNumber(String revision) throws RevisionNotFoundException { + public static long getRevisionNumber(String revision, Repository repository) { // REVIEW Bei SVN wird ohne Revision die -1 genommen, was zu einem Fehler führt long revisionNumber = -1; @@ -351,7 +354,7 @@ public final class SvnUtil } catch (NumberFormatException ex) { - throw new RevisionNotFoundException(revision); + throw notFound(entity("Revision", revision).in(repository)); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java new file mode 100644 index 0000000000..70615698b8 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SCMSvnDiffGenerator.java @@ -0,0 +1,1256 @@ +package sonia.scm.repository.spi; + +import de.regnis.q.sequence.line.diff.QDiffGenerator; +import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory; +import de.regnis.q.sequence.line.diff.QDiffManager; +import de.regnis.q.sequence.line.diff.QDiffUniGenerator; +import org.tmatesoft.svn.core.SVNErrorCode; +import org.tmatesoft.svn.core.SVNErrorMessage; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNMergeRangeList; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.SVNPropertyValue; +import org.tmatesoft.svn.core.internal.util.SVNHashMap; +import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil; +import org.tmatesoft.svn.core.internal.util.SVNPathUtil; +import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback; +import org.tmatesoft.svn.core.internal.wc.SVNErrorManager; +import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; +import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNDiffOptions; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import org.tmatesoft.svn.util.SVNLogType; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * This is a copy of the SvnDiffGenerator class from the patched SVNKit library used in SCM-Manager + * (a fork of SVNKit from TMate Software (http://svnkit.com/)). + * + * The original class can be found here: https://bitbucket.org/sdorra/svnkit/src/default/svnkit/src/main/java/org/tmatesoft/svn/core/internal/wc2/ng/SvnDiffGenerator.java + * + * The folowing modifications are applied when using the git format + * <ul> + * <li> + * remove the svn header + * </li> + * <li> + * use the git diff code 100644 on new file and deleted file actions + * </li> + * <li> + * remove the labels in the added and deleted file headers, eg. <code>[+++ a/a.txt (revision 4)]</code> is + * replaced with <code>[+++ a/a.txt]</code> + * </li> + * </ul> + */ +@SuppressWarnings("all") +public class SCMSvnDiffGenerator implements ISvnDiffGenerator { + + protected static final String WC_REVISION_LABEL = "(working copy)"; + protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________"; + protected static final String HEADER_SEPARATOR = "==================================================================="; + protected static final String HEADER_ENCODING = "UTF-8"; + + private SvnTarget originalTarget1; + private SvnTarget originalTarget2; + private SvnTarget baseTarget; + private SvnTarget relativeToTarget; + private SvnTarget repositoryRoot; + private String encoding; + private byte[] eol; + private boolean useGitFormat; + private boolean forcedBinaryDiff; + + private boolean diffDeleted; + private boolean diffAdded; + private List<String> rawDiffOptions; + private boolean forceEmpty; + + private Set<String> visitedPaths; + private String externalDiffCommand; + private SVNDiffOptions diffOptions; + private boolean fallbackToAbsolutePath; + private ISVNOptions options; + private boolean propertiesOnly; + private boolean ignoreProperties; + + private String getDisplayPath(SvnTarget target) { + String relativePath; + if (baseTarget == null) { + relativePath = null; + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = baseTarget.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) { + String relativePath; + if (repositoryRoot == null) { + relativePath = null; + } else { + if (repositoryRoot.isFile() == target.isFile()) { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = repositoryRoot.getPathOrUrlDecodedString(); + relativePath = getRelativePath(targetString, baseTargetString); + } else { + String targetString = target.getPathOrUrlDecodedString(); + String baseTargetString = new File("").getAbsolutePath(); + relativePath = getRelativePath(targetString, baseTargetString); + } + } + + return relativePath != null ? relativePath : target.getPathOrUrlString(); + } + + private String getRelativePath(String targetString, String baseTargetString) { + if (targetString != null) { + targetString = targetString.replace(File.separatorChar, '/'); + } + if (baseTargetString != null) { + baseTargetString = baseTargetString.replace(File.separatorChar, '/'); + } + + final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString); + if (pathAsChild != null) { + return pathAsChild; + } + if (targetString.equals(baseTargetString)) { + return ""; + } + return null; + } + + private String getChildPath(String path, String relativeToPath) { + if (relativeToTarget == null) { + return null; + } + + String relativePath = getRelativePath(path, relativeToPath); + if (relativePath == null) { + return path; + } + + if (relativePath.length() > 0) { + return relativePath; + } + + if (relativeToPath.equals(path)) { + return "."; + } + + return null; + } + + public SCMSvnDiffGenerator() { + this.originalTarget1 = null; + this.originalTarget2 = null; + this.visitedPaths = new HashSet<String>(); + this.diffDeleted = true; + this.diffAdded = true; + } + + public void setBaseTarget(SvnTarget baseTarget) { + this.baseTarget = baseTarget; + } + + public void setUseGitFormat(boolean useGitFormat) { + this.useGitFormat = useGitFormat; + } + + public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) { + this.originalTarget1 = originalTarget1; + this.originalTarget2 = originalTarget2; + } + + public void setRelativeToTarget(SvnTarget relativeToTarget) { + this.relativeToTarget = relativeToTarget; + } + + public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) { + //anchors are not used + } + + public void setRepositoryRoot(SvnTarget repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public void setForceEmpty(boolean forceEmpty) { + this.forceEmpty = forceEmpty; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + public String getGlobalEncoding() { + ISVNOptions options = getOptions(); + + if (options != null && options instanceof DefaultSVNOptions) { + DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options; + return defaultOptions.getGlobalCharset(); + } + return null; + } + + public void setEOL(byte[] eol) { + this.eol = eol; + } + + public byte[] getEOL() { + return eol; + } + + public boolean isForcedBinaryDiff() { + return forcedBinaryDiff; + } + + public void setForcedBinaryDiff(boolean forcedBinaryDiff) { + this.forcedBinaryDiff = forcedBinaryDiff; + } + + public boolean isPropertiesOnly() { + return propertiesOnly; + } + + public void setPropertiesOnly(boolean propertiesOnly) { + this.propertiesOnly = propertiesOnly; + } + + public boolean isIgnoreProperties() { + return ignoreProperties; + } + + public void setIgnoreProperties(boolean ignoreProperties) { + this.ignoreProperties = ignoreProperties; + } + + public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException { + } + + public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isIgnoreProperties()) { + return; + } + if (dirWasAdded && !isDiffAdded()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (displayPath == null || displayPath.length() == 0) { + displayPath = "."; + } + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + boolean showDiffHeader = !visitedPaths.contains(displayPath); + if (showDiffHeader) { + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified); + visitedPaths.add(displayPath); + if (useGitFormat) { + displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + if (shouldStopDisplaying) { + return; + } + +// if (useGitFormat) { +// String copyFromPath = null; +// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified; +// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1); +// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2); +// displayGitDiffHeader(outputStream, operationKind, +// getRelativeToRootPath(target, originalTarget1), +// getRelativeToRootPath(target, originalTarget2), +// copyFromPath); +// } + + if (useGitFormat) { + displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null); + } else { + displayHeaderFields(outputStream, label1, label2); + } + } + + displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream); + + displayPropDiffValues(outputStream, propChanges, originalProps); + } + + private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''", + displayPath, relativeToPath); + SVNErrorManager.error(errorMessage, SVNLogType.CLIENT); + } + + private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + + try { + displayString(outputStream, "--- "); + displayFirstGitLabelPath(outputStream, path1, revision1, operation); + displayEOL(outputStream); + displayString(outputStream, "+++ "); + displaySecondGitLabelPath(outputStream, path2, revision2, operation); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private String adjustRelativeToReposRoot(String targetString) { + if (repositoryRoot != null) { + String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString(); + String relativePath = getRelativePath(targetString, repositoryRootString); + return relativePath == null ? "" : relativePath; + } + return targetString; + } + + private String computeLabel(String targetString, String originalTargetString) { + if (originalTargetString.length() == 0) { + return targetString; + } else if (originalTargetString.charAt(0) == '/') { + return targetString + "\t(..." + originalTargetString + ")"; + } else { + return targetString + "\t(.../" + originalTargetString + ")"; + } + } + + public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException { + if (isPropertiesOnly()) { + return; + } + ensureEncodingAndEOLSet(); + String displayPath = getDisplayPath(target); + + String targetString1 = originalTarget1.getPathOrUrlDecodedString(); + String targetString2 = originalTarget2.getPathOrUrlDecodedString(); + + if (useGitFormat) { + targetString1 = adjustRelativeToReposRoot(targetString1); + targetString2 = adjustRelativeToReposRoot(targetString2); + } + + String newTargetString = displayPath; + String newTargetString1 = targetString1; + String newTargetString2 = targetString2; + + String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2); + int commonLength = commonAncestor == null ? 0 : commonAncestor.length(); + + newTargetString1 = newTargetString1.substring(commonLength); + newTargetString2 = newTargetString2.substring(commonLength); + + newTargetString1 = computeLabel(newTargetString, newTargetString1); + newTargetString2 = computeLabel(newTargetString, newTargetString2); + + if (relativeToTarget != null) { + String relativeToPath = relativeToTarget.getPathOrUrlDecodedString(); + String absolutePath = target.getPathOrUrlDecodedString(); + + String childPath = getChildPath(absolutePath, relativeToPath); + if (childPath == null) { + throwBadRelativePathException(absolutePath, relativeToPath); + } + String childPath1 = getChildPath(newTargetString1, relativeToPath); + if (childPath1 == null) { + throwBadRelativePathException(newTargetString1, relativeToPath); + } + String childPath2 = getChildPath(newTargetString2, relativeToPath); + if (childPath2 == null) { + throwBadRelativePathException(newTargetString2, relativeToPath); + } + + displayPath = childPath; + newTargetString1 = childPath1; + newTargetString2 = childPath2; + } + + String label1 = getLabel(newTargetString1, revision1); + String label2 = getLabel(newTargetString2, revision2); + + boolean leftIsBinary = false; + boolean rightIsBinary = false; + + if (mimeType1 != null) { + leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1); + } + if (mimeType2 != null) { + rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2); + } + + if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) { + boolean shouldStopDisplaying = false; + if (!useGitFormat) { + shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + } else { + String path1 = getRelativeToRootPath(target, originalTarget1); + String path2 = getRelativeToRootPath(target, originalTarget2); + displayGitDiffHeader(outputStream, operation,path1,path2,null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + if (!useGitFormat){ + displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary); + } + + return; + } + + final String diffCommand = getExternalDiffCommand(); + if (diffCommand != null) { + boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation); + if (useGitFormat) { + displayGitDiffHeader(outputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + null); + } + visitedPaths.add(displayPath); + if (shouldStopDisplaying) { + return; + } + + runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2); + } else { + internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2); + } + } + + private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException { + displayCannotDisplayFileMarkedBinary(outputStream); + + if (leftIsBinary && !rightIsBinary) { + displayMimeType(outputStream, mimeType1); + } else if (!leftIsBinary && rightIsBinary) { + displayMimeType(outputStream, mimeType2); + } else if (leftIsBinary && rightIsBinary) { + if (mimeType1.equals(mimeType2)) { + displayMimeType(outputStream, mimeType1); + } else { + displayMimeTypes(outputStream, mimeType1, mimeType2); + } + } + } + + private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException { + String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath); + if (file2 == null && !isDiffDeleted()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + if (file1 == null && !isDiffAdded()) { + try { + displayString(outputStream, header); + } catch (IOException e) { + wrapException(e); + } + visitedPaths.add(displayPath); + return; + } + String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath); + + RandomAccessFile is1 = null; + RandomAccessFile is2 = null; + try { + is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1); + is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2); + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + final String diffHeader; + if (forceEmpty || useGitFormat) { + displayString(outputStream, header); + diffHeader = headerFields; + + visitedPaths.add(displayPath); + } else { + diffHeader = header + headerFields; + } + QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader); + EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream); + QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator); + if (emptyDetectionOutputStream.isSomethingWritten()) { + visitedPaths.add(displayPath); + } + emptyDetectionOutputStream.flush(); + } catch (IOException e) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage()); + SVNErrorManager.error(err, e, SVNLogType.DEFAULT); + } finally { + SVNFileUtil.closeFile(is1); + SVNFileUtil.closeFile(is2); + } + } + + private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (useGitFormat) { + displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath); + } else { + displayHeaderFields(byteArrayOutputStream, label1, label2); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + if (!useGitFormat) { + // display the svn header only if the git format is not required + displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation); + } else { + displayGitDiffHeader(byteArrayOutputStream, operation, + getRelativeToRootPath(target, originalTarget1), + getRelativeToRootPath(target, originalTarget2), + copyFromPath); + } + } catch (SVNException e) { + SVNFileUtil.closeFile(byteArrayOutputStream); + + try { + byteArrayOutputStream.writeTo(byteArrayOutputStream); + } catch (IOException e1) { + } + + throw e; + } + + try { + byteArrayOutputStream.close(); + return byteArrayOutputStream.toString(HEADER_ENCODING); + } catch (IOException e) { + return ""; + } + } + + private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException { + final List<String> args = new ArrayList<String>(); + args.add(diffCommand); + if (rawDiffOptions != null) { + args.addAll(rawDiffOptions); + } else { + Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection(); + args.addAll(svnDiffOptionsCollection); + args.add("-u"); + } + + if (label1 != null) { + args.add("-L"); + args.add(label1); + } + + if (label2 != null) { + args.add("-L"); + args.add(label2); + } + + boolean tmpFile1 = false; + boolean tmpFile2 = false; + if (file1 == null) { + file1 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile1 = true; + } + if (file2 == null) { + file2 = SVNFileUtil.createTempFile("svn.", ".tmp"); + tmpFile2 = true; + } + + String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/'); + String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/'); + + args.add(file1Path); + args.add(file2Path); + try { + final Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + + SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true, + new ISVNReturnValueCallback() { + + public void handleReturnValue(int returnValue) throws SVNException { + if (returnValue != 0 && returnValue != 1) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, + "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)}); + SVNErrorManager.error(err, SVNLogType.DEFAULT); + } + } + + public void handleChar(char ch) throws SVNException { + try { + writer.write(ch); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } + } + + public boolean isHandleProgramOutput() { + return true; + } + }); + + writer.flush(); + } catch (IOException ioe) { + SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage()); + SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT); + } finally { + try { + if (tmpFile1) { + SVNFileUtil.deleteFile(file1); + } + if (tmpFile2) { + SVNFileUtil.deleteFile(file2); + } + } catch (SVNException e) { + // skip + } + } + } + + private String getExternalDiffCommand() { + return externalDiffCommand; + } + + private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = "); + displayString(outputStream, mimeType); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException { + try { + displayString(outputStream, SVNProperty.MIME_TYPE); + displayString(outputStream, " = ("); + displayString(outputStream, mimeType1); + displayString(outputStream, ", "); + displayString(outputStream, mimeType2); + displayString(outputStream, ")"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException { + try { + displayString(outputStream, "Cannot display: file marked as a binary type."); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void ensureEncodingAndEOLSet() { + if (getEOL() == null) { + setEOL(SVNProperty.EOL_LF_BYTES); + } + if (getEncoding() == null) { + final ISVNOptions options = getOptions(); + if (options != null && options.getNativeCharset() != null) { + setEncoding(options.getNativeCharset()); + } else { + setEncoding("UTF-8"); + } + } + } + + private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException { + for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) { + String name = (String) changedPropNames.next(); + SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null; + SVNPropertyValue newValue = diff.getSVNPropertyValue(name); + String headerFormat = null; + + if (originalValue == null) { + headerFormat = "Added: "; + } else if (newValue == null) { + headerFormat = "Deleted: "; + } else { + headerFormat = "Modified: "; + } + + try { + displayString(outputStream, (headerFormat + name)); + displayEOL(outputStream); + if (SVNProperty.MERGE_INFO.equals(name)) { + displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString()); + continue; + } + + byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding()); + byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding()); + + if (originalValueBytes == null) { + originalValueBytes = new byte[0]; + } else { + originalValueBytes = maybeAppendEOL(originalValueBytes); + } + + boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 && + (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] || + newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]); + + if (newValueBytes == null) { + newValueBytes = new byte[0]; + } else { + newValueBytes = maybeAppendEOL(newValueBytes); + } + + QDiffUniGenerator.setup(); + Map properties = new SVNHashMap(); + + properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle())); + properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL())); + properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##"); + if (getDiffOptions().isIgnoreAllWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE); + } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) { + properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE); + } + + QDiffGenerator generator = new QDiffUniGenerator(properties, ""); + Writer writer = new OutputStreamWriter(outputStream, getEncoding()); + QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes), + null, writer, generator); + writer.flush(); + if (!newValueHadEol) { + displayString(outputStream, "\\ No newline at end of property"); + displayEOL(outputStream); + } + } catch (IOException e) { + wrapException(e); + } + } + + } + + private byte[] maybeAppendEOL(byte[] buffer) { + if (buffer.length == 0) { + return buffer; + } + + byte lastByte = buffer[buffer.length - 1]; + if (lastByte == SVNProperty.EOL_CR_BYTES[0]) { + return buffer; + } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) { + final byte[] newBuffer = new byte[buffer.length + getEOL().length]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length); + return newBuffer; + } else { + return buffer; + } + } + + private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("a/" + copyFromPath, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("a/" + path1, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("a/" + copyFromPath, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + return getLabel("/dev/null", revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + return getLabel("b/" + path2, revision); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + return getLabel("b/" + path2, revision); + } + throw new IllegalArgumentException("Unsupported operation: " + operationKind); + } + + private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException { + if (operationKind == SvnDiffCallback.OperationKind.Deleted) { + displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Copied) { + displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Added) { + displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Modified) { + displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath); + } else if (operationKind == SvnDiffCallback.OperationKind.Moved) { + displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath); + } + } + + private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + // 100644 is the mode code used from git for new and deleted file mode + displayString(outputStream, "new file mode 100644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + // 100644 is the mode code used from git for new and deleted file mode + displayString(outputStream, "deleted file mode 100644"); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "copy from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "copy to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, copyFromPath); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + displayString(outputStream, "rename from "); + displayString(outputStream, copyFromPath); + displayEOL(outputStream); + displayString(outputStream, "rename to "); + displayString(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException { + try { + displayString(outputStream, "diff --git "); + displayFirstGitPath(outputStream, path1); + displayString(outputStream, " "); + displaySecondGitPath(outputStream, path2); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException { + displayGitPath(outputStream, path1, "a/"); + } + + private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException { + displayGitPath(outputStream, path2, "b/"); + } + + private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "a/"; + if (operation == SvnDiffCallback.OperationKind.Added) { + path1 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix); + } + + private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException { + String pathPrefix = "b/"; + if (operation == SvnDiffCallback.OperationKind.Deleted) { + path2 = "/dev/null"; + pathPrefix = ""; + } + displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix); + } + + private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix) throws IOException { + displayString(outputStream, pathPrefix); + displayString(outputStream, path1); + } + + private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) { + String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor); + return getLabel(adjustedPath, revision); + } + + private String getAdjustedPath(String displayPath, String path1, String commonAncestor) { + String adjustedPath = getRelativePath(path1, commonAncestor); + + if (adjustedPath == null || adjustedPath.length() == 0) { + adjustedPath = displayPath; + } else if (adjustedPath.charAt(0) == '/') { + adjustedPath = displayPath + "\t(..." + adjustedPath + ")"; + } else { + adjustedPath = displayPath + "\t(.../" + adjustedPath + ")"; + } + return adjustedPath; + //TODO: respect relativeToDir + } + + protected String getLabel(String path, String revToken) { + if (useGitFormat){ + // the label in the git format contains only the path + return path; + } + revToken = revToken == null ? WC_REVISION_LABEL : revToken; + return path + "\t" + revToken; + } + + protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException { + try { + if (deleted && !isDiffDeleted()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (deleted)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + if (added && !isDiffAdded()) { + displayString(os, "Index: "); + displayString(os, path); + displayString(os, " (added)"); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return true; + } + displayString(os, "Index: "); + displayString(os, path); + displayEOL(os); + displayString(os, HEADER_SEPARATOR); + displayEOL(os); + return false; + } catch (IOException e) { + wrapException(e); + } + return false; + } + + protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException { + try { + displayString(os, "--- "); + displayString(os, label1); + displayEOL(os); + displayString(os, "+++ "); + displayString(os, label2); + displayEOL(os); + } catch (IOException e) { + wrapException(e); + } + } + + private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException { + try { + displayEOL(outputStream); + displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path))); + displayEOL(outputStream); + displayString(outputStream, PROPERTIES_SEPARATOR); + displayEOL(outputStream); + } catch (IOException e) { + wrapException(e); + } + } + + private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) { + if (value == null) { + return null; + } + if (value.isString()) { + try { + return value.getString().getBytes(encoding); + } catch (UnsupportedEncodingException e) { + return value.getString().getBytes(); + } + } + return value.getBytes(); + } + + private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException { + Map oldMergeInfo = null; + Map newMergeInfo = null; + if (oldValue != null) { + oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null); + } + if (newValue != null) { + newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null); + } + + Map deleted = new TreeMap(); + Map added = new TreeMap(); + SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true); + + for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path); + displayString(outputStream, (" Reverse-merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + + for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) { + String path = (String) paths.next(); + SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path); + displayString(outputStream, (" Merged " + path + ":r")); + displayString(outputStream, rangeList.toString()); + displayEOL(outputStream); + } + } + + private boolean useLocalFileSeparatorChar() { + return true; + } + + public boolean isDiffDeleted() { + return diffDeleted; + } + + public boolean isDiffAdded() { + return diffAdded; + } + + private void wrapException(IOException e) throws SVNException { + SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e); + SVNErrorManager.error(errorMessage, e, SVNLogType.WC); + } + + private void displayString(OutputStream outputStream, String s) throws IOException { + outputStream.write(s.getBytes(HEADER_ENCODING)); + } + + private void displayEOL(OutputStream os) throws IOException { + os.write(getEOL()); + } + + public SVNDiffOptions getDiffOptions() { + if (diffOptions == null) { + diffOptions = new SVNDiffOptions(); + } + return diffOptions; + } + + public void setExternalDiffCommand(String externalDiffCommand) { + this.externalDiffCommand = externalDiffCommand; + } + + public void setRawDiffOptions(List<String> rawDiffOptions) { + this.rawDiffOptions = rawDiffOptions; + } + + public void setDiffOptions(SVNDiffOptions diffOptions) { + this.diffOptions = diffOptions; + } + + public void setDiffDeleted(boolean diffDeleted) { + this.diffDeleted = diffDeleted; + } + + public void setDiffAdded(boolean diffAdded) { + this.diffAdded = diffAdded; + } + + public void setBasePath(File absoluteFile) { + setBaseTarget(SvnTarget.fromFile(absoluteFile)); + } + + public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) { + this.fallbackToAbsolutePath = fallbackToAbsolutePath; + } + + public void setOptions(ISVNOptions options) { + this.options = options; + } + + public ISVNOptions getOptions() { + return options; + } + + private class EmptyDetectionOutputStream extends OutputStream { + + private final OutputStream outputStream; + private boolean somethingWritten; + + public EmptyDetectionOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + this.somethingWritten = false; + } + + public boolean isSomethingWritten() { + return somethingWritten; + } + + @Override + public void write(int c) throws IOException { + somethingWritten = true; + outputStream.write(c); + } + + @Override + public void write(byte[] bytes) throws IOException { + somethingWritten = bytes.length > 0; + outputStream.write(bytes); + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + somethingWritten = length > 0; + outputStream.write(bytes, offset, length); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBlameCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBlameCommand.java index fe9a72ffce..b589734d03 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBlameCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBlameCommand.java @@ -98,7 +98,7 @@ public class SvnBlameCommand extends AbstractSvnCommand implements BlameCommand } catch (SVNException ex) { - throw new InternalRepositoryException("could not create blame result", ex); + throw new InternalRepositoryException(repository, "could not create blame result", ex); } return new BlameResult(blameLines.size(), blameLines); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 43cc1c3c70..e2f58b593b 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -48,7 +48,6 @@ import org.tmatesoft.svn.core.io.SVNRepository; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.Repository; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.SubRepository; import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; @@ -79,9 +78,9 @@ public class SvnBrowseCommand extends AbstractSvnCommand @Override @SuppressWarnings("unchecked") - public BrowserResult getBrowserResult(BrowseCommandRequest request) throws RevisionNotFoundException { + public BrowserResult getBrowserResult(BrowseCommandRequest request) { String path = Strings.nullToEmpty(request.getPath()); - long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision()); + long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision(), repository); if (logger.isDebugEnabled()) { logger.debug("browser repository {} in path \"{}\" at revision {}", repository.getName(), path, revisionNumber); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java index 4936a16a8c..9ee43b2259 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java @@ -44,9 +44,7 @@ import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.admin.SVNLookClient; import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.PathNotFoundException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.SvnUtil; import java.io.ByteArrayInputStream; @@ -54,6 +52,9 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + //~--- JDK imports ------------------------------------------------------------ /** @@ -79,7 +80,7 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand //~--- get methods ---------------------------------------------------------- @Override - public void getCatResult(CatCommandRequest request, OutputStream output) throws RevisionNotFoundException, PathNotFoundException { + public void getCatResult(CatCommandRequest request, OutputStream output) { if (logger.isDebugEnabled()) { logger.debug("try to get content for {}", request); @@ -96,14 +97,14 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand else { - long revisionNumber = SvnUtil.getRevisionNumber(revision); + long revisionNumber = SvnUtil.getRevisionNumber(revision, repository); getCatFromRevision(request, output, revisionNumber); } } @Override - public InputStream getCatResultStream(CatCommandRequest request) throws RevisionNotFoundException, PathNotFoundException { + public InputStream getCatResultStream(CatCommandRequest request) { // There seems to be no method creating an input stream as a result, so // we have no other possibility then to copy the content into a buffer and // stream it from there. @@ -112,7 +113,7 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand return new ByteArrayInputStream(output.toByteArray()); } - private void getCatFromRevision(CatCommandRequest request, OutputStream output, long revision) throws PathNotFoundException, RevisionNotFoundException { + private void getCatFromRevision(CatCommandRequest request, OutputStream output, long revision) { logger.debug("try to read content from revision {} and path {}", revision, request.getPath()); @@ -129,14 +130,14 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand } } - private void handleSvnException(CatCommandRequest request, SVNException ex) throws PathNotFoundException, RevisionNotFoundException { + private void handleSvnException(CatCommandRequest request, SVNException ex) { int svnErrorCode = ex.getErrorMessage().getErrorCode().getCode(); if (SVNErrorCode.FS_NOT_FOUND.getCode() == svnErrorCode) { - throw new PathNotFoundException(request.getPath()); + throw notFound(entity("Path", request.getPath()).in("Revision", request.getRevision()).in(repository)); } else if (SVNErrorCode.FS_NO_SUCH_REVISION.getCode() == svnErrorCode) { - throw new RevisionNotFoundException(request.getRevision()); + throw notFound(entity("Revision", request.getRevision()).in(repository)); } else { - throw new InternalRepositoryException("could not get content from revision", ex); + throw new InternalRepositoryException(repository, "could not get content from revision", ex); } } @@ -156,7 +157,7 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand } catch (SVNException ex) { - throw new InternalRepositoryException("could not get content from transaction", ex); + throw new InternalRepositoryException(repository, "could not get content from transaction", ex); } finally { diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java index 0b65466a42..4aaa12e28f 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java @@ -1,19 +1,19 @@ -/** +/* * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + * <p> * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * <p> * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * 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. + * 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. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * <p> * 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 @@ -24,13 +24,11 @@ * 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. - * + * <p> * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -41,14 +39,12 @@ import org.slf4j.LoggerFactory; import org.tmatesoft.svn.core.SVNDepth; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; -import org.tmatesoft.svn.core.wc.DefaultSVNDiffGenerator; -import org.tmatesoft.svn.core.wc.ISVNDiffGenerator; +import org.tmatesoft.svn.core.internal.wc2.ng.SvnNewDiffGenerator; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNDiffClient; import org.tmatesoft.svn.core.wc.SVNRevision; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.SvnUtil; import sonia.scm.repository.api.DiffFormat; import sonia.scm.util.Util; @@ -61,8 +57,7 @@ import java.io.OutputStream; * * @author Sebastian Sdorra */ -public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand -{ +public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand { /** * the logger for SvnDiffCommand @@ -70,61 +65,37 @@ public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand private static final Logger logger = LoggerFactory.getLogger(SvnDiffCommand.class); - public SvnDiffCommand(SvnContext context, Repository repository) - { + public SvnDiffCommand(SvnContext context, Repository repository) { super(context, repository); } @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) throws RevisionNotFoundException { - if (logger.isDebugEnabled()) - { - logger.debug("create diff for {}", request); - } - + public void getDiffResult(DiffCommandRequest request, OutputStream output) { + logger.debug("create diff for {}", request); Preconditions.checkNotNull(request, "request is required"); Preconditions.checkNotNull(output, "outputstream is required"); String path = request.getPath(); SVNClientManager clientManager = null; - - try - { + try { SVNURL svnurl = context.createUrl(); - - if (Util.isNotEmpty(path)) - { + if (Util.isNotEmpty(path)) { svnurl = svnurl.appendPath(path, true); } - clientManager = SVNClientManager.newInstance(); - SVNDiffClient diffClient = clientManager.getDiffClient(); - ISVNDiffGenerator diffGenerator = diffClient.getDiffGenerator(); + diffClient.setDiffGenerator(new SvnNewDiffGenerator(new SCMSvnDiffGenerator())); - if (diffGenerator == null) - { - diffGenerator = new DefaultSVNDiffGenerator(); - } - - diffGenerator.setDiffAdded(true); - diffGenerator.setDiffDeleted(true); - diffClient.setDiffGenerator(diffGenerator); - - long currentRev = SvnUtil.getRevisionNumber(request.getRevision()); + long currentRev = SvnUtil.getRevisionNumber(request.getRevision(), repository); diffClient.setGitDiffFormat(request.getFormat() == DiffFormat.GIT); diffClient.doDiff(svnurl, SVNRevision.HEAD, SVNRevision.create(currentRev - 1), SVNRevision.create(currentRev), SVNDepth.INFINITY, false, output); - } - catch (SVNException ex) - { - throw new InternalRepositoryException("could not create diff", ex); - } - finally - { + } catch (SVNException ex) { + throw new InternalRepositoryException(repository, "could not create diff", ex); + } finally { SvnUtil.dispose(clientManager); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java index 332dcb55a6..be102be1bd 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java @@ -47,7 +47,6 @@ import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; @@ -76,7 +75,7 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand @Override @SuppressWarnings("unchecked") - public Changeset getChangeset(String revision) throws RevisionNotFoundException { + public Changeset getChangeset(String revision) { Changeset changeset = null; if (logger.isDebugEnabled()) @@ -86,7 +85,7 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand try { - long revisioNumber = parseRevision(revision); + long revisioNumber = parseRevision(revision, repository); SVNRepository repo = open(); Collection<SVNLogEntry> entries = repo.log(null, null, revisioNumber, revisioNumber, true, true); @@ -98,7 +97,7 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand } catch (SVNException ex) { - throw new InternalRepositoryException("could not open repository", ex); + throw new InternalRepositoryException(repository, "could not open repository", ex); } return changeset; @@ -106,7 +105,7 @@ public class SvnLogCommand extends AbstractSvnCommand 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); @@ -115,8 +114,8 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand ChangesetPagingResult changesets = null; int start = request.getPagingStart(); int limit = request.getPagingLimit(); - long startRevision = parseRevision(request.getStartChangeset()); - long endRevision = parseRevision(request.getEndChangeset()); + long startRevision = parseRevision(request.getStartChangeset(), repository); + long endRevision = parseRevision(request.getEndChangeset(), repository); String[] pathArray = null; if (!Strings.isNullOrEmpty(request.getPath())) @@ -140,7 +139,7 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand } catch (SVNException ex) { - throw new InternalRepositoryException("could not open repository", ex); + throw new InternalRepositoryException(repository, "could not open repository", ex); } return changesets; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java index e6cedc8ebf..4b4f655b12 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java @@ -7,11 +7,9 @@ import org.tmatesoft.svn.core.io.SVNRepository; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Modifications; import sonia.scm.repository.Repository; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; -import java.io.IOException; import java.util.Collection; @Slf4j @@ -24,11 +22,11 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif @Override @SuppressWarnings("unchecked") - public Modifications getModifications(String revision) throws IOException, RevisionNotFoundException { + public Modifications getModifications(String revision) { Modifications modifications = null; log.debug("get modifications {}", revision); try { - long revisionNumber = SvnUtil.parseRevision(revision); + long revisionNumber = SvnUtil.parseRevision(revision, repository); SVNRepository repo = open(); Collection<SVNLogEntry> entries = repo.log(null, null, revisionNumber, revisionNumber, true, true); @@ -36,13 +34,13 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif modifications = SvnUtil.createModifications(entries.iterator().next(), revision); } } catch (SVNException ex) { - throw new InternalRepositoryException("could not open repository", ex); + throw new InternalRepositoryException(repository, "could not open repository", ex); } return modifications; } @Override - public Modifications getModifications(ModificationsCommandRequest request) throws IOException, RevisionNotFoundException { + public Modifications getModifications(ModificationsCommandRequest request) { return getModifications(request.getRevision()); } diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnConfigurationForm.js b/scm-plugins/scm-svn-plugin/src/main/js/SvnConfigurationForm.js new file mode 100644 index 0000000000..9470550ef2 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnConfigurationForm.js @@ -0,0 +1,107 @@ +//@flow +import React from "react"; +import type { Links } from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import { InputField, Checkbox, Select } from "@scm-manager/ui-components"; + +type Configuration = { + repositoryDirectory: string, + compatibility: string, + enabledGZip: boolean, + disabled: boolean, + _links: Links +}; + +type Props = { + initialConfiguration: Configuration, + readOnly: boolean, + + onConfigurationChange: (Configuration, boolean) => void, + + // context props + t: (string) => string +} + +type State = Configuration; + +class HgConfigurationForm extends React.Component<Props, State> { + + constructor(props: Props) { + super(props); + this.state = { ...props.initialConfiguration, validationErrors: [] }; + } + + isValid = () => { + return !!this.state.repositoryDirectory; + }; + + handleChange = (value: any, name: string) => { + this.setState({ + [name]: value + }, () => this.props.onConfigurationChange(this.state, this.isValid())); + }; + + compatibilityOptions = (values: string[]) => { + const options = []; + for (let value of values) { + options.push( this.compatibilityOption(value) ); + } + return options; + }; + + compatibilityOption = (value: string) => { + return { + value, + label: this.props.t("scm-svn-plugin.config.compatibility-values." + value.toLowerCase()) + }; + }; + + render() { + const { readOnly, t } = this.props; + const compatibilityOptions = this.compatibilityOptions([ + "NONE", "PRE14", "PRE15", "PRE16", "PRE17", "WITH17" + ]); + + return ( + <> + <InputField + name="repositoryDirectory" + label={t("scm-svn-plugin.config.directory")} + helpText={t("scm-svn-plugin.config.directoryHelpText")} + value={this.state.repositoryDirectory} + errorMessage={t("scm-svn-plugin.config.required")} + validationError={!this.state.repositoryDirectory} + onChange={this.handleChange} + disabled={readOnly} + /> + <Select + name="compatibility" + label={t("scm-svn-plugin.config.compatibility")} + helpText={t("scm-svn-plugin.config.compatibilityHelpText")} + value={this.state.compatibility} + options={compatibilityOptions} + onChange={this.handleChange} + /> + <Checkbox + name="enabledGZip" + label={t("scm-svn-plugin.config.enabledGZip")} + helpText={t("scm-svn-plugin.config.enabledGZipHelpText")} + checked={this.state.enabledGZip} + onChange={this.handleChange} + disabled={readOnly} + /> + <Checkbox + name="disabled" + label={t("scm-svn-plugin.config.disabled")} + helpText={t("scm-svn-plugin.config.disabledHelpText")} + checked={this.state.disabled} + onChange={this.handleChange} + disabled={readOnly} + /> + </> + ); + } + +} + +export default translate("plugins")(HgConfigurationForm); diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.js b/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.js new file mode 100644 index 0000000000..c17829a67f --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import { Title, GlobalConfiguration } from "@scm-manager/ui-components"; +import SvnConfigurationForm from "./SvnConfigurationForm"; + +type Props = { + link: string, + + // context props + t: (string) => string +} + +class SvnGlobalConfiguration extends React.Component<Props> { + + render() { + const { link, t } = this.props; + return ( + <div> + <Title title={t("scm-svn-plugin.config.title")}/> + <GlobalConfiguration link={link} render={props => <SvnConfigurationForm {...props} />}/> + </div> + ); + } + +} + +export default translate("plugins")(SvnGlobalConfiguration); diff --git a/scm-plugins/scm-svn-plugin/src/main/js/index.js b/scm-plugins/scm-svn-plugin/src/main/js/index.js index 9df7ef1649..dc0c68c395 100644 --- a/scm-plugins/scm-svn-plugin/src/main/js/index.js +++ b/scm-plugins/scm-svn-plugin/src/main/js/index.js @@ -1,7 +1,9 @@ // @flow import { binder } from "@scm-manager/ui-extensions"; +import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components"; import ProtocolInformation from "./ProtocolInformation"; import SvnAvatar from "./SvnAvatar"; +import SvnGlobalConfiguration from "./SvnGlobalConfiguration"; const svnPredicate = (props: Object) => { return props.repository && props.repository.type === "svn"; @@ -9,3 +11,7 @@ const svnPredicate = (props: Object) => { binder.bind("repos.repository-details.information", ProtocolInformation, svnPredicate); binder.bind("repos.repository-avatar", SvnAvatar, svnPredicate); + +// bind global configuration + +cfgBinder.bindGlobal("/svn", "scm-svn-plugin.config.link", "svnConfig", SvnGlobalConfiguration); diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json index 07b34baf10..5181f76941 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json @@ -2,6 +2,27 @@ "scm-svn-plugin": { "information": { "checkout" : "Checkout repository" + }, + "config": { + "link": "Subversion", + "title": "Subversion Configuration", + "directory": "Repository Directory", + "directoryHelpText": "Location of Subversion repositories.", + "compatibility": "Version Compatibility", + "compatibilityHelpText": "Specifies with which subversion version repositories are compatible.", + "compatibility-values": { + "none": "No compatibility", + "pre14": "Pre 1.4 Compatible", + "pre15": "Pre 1.5 Compatible", + "pre16": "Pre 1.6 Compatible", + "pre17": "Pre 1.7 Compatible", + "with17": "With 1.7 Compatible" + }, + "enabledGZip": "Enable GZip Compression", + "enabledGZipHelpText": "Enable GZip compression for svn responses.", + "disabled": "Disabled", + "disabledHelpText": "Enable or disable the Git plugin", + "required": "This configuration value is required" } } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index bcf5d2ec55..d3e6a98558 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -36,7 +36,6 @@ package sonia.scm.repository.spi; import org.junit.Test; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; -import sonia.scm.repository.RevisionNotFoundException; import java.io.IOException; import java.util.Collection; @@ -55,7 +54,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase { @Test - public void testBrowseWithFilePath() throws RevisionNotFoundException { + public void testBrowseWithFilePath() { BrowseCommandRequest request = new BrowseCommandRequest(); request.setPath("a.txt"); FileObject file = createCommand().getBrowserResult(request).getFile(); @@ -65,7 +64,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase } @Test - public void testBrowse() throws RevisionNotFoundException { + public void testBrowse() { Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest()); FileObject a = getFileObject(foList, "a.txt"); @@ -89,7 +88,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase * @throws IOException */ @Test - public void testBrowseSubDirectory() throws RevisionNotFoundException { + public void testBrowseSubDirectory() { BrowseCommandRequest request = new BrowseCommandRequest(); request.setPath("c"); @@ -136,7 +135,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase } @Test - public void testDisableLastCommit() throws RevisionNotFoundException { + public void testDisableLastCommit() { BrowseCommandRequest request = new BrowseCommandRequest(); request.setDisableLastCommit(true); @@ -150,7 +149,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase } @Test - public void testRecursive() throws RevisionNotFoundException { + public void testRecursive() { BrowseCommandRequest request = new BrowseCommandRequest(); request.setRecursive(true); BrowserResult result = createCommand().getBrowserResult(request); @@ -199,7 +198,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase .orElseThrow(() -> new AssertionError("file " + name + " not found")); } - private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException { + private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) { BrowserResult result = createCommand().getBrowserResult(request); assertNotNull(result); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java index 3980b3e558..e899c63b19 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java @@ -32,9 +32,12 @@ package sonia.scm.repository.spi; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Rule; import org.junit.Test; -import sonia.scm.repository.PathNotFoundException; -import sonia.scm.repository.RevisionNotFoundException; +import org.junit.rules.ExpectedException; +import sonia.scm.NotFoundException; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -46,8 +49,11 @@ import static org.junit.Assert.assertEquals; public class SvnCatCommandTest extends AbstractSvnCommandTestBase { + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + @Test - public void testCat() throws PathNotFoundException, RevisionNotFoundException { + public void testCat() { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -56,35 +62,59 @@ public class SvnCatCommandTest extends AbstractSvnCommandTestBase { } @Test - public void testSimpleCat() throws PathNotFoundException, RevisionNotFoundException { + public void testSimpleCat() { CatCommandRequest request = new CatCommandRequest(); request.setPath("c/d.txt"); assertEquals("d", execute(request)); } - @Test(expected = PathNotFoundException.class) - public void testUnknownFile() throws PathNotFoundException, RevisionNotFoundException { + @Test + public void testUnknownFile() { CatCommandRequest request = new CatCommandRequest(); request.setPath("unknown"); request.setRevision("1"); - execute(request); - } + expectedException.expect(new BaseMatcher<Object>() { + @Override + public void describeTo(Description description) { + description.appendText("expected NotFoundException for path"); + } - @Test(expected = RevisionNotFoundException.class) - public void testUnknownRevision() throws PathNotFoundException, RevisionNotFoundException { - CatCommandRequest request = new CatCommandRequest(); - - request.setPath("a.txt"); - request.setRevision("42"); + @Override + public boolean matches(Object item) { + return "Path".equals(((NotFoundException)item).getContext().get(0).getType()); + } + }); execute(request); } @Test - public void testSimpleStream() throws IOException, PathNotFoundException, RevisionNotFoundException { + public void testUnknownRevision() { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("a.txt"); + request.setRevision("42"); + + expectedException.expect(new BaseMatcher<Object>() { + @Override + public void describeTo(Description description) { + description.appendText("expected NotFoundException for revision"); + } + + @Override + public boolean matches(Object item) { + return "Revision".equals(((NotFoundException)item).getContext().get(0).getType()); + } + }); + + execute(request); + } + + @Test + public void testSimpleStream() throws IOException { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); request.setRevision("1"); @@ -98,7 +128,7 @@ public class SvnCatCommandTest extends AbstractSvnCommandTestBase { catResultStream.close(); } - private String execute(CatCommandRequest request) throws PathNotFoundException, RevisionNotFoundException { + private String execute(CatCommandRequest request) { String content = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java index a55138f151..f2511a9ad9 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java @@ -38,7 +38,6 @@ import org.junit.Test; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Modifications; -import sonia.scm.repository.RevisionNotFoundException; import java.io.IOException; @@ -57,7 +56,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase { @Test - public void testGetAll() throws RevisionNotFoundException { + public void testGetAll() { ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); @@ -67,7 +66,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase } @Test - public void testGetAllByPath() throws RevisionNotFoundException { + public void testGetAllByPath() { LogCommandRequest request = new LogCommandRequest(); request.setPath("a.txt"); @@ -83,7 +82,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase } @Test - public void testGetAllWithLimit() throws RevisionNotFoundException { + public void testGetAllWithLimit() { LogCommandRequest request = new LogCommandRequest(); request.setPagingLimit(2); @@ -106,7 +105,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase } @Test - public void testGetAllWithPaging() throws RevisionNotFoundException { + public void testGetAllWithPaging() { LogCommandRequest request = new LogCommandRequest(); request.setPagingStart(1); @@ -130,7 +129,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase } @Test - public void testGetCommit() throws RevisionNotFoundException, IOException { + public void testGetCommit() { Changeset c = createCommand().getChangeset("3"); assertNotNull(c); @@ -151,7 +150,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase } @Test - public void testGetRange() throws RevisionNotFoundException { + public void testGetRange() { LogCommandRequest request = new LogCommandRequest(); request.setStartChangeset("2"); diff --git a/scm-plugins/scm-svn-plugin/yarn.lock b/scm-plugins/scm-svn-plugin/yarn.lock index c3e8cc476f..a211aa0ca1 100644 --- a/scm-plugins/scm-svn-plugin/yarn.lock +++ b/scm-plugins/scm-svn-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb" +"@scm-manager/ui-bundler@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -681,9 +681,9 @@ vinyl-source-stream "^2.0.0" watchify "^3.11.0" -"@scm-manager/ui-extensions@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e" +"@scm-manager/ui-extensions@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45" dependencies: react "^16.4.2" react-dom "^16.4.2" diff --git a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java index 40b13243f7..07c6cd25ba 100644 --- a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java +++ b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java @@ -70,10 +70,10 @@ public class DummyRepositoryHandler @Override - protected void create(Repository repository, File directory) throws AlreadyExistsException { + protected void create(Repository repository, File directory) { String key = repository.getNamespace() + "/" + repository.getName(); if (existingRepoNames.contains(key)) { - throw new AlreadyExistsException(); + throw new AlreadyExistsException(repository); } else { existingRepoNames.add(key); } diff --git a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java index 23f0e33987..0118b88743 100644 --- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java @@ -35,7 +35,6 @@ package sonia.scm.repository; import org.junit.Test; import sonia.scm.AbstractTestBase; -import sonia.scm.AlreadyExistsException; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.InMemoryConfigurationStoreFactory; import sonia.scm.util.IOUtil; @@ -62,12 +61,12 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { ConfigurationStoreFactory factory, File directory) throws IOException, RepositoryPathNotFoundException; @Test - public void testCreate() throws AlreadyExistsException { + public void testCreate() { createRepository(); } @Test - public void testCreateResourcePath() throws AlreadyExistsException { + public void testCreateResourcePath() { Repository repository = createRepository(); String path = handler.createResourcePath(repository); @@ -77,7 +76,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { } @Test - public void testDelete() throws Exception { + public void testDelete() { Repository repository = createRepository(); handler.delete(repository); @@ -102,7 +101,7 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase { } } - private Repository createRepository() throws AlreadyExistsException { + private Repository createRepository() { Repository repository = RepositoryTestData.createHeartOfGold(); handler.create(repository); diff --git a/scm-test/src/main/java/sonia/scm/user/UserManagerTestBase.java b/scm-test/src/main/java/sonia/scm/user/UserManagerTestBase.java index dddce344ce..3c684f58c9 100644 --- a/scm-test/src/main/java/sonia/scm/user/UserManagerTestBase.java +++ b/scm-test/src/main/java/sonia/scm/user/UserManagerTestBase.java @@ -39,12 +39,10 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.junit.Test; import sonia.scm.AlreadyExistsException; -import sonia.scm.ConcurrentModificationException; import sonia.scm.Manager; import sonia.scm.ManagerTestBase; import sonia.scm.NotFoundException; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -63,7 +61,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { public static final int THREAD_COUNT = 10; @Test - public void testCreate() throws AlreadyExistsException { + public void testCreate() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -75,7 +73,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { } @Test(expected = AlreadyExistsException.class) - public void testCreateExisting() throws AlreadyExistsException { + public void testCreateExisting() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -87,7 +85,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { } @Test - public void testDelete() throws Exception { + public void testDelete() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -97,12 +95,12 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { } @Test(expected = NotFoundException.class) - public void testDeleteNotFound() throws Exception { + public void testDeleteNotFound() { manager.delete(UserTestData.createDent()); } @Test - public void testGet() throws AlreadyExistsException { + public void testGet() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -116,7 +114,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { } @Test - public void testGetAll() throws AlreadyExistsException { + public void testGetAll() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -181,7 +179,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { } @Test - public void testModify() throws AlreadyExistsException, NotFoundException, ConcurrentModificationException { + public void testModify() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -238,7 +236,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { } @Test - public void testRefresh() throws AlreadyExistsException, NotFoundException { + public void testRefresh() { User zaphod = UserTestData.createZaphod(); manager.create(zaphod); @@ -289,7 +287,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { finished = true; } - private User createUser() throws AlreadyExistsException { + private User createUser() { String id = UUID.randomUUID().toString(); User user = new User(id, id.concat(" displayName"), id.concat("@mail.com")); @@ -299,7 +297,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> { return user; } - private void modifyAndDeleteUser(User user) throws IOException, NotFoundException, ConcurrentModificationException { + private void modifyAndDeleteUser(User user) { String name = user.getName(); String nd = name.concat(" new displayname"); diff --git a/scm-ui-components/package.json b/scm-ui-components/package.json index 527acbd9dc..bddbaba1c5 100644 --- a/scm-ui-components/package.json +++ b/scm-ui-components/package.json @@ -1,13 +1,16 @@ { "name": "scm-ui-components", + "version": "0.0.3", "description": "Lerna root for SCM-Manager UI Components", "private": true, "scripts": { "bootstrap": "lerna bootstrap", "link": "lerna exec -- yarn link", - "unlink": "lerna exec --no-bail -- yarn unlink || true" + "unlink": "lerna exec --no-bail -- yarn unlink || true", + "deploy": "node ./scripts/publish.js" }, "devDependencies": { - "lerna": "^3.2.1" + "lerna": "^3.4.3", + "xml2js": "^0.4.19" } } diff --git a/scm-ui-components/packages/ui-components/flow-typed/npm/react-i18next_v7.x.x.js b/scm-ui-components/packages/ui-components/flow-typed/npm/react-i18next_v7.x.x.js new file mode 100644 index 0000000000..93d27674d7 --- /dev/null +++ b/scm-ui-components/packages/ui-components/flow-typed/npm/react-i18next_v7.x.x.js @@ -0,0 +1,95 @@ +// flow-typed signature: 65d42f62f8de603dcc631ea5a6b00580 +// flow-typed version: f3f13164e0/react-i18next_v7.x.x/flow_>=v0.64.x + +declare module "react-i18next" { + declare type TFunction = (key?: ?string, data?: ?Object) => string; + + declare type TranslatorProps = {| + t: TFunction, + i18nLoadedAt: Date, + i18n: Object + |}; + + declare type TranslatorPropsVoid = { + t: TFunction | void, + i18nLoadedAt: Date | void, + i18n: Object | void + }; + + declare type Translator<P: {}, Component: React$ComponentType<P>> = ( + WrappedComponent: Component + ) => React$ComponentType< + $Diff<React$ElementConfig<Component>, TranslatorPropsVoid> + >; + + declare type TranslateOptions = $Shape<{ + wait: boolean, + nsMode: "default" | "fallback", + bindi18n: false | string, + bindStore: false | string, + withRef: boolean, + translateFuncName: string, + i18n: Object, + usePureComponent: boolean + }>; + + declare function translate<P: {}, Component: React$ComponentType<P>>( + namespaces?: | string + | Array<string> + | (($Diff<P, TranslatorPropsVoid>) => string | Array<string>), + options?: TranslateOptions + ): Translator<P, Component>; + + declare type I18nProps = { + i18n?: Object, + ns?: string | Array<string>, + children: (t: TFunction, { i18n: Object, t: TFunction }) => React$Node, + initialI18nStore?: Object, + initialLanguage?: string + }; + declare var I18n: React$ComponentType<I18nProps>; + + declare type InterpolateProps = { + className?: string, + dangerouslySetInnerHTMLPartElement?: string, + i18n?: Object, + i18nKey?: string, + options?: Object, + parent?: string, + style?: Object, + t?: TFunction, + useDangerouslySetInnerHTML?: boolean + }; + declare var Interpolate: React$ComponentType<InterpolateProps>; + + declare type TransProps = { + count?: number, + parent?: string, + i18n?: Object, + i18nKey?: string, + t?: TFunction + }; + declare var Trans: React$ComponentType<TransProps>; + + declare type ProviderProps = { i18n: Object, children: React$Element<*> }; + declare var I18nextProvider: React$ComponentType<ProviderProps>; + + declare type NamespacesProps = { + components: Array<React$ComponentType<*>>, + i18n: { loadNamespaces: Function } + }; + declare function loadNamespaces(props: NamespacesProps): Promise<void>; + + declare var reactI18nextModule: { + type: "3rdParty", + init: (instance: Object) => void + }; + + declare function setDefaults(options: TranslateOptions): void; + + declare function getDefaults(): TranslateOptions; + + declare function getI18n(): Object; + + declare function setI18n(instance: Object): void; +} diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 096a8636b3..823ca7143e 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -1,18 +1,20 @@ { "name": "@scm-manager/ui-components", - "version": "0.0.1", + "version": "2.0.0-SNAPSHOT", "description": "UI Components for SCM-Manager and its plugins", "main": "src/index.js", - "files": ["src"], + "files": [ + "src" + ], "repository": "https://bitbucket.org/sdorra/scm-manager", "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", - "license" : "BSD-3-Clause", + "license": "BSD-3-Clause", "scripts": { "update-index": "create-index -r src", "eslint-fix": "eslint src --fix" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.17", + "@scm-manager/ui-bundler": "^0.0.21", "create-index": "^2.3.0", "enzyme": "^3.5.0", "enzyme-adapter-react-16": "^1.3.1", @@ -23,25 +25,34 @@ "react-router-enzyme-context": "^1.2.0" }, "dependencies": { + "@scm-manager/ui-extensions": "^0.1.1", + "@scm-manager/ui-types": "2.0.0-SNAPSHOT", "classnames": "^2.2.6", "moment": "^2.22.2", "react": "^16.5.2", "react-dom": "^16.5.2", "react-i18next": "^7.11.0", "react-jss": "^8.6.1", - "react-router-dom": "^4.3.1", - "@scm-manager/ui-types": "0.0.1" + "react-router-dom": "^4.3.1" }, "browserify": { "transform": [ - ["browserify-css"], + [ + "browserify-css" + ], [ "babelify", { - "plugins": ["@babel/plugin-proposal-class-properties"], - "presets": ["@babel/preset-env", "@babel/preset-flow", "@babel/preset-react"] + "plugins": [ + "@babel/plugin-proposal-class-properties" + ], + "presets": [ + "@babel/preset-env", + "@babel/preset-flow", + "@babel/preset-react" + ] } ] ] } -} +} \ No newline at end of file diff --git a/scm-ui-components/packages/ui-components/src/Help.js b/scm-ui-components/packages/ui-components/src/Help.js index 965d16f145..047f79244f 100644 --- a/scm-ui-components/packages/ui-components/src/Help.js +++ b/scm-ui-components/packages/ui-components/src/Help.js @@ -1,39 +1,33 @@ //@flow import React from "react"; import injectSheet from "react-jss"; -import classNames from "classnames"; +import Tooltip from './Tooltip'; +import HelpIcon from './HelpIcon'; const styles = { - img: { - display: "block" - }, - q: { - float: "left", - paddingLeft: "3px", - float: "right" + tooltip: { + display: "inline-block", + paddingLeft: "3px" } }; type Props = { message: string, classes: any -}; +} class Help extends React.Component<Props> { + render() { const { message, classes } = this.props; - const multiline = message.length > 60 ? "is-tooltip-multiline" : ""; return ( - <div - className={classNames("tooltip is-tooltip-right", multiline, classes.q)} - data-tooltip={message} - > - <i - className={classNames("fa fa-question has-text-info", classes.img)} - /> - </div> + <Tooltip className={classes.tooltip} message={message}> + <HelpIcon /> + </Tooltip> ); } + } export default injectSheet(styles)(Help); + diff --git a/scm-ui-components/packages/ui-components/src/HelpIcon.js b/scm-ui-components/packages/ui-components/src/HelpIcon.js new file mode 100644 index 0000000000..fba8ead422 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/HelpIcon.js @@ -0,0 +1,14 @@ +//@flow +import React from "react"; +import classNames from "classnames"; + +type Props = { +}; + +class HelpIcon extends React.Component<Props> { + render() { + return <i className={classNames("fa fa-question has-text-info")} /> + } +} + +export default HelpIcon; diff --git a/scm-ui-components/packages/ui-components/src/Image.js b/scm-ui-components/packages/ui-components/src/Image.js index 5cb7fd6aa9..f5760277e4 100644 --- a/scm-ui-components/packages/ui-components/src/Image.js +++ b/scm-ui-components/packages/ui-components/src/Image.js @@ -1,6 +1,6 @@ //@flow import React from "react"; -import { withContextPath } from "./urls"; +import {withContextPath} from "./urls"; type Props = { src: string, diff --git a/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js b/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js deleted file mode 100644 index b5d049e68d..0000000000 --- a/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js +++ /dev/null @@ -1,46 +0,0 @@ -//@flow -import React from "react"; -import { Help } from "./index"; - -type Props = { - label: string, - helpText?: string -}; - -class LabelWithHelpIcon extends React.Component<Props> { - renderLabel = () => { - const label = this.props.label; - if (label) { - return <label className="label">{label}</label>; - } - return ""; - }; - - renderHelp = () => { - const helpText = this.props.helpText; - if (helpText) { - return ( - <div className="control columns is-vcentered"> - <Help message={helpText} /> - </div> - ); - } else return null; - }; - - renderLabelWithHelpIcon = () => { - if (this.props.label) { - return ( - <div className="field is-grouped"> - <div className="control">{this.renderLabel()}</div> - {this.renderHelp()} - </div> - ); - } else return null; - }; - - render() { - return this.renderLabelWithHelpIcon(); - } -} - -export default LabelWithHelpIcon; diff --git a/scm-ui-components/packages/ui-components/src/Loading.js b/scm-ui-components/packages/ui-components/src/Loading.js index 0a472ecb02..6b4817131f 100644 --- a/scm-ui-components/packages/ui-components/src/Loading.js +++ b/scm-ui-components/packages/ui-components/src/Loading.js @@ -1,10 +1,14 @@ //@flow + import React from "react"; import { translate } from "react-i18next"; import injectSheet from "react-jss"; import Image from "./Image"; const styles = { + minHeightContainer: { + minHeight: "256px" + }, wrapper: { position: "relative" }, @@ -34,14 +38,16 @@ class Loading extends React.Component<Props> { render() { const { message, t, classes } = this.props; return ( - <div className={classes.wrapper}> - <div className={classes.loading}> - <Image - className={classes.image} - src="/images/loading.svg" - alt={t("loading.alt")} - /> - <p className="has-text-centered">{message}</p> + <div className={classes.minHeightContainer}> + <div className={classes.wrapper}> + <div className={classes.loading}> + <Image + className={classes.image} + src="/images/loading.svg" + alt={t("loading.alt")} + /> + <p className="has-text-centered">{message}</p> + </div> </div> </div> ); diff --git a/scm-ui-components/packages/ui-components/src/Tooltip.js b/scm-ui-components/packages/ui-components/src/Tooltip.js new file mode 100644 index 0000000000..d935b323c7 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/Tooltip.js @@ -0,0 +1,26 @@ +//@flow +import * as React from "react"; +import classNames from "classnames"; + +type Props = { + message: string, + className: string, + children: React.Node +}; + +class Tooltip extends React.Component<Props> { + render() { + const { className, message, children } = this.props; + const multiline = message.length > 60 ? "is-tooltip-multiline" : ""; + return ( + <div + className={classNames("tooltip", "is-tooltip-right", multiline, className)} + data-tooltip={message} + > + {children} + </div> + ); + } +} + +export default Tooltip; diff --git a/scm-ui-components/packages/ui-components/src/apiclient.js b/scm-ui-components/packages/ui-components/src/apiclient.js index 0b57abeada..bd19dcdf14 100644 --- a/scm-ui-components/packages/ui-components/src/apiclient.js +++ b/scm-ui-components/packages/ui-components/src/apiclient.js @@ -48,6 +48,14 @@ class ApiClient { return this.httpRequestWithJSONBody("PUT", url, contentType, payload); } + head(url: string) { + let options: RequestOptions = { + method: "HEAD" + }; + options = Object.assign(options, fetchOptions); + return fetch(createUrl(url), options).then(handleStatusCode); + } + delete(url: string): Promise<Response> { let options: RequestOptions = { method: "DELETE" diff --git a/scm-ui-components/packages/ui-components/src/buttons/DownloadButton.js b/scm-ui-components/packages/ui-components/src/buttons/DownloadButton.js new file mode 100644 index 0000000000..3a99dc876b --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/buttons/DownloadButton.js @@ -0,0 +1,25 @@ +//@flow +import React from "react"; +import Button, { type ButtonProps } from "./Button"; +import type {File} from "@scm-manager/ui-types"; + +type Props = { + displayName: string, + url: string +}; + +class DownloadButton extends React.Component<Props> { + render() { + const {displayName, url} = this.props; + return ( + <a className="button is-large is-info" href={url}> + <span className="icon is-medium"> + <i className="fas fa-arrow-circle-down" /> + </span> + <span>{displayName}</span> + </a> + ); + } +} + +export default DownloadButton; diff --git a/scm-ui-components/packages/ui-components/src/buttons/index.js b/scm-ui-components/packages/ui-components/src/buttons/index.js index d7da320fc2..2e166e1d93 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/index.js +++ b/scm-ui-components/packages/ui-components/src/buttons/index.js @@ -7,4 +7,4 @@ export { default as DeleteButton } from "./DeleteButton.js"; export { default as EditButton } from "./EditButton.js"; export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton.js"; export { default as SubmitButton } from "./SubmitButton.js"; - +export {default as DownloadButton} from "./DownloadButton.js"; diff --git a/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js new file mode 100644 index 0000000000..477eee5238 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js @@ -0,0 +1,43 @@ +// @flow +import React from "react"; +import { binder } from "@scm-manager/ui-extensions"; +import { NavLink } from "../navigation"; +import { Route } from "react-router-dom"; +import { translate } from "react-i18next"; + +class ConfigurationBinder { + + i18nNamespace: string = "plugins"; + + bindGlobal(to: string, labelI18nKey: string, linkName: string, ConfigurationComponent: any) { + + // create predicate based on the link name of the index resource + // if the linkname is not available, the navigation link and the route are not bound to the extension points + const configPredicate = (props: Object) => { + return props.links && props.links[linkName]; + }; + + // create NavigationLink with translated label + const ConfigNavLink = translate(this.i18nNamespace)(({t}) => { + return <NavLink to={"/config" + to} label={t(labelI18nKey)} />; + }); + + // bind navigation link to extension point + binder.bind("config.navigation", ConfigNavLink, configPredicate); + + + // route for global configuration, passes the link from the index resource to component + const ConfigRoute = ({ url, links }) => { + const link = links[linkName].href; + return <Route path={url + to} + render={() => <ConfigurationComponent link={link}/>} + exact/>; + }; + + // bind config route to extension point + binder.bind("config.route", ConfigRoute, configPredicate); + } + +} + +export default new ConfigurationBinder(); diff --git a/scm-ui-components/packages/ui-components/src/config/GlobalConfiguration.js b/scm-ui-components/packages/ui-components/src/config/GlobalConfiguration.js new file mode 100644 index 0000000000..b2b7dca647 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/config/GlobalConfiguration.js @@ -0,0 +1,162 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Links } from "@scm-manager/ui-types"; +import { + apiClient, + SubmitButton, + Loading, + ErrorNotification +} from "../"; + +type RenderProps = { + readOnly: boolean, + initialConfiguration: Configuration, + onConfigurationChange: (Configuration, boolean) => void +}; + +type Props = { + link: string, + render: (props: RenderProps) => any, // ??? + + // context props + t: (string) => string +}; + +type Configuration = { + _links: Links +} & Object; + +type State = { + error?: Error, + fetching: boolean, + modifying: boolean, + contentType?: string, + + configuration?: Configuration, + modifiedConfiguration?: Configuration, + valid: boolean +}; + +/** + * GlobalConfiguration uses the render prop pattern to encapsulate the logic for + * synchronizing the configuration with the backend. + */ +class GlobalConfiguration extends React.Component<Props, State> { + + constructor(props: Props) { + super(props); + this.state = { + fetching: true, + modifying: false, + valid: false + }; + } + + componentDidMount() { + const { link } = this.props; + + apiClient.get(link) + .then(this.captureContentType) + .then(response => response.json()) + .then(this.loadConfig) + .catch(this.handleError); + } + + captureContentType = (response: Response) => { + const contentType = response.headers.get("Content-Type"); + this.setState({ + contentType + }); + return response; + }; + + getContentType = (): string => { + const { contentType } = this.state; + return contentType ? contentType : "application/json"; + }; + + handleError = (error: Error) => { + this.setState({ + error, + fetching: false, + modifying: false + }); + }; + + loadConfig = (configuration: Configuration) => { + this.setState({ + configuration, + fetching: false, + error: undefined + }); + }; + + getModificationUrl = (): ?string => { + const { configuration } = this.state; + if (configuration) { + const links = configuration._links; + if (links && links.update) { + return links.update.href; + } + } + }; + + isReadOnly = (): boolean => { + const modificationUrl = this.getModificationUrl(); + return !modificationUrl; + }; + + configurationChanged = (configuration: Configuration, valid: boolean) => { + this.setState({ + modifiedConfiguration: configuration, + valid + }); + }; + + modifyConfiguration = (event: Event) => { + event.preventDefault(); + + this.setState({ modifying: true }); + + const {modifiedConfiguration} = this.state; + + apiClient.put(this.getModificationUrl(), modifiedConfiguration, this.getContentType()) + .then(() => this.setState({ modifying: false })) + .catch(this.handleError); + }; + + render() { + const { t } = this.props; + const { fetching, error, configuration, modifying, valid } = this.state; + + if (error) { + return <ErrorNotification error={error}/>; + } else if (fetching || !configuration) { + return <Loading />; + } else { + const readOnly = this.isReadOnly(); + + const renderProps: RenderProps = { + readOnly, + initialConfiguration: configuration, + onConfigurationChange: this.configurationChanged + }; + + return ( + <form onSubmit={this.modifyConfiguration}> + { this.props.render(renderProps) } + <hr/> + <SubmitButton + label={t("config-form.submit")} + disabled={!valid || readOnly} + loading={modifying} + /> + </form> + ); + } + } + +} + +export default translate("config")(GlobalConfiguration); diff --git a/scm-ui-components/packages/ui-components/src/config/index.js b/scm-ui-components/packages/ui-components/src/config/index.js new file mode 100644 index 0000000000..9596e9cda5 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/config/index.js @@ -0,0 +1,3 @@ +// @flow +export { default as ConfigurationBinder } from "./ConfigurationBinder"; +export { default as GlobalConfiguration } from "./GlobalConfiguration"; diff --git a/scm-ui-components/packages/ui-components/src/forms/Checkbox.js b/scm-ui-components/packages/ui-components/src/forms/Checkbox.js index 331b921d01..93cd28c28f 100644 --- a/scm-ui-components/packages/ui-components/src/forms/Checkbox.js +++ b/scm-ui-components/packages/ui-components/src/forms/Checkbox.js @@ -4,27 +4,26 @@ import { Help } from "../index"; type Props = { label?: string, + name?: string, checked: boolean, - onChange?: boolean => void, + onChange?: (value: boolean, name?: string) => void, disabled?: boolean, helpText?: string }; + class Checkbox extends React.Component<Props> { + onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => { if (this.props.onChange) { - this.props.onChange(event.target.checked); + this.props.onChange(event.target.checked, this.props.name); } }; renderHelp = () => { const helpText = this.props.helpText; if (helpText) { - return ( - <div className="control columns is-vcentered"> - <Help message={helpText} /> - </div> - ); - } else return null; + return <Help message={helpText} />; + } }; render() { @@ -38,10 +37,11 @@ class Checkbox extends React.Component<Props> { onChange={this.onCheckboxChange} disabled={this.props.disabled} /> + {" "} {this.props.label} + {this.renderHelp()} </label> </div> - {this.renderHelp()} </div> ); } diff --git a/scm-ui-components/packages/ui-components/src/forms/InputField.js b/scm-ui-components/packages/ui-components/src/forms/InputField.js index 79b71298f8..52ff9a369b 100644 --- a/scm-ui-components/packages/ui-components/src/forms/InputField.js +++ b/scm-ui-components/packages/ui-components/src/forms/InputField.js @@ -1,15 +1,16 @@ //@flow import React from "react"; import classNames from "classnames"; -import { LabelWithHelpIcon } from "../index"; +import LabelWithHelpIcon from "./LabelWithHelpIcon"; type Props = { label?: string, + name?: string, placeholder?: string, value?: string, type?: string, autofocus?: boolean, - onChange: string => void, + onChange: (value: string, name?: string) => void, onReturnPressed?: () => void, validationError: boolean, errorMessage: string, @@ -32,7 +33,7 @@ class InputField extends React.Component<Props> { } handleInput = (event: SyntheticInputEvent<HTMLInputElement>) => { - this.props.onChange(event.target.value); + this.props.onChange(event.target.value, this.props.name); }; handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => { diff --git a/scm-ui-components/packages/ui-components/src/forms/LabelWithHelpIcon.js b/scm-ui-components/packages/ui-components/src/forms/LabelWithHelpIcon.js new file mode 100644 index 0000000000..8a917828ec --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/forms/LabelWithHelpIcon.js @@ -0,0 +1,37 @@ +//@flow +import React from "react"; +import Help from '../Help'; + +type Props = { + label?: string, + helpText?: string +}; + +class LabelWithHelpIcon extends React.Component<Props> { + + renderHelp() { + const { helpText } = this.props; + if (helpText) { + return ( + <Help message={helpText} /> + ); + } + } + + render() { + const {label } = this.props; + + if (label) { + const help = this.renderHelp(); + return ( + <label className="label"> + {label} { help } + </label> + ); + } + + return ""; + } +} + +export default LabelWithHelpIcon; diff --git a/scm-ui-components/packages/ui-components/src/forms/Select.js b/scm-ui-components/packages/ui-components/src/forms/Select.js index 880b375999..ccb82e62da 100644 --- a/scm-ui-components/packages/ui-components/src/forms/Select.js +++ b/scm-ui-components/packages/ui-components/src/forms/Select.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import classNames from "classnames"; -import { LabelWithHelpIcon } from "../index"; +import LabelWithHelpIcon from "./LabelWithHelpIcon"; export type SelectItem = { value: string, @@ -9,10 +9,11 @@ export type SelectItem = { }; type Props = { + name?: string, label?: string, options: SelectItem[], - value?: SelectItem, - onChange: string => void, + value?: string, + onChange: (value: string, name?: string) => void, loading?: boolean, helpText?: string }; @@ -29,7 +30,7 @@ class Select extends React.Component<Props> { } handleInput = (event: SyntheticInputEvent<HTMLSelectElement>) => { - this.props.onChange(event.target.value); + this.props.onChange(event.target.value, this.props.name); }; render() { @@ -40,10 +41,10 @@ class Select extends React.Component<Props> { return ( <div className="field"> <LabelWithHelpIcon label={label} helpText={helpText} /> - <div className={classNames( - "control select", - loadingClass - )}> + <div className={classNames( + "control select", + loadingClass + )}> <select ref={input => { this.field = input; diff --git a/scm-ui-components/packages/ui-components/src/forms/Textarea.js b/scm-ui-components/packages/ui-components/src/forms/Textarea.js index 0bf386aca7..3db590122a 100644 --- a/scm-ui-components/packages/ui-components/src/forms/Textarea.js +++ b/scm-ui-components/packages/ui-components/src/forms/Textarea.js @@ -1,6 +1,6 @@ //@flow import React from "react"; -import { LabelWithHelpIcon } from "../index"; +import LabelWithHelpIcon from "./LabelWithHelpIcon"; export type SelectItem = { value: string, @@ -8,10 +8,11 @@ export type SelectItem = { }; type Props = { + name?: string, label?: string, placeholder?: SelectItem[], value?: string, - onChange: string => void, + onChange: (value: string, name?: string) => void, helpText?: string }; @@ -19,7 +20,7 @@ class Textarea extends React.Component<Props> { field: ?HTMLTextAreaElement; handleInput = (event: SyntheticInputEvent<HTMLTextAreaElement>) => { - this.props.onChange(event.target.value); + this.props.onChange(event.target.value, this.props.name); }; render() { diff --git a/scm-ui-components/packages/ui-components/src/forms/index.js b/scm-ui-components/packages/ui-components/src/forms/index.js index 24e52daa1d..c96f3a8196 100644 --- a/scm-ui-components/packages/ui-components/src/forms/index.js +++ b/scm-ui-components/packages/ui-components/src/forms/index.js @@ -5,4 +5,5 @@ export { default as Checkbox } from "./Checkbox.js"; export { default as InputField } from "./InputField.js"; export { default as Select } from "./Select.js"; export { default as Textarea } from "./Textarea.js"; +export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon"; diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 196c33c08f..41e385af8d 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -17,13 +17,15 @@ export { default as Notification } from "./Notification.js"; export { default as Paginator } from "./Paginator.js"; export { default as LinkPaginator } from "./LinkPaginator.js"; export { default as ProtectedRoute } from "./ProtectedRoute.js"; -export { default as Help } from "./Help.js"; -export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js"; +export { default as Help } from "./Help"; +export { default as HelpIcon } from "./HelpIcon"; +export { default as Tooltip } from "./Tooltip"; export { getPageFromMatch } from "./urls"; export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js"; export * from "./buttons"; +export * from "./config"; export * from "./forms"; export * from "./layout"; export * from "./modals"; diff --git a/scm-ui-components/packages/ui-components/src/layout/Footer.js b/scm-ui-components/packages/ui-components/src/layout/Footer.js index 4072711c7a..11ed52ac63 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Footer.js +++ b/scm-ui-components/packages/ui-components/src/layout/Footer.js @@ -1,6 +1,7 @@ //@flow import React from "react"; import type { Me } from "@scm-manager/ui-types"; +import {Link} from "react-router-dom"; type Props = { me?: Me @@ -15,7 +16,7 @@ class Footer extends React.Component<Props> { return ( <footer className="footer"> <div className="container is-centered"> - <p className="has-text-centered">{me.displayName}</p> + <p className="has-text-centered"><Link to={"/me"}>{me.displayName}</Link></p> </div> </footer> ); diff --git a/scm-ui-components/packages/ui-components/src/navigation/NavLink.js b/scm-ui-components/packages/ui-components/src/navigation/NavLink.js index 20a7f2469f..9a7c72adb1 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/NavLink.js +++ b/scm-ui-components/packages/ui-components/src/navigation/NavLink.js @@ -1,6 +1,6 @@ //@flow import * as React from "react"; -import { Route, Link } from "react-router-dom"; +import {Link, Route} from "react-router-dom"; // TODO mostly copy of PrimaryNavigationLink diff --git a/scm-ui-components/packages/ui-components/src/urls.test.js b/scm-ui-components/packages/ui-components/src/urls.test.js index e1d88bfe55..60f27510b8 100644 --- a/scm-ui-components/packages/ui-components/src/urls.test.js +++ b/scm-ui-components/packages/ui-components/src/urls.test.js @@ -1,5 +1,5 @@ // @flow -import { concat, getPageFromMatch, withEndingSlash } from "./urls"; +import {concat, getPageFromMatch, withEndingSlash} from "./urls"; describe("tests for withEndingSlash", () => { diff --git a/scm-ui-components/packages/ui-components/yarn.lock b/scm-ui-components/packages/ui-components/yarn.lock index b4b0cc32a8..f11cfa5bcd 100644 --- a/scm-ui-components/packages/ui-components/yarn.lock +++ b/scm-ui-components/packages/ui-components/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb" +"@scm-manager/ui-bundler@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -681,6 +681,13 @@ vinyl-source-stream "^2.0.0" watchify "^3.11.0" +"@scm-manager/ui-extensions@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45" + dependencies: + react "^16.4.2" + react-dom "^16.4.2" + "@types/node@*": version "10.12.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.0.tgz#ea6dcbddbc5b584c83f06c60e82736d8fbb0c235" @@ -6229,6 +6236,15 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-dom@^16.4.2: + version "16.6.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.0.tgz#6375b8391e019a632a89a0988bce85f0cc87a92f" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.10.0" + react-dom@^16.5.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7" @@ -6299,6 +6315,15 @@ react-test-renderer@^16.0.0-0: react-is "^16.5.2" schedule "^0.5.0" +react@^16.4.2: + version "16.6.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.6.0.tgz#b34761cfaf3e30f5508bc732fb4736730b7da246" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.10.0" + react@^16.5.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" @@ -6726,6 +6751,13 @@ schedule@^0.5.0: dependencies: object-assign "^4.1.1" +scheduler@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.10.0.tgz#7988de90fe7edccc774ea175a783e69c40c521e1" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + "semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" diff --git a/scm-ui-components/packages/ui-types/package.json b/scm-ui-components/packages/ui-types/package.json index 0701870562..78452c2ef5 100644 --- a/scm-ui-components/packages/ui-types/package.json +++ b/scm-ui-components/packages/ui-types/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-types", - "version": "0.0.1", + "version": "2.0.0-SNAPSHOT", "description": "Flow types for SCM-Manager related Objects", "main": "src/index.js", "files": [ @@ -8,23 +8,29 @@ ], "repository": "https://bitbucket.org/sdorra/scm-manager", "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", - "license" : "BSD-3-Clause", + "license": "BSD-3-Clause", "scripts": { "lint": "ui-bunder lint", "check": "flow check" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.17" + "@scm-manager/ui-bundler": "^0.0.21" }, "browserify": { "transform": [ [ "babelify", { - "plugins": ["@babel/plugin-proposal-class-properties"], - "presets": ["@babel/preset-env", "@babel/preset-flow", "@babel/preset-react"] + "plugins": [ + "@babel/plugin-proposal-class-properties" + ], + "presets": [ + "@babel/preset-env", + "@babel/preset-flow", + "@babel/preset-react" + ] } ] ] } -} +} \ No newline at end of file diff --git a/scm-ui-components/packages/ui-types/src/Me.js b/scm-ui-components/packages/ui-types/src/Me.js index 65e4fc8336..ab67debae7 100644 --- a/scm-ui-components/packages/ui-types/src/Me.js +++ b/scm-ui-components/packages/ui-types/src/Me.js @@ -2,5 +2,6 @@ export type Me = { name: string, - displayName: string + displayName: string, + mail: string }; diff --git a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js index d86e499378..4352c21da6 100644 --- a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js +++ b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js @@ -1,5 +1,5 @@ //@flow -import type { Links } from "./hal"; +import type {Links} from "./hal"; export type Permission = PermissionCreateEntry & { _links: Links diff --git a/scm-ui-components/packages/ui-types/yarn-error.log b/scm-ui-components/packages/ui-types/yarn-error.log deleted file mode 100644 index b799a25089..0000000000 --- a/scm-ui-components/packages/ui-types/yarn-error.log +++ /dev/null @@ -1,52 +0,0 @@ -Arguments: - /usr/bin/node /home/ssdorra/.yarn/bin/yarn.js add --dev ui-bundler - -PATH: - /home/ssdorra/.yarn/bin:/usr/local/go/bin:/home/ssdorra/Projects/go/bin:/home/ssdorra/.local/bin:/usr/local/google-cloud-sdk/bin:/usr/local/go/bin:/home/ssdorra/.sdkman/candidates/maven/current/bin:/home/ssdorra/.sdkman/candidates/groovy/current/bin:/home/ssdorra/bin:/home/ssdorra/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/lib/jvm/java-8-oracle/bin:/usr/lib/jvm/java-8-oracle/db/bin:/usr/lib/jvm/java-8-oracle/jre/bin:/home/ssdorra/Projects/go/bin - -Yarn version: - 1.9.2 - -Node version: - 8.11.4 - -Platform: - linux x64 - -Trace: - Error: https://registry.yarnpkg.com/ui-bundler: Not found - at Request.params.callback [as _callback] (/home/ssdorra/.yarn/lib/cli.js:64150:18) - at Request.self.callback (/home/ssdorra/.yarn/lib/cli.js:137416:22) - at emitTwo (events.js:126:13) - at Request.emit (events.js:214:7) - at Request.<anonymous> (/home/ssdorra/.yarn/lib/cli.js:138388:10) - at emitOne (events.js:116:13) - at Request.emit (events.js:211:7) - at IncomingMessage.<anonymous> (/home/ssdorra/.yarn/lib/cli.js:138310:12) - at Object.onceWrapper (events.js:313:30) - at emitNone (events.js:111:20) - -npm manifest: - { - "name": "@scm-manager/ui-types", - "version": "0.0.1", - "description": "Flow types for SCM-Manager related Objects", - "main": "src/index.js", - "files": [ - "src" - ], - "repository": "https://bitbucket.org/sdorra/scm-manager", - "author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>", - "license": "MIT", - "scripts": { - "check": "flow check" - }, - "devDependencies": {} - } - -yarn manifest: - No manifest - -Lockfile: - # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. - # yarn lockfile v1 diff --git a/scm-ui-components/packages/ui-types/yarn.lock b/scm-ui-components/packages/ui-types/yarn.lock index 58b579d6ae..fe2df2f76a 100644 --- a/scm-ui-components/packages/ui-types/yarn.lock +++ b/scm-ui-components/packages/ui-types/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb" +"@scm-manager/ui-bundler@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-ui-components/pom.xml b/scm-ui-components/pom.xml index f5544a9e24..3d4e31a50a 100644 --- a/scm-ui-components/pom.xml +++ b/scm-ui-components/pom.xml @@ -68,6 +68,16 @@ <script>link</script> </configuration> </execution> + <execution> + <id>deploy</id> + <phase>deploy</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <script>deploy</script> + </configuration> + </execution> </executions> </plugin> diff --git a/scm-ui-components/scripts/publish.js b/scm-ui-components/scripts/publish.js new file mode 100644 index 0000000000..9eaae6fede --- /dev/null +++ b/scm-ui-components/scripts/publish.js @@ -0,0 +1,140 @@ +const fs = require("fs"); +const path = require("path"); +const {spawn} = require("child_process"); +const parseString = require('xml2js').parseString; + +function isSnapshot(version) { + return version.indexOf("SNAPSHOT") > 0; +} + +function createSnapshotVersion(version) { + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth().toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + const seconds = date.getSeconds().toString().padStart(2, "0"); + + return version.replace("SNAPSHOT", `${year}${month}${day}-${hours}${minutes}${seconds}`); +} + +function createVersionForPublishing(version) { + if (isSnapshot(version)) { + return createSnapshotVersion(version); + } + return version; +} + +function publishPackages(version) { + console.log(`publish ${version} of all packages`); + return new Promise((resolve, reject) => { + const lerna = spawn("lerna", [ + "exec", "--", "yarn", "publish", "--new-version", version, "--access", "public" + ], { + stdio: [ + process.stdin, + process.stdout, + process.stderr + ] + }); + + lerna.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error("publishing of packages failed with status code: " + code)); + } + }); + }); +} + +function setVersion(package, packages, newVersion) { + return new Promise((resolve, reject) => { + const packageJsonPath = path.join(package, "package.json"); + fs.readFile(packageJsonPath, "utf-8" , (err, content) => { + if (err) { + reject(err); + } else { + const packageJson = JSON.parse(content); + packageJson.version = newVersion; + + for (let dep in packageJson.dependencies) { + if (packages.indexOf(dep) >= 0) { + packageJson.dependencies[ dep ] = newVersion; + } + } + + fs.writeFile( packageJsonPath, JSON.stringify(packageJson, null, 2), (err) => { + if (err) { + reject(err) + } else { + console.log("modified", packageJsonPath); + resolve(); + } + }); + } + }); + }); +} + +function setVersions(newVersion) { + console.log("set versions of packages to", newVersion); + return new Promise((resolve, reject) => { + fs.readdir("packages", (err, packages) => { + if ( err ) { + reject(err); + } else { + const actions = []; + const packagesWithOrg = packages.map((name) => `@scm-manager/${name}`); + for (let pkg of packages) { + const action = setVersion(path.join("packages", pkg), packagesWithOrg, newVersion); + actions.push(action); + } + + resolve(Promise.all(actions)); + } + }); + }); +} + +function getVersion() { + return new Promise((resolve, reject) => { + fs.readFile("pom.xml", "utf8", (err, xml) => { + if (err) { + reject(err); + } else { + + parseString(xml, function (err, json) { + if (err) { + reject(err) + } else { + const project = json.project; + + let version = project.version; + if (!version) { + version = project.parent.version; + } + version = version[0]; + + resolve(version) + } + + }); + + } + }); + }); +} + +getVersion() + .then(version => { + const publishVersion = createVersionForPublishing(version); + return setVersions(publishVersion) + .then(() => publishPackages(publishVersion)) + .then(() => setVersions(version)); + }) + .catch((err) => { + throw err; + }); diff --git a/scm-ui-components/yarn.lock b/scm-ui-components/yarn.lock index eb7d40a3b3..2862a46547 100644 --- a/scm-ui-components/yarn.lock +++ b/scm-ui-components/yarn.lock @@ -5,6 +5,7 @@ "@lerna/add@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.4.1.tgz#d41068317e30f530df48220d256b5e79690b1877" + integrity sha512-Vf54B42jlD6G52qnv/cAGH70cVQIa+LX//lfsbkxHvzkhIqBl5J4KsnTOPkA9uq3R+zP58ayicCHB9ReiEWGJg== dependencies: "@lerna/bootstrap" "^3.4.1" "@lerna/command" "^3.3.0" @@ -20,6 +21,7 @@ "@lerna/batch-packages@^3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@lerna/batch-packages/-/batch-packages-3.1.2.tgz#74b5312a01a8916204cbc71237ffbe93144b99df" + integrity sha512-HAkpptrYeUVlBYbLScXgeCgk6BsNVXxDd53HVWgzzTWpXV4MHpbpeKrByyt7viXlNhW0w73jJbipb/QlFsHIhQ== dependencies: "@lerna/package-graph" "^3.1.2" "@lerna/validation-error" "^3.0.0" @@ -28,6 +30,7 @@ "@lerna/bootstrap@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@lerna/bootstrap/-/bootstrap-3.4.1.tgz#10635e9b547fb7d685949ac78e0923f73da2f52a" + integrity sha512-yZDJgNm/KDoRH2klzmQGmpWMg/XMzWgeWvauXkrfW/mj1wwmufOuh5pN4fBFxVmUUa/RFZdfMeaaJt3+W3PPBw== dependencies: "@lerna/batch-packages" "^3.1.2" "@lerna/command" "^3.3.0" @@ -56,6 +59,7 @@ "@lerna/changed@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@lerna/changed/-/changed-3.4.1.tgz#84a049359a53b8812c3a07a664bd41b1768f5938" + integrity sha512-gT7fhl4zQWyGETDO4Yy5wsFnqNlBSsezncS1nkMW1uO6jwnolwYqcr1KbrMR8HdmsZBn/00Y0mRnbtbpPPey8w== dependencies: "@lerna/collect-updates" "^3.3.2" "@lerna/command" "^3.3.0" @@ -66,6 +70,7 @@ "@lerna/check-working-tree@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/check-working-tree/-/check-working-tree-3.3.0.tgz#2118f301f28ccb530812e5b27a341b1e6b3c84e2" + integrity sha512-oeEP1dNhiiKUaO0pmcIi73YXJpaD0n5JczNctvVNZ8fGZmrALZtEnmC28o6Z7JgQaqq5nd2kO7xbnjoitrC51g== dependencies: "@lerna/describe-ref" "^3.3.0" "@lerna/validation-error" "^3.0.0" @@ -73,6 +78,7 @@ "@lerna/child-process@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-3.3.0.tgz#71184a763105b6c8ece27f43f166498d90fe680f" + integrity sha512-q2d/OPlNX/cBXB6Iz1932RFzOmOHq6ZzPjqebkINNaTojHWuuRpvJJY4Uz3NGpJ3kEtPDvBemkZqUBTSO5wb1g== dependencies: chalk "^2.3.1" execa "^1.0.0" @@ -81,6 +87,7 @@ "@lerna/clean@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@lerna/clean/-/clean-3.3.2.tgz#9a7e8a1e400e580de260fa124945b2939a025069" + integrity sha512-mvqusgSp2ou5SGqQgTEoTvGJpGfH4+L6XSeN+Ims+eNFGXuMazmKCf+rz2PZBMFufaHJ/Os+JF0vPCcWI1Fzqg== dependencies: "@lerna/command" "^3.3.0" "@lerna/filter-options" "^3.3.2" @@ -93,6 +100,7 @@ "@lerna/cli@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@lerna/cli/-/cli-3.2.0.tgz#3ed25bcbc0b8f0878bc6a102ee0296f01476cfdf" + integrity sha512-JdbLyTxHqxUlrkI+Ke+ltXbtyA+MPu9zR6kg/n8Fl6uaez/2fZWtReXzYi8MgLxfUFa7+1OHWJv4eAMZlByJ+Q== dependencies: "@lerna/global-options" "^3.1.3" dedent "^0.7.0" @@ -102,6 +110,7 @@ "@lerna/collect-updates@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@lerna/collect-updates/-/collect-updates-3.3.2.tgz#54df5ce59ca05e8aa04ff8a9299f89cc253a9304" + integrity sha512-9WyBJI2S5sYgEZEScu525Lbi6nknNrdBKop35sCDIC9y6AIGvH6Dr5tkTd+Kg3n1dE+kHwW/xjERkx3+h7th3w== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/describe-ref" "^3.3.0" @@ -112,6 +121,7 @@ "@lerna/command@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/command/-/command-3.3.0.tgz#e81c4716a676b02dbe9d3f548d5f45b4ba32c25a" + integrity sha512-NTOkLEKlWcBLHSvUr9tzVpV7RJ4GROLeOuZ6RfztGOW/31JPSwVVBD2kPifEXNZunldOx5GVWukR+7+NpAWhsg== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/package-graph" "^3.1.2" @@ -127,6 +137,7 @@ "@lerna/conventional-commits@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-3.4.1.tgz#0b47f9fc0c4a10951883e949d939188da1b527bc" + integrity sha512-3NETrA58aUkaEW3RdwdJ766Bg9NVpLzb26mtdlsJQcvB5sQBWH5dJSHIVQH1QsGloBeH2pE/mDUEVY8ZJXuR4w== dependencies: "@lerna/validation-error" "^3.0.0" conventional-changelog-angular "^5.0.1" @@ -141,6 +152,7 @@ "@lerna/create-symlink@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/create-symlink/-/create-symlink-3.3.0.tgz#91de00fd576018ba4251f0c6a5b4b7f768f22a82" + integrity sha512-0lb88Nnq1c/GG+fwybuReOnw3+ah4dB81PuWwWwuqUNPE0n50qUf/M/7FfSb5JEh/93fcdbZI0La8t3iysNW1w== dependencies: cmd-shim "^2.0.2" fs-extra "^7.0.0" @@ -149,6 +161,7 @@ "@lerna/create@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@lerna/create/-/create-3.4.1.tgz#7cad78a5701d7666a0f5d0fe0e325acd8d8f5b63" + integrity sha512-l+4t2SRO5nvW0MNYY+EWxbaMHsAN8bkWH3nyt7EzhBjs4+TlRAJRIEqd8o9NWznheE3pzwczFz1Qfl3BWbyM5A== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/command" "^3.3.0" @@ -170,6 +183,7 @@ "@lerna/describe-ref@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/describe-ref/-/describe-ref-3.3.0.tgz#d373adb530d5428ab91e303ccbfcf51a98374a3a" + integrity sha512-4t7M4OupnYMSPNLrLUau8qkS+dgLEi4w+DkRkV0+A+KNYga1W0jVgNLPIIsxta7OHfodPkCNAqZCzNCw/dmAwA== dependencies: "@lerna/child-process" "^3.3.0" npmlog "^4.1.2" @@ -177,6 +191,7 @@ "@lerna/diff@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/diff/-/diff-3.3.0.tgz#c8130a5f508b47fad5fec81404498bc3acdf9cb5" + integrity sha512-sIoMjsm3NVxvmt6ofx8Uu/2fxgldQqLl0zmC9X1xW00j831o5hBffx1EoKj9CnmaEvoSP6j/KFjxy2RWjebCIg== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/command" "^3.3.0" @@ -186,6 +201,7 @@ "@lerna/exec@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@lerna/exec/-/exec-3.3.2.tgz#95ecaca617fd85abdb91e9a378ed06ec1763d665" + integrity sha512-mN6vGxNir7JOGvWLwKr3DW3LNy1ecCo2ziZj5rO9Mw5Rew3carUu1XLmhF/4judtsvXViUY+rvGIcqHe0vvb+w== dependencies: "@lerna/batch-packages" "^3.1.2" "@lerna/child-process" "^3.3.0" @@ -197,6 +213,7 @@ "@lerna/filter-options@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@lerna/filter-options/-/filter-options-3.3.2.tgz#ac90702b7876ff4980dcdeaeac049c433dd01773" + integrity sha512-0WHqdDgAnt5WKoByi1q+lFw8HWt5tEKP2DnLlGqWv3YFwVF5DsPRlO7xbzjY9sJgvyJtZcnkMtccdBPFhGGyIQ== dependencies: "@lerna/collect-updates" "^3.3.2" "@lerna/filter-packages" "^3.0.0" @@ -205,6 +222,7 @@ "@lerna/filter-packages@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/filter-packages/-/filter-packages-3.0.0.tgz#5eb25ad1610f3e2ab845133d1f8d7d40314e838f" + integrity sha512-zwbY1J4uRjWRZ/FgYbtVkq7I3Nduwsg2V2HwLKSzwV2vPglfGqgovYOVkND6/xqe2BHwDX4IyA2+e7OJmLaLSA== dependencies: "@lerna/validation-error" "^3.0.0" multimatch "^2.1.0" @@ -213,16 +231,19 @@ "@lerna/get-npm-exec-opts@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.0.0.tgz#8fc7866e8d8e9a2f2dc385287ba32eb44de8bdeb" + integrity sha512-arcYUm+4xS8J3Palhl+5rRJXnZnFHsLFKHBxznkPIxjwGQeAEw7df38uHdVjEQ+HNeFmHnBgSqfbxl1VIw5DHg== dependencies: npmlog "^4.1.2" "@lerna/global-options@^3.1.3": version "3.1.3" resolved "https://registry.yarnpkg.com/@lerna/global-options/-/global-options-3.1.3.tgz#cf85e24655a91d04d4efc9a80c1f83fc768d08ae" + integrity sha512-LVeZU/Zgc0XkHdGMRYn+EmHfDmmYNwYRv3ta59iCVFXLVp7FRFWF7oB1ss/WRa9x/pYU0o6L8as/5DomLUGASA== "@lerna/has-npm-version@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/has-npm-version/-/has-npm-version-3.3.0.tgz#8a73c2c437a0e1e68a19ccbd0dd3c014d4d39135" + integrity sha512-GX7omRep1eBRZHgjZLRw3MpBJSdA5gPZFz95P7rxhpvsiG384Tdrr/cKFMhm0A09yq27Tk/nuYTaZIj7HsVE6g== dependencies: "@lerna/child-process" "^3.3.0" semver "^5.5.0" @@ -230,6 +251,7 @@ "@lerna/import@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@lerna/import/-/import-3.3.1.tgz#deca8c93c9cc03c5844b975c6da9937dd7530440" + integrity sha512-2OzTQDkYKbBPpyP2iOI1sWfcvMjNLjjHjmREq/uOWJaSIk5J3Ukt71OPpcOHh4V2CBOlXidCcO+Hyb4FVIy8fw== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/command" "^3.3.0" @@ -242,6 +264,7 @@ "@lerna/init@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/init/-/init-3.3.0.tgz#998f3497da3d891867c593b808b6db4b8fc4ccb9" + integrity sha512-HvgRLkIG6nDIeAO6ix5sUVIVV+W9UMk2rSSmFT66CDOefRi7S028amiyYnFUK1QkIAaUbVUyOnYaErtbJwICuw== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/command" "^3.3.0" @@ -252,6 +275,7 @@ "@lerna/link@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/link/-/link-3.3.0.tgz#c0c05ff52d0f0c659fcf221627edfcd58e477a5c" + integrity sha512-8CeXzGL7okrsVXsy2sHXI2KuBaczw3cblAnA2+FJPUqSKMPNbUTRzeU3bOlCjYtK0LbxC4ngENJTL3jJ8RaYQQ== dependencies: "@lerna/command" "^3.3.0" "@lerna/package-graph" "^3.1.2" @@ -262,6 +286,7 @@ "@lerna/list@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@lerna/list/-/list-3.3.2.tgz#1412b3cce2a83b1baa4ff6fb962d50b46c28ec98" + integrity sha512-XXEVy7w+i/xx8NeJmGirw4upEoEF9OfD6XPLjISNQc24VgQV+frXdVJ02QcP7Y/PkY1rdIVrOjvo3ipKVLUxaQ== dependencies: "@lerna/command" "^3.3.0" "@lerna/filter-options" "^3.3.2" @@ -271,6 +296,7 @@ "@lerna/listable@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/listable/-/listable-3.0.0.tgz#27209b1382c87abdbc964220e75c247d803d4199" + integrity sha512-HX/9hyx1HLg2kpiKXIUc1EimlkK1T58aKQ7ovO7rQdTx9ForpefoMzyLnHE1n4XrUtEszcSWJIICJ/F898M6Ag== dependencies: chalk "^2.3.1" columnify "^1.5.4" @@ -278,6 +304,7 @@ "@lerna/log-packed@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@lerna/log-packed/-/log-packed-3.0.4.tgz#6d1f6ce5ca68b9971f2a27f0ecf3c50684be174a" + integrity sha512-vVQHgMagE2wnbxhNY9nFkdu+Cx2TsyWalkJfkxbNzmo6gOCrDsxCBDj9vTEV8Q+4aWx0C0Bsc0sB2Eb8y/+ofA== dependencies: byte-size "^4.0.3" columnify "^1.5.4" @@ -287,6 +314,7 @@ "@lerna/npm-conf@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@lerna/npm-conf/-/npm-conf-3.4.1.tgz#859e931b0bc9a5eed86309cc09508810c1e7d121" + integrity sha512-i9G6DnbCqiAqxKx2rSXej/n14qxlV/XOebL6QZonxJKzNTB+Q2wglnhTXmfZXTPJfoqimLaY4NfAEtbOXRWOXQ== dependencies: config-chain "^1.1.11" pify "^3.0.0" @@ -294,6 +322,7 @@ "@lerna/npm-dist-tag@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/npm-dist-tag/-/npm-dist-tag-3.3.0.tgz#e1c5ab67674216d901266a16846b21cc81ff6afd" + integrity sha512-EtZJXzh3w5tqXEev+EBBPrWKWWn0WgJfxm4FihfS9VgyaAW8udIVZHGkIQ3f+tBtupcAzA9Q8cQNUkGF2efwmA== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/get-npm-exec-opts" "^3.0.0" @@ -302,6 +331,7 @@ "@lerna/npm-install@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/npm-install/-/npm-install-3.3.0.tgz#16d00ffd668d11b2386b3ac68bdac2cf8320e533" + integrity sha512-WoVvKdS8ltROTGSNQwo6NDq0YKnjwhvTG4li1okcN/eHKOS3tL9bxbgPx7No0wOq5DKBpdeS9KhAfee6LFAZ5g== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/get-npm-exec-opts" "^3.0.0" @@ -314,6 +344,7 @@ "@lerna/npm-publish@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@lerna/npm-publish/-/npm-publish-3.3.1.tgz#30384665d7ee387343332ece62ca231207bbabea" + integrity sha512-bVTlWIcBL6Zpyzqvr9C7rxXYcoPw+l7IPz5eqQDNREj1R39Wj18OWB2KTJq8l7LIX7Wf4C2A1uT5hJaEf9BuvA== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/get-npm-exec-opts" "^3.0.0" @@ -326,6 +357,7 @@ "@lerna/npm-run-script@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/npm-run-script/-/npm-run-script-3.3.0.tgz#3c79601c27c67121155b20e039be53130217db72" + integrity sha512-YqDguWZzp4jIomaE4aWMUP7MIAJAFvRAf6ziQLpqwoQskfWLqK5mW0CcszT1oLjhfb3cY3MMfSTFaqwbdKmICg== dependencies: "@lerna/child-process" "^3.3.0" "@lerna/get-npm-exec-opts" "^3.0.0" @@ -334,12 +366,14 @@ "@lerna/output@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/output/-/output-3.0.0.tgz#4ed4a30ed2f311046b714b3840a090990ba3ce35" + integrity sha512-EFxnSbO0zDEVKkTKpoCUAFcZjc3gn3DwPlyTDxbeqPU7neCfxP4rA4+0a6pcOfTlRS5kLBRMx79F2TRCaMM3DA== dependencies: npmlog "^4.1.2" "@lerna/package-graph@^3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@lerna/package-graph/-/package-graph-3.1.2.tgz#b70298a3a8c82e12090da33233bf242223a38f20" + integrity sha512-9wIWb49I1IJmyjPdEVZQ13IAi9biGfH/OZHOC04U2zXGA0GLiY+B3CAx6FQvqkZ8xEGfqzmXnv3LvZ0bQfc1aQ== dependencies: "@lerna/validation-error" "^3.0.0" npm-package-arg "^6.0.0" @@ -348,6 +382,7 @@ "@lerna/package@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/package/-/package-3.0.0.tgz#14afc9a6cb1f7f7b23c1d7c7aa81bdac7d44c0e5" + integrity sha512-djzEJxzn212wS8d9znBnlXkeRlPL7GqeAYBykAmsuq51YGvaQK67Umh5ejdO0uxexF/4r7yRwgrlRHpQs8Rfqg== dependencies: npm-package-arg "^6.0.0" write-pkg "^3.1.0" @@ -355,6 +390,7 @@ "@lerna/project@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/project/-/project-3.0.0.tgz#4320d2a2b4080cabcf95161d9c48475217d8a545" + integrity sha512-XhDFVfqj79jG2Speggd15RpYaE8uiR25UKcQBDmumbmqvTS7xf2cvl2pq2UTvDafaJ0YwFF3xkxQZeZnFMwdkw== dependencies: "@lerna/package" "^3.0.0" "@lerna/validation-error" "^3.0.0" @@ -372,13 +408,15 @@ "@lerna/prompt@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@lerna/prompt/-/prompt-3.3.1.tgz#ec53f9034a7a02a671627241682947f65078ab88" + integrity sha512-eJhofrUCUaItMIH6et8kI7YqHfhjWqGZoTsE+40NRCfAraOMWx+pDzfRfeoAl3qeRAH2HhNj1bkYn70FbUOxuQ== dependencies: inquirer "^6.2.0" npmlog "^4.1.2" -"@lerna/publish@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.4.1.tgz#abbbc656b3bfafc2289399a46da060b90f6baf32" +"@lerna/publish@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.4.3.tgz#fb956ca2a871729982022889f90d0e8eb8528340" + integrity sha512-baeRL8xmOR25p86cAaS9mL0jdRzdv4dUo04PlK2Wes+YlL705F55cSXeC9npNie+9rGwFyLzCTQe18WdbZyLuw== dependencies: "@lerna/batch-packages" "^3.1.2" "@lerna/check-working-tree" "^3.3.0" @@ -410,6 +448,7 @@ "@lerna/resolve-symlink@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/resolve-symlink/-/resolve-symlink-3.3.0.tgz#c5d99a60cb17e2ea90b3521a0ba445478d194a44" + integrity sha512-KmoPDcFJ2aOK2inYHbrsiO9SodedUj0L1JDvDgirVNIjMUaQe2Q6Vi4Gh+VCJcyB27JtfHioV9R2NxU72Pk2hg== dependencies: fs-extra "^7.0.0" npmlog "^4.1.2" @@ -418,6 +457,7 @@ "@lerna/rimraf-dir@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/rimraf-dir/-/rimraf-dir-3.3.0.tgz#687e9bb3668a9e540e281302a52d9a573860f5db" + integrity sha512-vSqOcZ4kZduiSprbt+y40qziyN3VKYh+ygiCdnbBbsaxpdKB6CfrSMUtrLhVFrqUfBHIZRzHIzgjTdtQex1KLw== dependencies: "@lerna/child-process" "^3.3.0" npmlog "^4.1.2" @@ -427,6 +467,7 @@ "@lerna/run-lifecycle@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@lerna/run-lifecycle/-/run-lifecycle-3.4.1.tgz#6d7e44eada31cb4ec78b18ef050da0d86f6c892b" + integrity sha512-N/hi2srM9A4BWEkXccP7vCEbf4MmIuALF00DTBMvc0A/ccItwUpl3XNuM7+ADDRK0mkwE3hDw89lJ3A7f8oUQw== dependencies: "@lerna/npm-conf" "^3.4.1" npm-lifecycle "^2.0.0" @@ -435,6 +476,7 @@ "@lerna/run-parallel-batches@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/run-parallel-batches/-/run-parallel-batches-3.0.0.tgz#468704934084c74991d3124d80607857d4dfa840" + integrity sha512-Mj1ravlXF7AkkewKd9YFq9BtVrsStNrvVLedD/b2wIVbNqcxp8lS68vehXVOzoL/VWNEDotvqCQtyDBilCodGw== dependencies: p-map "^1.2.0" p-map-series "^1.0.0" @@ -442,6 +484,7 @@ "@lerna/run@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@lerna/run/-/run-3.3.2.tgz#f521f4a22585c90758f34a584cb1871f8bb2a83e" + integrity sha512-cruwRGZZWnQ5I0M+AqcoT3Xpq2wj3135iVw4n59/Op6dZu50sMFXZNLiTTTZ15k8rTKjydcccJMdPSpTHbH7/A== dependencies: "@lerna/batch-packages" "^3.1.2" "@lerna/command" "^3.3.0" @@ -455,6 +498,7 @@ "@lerna/symlink-binary@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/symlink-binary/-/symlink-binary-3.3.0.tgz#99ea570b21baabd61ecab27582eeb1d7b2c5f9cf" + integrity sha512-zRo6CimhvH/VJqCFl9T4IC6syjpWyQIxEfO2sBhrapEcfwjtwbhoGgKwucsvt4rIpFazCw63jQ/AXMT27KUIHg== dependencies: "@lerna/create-symlink" "^3.3.0" "@lerna/package" "^3.0.0" @@ -465,6 +509,7 @@ "@lerna/symlink-dependencies@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@lerna/symlink-dependencies/-/symlink-dependencies-3.3.0.tgz#13bcaed3e37986ab01b13498a459c7f609397dc3" + integrity sha512-IRngSNCmuD5uBKVv23tHMvr7Mplti0lKHilFKcvhbvhAfu6m/Vclxhkfs/uLyHzG+DeRpl/9o86SQET3h4XDhg== dependencies: "@lerna/create-symlink" "^3.3.0" "@lerna/resolve-symlink" "^3.3.0" @@ -477,12 +522,14 @@ "@lerna/validation-error@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/validation-error/-/validation-error-3.0.0.tgz#a27e90051c3ba71995e2a800a43d94ad04b3e3f4" + integrity sha512-5wjkd2PszV0kWvH+EOKZJWlHEqCTTKrWsvfHnHhcUaKBe/NagPZFWs+0xlsDPZ3DJt5FNfbAPAnEBQ05zLirFA== dependencies: npmlog "^4.1.2" "@lerna/version@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.4.1.tgz#029448cccd3ccefb4d5f666933bd13cfb37edab0" + integrity sha512-oefNaQLBJSI2WLZXw5XxDXk4NyF5/ct0V9ys/J308NpgZthPgwRPjk9ZR0o1IOxW1ABi6z3E317W/dxHDjvAkg== dependencies: "@lerna/batch-packages" "^3.1.2" "@lerna/check-working-tree" "^3.3.0" @@ -509,6 +556,7 @@ "@lerna/write-log-file@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@lerna/write-log-file/-/write-log-file-3.0.0.tgz#2f95fee80c6821fe1ee6ccf8173d2b4079debbd2" + integrity sha512-SfbPp29lMeEVOb/M16lJwn4nnx5y+TwCdd7Uom9umd7KcZP0NOvpnX0PHehdonl7TyHZ1Xx2maklYuCLbQrd/A== dependencies: npmlog "^4.1.2" write-file-atomic "^2.3.0" @@ -516,17 +564,20 @@ "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== dependencies: call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" "@nodelib/fs.stat@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.2.tgz#54c5a964462be3d4d78af631363c18d6fa91ac26" + version "1.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" + integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== JSONStream@^1.0.4, JSONStream@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.4.tgz#615bb2adb0cd34c8f4c447b5f6512fa1d8f16a2e" + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== dependencies: jsonparse "^1.2.0" through ">=2.2.7 <3" @@ -534,22 +585,26 @@ JSONStream@^1.0.4, JSONStream@^1.3.4: abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== dependencies: es6-promisify "^5.0.0" agentkeepalive@^3.4.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.1.tgz#4eba75cf2ad258fc09efd506cdb8d8c2971d35a4" + version "3.5.2" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" + integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ== dependencies: humanize-ms "^1.2.1" ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" @@ -559,32 +614,39 @@ ajv@^5.3.0: ansi-escapes@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" + integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== aproba@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== dependencies: delegates "^1.0.0" readable-stream "^2.0.6" @@ -592,98 +654,120 @@ are-we-there-yet@~1.1.2: argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= arr-flatten@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= array-differ@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" + integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= dependencies: array-uniq "^1.0.1" array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== dependencies: safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= async@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== dependencies: lodash "^4.17.10" asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= atob@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== dependencies: cache-base "^1.0.1" class-utils "^0.3.5" @@ -696,22 +780,26 @@ base@^0.11.1: bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= dependencies: tweetnacl "^0.14.3" block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= dependencies: inherits "~2.0.0" -bluebird@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.2.tgz#1be0908e054a751754549c270489c1505d4ab15a" +bluebird@^3.5.1, bluebird@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" + integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -719,6 +807,7 @@ brace-expansion@^1.1.7: braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== dependencies: arr-flatten "^1.1.0" array-unique "^0.3.2" @@ -734,26 +823,32 @@ braces@^2.3.1: buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= byte-size@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-4.0.3.tgz#b7c095efc68eadf82985fccd9a2df43a74fa2ccd" + version "4.0.4" + resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-4.0.4.tgz#29d381709f41aae0d89c631f1c81aec88cd40b23" + integrity sha512-82RPeneC6nqCdSwCX2hZUz3JPOvN5at/nTEw/CMf05Smu3Hrpo9Psb7LjN+k+XndNArG1EY8L4+BM3aTM4BCvw== -cacache@^11.0.1, cacache@^11.0.2: - version "11.2.0" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.2.0.tgz#617bdc0b02844af56310e411c0878941d5739965" +cacache@^11.0.1, cacache@^11.2.0: + version "11.3.1" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.1.tgz#d09d25f6c4aca7a6d305d141ae332613aa1d515f" + integrity sha512-2PEw4cRRDu+iQvBTTuttQifacYjLPhET+SYO/gEFMy8uhi+jlJREDAjSF5FWSdV/Aw5h18caHA7vMTw2c+wDzA== dependencies: bluebird "^3.5.1" chownr "^1.0.1" @@ -773,6 +868,7 @@ cacache@^11.0.1, cacache@^11.0.2: cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== dependencies: collection-visit "^1.0.0" component-emitter "^1.2.1" @@ -787,10 +883,31 @@ cache-base@^1.0.1: call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= dependencies: camelcase "^2.0.0" map-obj "^1.0.0" @@ -798,6 +915,7 @@ camelcase-keys@^2.0.0: camelcase-keys@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" + integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= dependencies: camelcase "^4.1.0" map-obj "^2.0.0" @@ -806,18 +924,22 @@ camelcase-keys@^4.0.0: camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= chalk@^2.0.0, chalk@^2.3.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" @@ -826,18 +948,22 @@ chalk@^2.0.0, chalk@^2.3.1: chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chownr@^1.0.1: +chownr@^1.0.1, chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" + integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== ci-info@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" + integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== dependencies: arr-union "^3.1.0" define-property "^0.2.5" @@ -847,16 +973,19 @@ class-utils@^0.3.5: cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= dependencies: restore-cursor "^2.0.0" cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= cliui@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== dependencies: string-width "^2.1.1" strip-ansi "^4.0.0" @@ -865,10 +994,12 @@ cliui@^4.0.0: clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= cmd-shim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" + integrity sha1-b8vamUg6j9FdfTChlspp1oii79s= dependencies: graceful-fs "^4.1.2" mkdirp "~0.5.0" @@ -876,14 +1007,17 @@ cmd-shim@^2.0.2: co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= dependencies: map-visit "^1.0.0" object-visit "^1.0.0" @@ -891,39 +1025,39 @@ collection-visit@^1.0.0: color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= columnify@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" + integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= dependencies: strip-ansi "^3.0.0" wcwidth "^1.0.0" -combined-stream@1.0.6: - version "1.0.6" - resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" - dependencies: - delayed-stream "~1.0.0" - -combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== dependencies: delayed-stream "~1.0.0" commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== compare-func@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648" + integrity sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg= dependencies: array-ify "^1.0.0" dot-prop "^3.0.0" @@ -931,14 +1065,17 @@ compare-func@^1.3.1: component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= concat-stream@^1.5.0, concat-stream@^1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== dependencies: buffer-from "^1.0.0" inherits "^2.0.3" @@ -948,6 +1085,7 @@ concat-stream@^1.5.0, concat-stream@^1.6.0: config-chain@^1.1.11: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" + integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== dependencies: ini "^1.3.4" proto-list "~1.2.1" @@ -955,42 +1093,47 @@ config-chain@^1.1.11: console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= conventional-changelog-angular@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.1.tgz#f96431b76de453333a909decd02b15cb5bd2d364" + version "5.0.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.2.tgz#39d945635e03b6d0c9d4078b1df74e06163dc66a" + integrity sha512-yx7m7lVrXmt4nKWQgWZqxSALEiAKZhOAcbxdUaU9575mB0CzXVbgrgpfSnSP7OqWDUTYGD0YVJ0MSRdyOPgAwA== dependencies: compare-func "^1.3.1" q "^1.5.1" conventional-changelog-core@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-3.1.0.tgz#96a81bb3301b4b2a3dc2851cc54c5fb674ac1942" + version "3.1.5" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-3.1.5.tgz#c2edf928539308b54fe1b90a2fc731abc021852c" + integrity sha512-iwqAotS4zk0wA4S84YY1JCUG7X3LxaRjJxuUo6GI4dZuIy243j5nOg/Ora35ExT4DOiw5dQbMMQvw2SUjh6moQ== dependencies: - conventional-changelog-writer "^4.0.0" - conventional-commits-parser "^3.0.0" + conventional-changelog-writer "^4.0.2" + conventional-commits-parser "^3.0.1" dateformat "^3.0.0" get-pkg-repo "^1.0.0" - git-raw-commits "^2.0.0" + git-raw-commits "2.0.0" git-remote-origin-url "^2.0.0" - git-semver-tags "^2.0.0" + git-semver-tags "^2.0.2" lodash "^4.2.1" normalize-package-data "^2.3.5" q "^1.5.1" - read-pkg "^1.1.0" - read-pkg-up "^1.0.1" + read-pkg "^3.0.0" + read-pkg-up "^3.0.0" through2 "^2.0.0" -conventional-changelog-preset-loader@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.0.1.tgz#d134734e0cc1b91b88b30586c5991f31442029f1" +conventional-changelog-preset-loader@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.0.2.tgz#81d1a07523913f3d17da3a49f0091f967ad345b0" + integrity sha512-pBY+qnUoJPXAXXqVGwQaVmcye05xi6z231QM98wHWamGAmu/ghkBprQAwmF5bdmyobdVxiLhPY3PrCfSeUNzRQ== -conventional-changelog-writer@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.0.0.tgz#3ed983c8ef6a3aa51fe44e82c9c75e86f1b5aa42" +conventional-changelog-writer@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.0.2.tgz#eb493ed84269e7a663da36e49af51c54639c9a67" + integrity sha512-d8/FQY/fix2xXEBUhOo8u3DCbyEw3UOQgYHxLsPDw+wHUDma/GQGAGsGtoH876WyNs32fViHmTOUrgRKVLvBug== dependencies: compare-func "^1.3.1" - conventional-commits-filter "^2.0.0" + conventional-commits-filter "^2.0.1" dateformat "^3.0.0" handlebars "^4.0.2" json-stringify-safe "^5.0.1" @@ -1000,16 +1143,18 @@ conventional-changelog-writer@^4.0.0: split "^1.0.0" through2 "^2.0.0" -conventional-commits-filter@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.0.tgz#a0ce1d1ff7a1dd7fab36bee8e8256d348d135651" +conventional-commits-filter@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.1.tgz#55a135de1802f6510b6758e0a6aa9e0b28618db3" + integrity sha512-92OU8pz/977udhBjgPEbg3sbYzIxMDFTlQT97w7KdhR9igNqdJvy8smmedAAgn4tPiqseFloKkrVfbXCVd+E7A== dependencies: is-subset "^0.1.1" modify-values "^1.0.0" -conventional-commits-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.0.0.tgz#7f604549a50bd8f60443fbe515484b1c2f06a5c4" +conventional-commits-parser@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.0.1.tgz#fe1c49753df3f98edb2285a5e485e11ffa7f2e4c" + integrity sha512-P6U5UOvDeidUJ8ebHVDIoXzI7gMlQ1OF/id6oUvp8cnZvOXMt1n8nYl74Ey9YMn0uVQtxmCtjPQawpsssBWtGg== dependencies: JSONStream "^1.0.4" is-text-path "^1.0.0" @@ -1020,21 +1165,23 @@ conventional-commits-parser@^3.0.0: trim-off-newlines "^1.0.0" conventional-recommended-bump@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-4.0.1.tgz#304a45a412cfec050a10ea2e7e4a89320eaf3991" + version "4.0.4" + resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-4.0.4.tgz#05540584641d3da758c8863c09788fcaeb586872" + integrity sha512-9mY5Yoblq+ZMqJpBzgS+RpSq+SUfP2miOR3H/NR9drGf08WCrY9B6HAGJZEm6+ThsVP917VHAahSOjM6k1vhPg== dependencies: concat-stream "^1.6.0" - conventional-changelog-preset-loader "^2.0.1" - conventional-commits-filter "^2.0.0" - conventional-commits-parser "^3.0.0" - git-raw-commits "^2.0.0" - git-semver-tags "^2.0.0" + conventional-changelog-preset-loader "^2.0.2" + conventional-commits-filter "^2.0.1" + conventional-commits-parser "^3.0.1" + git-raw-commits "2.0.0" + git-semver-tags "^2.0.2" meow "^4.0.0" q "^1.5.1" copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== dependencies: aproba "^1.1.1" fs-write-stream-atomic "^1.0.8" @@ -1046,15 +1193,19 @@ copy-concurrently@^1.0.0: copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= cosmiconfig@^5.0.2: - version "5.0.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.6.tgz#dca6cf680a0bd03589aff684700858c81abeeb39" + version "5.0.7" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" + integrity sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA== dependencies: + import-fresh "^2.0.0" is-directory "^0.3.1" js-yaml "^3.9.0" parse-json "^4.0.0" @@ -1062,6 +1213,7 @@ cosmiconfig@^5.0.2: cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== dependencies: nice-try "^1.0.4" path-key "^2.0.1" @@ -1072,54 +1224,64 @@ cross-spawn@^6.0.0: currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= dependencies: array-find-index "^1.0.1" cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= dargs@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17" + integrity sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc= dependencies: number-is-nan "^1.0.0" dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= dependencies: assert-plus "^1.0.0" dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== debug@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== dependencies: ms "2.0.0" debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" debug@^3.1.0: - version "3.2.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.5.tgz#c2418fbfd7a29f4d4f70ff4cea604d4b64c46407" + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= decamelize-keys@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= dependencies: decamelize "^1.1.0" map-obj "^1.0.0" @@ -1127,42 +1289,50 @@ decamelize-keys@^1.0.0: decamelize@^1.1.0, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= decamelize@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" + integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg== dependencies: xregexp "4.0.0" decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= dependencies: clone "^1.0.2" define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= dependencies: is-descriptor "^0.1.0" define-property@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= dependencies: is-descriptor "^1.0.0" define-property@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== dependencies: is-descriptor "^1.0.2" isobject "^3.0.1" @@ -1170,18 +1340,22 @@ define-property@^2.0.2: delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" + integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= dependencies: asap "^2.0.0" wrappy "1" @@ -1189,6 +1363,7 @@ dezalgo@^1.0.0: dir-glob@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + integrity sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag== dependencies: arrify "^1.0.1" path-type "^3.0.0" @@ -1196,22 +1371,26 @@ dir-glob@^2.0.0: dot-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" + integrity sha1-G3CK8JSknJoOfbyteQq6U52sEXc= dependencies: is-obj "^1.0.0" dot-prop@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== dependencies: is-obj "^1.0.0" duplexer@^0.1.1: version "0.1.1" - resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= duplexify@^3.4.2, duplexify@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410" + version "3.6.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.1.tgz#b1a7a29c4abfd639585efaecce80d666b1e34125" + integrity sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA== dependencies: end-of-stream "^1.0.0" inherits "^2.0.1" @@ -1221,6 +1400,7 @@ duplexify@^3.4.2, duplexify@^3.6.0: ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= dependencies: jsbn "~0.1.0" safer-buffer "^2.1.0" @@ -1228,46 +1408,55 @@ ecc-jsbn@~0.1.1: encoding@^0.1.11: version "0.1.12" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= dependencies: iconv-lite "~0.4.13" end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== dependencies: once "^1.4.0" err-code@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" + integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" es6-promise@^4.0.3: version "4.2.5" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" + integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== es6-promisify@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= dependencies: es6-promise "^4.0.3" escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== execa@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" + integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== dependencies: cross-spawn "^6.0.0" get-stream "^3.0.0" @@ -1280,6 +1469,7 @@ execa@^0.10.0: execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: cross-spawn "^6.0.0" get-stream "^4.0.0" @@ -1292,6 +1482,7 @@ execa@^1.0.0: expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= dependencies: debug "^2.3.3" define-property "^0.2.5" @@ -1304,12 +1495,14 @@ expand-brackets@^2.1.4: extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= dependencies: is-extendable "^0.1.0" extend-shallow@^3.0.0, extend-shallow@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= dependencies: assign-symbols "^1.0.0" is-extendable "^1.0.1" @@ -1317,10 +1510,12 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== external-editor@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== dependencies: chardet "^0.7.0" iconv-lite "^0.4.24" @@ -1329,6 +1524,7 @@ external-editor@^3.0.0: extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== dependencies: array-unique "^0.3.2" define-property "^1.0.0" @@ -1342,18 +1538,22 @@ extglob@^2.0.4: extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= fast-glob@^2.0.2: version "2.2.3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.3.tgz#d09d378e9ef6b0076a0fa1ba7519d9d4d9699c28" + integrity sha512-NiX+JXjnx43RzvVFwRWfPKo4U+1BrK5pJPsHQdKMlLoFHrrGktXglQhHliSihWAq+m1z6fHk3uwGHrtRbS9vLA== dependencies: "@mrmlnc/readdir-enhanced" "^2.2.1" "@nodelib/fs.stat" "^1.0.1" @@ -1365,20 +1565,24 @@ fast-glob@^2.0.2: fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -figgy-pudding@^3.1.0, figgy-pudding@^3.2.1, figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: +figgy-pudding@^3.1.0, figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" + integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= dependencies: escape-string-regexp "^1.0.5" fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= dependencies: extend-shallow "^2.0.1" is-number "^3.0.0" @@ -1388,6 +1592,7 @@ fill-range@^4.0.0: find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= dependencies: path-exists "^2.0.0" pinkie-promise "^2.0.0" @@ -1395,18 +1600,21 @@ find-up@^1.0.0: find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= dependencies: locate-path "^2.0.0" find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== dependencies: locate-path "^3.0.0" flush-write-stream@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" + integrity sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw== dependencies: inherits "^2.0.1" readable-stream "^2.0.4" @@ -1414,35 +1622,41 @@ flush-write-stream@^1.0.0: for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= form-data@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== dependencies: asynckit "^0.4.0" - combined-stream "1.0.6" + combined-stream "^1.0.6" mime-types "^2.1.12" fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= dependencies: map-cache "^0.2.2" from2@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= dependencies: inherits "^2.0.1" readable-stream "^2.0.0" fs-extra@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6" + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== dependencies: graceful-fs "^4.1.2" jsonfile "^4.0.0" @@ -1451,12 +1665,14 @@ fs-extra@^7.0.0: fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== dependencies: minipass "^2.2.1" fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= dependencies: graceful-fs "^4.1.2" iferr "^0.1.5" @@ -1466,10 +1682,12 @@ fs-write-stream-atomic@^1.0.8: fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fstream@^1.0.0, fstream@^1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + integrity sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE= dependencies: graceful-fs "^4.1.2" inherits "~2.0.0" @@ -1479,6 +1697,7 @@ fstream@^1.0.0, fstream@^1.0.2: gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= dependencies: aproba "^1.0.3" console-control-strings "^1.0.0" @@ -1489,17 +1708,20 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -genfun@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/genfun/-/genfun-4.0.1.tgz#ed10041f2e4a7f1b0a38466d17a5c3e27df1dfc1" +genfun@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" + integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== get-pkg-repo@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" + integrity sha1-xztInAbYDMVTbCyFP54FIyBWly0= dependencies: hosted-git-info "^2.1.4" meow "^3.3.0" @@ -1510,34 +1732,41 @@ get-pkg-repo@^1.0.0: get-port@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" + integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw= get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= get-stream@^3.0.0: version "3.0.0" - resolved "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= -get-stream@^4.0.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= dependencies: assert-plus "^1.0.0" -git-raw-commits@^2.0.0: +git-raw-commits@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" + integrity sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg== dependencies: dargs "^4.0.1" lodash.template "^4.0.2" @@ -1548,13 +1777,15 @@ git-raw-commits@^2.0.0: git-remote-origin-url@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" + integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= dependencies: gitconfiglocal "^1.0.0" pify "^2.3.0" -git-semver-tags@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-2.0.0.tgz#c218fd895bdf8e8e02f6bde555b2c3893ac73cd7" +git-semver-tags@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-2.0.2.tgz#f506ec07caade191ac0c8d5a21bdb8131b4934e3" + integrity sha512-34lMF7Yo1xEmsK2EkbArdoU79umpvm0MfzaDkSNYSJqtM5QLAVTPWgpiXSVI5o/O9EvZPSrP4Zvnec/CqhSd5w== dependencies: meow "^4.0.0" semver "^5.5.0" @@ -1562,12 +1793,14 @@ git-semver-tags@^2.0.0: gitconfiglocal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" + integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= dependencies: ini "^1.3.2" glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= dependencies: is-glob "^3.1.0" path-dirname "^1.0.0" @@ -1575,10 +1808,12 @@ glob-parent@^3.1.0: glob-to-regexp@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: +glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -1590,6 +1825,7 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: globby@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" + integrity sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw== dependencies: array-union "^1.0.1" dir-glob "^2.0.0" @@ -1600,12 +1836,14 @@ globby@^8.0.1: slash "^1.0.0" graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== handlebars@^4.0.2: version "4.0.12" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" + integrity sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA== dependencies: async "^2.5.0" optimist "^0.6.1" @@ -1616,10 +1854,12 @@ handlebars@^4.0.2: har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= har-validator@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + integrity sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA== dependencies: ajv "^5.3.0" har-schema "^2.0.0" @@ -1627,14 +1867,17 @@ har-validator@~5.1.0: has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= dependencies: get-value "^2.0.3" has-values "^0.1.4" @@ -1643,6 +1886,7 @@ has-value@^0.3.1: has-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= dependencies: get-value "^2.0.6" has-values "^1.0.0" @@ -1651,10 +1895,12 @@ has-value@^1.0.0: has-values@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= has-values@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= dependencies: is-number "^3.0.0" kind-of "^4.0.0" @@ -1662,14 +1908,17 @@ has-values@^1.0.0: hosted-git-info@^2.1.4, hosted-git-info@^2.6.0: version "2.7.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" + integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== http-cache-semantics@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" + integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== http-proxy-agent@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== dependencies: agent-base "4" debug "3.1.0" @@ -1677,6 +1926,7 @@ http-proxy-agent@^2.1.0: http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= dependencies: assert-plus "^1.0.0" jsprim "^1.2.2" @@ -1685,6 +1935,7 @@ http-signature@~1.2.0: https-proxy-agent@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ== dependencies: agent-base "^4.1.0" debug "^3.1.0" @@ -1692,32 +1943,46 @@ https-proxy-agent@^2.2.1: humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= dependencies: ms "^2.0.0" iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" iferr@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= ignore-walk@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== dependencies: minimatch "^3.0.4" ignore@^3.3.5: version "3.3.10" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" import-local@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" + integrity sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ== dependencies: pkg-dir "^2.0.0" resolve-cwd "^2.0.0" @@ -1725,20 +1990,24 @@ import-local@^1.0.0: imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= indent-string@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= dependencies: repeating "^2.0.0" indent-string@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" wrappy "1" @@ -1746,14 +2015,17 @@ inflight@^1.0.4: inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.2, ini@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== init-package-json@^1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe" + integrity sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw== dependencies: glob "^7.1.1" npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0" @@ -1767,6 +2039,7 @@ init-package-json@^1.10.3: inquirer@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" + integrity sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg== dependencies: ansi-escapes "^3.0.0" chalk "^2.0.0" @@ -1785,58 +2058,69 @@ inquirer@^6.2.0: invert-kv@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= dependencies: kind-of "^3.0.2" is-accessor-descriptor@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== dependencies: kind-of "^6.0.0" is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== is-builtin-module@^1.0.0: version "1.0.0" - resolved "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74= dependencies: builtin-modules "^1.0.0" is-ci@^1.0.10: version "1.2.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" + integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== dependencies: ci-info "^1.5.0" is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= dependencies: kind-of "^3.0.2" is-data-descriptor@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== dependencies: kind-of "^6.0.0" is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== dependencies: is-accessor-descriptor "^0.1.6" is-data-descriptor "^0.1.4" @@ -1845,6 +2129,7 @@ is-descriptor@^0.1.0: is-descriptor@^1.0.0, is-descriptor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== dependencies: is-accessor-descriptor "^1.0.0" is-data-descriptor "^1.0.0" @@ -1853,124 +2138,150 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-directory@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= is-extendable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== dependencies: is-plain-object "^2.0.4" is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= dependencies: number-is-nan "^1.0.0" is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= dependencies: number-is-nan "^1.0.0" is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= dependencies: is-extglob "^2.1.0" is-glob@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A= dependencies: is-extglob "^2.1.1" is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= dependencies: kind-of "^3.0.2" is-obj@^1.0.0: version "1.0.1" - resolved "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== dependencies: isobject "^3.0.1" is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= is-subset@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= is-text-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" + integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= dependencies: text-extensions "^1.0.0" is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= dependencies: isarray "1.0.0" isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" + integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -1978,36 +2289,44 @@ js-yaml@^3.9.0: jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= optionalDependencies: graceful-fs "^4.1.6" jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= dependencies: assert-plus "1.0.0" extsprintf "1.3.0" @@ -2017,32 +2336,38 @@ jsprim@^1.2.2: kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= dependencies: is-buffer "^1.1.5" kind-of@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= dependencies: is-buffer "^1.1.5" kind-of@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== dependencies: invert-kv "^2.0.0" -lerna@^3.2.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.4.1.tgz#4acc5a6b9d843993db7a7bb1350274bcaf20ca80" +lerna@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.4.3.tgz#501454efb453c65c305802d370ee337f7298787e" + integrity sha512-tWq1LvpHqkyB+FaJCmkEweivr88yShDMmauofPVdh0M5gU1cVucszYnIgWafulKYu2LMQ3IfUMUU5Pp3+MvADQ== dependencies: "@lerna/add" "^3.4.1" "@lerna/bootstrap" "^3.4.1" @@ -2056,7 +2381,7 @@ lerna@^3.2.1: "@lerna/init" "^3.3.0" "@lerna/link" "^3.3.0" "@lerna/list" "^3.3.2" - "@lerna/publish" "^3.4.1" + "@lerna/publish" "^3.4.3" "@lerna/run" "^3.3.2" "@lerna/version" "^3.4.1" import-local "^1.0.0" @@ -2065,6 +2390,7 @@ lerna@^3.2.1: libnpmaccess@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-3.0.0.tgz#33cc9c8a5cb53e87d06bf2e547c2eba974f619af" + integrity sha512-SiE4AZAzMpD7pmmXHfgD7rof8QIQGoKaeyAS8exgx2CKA6tzRTbRljq1xM4Tgj8/tIg+KBJPJWkR0ifqKT3irQ== dependencies: aproba "^2.0.0" get-stream "^4.0.0" @@ -2073,7 +2399,8 @@ libnpmaccess@^3.0.0: load-json-file@^1.0.0: version "1.1.0" - resolved "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= dependencies: graceful-fs "^4.1.2" parse-json "^2.2.0" @@ -2084,6 +2411,7 @@ load-json-file@^1.0.0: load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= dependencies: graceful-fs "^4.1.2" parse-json "^4.0.0" @@ -2093,6 +2421,7 @@ load-json-file@^4.0.0: locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= dependencies: p-locate "^2.0.0" path-exists "^3.0.0" @@ -2100,6 +2429,7 @@ locate-path@^2.0.0: locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== dependencies: p-locate "^3.0.0" path-exists "^3.0.0" @@ -2107,14 +2437,17 @@ locate-path@^3.0.0: lodash._reinterpolate@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= lodash.template@^4.0.2: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A= dependencies: lodash._reinterpolate "~3.0.0" lodash.templatesettings "^4.0.0" @@ -2122,16 +2455,19 @@ lodash.template@^4.0.2: lodash.templatesettings@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY= dependencies: lodash._reinterpolate "~3.0.0" lodash@^4.17.10, lodash@^4.17.5, lodash@^4.2.1: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= dependencies: currently-unhandled "^0.4.1" signal-exit "^3.0.0" @@ -2139,6 +2475,7 @@ loud-rejection@^1.0.0: lru-cache@^4.1.2, lru-cache@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" + integrity sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA== dependencies: pseudomap "^1.0.2" yallist "^2.1.2" @@ -2146,12 +2483,14 @@ lru-cache@^4.1.2, lru-cache@^4.1.3: make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== dependencies: pify "^3.0.0" make-fetch-happen@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-4.0.1.tgz#141497cb878f243ba93136c83d8aba12c216c083" + integrity sha512-7R5ivfy9ilRJ1EMKIOziwrns9fGeAD4bAha8EB7BIiBBLHm2KeTUGCrICFt2rbHfzheTLynv50GnNTK1zDTrcQ== dependencies: agentkeepalive "^3.4.1" cacache "^11.0.1" @@ -2168,30 +2507,36 @@ make-fetch-happen@^4.0.1: map-age-cleaner@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz#098fb15538fd3dbe461f12745b0ca8568d4e3f74" + integrity sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ== dependencies: p-defer "^1.0.0" map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= map-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" + integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= dependencies: object-visit "^1.0.0" mem@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf" + integrity sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA== dependencies: map-age-cleaner "^0.1.1" mimic-fn "^1.0.0" @@ -2200,6 +2545,7 @@ mem@^4.0.0: meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= dependencies: camelcase-keys "^2.0.0" decamelize "^1.1.2" @@ -2215,6 +2561,7 @@ meow@^3.3.0: meow@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/meow/-/meow-4.0.1.tgz#d48598f6f4b1472f35bf6317a95945ace347f975" + integrity sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A== dependencies: camelcase-keys "^4.0.0" decamelize-keys "^1.0.0" @@ -2227,12 +2574,14 @@ meow@^4.0.0: trim-newlines "^2.0.0" merge2@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34" + version "1.2.3" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" + integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA== micromatch@^3.1.10: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -2248,61 +2597,72 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" -mime-db@~1.36.0: - version "1.36.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" +mime-db@~1.37.0: + version "1.37.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" + integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.20" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + version "2.1.21" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" + integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== dependencies: - mime-db "~1.36.0" + mime-db "~1.37.0" mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== minimatch@^3.0.0, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" minimist-options@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" + integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== dependencies: arrify "^1.0.1" is-plain-obj "^1.1.0" minimist@0.0.8: version "0.0.8" - resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" - resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= minimist@~0.0.1: version "0.0.10" - resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= -minipass@^2.2.1, minipass@^2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957" +minipass@^2.2.1, minipass@^2.3.4, minipass@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" + integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== dependencies: safe-buffer "^5.1.2" yallist "^3.0.0" -minizlib@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" +minizlib@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.1.tgz#6734acc045a46e61d596a43bb9d9cd326e19cc42" + integrity sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg== dependencies: minipass "^2.2.1" mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== dependencies: concat-stream "^1.5.0" duplexify "^3.4.2" @@ -2318,23 +2678,27 @@ mississippi@^3.0.0: mixin-deep@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== dependencies: for-in "^1.0.2" is-extendable "^1.0.1" "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: version "0.5.1" - resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" + integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= dependencies: aproba "^1.1.1" copy-concurrently "^1.0.0" @@ -2346,14 +2710,17 @@ move-concurrently@^1.0.1: ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== multimatch@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" + integrity sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis= dependencies: array-differ "^1.0.0" array-union "^1.0.1" @@ -2363,10 +2730,12 @@ multimatch@^2.1.0: mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -2383,10 +2752,12 @@ nanomatch@^1.2.9: nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== node-fetch-npm@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7" + integrity sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw== dependencies: encoding "^0.1.11" json-parse-better-errors "^1.0.0" @@ -2395,6 +2766,7 @@ node-fetch-npm@^2.0.2: node-gyp@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" + integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== dependencies: fstream "^1.0.0" glob "^7.0.3" @@ -2412,12 +2784,14 @@ node-gyp@^3.8.0: "nopt@2 || 3": version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= dependencies: abbrev "1" normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw== dependencies: hosted-git-info "^2.1.4" is-builtin-module "^1.0.0" @@ -2427,10 +2801,12 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package- npm-bundled@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" + integrity sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g== npm-lifecycle@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.1.0.tgz#1eda2eedb82db929e3a0c50341ab0aad140ed569" + integrity sha512-QbBfLlGBKsktwBZLj6AviHC6Q9Y3R/AY4a2PYSIRhSKSS0/CxRyD/PfxEX6tPeOCXQgMSNdwGeECacstgptc+g== dependencies: byline "^5.0.0" graceful-fs "^4.1.11" @@ -2444,29 +2820,34 @@ npm-lifecycle@^2.0.0: "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1" + integrity sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA== dependencies: hosted-git-info "^2.6.0" osenv "^0.1.5" semver "^5.5.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.10: - version "1.1.11" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" +npm-packlist@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a" + integrity sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" -npm-pick-manifest@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.1.0.tgz#dc381bdd670c35d81655e1d5a94aa3dd4d87fce5" +npm-pick-manifest@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz#32111d2a9562638bb2c8f2bf27f7f3092c8fae40" + integrity sha512-+IluBC5K201+gRU85vFlUwX3PFShZAbAgDNp2ewJdWMVSppdo/Zih0ul2Ecky/X7b51J7LrrUAP+XOmOCvYZqA== dependencies: + figgy-pudding "^3.5.1" npm-package-arg "^6.0.0" semver "^5.4.1" -npm-registry-fetch@^3.0.0, npm-registry-fetch@^3.8.0: +npm-registry-fetch@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-3.8.0.tgz#aa7d9a7c92aff94f48dba0984bdef4bd131c88cc" + integrity sha512-hrw8UMD+Nob3Kl3h8Z/YjmKamb1gf7D1ZZch2otrIXM3uFLB5vjEY6DhMlq80z/zZet6eETLbOXcuQudCB3Zpw== dependencies: JSONStream "^1.3.4" bluebird "^3.5.1" @@ -2478,12 +2859,14 @@ npm-registry-fetch@^3.0.0, npm-registry-fetch@^3.8.0: npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= dependencies: path-key "^2.0.0" "npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== dependencies: are-we-there-yet "~1.1.2" console-control-strings "~1.1.0" @@ -2493,18 +2876,22 @@ npm-run-path@^2.0.0: number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= dependencies: copy-descriptor "^0.1.0" define-property "^0.2.5" @@ -2513,30 +2900,35 @@ object-copy@^0.1.0: object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= dependencies: isobject "^3.0.0" object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= dependencies: isobject "^3.0.1" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" onetime@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= dependencies: mimic-fn "^1.0.0" optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= dependencies: minimist "~0.0.1" wordwrap "~0.0.2" @@ -2544,10 +2936,12 @@ optimist@^0.6.1: os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= os-locale@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620" + integrity sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw== dependencies: execa "^0.10.0" lcid "^2.0.0" @@ -2556,10 +2950,12 @@ os-locale@^3.0.0: os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= osenv@0, osenv@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== dependencies: os-homedir "^1.0.0" os-tmpdir "^1.0.0" @@ -2567,106 +2963,122 @@ osenv@0, osenv@^0.1.5: p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= p-is-promise@^1.1.0: version "1.1.0" - resolved "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== dependencies: p-try "^1.0.0" p-limit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" + integrity sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A== dependencies: p-try "^2.0.0" p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= dependencies: p-limit "^1.1.0" p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== dependencies: p-limit "^2.0.0" p-map-series@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" + integrity sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco= dependencies: p-reduce "^1.0.0" p-map@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== p-pipe@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" + integrity sha1-SxoROZoRUgpneQ7loMHViB1r7+k= p-reduce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= p-try@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" + integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== p-waterfall@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-waterfall/-/p-waterfall-1.0.0.tgz#7ed94b3ceb3332782353af6aae11aa9fc235bb00" + integrity sha1-ftlLPOszMngjU69qrhGqn8I1uwA= dependencies: p-reduce "^1.0.0" pacote@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.1.0.tgz#59810859bbd72984dcb267269259375d32f391e5" + version "9.2.3" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.2.3.tgz#48cfe87beb9177acd6594355a584a538835424b3" + integrity sha512-Y3+yY3nBRAxMlZWvr62XLJxOwCmG9UmkGZkFurWHoCjqF0cZL72cTOCRJTvWw8T4OhJS2RTg13x4oYYriauvEw== dependencies: - bluebird "^3.5.1" - cacache "^11.0.2" - figgy-pudding "^3.2.1" - get-stream "^3.0.0" - glob "^7.1.2" + bluebird "^3.5.2" + cacache "^11.2.0" + figgy-pudding "^3.5.1" + get-stream "^4.1.0" + glob "^7.1.3" lru-cache "^4.1.3" make-fetch-happen "^4.0.1" minimatch "^3.0.4" - minipass "^2.3.3" + minipass "^2.3.5" mississippi "^3.0.0" mkdirp "^0.5.1" normalize-package-data "^2.4.0" npm-package-arg "^6.1.0" - npm-packlist "^1.1.10" - npm-pick-manifest "^2.1.0" - npm-registry-fetch "^3.0.0" + npm-packlist "^1.1.12" + npm-pick-manifest "^2.2.3" + npm-registry-fetch "^3.8.0" osenv "^0.1.5" promise-inflight "^1.0.1" promise-retry "^1.1.1" - protoduck "^5.0.0" + protoduck "^5.0.1" rimraf "^2.6.2" safe-buffer "^5.1.2" - semver "^5.5.0" - ssri "^6.0.0" - tar "^4.4.3" - unique-filename "^1.1.0" - which "^1.3.0" + semver "^5.6.0" + ssri "^6.0.1" + tar "^4.4.6" + unique-filename "^1.1.1" + which "^1.3.1" parallel-transform@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY= dependencies: cyclist "~0.2.2" inherits "^2.0.3" @@ -2675,16 +3087,19 @@ parallel-transform@^1.1.0: parse-github-repo-url@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" + integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= dependencies: error-ex "^1.2.0" parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= dependencies: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" @@ -2692,32 +3107,39 @@ parse-json@^4.0.0: pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= dependencies: pinkie-promise "^2.0.0" path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= dependencies: graceful-fs "^4.1.2" pify "^2.0.0" @@ -2726,52 +3148,63 @@ path-type@^1.0.0: path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== dependencies: pify "^3.0.0" performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= dependencies: pinkie "^2.0.0" pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= dependencies: find-up "^2.1.0" posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= promise-retry@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" + integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= dependencies: err-code "^1.0.0" retry "^0.10.0" @@ -2779,30 +3212,36 @@ promise-retry@^1.1.1: promzard@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" + integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4= dependencies: read "1" proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= -protoduck@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.0.tgz#752145e6be0ad834cb25716f670a713c860dce70" +protoduck@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f" + integrity sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg== dependencies: - genfun "^4.0.1" + genfun "^5.0.0" pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= psl@^1.1.24: version "1.1.29" resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" + integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ== pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== dependencies: end-of-stream "^1.1.0" once "^1.3.1" @@ -2810,6 +3249,7 @@ pump@^2.0.0: pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== dependencies: end-of-stream "^1.1.0" once "^1.3.1" @@ -2817,6 +3257,7 @@ pump@^3.0.0: pumpify@^1.3.3: version "1.5.1" resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== dependencies: duplexify "^3.6.0" inherits "^2.0.3" @@ -2825,32 +3266,39 @@ pumpify@^1.3.3: punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== quick-lru@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" + integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= read-cmd-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz#2d5d157786a37c055d22077c32c53f8329e91c7b" + integrity sha1-LV0Vd4ajfAVdIgd8MsU/gynpHHs= dependencies: graceful-fs "^4.1.2" "read-package-json@1 || 2", read-package-json@^2.0.0: version "2.0.13" resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.13.tgz#2e82ebd9f613baa6d2ebe3aa72cefe3f68e41f4a" + integrity sha512-/1dZ7TRZvGrYqE0UAfN6qQb5GYBsNcqS1C0tNK601CFOJmtHI7NIGXwetEPU/OtoFHZL3hDxm4rolFFVE9Bnmg== dependencies: glob "^7.1.1" json-parse-better-errors "^1.0.1" @@ -2862,6 +3310,7 @@ read-cmd-shim@^1.0.1: read-package-tree@^5.1.6: version "5.2.1" resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.2.1.tgz#6218b187d6fac82289ce4387bbbaf8eef536ad63" + integrity sha512-2CNoRoh95LxY47LvqrehIAfUVda2JbuFE/HaGYs42bNrGG+ojbw1h3zOcPcQ+1GQ3+rkzNndZn85u1XyZ3UsIA== dependencies: debuglog "^1.0.1" dezalgo "^1.0.0" @@ -2872,6 +3321,7 @@ read-package-tree@^5.1.6: read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= dependencies: find-up "^1.0.0" read-pkg "^1.0.0" @@ -2879,13 +3329,15 @@ read-pkg-up@^1.0.1: read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= dependencies: find-up "^2.0.0" read-pkg "^3.0.0" -read-pkg@^1.0.0, read-pkg@^1.1.0: +read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= dependencies: load-json-file "^1.0.0" normalize-package-data "^2.3.2" @@ -2894,6 +3346,7 @@ read-pkg@^1.0.0, read-pkg@^1.1.0: read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= dependencies: load-json-file "^4.0.0" normalize-package-data "^2.3.2" @@ -2902,12 +3355,14 @@ read-pkg@^3.0.0: read@1, read@~1.0.1: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.6" - resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -2920,6 +3375,7 @@ read@1, read@~1.0.1: readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" + integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c= dependencies: debuglog "^1.0.1" dezalgo "^1.0.0" @@ -2929,6 +3385,7 @@ readdir-scoped-modules@^1.0.0: redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= dependencies: indent-string "^2.1.0" strip-indent "^1.0.1" @@ -2936,6 +3393,7 @@ redent@^1.0.0: redent@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" + integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= dependencies: indent-string "^3.0.0" strip-indent "^2.0.0" @@ -2943,6 +3401,7 @@ redent@^2.0.0: regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== dependencies: extend-shallow "^3.0.2" safe-regex "^1.1.0" @@ -2950,20 +3409,24 @@ regex-not@^1.0.0, regex-not@^1.0.2: repeat-element@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= repeating@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= dependencies: is-finite "^1.0.0" request@^2.87.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -2989,32 +3452,39 @@ request@^2.87.0: require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= dependencies: resolve-from "^3.0.0" resolve-from@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= dependencies: onetime "^2.0.0" signal-exit "^3.0.2" @@ -3022,64 +3492,82 @@ restore-cursor@^2.0.0: ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== retry@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" + integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= rimraf@2, rimraf@^2.5.4, rimraf@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== dependencies: glob "^7.0.5" run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= dependencies: is-promise "^2.1.0" run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= dependencies: aproba "^1.1.1" rxjs@^6.1.0: version "6.3.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" + integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== dependencies: tslib "^1.9.0" safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= dependencies: ret "~0.1.10" "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.4.1, semver@^5.5.0: - version "5.5.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= set-value@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= dependencies: extend-shallow "^2.0.1" is-extendable "^0.1.1" @@ -3089,6 +3577,7 @@ set-value@^0.4.3: set-value@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== dependencies: extend-shallow "^2.0.1" is-extendable "^0.1.1" @@ -3098,32 +3587,39 @@ set-value@^2.0.0: shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= dependencies: shebang-regex "^1.0.0" shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= slide@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= smart-buffer@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.1.tgz#07ea1ca8d4db24eb4cac86537d7d18995221ace3" + integrity sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg== snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== dependencies: define-property "^1.0.0" isobject "^3.0.0" @@ -3132,12 +3628,14 @@ snapdragon-node@^2.0.1: snapdragon-util@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== dependencies: kind-of "^3.2.0" snapdragon@^0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== dependencies: base "^0.11.1" debug "^2.2.0" @@ -3151,13 +3649,15 @@ snapdragon@^0.8.1: socks-proxy-agent@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz#5936bf8b707a993079c6f37db2091821bffa6473" + integrity sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw== dependencies: agent-base "~4.2.0" socks "~2.2.0" socks@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.1.tgz#68ad678b3642fbc5d99c64c165bc561eab0215f9" + version "2.2.2" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.2.tgz#f061219fc2d4d332afb4af93e865c84d3fa26e2b" + integrity sha512-g6wjBnnMOZpE0ym6e0uHSddz9p3a+WsBaaYQaBaSCJYvrC4IXykQR9MNGjLQf38e9iIIhp3b1/Zk8YZI3KGJ0Q== dependencies: ip "^1.1.5" smart-buffer "^4.0.1" @@ -3165,12 +3665,14 @@ socks@~2.2.0: sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= dependencies: is-plain-obj "^1.0.0" source-map-resolve@^0.5.0: version "0.5.2" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== dependencies: atob "^2.1.1" decode-uri-component "^0.2.0" @@ -3181,18 +3683,22 @@ source-map-resolve@^0.5.0: source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== spdx-correct@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.2.tgz#19bb409e91b47b1ad54159243f7312a858db3c2e" + integrity sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" @@ -3200,64 +3706,73 @@ spdx-correct@^3.0.0: spdx-exceptions@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== spdx-expression-parse@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz#e2a303236cac54b04031fa7a5a79c7e701df852f" + version "3.0.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz#a59efc09784c2a5bada13cfeaf5c75dd214044d2" + integrity sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== dependencies: extend-shallow "^3.0.0" split2@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" + integrity sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw== dependencies: through2 "^2.0.2" split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== dependencies: through "2" sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sshpk@^1.7.0: - version "1.14.2" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" + version "1.15.2" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629" + integrity sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - safer-buffer "^2.0.2" - optionalDependencies: bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" ecc-jsbn "~0.1.1" + getpass "^0.1.1" jsbn "~0.1.0" + safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssri@^6.0.0: +ssri@^6.0.0, ssri@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" + integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== dependencies: figgy-pudding "^3.5.1" static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= dependencies: define-property "^0.2.5" object-copy "^0.1.0" @@ -3265,6 +3780,7 @@ static-extend@^0.1.1: stream-each@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== dependencies: end-of-stream "^1.1.0" stream-shift "^1.0.0" @@ -3272,10 +3788,12 @@ stream-each@^1.1.0: stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= dependencies: code-point-at "^1.0.0" is-fullwidth-code-point "^1.0.0" @@ -3284,6 +3802,7 @@ string-width@^1.0.1: "string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== dependencies: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" @@ -3291,48 +3810,57 @@ string-width@^1.0.1: string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" - resolved "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= dependencies: ansi-regex "^2.0.0" strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= dependencies: ansi-regex "^3.0.0" strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= dependencies: is-utf8 "^0.2.0" strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= dependencies: get-stdin "^4.0.1" strip-indent@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= strong-log-transformer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.0.0.tgz#fa6d8e0a9e62b3c168c3cad5ae5d00dc97ba26cc" + integrity sha512-FQmNqAXJgOX8ygOcvPLlGWBNT41mvNJ9ALoYf0GTwVt9t30mGTqpmp/oJx5gLcu52DXK10kS7dVWhx8aPXDTlg== dependencies: byline "^5.0.0" duplexer "^0.1.1" @@ -3342,25 +3870,28 @@ strong-log-transformer@^2.0.0: supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" tar@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + integrity sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE= dependencies: block-stream "*" fstream "^1.0.2" inherits "2" -tar@^4.4.3: - version "4.4.6" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" +tar@^4.4.6: + version "4.4.8" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" + integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== dependencies: - chownr "^1.0.1" + chownr "^1.1.1" fs-minipass "^1.2.5" - minipass "^2.3.3" - minizlib "^1.1.0" + minipass "^2.3.4" + minizlib "^1.1.1" mkdirp "^0.5.0" safe-buffer "^5.1.2" yallist "^3.0.2" @@ -3368,10 +3899,12 @@ tar@^4.4.3: temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= temp-write@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" + integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI= dependencies: graceful-fs "^4.1.2" is-stream "^1.1.0" @@ -3381,35 +3914,41 @@ temp-write@^3.4.0: uuid "^3.0.1" text-extensions@^1.0.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.8.0.tgz#6f343c62268843019b21a616a003557bdb952d2b" + version "1.9.0" + resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" + integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== through2@^2.0.0, through2@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== dependencies: - readable-stream "^2.1.5" + readable-stream "~2.3.6" xtend "~4.0.1" through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: version "2.3.8" - resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== dependencies: os-tmpdir "~1.0.2" to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= dependencies: kind-of "^3.0.2" to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= dependencies: is-number "^3.0.0" repeat-string "^1.6.1" @@ -3417,6 +3956,7 @@ to-regex-range@^2.1.0: to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== dependencies: define-property "^2.0.2" extend-shallow "^3.0.2" @@ -3426,6 +3966,7 @@ to-regex@^3.0.1, to-regex@^3.0.2: tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== dependencies: psl "^1.1.24" punycode "^1.4.1" @@ -3433,42 +3974,51 @@ tough-cookie@~2.4.3: tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= dependencies: punycode "^2.1.0" trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= trim-newlines@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" + integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= trim-off-newlines@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" + integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= dependencies: safe-buffer "^5.0.1" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= uglify-js@^3.1.4: version "3.4.9" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== dependencies: commander "~2.17.1" source-map "~0.6.1" @@ -3476,39 +4026,46 @@ uglify-js@^3.1.4: uid-number@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= umask@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" + integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= dependencies: arr-union "^3.1.0" get-value "^2.0.6" is-extendable "^0.1.1" set-value "^0.4.3" -unique-filename@^1.1.0: +unique-filename@^1.1.0, unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== dependencies: unique-slug "^2.0.0" unique-slug@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.1.tgz#5e9edc6d1ce8fb264db18a507ef9bd8544451ca6" + integrity sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg== dependencies: imurmurhash "^0.1.4" universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= dependencies: has-value "^0.3.1" isobject "^3.0.0" @@ -3516,22 +4073,27 @@ unset-value@^1.0.0: urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= uuid@^3.0.1, uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== dependencies: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" @@ -3539,12 +4101,14 @@ validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: validate-npm-package-name@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= dependencies: builtins "^1.0.3" verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= dependencies: assert-plus "^1.0.0" core-util-is "1.0.2" @@ -3553,16 +4117,19 @@ verror@1.10.0: wcwidth@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= dependencies: defaults "^1.0.3" webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== whatwg-url@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== dependencies: lodash.sortby "^4.7.0" tr46 "^1.0.1" @@ -3571,26 +4138,31 @@ whatwg-url@^7.0.0: which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@1, which@^1.2.9, which@^1.3.0, which@^1.3.1: +which@1, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== dependencies: string-width "^1.0.2 || 2" wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= wrap-ansi@^2.0.0: version "2.1.0" - resolved "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= dependencies: string-width "^1.0.1" strip-ansi "^3.0.1" @@ -3598,10 +4170,12 @@ wrap-ansi@^2.0.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= write-file-atomic@^2.0.0, write-file-atomic@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" + integrity sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA== dependencies: graceful-fs "^4.1.11" imurmurhash "^0.1.4" @@ -3610,6 +4184,7 @@ write-file-atomic@^2.0.0, write-file-atomic@^2.3.0: write-json-file@^2.2.0, write-json-file@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f" + integrity sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8= dependencies: detect-indent "^5.0.0" graceful-fs "^4.1.2" @@ -3621,39 +4196,60 @@ write-json-file@^2.2.0, write-json-file@^2.3.0: write-pkg@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/write-pkg/-/write-pkg-3.2.0.tgz#0e178fe97820d389a8928bc79535dbe68c2cff21" + integrity sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw== dependencies: sort-keys "^2.0.0" write-json-file "^2.2.0" +xml2js@^0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xregexp@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= yallist@^3.0.0, yallist@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" + integrity sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k= yargs-parser@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== dependencies: camelcase "^4.1.0" yargs@^12.0.1: version "12.0.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" + integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ== dependencies: cliui "^4.0.0" decamelize "^2.0.0" diff --git a/scm-ui/package.json b/scm-ui/package.json index de42ff2a36..c4b7cb3983 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -6,23 +6,28 @@ "main": "src/index.js", "dependencies": { "@fortawesome/fontawesome-free": "^5.3.1", - "@scm-manager/ui-extensions": "^0.0.7", + "@scm-manager/ui-extensions": "^0.1.1", "bulma": "^0.7.1", "bulma-tooltip": "^2.0.2", "classnames": "^2.2.5", + "diff2html": "^2.4.0", "font-awesome": "^4.7.0", "history": "^4.7.2", "i18next": "^11.4.0", "i18next-browser-languagedetector": "^2.2.2", "i18next-fetch-backend": "^0.1.0", "moment": "^2.22.2", - "react": "^16.5.2", - "react-dom": "^16.5.2", + "node-sass": "^4.9.3", + "postcss-easy-import": "^3.0.0", + "react": "^16.4.2", + "react-diff-view": "^1.7.0", + "react-dom": "^16.4.2", "react-i18next": "^7.9.0", "react-jss": "^8.6.0", "react-redux": "^5.0.7", "react-router-dom": "^4.3.1", "react-router-redux": "^5.0.0-alpha.9", + "react-syntax-highlighter": "^9.0.1", "redux": "^4.0.0", "redux-devtools-extension": "^2.13.5", "redux-logger": "^3.0.6", @@ -43,7 +48,7 @@ "pre-commit": "jest && flow && eslint src" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.17", + "@scm-manager/ui-bundler": "^0.0.21", "copyfiles": "^2.0.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", @@ -53,6 +58,7 @@ "node-sass": "^4.9.3", "node-sass-chokidar": "^1.3.0", "npm-run-all": "^4.1.3", + "postcss-easy-import": "^3.0.0", "prettier": "^1.13.7", "react-router-enzyme-context": "^1.2.0", "react-test-renderer": "^16.4.1", diff --git a/scm-ui/public/index.mustache b/scm-ui/public/index.mustache index 802be2ca97..62a40d8e93 100644 --- a/scm-ui/public/index.mustache +++ b/scm-ui/public/index.mustache @@ -36,5 +36,9 @@ </script> <script src="{{ contextPath }}/vendor.bundle.js"></script> <script src="{{ contextPath }}/scm-ui.bundle.js"></script> + + {{#liveReloadURL}} + <script src="{{liveReloadURL}}"></script> + {{/liveReloadURL}} </body> </html> diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index b1bad56993..05e9f79d16 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -38,5 +38,14 @@ "paginator": { "next": "Next", "previous": "Previous" + }, + "profile": { + "actions-label": "Actions", + "username": "Username", + "displayName": "Display Name", + "mail": "E-Mail", + "change-password": "Change password", + "error-title": "Error", + "error-subtitle": "Cannot display profile" } } diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 60ee220318..d4fd950c45 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -51,7 +51,11 @@ "name": "Name", "length": "Length", "lastModified": "Last modified", - "description": "Description" + "description": "Description", + "branch": "Branch" + }, + "content": { + "downloadButton": "Download" } }, "changesets": { diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index 07540134be..7199cb2135 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -29,6 +29,9 @@ "edit-user-button": { "label": "Edit" }, + "set-password-button": { + "label": "Set password" + }, "user-form": { "submit": "Submit" }, @@ -49,8 +52,11 @@ "name-invalid": "This name is invalid", "displayname-invalid": "This displayname is invalid", "password-invalid": "Password has to be between 6 and 32 characters", - "passwordValidation-invalid": "Passwords have to be the same", - "validatePassword": "Please validate password here" + "passwordValidation-invalid": "Passwords have to be identical", + "validatePassword": "Confirm password" + }, + "password": { + "set-password-successful": "Password successfully set" }, "help": { "usernameHelpText": "Unique name of the user.", diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index bc973bb925..6d935fe33a 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -2,12 +2,19 @@ import React from "react"; import { translate } from "react-i18next"; import { Route } from "react-router"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import type { Links } from "@scm-manager/ui-types"; import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components"; import GlobalConfig from "./GlobalConfig"; import type { History } from "history"; +import {connect} from "react-redux"; +import {compose} from "redux"; +import { getLinks } from "../../modules/indexResource"; type Props = { + links: Links, + // context objects t: string => string, match: any, @@ -27,15 +34,23 @@ class Config extends React.Component<Props> { }; render() { - const { t } = this.props; + const { links, t } = this.props; const url = this.matchedUrl(); + const extensionProps = { + links, + url + }; return ( <Page> <div className="columns"> <div className="column is-three-quarters"> <Route path={url} exact component={GlobalConfig} /> + <ExtensionPoint name="config.route" + props={extensionProps} + renderAll={true} + /> </div> <div className="column"> <Navigation> @@ -44,6 +59,10 @@ class Config extends React.Component<Props> { to={`${url}`} label={t("global-config.navigation-label")} /> + <ExtensionPoint name="config.navigation" + props={extensionProps} + renderAll={true} + /> </Section> </Navigation> </div> @@ -53,4 +72,15 @@ class Config extends React.Component<Props> { } } -export default translate("config")(Config); +const mapStateToProps = (state: any) => { + const links = getLinks(state); + return { + links + }; +}; + +export default compose( + connect(mapStateToProps), + translate("config") +)(Config); + diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 252e880a42..6046aa4a09 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -14,7 +14,7 @@ import { modifyConfigReset } from "../modules/config"; import { connect } from "react-redux"; -import type { Config, Link } from "@scm-manager/ui-types"; +import type { Config } from "@scm-manager/ui-types"; import ConfigForm from "../components/form/ConfigForm"; import { getConfigLink } from "../../modules/indexResource"; diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 12c6b347c3..b6c97826b0 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -22,7 +22,6 @@ import reducer, { getConfig, getConfigUpdatePermission } from "./config"; -import { getConfigLink } from "../../modules/indexResource"; const CONFIG_URL = "/config"; const URL = "/api/v2" + CONFIG_URL; diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index 768b1776d4..50fc805eb2 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -19,9 +19,8 @@ import { Footer, Header } from "@scm-manager/ui-components"; -import type { Me, Link } from "@scm-manager/ui-types"; +import type { Me } from "@scm-manager/ui-types"; import { - fetchIndexResources, getConfigLink, getFetchIndexResourcesFailure, getGroupsLink, diff --git a/scm-ui/src/containers/Index.js b/scm-ui/src/containers/Index.js index 0fe6364f6e..71bd6b35c2 100644 --- a/scm-ui/src/containers/Index.js +++ b/scm-ui/src/containers/Index.js @@ -27,13 +27,32 @@ type Props = { t: string => string }; -class Index extends Component<Props> { +type State = { + pluginsLoaded: boolean +}; + +class Index extends Component<Props, State> { + + constructor(props: Props) { + super(props); + this.state = { + pluginsLoaded: false + }; + } + componentDidMount() { this.props.fetchIndexResources(); } + pluginLoaderCallback = () => { + this.setState({ + pluginsLoaded: true + }); + }; + render() { const { indexResources, loading, error, t } = this.props; + const { pluginsLoaded } = this.state; if (error) { return ( @@ -47,7 +66,7 @@ class Index extends Component<Props> { return <Loading />; } else { return ( - <PluginLoader> + <PluginLoader loaded={ pluginsLoaded } callback={ this.pluginLoaderCallback }> <App /> </PluginLoader> ); diff --git a/scm-ui/src/containers/Logout.js b/scm-ui/src/containers/Logout.js index 7875a6b92a..fe6662da42 100644 --- a/scm-ui/src/containers/Logout.js +++ b/scm-ui/src/containers/Logout.js @@ -11,7 +11,7 @@ import { getLogoutFailure } from "../modules/auth"; import { Loading, ErrorPage } from "@scm-manager/ui-components"; -import { fetchIndexResources, getLogoutLink } from "../modules/indexResource"; +import { getLogoutLink } from "../modules/indexResource"; type Props = { authenticated: boolean, diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index a971bab54d..1cfa379e74 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -19,6 +19,7 @@ import SingleGroup from "../groups/containers/SingleGroup"; import AddGroup from "../groups/containers/AddGroup"; import Config from "../config/containers/Config"; +import Profile from "./Profile"; type Props = { authenticated?: boolean @@ -101,11 +102,16 @@ class Main extends React.Component<Props> { authenticated={authenticated} /> <ProtectedRoute - exact path="/config" component={Config} authenticated={authenticated} /> + <ProtectedRoute + exact + path="/me" + component={Profile} + authenticated={authenticated} + /> </Switch> </div> ); diff --git a/scm-ui/src/containers/PluginLoader.js b/scm-ui/src/containers/PluginLoader.js index 8e44a1d427..308f532270 100644 --- a/scm-ui/src/containers/PluginLoader.js +++ b/scm-ui/src/containers/PluginLoader.js @@ -5,12 +5,13 @@ import { getUiPluginsLink } from "../modules/indexResource"; import { connect } from "react-redux"; type Props = { + loaded: boolean, children: React.Node, - link: string + link: string, + callback: () => void }; type State = { - finished: boolean, message: string }; @@ -23,17 +24,19 @@ class PluginLoader extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { - finished: false, message: "booting" }; } componentDidMount() { - this.setState({ - message: "loading plugin information" - }); - - this.getPlugins(this.props.link); + const { loaded } = this.props; + if (!loaded) { + this.setState({ + message: "loading plugin information" + }); + + this.getPlugins(this.props.link); + } } getPlugins = (link: string): Promise<any> => { @@ -43,11 +46,7 @@ class PluginLoader extends React.Component<Props, State> { .then(JSON.parse) .then(pluginCollection => pluginCollection._embedded.plugins) .then(this.loadPlugins) - .then(() => { - this.setState({ - finished: true - }); - }); + .then(this.props.callback); }; loadPlugins = (plugins: Plugin[]) => { @@ -87,8 +86,9 @@ class PluginLoader extends React.Component<Props, State> { }; render() { - const { message, finished } = this.state; - if (finished) { + const { loaded } = this.props; + const { message } = this.state; + if (loaded) { return <div>{this.props.children}</div>; } return <Loading message={message} />; diff --git a/scm-ui/src/containers/Profile.js b/scm-ui/src/containers/Profile.js new file mode 100644 index 0000000000..adb1ba5d6c --- /dev/null +++ b/scm-ui/src/containers/Profile.js @@ -0,0 +1,98 @@ +// @flow + +import React from "react"; + +import { + Page, + Navigation, + Section, + MailLink +} from "../../../scm-ui-components/packages/ui-components/src/index"; +import { NavLink } from "react-router-dom"; +import { getMe } from "../modules/auth"; +import { compose } from "redux"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import type { Me } from "../../../scm-ui-components/packages/ui-types/src/index"; +import AvatarWrapper from "../repos/components/changesets/AvatarWrapper"; +import { ErrorPage } from "@scm-manager/ui-components"; + +type Props = { + me: Me, + + // Context props + t: string => string +}; +type State = {}; + +class Profile extends React.Component<Props, State> { + render() { + const { me, t } = this.props; + + if (me) { + return ( + <ErrorPage + title={t("profile.error-title")} + subtitle={t("profile.error-subtitle")} + error={{ name: "Error", message: "'me' is undefined" }} + /> + ); + } + return ( + <Page title={me.displayName}> + <div className="columns"> + <AvatarWrapper> + <div> + <figure className="media-left"> + <p className="image is-64x64"> + { + // TODO: add avatar + } + </p> + </figure> + </div> + </AvatarWrapper> + <div className="column is-two-quarters"> + <table className="table"> + <tbody> + <tr> + <td>{t("profile.username")}</td> + <td>{me.name}</td> + </tr> + <tr> + <td>{t("profile.displayName")}</td> + <td>{me.displayName}</td> + </tr> + <tr> + <td>{t("profile.mail")}</td> + <td> + <MailLink address={me.mail} /> + </td> + </tr> + </tbody> + </table> + </div> + <div className="column is-one-quarter"> + <Navigation> + <Section label={t("profile.actions-label")} /> + <NavLink to={"me/password"}> + {t("profile.change-password")} + </NavLink> + </Navigation> + </div> + </div> + </Page> + ); + } +} + +const mapStateToProps = state => { + return { + me: getMe(state) + }; +}; + +export default compose( + translate("commons"), + connect(mapStateToProps) +)(Profile); diff --git a/scm-ui/src/groups/containers/AddGroup.js b/scm-ui/src/groups/containers/AddGroup.js index bcb19846b8..9b13ac0309 100644 --- a/scm-ui/src/groups/containers/AddGroup.js +++ b/scm-ui/src/groups/containers/AddGroup.js @@ -9,8 +9,7 @@ import { createGroup, isCreateGroupPending, getCreateGroupFailure, - createGroupReset, - getCreateGroupLink + createGroupReset } from "../modules/groups"; import type { Group } from "@scm-manager/ui-types"; import type { History } from "history"; diff --git a/scm-ui/src/groups/containers/EditGroup.js b/scm-ui/src/groups/containers/EditGroup.js index e21731a323..ac6e737dac 100644 --- a/scm-ui/src/groups/containers/EditGroup.js +++ b/scm-ui/src/groups/containers/EditGroup.js @@ -2,16 +2,21 @@ import React from "react"; import { connect } from "react-redux"; import GroupForm from "../components/GroupForm"; -import { modifyGroup, fetchGroup } from "../modules/groups"; +import { + modifyGroup, + modifyGroupReset, + isModifyGroupPending, + getModifyGroupFailure +} from "../modules/groups"; import type { History } from "history"; import { withRouter } from "react-router-dom"; import type { Group } from "@scm-manager/ui-types"; -import { isModifyGroupPending, getModifyGroupFailure } from "../modules/groups"; import { ErrorNotification } from "@scm-manager/ui-components"; type Props = { group: Group, modifyGroup: (group: Group, callback?: () => void) => void, + modifyGroupReset: Group => void, fetchGroup: (name: string) => void, history: History, loading?: boolean, @@ -19,8 +24,12 @@ type Props = { }; class EditGroup extends React.Component<Props> { + componentDidMount() { + const { group, modifyGroupReset } = this.props; + modifyGroupReset(group); + } + groupModified = (group: Group) => () => { - this.props.fetchGroup(group.name); this.props.history.push(`/group/${group.name}`); }; @@ -59,8 +68,8 @@ const mapDispatchToProps = dispatch => { modifyGroup: (group: Group, callback?: () => void) => { dispatch(modifyGroup(group, callback)); }, - fetchGroup: (name: string) => { - dispatch(fetchGroup(name)); + modifyGroupReset: (group: Group) => { + dispatch(modifyGroupReset(group)); } }; }; diff --git a/scm-ui/src/groups/containers/SingleGroup.js b/scm-ui/src/groups/containers/SingleGroup.js index d681859808..1dd4aa569f 100644 --- a/scm-ui/src/groups/containers/SingleGroup.js +++ b/scm-ui/src/groups/containers/SingleGroup.js @@ -16,7 +16,7 @@ import type { Group } from "@scm-manager/ui-types"; import type { History } from "history"; import { deleteGroup, - fetchGroup, + fetchGroupByName, getGroupByName, isFetchGroupPending, getFetchGroupFailure, @@ -37,7 +37,7 @@ type Props = { // dispatcher functions deleteGroup: (group: Group, callback?: () => void) => void, - fetchGroup: (string, string) => void, + fetchGroupByName: (string, string) => void, // context objects t: string => string, @@ -47,7 +47,7 @@ type Props = { class SingleGroup extends React.Component<Props> { componentDidMount() { - this.props.fetchGroup(this.props.groupLink, this.props.name); + this.props.fetchGroupByName(this.props.groupLink, this.props.name); } stripEndingSlash = (url: string) => { @@ -147,8 +147,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchGroup: (link: string, name: string) => { - dispatch(fetchGroup(link, name)); + fetchGroupByName: (link: string, name: string) => { + dispatch(fetchGroupByName(link, name)); }, deleteGroup: (group: Group, callback?: () => void) => { dispatch(deleteGroup(group, callback)); diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index 74e7214052..165648edaa 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -26,6 +26,7 @@ export const MODIFY_GROUP = "scm/groups/MODIFY_GROUP"; export const MODIFY_GROUP_PENDING = `${MODIFY_GROUP}_${types.PENDING_SUFFIX}`; export const MODIFY_GROUP_SUCCESS = `${MODIFY_GROUP}_${types.SUCCESS_SUFFIX}`; export const MODIFY_GROUP_FAILURE = `${MODIFY_GROUP}_${types.FAILURE_SUFFIX}`; +export const MODIFY_GROUP_RESET = `${MODIFY_GROUP}_${types.RESET_SUFFIX}`; export const DELETE_GROUP = "scm/groups/DELETE"; export const DELETE_GROUP_PENDING = `${DELETE_GROUP}_${types.PENDING_SUFFIX}`; @@ -84,12 +85,20 @@ export function fetchGroupsFailure(url: string, error: Error): Action { } //fetch group -export function fetchGroup(link: string, name: string) { +export function fetchGroupByLink(group: Group) { + return fetchGroup(group._links.self.href, group.name); +} + +export function fetchGroupByName(link: string, name: string) { const groupUrl = link.endsWith("/") ? link + name : link + "/" + name; + return fetchGroup(groupUrl, name); +} + +function fetchGroup(link: string, name: string) { return function(dispatch: any) { dispatch(fetchGroupPending(name)); return apiClient - .get(groupUrl) + .get(link) .then(response => { return response.json(); }) @@ -189,6 +198,9 @@ export function modifyGroup(group: Group, callback?: () => void) { callback(); } }) + .then(() => { + dispatch(fetchGroupByLink(group)); + }) .catch(cause => { dispatch( modifyGroupFailure( @@ -227,6 +239,13 @@ export function modifyGroupFailure(group: Group, error: Error): Action { }; } +export function modifyGroupReset(group: Group): Action { + return { + type: MODIFY_GROUP_RESET, + itemId: group.name + }; +} + //delete group export function deleteGroup(group: Group, callback?: () => void) { @@ -361,8 +380,6 @@ function byNamesReducer(state: any = {}, action: any = {}) { }; case FETCH_GROUP_SUCCESS: return reducerByName(state, action.payload.name, action.payload); - case MODIFY_GROUP_SUCCESS: - return reducerByName(state, action.payload.name, action.payload); case DELETE_GROUP_SUCCESS: const newGroupByNames = deleteGroupInGroupsByNames( state, diff --git a/scm-ui/src/groups/modules/groups.test.js b/scm-ui/src/groups/modules/groups.test.js index 63ab375cd3..57d41975a1 100644 --- a/scm-ui/src/groups/modules/groups.test.js +++ b/scm-ui/src/groups/modules/groups.test.js @@ -15,7 +15,8 @@ import reducer, { getFetchGroupsFailure, isFetchGroupsPending, selectListAsCollection, - fetchGroup, + fetchGroupByLink, + fetchGroupByName, FETCH_GROUP_PENDING, FETCH_GROUP_SUCCESS, FETCH_GROUP_FAILURE, @@ -46,6 +47,7 @@ import reducer, { getCreateGroupLink } from "./groups"; const GROUPS_URL = "/api/v2/groups"; +const URL_HUMAN_GROUP = "http://localhost:8081/api/v2/groups/humanGroup"; const URL = "/groups"; const error = new Error("You have an error!"); @@ -59,13 +61,13 @@ const humanGroup = { members: ["userZaphod"], _links: { self: { - href: "http://localhost:8081/api/v2/groups/humanGroup" + href: URL_HUMAN_GROUP }, delete: { - href: "http://localhost:8081/api/v2/groups/humanGroup" + href: URL_HUMAN_GROUP }, update: { - href: "http://localhost:8081/api/v2/groups/humanGroup" + href: URL_HUMAN_GROUP } }, _embedded: { @@ -171,11 +173,37 @@ describe("groups fetch()", () => { }); }); - it("should sucessfully fetch single group", () => { + it("should sucessfully fetch single group by name", () => { fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup); const store = mockStore({}); - return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => { + return store.dispatch(fetchGroupByName(URL, "humanGroup")).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); + expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should fail fetching single group by name on HTTP 500", () => { + fetchMock.getOnce(GROUPS_URL + "/humanGroup", { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(fetchGroupByName(URL, "humanGroup")).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); + expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should sucessfully fetch single group", () => { + fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup); + + const store = mockStore({}); + return store.dispatch(fetchGroupByLink(humanGroup)).then(() => { const actions = store.getActions(); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS); @@ -184,12 +212,12 @@ describe("groups fetch()", () => { }); it("should fail fetching single group on HTTP 500", () => { - fetchMock.getOnce(GROUPS_URL + "/humanGroup", { + fetchMock.getOnce(URL_HUMAN_GROUP, { status: 500 }); const store = mockStore({}); - return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => { + return store.dispatch(fetchGroupByLink(humanGroup)).then(() => { const actions = store.getActions(); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE); @@ -244,9 +272,10 @@ describe("groups fetch()", () => { }); it("should successfully modify group", () => { - fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", { + fetchMock.putOnce(URL_HUMAN_GROUP, { status: 204 }); + fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup); const store = mockStore({}); @@ -254,14 +283,16 @@ describe("groups fetch()", () => { const actions = store.getActions(); expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING); expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS); + expect(actions[2].type).toEqual(FETCH_GROUP_PENDING); expect(actions[1].payload).toEqual(humanGroup); }); }); it("should call the callback after modifying group", () => { - fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", { + fetchMock.putOnce(URL_HUMAN_GROUP, { status: 204 }); + fetchMock.getOnce(URL_HUMAN_GROUP, humanGroup); let called = false; const callback = () => { @@ -273,12 +304,13 @@ describe("groups fetch()", () => { const actions = store.getActions(); expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING); expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS); + expect(actions[2].type).toEqual(FETCH_GROUP_PENDING); expect(called).toBe(true); }); }); it("should fail modifying group on HTTP 500", () => { - fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", { + fetchMock.putOnce(URL_HUMAN_GROUP, { status: 500 }); @@ -293,7 +325,7 @@ describe("groups fetch()", () => { }); it("should delete successfully group humanGroup", () => { - fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", { + fetchMock.deleteOnce(URL_HUMAN_GROUP, { status: 204 }); @@ -308,7 +340,7 @@ describe("groups fetch()", () => { }); it("should call the callback, after successful delete", () => { - fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", { + fetchMock.deleteOnce(URL_HUMAN_GROUP, { status: 204 }); @@ -324,7 +356,7 @@ describe("groups fetch()", () => { }); it("should fail to delete group humanGroup", () => { - fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", { + fetchMock.deleteOnce(URL_HUMAN_GROUP, { status: 500 }); diff --git a/scm-ui/src/index.js b/scm-ui/src/index.js index 3ecd38e6d0..08e3e8a58c 100644 --- a/scm-ui/src/index.js +++ b/scm-ui/src/index.js @@ -14,7 +14,6 @@ import type { BrowserHistory } from "history/createBrowserHistory"; import createReduxStore from "./createReduxStore"; import { ConnectedRouter } from "react-router-redux"; -import PluginLoader from "./containers/PluginLoader"; import { urls } from "@scm-manager/ui-components"; @@ -37,7 +36,7 @@ ReactDOM.render( <I18nextProvider i18n={i18n}> {/* ConnectedRouter will use the store from Provider automatically */} <ConnectedRouter history={history}> - <Index /> + <Index /> </ConnectedRouter> </I18nextProvider> </Provider>, diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index fd5068aeb8..691ae2b128 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -1,5 +1,5 @@ // @flow -import type { Me } from "@scm-manager/ui-components"; +import type { Me } from "@scm-manager/ui-types"; import * as types from "./types"; import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components"; @@ -7,8 +7,8 @@ import { isPending } from "./pending"; import { getFailure } from "./failure"; import { callFetchIndexResources, - FETCH_INDEXRESOURCES_SUCCESS, - fetchIndexResources, fetchIndexResourcesPending, + fetchIndexResources, + fetchIndexResourcesPending, fetchIndexResourcesSuccess } from "./indexResource"; @@ -136,7 +136,11 @@ const callFetchMe = (link: string): Promise<Me> => { return response.json(); }) .then(json => { - return { name: json.name, displayName: json.displayName }; + return { + name: json.name, + displayName: json.displayName, + mail: json.mail + }; }); }; @@ -156,7 +160,7 @@ export const login = ( return apiClient .post(loginLink, login_data) .then(response => { - dispatch(fetchIndexResourcesPending()) + dispatch(fetchIndexResourcesPending()); return callFetchIndexResources(); }) .then(response => { diff --git a/scm-ui/src/modules/auth.test.js b/scm-ui/src/modules/auth.test.js index 1839701e0a..7236f803a8 100644 --- a/scm-ui/src/modules/auth.test.js +++ b/scm-ui/src/modules/auth.test.js @@ -37,7 +37,11 @@ import { FETCH_INDEXRESOURCES_SUCCESS } from "./indexResource"; -const me = { name: "tricia", displayName: "Tricia McMillian" }; +const me = { + name: "tricia", + displayName: "Tricia McMillian", + mail: "trillian@heartofgold.universe" +}; describe("auth reducer", () => { it("should set me and login on successful fetch of me", () => { diff --git a/scm-ui/src/repos/components/changesets/AvatarImage.js b/scm-ui/src/repos/components/changesets/AvatarImage.js index 77792b1690..6d730e87cd 100644 --- a/scm-ui/src/repos/components/changesets/AvatarImage.js +++ b/scm-ui/src/repos/components/changesets/AvatarImage.js @@ -1,8 +1,8 @@ //@flow import React from "react"; -import { binder } from "@scm-manager/ui-extensions"; -import type { Changeset } from "@scm-manager/ui-types"; -import { Image } from "@scm-manager/ui-components"; +import {binder} from "@scm-manager/ui-extensions"; +import type {Changeset} from "@scm-manager/ui-types"; +import {Image} from "@scm-manager/ui-components"; type Props = { changeset: Changeset diff --git a/scm-ui/src/repos/components/changesets/AvatarWrapper.js b/scm-ui/src/repos/components/changesets/AvatarWrapper.js index 0d3d55e62a..c014b33281 100644 --- a/scm-ui/src/repos/components/changesets/AvatarWrapper.js +++ b/scm-ui/src/repos/components/changesets/AvatarWrapper.js @@ -1,6 +1,6 @@ //@flow import * as React from "react"; -import { binder } from "@scm-manager/ui-extensions"; +import {binder} from "@scm-manager/ui-extensions"; type Props = { children: React.Node diff --git a/scm-ui/src/repos/components/changesets/ChangesetDetails.js b/scm-ui/src/repos/components/changesets/ChangesetDetails.js index a8edf0365c..14f48362b8 100644 --- a/scm-ui/src/repos/components/changesets/ChangesetDetails.js +++ b/scm-ui/src/repos/components/changesets/ChangesetDetails.js @@ -1,20 +1,18 @@ //@flow import React from "react"; -import type { - Changeset, - Repository -} from "../../../../../scm-ui-components/packages/ui-types/src/index"; +import type { Changeset, Repository } from "@scm-manager/ui-types"; import { Interpolate, translate } from "react-i18next"; import injectSheet from "react-jss"; import ChangesetTag from "./ChangesetTag"; import ChangesetAuthor from "./ChangesetAuthor"; import { parseDescription } from "./changesets"; -import { DateFromNow } from "../../../../../scm-ui-components/packages/ui-components/src/index"; +import { DateFromNow } from "@scm-manager/ui-components"; import AvatarWrapper from "./AvatarWrapper"; import AvatarImage from "./AvatarImage"; import classNames from "classnames"; import ChangesetId from "./ChangesetId"; import type { Tag } from "@scm-manager/ui-types"; +import ScmDiff from "../../containers/ScmDiff"; const styles = { spacing: { @@ -41,38 +39,43 @@ class ChangesetDetails extends React.Component<Props> { const date = <DateFromNow date={changeset.date} />; return ( - <div className="content"> - <h4>{description.title}</h4> - <article className="media"> - <AvatarWrapper> - <p className={classNames("image", "is-64x64", classes.spacing)}> - <AvatarImage changeset={changeset} /> - </p> - </AvatarWrapper> - <div className="media-content"> - <p> - <ChangesetAuthor changeset={changeset} /> - </p> - <p> - <Interpolate - i18nKey="changesets.changeset.summary" - id={id} - time={date} - /> - </p> - </div> - <div className="media-right">{this.renderTags()}</div> - </article> - <p> - {description.message.split("\n").map((item, key) => { - return ( - <span key={key}> - {item} - <br /> - </span> - ); - })} - </p> + <div> + <div className="content"> + <h4>{description.title}</h4> + <article className="media"> + <AvatarWrapper> + <p className={classNames("image", "is-64x64", classes.spacing)}> + <AvatarImage changeset={changeset} /> + </p> + </AvatarWrapper> + <div className="media-content"> + <p> + <ChangesetAuthor changeset={changeset} /> + </p> + <p> + <Interpolate + i18nKey="changesets.changeset.summary" + id={id} + time={date} + /> + </p> + </div> + <div className="media-right">{this.renderTags()}</div> + </article> + <p> + {description.message.split("\n").map((item, key) => { + return ( + <span key={key}> + {item} + <br /> + </span> + ); + })} + </p> + </div> + <div> + <ScmDiff changeset={changeset} sideBySide={false} /> + </div> </div> ); } diff --git a/scm-ui/src/repos/components/changesets/ChangesetId.js b/scm-ui/src/repos/components/changesets/ChangesetId.js index ba38e6179c..aec1029427 100644 --- a/scm-ui/src/repos/components/changesets/ChangesetId.js +++ b/scm-ui/src/repos/components/changesets/ChangesetId.js @@ -1,8 +1,8 @@ //@flow -import { Link } from "react-router-dom"; +import {Link} from "react-router-dom"; import React from "react"; -import type { Repository, Changeset } from "@scm-manager/ui-types"; +import type {Changeset, Repository} from "@scm-manager/ui-types"; type Props = { repository: Repository, diff --git a/scm-ui/src/repos/components/changesets/ChangesetRow.js b/scm-ui/src/repos/components/changesets/ChangesetRow.js index ffe2a7eda4..0215abedd1 100644 --- a/scm-ui/src/repos/components/changesets/ChangesetRow.js +++ b/scm-ui/src/repos/components/changesets/ChangesetRow.js @@ -1,15 +1,15 @@ //@flow import React from "react"; -import type { Changeset, Repository, Tag } from "@scm-manager/ui-types"; +import type {Changeset, Repository, Tag} from "@scm-manager/ui-types"; import classNames from "classnames"; -import { translate, Interpolate } from "react-i18next"; +import {Interpolate, translate} from "react-i18next"; import ChangesetId from "./ChangesetId"; import injectSheet from "react-jss"; -import { DateFromNow } from "@scm-manager/ui-components"; +import {DateFromNow} from "@scm-manager/ui-components"; import ChangesetAuthor from "./ChangesetAuthor"; import ChangesetTag from "./ChangesetTag"; -import { compose } from "redux"; -import { parseDescription } from "./changesets"; +import {compose} from "redux"; +import {parseDescription} from "./changesets"; import AvatarWrapper from "./AvatarWrapper"; import AvatarImage from "./AvatarImage"; diff --git a/scm-ui/src/repos/components/changesets/changesets.test.js b/scm-ui/src/repos/components/changesets/changesets.test.js index ea92bcead3..3c13542a56 100644 --- a/scm-ui/src/repos/components/changesets/changesets.test.js +++ b/scm-ui/src/repos/components/changesets/changesets.test.js @@ -1,6 +1,6 @@ // @flow -import { parseDescription } from "./changesets"; +import {parseDescription} from "./changesets"; describe("parseDescription tests", () => { it("should return a description with title and message", () => { diff --git a/scm-ui/src/repos/components/form/RepositoryForm.js b/scm-ui/src/repos/components/form/RepositoryForm.js index 54abf8e08d..8f5d932778 100644 --- a/scm-ui/src/repos/components/form/RepositoryForm.js +++ b/scm-ui/src/repos/components/form/RepositoryForm.js @@ -124,7 +124,7 @@ class RepositoryForm extends React.Component<Props, State> { const { repositoryTypes, t } = this.props; const repository = this.state.repository; return ( - <div> + <> <InputField label={t("repository.name")} onChange={this.handleNameChange} @@ -140,7 +140,7 @@ class RepositoryForm extends React.Component<Props, State> { options={this.createSelectOptions(repositoryTypes)} helpText={t("help.typeHelpText")} /> - </div> + </> ); } diff --git a/scm-ui/src/repos/components/list/RepositoryEntry.js b/scm-ui/src/repos/components/list/RepositoryEntry.js index ef1df0d3d9..bc170144aa 100644 --- a/scm-ui/src/repos/components/list/RepositoryEntry.js +++ b/scm-ui/src/repos/components/list/RepositoryEntry.js @@ -1,9 +1,9 @@ //@flow import React from "react"; -import {Link} from "react-router-dom"; +import { Link } from "react-router-dom"; import injectSheet from "react-jss"; -import type {Repository} from "@scm-manager/ui-types"; -import {DateFromNow} from "@scm-manager/ui-components"; +import type { Repository } from "@scm-manager/ui-types"; +import { DateFromNow } from "@scm-manager/ui-components"; import RepositoryEntryLink from "./RepositoryEntryLink"; import classNames from "classnames"; import RepositoryAvatar from "./RepositoryAvatar"; @@ -45,7 +45,7 @@ class RepositoryEntry extends React.Component<Props> { return ( <RepositoryEntryLink iconClass="fa-code-branch" - to={repositoryLink + "/history"} + to={repositoryLink + "/changesets"} /> ); } diff --git a/scm-ui/src/repos/containers/ChangesetView.js b/scm-ui/src/repos/containers/ChangesetView.js index b164ca03ec..80ab0b71d6 100644 --- a/scm-ui/src/repos/containers/ChangesetView.js +++ b/scm-ui/src/repos/containers/ChangesetView.js @@ -11,7 +11,7 @@ import { } from "../modules/changesets"; import ChangesetDetails from "../components/changesets/ChangesetDetails"; import { translate } from "react-i18next"; -import { Loading, ErrorPage } from "@scm-manager/ui-components"; +import { ErrorPage, Loading } from "@scm-manager/ui-components"; type Props = { id: string, diff --git a/scm-ui/src/repos/containers/Changesets.js b/scm-ui/src/repos/containers/Changesets.js index 224df5042a..3d30e9f8be 100644 --- a/scm-ui/src/repos/containers/Changesets.js +++ b/scm-ui/src/repos/containers/Changesets.js @@ -1,13 +1,8 @@ // @flow import React from "react"; -import { withRouter } from "react-router-dom"; -import type { - Branch, - Changeset, - PagedCollection, - Repository -} from "@scm-manager/ui-types"; +import {withRouter} from "react-router-dom"; +import type {Branch, Changeset, PagedCollection, Repository} from "@scm-manager/ui-types"; import { fetchChangesets, getChangesets, @@ -16,15 +11,10 @@ import { selectListAsCollection } from "../modules/changesets"; -import { connect } from "react-redux"; +import {connect} from "react-redux"; import ChangesetList from "../components/changesets/ChangesetList"; -import { - ErrorNotification, - LinkPaginator, - Loading, - getPageFromMatch -} from "@scm-manager/ui-components"; -import { compose } from "redux"; +import {ErrorNotification, getPageFromMatch, LinkPaginator, Loading} from "@scm-manager/ui-components"; +import {compose} from "redux"; type Props = { repository: Repository, diff --git a/scm-ui/src/repos/containers/Edit.js b/scm-ui/src/repos/containers/Edit.js index 1edd9bb150..816dae8de9 100644 --- a/scm-ui/src/repos/containers/Edit.js +++ b/scm-ui/src/repos/containers/Edit.js @@ -7,7 +7,8 @@ import type { Repository } from "@scm-manager/ui-types"; import { modifyRepo, isModifyRepoPending, - getModifyRepoFailure + getModifyRepoFailure, + modifyRepoReset } from "../modules/repos"; import { withRouter } from "react-router-dom"; import type { History } from "history"; @@ -16,6 +17,7 @@ import { ErrorNotification } from "@scm-manager/ui-components"; type Props = { repository: Repository, modifyRepo: (Repository, () => void) => void, + modifyRepoReset: Repository => void, loading: boolean, error: Error, @@ -25,6 +27,10 @@ type Props = { }; class Edit extends React.Component<Props> { + componentDidMount() { + const { modifyRepoReset, repository } = this.props; + modifyRepoReset(repository); + } repoModified = () => { const { history, repository } = this.props; history.push(`/repo/${repository.namespace}/${repository.name}`); @@ -61,6 +67,9 @@ const mapDispatchToProps = dispatch => { return { modifyRepo: (repo: Repository, callback: () => void) => { dispatch(modifyRepo(repo, callback)); + }, + modifyRepoReset: (repo: Repository) => { + dispatch(modifyRepoReset(repo)); } }; }; diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 9e6ada9a4e..9b8991a91a 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -2,7 +2,7 @@ import React from "react"; import { deleteRepo, - fetchRepo, + fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending @@ -35,6 +35,7 @@ import PermissionsNavLink from "../components/PermissionsNavLink"; import Sources from "../sources/containers/Sources"; import RepositoryNavLink from "../components/RepositoryNavLink"; import { getRepositoriesLink } from "../../modules/indexResource"; +import {ExtensionPoint} from '@scm-manager/ui-extensions'; type Props = { namespace: string, @@ -45,7 +46,7 @@ type Props = { repoLink: string, // dispatch functions - fetchRepo: (link: string, namespace: string, name: string) => void, + fetchRepoByName: (link: string, namespace: string, name: string) => void, deleteRepo: (repository: Repository, () => void) => void, // context props @@ -56,9 +57,9 @@ type Props = { class RepositoryRoot extends React.Component<Props> { componentDidMount() { - const { fetchRepo, namespace, name, repoLink } = this.props; + const { fetchRepoByName, namespace, name, repoLink } = this.props; - fetchRepo(repoLink, namespace, name); + fetchRepoByName(repoLink, namespace, name); } stripEndingSlash = (url: string) => { @@ -80,11 +81,6 @@ class RepositoryRoot extends React.Component<Props> { this.props.deleteRepo(repository, this.deleted); }; - matchChangeset = (route: any) => { - const url = this.matchedUrl(); - return route.location.pathname.match(`${url}/changeset/`); - }; - matches = (route: any) => { const url = this.matchedUrl(); const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`); @@ -109,6 +105,12 @@ class RepositoryRoot extends React.Component<Props> { } const url = this.matchedUrl(); + + const extensionProps = { + repository, + url + }; + return ( <Page title={repository.namespace + "/" + repository.name}> <div className="columns"> @@ -125,7 +127,7 @@ class RepositoryRoot extends React.Component<Props> { /> <Route path={`${url}/permissions`} - render={props => ( + render={() => ( <Permissions namespace={this.props.repository.namespace} repoName={this.props.repository.name} @@ -170,6 +172,10 @@ class RepositoryRoot extends React.Component<Props> { /> )} /> + <ExtensionPoint name="repository.route" + props={extensionProps} + renderAll={true} + /> </Switch> </div> <div className="column"> @@ -191,11 +197,15 @@ class RepositoryRoot extends React.Component<Props> { label={t("repository-root.sources")} activeOnlyWhenExact={false} /> - <EditNavLink repository={repository} editUrl={`${url}/edit`} /> + <ExtensionPoint name="repository.navigation" + props={extensionProps} + renderAll={true} + /> <PermissionsNavLink permissionUrl={`${url}/permissions`} repository={repository} /> + <EditNavLink repository={repository} editUrl={`${url}/edit`} /> </Section> <Section label={t("repository-root.actions-label")}> <DeleteNavAction repository={repository} delete={this.delete} /> @@ -227,8 +237,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchRepo: (link: string, namespace: string, name: string) => { - dispatch(fetchRepo(link, namespace, name)); + fetchRepoByName: (link: string, namespace: string, name: string) => { + dispatch(fetchRepoByName(link, namespace, name)); }, deleteRepo: (repository: Repository, callback: () => void) => { dispatch(deleteRepo(repository, callback)); diff --git a/scm-ui/src/repos/containers/ScmDiff.js b/scm-ui/src/repos/containers/ScmDiff.js new file mode 100644 index 0000000000..b2df677eb3 --- /dev/null +++ b/scm-ui/src/repos/containers/ScmDiff.js @@ -0,0 +1,51 @@ +// @flow + +import React from "react"; +import { apiClient } from "@scm-manager/ui-components"; +import type { Changeset } from "@scm-manager/ui-types"; +import { Diff2Html } from "diff2html"; + +type Props = { + changeset: Changeset, + sideBySide: boolean +}; + +type State = { + diff: string, + error?: Error +}; + +class ScmDiff extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { diff: "" }; + } + + componentDidMount() { + const { changeset } = this.props; + const url = changeset._links.diff.href+"?format=GIT"; + apiClient + .get(url) + .then(response => response.text()) + .then(text => this.setState({ ...this.state, diff: text })) + .catch(error => this.setState({ ...this.state, error })); + } + + render() { + const options = { + inputFormat: "diff", + outputFormat: this.props.sideBySide ? "side-by-side" : "line-by-line", + showFiles: false, + matching: "lines" + }; + + const outputHtml = Diff2Html.getPrettyHtml(this.state.diff, options); + + return ( + // eslint-disable-next-line react/no-danger + <div dangerouslySetInnerHTML={{ __html: outputHtml }} /> + ); + } +} + +export default ScmDiff; diff --git a/scm-ui/src/repos/modules/changesets.js b/scm-ui/src/repos/modules/changesets.js index 3cd617ac56..80c405f5de 100644 --- a/scm-ui/src/repos/modules/changesets.js +++ b/scm-ui/src/repos/modules/changesets.js @@ -1,19 +1,10 @@ // @flow -import { - FAILURE_SUFFIX, - PENDING_SUFFIX, - SUCCESS_SUFFIX -} from "../../modules/types"; -import { apiClient, urls } from "@scm-manager/ui-components"; -import { isPending } from "../../modules/pending"; -import { getFailure } from "../../modules/failure"; -import type { - Action, - Branch, - PagedCollection, - Repository -} from "@scm-manager/ui-types"; +import {FAILURE_SUFFIX, PENDING_SUFFIX, SUCCESS_SUFFIX} from "../../modules/types"; +import {apiClient, urls} from "@scm-manager/ui-components"; +import {isPending} from "../../modules/pending"; +import {getFailure} from "../../modules/failure"; +import type {Action, Branch, PagedCollection, Repository} from "@scm-manager/ui-types"; export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS"; export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`; diff --git a/scm-ui/src/repos/modules/changesets.test.js b/scm-ui/src/repos/modules/changesets.test.js index 489312688d..b903981c80 100644 --- a/scm-ui/src/repos/modules/changesets.test.js +++ b/scm-ui/src/repos/modules/changesets.test.js @@ -4,27 +4,27 @@ import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; import reducer, { - FETCH_CHANGESETS, - FETCH_CHANGESETS_FAILURE, - FETCH_CHANGESETS_PENDING, - FETCH_CHANGESETS_SUCCESS, FETCH_CHANGESET, FETCH_CHANGESET_FAILURE, FETCH_CHANGESET_PENDING, FETCH_CHANGESET_SUCCESS, + FETCH_CHANGESETS, + FETCH_CHANGESETS_FAILURE, + FETCH_CHANGESETS_PENDING, + FETCH_CHANGESETS_SUCCESS, + fetchChangeset, + fetchChangesetIfNeeded, fetchChangesets, fetchChangesetsSuccess, - getChangesets, - getFetchChangesetsFailure, - isFetchChangesetsPending, - fetchChangeset, - getChangeset, - fetchChangesetIfNeeded, - shouldFetchChangeset, - isFetchChangesetPending, - getFetchChangesetFailure, fetchChangesetSuccess, - selectListAsCollection + getChangeset, + getChangesets, + getFetchChangesetFailure, + getFetchChangesetsFailure, + isFetchChangesetPending, + isFetchChangesetsPending, + selectListAsCollection, + shouldFetchChangeset } from "./changesets"; const branch = { diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index b5016bbb43..642f6cf395 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -29,6 +29,7 @@ export const MODIFY_REPO = "scm/repos/MODIFY_REPO"; export const MODIFY_REPO_PENDING = `${MODIFY_REPO}_${types.PENDING_SUFFIX}`; export const MODIFY_REPO_SUCCESS = `${MODIFY_REPO}_${types.SUCCESS_SUFFIX}`; export const MODIFY_REPO_FAILURE = `${MODIFY_REPO}_${types.FAILURE_SUFFIX}`; +export const MODIFY_REPO_RESET = `${MODIFY_REPO}_${types.RESET_SUFFIX}`; export const DELETE_REPO = "scm/repos/DELETE_REPO"; export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`; @@ -99,13 +100,20 @@ export function fetchReposFailure(err: Error): Action { } // fetch repo +export function fetchRepoByLink(repo: Repository) { + return fetchRepo(repo._links.self.href, repo.namespace, repo.name); +} -export function fetchRepo(link: string, namespace: string, name: string) { +export function fetchRepoByName(link: string, namespace: string, name: string) { const repoUrl = link.endsWith("/") ? link : link + "/"; + return fetchRepo(`${repoUrl}${namespace}/${name}`, namespace, name); +} + +function fetchRepo(link: string, namespace: string, name: string) { return function(dispatch: any) { dispatch(fetchRepoPending(namespace, name)); return apiClient - .get(`${repoUrl}${namespace}/${name}`) + .get(link) .then(response => response.json()) .then(repository => { dispatch(fetchRepoSuccess(repository)); @@ -213,6 +221,9 @@ export function modifyRepo(repository: Repository, callback?: () => void) { callback(); } }) + .then(() => { + dispatch(fetchRepoByLink(repository)); + }) .catch(cause => { const error = new Error(`failed to modify repo: ${cause.message}`); dispatch(modifyRepoFailure(repository, error)); @@ -247,6 +258,14 @@ export function modifyRepoFailure( }; } +export function modifyRepoReset(repository: Repository): Action { + return { + type: MODIFY_REPO_RESET, + payload: { repository }, + itemId: createIdentifier(repository) + }; +} + // delete export function deleteRepo(repository: Repository, callback?: () => void) { @@ -347,8 +366,6 @@ export default function reducer( switch (action.type) { case FETCH_REPOS_SUCCESS: return normalizeByNamespaceAndName(action.payload); - case MODIFY_REPO_SUCCESS: - return reducerByNames(state, action.payload); case FETCH_REPO_SUCCESS: return reducerByNames(state, action.payload); default: diff --git a/scm-ui/src/repos/modules/repos.test.js b/scm-ui/src/repos/modules/repos.test.js index 5b5c2d3abd..e8d9873e99 100644 --- a/scm-ui/src/repos/modules/repos.test.js +++ b/scm-ui/src/repos/modules/repos.test.js @@ -15,7 +15,8 @@ import reducer, { fetchReposByLink, fetchReposByPage, FETCH_REPO, - fetchRepo, + fetchRepoByLink, + fetchRepoByName, FETCH_REPO_PENDING, FETCH_REPO_SUCCESS, FETCH_REPO_FAILURE, @@ -45,7 +46,6 @@ import reducer, { MODIFY_REPO, isModifyRepoPending, getModifyRepoFailure, - modifyRepoSuccess, getPermissionsLink } from "./repos"; import type { Repository, RepositoryCollection } from "@scm-manager/ui-types"; @@ -323,7 +323,7 @@ describe("repos fetch", () => { }); }); - it("should successfully fetch repo slarti/fjords", () => { + it("should successfully fetch repo slarti/fjords by name", () => { fetchMock.getOnce(REPOS_URL + "/slarti/fjords", slartiFjords); const expectedActions = [ @@ -343,18 +343,66 @@ describe("repos fetch", () => { ]; const store = mockStore({}); - return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => { + return store.dispatch(fetchRepoByName(URL, "slarti", "fjords")).then(() => { expect(store.getActions()).toEqual(expectedActions); }); }); - it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => { + it("should dispatch FETCH_REPO_FAILURE, if the request for slarti/fjords by name fails", () => { fetchMock.getOnce(REPOS_URL + "/slarti/fjords", { status: 500 }); const store = mockStore({}); - return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => { + return store.dispatch(fetchRepoByName(URL, "slarti", "fjords")).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_REPO_PENDING); + expect(actions[1].type).toEqual(FETCH_REPO_FAILURE); + expect(actions[1].payload.namespace).toBe("slarti"); + expect(actions[1].payload.name).toBe("fjords"); + expect(actions[1].payload.error).toBeDefined(); + expect(actions[1].itemId).toBe("slarti/fjords"); + }); + }); + + it("should successfully fetch repo slarti/fjords", () => { + fetchMock.getOnce( + "http://localhost:8081/api/v2/repositories/slarti/fjords", + slartiFjords + ); + + const expectedActions = [ + { + type: FETCH_REPO_PENDING, + payload: { + namespace: "slarti", + name: "fjords" + }, + itemId: "slarti/fjords" + }, + { + type: FETCH_REPO_SUCCESS, + payload: slartiFjords, + itemId: "slarti/fjords" + } + ]; + + const store = mockStore({}); + return store.dispatch(fetchRepoByLink(slartiFjords)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it("should dispatch FETCH_REPO_FAILURE, it the request for slarti/fjords fails", () => { + fetchMock.getOnce( + "http://localhost:8081/api/v2/repositories/slarti/fjords", + { + status: 500 + } + ); + + const store = mockStore({}); + return store.dispatch(fetchRepoByLink(slartiFjords)).then(() => { const actions = store.getActions(); expect(actions[0].type).toEqual(FETCH_REPO_PENDING); expect(actions[1].type).toEqual(FETCH_REPO_FAILURE); @@ -485,6 +533,12 @@ describe("repos fetch", () => { fetchMock.putOnce(slartiFjords._links.update.href, { status: 204 }); + fetchMock.getOnce( + "http://localhost:8081/api/v2/repositories/slarti/fjords", + { + status: 500 + } + ); let editedFjords = { ...slartiFjords }; editedFjords.description = "coast of africa"; @@ -495,6 +549,7 @@ describe("repos fetch", () => { const actions = store.getActions(); expect(actions[0].type).toEqual(MODIFY_REPO_PENDING); expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS); + expect(actions[2].type).toEqual(FETCH_REPO_PENDING); }); }); @@ -502,6 +557,12 @@ describe("repos fetch", () => { fetchMock.putOnce(slartiFjords._links.update.href, { status: 204 }); + fetchMock.getOnce( + "http://localhost:8081/api/v2/repositories/slarti/fjords", + { + status: 500 + } + ); let editedFjords = { ...slartiFjords }; editedFjords.description = "coast of africa"; @@ -517,6 +578,7 @@ describe("repos fetch", () => { const actions = store.getActions(); expect(actions[0].type).toEqual(MODIFY_REPO_PENDING); expect(actions[1].type).toEqual(MODIFY_REPO_SUCCESS); + expect(actions[2].type).toEqual(FETCH_REPO_PENDING); expect(called).toBe(true); }); }); @@ -574,18 +636,6 @@ describe("repos reducer", () => { const newState = reducer({}, fetchRepoSuccess(slartiFjords)); expect(newState.byNames["slarti/fjords"]).toBe(slartiFjords); }); - - it("should update reposByNames", () => { - const oldState = { - byNames: { - "slarti/fjords": slartiFjords - } - }; - let slartiFjordsEdited = { ...slartiFjords }; - slartiFjordsEdited.description = "I bless the rains down in Africa"; - const newState = reducer(oldState, modifyRepoSuccess(slartiFjordsEdited)); - expect(newState.byNames["slarti/fjords"]).toEqual(slartiFjordsEdited); - }); }); describe("repos selectors", () => { diff --git a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js b/scm-ui/src/repos/permissions/components/CreatePermissionForm.js index 0bc42fac9f..a674f6b41f 100644 --- a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/components/CreatePermissionForm.js @@ -1,12 +1,9 @@ // @flow import React from "react"; -import { translate } from "react-i18next"; -import { Checkbox, InputField, SubmitButton } from "@scm-manager/ui-components"; +import {translate} from "react-i18next"; +import {Checkbox, InputField, SubmitButton} from "@scm-manager/ui-components"; import TypeSelector from "./TypeSelector"; -import type { - PermissionCollection, - PermissionCreateEntry -} from "@scm-manager/ui-types"; +import type {PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types"; import * as validator from "./permissionValidation"; type Props = { diff --git a/scm-ui/src/repos/permissions/components/permissionValidation.js b/scm-ui/src/repos/permissions/components/permissionValidation.js index d63ab72d54..6e581f2832 100644 --- a/scm-ui/src/repos/permissions/components/permissionValidation.js +++ b/scm-ui/src/repos/permissions/components/permissionValidation.js @@ -1,6 +1,7 @@ // @flow -import { validation } from "@scm-manager/ui-components"; -import type { PermissionCollection } from "@scm-manager/ui-types"; +import {validation} from "@scm-manager/ui-components"; +import type {PermissionCollection} from "@scm-manager/ui-types"; + const isNameValid = validation.isNameValid; export { isNameValid }; diff --git a/scm-ui/src/repos/permissions/modules/permissions.js b/scm-ui/src/repos/permissions/modules/permissions.js index 465d7a4ee0..154ee8123f 100644 --- a/scm-ui/src/repos/permissions/modules/permissions.js +++ b/scm-ui/src/repos/permissions/modules/permissions.js @@ -1,16 +1,12 @@ // @flow -import { apiClient } from "@scm-manager/ui-components"; +import type {Action} from "@scm-manager/ui-components"; +import {apiClient} from "@scm-manager/ui-components"; import * as types from "../../../modules/types"; -import type { Action } from "@scm-manager/ui-components"; -import type { - PermissionCollection, - Permission, - PermissionCreateEntry -} from "@scm-manager/ui-types"; -import { isPending } from "../../../modules/pending"; -import { getFailure } from "../../../modules/failure"; -import { Dispatch } from "redux"; +import type {Permission, PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types"; +import {isPending} from "../../../modules/pending"; +import {getFailure} from "../../../modules/failure"; +import {Dispatch} from "redux"; export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS"; export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${ diff --git a/scm-ui/src/repos/permissions/modules/permissions.test.js b/scm-ui/src/repos/permissions/modules/permissions.test.js index 3043a7db29..8ac02b7ec0 100644 --- a/scm-ui/src/repos/permissions/modules/permissions.test.js +++ b/scm-ui/src/repos/permissions/modules/permissions.test.js @@ -3,44 +3,44 @@ import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; import reducer, { - fetchPermissions, - fetchPermissionsSuccess, - getPermissionsOfRepo, - isFetchPermissionsPending, - getFetchPermissionsFailure, - modifyPermission, - modifyPermissionSuccess, - getModifyPermissionFailure, - isModifyPermissionPending, - createPermission, - hasCreatePermission, - deletePermission, - deletePermissionSuccess, - getDeletePermissionFailure, - isDeletePermissionPending, - getModifyPermissionsFailure, - MODIFY_PERMISSION_FAILURE, - MODIFY_PERMISSION_PENDING, - FETCH_PERMISSIONS, - FETCH_PERMISSIONS_PENDING, - FETCH_PERMISSIONS_SUCCESS, - FETCH_PERMISSIONS_FAILURE, - MODIFY_PERMISSION_SUCCESS, - MODIFY_PERMISSION, + CREATE_PERMISSION, + CREATE_PERMISSION_FAILURE, CREATE_PERMISSION_PENDING, CREATE_PERMISSION_SUCCESS, - CREATE_PERMISSION_FAILURE, + createPermission, + createPermissionSuccess, DELETE_PERMISSION, + DELETE_PERMISSION_FAILURE, DELETE_PERMISSION_PENDING, DELETE_PERMISSION_SUCCESS, - DELETE_PERMISSION_FAILURE, - CREATE_PERMISSION, - createPermissionSuccess, + deletePermission, + deletePermissionSuccess, + FETCH_PERMISSIONS, + FETCH_PERMISSIONS_FAILURE, + FETCH_PERMISSIONS_PENDING, + FETCH_PERMISSIONS_SUCCESS, + fetchPermissions, + fetchPermissionsSuccess, getCreatePermissionFailure, + getDeletePermissionFailure, + getDeletePermissionsFailure, + getFetchPermissionsFailure, + getModifyPermissionFailure, + getModifyPermissionsFailure, + getPermissionsOfRepo, + hasCreatePermission, isCreatePermissionPending, - getDeletePermissionsFailure + isDeletePermissionPending, + isFetchPermissionsPending, + isModifyPermissionPending, + MODIFY_PERMISSION, + MODIFY_PERMISSION_FAILURE, + MODIFY_PERMISSION_PENDING, + MODIFY_PERMISSION_SUCCESS, + modifyPermission, + modifyPermissionSuccess } from "./permissions"; -import type { Permission, PermissionCollection } from "@scm-manager/ui-types"; +import type {Permission, PermissionCollection} from "@scm-manager/ui-types"; const hitchhiker_puzzle42Permission_user_eins: Permission = { name: "user_eins", diff --git a/scm-ui/src/repos/sources/components/FileTree.js b/scm-ui/src/repos/sources/components/FileTree.js index 02aa22f942..e9b5c70d3d 100644 --- a/scm-ui/src/repos/sources/components/FileTree.js +++ b/scm-ui/src/repos/sources/components/FileTree.js @@ -7,7 +7,6 @@ import FileTreeLeaf from "./FileTreeLeaf"; import type { Repository, File } from "@scm-manager/ui-types"; import { ErrorNotification, Loading } from "@scm-manager/ui-components"; import { - fetchSources, getFetchSourcesFailure, isFetchSourcesPending, getSources @@ -29,7 +28,6 @@ type Props = { revision: string, path: string, baseUrl: string, - fetchSources: (Repository, string, string) => void, // context props classes: any, t: string => string, @@ -49,19 +47,6 @@ export function findParent(path: string) { } class FileTree extends React.Component<Props> { - componentDidMount() { - const { fetchSources, repository, revision, path } = this.props; - - fetchSources(repository, revision, path); - } - - componentDidUpdate(prevProps) { - const { fetchSources, repository, revision, path } = this.props; - if (prevProps.revision !== revision || prevProps.path !== path) { - fetchSources(repository, revision, path); - } - } - render() { const { error, @@ -167,18 +152,7 @@ const mapStateToProps = (state: any, ownProps: Props) => { }; }; -const mapDispatchToProps = dispatch => { - return { - fetchSources: (repository: Repository, revision: string, path: string) => { - dispatch(fetchSources(repository, revision, path)); - } - }; -}; - export default compose( withRouter, - connect( - mapStateToProps, - mapDispatchToProps - ) + connect(mapStateToProps) )(injectSheet(styles)(translate("repos")(FileTree))); diff --git a/scm-ui/src/repos/sources/components/FileTreeLeaf.js b/scm-ui/src/repos/sources/components/FileTreeLeaf.js index 033d3b9b8a..b4e2ad59ea 100644 --- a/scm-ui/src/repos/sources/components/FileTreeLeaf.js +++ b/scm-ui/src/repos/sources/components/FileTreeLeaf.js @@ -49,14 +49,18 @@ class FileTreeLeaf extends React.Component<Props> { </Link> ); } - return <FileIcon file={file} />; + return ( + <Link to={this.createLink(file)}> + <FileIcon file={file} /> + </Link> + ); }; createFileName = (file: File) => { if (file.directory) { return <Link to={this.createLink(file)}>{file.name}</Link>; } - return file.name; + return <Link to={this.createLink(file)}>{file.name}</Link>; }; render() { diff --git a/scm-ui/src/repos/sources/components/content/DownloadViewer.js b/scm-ui/src/repos/sources/components/content/DownloadViewer.js new file mode 100644 index 0000000000..4b84d7a53d --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/DownloadViewer.js @@ -0,0 +1,26 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { File } from "@scm-manager/ui-types"; +import { DownloadButton } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + file: File +}; + +class DownloadViewer extends React.Component<Props> { + render() { + const { t, file } = this.props; + return ( + <div className="has-text-centered"> + <DownloadButton + url={file._links.self.href} + displayName={t("sources.content.downloadButton")} + /> + </div> + ); + } +} + +export default translate("repos")(DownloadViewer); diff --git a/scm-ui/src/repos/sources/components/content/ImageViewer.js b/scm-ui/src/repos/sources/components/content/ImageViewer.js new file mode 100644 index 0000000000..67003fa357 --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/ImageViewer.js @@ -0,0 +1,24 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { File } from "@scm-manager/ui-types"; + +type Props = { + t: string => string, + file: File +}; + +class ImageViewer extends React.Component<Props> { + render() { + const { file } = this.props; + return ( + <div className="has-text-centered"> + <figure> + <img src={file._links.self.href} alt={file._links.self.href} /> + </figure> + </div> + ); + } +} + +export default translate("repos")(ImageViewer); diff --git a/scm-ui/src/repos/sources/components/content/SourcecodeViewer.js b/scm-ui/src/repos/sources/components/content/SourcecodeViewer.js new file mode 100644 index 0000000000..e54f8e1b3b --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/SourcecodeViewer.js @@ -0,0 +1,97 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { apiClient } from "@scm-manager/ui-components"; +import type { File } from "@scm-manager/ui-types"; +import { ErrorNotification, Loading } from "@scm-manager/ui-components"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { arduinoLight } from "react-syntax-highlighter/styles/hljs"; + +type Props = { + t: string => string, + file: File, + language: string +}; + +type State = { + content: string, + error?: Error, + loaded: boolean +}; + +class SourcecodeViewer extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + content: "", + loaded: false + }; + } + + componentDidMount() { + const { file } = this.props; + getContent(file._links.self.href) + .then(result => { + if (result.error) { + this.setState({ + ...this.state, + error: result.error, + loaded: true + }); + } else { + this.setState({ + ...this.state, + content: result, + loaded: true + }); + } + }) + .catch(err => {}); + } + + render() { + const { content, error, loaded } = this.state; + const language = this.props.language; + + if (error) { + return <ErrorNotification error={error} />; + } + + if (!loaded) { + return <Loading />; + } + + if (!content) { + return null; + } + + return ( + <SyntaxHighlighter + showLineNumbers="true" + language={getLanguage(language)} + style={arduinoLight} + > + {content} + </SyntaxHighlighter> + ); + } +} + +export function getLanguage(language: string) { + return language.toLowerCase(); +} + +export function getContent(url: string) { + return apiClient + .get(url) + .then(response => response.text()) + .then(response => { + return response; + }) + .catch(err => { + return { error: err }; + }); +} + +export default translate("repos")(SourcecodeViewer); diff --git a/scm-ui/src/repos/sources/components/content/SourcecodeViewer.test.js b/scm-ui/src/repos/sources/components/content/SourcecodeViewer.test.js new file mode 100644 index 0000000000..11e701f626 --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/SourcecodeViewer.test.js @@ -0,0 +1,33 @@ +//@flow +import fetchMock from "fetch-mock"; +import { + getContent, + getLanguage +} from "./SourcecodeViewer"; + +describe("get content", () => { + const CONTENT_URL = "/repositories/scmadmin/TestRepo/content/testContent"; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should return content", done => { + fetchMock.getOnce("/api/v2" + CONTENT_URL, "This is a testContent"); + + getContent(CONTENT_URL).then(content => { + expect(content).toBe("This is a testContent"); + done(); + }); + }); +}); + +describe("get correct language type", () => { + it("should return javascript", () => { + expect(getLanguage("JAVASCRIPT")).toBe("javascript"); + }); + it("should return nothing for plain text", () => { + expect(getLanguage("")).toBe(""); + }); +}); diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js new file mode 100644 index 0000000000..0b764aa6a1 --- /dev/null +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -0,0 +1,217 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { getSources } from "../modules/sources"; +import type { Repository, File } from "@scm-manager/ui-types"; +import { + ErrorNotification, + Loading, + DateFromNow +} from "@scm-manager/ui-components"; +import { connect } from "react-redux"; +import ImageViewer from "../components/content/ImageViewer"; +import SourcecodeViewer from "../components/content/SourcecodeViewer"; +import DownloadViewer from "../components/content/DownloadViewer"; +import FileSize from "../components/FileSize"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { getContentType } from "./contentType"; + +type Props = { + loading: boolean, + error: Error, + file: File, + repository: Repository, + revision: string, + path: string, + classes: any, + t: string => string +}; + +type State = { + contentType: string, + language: string, + loaded: boolean, + collapsed: boolean, + error?: Error +}; + +const styles = { + toCenterContent: { + display: "block" + }, + pointer: { + cursor: "pointer" + } +}; + +class Content extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + contentType: "", + language: "", + loaded: false, + collapsed: true + }; + } + + componentDidMount() { + const { file } = this.props; + getContentType(file._links.self.href) + .then(result => { + if (result.error) { + this.setState({ + ...this.state, + error: result.error, + loaded: true + }); + } else { + this.setState({ + ...this.state, + contentType: result.type, + language: result.language, + loaded: true + }); + } + }) + .catch(err => {}); + } + + toggleCollapse = () => { + this.setState(prevState => ({ + collapsed: !prevState.collapsed + })); + }; + + showHeader() { + const { file, classes } = this.props; + const collapsed = this.state.collapsed; + const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; + const fileSize = file.directory ? "" : <FileSize bytes={file.length} />; + + return ( + <span className={classes.pointer} onClick={this.toggleCollapse}> + <article className="media"> + <div className="media-left"> + <i className={classNames("fa", icon)} /> + </div> + <div className="media-content"> + <div className="content">{file.name}</div> + </div> + <p className="media-right">{fileSize}</p> + </article> + </span> + ); + } + + showMoreInformation() { + const collapsed = this.state.collapsed; + const { classes, file, revision } = this.props; + const date = <DateFromNow date={file.lastModified} />; + const description = file.description ? ( + <p> + {file.description.split("\n").map((item, key) => { + return ( + <span key={key}> + {item} + <br /> + </span> + ); + })} + </p> + ) : null; + if (!collapsed) { + return ( + <div className={classNames("panel-block", classes.toCenterContent)}> + <table className="table"> + <tbody> + <tr> + <td>Path</td> + <td>{file.path}</td> + </tr> + <tr> + <td>Branch</td> + <td>{revision}</td> + </tr> + <tr> + <td>Last modified</td> + <td>{date}</td> + </tr> + <tr> + <td>Description</td> + <td>{description}</td> + </tr> + </tbody> + </table> + </div> + ); + } + return null; + } + + showContent() { + const { file, revision } = this.props; + const { contentType, language } = this.state; + if (contentType.startsWith("image/")) { + return <ImageViewer file={file} />; + } else if (language) { + return <SourcecodeViewer file={file} language={language} />; + } else if (contentType.startsWith("text/")) { + return <SourcecodeViewer file={file} language="none" />; + } else { + return ( + <ExtensionPoint + name="repos.sources.view" + props={{ file, contentType, revision }} + > + <DownloadViewer file={file} /> + </ExtensionPoint> + ); + } + } + + render() { + const { file, classes } = this.props; + const { loaded, error } = this.state; + + if (!file || !loaded) { + return <Loading />; + } + if (error) { + return <ErrorNotification error={error} />; + } + + const header = this.showHeader(); + const content = this.showContent(); + const moreInformation = this.showMoreInformation(); + + return ( + <div> + <nav className="panel"> + <article className="panel-heading">{header}</article> + {moreInformation} + <div className={classNames("panel-block", classes.toCenterContent)}> + {content} + </div> + </nav> + </div> + ); + } +} + +const mapStateToProps = (state: any, ownProps: Props) => { + const { repository, revision, path } = ownProps; + + const file = getSources(state, repository, revision, path); + + return { + file + }; +}; + +export default injectSheet(styles)( + connect(mapStateToProps)(translate("repos")(Content)) +); diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index cf072e958e..1a9f1d62e7 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -13,6 +13,8 @@ import { isFetchBranchesPending } from "../../modules/branches"; import { compose } from "redux"; +import Content from "./Content"; +import { fetchSources, isDirectory } from "../modules/sources"; type Props = { repository: Repository, @@ -22,9 +24,11 @@ type Props = { branches: Branch[], revision: string, path: string, + currentFileIsDirectory: boolean, // dispatch props fetchBranches: Repository => void, + fetchSources: (Repository, string, string) => void, // Context props history: any, @@ -33,14 +37,26 @@ type Props = { class Sources extends React.Component<Props> { componentDidMount() { - const { fetchBranches, repository } = this.props; + const { + fetchBranches, + repository, + revision, + path, + fetchSources + } = this.props; fetchBranches(repository); + fetchSources(repository, revision, path); + } + componentDidUpdate(prevProps) { + const { fetchSources, repository, revision, path } = this.props; + if (prevProps.revision !== revision || prevProps.path !== path) { + fetchSources(repository, revision, path); + } } branchSelected = (branch?: Branch) => { const { baseUrl, history, path } = this.props; - let url; if (branch) { if (path) { @@ -55,7 +71,15 @@ class Sources extends React.Component<Props> { }; render() { - const { repository, baseUrl, loading, error, revision, path } = this.props; + const { + repository, + baseUrl, + loading, + error, + revision, + path, + currentFileIsDirectory + } = this.props; if (error) { return <ErrorNotification error={error} />; @@ -65,21 +89,28 @@ class Sources extends React.Component<Props> { return <Loading />; } - return ( - <> - {this.renderBranchSelector()} - <FileTree - repository={repository} - revision={revision} - path={path} - baseUrl={baseUrl} - /> - </> - ); + if (currentFileIsDirectory) { + return ( + <> + {this.renderBranchSelector()} + <FileTree + repository={repository} + revision={revision} + path={path} + baseUrl={baseUrl} + /> + </> + ); + } else { + return ( + <Content repository={repository} revision={revision} path={path} /> + ); + } } renderBranchSelector = () => { const { repository, branches, revision } = this.props; + if (repository._links.branches) { return ( <BranchSelector @@ -99,10 +130,12 @@ const mapStateToProps = (state, ownProps) => { const { repository, match } = ownProps; const { revision, path } = match.params; const decodedRevision = revision ? decodeURIComponent(revision) : undefined; - const loading = isFetchBranchesPending(state, repository); const error = getFetchBranchesFailure(state, repository); const branches = getBranches(state, repository); + const currentFileIsDirectory = decodedRevision + ? isDirectory(state, repository, decodedRevision, path) + : isDirectory(state, repository, revision, path); return { repository, @@ -110,7 +143,8 @@ const mapStateToProps = (state, ownProps) => { path, loading, error, - branches + branches, + currentFileIsDirectory }; }; @@ -118,6 +152,9 @@ const mapDispatchToProps = dispatch => { return { fetchBranches: (repository: Repository) => { dispatch(fetchBranches(repository)); + }, + fetchSources: (repository: Repository, revision: string, path: string) => { + dispatch(fetchSources(repository, revision, path)); } }; }; diff --git a/scm-ui/src/repos/sources/containers/contentType.js b/scm-ui/src/repos/sources/containers/contentType.js new file mode 100644 index 0000000000..bf9888834b --- /dev/null +++ b/scm-ui/src/repos/sources/containers/contentType.js @@ -0,0 +1,16 @@ +//@flow +import { apiClient } from "@scm-manager/ui-components"; + +export function getContentType(url: string) { + return apiClient + .head(url) + .then(response => { + return { + type: response.headers.get("Content-Type"), + language: response.headers.get("X-Programming-Language") + }; + }) + .catch(err => { + return { error: err }; + }); +} diff --git a/scm-ui/src/repos/sources/containers/contentType.test.js b/scm-ui/src/repos/sources/containers/contentType.test.js new file mode 100644 index 0000000000..c3ab85ed80 --- /dev/null +++ b/scm-ui/src/repos/sources/containers/contentType.test.js @@ -0,0 +1,29 @@ +//@flow +import fetchMock from "fetch-mock"; +import { getContentType } from "./contentType"; + +describe("get content type", () => { + const CONTENT_URL = "/repositories/scmadmin/TestRepo/content/testContent"; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should return content", done => { + let headers = { + "Content-Type": "application/text", + "X-Programming-Language": "JAVA" + }; + + fetchMock.head("/api/v2" + CONTENT_URL, { + headers + }); + + getContentType(CONTENT_URL).then(content => { + expect(content.type).toBe("application/text"); + expect(content.language).toBe("JAVA"); + done(); + }); + }); +}); diff --git a/scm-ui/src/repos/sources/modules/sources.js b/scm-ui/src/repos/sources/modules/sources.js index 719770d75c..641c1550b6 100644 --- a/scm-ui/src/repos/sources/modules/sources.js +++ b/scm-ui/src/repos/sources/modules/sources.js @@ -102,6 +102,20 @@ export default function reducer( // selectors +export function isDirectory( + state: any, + repository: Repository, + revision: string, + path: string +): boolean { + const currentFile = getSources(state, repository, revision, path); + if (currentFile && !currentFile.directory) { + return false; + } else { + return true; //also return true if no currentFile is found since it is the "default" path + } +} + export function getSources( state: any, repository: Repository, diff --git a/scm-ui/src/repos/sources/modules/sources.test.js b/scm-ui/src/repos/sources/modules/sources.test.js index 068fa39e8f..1a5c81e908 100644 --- a/scm-ui/src/repos/sources/modules/sources.test.js +++ b/scm-ui/src/repos/sources/modules/sources.test.js @@ -1,6 +1,6 @@ // @flow -import type { Repository } from "@scm-manager/ui-types"; +import type { Repository, File } from "@scm-manager/ui-types"; import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; @@ -14,7 +14,8 @@ import { isFetchSourcesPending, default as reducer, getSources, - fetchSourcesSuccess + fetchSourcesSuccess, + isDirectory } from "./sources"; const sourcesUrl = @@ -79,6 +80,21 @@ const collection = { } }; +const noDirectory: File = { + name: "src", + path: "src", + directory: true, + length: 176, + revision: "abc", + _links: { + self: { + href: + "http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/src" + } + }, + _embedded: collection +}; + describe("sources fetch", () => { const mockStore = configureMockStore([thunk]); @@ -168,6 +184,28 @@ describe("reducer tests", () => { }); describe("selector tests", () => { + it("should return false if it is no directory", () => { + const state = { + sources: { + "scm/core/abc/src/main/package.json": { + noDirectory + } + } + }; + expect( + isDirectory(state, repository, "abc", "src/main/package.json") + ).toBeFalsy(); + }); + + it("should return true if it is directory", () => { + const state = { + sources: { + "scm/core/abc/src": noDirectory + } + }; + expect(isDirectory(state, repository, "abc", "src")).toBe(true); + }); + it("should return null", () => { expect(getSources({}, repository)).toBeFalsy(); }); @@ -181,7 +219,7 @@ describe("selector tests", () => { expect(getSources(state, repository)).toBe(collection); }); - it("should return the source collection without revision and path", () => { + it("should return the source collection with revision and path", () => { const state = { sources: { "scm/core/abc/src/main": collection diff --git a/scm-ui/src/users/components/SetUserPassword.js b/scm-ui/src/users/components/SetUserPassword.js new file mode 100644 index 0000000000..ff859f59bf --- /dev/null +++ b/scm-ui/src/users/components/SetUserPassword.js @@ -0,0 +1,177 @@ +// @flow +import React from "react"; +import type { User } from "@scm-manager/ui-types"; +import { + InputField, + SubmitButton, + Notification, + ErrorNotification +} from "@scm-manager/ui-components"; +import * as userValidator from "./userValidation"; +import { translate } from "react-i18next"; +import { updatePassword } from "./updatePassword"; + +type Props = { + user: User, + t: string => string +}; + +type State = { + password: string, + loading: boolean, + passwordConfirmationError: boolean, + validatePasswordError: boolean, + validatePassword: string, + error?: Error, + passwordChanged: boolean +}; + +class SetUserPassword extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + password: "", + loading: false, + passwordConfirmationError: false, + validatePasswordError: false, + validatePassword: "", + passwordChanged: false + }; + } + + passwordIsValid = () => { + return !( + this.state.validatePasswordError || this.state.passwordConfirmationError + ); + }; + + setLoadingState = () => { + this.setState({ + ...this.state, + loading: true + }); + }; + + setErrorState = (error: Error) => { + this.setState({ + ...this.state, + error: error, + loading: false + }); + }; + + setSuccessfulState = () => { + this.setState({ + ...this.state, + loading: false, + passwordChanged: true, + password: "", + validatePassword: "", + validatePasswordError: false, + passwordConfirmationError: false + }); + }; + + submit = (event: Event) => { + event.preventDefault(); + if (this.passwordIsValid()) { + const { user } = this.props; + const { password } = this.state; + this.setLoadingState(); + updatePassword(user._links.password.href, password) + .then(result => { + if (result.error) { + this.setErrorState(result.error); + } else { + this.setSuccessfulState(); + } + }) + .catch(err => {}); + } + }; + + render() { + const { t } = this.props; + const { loading, passwordChanged, error } = this.state; + + let message = null; + + if (passwordChanged) { + message = ( + <Notification + type={"success"} + children={t("password.set-password-successful")} + onClose={() => this.onClose()} + /> + ); + } else if (error) { + message = <ErrorNotification error={error} />; + } + + return ( + <form onSubmit={this.submit}> + {message} + <InputField + label={t("user.password")} + type="password" + onChange={this.handlePasswordChange} + value={this.state.password ? this.state.password : ""} + validationError={this.state.validatePasswordError} + errorMessage={t("validation.password-invalid")} + helpText={t("help.passwordHelpText")} + /> + <InputField + label={t("validation.validatePassword")} + type="password" + onChange={this.handlePasswordValidationChange} + value={this.state ? this.state.validatePassword : ""} + validationError={this.state.passwordConfirmationError} + errorMessage={t("validation.passwordValidation-invalid")} + helpText={t("help.passwordConfirmHelpText")} + /> + <SubmitButton + disabled={!this.passwordIsValid()} + loading={loading} + label={t("user-form.submit")} + /> + </form> + ); + } + + handlePasswordChange = (password: string) => { + const validatePasswordError = !this.checkPasswords( + password, + this.state.validatePassword + ); + this.setState({ + validatePasswordError: !userValidator.isPasswordValid(password), + passwordConfirmationError: validatePasswordError, + password: password + }); + }; + + handlePasswordValidationChange = (validatePassword: string) => { + const passwordConfirmed = this.checkPasswords( + this.state.password, + validatePassword + ); + this.setState({ + validatePassword, + passwordConfirmationError: !passwordConfirmed + }); + }; + + checkPasswords = (password1: string, password2: string) => { + return password1 === password2; + }; + + onClose = () => { + this.setState({ + ...this.state, + passwordChanged: false + }); + }; +} + +export default translate("users")(SetUserPassword); diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 80ade5e070..2b8aa7b1e8 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -22,7 +22,7 @@ type State = { mailValidationError: boolean, nameValidationError: boolean, displayNameValidationError: boolean, - passwordValidationError: boolean, + passwordConfirmationError: boolean, validatePasswordError: boolean, validatePassword: string }; @@ -44,7 +44,7 @@ class UserForm extends React.Component<Props, State> { mailValidationError: false, displayNameValidationError: false, nameValidationError: false, - passwordValidationError: false, + passwordConfirmationError: false, validatePasswordError: false, validatePassword: "" }; @@ -70,7 +70,7 @@ class UserForm extends React.Component<Props, State> { this.state.validatePasswordError || this.state.nameValidationError || this.state.mailValidationError || - this.state.passwordValidationError || + this.state.passwordConfirmationError || this.state.displayNameValidationError || this.isFalsy(user.name) || this.isFalsy(user.displayName) @@ -89,6 +89,7 @@ class UserForm extends React.Component<Props, State> { const user = this.state.user; let nameField = null; + let passwordFields = null; if (!this.props.user) { nameField = ( <InputField @@ -100,6 +101,28 @@ class UserForm extends React.Component<Props, State> { helpText={t("help.usernameHelpText")} /> ); + passwordFields = ( + <> + <InputField + label={t("user.password")} + type="password" + onChange={this.handlePasswordChange} + value={user ? user.password : ""} + validationError={this.state.validatePasswordError} + errorMessage={t("validation.password-invalid")} + helpText={t("help.passwordHelpText")} + /> + <InputField + label={t("validation.validatePassword")} + type="password" + onChange={this.handlePasswordValidationChange} + value={this.state ? this.state.validatePassword : ""} + validationError={this.state.passwordConfirmationError} + errorMessage={t("validation.passwordValidation-invalid")} + helpText={t("help.passwordConfirmHelpText")} + /> + </> + ); } return ( <form onSubmit={this.submit}> @@ -120,24 +143,7 @@ class UserForm extends React.Component<Props, State> { errorMessage={t("validation.mail-invalid")} helpText={t("help.mailHelpText")} /> - <InputField - label={t("user.password")} - type="password" - onChange={this.handlePasswordChange} - value={user ? user.password : ""} - validationError={this.state.validatePasswordError} - errorMessage={t("validation.password-invalid")} - helpText={t("help.passwordHelpText")} - /> - <InputField - label={t("validation.validatePassword")} - type="password" - onChange={this.handlePasswordValidationChange} - value={this.state ? this.state.validatePassword : ""} - validationError={this.state.passwordValidationError} - errorMessage={t("validation.passwordValidation-invalid")} - helpText={t("help.passwordConfirmHelpText")} - /> + {passwordFields} <Checkbox label={t("user.admin")} onChange={this.handleAdminChange} @@ -189,7 +195,7 @@ class UserForm extends React.Component<Props, State> { ); this.setState({ validatePasswordError: !userValidator.isPasswordValid(password), - passwordValidationError: validatePasswordError, + passwordConfirmationError: validatePasswordError, user: { ...this.state.user, password } }); }; @@ -201,7 +207,7 @@ class UserForm extends React.Component<Props, State> { ); this.setState({ validatePassword, - passwordValidationError: !validatePasswordError + passwordConfirmationError: !validatePasswordError }); }; diff --git a/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js new file mode 100644 index 0000000000..43b7a4b5a4 --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { User } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + user: User, + passwordUrl: String +}; + +class ChangePasswordNavLink extends React.Component<Props> { + render() { + const { t, passwordUrl } = this.props; + + if (!this.hasPermissionToSetPassword()) { + return null; + } + return <NavLink label={t("set-password-button.label")} to={passwordUrl} />; + } + + hasPermissionToSetPassword = () => { + return this.props.user._links.password; + }; +} + +export default translate("users")(ChangePasswordNavLink); diff --git a/scm-ui/src/users/components/navLinks/SetPasswordNavLink.test.js b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.test.js new file mode 100644 index 0000000000..75ce4e58cf --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.test.js @@ -0,0 +1,31 @@ +import React from "react"; +import { shallow } from "enzyme"; +import "../../../tests/enzyme"; +import "../../../tests/i18n"; +import ChangePasswordNavLink from "./SetPasswordNavLink"; + +it("should render nothing, if the password link is missing", () => { + const user = { + _links: {} + }; + + const navLink = shallow( + <ChangePasswordNavLink user={user} passwordUrl="/user/password" /> + ); + expect(navLink.text()).toBe(""); +}); + +it("should render the navLink", () => { + const user = { + _links: { + password: { + href: "/password" + } + } + }; + + const navLink = shallow( + <ChangePasswordNavLink user={user} passwordUrl="/user/password" /> + ); + expect(navLink.text()).not.toBe(""); +}); diff --git a/scm-ui/src/users/components/navLinks/index.js b/scm-ui/src/users/components/navLinks/index.js index a3ccd16a32..a6d8370c00 100644 --- a/scm-ui/src/users/components/navLinks/index.js +++ b/scm-ui/src/users/components/navLinks/index.js @@ -1,2 +1,3 @@ export { default as DeleteUserNavLink } from "./DeleteUserNavLink"; export { default as EditUserNavLink } from "./EditUserNavLink"; +export { default as SetPasswordNavLink } from "./SetPasswordNavLink"; diff --git a/scm-ui/src/users/components/updatePassword.js b/scm-ui/src/users/components/updatePassword.js new file mode 100644 index 0000000000..3915c90bd9 --- /dev/null +++ b/scm-ui/src/users/components/updatePassword.js @@ -0,0 +1,15 @@ +//@flow +import { apiClient } from "@scm-manager/ui-components"; +const CONTENT_TYPE_PASSWORD_OVERWRITE = + "application/vnd.scmm-passwordOverwrite+json;v=2"; + +export function updatePassword(url: string, password: string) { + return apiClient + .put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE) + .then(response => { + return response; + }) + .catch(err => { + return { error: err }; + }); +} diff --git a/scm-ui/src/users/components/updatePassword.test.js b/scm-ui/src/users/components/updatePassword.test.js new file mode 100644 index 0000000000..a5762406b2 --- /dev/null +++ b/scm-ui/src/users/components/updatePassword.test.js @@ -0,0 +1,23 @@ +//@flow +import fetchMock from "fetch-mock"; +import { updatePassword } from "./updatePassword"; + +describe("get content type", () => { + const PASSWORD_URL = "/users/testuser/password"; + const password = "testpw123"; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should update password", done => { + + fetchMock.put("/api/v2" + PASSWORD_URL, 204); + + updatePassword(PASSWORD_URL, password).then(content => { + + done(); + }); + }); +}); diff --git a/scm-ui/src/users/containers/EditUser.js b/scm-ui/src/users/containers/EditUser.js index b99601f3ca..55062ecb5b 100644 --- a/scm-ui/src/users/containers/EditUser.js +++ b/scm-ui/src/users/containers/EditUser.js @@ -7,7 +7,8 @@ import type { User } from "@scm-manager/ui-types"; import { modifyUser, isModifyUserPending, - getModifyUserFailure + getModifyUserFailure, + modifyUserReset } from "../modules/users"; import type { History } from "history"; import { ErrorNotification } from "@scm-manager/ui-components"; @@ -18,6 +19,7 @@ type Props = { // dispatch functions modifyUser: (user: User, callback?: () => void) => void, + modifyUserReset: User => void, // context objects user: User, @@ -25,6 +27,10 @@ type Props = { }; class EditUser extends React.Component<Props> { + componentDidMount() { + const { modifyUserReset, user } = this.props; + modifyUserReset(user); + } userModified = (user: User) => () => { this.props.history.push(`/user/${user.name}`); }; @@ -52,6 +58,9 @@ const mapDispatchToProps = dispatch => { return { modifyUser: (user: User, callback?: () => void) => { dispatch(modifyUser(user, callback)); + }, + modifyUserReset: (user: User) => { + dispatch(modifyUserReset(user)); } }; }; diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index f581874742..5f20598962 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -15,7 +15,7 @@ import EditUser from "./EditUser"; import type { User } from "@scm-manager/ui-types"; import type { History } from "history"; import { - fetchUser, + fetchUserByName, deleteUser, getUserByName, isFetchUserPending, @@ -24,9 +24,14 @@ import { getDeleteUserFailure } from "../modules/users"; -import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks"; +import { + DeleteUserNavLink, + EditUserNavLink, + SetPasswordNavLink +} from "./../components/navLinks"; import { translate } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; +import SetUserPassword from "../components/SetUserPassword"; type Props = { name: string, @@ -37,7 +42,7 @@ type Props = { // dispatcher functions deleteUser: (user: User, callback?: () => void) => void, - fetchUser: (string, string) => void, + fetchUserByName: (string, string) => void, // context objects t: string => string, @@ -47,7 +52,7 @@ type Props = { class SingleUser extends React.Component<Props> { componentDidMount() { - this.props.fetchUser(this.props.usersLink, this.props.name); + this.props.fetchUserByName(this.props.usersLink, this.props.name); } userDeleted = () => { @@ -97,6 +102,10 @@ class SingleUser extends React.Component<Props> { path={`${url}/edit`} component={() => <EditUser user={user} />} /> + <Route + path={`${url}/password`} + component={() => <SetUserPassword user={user} />} + /> </div> <div className="column"> <Navigation> @@ -106,6 +115,10 @@ class SingleUser extends React.Component<Props> { label={t("single-user.information-label")} /> <EditUserNavLink user={user} editUrl={`${url}/edit`} /> + <SetPasswordNavLink + user={user} + passwordUrl={`${url}/password`} + /> </Section> <Section label={t("single-user.actions-label")}> <DeleteUserNavLink user={user} deleteUser={this.deleteUser} /> @@ -138,8 +151,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchUser: (link: string, name: string) => { - dispatch(fetchUser(link, name)); + fetchUserByName: (link: string, name: string) => { + dispatch(fetchUserByName(link, name)); }, deleteUser: (user: User, callback?: () => void) => { dispatch(deleteUser(user, callback)); diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index 80d8107b3e..fe751d13d4 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -26,6 +26,7 @@ export const MODIFY_USER = "scm/users/MODIFY_USER"; export const MODIFY_USER_PENDING = `${MODIFY_USER}_${types.PENDING_SUFFIX}`; export const MODIFY_USER_SUCCESS = `${MODIFY_USER}_${types.SUCCESS_SUFFIX}`; export const MODIFY_USER_FAILURE = `${MODIFY_USER}_${types.FAILURE_SUFFIX}`; +export const MODIFY_USER_RESET = `${MODIFY_USER}_${types.RESET_SUFFIX}`; export const DELETE_USER = "scm/users/DELETE"; export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`; @@ -87,12 +88,20 @@ export function fetchUsersFailure(url: string, error: Error): Action { } //fetch user -export function fetchUser(link: string, name: string) { +export function fetchUserByName(link: string, name: string) { const userUrl = link.endsWith("/") ? link + name : link + "/" + name; + return fetchUser(userUrl, name); +} + +export function fetchUserByLink(user: User) { + return fetchUser(user._links.self.href, user.name); +} + +function fetchUser(link: string, name: string) { return function(dispatch: any) { dispatch(fetchUserPending(name)); return apiClient - .get(userUrl) + .get(link) .then(response => { return response.json(); }) @@ -195,6 +204,9 @@ export function modifyUser(user: User, callback?: () => void) { callback(); } }) + .then(() => { + dispatch(fetchUserByLink(user)); + }) .catch(err => { dispatch(modifyUserFailure(user, err)); }); @@ -228,6 +240,13 @@ export function modifyUserFailure(user: User, error: Error): Action { }; } +export function modifyUserReset(user: User): Action { + return { + type: MODIFY_USER_RESET, + itemId: user.name + }; +} + //delete user export function deleteUser(user: User, callback?: () => void) { @@ -365,9 +384,6 @@ function byNamesReducer(state: any = {}, action: any = {}) { case FETCH_USER_SUCCESS: return reducerByName(state, action.payload.name, action.payload); - case MODIFY_USER_SUCCESS: - return reducerByName(state, action.payload.name, action.payload); - case DELETE_USER_SUCCESS: const newUserByNames = deleteUserInUsersByNames( state, diff --git a/scm-ui/src/users/modules/users.test.js b/scm-ui/src/users/modules/users.test.js index 03da4658c2..ac4d1c97a9 100644 --- a/scm-ui/src/users/modules/users.test.js +++ b/scm-ui/src/users/modules/users.test.js @@ -20,7 +20,8 @@ import reducer, { FETCH_USERS_FAILURE, FETCH_USERS_PENDING, FETCH_USERS_SUCCESS, - fetchUser, + fetchUserByLink, + fetchUserByName, fetchUserSuccess, getFetchUserFailure, fetchUsers, @@ -33,7 +34,6 @@ import reducer, { MODIFY_USER_PENDING, MODIFY_USER_SUCCESS, modifyUser, - modifyUserSuccess, getUsersFromState, FETCH_USERS, getFetchUsersFailure, @@ -124,6 +124,7 @@ const response = { const URL = "users"; const USERS_URL = "/api/v2/users"; +const USER_ZAPHOD_URL = "http://localhost:8081/api/v2/users/zaphod"; const error = new Error("KAPUTT"); @@ -166,11 +167,37 @@ describe("users fetch()", () => { }); }); - it("should sucessfully fetch single user", () => { + it("should sucessfully fetch single user by name", () => { fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod); const store = mockStore({}); - return store.dispatch(fetchUser(URL, "zaphod")).then(() => { + return store.dispatch(fetchUserByName(URL, "zaphod")).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_USER_PENDING); + expect(actions[1].type).toEqual(FETCH_USER_SUCCESS); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should fail fetching single user by name on HTTP 500", () => { + fetchMock.getOnce(USERS_URL + "/zaphod", { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(fetchUserByName(URL, "zaphod")).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_USER_PENDING); + expect(actions[1].type).toEqual(FETCH_USER_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + it("should sucessfully fetch single user", () => { + fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod); + + const store = mockStore({}); + return store.dispatch(fetchUserByLink(userZaphod)).then(() => { const actions = store.getActions(); expect(actions[0].type).toEqual(FETCH_USER_PENDING); expect(actions[1].type).toEqual(FETCH_USER_SUCCESS); @@ -179,12 +206,12 @@ describe("users fetch()", () => { }); it("should fail fetching single user on HTTP 500", () => { - fetchMock.getOnce(USERS_URL + "/zaphod", { + fetchMock.getOnce(USER_ZAPHOD_URL, { status: 500 }); const store = mockStore({}); - return store.dispatch(fetchUser(URL, "zaphod")).then(() => { + return store.dispatch(fetchUserByLink(userZaphod)).then(() => { const actions = store.getActions(); expect(actions[0].type).toEqual(FETCH_USER_PENDING); expect(actions[1].type).toEqual(FETCH_USER_FAILURE); @@ -242,23 +269,26 @@ describe("users fetch()", () => { }); it("successfully update user", () => { - fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", { + fetchMock.putOnce(USER_ZAPHOD_URL, { status: 204 }); + fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod); const store = mockStore({}); return store.dispatch(modifyUser(userZaphod)).then(() => { const actions = store.getActions(); - expect(actions.length).toBe(2); + expect(actions.length).toBe(3); expect(actions[0].type).toEqual(MODIFY_USER_PENDING); expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS); + expect(actions[2].type).toEqual(FETCH_USER_PENDING); }); }); it("should call callback, after successful modified user", () => { - fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", { + fetchMock.putOnce(USER_ZAPHOD_URL, { status: 204 }); + fetchMock.getOnce(USER_ZAPHOD_URL, userZaphod); let called = false; const callMe = () => { @@ -415,20 +445,6 @@ describe("users reducer", () => { expect(newState.byNames["ford"]).toBe(userFord); expect(newState.list.entries).toEqual(["zaphod"]); }); - - it("should update state according to MODIFY_USER_SUCCESS action", () => { - const newState = reducer( - { - byNames: { - ford: { - name: "ford" - } - } - }, - modifyUserSuccess(userFord) - ); - expect(newState.byNames["ford"]).toBe(userFord); - }); }); describe("selector tests", () => { diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index 3ebe407a38..4d306df6ad 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -1,6 +1,7 @@ @import "bulma/sass/utilities/initial-variables"; @import "bulma/sass/utilities/functions"; + $blue: #33B2E8; // $footer-background-color @@ -52,3 +53,5 @@ $blue: #33B2E8; @import "@fortawesome/fontawesome-free/scss/fontawesome.scss"; $fa-font-path: "webfonts"; @import "@fortawesome/fontawesome-free/scss/solid.scss"; + +@import "diff2html/dist/diff2html"; diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 18b198622c..ec5a53aecc 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -645,9 +645,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb" +"@scm-manager/ui-bundler@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -685,9 +685,9 @@ vinyl-source-stream "^2.0.0" watchify "^3.11.0" -"@scm-manager/ui-extensions@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e" +"@scm-manager/ui-extensions@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45" dependencies: react "^16.4.2" react-dom "^16.4.2" @@ -1181,7 +1181,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -1731,7 +1731,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" dependencies: @@ -1739,6 +1739,18 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +character-entities-legacy@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz#7c6defb81648498222c9855309953d05f4d63a9c" + +character-entities@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.2.tgz#58c8f371c0774ef0ba9b2aca5f00d8f100e6e363" + +character-reference-invalid@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz#21e421ad3d84055952dab4a43a04e73cd425d3ed" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -1820,7 +1832,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.5: +classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" @@ -1840,6 +1852,14 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" +clipboard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.1.tgz#a12481e1c13d8a50f5f036b0560fe5d16d74e46a" + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -1946,6 +1966,12 @@ combined-stream@^1.0.5, combined-stream@~1.0.5, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.5.tgz#b13793131d9ea2d2431cf5b507ddec258f0ce0db" + dependencies: + trim "0.0.1" + commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -2327,6 +2353,10 @@ delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" +delegate@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" + delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -2395,7 +2425,16 @@ dev-ip@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0" -diff@^3.2.0: +diff2html@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/diff2html/-/diff2html-2.4.0.tgz#de632384eefa5a7f6b0e92eafb1fa25d22dc88ab" + dependencies: + diff "^3.5.0" + hogan.js "^3.0.2" + lodash "^4.17.10" + whatwg-fetch "^2.0.4" + +diff@^3.2.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -3068,6 +3107,12 @@ fast-xml-parser@^3.12.0: dependencies: nimnjs "^1.3.2" +fault@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa" + dependencies: + format "^0.2.2" + fb-watchman@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" @@ -3280,6 +3325,10 @@ form-data@~2.3.1, form-data@~2.3.2: combined-stream "1.0.6" mime-types "^2.1.12" +format@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -3419,6 +3468,10 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gitdiff-parser@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/gitdiff-parser/-/gitdiff-parser-0.1.2.tgz#26a256e05e9c2d5016b512a96c1dacb40862b92a" + glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" @@ -3547,6 +3600,16 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + globule@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d" @@ -3569,6 +3632,12 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + dependencies: + delegate "^3.1.2" + got@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" @@ -3814,6 +3883,19 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hast-util-parse-selector@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.0.tgz#2175f18cdd697308fc3431d5c29a9e48dfa4817a" + +hastscript@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-4.1.0.tgz#ea5593fa6f6709101fc790ced818393ddaa045ce" + dependencies: + comma-separated-tokens "^1.0.0" + hast-util-parse-selector "^2.2.0" + property-information "^4.0.0" + space-separated-tokens "^1.0.0" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -3823,6 +3905,10 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +highlight.js@~9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" + history@^4.7.2: version "4.7.2" resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" @@ -3845,6 +3931,13 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hogan.js@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd" + dependencies: + mkdirp "0.3.0" + nopt "1.0.10" + hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" @@ -4115,6 +4208,17 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-alphabetical@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.2.tgz#1fa6e49213cb7885b75d15862fb3f3d96c884f41" + +is-alphanumerical@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz#1138e9ae5040158dc6ff76b820acd6b7a181fd40" + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4165,6 +4269,10 @@ is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" +is-decimal@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.2.tgz#894662d6a8709d307f3a276ca4339c8fa5dff0ff" + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -4251,6 +4359,10 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-hexadecimal@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835" + is-in-browser@^1.0.2, is-in-browser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" @@ -5222,6 +5334,10 @@ lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" +lodash.findlastindex@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.findlastindex/-/lodash.findlastindex-4.6.0.tgz#b8375ac0f02e9b926375cdf8dc3ea814abf9c6ac" + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -5254,6 +5370,10 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.mapvalues@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" + lodash.memoize@~3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" @@ -5320,6 +5440,13 @@ lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" +lowlight@~1.9.1: + version "1.9.2" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.9.2.tgz#0b9127e3cec2c3021b7795dd81005c709a42fdd1" + dependencies: + fault "^1.0.2" + highlight.js "~9.12.0" + lru-cache@2: version "2.7.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" @@ -5581,6 +5708,10 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" + "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -5820,6 +5951,12 @@ noms@0.0.0: inherits "^2.0.1" readable-stream "~1.0.31" +nopt@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + dependencies: + abbrev "1" + "nopt@2 || 3": version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -6175,6 +6312,17 @@ parse-asn1@^5.0.0: evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" +parse-entities@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.0.tgz#9deac087661b2e36814153cb78d7e54a4c5fd6f4" + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-filepath@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" @@ -6337,7 +6485,7 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" -pify@^2.0.0: +pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -6395,6 +6543,41 @@ posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" +postcss-easy-import@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-easy-import/-/postcss-easy-import-3.0.0.tgz#8eaaf5ae59566083d0cae98735dfd803e3ab194d" + dependencies: + globby "^6.1.0" + is-glob "^4.0.0" + lodash "^4.17.4" + object-assign "^4.0.1" + pify "^3.0.0" + postcss "^6.0.11" + postcss-import "^10.0.0" + resolve "^1.1.7" + +postcss-import@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-10.0.0.tgz#4c85c97b099136cc5ea0240dc1dfdbfde4e2ebbe" + dependencies: + object-assign "^4.0.1" + postcss "^6.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-value-parser@^3.2.3: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + +postcss@^6.0.1, postcss@^6.0.11: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -6422,6 +6605,12 @@ pretty-hrtime@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" +prismjs@^1.8.4, prismjs@~1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.15.0.tgz#8801d332e472091ba8def94976c8877ad60398d9" + optionalDependencies: + clipboard "^2.0.0" + private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -6456,6 +6645,12 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: loose-envify "^1.3.1" object-assign "^4.1.1" +property-information@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-4.2.0.tgz#f0e66e07cbd6fed31d96844d958d153ad3eb486e" + dependencies: + xtend "^4.0.1" + ps-tree@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" @@ -6573,7 +6768,19 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.4.2, react-dom@^16.5.2: +react-diff-view@^1.7.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/react-diff-view/-/react-diff-view-1.8.1.tgz#0b9b4adcb92de6730d28177d68654dfcc2097f73" + dependencies: + classnames "^2.2.6" + gitdiff-parser "^0.1.2" + leven "^2.1.0" + lodash.escape "^4.0.1" + lodash.findlastindex "^4.6.0" + lodash.mapvalues "^4.6.0" + warning "^4.0.1" + +react-dom@^16.4.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7" dependencies: @@ -6653,6 +6860,16 @@ react-router@^4.2.0, react-router@^4.3.1: prop-types "^15.6.1" warning "^4.0.1" +react-syntax-highlighter@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-9.0.1.tgz#cad91692e1976f68290f24762ac3451b1fec3d26" + dependencies: + babel-runtime "^6.18.0" + highlight.js "~9.12.0" + lowlight "~1.9.1" + prismjs "^1.8.4" + refractor "^2.4.1" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.4.1: version "16.5.2" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.5.2.tgz#92e9d2c6f763b9821b2e0b22f994ee675068b5ae" @@ -6662,7 +6879,7 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.4.1: react-is "^16.5.2" schedule "^0.5.0" -react@^16.4.2, react@^16.5.2: +react@^16.4.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" dependencies: @@ -6671,6 +6888,12 @@ react@^16.4.2, react@^16.5.2: prop-types "^15.6.2" schedule "^0.5.0" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + dependencies: + pify "^2.3.0" + read-only-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" @@ -6811,6 +7034,14 @@ redux@^4.0.0: loose-envify "^1.1.0" symbol-observable "^1.2.0" +refractor@^2.4.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.6.0.tgz#6b0d88f654c8534eefed3329a35bc7bb74ae0979" + dependencies: + hastscript "^4.0.0" + parse-entities "^1.1.2" + prismjs "~1.15.0" + regenerate-unicode-properties@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" @@ -7172,6 +7403,10 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + "semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" @@ -7440,6 +7675,12 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" +space-separated-tokens@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412" + dependencies: + trim "0.0.1" + sparkles@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" @@ -7700,7 +7941,7 @@ supports-color@^3.1.2: dependencies: has-flag "^1.0.0" -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.4.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" dependencies: @@ -7834,6 +8075,10 @@ timers-ext@^0.1.5: es5-ext "~0.10.46" next-tick "1" +tiny-emitter@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -7917,6 +8162,10 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + "true-case-path@^1.0.2": version "1.0.3" resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" @@ -8277,6 +8526,10 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: dependencies: iconv-lite "0.4.24" +whatwg-fetch@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" + whatwg-mimetype@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171" diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 644a1bc0f9..4dfb749690 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -103,7 +103,6 @@ <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> - <scope>test</scope> </dependency> <!-- rest api --> diff --git a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java index 7979eca0e7..fb5cd9f5cc 100644 --- a/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/ManagerDaoAdapter.java @@ -30,19 +30,19 @@ public class ManagerDaoAdapter<T extends ModelObject> { afterUpdate.handle(notModified); } else { - throw new NotFoundException(); + throw new NotFoundException(object.getClass(), object.getId()); } } - public T create(T newObject, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeCreate, AroundHandler<T> afterCreate) throws AlreadyExistsException { + public T create(T newObject, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeCreate, AroundHandler<T> afterCreate) { return create(newObject, permissionCheck, beforeCreate, afterCreate, dao::contains); } - public T create(T newObject, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeCreate, AroundHandler<T> afterCreate, Predicate<T> existsCheck) throws AlreadyExistsException { + public T create(T newObject, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeCreate, AroundHandler<T> afterCreate, Predicate<T> existsCheck) { permissionCheck.get().check(); AssertUtil.assertIsValid(newObject); if (existsCheck.test(newObject)) { - throw new AlreadyExistsException(); + throw new AlreadyExistsException(newObject); } newObject.setCreationDate(System.currentTimeMillis()); beforeCreate.handle(newObject); @@ -58,7 +58,7 @@ public class ManagerDaoAdapter<T extends ModelObject> { dao.delete(toDelete); afterDelete.handle(toDelete); } else { - throw new NotFoundException(); + throw new NotFoundException(toDelete.getClass(), toDelete.getId()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java b/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java new file mode 100644 index 0000000000..a85c3b6d06 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/ResteasyModule.java @@ -0,0 +1,20 @@ +package sonia.scm; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.servlet.ServletModule; +import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher; +import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters; + +import javax.inject.Singleton; +import java.util.Map; + +public class ResteasyModule extends ServletModule { + + @Override + protected void configureServlets() { + bind(HttpServletDispatcher.class).in(Singleton.class); + + Map<String, String> initParams = ImmutableMap.of(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX, "/api"); + serve("/api/*").with(HttpServletDispatcher.class, initParams); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 5a087a1c70..8ae005e826 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -126,6 +126,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList ClassOverrides overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader()); List<Module> moduleList = Lists.newArrayList(); + moduleList.add(new ResteasyModule()); moduleList.add(new ScmInitializerModule()); moduleList.add(new ScmEventBusModule()); moduleList.add(new EagerSingletonModule()); diff --git a/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java b/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java index 6652975c4a..0888a45dc2 100644 --- a/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java +++ b/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java @@ -57,5 +57,9 @@ public class TemplatingPushStateDispatcher implements PushStateDispatcher { return request.getContextPath(); } + public String getLiveReloadURL() { + return System.getProperty("livereload.url"); + } + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/FallbackExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/FallbackExceptionMapper.java new file mode 100644 index 0000000000..feb5341e2d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/FallbackExceptionMapper.java @@ -0,0 +1,43 @@ +package sonia.scm.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import sonia.scm.api.v2.resources.ErrorDto; +import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.Collections; + +@Provider +public class FallbackExceptionMapper implements ExceptionMapper<Exception> { + + private static final Logger logger = LoggerFactory.getLogger(FallbackExceptionMapper.class); + + private static final String ERROR_CODE = "CmR8GCJb31"; + + private final ExceptionWithContextToErrorDtoMapper mapper; + + @Inject + public FallbackExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) { + this.mapper = mapper; + } + + @Override + public Response toResponse(Exception exception) { + logger.debug("map {} to status code 500", exception); + ErrorDto errorDto = new ErrorDto(); + errorDto.setMessage("internal server error"); + errorDto.setContext(Collections.emptyList()); + errorDto.setErrorCode(ERROR_CODE); + errorDto.setTransactionId(MDC.get("transaction_id")); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorDto) + .type(VndMediaType.ERROR_TYPE) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java index 13d341e1e6..65e4426fde 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java @@ -1,18 +1,16 @@ package sonia.scm.api.rest; import sonia.scm.AlreadyExistsException; +import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper; -import javax.ws.rs.core.Response; +import javax.inject.Inject; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider -public class AlreadyExistsExceptionMapper implements ExceptionMapper<AlreadyExistsException> { - @Override - public Response toResponse(AlreadyExistsException exception) { - return Response.status(Status.CONFLICT) - .entity(exception.getMessage()) - .build(); +public class AlreadyExistsExceptionMapper extends ContextualExceptionMapper<AlreadyExistsException> { + @Inject + public AlreadyExistsExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) { + super(AlreadyExistsException.class, Status.CONFLICT, mapper); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/AuthenticationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/AuthenticationExceptionMapper.java new file mode 100644 index 0000000000..6ba9984190 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/AuthenticationExceptionMapper.java @@ -0,0 +1,13 @@ +package sonia.scm.api.rest; + +import org.apache.shiro.authc.AuthenticationException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +@Provider +public class AuthenticationExceptionMapper extends StatusExceptionMapper<AuthenticationException> { + public AuthenticationExceptionMapper() { + super(AuthenticationException.class, Response.Status.UNAUTHORIZED); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/ConcurrentModificationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/ConcurrentModificationExceptionMapper.java index d01b2fd7de..edcb48c91b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/ConcurrentModificationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/ConcurrentModificationExceptionMapper.java @@ -1,15 +1,16 @@ package sonia.scm.api.rest; import sonia.scm.ConcurrentModificationException; +import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper; +import javax.inject.Inject; import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider -public class ConcurrentModificationExceptionMapper implements ExceptionMapper<ConcurrentModificationException> { - @Override - public Response toResponse(ConcurrentModificationException exception) { - return Response.status(Response.Status.CONFLICT).build(); +public class ConcurrentModificationExceptionMapper extends ContextualExceptionMapper<ConcurrentModificationException> { + @Inject + public ConcurrentModificationExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) { + super(ConcurrentModificationException.class, Response.Status.CONFLICT, mapper); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/ContextualExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/ContextualExceptionMapper.java new file mode 100644 index 0000000000..9584a1ea2c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/ContextualExceptionMapper.java @@ -0,0 +1,39 @@ +package sonia.scm.api.rest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ExceptionWithContext; +import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +public class ContextualExceptionMapper<E extends ExceptionWithContext> implements ExceptionMapper<E> { + + private static final Logger logger = LoggerFactory.getLogger(ContextualExceptionMapper.class); + + private final ExceptionWithContextToErrorDtoMapper mapper; + + private final Response.Status status; + private final Class<E> type; + + public ContextualExceptionMapper(Class<E> type, Response.Status status, ExceptionWithContextToErrorDtoMapper mapper) { + this.mapper = mapper; + this.type = type; + this.status = status; + } + + @Override + public Response toResponse(E exception) { + if (logger.isTraceEnabled()) { + logger.trace("map {} to status code {}", type.getSimpleName(), status.getStatusCode(), exception); + } else { + logger.debug("map {} to status code {} with message '{}'", type.getSimpleName(), status.getStatusCode(), exception.getMessage()); + } + return Response.status(status) + .entity(mapper.map(exception)) + .type(VndMediaType.ERROR_TYPE) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java deleted file mode 100644 index ff29a770cf..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java +++ /dev/null @@ -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.api.rest; - -//~--- JDK imports ------------------------------------------------------------ - -import lombok.extern.slf4j.Slf4j; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * - * @author Sebastian Sdorra - * @since 1.36 - */ -@Provider -@Slf4j -public class IllegalArgumentExceptionMapper - implements ExceptionMapper<IllegalArgumentException> -{ - - /** - * Method description - * - * - * @param exception - * - * @return - */ - @Override - public Response toResponse(IllegalArgumentException exception) - { - log.info("caught IllegalArgumentException -- mapping to bad request", exception); - return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build(); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/NotAuthorizedExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/NotAuthorizedExceptionMapper.java new file mode 100644 index 0000000000..fee3803de7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/NotAuthorizedExceptionMapper.java @@ -0,0 +1,13 @@ +package sonia.scm.api.rest; + +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +@Provider +public class NotAuthorizedExceptionMapper extends StatusExceptionMapper<NotAuthorizedException> { + public NotAuthorizedExceptionMapper() + { + super(NotAuthorizedException.class, Response.Status.UNAUTHORIZED); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java index 2c78df627b..d2ce744c19 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java @@ -2,8 +2,6 @@ package sonia.scm.api.rest.resources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.repository.PathNotFoundException; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.api.CatCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.util.IOUtil; @@ -34,18 +32,6 @@ public class BrowserStreamingOutput implements StreamingOutput { public void write(OutputStream output) throws IOException { try { builder.retriveContent(output, path); - } catch (PathNotFoundException ex) { - if (logger.isWarnEnabled()) { - logger.warn("could not find path {}", ex.getPath()); - } - - throw new WebApplicationException(Response.Status.NOT_FOUND); - } catch (RevisionNotFoundException ex) { - if (logger.isWarnEnabled()) { - logger.warn("could not find revision {}", ex.getRevision()); - } - - throw new WebApplicationException(Response.Status.NOT_FOUND); } finally { IOUtil.close(repositoryService); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java index 6592bfab8b..828cbf8164 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java @@ -44,8 +44,6 @@ import org.apache.shiro.authc.credential.PasswordService; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.ConcurrentModificationException; -import sonia.scm.NotFoundException; import sonia.scm.api.rest.RestActionResult; import sonia.scm.security.Role; import sonia.scm.security.ScmSecurityException; diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ConfigurationResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ConfigurationResource.java deleted file mode 100644 index a9beea7679..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ConfigurationResource.java +++ /dev/null @@ -1,148 +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 org.apache.shiro.SecurityUtils; -import org.apache.shiro.subject.Subject; - -import sonia.scm.config.ScmConfiguration; -import sonia.scm.security.Role; -import sonia.scm.security.ScmSecurityException; -import sonia.scm.util.ScmConfigurationUtil; - -//~--- 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") -public class ConfigurationResource -{ - - /** - * Constructs ... - * - * - * @param configuration - * @param securityContextProvider - */ - @Inject - public ConfigurationResource(ScmConfiguration configuration) - { - this.configuration = configuration; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @GET - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response getConfiguration() - { - Response response = null; - - if (SecurityUtils.getSubject().hasRole(Role.ADMIN)) - { - response = Response.ok(configuration).build(); - } - else - { - response = Response.status(Response.Status.FORBIDDEN).build(); - } - - return response; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param uriInfo - * @param newConfig - * - * @return - */ - @POST - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response setConfig(@Context UriInfo uriInfo, - ScmConfiguration newConfig) - { - - // TODO replace by checkRole - Subject subject = SecurityUtils.getSubject(); - - if (!subject.hasRole(Role.ADMIN)) - { - throw new ScmSecurityException("admin privileges required"); - } - - configuration.load(newConfig); - - synchronized (ScmConfiguration.class) - { - ScmConfigurationUtil.getInstance().store(configuration); - } - - return Response.created(uriInfo.getRequestUri()).build(); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - public ScmConfiguration configuration; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java index db29725917..d177e05a5e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java @@ -37,7 +37,6 @@ package sonia.scm.api.rest.resources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.util.IOUtil; @@ -93,24 +92,8 @@ public class DiffStreamingOutput implements StreamingOutput public void write(OutputStream output) throws IOException { try { - builder.retriveContent(output); + builder.retrieveContent(output); } - catch (RevisionNotFoundException ex) - { - if (logger.isWarnEnabled()) - { - logger.warn("could not find revision {}", ex.getRevision()); - } - - throw new WebApplicationException(Response.Status.NOT_FOUND); - } -// catch (RepositoryException ex) -// { -// logger.error("could not write content to page", ex); -// -// throw new WebApplicationException(ex, -// Response.Status.INTERNAL_SERVER_ERROR); -// } finally { IOUtil.close(repositoryService); diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java deleted file mode 100644 index a560cc278e..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java +++ /dev/null @@ -1,248 +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 com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; -import org.apache.shiro.SecurityUtils; -import sonia.scm.group.Group; -import sonia.scm.group.GroupManager; -import sonia.scm.security.Role; - -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; -import java.util.Collection; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * RESTful Web Service Resource to manage groups and their members. - * - * @author Sebastian Sdorra - */ -@Path("groups") -@Singleton -public class GroupResource extends AbstractManagerResource<Group> { - - /** Field description */ - public static final String PATH_PART = "groups"; - - //~--- constructors --------------------------------------------------------- - - @Inject - public GroupResource(GroupManager groupManager) - { - super(groupManager, Group.class); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Creates a new group. <strong>Note:</strong> This method requires admin privileges. - * - * @param uriInfo current uri informations - * @param group the group to be created - * - * @return - */ - @POST - @StatusCodes({ - @ResponseCode(code = 201, condition = "create success", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the created group") - }), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response create(@Context UriInfo uriInfo, Group group) - { - return super.create(uriInfo, group); - } - - /** - * Deletes a group. <strong>Note:</strong> This method requires admin privileges. - * - * @param name the name of the group to delete. - * - * @return - */ - @DELETE - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Override - public Response delete(@PathParam("id") String name) - { - return super.delete(name); - } - - /** - * Modifies the given group. <strong>Note:</strong> This method requires admin privileges. - * - * @param name name of the group to be modified - * @param group group object to modify - * - * @return - */ - @PUT - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response update(@PathParam("id") String name, Group group) - { - return super.update(name, group); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Fetches a group by its name or id. <strong>Note:</strong> This method requires admin privileges. - * - * @param request the current request - * @param id the id/name of the group - * - * @return the {@link Group} with the specified id - */ - @GET - @Path("{id}") - @TypeHint(Group.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response get(@Context Request request, @PathParam("id") String id) - { - Response response = null; - - if (SecurityUtils.getSubject().hasRole(Role.ADMIN)) - { - response = super.get(request, id); - } - else - { - response = Response.status(Response.Status.FORBIDDEN).build(); - } - - return response; - } - - /** - * Returns all groups. <strong>Note:</strong> This method requires admin privileges. - * - * @param request the current request - * @param start the start value for paging - * @param limit the limit value for paging - * @param sortby sort parameter - * @param desc sort direction desc or aesc - * - * @return - */ - @GET - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @TypeHint(Group[].class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Override - public Response getAll(@Context Request request, @DefaultValue("0") - @QueryParam("start") int start, @DefaultValue("-1") - @QueryParam("limit") int limit, @QueryParam("sortby") String sortby, - @DefaultValue("false") - @QueryParam("desc") boolean desc) - { - return super.getAll(request, start, limit, sortby, desc); - } - - //~--- methods -------------------------------------------------------------- - - @Override - protected GenericEntity<Collection<Group>> createGenericEntity( - Collection<Group> items) - { - return new GenericEntity<Collection<Group>>(items) {} - ; - } - - //~--- get methods ---------------------------------------------------------- - - @Override - protected String getId(Group group) - { - return group.getName(); - } - - @Override - protected String getPathPart() - { - return PATH_PART; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java deleted file mode 100644 index 4a770206eb..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java +++ /dev/null @@ -1,362 +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.common.collect.Lists; -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.api.rest.RestActionResult; -import sonia.scm.api.rest.RestActionUploadResult; -import sonia.scm.plugin.OverviewPluginPredicate; -import sonia.scm.plugin.PluginConditionFailedException; -import sonia.scm.plugin.PluginInformation; -import sonia.scm.plugin.PluginInformationComparator; -import sonia.scm.plugin.PluginManager; - -//~--- JDK imports ------------------------------------------------------------ - -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; - -import java.io.IOException; -import java.io.InputStream; - -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import javax.ws.rs.Consumes; -import javax.ws.rs.FormParam; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; - -/** - * RESTful Web Service Endpoint to manage plugins. - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("plugins") -public class PluginResource -{ - - /** - * the logger for PluginResource - */ - private static final Logger logger = - LoggerFactory.getLogger(PluginResource.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param pluginManager - */ - @Inject - public PluginResource(PluginManager pluginManager) - { - this.pluginManager = pluginManager; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Installs a plugin from a package. - * - * @param uploadedInputStream - * - * @return - * - * @throws IOException - */ - @POST - @Path("install-package") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 412, condition = "precondition failed"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response install( - /*@FormParam("package")*/ InputStream uploadedInputStream) - throws IOException - { - Response response = null; - - try - { - pluginManager.installPackage(uploadedInputStream); - response = Response.ok(new RestActionUploadResult(true)).build(); - } - catch (PluginConditionFailedException ex) - { - logger.warn( - "could not install plugin package, because the condition failed", ex); - response = Response.status(Status.PRECONDITION_FAILED).entity( - new RestActionResult(false)).build(); - } - catch (Exception ex) - { - logger.warn("plugin installation failed", ex); - response = - Response.serverError().entity(new RestActionResult(false)).build(); - } - - return response; - } - - /** - * Installs a plugin. - * - * @param id id of the plugin to be installed - * - * @return - */ - @POST - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Path("install/{id}") - public Response install(@PathParam("id") String id) - { - pluginManager.install(id); - - // TODO should return 204 no content - return Response.ok().build(); - } - - /** - * Installs a plugin from a package. This method is a workaround for ExtJS - * file upload, which requires text/html as content-type. - * - * @param uploadedInputStream - * @return - * - * @throws IOException - */ - @POST - @Path("install-package.html") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 412, condition = "precondition failed"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Produces(MediaType.TEXT_HTML) - public Response installFromUI( - /*@FormParam("package")*/ InputStream uploadedInputStream) - throws IOException - { - return install(uploadedInputStream); - } - - /** - * Uninstalls a plugin. - * - * @param id id of the plugin to be uninstalled - * - * @return - */ - @POST - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Path("uninstall/{id}") - public Response uninstall(@PathParam("id") String id) - { - pluginManager.uninstall(id); - - // TODO should return 204 content - // consider to do a uninstall with a delete - return Response.ok().build(); - } - - /** - * Updates a plugin. - * - * @param id id of the plugin to be updated - * - * @return - */ - @POST - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Path("update/{id}") - public Response update(@PathParam("id") String id) - { - pluginManager.update(id); - - // TODO should return 204 content - // consider to do an update with a put - - return Response.ok().build(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns all plugins. - * - * @return all plugins - */ - @GET - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Collection<PluginInformation> getAll() - { - return pluginManager.getAll(); - } - - /** - * Returns all available plugins. - * - * @return all available plugins - */ - @GET - @Path("available") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Collection<PluginInformation> getAvailable() - { - return pluginManager.getAvailable(); - } - - /** - * Returns all plugins which are available for update. - * - * @return all plugins which are available for update - */ - @GET - @Path("updates") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Collection<PluginInformation> getAvailableUpdates() - { - return pluginManager.getAvailableUpdates(); - } - - /** - * Returns all installed plugins. - * - * @return all installed plugins - */ - @GET - @Path("installed") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - public Collection<PluginInformation> getInstalled() - { - return pluginManager.getInstalled(); - } - - /** - * Returns all plugins for the overview. - * - * @return all plugins for the overview - */ - @GET - @Path("overview") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Collection<PluginInformation> getOverview() - { - //J- - List<PluginInformation> plugins = Lists.newArrayList( - pluginManager.get(OverviewPluginPredicate.INSTANCE) - ); - //J+ - - Collections.sort(plugins, PluginInformationComparator.INSTANCE); - - Iterator<PluginInformation> it = plugins.iterator(); - String last = null; - - while (it.hasNext()) - { - PluginInformation pi = it.next(); - String id = pi.getId(false); - - if ((last != null) && id.equals(last)) - { - it.remove(); - } - - last = id; - } - - return plugins; - } - - //~--- fields --------------------------------------------------------------- - - /** plugin manager */ - private final PluginManager pluginManager; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java index 8a5ba8b1e3..6bf6c8e803 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java @@ -45,11 +45,11 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.AlreadyExistsException; import sonia.scm.NotFoundException; -import sonia.scm.NotSupportedFeatuerException; +import sonia.scm.NotSupportedFeatureException; import sonia.scm.Type; import sonia.scm.api.rest.RestActionUploadResult; +import sonia.scm.api.v2.resources.RepositoryResource; import sonia.scm.repository.*; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; @@ -394,7 +394,7 @@ public class RepositoryImportResource response = Response.ok(result).build(); } - catch (NotSupportedFeatuerException ex) + catch (NotSupportedFeatureException ex) { logger .warn( @@ -515,13 +515,6 @@ public class RepositoryImportResource // repository = new Repository(null, type, name); manager.create(repository); } - catch (AlreadyExistsException ex) - { - logger.warn("a {} repository with the name {} already exists", type, - name); - - throw new WebApplicationException(Response.Status.CONFLICT); - } catch (InternalRepositoryException ex) { handleGenericCreationFailure(ex, type, name); @@ -616,7 +609,7 @@ public class RepositoryImportResource types.add(t); } } - catch (NotSupportedFeatuerException ex) + catch (NotSupportedFeatureException ex) { if (logger.isTraceEnabled()) { @@ -718,7 +711,7 @@ public class RepositoryImportResource } } } - catch (NotSupportedFeatuerException ex) + catch (NotSupportedFeatureException ex) { throw new WebApplicationException(ex, Response.Status.BAD_REQUEST); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java deleted file mode 100644 index 4a9fd9e38f..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java +++ /dev/null @@ -1,1067 +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.common.base.Strings; -import com.google.inject.Inject; -import com.google.inject.Singleton; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.authz.AuthorizationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.NotFoundException; -import sonia.scm.repository.BlameResult; -import sonia.scm.repository.Branches; -import sonia.scm.repository.BrowserResult; -import sonia.scm.repository.Changeset; -import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.HealthChecker; -import sonia.scm.repository.Permission; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryIsNotArchivedException; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.Tags; -import sonia.scm.repository.api.BlameCommandBuilder; -import sonia.scm.repository.api.BrowseCommandBuilder; -import sonia.scm.repository.api.CatCommandBuilder; -import sonia.scm.repository.api.CommandNotSupportedException; -import sonia.scm.repository.api.DiffCommandBuilder; -import sonia.scm.repository.api.DiffFormat; -import sonia.scm.repository.api.LogCommandBuilder; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.repository.api.RepositoryServiceFactory; -import sonia.scm.util.AssertUtil; -import sonia.scm.util.HttpUtil; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; - -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.StreamingOutput; -import javax.ws.rs.core.UriInfo; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * Repository related RESTful Web Service Endpoint. - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("repositories") -public class RepositoryResource extends AbstractManagerResource<Repository> -{ - - /** Field description */ - public static final String PATH_PART = "repositories"; - - /** the logger for RepositoryResource */ - private static final Logger logger = - LoggerFactory.getLogger(RepositoryResource.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * @param repositoryManager - * @param servicefactory - * @param healthChecker - */ - @Inject - public RepositoryResource(RepositoryManager repositoryManager, - RepositoryServiceFactory servicefactory, HealthChecker healthChecker) - { - super(repositoryManager, Repository.class); - this.repositoryManager = repositoryManager; - this.servicefactory = servicefactory; - this.healthChecker = healthChecker; - setDisableCache(false); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Creates a new repository.<strong>Note:</strong> This method requires admin privileges. - * - * @param uriInfo current uri informations - * @param repository the repository to be created - * - * @return empty response with location header to the new repository - */ - @POST - @StatusCodes({ - @ResponseCode(code = 201, condition = "success", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the new created repository") - }), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response create(@Context UriInfo uriInfo, Repository repository) - { - return super.create(uriInfo, repository); - } - - /** - * Deletes a repository. <strong>Note:</strong> This method requires owner privileges. - * - * @param id the id of the repository to delete. - * - * @return - */ - @DELETE - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"), - @ResponseCode(code = 404, condition = "could not find repository"), - @ResponseCode( - code = 412, - condition = "precondition failed, the repository is not archived, this error occurs only with enabled repository archive" - ), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Override - public Response delete(@PathParam("id") String id) - { - Response response; - Repository repository = manager.get(id); - - if (repository != null) - { - preDelete(repository); - - try - { - manager.delete(repository); - response = Response.noContent().build(); - } - catch (RepositoryIsNotArchivedException ex) - { - logger.warn("non archived repository could not be deleted", ex); - response = Response.status(Response.Status.PRECONDITION_FAILED).build(); - } - catch (AuthorizationException ex) - { - logger.warn("delete not allowed", ex); - response = Response.status(Response.Status.FORBIDDEN).build(); - } catch (NotFoundException e) { - // there is nothing to do because delete should be idempotent - response = Response.ok().build(); - } -// catch (IOException ex) -// { -// logger.error("error during delete", ex); -// response = Response.serverError().build(); -// } - } - else - { - logger.warn("could not find repository {}", id); - response = Response.status(Status.NOT_FOUND).build(); - } - - return response; - } - - /** - * Re run repository health checks. - * - * @param id id of the repository - * - * @return - */ - @POST - @StatusCodes({ - @ResponseCode(code = 200, condition = "re run success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"), - @ResponseCode(code = 404, condition = "could not find repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Path("{id}/healthcheck") - public Response runHealthChecks(@PathParam("id") String id) - { - Response response; - - try - { - healthChecker.check(id); - // TODO should return 204 instead of 200 - response = Response.ok().build(); - } - catch (RepositoryNotFoundException ex) - { - logger.warn("could not find repository ".concat(id), ex); - response = Response.status(Status.NOT_FOUND).build(); - } catch (NotFoundException e) { - logger.error("error occured during health check", e); - response = Response.serverError().build(); - } - - return response; - } - - /** - * Modifies the given repository. <strong>Note:</strong> This method requires owner privileges. - * - * @param id id of the repository to be modified - * @param repository repository object to modify - * - * @return - */ - @PUT - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update successful"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"), - @ResponseCode(code = 404, condition = "could not find repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response update(@PathParam("id") String id, Repository repository) - { - return super.update(id, repository); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns the {@link Repository} with the specified id. - * - * @param request the current request - * @param id the id/name of the user - * - * @return the {@link Repository} with the specified id - */ - @GET - @Path("{id}") - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 404, condition = "not found, no repository with the specified id available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Repository.class) - @Override - public Response get(@Context Request request, @PathParam("id") String id) - { - return super.get(request, id); - } - - /** - * Returns all repositories. - * - * @param request the current request - * @param start the start value for paging - * @param limit the limit value for paging - * @param sortby sort parameter - * @param desc sort direction desc or aesc - * - * @return all repositories - */ - @GET - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Repository[].class) - @Override - public Response getAll(@Context Request request, @DefaultValue("0") - @QueryParam("start") int start, @DefaultValue("-1") - @QueryParam("limit") int limit, @QueryParam("sortby") String sortby, - @DefaultValue("false") - @QueryParam("desc") boolean desc) - { - return super.getAll(request, start, limit, sortby, desc); - } - - /** - * Returns a annotate/blame view for the given path. - * - * @param id the id of the repository - * @param revision the revision of the file - * @param path the path of the file - * - * @return a annotate/blame view for the given path - * - * @throws IOException - */ - @GET - @Path("{id}/blame") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the blame feature is not supported by this type of repositories."), - @ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(BlameResult.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response getBlame(@PathParam("id") String id, - @QueryParam("revision") String revision, @QueryParam("path") String path) - throws IOException - { - Response response = null; - RepositoryService service = null; - - try - { - AssertUtil.assertIsNotNull(path); - service = servicefactory.create(id); - - BlameCommandBuilder builder = service.getBlameCommand(); - - if (!Strings.isNullOrEmpty(revision)) - { - builder.setRevision(revision); - } - - BlameResult blamePagingResult = builder.getBlameResult(path); - - if (blamePagingResult != null) - { - response = Response.ok(blamePagingResult).build(); - } - else - { - response = Response.ok().build(); - } - } - catch (IllegalStateException ex) - { - response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } - catch (RepositoryNotFoundException ex) - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - catch (CommandNotSupportedException ex) - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - finally - { - IOUtil.close(service); - } - - return response; - } - - /** - * Returns all {@link Branches} of a repository. - * - * @param id the id of the repository - * - * @return all {@link Branches} of a repository - * - * @throws IOException - * - * @since 1.18 - */ - @GET - @Path("{id}/branches") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the branch feature is not supported by this type of repositories."), - @ResponseCode(code = 404, condition = "not found, the repository could not be found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Branches.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response getBranches(@PathParam("id") String id) - throws IOException - { - Response response = null; - RepositoryService service = null; - - try - { - service = servicefactory.create(id); - - Branches branches = service.getBranchesCommand().getBranches(); - - if (branches != null) - { - response = Response.ok(branches).build(); - } - else - { - response = Response.status(Status.NOT_FOUND).build(); - } - } - catch (RepositoryNotFoundException ex) - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - catch (CommandNotSupportedException ex) - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - finally - { - IOUtil.close(service); - } - - return response; - } - - /** - * Returns a list of folders and files for the given folder. - * - * @param id the id of the repository - * @param revision the revision of the file - * @param path the path of the folder - * @param disableLastCommit true disables fetch of last commit message - * @param disableSubRepositoryDetection true disables sub repository detection - * @param recursive true to enable recursive browsing - * - * @return a list of folders and files for the given folder - * - * @throws IOException - */ - @GET - @Path("{id}/browse") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the browse feature is not supported by this type of repositories."), - @ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(BrowserResult.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - //J- - public Response getBrowserResult( - @PathParam("id") String id, - @QueryParam("revision") String revision, - @QueryParam("path") String path, - @QueryParam("disableLastCommit") @DefaultValue("false") boolean disableLastCommit, - @QueryParam("disableSubRepositoryDetection") @DefaultValue("false") boolean disableSubRepositoryDetection, - @QueryParam("recursive") @DefaultValue("false") boolean recursive) - throws IOException - //J+ - { - Response response = null; - RepositoryService service = null; - - try - { - service = servicefactory.create(id); - - BrowseCommandBuilder builder = service.getBrowseCommand(); - - if (!Strings.isNullOrEmpty(revision)) - { - builder.setRevision(revision); - } - - if (!Strings.isNullOrEmpty(path)) - { - builder.setPath(path); - } - - //J- - builder.setDisableLastCommit(disableLastCommit) - .setDisableSubRepositoryDetection(disableSubRepositoryDetection) - .setRecursive(recursive); - //J+ - - BrowserResult result = builder.getBrowserResult(); - - if (result != null) - { - response = Response.ok(result).build(); - } - else - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - } - catch (NotFoundException ex) - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - catch (CommandNotSupportedException ex) - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - finally - { - IOUtil.close(service); - } - - return response; - } - - /** - * Returns the {@link Changeset} from the given repository - * with the specified revision. - * - * @param id the id of the repository - * @param revision the revision of the changeset - * - * @return a {@link Changeset} from the given repository - * - * @throws IOException - */ - @GET - @Path("{id}/changeset/{revision}") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the changeset feature is not supported by this type of repositories."), - @ResponseCode(code = 404, condition = "not found, the repository or the revision could not be found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Changeset.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response getChangeset(@PathParam("id") String id, - @PathParam("revision") String revision) - throws IOException - { - Response response = null; - - if (Util.isNotEmpty(id) && Util.isNotEmpty(revision)) - { - RepositoryService service = null; - - try - { - service = servicefactory.create(id); - - Changeset changeset = service.getLogCommand().getChangeset(revision); - - if (changeset != null) - { - response = Response.ok(changeset).build(); - } - else - { - response = Response.status(Status.NOT_FOUND).build(); - } - } - catch (NotFoundException ex) - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - catch (CommandNotSupportedException ex) - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - finally - { - IOUtil.close(service); - } - } - else - { - logger.warn("id or revision is empty"); - response = Response.status(Status.BAD_REQUEST).build(); - } - - return response; - } - - /** - * Returns a list of {@link Changeset} for the given repository. - * - * @param id the id of the repository - * @param path path of a file - * @param revision the revision of the file specified by the path parameter - * @param branch name of the branch - * @param start the start value for paging - * @param limit the limit value for paging - * - * @return a list of {@link Changeset} for the given repository - * - * @throws IOException - */ - @GET - @Path("{id}/changesets") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the changeset feature is not supported by this type of repositories."), - @ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(ChangesetPagingResult.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - //J- - public Response getChangesets( - @PathParam("id") String id, - @QueryParam("path") String path, - @QueryParam("revision") String revision, - @QueryParam("branch") String branch, - @DefaultValue("0") @QueryParam("start") int start, - @DefaultValue("20") @QueryParam("limit") int limit - ) throws IOException - //J+ - { - Response response = null; - RepositoryService service = null; - - try - { - ChangesetPagingResult changesets; - - service = servicefactory.create(id); - - LogCommandBuilder builder = service.getLogCommand(); - - if (!Strings.isNullOrEmpty(path)) - { - builder.setPath(path); - } - - if (!Strings.isNullOrEmpty(revision)) - { - builder.setStartChangeset(revision); - } - - if (!Strings.isNullOrEmpty(branch)) - { - builder.setBranch(branch); - } - - changesets = - builder.setPagingStart(start).setPagingLimit(limit).getChangesets(); - - if (changesets != null) - { - response = Response.ok(changesets).build(); - } - else - { - response = Response.ok().build(); - } - } - catch (NotFoundException ex) - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - catch (CommandNotSupportedException ex) - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - finally - { - IOUtil.close(service); - } - - return response; - } - - /** - * Returns the content of a file. - * - * @param id the id of the repository - * @param revision the revision of the file - * @param path path to the file - * - * @return the content of a file - */ - @GET - @Path("{id}/content") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the content feature is not supported by this type of repositories."), - @ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(StreamingOutput.class) - @Produces({ MediaType.APPLICATION_OCTET_STREAM }) - public Response getContent(@PathParam("id") String id, - @QueryParam("revision") String revision, @QueryParam("path") String path) - { - Response response; - StreamingOutput output; - RepositoryService service; - - try - { - service = servicefactory.create(id); - - CatCommandBuilder builder = service.getCatCommand(); - - if (!Strings.isNullOrEmpty(revision)) - { - builder.setRevision(revision); - } - - output = new BrowserStreamingOutput(service, builder, path); - - /** - * protection for crlf injection - * see https://bitbucket.org/sdorra/scm-manager/issue/320/crlf-injection-vulnerability-in-diff-api - */ - path = HttpUtil.removeCRLFInjectionChars(path); - - String contentDispositionName = getContentDispositionNameFromPath(path); - - response = Response.ok(output).header("Content-Disposition", - contentDispositionName).build(); - } - catch (RepositoryNotFoundException ex) - { - logger.warn("could not find repository browser for respository {}", id); - response = Response.status(Response.Status.NOT_FOUND).build(); - } - catch (CommandNotSupportedException ex) - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - catch (Exception ex) - { - logger.error("could not retrive content", ex); - response = createErrorResponse(ex); - } - - return response; - } - - /** - * Returns the modifications of a {@link Changeset}. - * - * @param id the id of the repository - * @param revision the revision of the file - * @param path path to the file - * @param format - * - * @return the modifications of a {@link Changeset} - * - * @throws IOException - */ - @GET - @Path("{id}/diff") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the diff feature is not supported by this type of repositories."), - @ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(DiffStreamingOutput.class) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response getDiff(@PathParam("id") String id, - @QueryParam("revision") String revision, @QueryParam("path") String path, - @QueryParam("format") DiffFormat format) - throws IOException - { - AssertUtil.assertIsNotEmpty(id); - AssertUtil.assertIsNotEmpty(revision); - - /** - * check for a crlf injection attack - * see https://bitbucket.org/sdorra/scm-manager/issue/320/crlf-injection-vulnerability-in-diff-api - */ - HttpUtil.checkForCRLFInjection(revision); - - RepositoryService service; - Response response; - - try - { - service = servicefactory.create(id); - - DiffCommandBuilder builder = service.getDiffCommand(); - - if (!Strings.isNullOrEmpty(revision)) - { - builder.setRevision(revision); - } - - if (!Strings.isNullOrEmpty(path)) - { - builder.setPath(path); - } - - if (format != null) - { - builder.setFormat(format); - } - - String name = service.getRepository().getName().concat("-").concat( - revision).concat(".diff"); - String contentDispositionName = getContentDispositionName(name); - - response = Response.ok(new DiffStreamingOutput(service, - builder)).header("Content-Disposition", contentDispositionName).build(); - } - catch (RepositoryNotFoundException ex) - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - catch (CommandNotSupportedException ex) - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - catch (Exception ex) - { - logger.error("could not create diff", ex); - response = createErrorResponse(ex); - } - - return response; - } - - /** - * Returns all {@link Tags} of a repository. - * - * @param id the id of the repository - * - * @return all {@link Tags} of a repository - * - * @throws IOException - * - * @since 1.18 - */ - @GET - @Path("{id}/tags") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the tag feature is not supported by this type of repositories."), - @ResponseCode(code = 404, condition = "not found, the repository could not be found"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(Tags.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response getTags(@PathParam("id") String id) - throws IOException - { - Response response = null; - RepositoryService service = null; - - try - { - service = servicefactory.create(id); - - Tags tags = service.getTagsCommand().getTags(); - - if (tags != null) - { - response = Response.ok(tags).build(); - } - else - { - response = Response.status(Status.NOT_FOUND).build(); - } - } - catch (RepositoryNotFoundException ex) - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - catch (CommandNotSupportedException ex) - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - finally - { - IOUtil.close(service); - } - - return response; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param items - * - * @return - */ - @Override - protected GenericEntity<Collection<Repository>> createGenericEntity( - Collection<Repository> items) - { - return new GenericEntity<Collection<Repository>>(items) {} - ; - } - - /** - * Method description - * - * - * - * @param repositories - * @return - */ - @Override - protected Collection<Repository> prepareForReturn( - Collection<Repository> repositories) - { - for (Repository repository : repositories) - { - prepareForReturn(repository); - } - - return repositories; - } - - /** - * Method description - * - * - * @param repository - * - * @return - */ - @Override - protected Repository prepareForReturn(Repository repository) - { - if (SecurityUtils.getSubject().isPermitted( - "repository:modify:".concat(repository.getId()))) - { - if (repository.getPermissions() == null) - { - repository.setPermissions(new ArrayList<Permission>()); - } - } - else - { - logger.trace("remove properties and permissions from repository, " - + "because the user is not privileged"); - - repository.setProperties(null); - repository.setPermissions(null); - repository.setHealthCheckFailures(null); - } - - return repository; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param item - * - * @return - */ - @Override - protected String getId(Repository item) - { - return item.getId(); - } - - /** - * Method description - * - * - * @return - */ - @Override - protected String getPathPart() - { - return PATH_PART; - } - - /** - * Method description - * - * - * - * @param name - * - * @return - */ - private String getContentDispositionName(String name) - { - return HttpUtil.createContentDispositionAttachmentHeader(name); - } - - /** - * Method description - * - * - * @param path - * - * @return - */ - private String getContentDispositionNameFromPath(String path) - { - String name = path; - int index = path.lastIndexOf('/'); - - if (index >= 0) - { - name = path.substring(index + 1); - } - - return getContentDispositionName(name); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final HealthChecker healthChecker; - - /** Field description */ - private final RepositoryManager repositoryManager; - - /** Field description */ - private final RepositoryServiceFactory servicefactory; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserResource.java deleted file mode 100644 index e054c4c32f..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserResource.java +++ /dev/null @@ -1,319 +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 com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.authc.credential.PasswordService; -import sonia.scm.security.Role; -import sonia.scm.user.User; -import sonia.scm.user.UserManager; -import sonia.scm.util.AssertUtil; -import sonia.scm.util.Util; - -import javax.ws.rs.DELETE; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; -import java.util.Collection; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * RESTful Web Service Resource to manage users. - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("users") -public class UserResource extends AbstractManagerResource<User> -{ - - /** Field description */ - public static final String DUMMY_PASSWORT = "__dummypassword__"; - - /** Field description */ - public static final String PATH_PART = "users"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param userManager - * @param passwordService - */ - @Inject - public UserResource(UserManager userManager, PasswordService passwordService) - { - super(userManager, User.class); - this.passwordService = passwordService; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Creates a new user. <strong>Note:</strong> This method requires admin privileges. - * - * @param uriInfo current uri informations - * @param user the user to be created - * - * @return - */ - @POST - @StatusCodes({ - @ResponseCode(code = 201, condition = "create success", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to the created group") - }), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response create(@Context UriInfo uriInfo, User user) - { - return super.create(uriInfo, user); - } - - /** - * Deletes a user. <strong>Note:</strong> This method requires admin privileges. - * - * @param name the name of the user to delete. - * - * @return - */ - @DELETE - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Override - public Response delete(@PathParam("id") String name) - { - return super.delete(name); - } - - /** - * Modifies the given user. <strong>Note:</strong> This method requires admin privileges. - * - * @param name name of the user to be modified - * @param user user object to modify - * - * @return - */ - @PUT - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response update(@PathParam("id") String name, User user) - { - return super.update(name, user); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns a user. <strong>Note:</strong> This method requires admin privileges. - * - * @param request the current request - * @param id the id/name of the user - * - * @return the {@link User} with the specified id - */ - @GET - @Path("{id}") - @TypeHint(User.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response get(@Context Request request, @PathParam("id") String id) - { - Response response = null; - - if (SecurityUtils.getSubject().hasRole(Role.ADMIN)) - { - response = super.get(request, id); - } - else - { - response = Response.status(Response.Status.FORBIDDEN).build(); - } - - return response; - } - - /** - * Returns all users. <strong>Note:</strong> This method requires admin privileges. - * - * @param request the current request - * @param start the start value for paging - * @param limit the limit value for paging - * @param sortby sort parameter - * @param desc sort direction desc or aesc - * - * @return - */ - @GET - @TypeHint(User[].class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - @Override - public Response getAll(@Context Request request, @DefaultValue("0") - @QueryParam("start") int start, @DefaultValue("-1") - @QueryParam("limit") int limit, @QueryParam("sortby") String sortby, - @DefaultValue("false") - @QueryParam("desc") boolean desc) - { - return super.getAll(request, start, limit, sortby, desc); - } - - //~--- methods -------------------------------------------------------------- - - @Override - protected GenericEntity<Collection<User>> createGenericEntity( - Collection<User> items) - { - return new GenericEntity<Collection<User>>(items) {} - ; - } - - @Override - protected void preCreate(User user) - { - encryptPassword(user); - } - - @Override - protected void preUpdate(User user) - { - if (DUMMY_PASSWORT.equals(user.getPassword())) - { - User o = manager.get(user.getName()); - - AssertUtil.assertIsNotNull(o); - user.setPassword(o.getPassword()); - } - else - { - encryptPassword(user); - } - } - - @Override - protected Collection<User> prepareForReturn(Collection<User> users) - { - if (Util.isNotEmpty(users)) - { - for (User u : users) - { - u.setPassword(DUMMY_PASSWORT); - } - } - - return users; - } - - @Override - protected User prepareForReturn(User user) - { - user.setPassword(DUMMY_PASSWORT); - - return user; - } - - @Override - protected String getId(User user) - { - return user.getName(); - } - - @Override - protected String getPathPart() - { - return PATH_PART; - } - - private void encryptPassword(User user) - { - String password = user.getPassword(); - - if (Util.isNotEmpty(password)) - { - user.setPassword(passwordService.encryptPassword(password)); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private PasswordService passwordService; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/NotFoundExceptionMapper.java similarity index 79% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/NotFoundExceptionMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/NotFoundExceptionMapper.java index 04264a7629..944ec276ce 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NotFoundExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/NotFoundExceptionMapper.java @@ -28,12 +28,14 @@ */ -package sonia.scm.api.v2.resources; +package sonia.scm.api.v2; import sonia.scm.NotFoundException; -import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.api.rest.ContextualExceptionMapper; +import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper; +import javax.inject.Inject; import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; @@ -41,9 +43,9 @@ import javax.ws.rs.ext.Provider; * @since 2.0.0 */ @Provider -public class NotFoundExceptionMapper extends StatusExceptionMapper<NotFoundException> { - - public NotFoundExceptionMapper() { - super(NotFoundException.class, Response.Status.NOT_FOUND); +public class NotFoundExceptionMapper extends ContextualExceptionMapper<NotFoundException> { + @Inject + public NotFoundExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) { + super(NotFoundException.class, Response.Status.NOT_FOUND, mapper); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/NotSupportedFeatureExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/NotSupportedFeatureExceptionMapper.java new file mode 100644 index 0000000000..6a48663aa5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/NotSupportedFeatureExceptionMapper.java @@ -0,0 +1,17 @@ +package sonia.scm.api.v2; + +import sonia.scm.NotSupportedFeatureException; +import sonia.scm.api.rest.ContextualExceptionMapper; +import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper; + +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +@Provider +public class NotSupportedFeatureExceptionMapper extends ContextualExceptionMapper<NotSupportedFeatureException> { + @Inject + public NotSupportedFeatureExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) { + super(NotSupportedFeatureException.class, Response.Status.BAD_REQUEST, mapper); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java index c6af6b8921..6fadce8500 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java @@ -1,58 +1,30 @@ package sonia.scm.api.v2; -import lombok.Getter; import org.jboss.resteasy.api.validation.ResteasyViolationException; +import sonia.scm.api.v2.resources.ViolationExceptionToErrorDtoMapper; -import javax.validation.ConstraintViolation; +import javax.inject.Inject; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; -import java.util.List; -import java.util.stream.Collectors; @Provider public class ValidationExceptionMapper implements ExceptionMapper<ResteasyViolationException> { + private final ViolationExceptionToErrorDtoMapper mapper; + + @Inject + public ValidationExceptionMapper(ViolationExceptionToErrorDtoMapper mapper) { + this.mapper = mapper; + } + @Override public Response toResponse(ResteasyViolationException exception) { - - List<ConstraintViolationBean> violations = - exception.getConstraintViolations() - .stream() - .map(ConstraintViolationBean::new) - .collect(Collectors.toList()); - return Response .status(Response.Status.BAD_REQUEST) .type(MediaType.APPLICATION_JSON_TYPE) - .entity(new ValidationError(violations)) + .entity(mapper.map(exception)) .build(); } - - @Getter - public static class ValidationError { - @XmlElement(name = "violation") - @XmlElementWrapper(name = "violations") - private List<ConstraintViolationBean> violations; - - public ValidationError(List<ConstraintViolationBean> violations) { - this.violations = violations; - } - } - - @XmlRootElement(name = "violation") - @Getter - public static class ConstraintViolationBean { - private String path; - private String message; - - public ConstraintViolationBean(ConstraintViolation<?> violation) { - message = violation.getMessage(); - path = violation.getPropertyPath().toString(); - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java index ffe0ce51d0..47918080ab 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -18,6 +18,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @Path(AuthenticationResource.PATH) +@AllowAnonymousAccess public class AuthenticationResource { private static final Logger LOG = LoggerFactory.getLogger(AuthenticationResource.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index 5f57d1bcc8..a0d73ad24e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -5,12 +5,12 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.NotFoundException; import sonia.scm.PageResult; +import sonia.scm.repository.Branch; import sonia.scm.repository.Branches; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.CommandNotSupportedException; import sonia.scm.repository.api.RepositoryService; @@ -26,6 +26,10 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; +import java.util.List; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; public class BranchRootResource { @@ -77,7 +81,7 @@ public class BranchRootResource { .build(); } catch (CommandNotSupportedException ex) { return Response.status(Response.Status.BAD_REQUEST).build(); - } catch (RepositoryNotFoundException e) { + } catch (NotFoundException e) { return Response.status(Response.Status.NOT_FOUND).build(); } } @@ -97,7 +101,7 @@ public class BranchRootResource { @PathParam("name") String name, @PathParam("branch") String branchName, @DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception { + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { boolean branchExists = repositoryService.getBranchesCommand() .getBranches() @@ -105,7 +109,7 @@ public class BranchRootResource { .stream() .anyMatch(branch -> branchName.equals(branch.getName())); if (!branchExists){ - throw new NotFoundException("branch", branchName); + throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name)); } Repository repository = repositoryService.getRepository(); RepositoryPermissions.read(repository).check(); @@ -124,6 +128,49 @@ public class BranchRootResource { } } + @Path("{branch}/diffchangesets/{otherBranchName}") + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), + @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.CHANGESET_COLLECTION) + @TypeHint(CollectionDto.class) + public Response changesetDiff(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("branch") String branchName, + @PathParam("otherBranchName") String otherBranchName, + @DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + List<Branch> allBranches = repositoryService.getBranchesCommand().getBranches().getBranches(); + if (allBranches.stream().noneMatch(branch -> branchName.equals(branch.getName()))) { + throw new NotFoundException("branch", branchName); + } + if (allBranches.stream().noneMatch(branch -> otherBranchName.equals(branch.getName()))) { + throw new NotFoundException("branch", otherBranchName); + } + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService) + .page(page) + .pageSize(pageSize) + .create() + .setBranch(branchName) + .setAncestorChangeset(otherBranchName) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null) { + PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); + return Response.ok(branchChangesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository, branchName)).build(); + } else { + return Response.ok().build(); + } + } + } + /** * Returns the branches for a repository. * @@ -150,8 +197,6 @@ public class BranchRootResource { return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build(); } catch (CommandNotSupportedException ex) { return Response.status(Response.Status.BAD_REQUEST).build(); - } catch (RepositoryNotFoundException e) { - return Response.status(Response.Status.NOT_FOUND).build(); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java index 7ab3ef25a8..8167e28f9a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java @@ -28,6 +28,7 @@ public abstract class BranchToBranchDtoMapper { Links.Builder linksBuilder = linkingTo() .self(resourceLinks.branch().self(namespaceAndName, target.getName())) .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build()) + .single(linkBuilder("changesetDiff", resourceLinks.branch().changesetDiff(namespaceAndName, target.getName())).build()) .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()) .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()); target.add(linksBuilder.build()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangePasswordNotAllowedExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangePasswordNotAllowedExceptionMapper.java index e9bb5304a5..18a6e6e75c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangePasswordNotAllowedExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangePasswordNotAllowedExceptionMapper.java @@ -1,17 +1,17 @@ package sonia.scm.api.v2.resources; +import sonia.scm.api.rest.ContextualExceptionMapper; import sonia.scm.user.ChangePasswordNotAllowedException; +import sonia.scm.user.InvalidPasswordException; +import javax.inject.Inject; import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider -public class ChangePasswordNotAllowedExceptionMapper implements ExceptionMapper<ChangePasswordNotAllowedException> { - @Override - public Response toResponse(ChangePasswordNotAllowedException exception) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(exception.getMessage()) - .build(); +public class ChangePasswordNotAllowedExceptionMapper extends ContextualExceptionMapper<ChangePasswordNotAllowedException> { + @Inject + public ChangePasswordNotAllowedExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) { + super(ChangePasswordNotAllowedException.class, Response.Status.BAD_REQUEST, mapper); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java index 8900183f5b..0766816d4d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java @@ -9,10 +9,7 @@ import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryPermissions; -import sonia.scm.repository.RevisionNotFoundException; -import sonia.scm.repository.api.LogCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; @@ -26,6 +23,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; +import java.util.Optional; @Slf4j @@ -56,7 +54,7 @@ public class ChangesetRootResource { @Produces(VndMediaType.CHANGESET_COLLECTION) @TypeHint(CollectionDto.class) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException { + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Repository repository = repositoryService.getRepository(); RepositoryPermissions.read(repository).check(); @@ -89,7 +87,7 @@ public class ChangesetRootResource { @Produces(VndMediaType.CHANGESET) @TypeHint(ChangesetDto.class) @Path("{id}") - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException, RevisionNotFoundException, RepositoryNotFoundException { + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Repository repository = repositoryService.getRepository(); RepositoryPermissions.read(repository).check(); @@ -97,8 +95,12 @@ public class ChangesetRootResource { .setStartChangeset(id) .setEndChangeset(id) .getChangesets(); - if (changesets != null && changesets.getChangesets() != null && changesets.getChangesets().size() == 1) { - return Response.ok(changesetToChangesetDtoMapper.map(changesets.getChangesets().get(0), repository)).build(); + if (changesets != null && changesets.getChangesets() != null && !changesets.getChangesets().isEmpty()) { + Optional<Changeset> changeset = changesets.getChangesets().stream().filter(ch -> ch.getId().equals(id)).findFirst(); + if (!changeset.isPresent()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(changesetToChangesetDtoMapper.map(changeset.get(), repository)).build(); } else { return Response.status(Response.Status.NOT_FOUND).build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java index 3ef19f7294..052bf771b1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java @@ -1,7 +1,6 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; -import sonia.scm.AlreadyExistsException; import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.PageResult; @@ -47,7 +46,7 @@ class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject, * Creates a model object for the given dto and returns a corresponding http response. * This handles all corner cases, eg. no conflicts or missing privileges. */ - public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) throws AlreadyExistsException { + public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) { if (dto == null) { return Response.status(BAD_REQUEST).build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java index dc7c305823..9f99efdea2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java @@ -6,10 +6,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.PathNotFoundException; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.util.IOUtil; @@ -64,8 +62,8 @@ public class ContentResource { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Response.ResponseBuilder responseBuilder = Response.ok(stream); return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder); - } catch (RepositoryNotFoundException e) { - LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); + } catch (NotFoundException e) { + LOG.debug(e.getMessage()); return Response.status(Status.NOT_FOUND).build(); } } @@ -75,14 +73,8 @@ public class ContentResource { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path); os.close(); - } catch (RepositoryNotFoundException e) { - LOG.debug("repository {}/{} not found", path, namespace, name, e); - throw new WebApplicationException(Status.NOT_FOUND); - } catch (PathNotFoundException e) { - LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); - throw new WebApplicationException(Status.NOT_FOUND); - } catch (RevisionNotFoundException e) { - LOG.debug("revision '{}' not found in repository {}/{}", revision, namespace, name, e); + } catch (NotFoundException e) { + LOG.debug(e.getMessage()); throw new WebApplicationException(Status.NOT_FOUND); } }; @@ -111,8 +103,8 @@ public class ContentResource { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Response.ResponseBuilder responseBuilder = Response.ok(); return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder); - } catch (RepositoryNotFoundException e) { - LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); + } catch (NotFoundException e) { + LOG.debug(e.getMessage()); return Response.status(Status.NOT_FOUND).build(); } } @@ -120,12 +112,6 @@ public class ContentResource { private Response createContentHeader(String namespace, String name, String revision, String path, RepositoryService repositoryService, Response.ResponseBuilder responseBuilder) { try { appendContentHeader(path, getHead(revision, path, repositoryService), responseBuilder); - } catch (PathNotFoundException e) { - LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); - return Response.status(Status.NOT_FOUND).build(); - } catch (RevisionNotFoundException e) { - LOG.debug("revision '{}' not found in repository {}/{}", revision, namespace, name, e); - return Response.status(Status.NOT_FOUND).build(); } catch (IOException e) { LOG.info("error reading repository resource {} from {}/{}", path, namespace, name, e); return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); @@ -136,10 +122,10 @@ public class ContentResource { private void appendContentHeader(String path, byte[] head, Response.ResponseBuilder responseBuilder) { ContentType contentType = ContentTypes.detect(path, head); responseBuilder.header("Content-Type", contentType.getRaw()); - contentType.getLanguage().ifPresent(language -> responseBuilder.header("Language", language)); + contentType.getLanguage().ifPresent(language -> responseBuilder.header("X-Programming-Language", language)); } - private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException, PathNotFoundException, RevisionNotFoundException { + private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException { InputStream stream = repositoryService.getCatCommand().setRevision(revision).getStream(path); try { byte[] buffer = new byte[HEAD_BUFFER_SIZE]; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java index cb78c961a2..e236b54005 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java @@ -4,24 +4,29 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.util.HttpUtil; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.validation.constraints.Pattern; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; public class DiffRootResource { public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; + + private static final String DIFF_FORMAT_VALUES_REGEX = "NATIVE|GIT|UNIFIED"; + private final RepositoryServiceFactory serviceFactory; @Inject @@ -50,17 +55,15 @@ public class DiffRootResource { @ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"), @ResponseCode(code = 500, condition = "internal server error") }) - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision){ + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ){ HttpUtil.checkForCRLFInjection(revision); + DiffFormat diffFormat = DiffFormat.valueOf(format); try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { StreamingOutput responseEntry = output -> { - try { - repositoryService.getDiffCommand() - .setRevision(revision) - .retriveContent(output); - } catch (RevisionNotFoundException e) { - throw new WebApplicationException(Response.Status.NOT_FOUND); - } + repositoryService.getDiffCommand() + .setRevision(revision) + .setFormat(diffFormat) + .retrieveContent(output); }; return Response.ok(responseEntry) .header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision))) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java new file mode 100644 index 0000000000..bd889d5de5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java @@ -0,0 +1,33 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.Setter; +import sonia.scm.ContextEntry; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +@Getter @Setter +public class ErrorDto { + private String transactionId; + private String errorCode; + private List<ContextEntry> context; + private String message; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @XmlElementWrapper(name = "violations") + private List<ConstraintViolationDto> violations; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String url; + + @XmlRootElement(name = "violation") + @Getter @Setter + public static class ConstraintViolationDto { + private String path; + private String message; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java new file mode 100644 index 0000000000..cdd545542a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java @@ -0,0 +1,23 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.slf4j.MDC; +import sonia.scm.ExceptionWithContext; + +@Mapper +public abstract class ExceptionWithContextToErrorDtoMapper { + + @Mapping(target = "errorCode", source = "code") + @Mapping(target = "transactionId", ignore = true) + @Mapping(target = "violations", ignore = true) + @Mapping(target = "url", ignore = true) + public abstract ErrorDto map(ExceptionWithContext exception); + + @AfterMapping + void setTransactionId(@MappingTarget ErrorDto dto) { + dto.setTransactionId(MDC.get("transaction_id")); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java index e38a6f699a..017568b613 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java @@ -7,11 +7,8 @@ import lombok.extern.slf4j.Slf4j; import sonia.scm.PageResult; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; @@ -26,6 +23,9 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + @Slf4j public class FileHistoryRootResource { @@ -51,8 +51,6 @@ public class FileHistoryRootResource { * @param pageSize pagination * @return all changesets related to the given file starting with the given revision * @throws IOException on io error - * @throws RevisionNotFoundException on missing revision - * @throws RepositoryNotFoundException on missing repository */ @GET @Path("{revision}/{path: .*}") @@ -69,8 +67,9 @@ public class FileHistoryRootResource { @PathParam("revision") String revision, @PathParam("path") String path, @DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException { - try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); + try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { log.info("Get changesets of the file {} and revision {}", path, revision); Repository repository = repositoryService.getRepository(); ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService) @@ -84,9 +83,9 @@ public class FileHistoryRootResource { PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); return Response.ok(fileHistoryCollectionToDtoMapper.map(page, pageSize, pageResult, repository, revision, path)).build(); } else { - String message = String.format("for the revision %s and the file %s there is no changesets", revision, path); + String message = String.format("for the revision %s and the file %s there are no changesets", revision, path); log.error(message); - throw new InternalRepositoryException(message); + throw notFound(entity("path", path).in("revision", revision).in(namespaceAndName)); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 8abab0f720..4c111e6707 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -5,12 +5,12 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import sonia.scm.AlreadyExistsException; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; @@ -71,7 +71,7 @@ public class GroupCollectionResource { /** * Creates a new group. - * @param groupDto The group to be created. + * @param group The group to be created. * @return A response with the link to the new group (if created successfully). */ @POST @@ -86,9 +86,9 @@ public class GroupCollectionResource { }) @TypeHint(TypeHint.NO_CONTENT.class) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created group")) - public Response create(@Valid GroupDto groupDto) throws AlreadyExistsException { - return adapter.create(groupDto, - () -> dtoToGroupMapper.map(groupDto), - group -> resourceLinks.group().self(group.getName())); + public Response create(@Valid GroupDto group) { + return adapter.create(group, + () -> dtoToGroupMapper.map(group), + g -> resourceLinks.group().self(g.getName())); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 789a9847cb..6d0b921d02 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -3,13 +3,12 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import sonia.scm.ConcurrentModificationException; -import sonia.scm.NotFoundException; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -84,7 +83,7 @@ public class GroupResource { * <strong>Note:</strong> This method requires "group" privilege. * * @param name name of the group to be modified - * @param groupDto group object to modify + * @param group group object to modify */ @PUT @Path("") @@ -98,7 +97,7 @@ public class GroupResource { @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - public Response update(@PathParam("id") String name, @Valid GroupDto groupDto) throws ConcurrentModificationException { - return adapter.update(name, existing -> dtoToGroupMapper.map(groupDto)); + public Response update(@PathParam("id") String name, @Valid GroupDto group) { + return adapter.update(name, existing -> dtoToGroupMapper.map(group)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index 9fe3d8284d..2b2a4cf0ad 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -1,10 +1,9 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; -import sonia.scm.AlreadyExistsException; -import sonia.scm.ConcurrentModificationException; import sonia.scm.Manager; import sonia.scm.ModelObject; +import sonia.scm.NotFoundException; import sonia.scm.PageResult; import javax.ws.rs.core.Response; @@ -22,6 +21,7 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject, DTO extends HalRepresentation> { private final Manager<MODEL_OBJECT> manager; + private final String type; private final SingleResourceManagerAdapter<MODEL_OBJECT, DTO> singleAdapter; private final CollectionResourceManagerAdapter<MODEL_OBJECT, DTO> collectionAdapter; @@ -30,13 +30,14 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject, this.manager = manager; singleAdapter = new SingleResourceManagerAdapter<>(manager, type); collectionAdapter = new CollectionResourceManagerAdapter<>(manager, type); + this.type = type.getSimpleName(); } Response get(String id, Function<MODEL_OBJECT, DTO> mapToDto) { return singleAdapter.get(loadBy(id), mapToDto); } - public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws ConcurrentModificationException { + public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) { return singleAdapter.update( loadBy(id), applyChanges, @@ -48,7 +49,7 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject, return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto); } - public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) throws AlreadyExistsException { + public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) { return collectionAdapter.create(dto, modelObjectSupplier, uriCreator); } @@ -56,8 +57,8 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject, return singleAdapter.delete(id); } - private Supplier<Optional<MODEL_OBJECT>> loadBy(String id) { - return () -> Optional.ofNullable(manager.get(id)); + private Supplier<MODEL_OBJECT> loadBy(String id) { + return () -> Optional.ofNullable(manager.get(id)).orElseThrow(() -> new NotFoundException(type, id)); } private Predicate<MODEL_OBJECT> idStaysTheSame(String id) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java index 088558c7dc..1eec99ea96 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.security.AllowAnonymousAccess; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -9,6 +10,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; @Path(IndexResource.INDEX_PATH_V2) +@AllowAnonymousAccess public class IndexResource { public static final String INDEX_PATH_V2 = "v2/"; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InternalRepositoryExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InternalRepositoryExceptionMapper.java index d30677c5f7..856a9310a0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InternalRepositoryExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InternalRepositoryExceptionMapper.java @@ -1,15 +1,17 @@ package sonia.scm.api.v2.resources; -import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.api.rest.ContextualExceptionMapper; import sonia.scm.repository.InternalRepositoryException; +import javax.inject.Inject; import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; @Provider -public class InternalRepositoryExceptionMapper extends StatusExceptionMapper<InternalRepositoryException> { +public class InternalRepositoryExceptionMapper extends ContextualExceptionMapper<InternalRepositoryException> { - public InternalRepositoryExceptionMapper() { - super(InternalRepositoryException.class, Response.Status.INTERNAL_SERVER_ERROR); + @Inject + public InternalRepositoryExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) { + super(InternalRepositoryException.class, Response.Status.INTERNAL_SERVER_ERROR, mapper); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InvalidPasswordExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InvalidPasswordExceptionMapper.java index 7c5364ba03..7a1d311a1c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InvalidPasswordExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InvalidPasswordExceptionMapper.java @@ -1,17 +1,17 @@ package sonia.scm.api.v2.resources; +import sonia.scm.api.rest.ContextualExceptionMapper; import sonia.scm.user.InvalidPasswordException; +import javax.inject.Inject; import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider -public class InvalidPasswordExceptionMapper implements ExceptionMapper<InvalidPasswordException> { - @Override - public Response toResponse(InvalidPasswordException exception) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(exception.getMessage()) - .build(); +public class InvalidPasswordExceptionMapper extends ContextualExceptionMapper<InvalidPasswordException> { + + @Inject + public InvalidPasswordExceptionMapper(ExceptionWithContextToErrorDtoMapper mapper) { + super(InvalidPasswordException.class, Response.Status.BAD_REQUEST, mapper); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 6497cb9315..35f58cef90 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -39,6 +39,9 @@ public class MapperModule extends AbstractModule { bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass()); + bind(ViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ViolationExceptionToErrorDtoMapper.class).getClass()); + bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass()); + // no mapstruct required bind(UIPluginDtoMapper.class); bind(UIPluginDtoCollectionMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java index f616aff852..20fc35923c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -10,6 +10,7 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -73,8 +74,8 @@ public class MeResource { }) @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(VndMediaType.PASSWORD_CHANGE) - public Response changePassword(@Valid PasswordChangeDto passwordChangeDto) { - userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChangeDto.getOldPassword()), passwordService.encryptPassword(passwordChangeDto.getNewPassword())); + public Response changePassword(@Valid PasswordChangeDto passwordChange) { + userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChange.getOldPassword()), passwordService.encryptPassword(passwordChange.getNewPassword())); return Response.noContent().build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java index 28f855f40c..eaf165cda1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java @@ -3,11 +3,8 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Modifications; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; @@ -46,7 +43,7 @@ public class ModificationsRootResource { @Produces(VndMediaType.MODIFICATIONS) @TypeHint(ModificationsDto.class) @Path("{revision}") - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException, RevisionNotFoundException, RepositoryNotFoundException , InternalRepositoryException { + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Modifications modifications = repositoryService.getModificationsCommand() .revision(revision) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index b1ea912acf..127a3f450e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -11,11 +11,11 @@ import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Permission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -30,6 +30,9 @@ import java.net.URI; import java.util.Optional; import java.util.function.Predicate; +import static sonia.scm.AlreadyExistsException.alreadyExists; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; @Slf4j @@ -71,12 +74,12 @@ public class PermissionRootResource { @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(VndMediaType.PERMISSION) @Path("") - public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) throws AlreadyExistsException, NotFoundException { + public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) { log.info("try to add new permission: {}", permission); Repository repository = load(namespace, name); RepositoryPermissions.permissionWrite(repository).check(); checkPermissionAlreadyExists(permission, repository); - repository.getPermissions().add(dtoToModelMapper.map(permission)); + repository.addPermission(dtoToModelMapper.map(permission)); manager.modify(repository); String urlPermissionName = modelToDtoMapper.getUrlPermissionName(permission); return Response.created(URI.create(resourceLinks.permission().self(namespace, name, urlPermissionName))).build(); @@ -109,7 +112,7 @@ public class PermissionRootResource { .filter(filterPermission(permissionName)) .map(permission -> modelToDtoMapper.map(permission, repository)) .findFirst() - .orElseThrow(NotFoundException::new) + .orElseThrow(() -> notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name))) ).build(); } @@ -131,7 +134,7 @@ public class PermissionRootResource { @Produces(VndMediaType.PERMISSION) @TypeHint(PermissionDto.class) @Path("") - public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) { Repository repository = load(namespace, name); RepositoryPermissions.permissionRead(repository).check(); return Response.ok(permissionCollectionToDtoMapper.map(repository)).build(); @@ -158,23 +161,23 @@ public class PermissionRootResource { public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName, - @Valid PermissionDto permission) throws AlreadyExistsException { + @Valid PermissionDto permission) { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = load(namespace, name); RepositoryPermissions.permissionWrite(repository).check(); String extractedPermissionName = getPermissionName(permissionName); if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) { - throw new NotFoundException("permission", extractedPermissionName); + throw notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name)); } permission.setGroupPermission(isGroupPermission(permissionName)); if (!extractedPermissionName.equals(permission.getName())) { - checkPermissionAlreadyExists(permission, repository, "target permission " + permission.getName() + " already exists"); + checkPermissionAlreadyExists(permission, repository); } Permission existingPermission = repository.getPermissions() .stream() .filter(filterPermission(permissionName)) .findFirst() - .orElseThrow(NotFoundException::new); + .orElseThrow(() -> notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name))); dtoToModelMapper.modify(existingPermission, permission); manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); @@ -206,7 +209,7 @@ public class PermissionRootResource { .stream() .filter(filterPermission(permissionName)) .findFirst() - .ifPresent(p -> repository.getPermissions().remove(p)) + .ifPresent(repository::removePermission) ; manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); @@ -237,12 +240,12 @@ public class PermissionRootResource { * @param namespace the repository namespace * @param name the repository name * @return the repository if the user is permitted - * @throws RepositoryNotFoundException if the repository does not exists + * @throws NotFoundException if the repository does not exists */ - private Repository load(String namespace, String name) throws RepositoryNotFoundException { + private Repository load(String namespace, String name) { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); return Optional.ofNullable(manager.get(namespaceAndName)) - .orElseThrow(() -> new RepositoryNotFoundException(namespaceAndName)); + .orElseThrow(() -> notFound(entity(namespaceAndName))); } /** @@ -250,12 +253,11 @@ public class PermissionRootResource { * * @param permission the searched permission * @param repository the repository to be inspected - * @param errorMessage error message * @throws AlreadyExistsException if the permission already exists in the repository */ - private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository, String errorMessage) throws AlreadyExistsException { + private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) { if (isPermissionExist(permission, repository)) { - throw new AlreadyExistsException(errorMessage); + throw alreadyExists(entity("permission", permission.getName()).in(repository)); } } @@ -264,10 +266,6 @@ public class PermissionRootResource { .stream() .anyMatch(p -> p.getName().equals(permission.getName()) && p.isGroupPermission() == permission.isGroupPermission()); } - - private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws AlreadyExistsException { - checkPermissionAlreadyExists(permission, repository, "the permission " + permission.getName() + " already exist."); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 9f4858d2f6..d8d5280456 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -5,9 +5,12 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import sonia.scm.AlreadyExistsException; +import org.apache.shiro.SecurityUtils; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; +import sonia.scm.user.User; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -21,6 +24,8 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import static java.util.Collections.singletonList; + public class RepositoryCollectionResource { private static final int DEFAULT_PAGE_SIZE = 10; @@ -72,7 +77,7 @@ public class RepositoryCollectionResource { * <strong>Note:</strong> This method requires "repository" privilege. The namespace of the given repository will * be ignored and set by the configured namespace strategy. * - * @param repositoryDto The repository to be created. + * @param repository The repository to be created. * @return A response with the link to the new repository (if created successfully). */ @POST @@ -87,9 +92,19 @@ public class RepositoryCollectionResource { }) @TypeHint(TypeHint.NO_CONTENT.class) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) - public Response create(@Valid RepositoryDto repositoryDto) throws AlreadyExistsException { - return adapter.create(repositoryDto, - () -> dtoToRepositoryMapper.map(repositoryDto, null), - repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName())); + public Response create(@Valid RepositoryDto repository) { + return adapter.create(repository, + () -> createModelObjectFromDto(repository), + r -> resourceLinks.repository().self(r.getNamespace(), r.getName())); + } + + private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) { + Repository repository = dtoToRepositoryMapper.map(repositoryDto, null); + repository.setPermissions(singletonList(new Permission(currentUser(), PermissionType.OWNER))); + return repository; + } + + private String currentUser() { + return SecurityUtils.getSubject().getPrincipals().oneByType(User.class).getName(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 56bb50d4e6..f65235db0b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -3,8 +3,6 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import sonia.scm.ConcurrentModificationException; -import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryIsNotArchivedException; @@ -12,6 +10,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Provider; import javax.validation.Valid; import javax.ws.rs.Consumes; @@ -26,6 +25,9 @@ import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + public class RepositoryResource { private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; @@ -124,7 +126,7 @@ public class RepositoryResource { * * @param namespace the namespace of the repository to be modified * @param name the name of the repository to be modified - * @param repositoryDto repository object to modify + * @param repository repository object to modify */ @PUT @Path("") @@ -138,10 +140,10 @@ public class RepositoryResource { @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repositoryDto) throws ConcurrentModificationException { + public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repository) { return adapter.update( loadBy(namespace, name), - existing -> processUpdate(repositoryDto, existing), + existing -> processUpdate(repository, existing), nameAndNamespaceStaysTheSame(namespace, name) ); } @@ -203,8 +205,9 @@ public class RepositoryResource { } } - private Supplier<Optional<Repository>> loadBy(String namespace, String name) { - return () -> Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))); + private Supplier<Repository> loadBy(String namespace, String name) { + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); + return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName))); } private Predicate<Repository> nameAndNamespaceStaysTheSame(String namespace, String name) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 1b397480a4..970166257a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -322,6 +322,10 @@ class ResourceLinks { public String history(NamespaceAndName namespaceAndName, String branch) { return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href(); } + + public String changesetDiff(NamespaceAndName namespaceAndName, String branch) { + return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("changesetDiff").parameters(branch, "").href() + "{otherBranch}"; + } } public BranchCollectionLinks branchCollection() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index d484567bf8..e61a9f3455 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -11,7 +11,6 @@ import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; import java.util.Collection; import java.util.Optional; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -33,45 +32,41 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, DTO extends HalRepresentation> extends AbstractManagerResource<MODEL_OBJECT> { private final Function<Throwable, Optional<Response>> errorHandler; + private final Class<MODEL_OBJECT> type; SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) { this(manager, type, e -> Optional.empty()); } - SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type, Function<Throwable, Optional<Response>> errorHandler) { + SingleResourceManagerAdapter( + Manager<MODEL_OBJECT> manager, + Class<MODEL_OBJECT> type, + Function<Throwable, Optional<Response>> errorHandler) { super(manager, type); this.errorHandler = errorHandler; + this.type = type; } /** * Reads the model object for the given id, transforms it to a dto and returns a corresponding http response. * This handles all corner cases, eg. no matching object for the id or missing privileges. */ - Response get(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, DTO> mapToDto) { - return reader.get() - .map(mapToDto) - .map(Response::ok) - .map(Response.ResponseBuilder::build) - .orElseThrow(NotFoundException::new); - } - public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException { - MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new); - checker.accept(existingModelObject); - return update(reader,applyChanges,hasSameKey); + Response get(Supplier<MODEL_OBJECT> reader, Function<MODEL_OBJECT, DTO> mapToDto) { + return Response.ok(mapToDto.apply(reader.get())).build(); } /** * Update the model object for the given id according to the given function and returns a corresponding http response. * This handles all corner cases, eg. no matching object for the id or missing privileges. */ - public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) throws NotFoundException, ConcurrentModificationException { - MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new); + Response update(Supplier<MODEL_OBJECT> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) { + MODEL_OBJECT existingModelObject = reader.get(); MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); if (!hasSameKey.test(changedModelObject)) { return Response.status(BAD_REQUEST).entity("illegal change of id").build(); } else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) { - throw new ConcurrentModificationException(); + throw new ConcurrentModificationException(type, existingModelObject.getId()); } return update(getId(existingModelObject), changedModelObject); } @@ -81,11 +76,13 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, && (updated.getLastModified() == null || existing.getLastModified() > updated.getLastModified()); } - public Response delete(Supplier<Optional<MODEL_OBJECT>> reader) { - return reader.get() - .map(MODEL_OBJECT::getId) - .map(this::delete) - .orElse(null); + public Response delete(Supplier<MODEL_OBJECT> reader) { + try { + return delete(reader.get().getId()); + } catch (NotFoundException e) { + // due to idempotency of delete this does not matter here. + return null; + } } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java index 8ca9c07cf6..d507a59f14 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import sonia.scm.NotFoundException; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.api.BrowseCommandBuilder; @@ -31,14 +30,14 @@ public class SourceRootResource { @GET @Produces(VndMediaType.SOURCE) @Path("") - public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws NotFoundException, IOException { + public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { return getSource(namespace, name, "/", null); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}") - public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws NotFoundException, IOException { + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException { return getSource(namespace, name, "/", revision); } @@ -49,7 +48,7 @@ public class SourceRootResource { return getSource(namespace, name, path, revision); } - private Response getSource(String namespace, String repoName, String path, String revision) throws IOException, NotFoundException { + private Response getSource(String namespace, String repoName, String path, String revision) throws IOException { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagNotFoundException.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagNotFoundException.java deleted file mode 100644 index b572d20c5b..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package sonia.scm.api.v2.resources; - -import sonia.scm.NotFoundException; - -public class TagNotFoundException extends NotFoundException { - -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java index b3601f7709..7acd59e3e1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java @@ -3,9 +3,9 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.Tag; import sonia.scm.repository.Tags; @@ -21,6 +21,9 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Response; import java.io.IOException; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + public class TagRootResource { private final RepositoryServiceFactory serviceFactory; @@ -47,7 +50,7 @@ public class TagRootResource { }) @Produces(VndMediaType.TAG_COLLECTION) @TypeHint(CollectionDto.class) - public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException, RepositoryNotFoundException { + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Tags tags = getTags(repositoryService); if (tags != null && tags.getTags() != null) { @@ -72,7 +75,7 @@ public class TagRootResource { @Produces(VndMediaType.TAG) @TypeHint(TagDto.class) @Path("{tagName}") - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException, RepositoryNotFoundException, TagNotFoundException { + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { Tags tags = getTags(repositoryService); @@ -80,7 +83,7 @@ public class TagRootResource { Tag tag = tags.getTags().stream() .filter(t -> tagName.equals(t.getName())) .findFirst() - .orElseThrow(TagNotFoundException::new); + .orElseThrow(() -> createNotFoundException(namespace, name, tagName)); return Response.ok(tagToTagDtoMapper.map(tag, namespaceAndName)).build(); } else { return Response.status(Response.Status.INTERNAL_SERVER_ERROR) @@ -90,6 +93,10 @@ public class TagRootResource { } } + private NotFoundException createNotFoundException(String namespace, String name, String tagName) { + return notFound(entity("Tag", tagName).in("Repository", namespace + "/" + name)); + } + private Tags getTags(RepositoryService repositoryService) throws IOException { Repository repository = repositoryService.getRepository(); RepositoryPermissions.read(repository).check(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java index a76c39dd69..b83f5310e3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginWrapper; +import sonia.scm.security.AllowAnonymousAccess; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -17,6 +18,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +@AllowAnonymousAccess public class UIPluginResource { private final PluginLoader pluginLoader; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 9b39888104..a4fe9adb94 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -6,12 +6,12 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; -import sonia.scm.AlreadyExistsException; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; @@ -76,7 +76,7 @@ public class UserCollectionResource { * * <strong>Note:</strong> This method requires "user" privilege. * - * @param userDto The user to be created. + * @param user The user to be created. * @return A response with the link to the new user (if created successfully). */ @POST @@ -91,7 +91,7 @@ public class UserCollectionResource { }) @TypeHint(TypeHint.NO_CONTENT.class) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user")) - public Response create(@Valid UserDto userDto) throws AlreadyExistsException { - return adapter.create(userDto, () -> dtoToUserMapper.map(userDto, passwordService.encryptPassword(userDto.getPassword())), user -> resourceLinks.user().self(user.getName())); + public Response create(@Valid UserDto user) { + return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName())); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index ef4b8a6eb4..0076d057ca 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -4,12 +4,12 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; -import sonia.scm.ConcurrentModificationException; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -87,7 +87,7 @@ public class UserResource { * <strong>Note:</strong> This method requires "user" privilege. * * @param name name of the user to be modified - * @param userDto user object to modify + * @param user user object to modify */ @PUT @Path("") @@ -101,8 +101,8 @@ public class UserResource { @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws ConcurrentModificationException { - return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword())); + public Response update(@PathParam("id") String name, @Valid UserDto user) { + return adapter.update(name, existing -> dtoToUserMapper.map(user, existing.getPassword())); } /** @@ -114,7 +114,7 @@ public class UserResource { * <strong>Note:</strong> This method requires "user:changeOwnPassword" privilege to modify the own password. * * @param name name of the user to be modified - * @param passwordOverwriteDto change password object to modify password. the old password is here not required + * @param passwordOverwrite change password object to modify password. the old password is here not required */ @PUT @Path("password") @@ -128,8 +128,8 @@ public class UserResource { @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwriteDto) { - userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwriteDto.getNewPassword())); + public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwrite) { + userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwrite.getNewPassword())); return Response.noContent().build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java new file mode 100644 index 0000000000..e713b031f7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java @@ -0,0 +1,54 @@ +package sonia.scm.api.v2.resources; + +import org.jboss.resteasy.api.validation.ResteasyViolationException; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.slf4j.MDC; + +import javax.validation.ConstraintViolation; +import java.util.List; +import java.util.stream.Collectors; + +@Mapper +public abstract class ViolationExceptionToErrorDtoMapper { + + @Mapping(target = "errorCode", ignore = true) + @Mapping(target = "transactionId", ignore = true) + @Mapping(target = "context", ignore = true) + @Mapping(target = "url", ignore = true) + public abstract ErrorDto map(ResteasyViolationException exception); + + @AfterMapping + void setTransactionId(@MappingTarget ErrorDto dto) { + dto.setTransactionId(MDC.get("transaction_id")); + } + + @AfterMapping + void mapViolations(ResteasyViolationException exception, @MappingTarget ErrorDto dto) { + List<ErrorDto.ConstraintViolationDto> violations = + exception.getConstraintViolations() + .stream() + .map(this::createViolationDto) + .collect(Collectors.toList()); + dto.setViolations(violations); + } + + private ErrorDto.ConstraintViolationDto createViolationDto(ConstraintViolation<?> violation) { + ErrorDto.ConstraintViolationDto constraintViolationDto = new ErrorDto.ConstraintViolationDto(); + constraintViolationDto.setMessage(violation.getMessage()); + constraintViolationDto.setPath(violation.getPropertyPath().toString()); + return constraintViolationDto; + } + + @AfterMapping + void setErrorCode(@MappingTarget ErrorDto dto) { + dto.setErrorCode("1wR7ZBe7H1"); + } + + @AfterMapping + void setMessage(@MappingTarget ErrorDto dto) { + dto.setMessage("input violates conditions (see violation list)"); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java index bfa2218e76..aec8e2d653 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextFilter.java @@ -34,22 +34,19 @@ package sonia.scm.boot; //~--- non-JDK imports -------------------------------------------------------- import com.github.legman.Subscribe; - import com.google.inject.servlet.GuiceFilter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContext; import sonia.scm.Stage; import sonia.scm.event.ScmEventBus; -//~--- JDK imports ------------------------------------------------------------ - import javax.servlet.FilterConfig; import javax.servlet.ServletContextEvent; import javax.servlet.ServletException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -65,6 +62,8 @@ public class BootstrapContextFilter extends GuiceFilter //~--- methods -------------------------------------------------------------- + private final BootstrapContextListener listener = new BootstrapContextListener(); + /** * Restart the whole webapp context. * @@ -85,29 +84,20 @@ public class BootstrapContextFilter extends GuiceFilter } else { - - logger.warn( - "destroy filter pipeline, because of a received restart event"); + logger.warn("destroy filter pipeline, because of a received restart event"); destroy(); - logger.warn( - "reinitialize filter pipeline, because of a received restart event"); - super.init(filterConfig); + + logger.warn("reinitialize filter pipeline, because of a received restart event"); + initGuice(); } } - /** - * Method description - * - * - * @param filterConfig - * - * @throws ServletException - */ @Override public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; - super.init(filterConfig); + + initGuice(); if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT) { @@ -116,6 +106,19 @@ public class BootstrapContextFilter extends GuiceFilter } } + public void initGuice() throws ServletException { + super.init(filterConfig); + + listener.contextInitialized(new ServletContextEvent(filterConfig.getServletContext())); + } + + @Override + public void destroy() { + super.destroy(); + listener.contextDestroyed(new ServletContextEvent(filterConfig.getServletContext())); + ServletContextCleaner.cleanup(filterConfig.getServletContext()); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java index 4f7a00ce56..afaa28bfe8 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapContextListener.java @@ -148,13 +148,15 @@ public class BootstrapContextListener implements ServletContextListener { context = sce.getServletContext(); - PluginIndex index = readCorePluginIndex(context); - File pluginDirectory = getPluginDirectory(); try { - extractCorePlugins(context, pluginDirectory, index); + if (!isCorePluginExtractionDisabled()) { + extractCorePlugins(context, pluginDirectory); + } else { + logger.info("core plugin extraction is disabled"); + } ClassLoader cl = ClassLoaders.getContextClassLoader(BootstrapContextListener.class); @@ -181,31 +183,8 @@ public class BootstrapContextListener implements ServletContextListener } } - /** - * Restart the whole webapp context. - * - * - * @param event restart event - */ - @Subscribe - public void handleRestartEvent(RestartEvent event) - { - logger.warn("received restart event from {} with reason: {}", - event.getCause(), event.getReason()); - - if (context == null) - { - logger.error("context is null, scm-manager is not initialized"); - } - else - { - ServletContextEvent sce = new ServletContextEvent(context); - - logger.warn("destroy context, because of a received restart event"); - contextDestroyed(sce); - logger.warn("reinitialize context, because of a received restart event"); - contextInitialized(sce); - } + private boolean isCorePluginExtractionDisabled() { + return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction"); } /** @@ -214,7 +193,6 @@ public class BootstrapContextListener implements ServletContextListener * * @param context * @param pluginDirectory - * @param name * @param entry * * @throws IOException @@ -269,17 +247,15 @@ public class BootstrapContextListener implements ServletContextListener * * @param context * @param pluginDirectory - * @param lines - * @param index * * @throws IOException */ - private void extractCorePlugins(ServletContext context, File pluginDirectory, - PluginIndex index) - throws IOException + private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException { IOUtil.mkdirs(pluginDirectory); + PluginIndex index = readCorePluginIndex(context); + for (PluginIndexEntry entry : index) { extractCorePlugin(context, pluginDirectory, entry); diff --git a/scm-webapp/src/main/java/sonia/scm/boot/RestartServlet.java b/scm-webapp/src/main/java/sonia/scm/boot/RestartServlet.java new file mode 100644 index 0000000000..c7177cc459 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/RestartServlet.java @@ -0,0 +1,97 @@ +package sonia.scm.boot; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.Priority; +import sonia.scm.SCMContext; +import sonia.scm.Stage; +import sonia.scm.event.ScmEventBus; +import sonia.scm.filter.WebElement; + +import javax.inject.Inject; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This servlet sends a {@link RestartEvent} to the {@link ScmEventBus} which causes scm-manager to restart the context. + * The {@link RestartServlet} can be used for reloading java code or for installing plugins without a complete restart. + * At the moment the Servlet accepts only request, if scm-manager was started in the {@link Stage#DEVELOPMENT} stage. + * + * @since 2.0.0 + */ +@Priority(0) +@WebElement("/restart") +public class RestartServlet extends HttpServlet { + + private static final Logger LOG = LoggerFactory.getLogger(RestartServlet.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final AtomicBoolean restarting = new AtomicBoolean(); + + private final ScmEventBus eventBus; + private final Stage stage; + + @Inject + public RestartServlet() { + this(ScmEventBus.getInstance(), SCMContext.getContext().getStage()); + } + + RestartServlet(ScmEventBus eventBus, Stage stage) { + this.eventBus = eventBus; + this.stage = stage; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + LOG.info("received sendRestartEvent request"); + + if (isRestartAllowed()) { + + try (InputStream requestInput = req.getInputStream()) { + Reason reason = objectMapper.readValue(requestInput, Reason.class); + sendRestartEvent(resp, reason); + } catch (IOException ex) { + LOG.warn("failed to trigger sendRestartEvent event", ex); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + } else { + LOG.debug("received restart event in non development stage"); + resp.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); + } + } + + private boolean isRestartAllowed() { + return stage == Stage.DEVELOPMENT; + } + + private void sendRestartEvent(HttpServletResponse response, Reason reason) { + if ( restarting.compareAndSet(false, true) ) { + LOG.info("trigger sendRestartEvent, because of {}", reason.getMessage()); + eventBus.post(new RestartEvent(RestartServlet.class, reason.getMessage())); + + response.setStatus(HttpServletResponse.SC_ACCEPTED); + } else { + LOG.warn("scm-manager restarts already"); + response.setStatus(HttpServletResponse.SC_CONFLICT); + } + } + + public static class Reason { + + private String message; + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/ServletContextCleaner.java b/scm-webapp/src/main/java/sonia/scm/boot/ServletContextCleaner.java new file mode 100644 index 0000000000..8b152ce329 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/boot/ServletContextCleaner.java @@ -0,0 +1,59 @@ +package sonia.scm.boot; + +import com.google.common.collect.ImmutableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletContext; +import java.util.Enumeration; +import java.util.Set; + +/** + * Remove cached resources from {@link ServletContext} to allow a clean restart of scm-manager without stale or + * duplicated data. + * + * @since 2.0.0 + */ +final class ServletContextCleaner { + + private static final Logger LOG = LoggerFactory.getLogger(ServletContextCleaner.class); + + private static final Set<String> REMOVE_PREFIX = ImmutableSet.of( + "org.jboss.resteasy", + "resteasy", + "org.apache.shiro", + "sonia.scm" + ); + + private ServletContextCleaner() { + } + + /** + * Remove cached attributes from {@link ServletContext}. + * + * @param servletContext servlet context + */ + static void cleanup(ServletContext servletContext) { + LOG.info("remove cached attributes from context"); + + Enumeration<String> attributeNames = servletContext.getAttributeNames(); + while( attributeNames.hasMoreElements()) { + String name = attributeNames.nextElement(); + if (shouldRemove(name)) { + LOG.info("remove attribute {} from servlet context", name); + servletContext.removeAttribute(name); + } else { + LOG.info("keep attribute {} in servlet context", name); + } + } + } + + private static boolean shouldRemove(String name) { + for (String prefix : REMOVE_PREFIX) { + if (name.startsWith(prefix)) { + return true; + } + } + return false; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java deleted file mode 100644 index a5455c6d21..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/filter/AdminSecurityFilter.java +++ /dev/null @@ -1,90 +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.filter; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; - -import org.apache.shiro.subject.Subject; - -import sonia.scm.Priority; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.security.Role; - -/** - * Security filter which allow only administrators to access the underlying - * resources. - * - * @author Sebastian Sdorra - */ -// TODO before releasing v2, delete this filter (we use Permission objects now) -@WebElement( - value = Filters.PATTERN_CONFIG, - morePatterns = { - Filters.PATTERN_USERS, - Filters.PATTERN_GROUPS, - Filters.PATTERN_PLUGINS - } -) -@Priority(Filters.PRIORITY_AUTHORIZATION + 1) -public class AdminSecurityFilter extends SecurityFilter -{ - - /** - * Constructs a new instance. - * - * @param configuration scm-manager main configuration - */ - @Inject - public AdminSecurityFilter(ScmConfiguration configuration) - { - super(configuration); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns {@code true} if the subject has the admin role. - * - * @param subject subject - * - * @return {@code true} if the subject has the admin role - */ - @Override - protected boolean hasPermission(Subject subject) - { - return subject.hasRole(Role.ADMIN); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java index 9198c19ef7..b77a927a2d 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/MDCFilter.java @@ -34,7 +34,6 @@ package sonia.scm.filter; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.annotations.VisibleForTesting; -import com.google.inject.Singleton; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; @@ -42,6 +41,8 @@ import org.apache.shiro.subject.Subject; import org.slf4j.MDC; import sonia.scm.SCMContext; +import sonia.scm.security.DefaultKeyGenerator; +import sonia.scm.security.KeyGenerator; import sonia.scm.web.filter.HttpFilter; //~--- JDK imports ------------------------------------------------------------ @@ -62,27 +63,26 @@ import sonia.scm.Priority; @WebElement(Filters.PATTERN_ALL) public class MDCFilter extends HttpFilter { + private static final DefaultKeyGenerator TRANSACTION_KEY_GENERATOR = new DefaultKeyGenerator(); - /** Field description */ @VisibleForTesting - static final String MDC_CLIEN_HOST = "client_host"; + static final String MDC_CLIENT_HOST = "client_host"; - /** Field description */ @VisibleForTesting - static final String MDC_CLIEN_IP = "client_ip"; - - /** url of the current request */ + static final String MDC_CLIENT_IP = "client_ip"; + @VisibleForTesting static final String MDC_REQUEST_URI = "request_uri"; - - /** request method */ + @VisibleForTesting static final String MDC_REQUEST_METHOD = "request_method"; - /** Field description */ @VisibleForTesting static final String MDC_USERNAME = "username"; + @VisibleForTesting + static final String MDC_TRANSACTION_ID = "transaction_id"; + //~--- methods -------------------------------------------------------------- /** @@ -102,10 +102,11 @@ public class MDCFilter extends HttpFilter throws IOException, ServletException { MDC.put(MDC_USERNAME, getUsername()); - MDC.put(MDC_CLIEN_IP, request.getRemoteAddr()); - MDC.put(MDC_CLIEN_HOST, request.getRemoteHost()); + MDC.put(MDC_CLIENT_IP, request.getRemoteAddr()); + MDC.put(MDC_CLIENT_HOST, request.getRemoteHost()); MDC.put(MDC_REQUEST_METHOD, request.getMethod()); MDC.put(MDC_REQUEST_URI, request.getRequestURI()); + MDC.put(MDC_TRANSACTION_ID, TRANSACTION_KEY_GENERATOR.createKey()); try { @@ -114,10 +115,11 @@ public class MDCFilter extends HttpFilter finally { MDC.remove(MDC_USERNAME); - MDC.remove(MDC_CLIEN_IP); - MDC.remove(MDC_CLIEN_HOST); + MDC.remove(MDC_CLIENT_IP); + MDC.remove(MDC_CLIENT_HOST); MDC.remove(MDC_REQUEST_METHOD); MDC.remove(MDC_REQUEST_URI); + MDC.remove(MDC_TRANSACTION_ID); } } diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java similarity index 72% rename from scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java rename to scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java index d97a5b050e..508e804d1f 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java @@ -42,9 +42,8 @@ import org.apache.shiro.subject.Subject; import sonia.scm.Priority; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; -import sonia.scm.security.SecurityRequests; import sonia.scm.web.filter.HttpFilter; -import sonia.scm.web.filter.SecurityHttpServletRequestWrapper; +import sonia.scm.web.filter.PropagatePrincipleServletRequestWrapper; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -61,10 +60,7 @@ import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; * @author Sebastian Sdorra */ @Priority(Filters.PRIORITY_AUTHORIZATION) -// TODO find a better way for unprotected resources -@WebElement(value = REST_API_PATH + "" + - "/(?!v2/ui).*", regex = true) -public class SecurityFilter extends HttpFilter +public class PropagatePrincipleFilter extends HttpFilter { /** name of request attribute for the primary principal */ @@ -74,7 +70,7 @@ public class SecurityFilter extends HttpFilter private final ScmConfiguration configuration; @Inject - public SecurityFilter(ScmConfiguration configuration) + public PropagatePrincipleFilter(ScmConfiguration configuration) { this.configuration = configuration; } @@ -84,31 +80,16 @@ public class SecurityFilter extends HttpFilter HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request)) + Subject subject = SecurityUtils.getSubject(); + if (hasPermission(subject)) { - Subject subject = SecurityUtils.getSubject(); - if (hasPermission(subject)) - { - // add primary principal as request attribute - // see https://goo.gl/JRjNmf - String username = getUsername(subject); - request.setAttribute(ATTRIBUTE_REMOTE_USER, username); + // add primary principal as request attribute + // see https://goo.gl/JRjNmf + String username = getUsername(subject); + request.setAttribute(ATTRIBUTE_REMOTE_USER, username); - // wrap servlet request to provide authentication informations - chain.doFilter(new SecurityHttpServletRequestWrapper(request, username), response); - } - else if (subject.isAuthenticated() || subject.isRemembered()) - { - response.sendError(HttpServletResponse.SC_FORBIDDEN); - } - else if (configuration.isAnonymousAccessEnabled()) - { - response.sendError(HttpServletResponse.SC_FORBIDDEN); - } - else - { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED); - } + // wrap servlet request to provide authentication information + chain.doFilter(new PropagatePrincipleServletRequestWrapper(request, username), response); } else { @@ -116,7 +97,7 @@ public class SecurityFilter extends HttpFilter } } - protected boolean hasPermission(Subject subject) + private boolean hasPermission(Subject subject) { return ((configuration != null) && configuration.isAnonymousAccessEnabled()) || subject.isAuthenticated() @@ -139,5 +120,4 @@ public class SecurityFilter extends HttpFilter return username; } - } diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index 236c6d0ebc..6bf850f99d 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -42,7 +42,6 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.AlreadyExistsException; import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; import sonia.scm.NotFoundException; @@ -106,7 +105,7 @@ public class DefaultGroupManager extends AbstractGroupManager } @Override - public Group create(Group group) throws AlreadyExistsException { + public Group create(Group group) { String type = group.getType(); if (Util.isEmpty(type)) { group.setType(groupDAO.getType()); @@ -172,7 +171,7 @@ public class DefaultGroupManager extends AbstractGroupManager if (fresh == null) { - throw new NotFoundException("group", group.getId()); + throw new NotFoundException(Group.class, group.getId()); } fresh.copyProperties(group); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java index 115a63fc2f..25b8390e53 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java @@ -41,6 +41,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.SCMContext; +import sonia.scm.Stage; import javax.servlet.ServletContext; import java.net.MalformedURLException; @@ -69,19 +71,21 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param servletContext - * @param plugins - */ - public DefaultUberWebResourceLoader(ServletContext servletContext, - Iterable<PluginWrapper> plugins) - { + public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins) { + this(servletContext, plugins, SCMContext.getContext().getStage()); + } + + public DefaultUberWebResourceLoader(ServletContext servletContext, Iterable<PluginWrapper> plugins, Stage stage) { this.servletContext = servletContext; this.plugins = plugins; - this.cache = CacheBuilder.newBuilder().build(); + this.cache = createCache(stage); + } + + private Cache<String, URL> createCache(Stage stage) { + if (stage == Stage.DEVELOPMENT) { + return CacheBuilder.newBuilder().maximumSize(0).build(); // Disable caching + } + return CacheBuilder.newBuilder().build(); } //~--- get methods ---------------------------------------------------------- @@ -97,7 +101,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader @Override public URL getResource(String path) { - URL resource = cache.getIfPresent(path); + URL resource = getFromCache(path); if (resource == null) { @@ -105,7 +109,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader if (resource != null) { - cache.put(path, resource); + addToCache(path, resource); } } else @@ -116,6 +120,14 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader return resource; } + private URL getFromCache(String path) { + return cache.getIfPresent(path); + } + + private void addToCache(String path, URL url) { + cache.put(path, url); + } + /** * Method description * diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 565d8854c7..d906873d80 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -65,6 +65,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + /** * Default implementation of {@link RepositoryManager}. * @@ -122,11 +125,11 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { } @Override - public Repository create(Repository repository) throws AlreadyExistsException { + public Repository create(Repository repository) { return create(repository, true); } - public Repository create(Repository repository, boolean initRepository) throws AlreadyExistsException { + public Repository create(Repository repository, boolean initRepository) { repository.setId(keyGenerator.createKey()); repository.setNamespace(namespaceStrategy.createNamespace(repository)); @@ -140,7 +143,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { try { getHandler(newRepository).create(newRepository); } catch (AlreadyExistsException e) { - throw new InternalRepositoryException("directory for repository does already exist", e); + throw new InternalRepositoryException(repository, "directory for repository does already exist", e); } } fireEvent(HandlerEventType.BEFORE_CREATE, newRepository); @@ -170,7 +173,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { } @Override - public void importRepository(Repository repository) throws AlreadyExistsException { + public void importRepository(Repository repository) { create(repository, false); } @@ -198,7 +201,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { } @Override - public void refresh(Repository repository) throws RepositoryNotFoundException { + public void refresh(Repository repository) { AssertUtil.assertIsNotNull(repository); RepositoryPermissions.read(repository).check(); @@ -207,7 +210,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { if (fresh != null) { fresh.copyProperties(repository); } else { - throw new RepositoryNotFoundException(repository); + throw notFound(entity(repository)); } } @@ -350,9 +353,9 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { RepositoryHandler handler = handlerMap.get(type); if (handler == null) { - throw new InternalRepositoryException("could not find handler for " + type); + throw new InternalRepositoryException(entity(repository), "could not find handler for " + type); } else if (!handler.isConfigured()) { - throw new InternalRepositoryException("handler is not configured for type " + type); + throw new InternalRepositoryException(entity(repository), "handler is not configured for type " + type); } return handler; diff --git a/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java b/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java index e9c415c4fa..8057db53dc 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java @@ -33,7 +33,6 @@ import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.ConcurrentModificationException; import sonia.scm.NotFoundException; import java.util.Set; @@ -61,7 +60,7 @@ public final class HealthChecker { Repository repository = repositoryManager.get(id); if (repository == null) { - throw new RepositoryNotFoundException(id); + throw new NotFoundException(Repository.class, id); } doCheck(repository); diff --git a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java index 5a954bb1e7..cf4c980625 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java +++ b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java @@ -167,7 +167,7 @@ public class AuthorizationChangedEventProducer { private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) { return repository.isArchived() != beforeModification.isArchived() || repository.isPublicReadable() != beforeModification.isPublicReadable() - || ! repository.getPermissions().equals(beforeModification.getPermissions()); + || !(repository.getPermissions().containsAll(beforeModification.getPermissions()) && beforeModification.getPermissions().containsAll(repository.getPermissions())); } private void fireEventForEveryUser() { diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index faa7208c66..8a79293642 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -54,13 +54,14 @@ import sonia.scm.cache.CacheManager; import sonia.scm.group.GroupNames; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.Extension; +import sonia.scm.repository.Permission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; -import sonia.scm.repository.RepositoryPermissions; import sonia.scm.user.User; import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; +import java.util.Collection; import java.util.List; import java.util.Set; @@ -198,7 +199,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private void collectRepositoryPermissions(Builder<String> builder, Repository repository, User user, GroupNames groups) { - List<sonia.scm.repository.Permission> repositoryPermissions + Collection<Permission> repositoryPermissions = repository.getPermissions(); if (Util.isNotEmpty(repositoryPermissions)) diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecurityRequestFilter.java b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequestFilter.java new file mode 100644 index 0000000000..2bbdf6f468 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequestFilter.java @@ -0,0 +1,45 @@ +package sonia.scm.security; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.subject.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; +import javax.ws.rs.ext.Provider; +import java.lang.reflect.Method; + +@Provider +public class SecurityRequestFilter implements ContainerRequestFilter { + + private static final Logger LOG = LoggerFactory.getLogger(SecurityRequestFilter.class); + + @Context + private ResourceInfo resourceInfo; + + @Override + public void filter(ContainerRequestContext requestContext) { + Method resourceMethod = resourceInfo.getResourceMethod(); + if (hasPermission() || anonymousAccessIsAllowed(resourceMethod)) { + LOG.debug("allowed unauthenticated request to method {}", resourceMethod); + // nothing further to do + } else { + LOG.debug("blocked unauthenticated request to method {}", resourceMethod); + throw new AuthenticationException(); + } + } + + private boolean anonymousAccessIsAllowed(Method method) { + return method.isAnnotationPresent(AllowAnonymousAccess.class) + || method.getDeclaringClass().isAnnotationPresent(AllowAnonymousAccess.class); + } + + private boolean hasPermission() { + Subject subject = SecurityUtils.getSubject(); + return subject.isAuthenticated() || subject.isRemembered(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 4efaaa1d1c..33762a5941 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -39,7 +39,7 @@ import com.google.inject.Singleton; import org.apache.shiro.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.AlreadyExistsException; +import sonia.scm.ContextEntry; import sonia.scm.EagerSingleton; import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; @@ -137,7 +137,7 @@ public class DefaultUserManager extends AbstractUserManager * @throws IOException */ @Override - public User create(User user) throws AlreadyExistsException { + public User create(User user) { String type = user.getType(); if (Util.isEmpty(type)) { user.setType(userDAO.getType()); @@ -219,7 +219,7 @@ public class DefaultUserManager extends AbstractUserManager if (fresh == null) { - throw new NotFoundException(); + throw new NotFoundException(User.class, user.getName()); } fresh.copyProperties(user); @@ -403,7 +403,7 @@ public class DefaultUserManager extends AbstractUserManager User user = get((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal()); if (!user.getPassword().equals(oldPassword)) { - throw new InvalidPasswordException(); + throw new InvalidPasswordException(ContextEntry.ContextBuilder.entity("passwordChange", "-").in(User.class, user.getName())); } user.setPassword(newPassword); @@ -419,10 +419,10 @@ public class DefaultUserManager extends AbstractUserManager public void overwritePassword(String userId, String newPassword) { User user = get(userId); if (user == null) { - throw new NotFoundException(); + throw new NotFoundException(User.class, userId); } if (!isTypeDefault(user)) { - throw new ChangePasswordNotAllowedException(user.getType()); + throw new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("passwordChange", "-").in(User.class, user.getName()), user.getType()); } user.setPassword(newPassword); this.modify(user); diff --git a/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java b/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java index 9773b91cf7..b074781fec 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java @@ -30,6 +30,9 @@ import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Function; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + /** * Collect the plugin translations. @@ -69,7 +72,7 @@ public class I18nServlet extends HttpServlet { createdFile.ifPresent(map -> createdJsonFileConsumer.accept(path, map)); return createdFile.orElse(null); } - )).orElseThrow(NotFoundException::new); + )).orElseThrow(() -> notFound(entity("jsonprovider", path))); } @VisibleForTesting diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java index bc085d2cc7..c6a96e4b9e 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -4,11 +4,11 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpStatus; +import sonia.scm.NotFoundException; import sonia.scm.PushStateDispatcher; import sonia.scm.filter.WebElement; import sonia.scm.repository.DefaultRepositoryProvider; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HttpScmProtocol; @@ -71,8 +71,8 @@ public class HttpProtocolServlet extends HttpServlet { requestProvider.get().setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository()); HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); protocol.serve(req, resp, getServletConfig()); - } catch (RepositoryNotFoundException e) { - log.debug("Repository not found for namespace and name {}", namespaceAndName, e); + } catch (NotFoundException e) { + log.debug(e.getMessage()); resp.setStatus(HttpStatus.SC_NOT_FOUND); } } diff --git a/scm-webapp/src/main/resources/META-INF/validation.xml b/scm-webapp/src/main/resources/META-INF/validation.xml new file mode 100644 index 0000000000..337bbff26e --- /dev/null +++ b/scm-webapp/src/main/resources/META-INF/validation.xml @@ -0,0 +1,9 @@ +<validation-config + xmlns="http://jboss.org/xml/ns/javax/validation/configuration" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration" + version="1.1"> + + <parameter-name-provider>org.hibernate.validator.parameternameprovider.ReflectionParameterNameProvider</parameter-name-provider> + +</validation-config> diff --git a/scm-webapp/src/main/resources/logback.default.xml b/scm-webapp/src/main/resources/logback.default.xml index 33e914d04d..a4e2eed965 100644 --- a/scm-webapp/src/main/resources/logback.default.xml +++ b/scm-webapp/src/main/resources/logback.default.xml @@ -46,7 +46,7 @@ <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder --> <encoder> - <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern> + <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-10X{transaction_id}] %-5level %logger - %msg%n</pattern> </encoder> </appender> @@ -65,9 +65,6 @@ <logger name="sonia.scm.event.LegmanScmEventBus" level="DEBUG" /> <logger name="sonia.scm.plugin.ext.DefaultAnnotationScanner" level="INFO" /> <logger name="sonia.scm.security.ConfigurableLoginAttemptHandler" level="DEBUG" /> - - <!-- event bus --> - <logger name="sonia.scm.event.LegmanScmEventBus" level="INFO" /> <!-- cgi --> <logger name="sonia.scm.web.cgi.DefaultCGIExecutor" level="DEBUG" /> @@ -93,7 +90,9 @@ <logger name="net.sf.ehcache" level="DEBUG" /> --> - <logger name="org.jboss.resteasy" level="DEBUG" /> + <logger name="org.jboss.resteasy" level="INFO" /> + + <logger name="sonia.scm.boot.RestartServlet" level="TRACE" /> <root level="WARN"> <appender-ref ref="STDOUT" /> diff --git a/scm-webapp/src/main/resources/logback.release.xml b/scm-webapp/src/main/resources/logback.release.xml index 4cf5906581..03ac987992 100644 --- a/scm-webapp/src/main/resources/logback.release.xml +++ b/scm-webapp/src/main/resources/logback.release.xml @@ -61,14 +61,14 @@ <append>true</append> <encoder> - <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern> + <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-10X{transaction_id}] %-5level %logger - %msg%n</pattern> </encoder> </appender> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> - <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern> + <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-10X{transaction_id}] %-5level %logger - %msg%n</pattern> </encoder> </appender> diff --git a/scm-webapp/src/main/webapp/WEB-INF/web.xml b/scm-webapp/src/main/webapp/WEB-INF/web.xml index 8abba66aa3..e69eb9fb2a 100644 --- a/scm-webapp/src/main/webapp/WEB-INF/web.xml +++ b/scm-webapp/src/main/webapp/WEB-INF/web.xml @@ -41,10 +41,6 @@ <!-- bootstraping --> - <listener> - <listener-class>sonia.scm.boot.BootstrapContextListener</listener-class> - </listener> - <filter> <filter-name>BootstrapFilter</filter-name> <filter-class>sonia.scm.boot.BootstrapContextFilter</filter-class> @@ -55,25 +51,6 @@ <url-pattern>/*</url-pattern> </filter-mapping> - <!-- rest --> - - <context-param> - <param-name>resteasy.servlet.mapping.prefix</param-name> - <param-value>/api</param-value> - </context-param> - - <servlet> - <servlet-name>Resteasy</servlet-name> - <servlet-class> - org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher - </servlet-class> - </servlet> - - <servlet-mapping> - <servlet-name>Resteasy</servlet-name> - <url-pattern>/api/*</url-pattern> - </servlet-mapping> - <!-- capture sessions --> <!-- TODO remove, we need no longer a session diff --git a/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java b/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java index 126ba9ac0f..cac14bfcf0 100644 --- a/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java +++ b/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java @@ -17,6 +17,7 @@ import java.io.StringWriter; import java.io.Writer; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,6 +46,23 @@ public class TemplatingPushStateDispatcherTest { @Test public void testDispatch() throws IOException { + TemplatingPushStateDispatcher.IndexHtmlModel model = dispatch(); + assertEquals("/scm", model.getContextPath()); + assertNull(model.getLiveReloadURL()); + } + + @Test + public void testDispatchWithLiveReloadURL() throws IOException { + System.setProperty("livereload.url", "/livereload.js"); + try { + TemplatingPushStateDispatcher.IndexHtmlModel model = dispatch(); + assertEquals("/livereload.js", model.getLiveReloadURL()); + } finally { + System.clearProperty("livereload.url"); + } + } + + private TemplatingPushStateDispatcher.IndexHtmlModel dispatch() throws IOException { when(request.getContextPath()).thenReturn("/scm"); when(templateEngine.getTemplate(TemplatingPushStateDispatcher.TEMPLATE)).thenReturn(template); @@ -59,8 +77,7 @@ public class TemplatingPushStateDispatcherTest { verify(template).execute(any(Writer.class), captor.capture()); - TemplatingPushStateDispatcher.IndexHtmlModel model = (TemplatingPushStateDispatcher.IndexHtmlModel) captor.getValue(); - assertEquals("/scm", model.getContextPath()); + return (TemplatingPushStateDispatcher.IndexHtmlModel) captor.getValue(); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java index 4e1a0f90f1..d5c0f91f81 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -9,7 +9,6 @@ import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; import org.assertj.core.util.Lists; import org.jboss.resteasy.core.Dispatcher; -import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.After; @@ -19,7 +18,6 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.NamespaceAndName; @@ -38,7 +36,6 @@ import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -50,7 +47,8 @@ public class ChangesetRootResourceTest extends RepositoryTestBase { public static final String CHANGESET_PATH = "space/repo/changesets/"; public static final String CHANGESET_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + CHANGESET_PATH; - private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private Dispatcher dispatcher; private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -77,16 +75,14 @@ public class ChangesetRootResourceTest extends RepositoryTestBase { @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper); super.changesetRootResource = Providers.of(changesetRootResource); - dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); + dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); - dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); when(repositoryService.getLogCommand()).thenReturn(logCommandBuilder); subjectThreadState.bind(); ThreadContext.bind(subject); @@ -169,7 +165,7 @@ public class ChangesetRootResourceTest extends RepositoryTestBase { when(logCommandBuilder.setStartChangeset(anyString())).thenReturn(logCommandBuilder); when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); MockHttpRequest request = MockHttpRequest - .get(CHANGESET_URL + "id") + .get(CHANGESET_URL + id) .accept(VndMediaType.CHANGESET); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java index 3d898119fb..5deab62e47 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java @@ -8,9 +8,8 @@ import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.PathNotFoundException; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.api.CatCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -58,8 +57,8 @@ public class ContentResourceTest { when(catCommand.setRevision(REV)).thenReturn(catCommand); // defaults for unknown things - doThrow(new RepositoryNotFoundException("x")).when(repositoryServiceFactory).create(not(eq(existingNamespaceAndName))); - doThrow(new PathNotFoundException("x")).when(catCommand).getStream(any()); + doThrow(new NotFoundException("Test", "r")).when(repositoryServiceFactory).create(not(eq(existingNamespaceAndName))); + doThrow(new NotFoundException("Test", "X")).when(catCommand).getStream(any()); } @Test @@ -93,7 +92,7 @@ public class ContentResourceTest { Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go"); assertEquals(200, response.getStatus()); - assertEquals("GO", response.getHeaderString("Language")); + assertEquals("GO", response.getHeaderString("X-Programming-Language")); assertEquals("text/x-go", response.getHeaderString("Content-Type")); } @@ -104,7 +103,7 @@ public class ContentResourceTest { Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile"); assertEquals(200, response.getStatus()); - assertEquals("DOCKERFILE", response.getHeaderString("Language")); + assertEquals("DOCKERFILE", response.getHeaderString("X-Programming-Language")); assertEquals("text/plain", response.getHeaderString("Content-Type")); } @@ -175,7 +174,7 @@ public class ContentResourceTest { } @Override - public void close() throws IOException { + public void close() { closed = true; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java index fe22c944c4..01d5e22b53 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java @@ -17,20 +17,23 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.NotFoundException; import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.api.v2.NotFoundExceptionMapper; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.api.DiffCommandBuilder; +import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; import java.net.URISyntaxException; +import java.util.Arrays; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -62,14 +65,15 @@ public class DiffResourceTest extends RepositoryTestBase { @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { diffRootResource = new DiffRootResource(serviceFactory); super.diffRootResource = Providers.of(diffRootResource); dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); - dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + ExceptionWithContextToErrorDtoMapperImpl mapper = new ExceptionWithContextToErrorDtoMapperImpl(); + dispatcher.getProviderFactory().register(new NotFoundExceptionMapper(mapper)); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(CRLFInjectionExceptionMapper.class); when(service.getDiffCommand()).thenReturn(diffCommandBuilder); @@ -86,19 +90,17 @@ public class DiffResourceTest extends RepositoryTestBase { @Test public void shouldGetDiffs() throws Exception { when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); - when(diffCommandBuilder.retriveContent(any())).thenReturn(diffCommandBuilder); - + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder); MockHttpRequest request = MockHttpRequest .get(DIFF_URL + "revision") .accept(VndMediaType.DIFF); MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); - assertEquals(200, response.getStatus()); - log.info("Response :{}", response.getContentAsString()); + assertThat(response.getStatus()) .isEqualTo(200); - assertThat(response.getContentAsString()) - .isNotNull(); String expectedHeader = "Content-Disposition"; String expectedValue = "attachment; filename=\"repo-revision.diff\"; filename*=utf-8''repo-revision.diff"; assertThat(response.getOutputHeaders().containsKey(expectedHeader)).isTrue(); @@ -107,8 +109,8 @@ public class DiffResourceTest extends RepositoryTestBase { } @Test - public void shouldGet404OnMissingRepository() throws URISyntaxException, RepositoryNotFoundException { - when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(RepositoryNotFoundException.class); + public void shouldGet404OnMissingRepository() throws URISyntaxException { + when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x")); MockHttpRequest request = MockHttpRequest .get(DIFF_URL + "revision") .accept(VndMediaType.DIFF); @@ -120,20 +122,24 @@ public class DiffResourceTest extends RepositoryTestBase { @Test public void shouldGet404OnMissingRevision() throws Exception { when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); - when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class); + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x")); MockHttpRequest request = MockHttpRequest .get(DIFF_URL + "revision") .accept(VndMediaType.DIFF); MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); } @Test public void shouldGet400OnCrlfInjection() throws Exception { when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); - when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class); + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x")); MockHttpRequest request = MockHttpRequest .get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634") @@ -143,6 +149,47 @@ public class DiffResourceTest extends RepositoryTestBase { assertEquals(400, response.getStatus()); } + @Test + public void shouldGet400OnUnknownFormat() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Test", "test")); + MockHttpRequest request = MockHttpRequest + .get(DIFF_URL + "revision?format=Unknown") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(400, response.getStatus()); + } + @Test + public void shouldAcceptDiffFormats() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder); + + Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach( + this::assertRequestOk + ); + } + + private void assertRequestOk(String format) { + MockHttpRequest request = null; + try { + request = MockHttpRequest + .get(DIFF_URL + "revision?format=" + format) + .accept(VndMediaType.DIFF); + } catch (URISyntaxException e) { + e.printStackTrace(); + fail("got exception: " + e); + } + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()) + .withFailMessage("diff format from DiffFormat enum must be accepted: " + format) + .isEqualTo(200); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java index 8a71abb670..9638a8aa49 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java @@ -5,20 +5,22 @@ import org.jboss.resteasy.mock.MockDispatcherFactory; import sonia.scm.api.rest.AlreadyExistsExceptionMapper; import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.api.rest.ConcurrentModificationExceptionMapper; -import sonia.scm.api.rest.IllegalArgumentExceptionMapper; +import sonia.scm.api.v2.NotFoundExceptionMapper; +import sonia.scm.api.v2.NotSupportedFeatureExceptionMapper; public class DispatcherMock { public static Dispatcher createDispatcher(Object resource) { Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); dispatcher.getRegistry().addSingletonResource(resource); - dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(AlreadyExistsExceptionMapper.class); + ExceptionWithContextToErrorDtoMapperImpl mapper = new ExceptionWithContextToErrorDtoMapperImpl(); + dispatcher.getProviderFactory().register(new NotFoundExceptionMapper(mapper)); + dispatcher.getProviderFactory().register(new AlreadyExistsExceptionMapper(mapper)); + dispatcher.getProviderFactory().register(new ConcurrentModificationExceptionMapper(mapper)); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(IllegalArgumentExceptionMapper.class); + dispatcher.getProviderFactory().register(new InternalRepositoryExceptionMapper(mapper)); + dispatcher.getProviderFactory().register(new ChangePasswordNotAllowedExceptionMapper(mapper)); + dispatcher.getProviderFactory().register(new InvalidPasswordExceptionMapper(mapper)); + dispatcher.getProviderFactory().register(new NotSupportedFeatureExceptionMapper(mapper)); return dispatcher; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java index 934e05d0d1..52c9a434c0 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java @@ -8,7 +8,6 @@ import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; import org.assertj.core.util.Lists; import org.jboss.resteasy.core.Dispatcher; -import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.After; @@ -18,15 +17,14 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.ContextEntry; +import sonia.scm.NotFoundException; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Person; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.api.LogCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -52,7 +50,6 @@ public class FileHistoryResourceTest extends RepositoryTestBase { public static final String FILE_HISTORY_PATH = "space/repo/history/"; public static final String FILE_HISTORY_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + FILE_HISTORY_PATH; - private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -73,23 +70,21 @@ public class FileHistoryResourceTest extends RepositoryTestBase { private FileHistoryRootResource fileHistoryRootResource; + private Dispatcher dispatcher; private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); - @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper); super.fileHistoryRootResource = Providers.of(fileHistoryRootResource); - dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); + dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); - dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); + ExceptionWithContextToErrorDtoMapperImpl mapper = new ExceptionWithContextToErrorDtoMapperImpl(); when(service.getLogCommand()).thenReturn(logCommandBuilder); subjectThreadState.bind(); ThreadContext.bind(subject); @@ -133,8 +128,8 @@ public class FileHistoryResourceTest extends RepositoryTestBase { @Test - public void shouldGet404OnMissingRepository() throws URISyntaxException, RepositoryNotFoundException { - when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(RepositoryNotFoundException.class); + public void shouldGet404OnMissingRepository() throws URISyntaxException { + when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x")); MockHttpRequest request = MockHttpRequest .get(FILE_HISTORY_URL + "revision/a.txt") .accept(VndMediaType.CHANGESET_COLLECTION); @@ -152,7 +147,7 @@ public class FileHistoryResourceTest extends RepositoryTestBase { when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); - when(logCommandBuilder.getChangesets()).thenThrow(RevisionNotFoundException.class); + when(logCommandBuilder.getChangesets()).thenThrow(new NotFoundException("Text", "x")); MockHttpRequest request = MockHttpRequest .get(FILE_HISTORY_URL + id + "/" + path) @@ -171,7 +166,7 @@ public class FileHistoryResourceTest extends RepositoryTestBase { when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); - when(logCommandBuilder.getChangesets()).thenThrow(InternalRepositoryException.class); + when(logCommandBuilder.getChangesets()).thenThrow(new InternalRepositoryException(ContextEntry.ContextBuilder.noContext(), "", new RuntimeException())); MockHttpRequest request = MockHttpRequest .get(FILE_HISTORY_URL + id + "/" + path) @@ -182,7 +177,7 @@ public class FileHistoryResourceTest extends RepositoryTestBase { } @Test - public void shouldGet500OnNullChangesets() throws Exception { + public void shouldGet404OnNullChangesets() throws Exception { String id = "revision_123"; String path = "root_dir/sub_dir/file-to-inspect.txt"; @@ -197,6 +192,6 @@ public class FileHistoryResourceTest extends RepositoryTestBase { .accept(VndMediaType.CHANGESET_COLLECTION); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); - assertEquals(500, response.getStatus()); + assertEquals(404, response.getStatus()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index 036579e9dd..f28cf49d03 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -62,7 +62,7 @@ public class GroupRootResourceTest { private ArgumentCaptor<Group> groupCaptor = ArgumentCaptor.forClass(Group.class); @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { initMocks(this); when(groupManager.create(groupCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); doNothing().when(groupManager).modify(groupCaptor.capture()); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index 143b469650..454e24aa92 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -13,6 +13,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import sonia.scm.ContextEntry; import sonia.scm.user.InvalidPasswordException; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -168,7 +169,8 @@ public class MeResourceTest { .content(content.getBytes()); MockHttpResponse response = new MockHttpResponse(); - doThrow(InvalidPasswordException.class).when(userManager).changePasswordForLoggedInUser(any(), any()); + doThrow(new InvalidPasswordException(ContextEntry.ContextBuilder.entity("passwortChange", "-"))) + .when(userManager).changePasswordForLoggedInUser(any(), any()); dispatcher.invoke(request, response); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java index 64942fb84b..fc4598081d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java @@ -8,7 +8,6 @@ import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; import org.assertj.core.util.Lists; import org.jboss.resteasy.core.Dispatcher; -import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.After; @@ -18,7 +17,6 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Modifications; import sonia.scm.repository.NamespaceAndName; @@ -37,6 +35,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static sonia.scm.ContextEntry.ContextBuilder.noContext; @Slf4j @RunWith(MockitoJUnitRunner.Silent.class) @@ -45,7 +44,8 @@ public class ModificationsResourceTest extends RepositoryTestBase { public static final String MODIFICATIONS_PATH = "space/repo/modifications/"; public static final String MODIFICATIONS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + MODIFICATIONS_PATH; - private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private Dispatcher dispatcher; private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -70,16 +70,13 @@ public class ModificationsResourceTest extends RepositoryTestBase { @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { modificationsRootResource = new ModificationsRootResource(serviceFactory, modificationsToDtoMapper); super.modificationsRootResource = Providers.of(modificationsRootResource); - dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); + dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); - dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); - dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); when(repositoryService.getModificationsCommand()).thenReturn(modificationsCommandBuilder); subjectThreadState.bind(); ThreadContext.bind(subject); @@ -107,7 +104,7 @@ public class ModificationsResourceTest extends RepositoryTestBase { @Test public void shouldGet500OnModificationsCommandError() throws Exception { when(modificationsCommandBuilder.revision(any())).thenReturn(modificationsCommandBuilder); - when(modificationsCommandBuilder.getModifications()).thenThrow(InternalRepositoryException.class); + when(modificationsCommandBuilder.getModifications()).thenThrow(new InternalRepositoryException(noContext(), "", new RuntimeException())); MockHttpRequest request = MockHttpRequest .get(MODIFICATIONS_URL + "revision") diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 635d994763..c008e0b8db 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -164,10 +164,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @TestFactory @DisplayName("test endpoints on missing permissions and user is Admin") Stream<DynamicTest> missedPermissionTestFactory() { - Repository mockRepository = mock(Repository.class); - when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); - when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); - when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + Repository mockRepository = new Repository(REPOSITORY_NAME, "git", REPOSITORY_NAMESPACE, REPOSITORY_NAME); when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); return createDynamicTestsToAssertResponses( requestGETPermission.expectedResponseStatus(404), @@ -180,10 +177,6 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @TestFactory @DisplayName("test endpoints on missing permissions and user is not Admin") Stream<DynamicTest> missedPermissionUserForbiddenTestFactory() { - Repository mockRepository = mock(Repository.class); - when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); - when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); - when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); return createDynamicTestsToAssertResponses( requestGETPermission.expectedResponseStatus(403), @@ -409,17 +402,17 @@ public class PermissionRootResourceTest extends RepositoryTestBase { } private Repository createUserWithRepository(String userPermission) { - Repository mockRepository = mock(Repository.class); - when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); - when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); - when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + Repository mockRepository = new Repository(); + mockRepository.setId(REPOSITORY_NAME); + mockRepository.setNamespace(REPOSITORY_NAMESPACE); + mockRepository.setName(REPOSITORY_NAME); when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); when(subject.isPermitted(userPermission != null ? eq(userPermission) : any(String.class))).thenReturn(true); return mockRepository; } private void createUserWithRepositoryAndPermissions(ArrayList<Permission> permissions, String userPermission) { - when(createUserWithRepository(userPermission).getPermissions()).thenReturn(permissions); + createUserWithRepository(userPermission).setPermissions(permissions); } private Stream<DynamicTest> createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) { @@ -427,10 +420,9 @@ public class PermissionRootResourceTest extends RepositoryTestBase { .map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry))); } - private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) throws URISyntaxException { + private void assertExpectedRequest(ExpectedRequest entry) throws URISyntaxException { MockHttpResponse response = new MockHttpResponse(); - HttpRequest request = null; - request = MockHttpRequest + HttpRequest request = MockHttpRequest .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) .content(entry.content) .contentType(VndMediaType.PERMISSION); @@ -442,7 +434,6 @@ public class PermissionRootResourceTest extends RepositoryTestBase { if (entry.responseValidator != null) { entry.responseValidator.accept(response); } - return response; } @ToString @@ -476,12 +467,12 @@ public class PermissionRootResourceTest extends RepositoryTestBase { return this; } - public ExpectedRequest expectedResponseStatus(int expectedResponseStatus) { + ExpectedRequest expectedResponseStatus(int expectedResponseStatus) { this.expectedResponseStatus = expectedResponseStatus; return this; } - public ExpectedRequest responseValidator(Consumer<MockHttpResponse> responseValidator) { + ExpectedRequest responseValidator(Consumer<MockHttpResponse> responseValidator) { this.responseValidator = responseValidator; return this; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 0b2e50b6d9..fe403088f2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -4,6 +4,9 @@ import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; import com.google.inject.util.Providers; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.assertj.core.api.Assertions; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; @@ -22,6 +25,7 @@ import sonia.scm.repository.RepositoryIsNotArchivedException; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.user.User; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; @@ -37,6 +41,7 @@ import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -59,6 +64,8 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; ) public class RepositoryRootResourceTest extends RepositoryTestBase { + private static final String REALM = "AdminRealm"; + private Dispatcher dispatcher; @Rule @@ -96,6 +103,13 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(scmPathInfoStore.get()).thenReturn(uriInfo); when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); + SimplePrincipalCollection trillian = new SimplePrincipalCollection("trillian", REALM); + trillian.add(new User("trillian"), REALM); + shiro.setSubject( + new Subject.Builder() + .principals(trillian) + .authenticated(true) + .buildSubject()); } @Test @@ -257,6 +271,34 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { verify(repositoryManager).create(any(Repository.class)); } + @Test + public void shouldSetCurrentUserAsOwner() throws Exception { + ArgumentCaptor<Repository> createCaptor = ArgumentCaptor.forClass(Repository.class); + when(repositoryManager.create(createCaptor.capture())).thenAnswer(invocation -> { + Repository repository = (Repository) invocation.getArguments()[0]; + repository.setNamespace("otherspace"); + return repository; + }); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repositoryJson = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2) + .contentType(VndMediaType.REPOSITORY) + .content(repositoryJson); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + Assertions.assertThat(createCaptor.getValue().getPermissions()) + .hasSize(1) + .allSatisfy(p -> { + assertThat(p.getName()).isEqualTo("trillian"); + assertThat(p.getType()).isEqualTo(PermissionType.OWNER); + }); + } + @Test public void shouldNotOverwriteExistingPermissionsOnUpdate() throws Exception { Repository existingRepository = mockRepository("space", "repo"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 96b8ac45f7..542c0d017f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -14,7 +14,6 @@ import sonia.scm.NotFoundException; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.api.BrowseCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -60,7 +59,7 @@ public class SourceRootResourceTest extends RepositoryTestBase { } @Test - public void shouldReturnSources() throws URISyntaxException, IOException, NotFoundException { + public void shouldReturnSources() throws URISyntaxException, IOException { BrowserResult result = createBrowserResult(); when(browseCommandBuilder.getBrowserResult()).thenReturn(result); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources"); @@ -74,8 +73,8 @@ public class SourceRootResourceTest extends RepositoryTestBase { } @Test - public void shouldReturn404IfRepoNotFound() throws URISyntaxException, RepositoryNotFoundException { - when(serviceFactory.create(new NamespaceAndName("idont", "exist"))).thenThrow(RepositoryNotFoundException.class); + public void shouldReturn404IfRepoNotFound() throws URISyntaxException { + when(serviceFactory.create(new NamespaceAndName("idont", "exist"))).thenThrow(new NotFoundException("Test", "a")); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "idont/exist/sources"); MockHttpResponse response = new MockHttpResponse(); @@ -84,7 +83,7 @@ public class SourceRootResourceTest extends RepositoryTestBase { } @Test - public void shouldGetResultForSingleFile() throws URISyntaxException, IOException, NotFoundException { + public void shouldGetResultForSingleFile() throws URISyntaxException, IOException { FileObject fileObject = new FileObject(); fileObject.setName("File Object!"); fileObject.setPath("/"); @@ -100,8 +99,8 @@ public class SourceRootResourceTest extends RepositoryTestBase { } @Test - public void shouldGet404ForSingleFileIfRepoNotFound() throws URISyntaxException, RepositoryNotFoundException { - when(serviceFactory.create(new NamespaceAndName("idont", "exist"))).thenThrow(RepositoryNotFoundException.class); + public void shouldGet404ForSingleFileIfRepoNotFound() throws URISyntaxException { + when(serviceFactory.create(new NamespaceAndName("idont", "exist"))).thenThrow(new NotFoundException("Test", "a")); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "idont/exist/sources/revision/fileabc"); MockHttpResponse response = new MockHttpResponse(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java index 5f49f31183..803f2b106c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java @@ -18,6 +18,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.api.v2.NotFoundExceptionMapper; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.Tag; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index b363b66bfb..284e7d1d7b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -14,6 +14,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import sonia.scm.ContextEntry; import sonia.scm.NotFoundException; import sonia.scm.PageResult; import sonia.scm.user.ChangePasswordNotAllowedException; @@ -102,7 +103,7 @@ public class UserRootResourceTest { @Test public void shouldGet400OnCreatingNewUserWithNotAllowedCharacters() throws URISyntaxException { // the @ character at the begin of the name is not allowed - String userJson = "{ \"name\": \"@user\", \"type\": \"db\" }"; + String userJson = "{ \"name\": \"@user\",\"active\": true,\"admin\": false,\"displayName\": \"someone\",\"mail\": \"x@example.com\",\"type\": \"db\" }"; MockHttpRequest request = MockHttpRequest .post("/" + UserRootResource.USERS_PATH_V2) .contentType(VndMediaType.USER) @@ -114,7 +115,7 @@ public class UserRootResourceTest { assertEquals(400, response.getStatus()); // the whitespace at the begin opf the name is not allowed - userJson = "{ \"name\": \" user\", \"type\": \"db\" }"; + userJson = "{ \"name\": \" user\",\"active\": true,\"admin\": false,\"displayName\": \"someone\",\"mail\": \"x@example.com\",\"type\": \"db\" }"; request = MockHttpRequest .post("/" + UserRootResource.USERS_PATH_V2) .contentType(VndMediaType.USER) @@ -167,7 +168,7 @@ public class UserRootResourceTest { .content(content.getBytes()); MockHttpResponse response = new MockHttpResponse(); - doThrow(ChangePasswordNotAllowedException.class).when(userManager).overwritePassword(any(), any()); + doThrow(new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("passwordChange", "-"), "xml")).when(userManager).overwritePassword(any(), any()); dispatcher.invoke(request, response); @@ -185,7 +186,7 @@ public class UserRootResourceTest { .content(content.getBytes()); MockHttpResponse response = new MockHttpResponse(); - doThrow(NotFoundException.class).when(userManager).overwritePassword(any(), any()); + doThrow(new NotFoundException("Test", "x")).when(userManager).overwritePassword(any(), any()); dispatcher.invoke(request, response); diff --git a/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java b/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java new file mode 100644 index 0000000000..b8b538c82b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/RestartServletTest.java @@ -0,0 +1,133 @@ +package sonia.scm.boot; + +import com.github.legman.Subscribe; +import com.google.common.base.Charsets; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.Stage; +import sonia.scm.event.ScmEventBus; +import sonia.scm.event.ScmTestEventBus; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RestartServletTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + private RestartServlet restartServlet; + + private EventListener listener; + + private void setUpObjectUnderTest(Stage stage) { + listener = new EventListener(); + ScmEventBus eventBus = ScmTestEventBus.getInstance(); + eventBus.register(listener); + + restartServlet = new RestartServlet(eventBus, stage); + } + + @Test + public void testRestart() throws IOException { + setUpObjectUnderTest(Stage.DEVELOPMENT); + setRequestInputReason("something changed"); + + restartServlet.doPost(request, response); + + verify(response).setStatus(HttpServletResponse.SC_ACCEPTED); + + RestartEvent restartEvent = listener.restartEvent; + assertThat(restartEvent).isNotNull(); + assertThat(restartEvent.getCause()).isEqualTo(RestartServlet.class); + assertThat(restartEvent.getReason()).isEqualTo("something changed"); + } + + @Test + public void testRestartCalledTwice() throws IOException { + setUpObjectUnderTest(Stage.DEVELOPMENT); + + setRequestInputReason("initial change"); + restartServlet.doPost(request, response); + verify(response).setStatus(HttpServletResponse.SC_ACCEPTED); + + setRequestInputReason("changed again"); + restartServlet.doPost(request, response); + verify(response).setStatus(HttpServletResponse.SC_CONFLICT); + } + + @Test + public void testRestartWithInvalidContent() throws IOException { + setUpObjectUnderTest(Stage.DEVELOPMENT); + + setRequestInputContent("invalid json"); + + restartServlet.doPost(request, response); + + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testRestartInProductionStage() throws IOException { + setUpObjectUnderTest(Stage.PRODUCTION); + + setRequestInputReason("initial change"); + + restartServlet.doPost(request, response); + + verify(response).setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); + } + + private void setRequestInputReason(String message) throws IOException { + String content = createReason(message); + setRequestInputContent(content); + } + + private void setRequestInputContent(String content) throws IOException { + InputStream input = createReasonAsInputStream(content); + when(request.getInputStream()).thenReturn(createServletInputStream(input)); + } + + private ServletInputStream createServletInputStream(final InputStream inputStream) { + return new ServletInputStream() { + @Override + public int read() throws IOException { + return inputStream.read(); + } + }; + } + + private InputStream createReasonAsInputStream(String content) { + return new ByteArrayInputStream(content.getBytes(Charsets.UTF_8)); + } + + private String createReason(String message) { + return String.format("{\"message\": \"%s\"}", message); + } + + public static class EventListener { + + private RestartEvent restartEvent; + + @Subscribe(async = false) + public void store(RestartEvent restartEvent) { + this.restartEvent = restartEvent; + } + + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java b/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java new file mode 100644 index 0000000000..a26cf3b215 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/boot/ServletContextCleanerTest.java @@ -0,0 +1,53 @@ +package sonia.scm.boot; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.servlet.ServletContext; + +import java.util.Collection; +import java.util.Enumeration; +import java.util.Set; +import java.util.Vector; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ServletContextCleanerTest { + + @Mock + private ServletContext servletContext; + + @Test + public void testCleanup() { + Set<String> names = ImmutableSet.of( + "org.jboss.resteasy.Dispatcher", + "resteasy.Deployment", + "sonia.scm.Context", + "org.eclipse.jetty.HttpServer", + "javax.servlet.Context", + "org.apache.shiro.SecurityManager" + ); + + when(servletContext.getAttributeNames()).thenReturn(toEnumeration(names)); + + ServletContextCleaner.cleanup(servletContext); + + verify(servletContext).removeAttribute("org.jboss.resteasy.Dispatcher"); + verify(servletContext).removeAttribute("resteasy.Deployment"); + verify(servletContext).removeAttribute("sonia.scm.Context"); + verify(servletContext, never()).removeAttribute("org.eclipse.jetty.HttpServer"); + verify(servletContext, never()).removeAttribute("javax.servlet.Context"); + verify(servletContext).removeAttribute("org.apache.shiro.SecurityManager"); + } + + private <T> Enumeration<T> toEnumeration(Collection<T> collection) { + return new Vector<>(collection).elements(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java deleted file mode 100644 index b1d7f54ec9..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/filter/AdminSecurityFilterTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - -package sonia.scm.filter; - -import com.github.sdorra.shiro.ShiroRule; -import com.github.sdorra.shiro.SubjectAware; -import org.apache.shiro.SecurityUtils; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Before; -import org.junit.Rule; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.config.ScmConfiguration; - -/** - * Unit tests for {@link AdminSecurityFilter}. - * - * @author Sebastian Sdorra - */ -@RunWith(MockitoJUnitRunner.class) -@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") -public class AdminSecurityFilterTest { - - private AdminSecurityFilter securityFilter; - - @Rule - public ShiroRule shiro = new ShiroRule(); - - /** - * Prepare object under test and mocks. - */ - @Before - public void setUp(){ - this.securityFilter = new AdminSecurityFilter(new ScmConfiguration()); - } - - /** - * Tests {@link AdminSecurityFilter#hasPermission(org.apache.shiro.subject.Subject)} as administrator. - */ - @Test - @SubjectAware(username = "dent", password = "secret") - public void testHasPermissionAsAdministrator() { - assertTrue(securityFilter.hasPermission(SecurityUtils.getSubject())); - } - - /** - * Tests {@link AdminSecurityFilter#hasPermission(org.apache.shiro.subject.Subject)} as user. - */ - @Test - @SubjectAware(username = "trillian", password = "secret") - public void testHasPermissionAsUser() { - assertFalse(securityFilter.hasPermission(SecurityUtils.getSubject())); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java index ac1b7335fc..efd3dbbbc0 100644 --- a/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java @@ -98,9 +98,10 @@ public class MDCFilterTest extends AbstractTestBase { assertNotNull(chain.ctx); assertEquals("trillian", chain.ctx.get(MDCFilter.MDC_USERNAME)); assertEquals("api/v1/repositories", chain.ctx.get(MDCFilter.MDC_REQUEST_URI)); - assertEquals("127.0.0.1", chain.ctx.get(MDCFilter.MDC_CLIEN_IP)); - assertEquals("localhost", chain.ctx.get(MDCFilter.MDC_CLIEN_HOST)); + assertEquals("127.0.0.1", chain.ctx.get(MDCFilter.MDC_CLIENT_IP)); + assertEquals("localhost", chain.ctx.get(MDCFilter.MDC_CLIENT_HOST)); assertEquals("GET", chain.ctx.get(MDCFilter.MDC_REQUEST_METHOD)); + assertNotNull(chain.ctx.get(MDCFilter.MDC_TRANSACTION_ID)); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/PropagatePrincipleFilterTest.java similarity index 60% rename from scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java rename to scm-webapp/src/test/java/sonia/scm/filter/PropagatePrincipleFilterTest.java index 50aa6980f6..0f2146beab 100644 --- a/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/filter/PropagatePrincipleFilterTest.java @@ -33,38 +33,38 @@ package sonia.scm.filter; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; -import java.io.IOException; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; -import org.junit.Test; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; import org.junit.Before; import org.junit.Rule; +import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.Mockito; -import static org.mockito.Mockito.*; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.user.User; import sonia.scm.user.UserTestData; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verify; + /** - * Unit tests for {@link SecurityFilter}. + * Unit tests for {@link PropagatePrincipleFilter}. * * @author Sebastian Sdorra */ @RunWith(MockitoJUnitRunner.class) @SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") -public class SecurityFilterTest { +public class PropagatePrincipleFilterTest { @Mock private HttpServletRequest request; @@ -83,7 +83,7 @@ public class SecurityFilterTest { private ScmConfiguration configuration; - private SecurityFilter securityFilter; + private PropagatePrincipleFilter propagatePrincipleFilter; @Rule public ShiroRule shiro = new ShiroRule(); @@ -94,38 +94,7 @@ public class SecurityFilterTest { @Before public void setUp(){ this.configuration = new ScmConfiguration(); - this.securityFilter = new SecurityFilter(configuration); - - when(request.getContextPath()).thenReturn("/scm"); - } - - /** - * Tests filter on authentication endpoint v1. - * - * @throws IOException - * @throws ServletException - */ - @Test - public void testDoOnAuthenticationUrlV1() throws IOException, ServletException { - checkIfAuthenticationUrlIsPassedThrough("/scm/api/auth/access_token"); - } - - /** - * Tests filter on authentication endpoint v2. - * - * @throws IOException - * @throws ServletException - */ - @Test - public void testDoOnAuthenticationUrlV2() throws IOException, ServletException { - checkIfAuthenticationUrlIsPassedThrough("/scm/api/v2/auth/access_token"); - } - - private void checkIfAuthenticationUrlIsPassedThrough(String uri) throws IOException, ServletException { - when(request.getRequestURI()).thenReturn(uri); - securityFilter.doFilter(request, response, chain); - verify(request, never()).setAttribute(Mockito.anyString(), Mockito.any()); - verify(chain).doFilter(request, response); + this.propagatePrincipleFilter = new PropagatePrincipleFilter(configuration); } /** @@ -136,8 +105,7 @@ public class SecurityFilterTest { */ @Test public void testAnonymous() throws IOException, ServletException { - when(request.getRequestURI()).thenReturn("/scm/api"); - securityFilter.doFilter(request, response, chain); + propagatePrincipleFilter.doFilter(request, response, chain); response.sendError(HttpServletResponse.SC_FORBIDDEN); } @@ -149,14 +117,13 @@ public class SecurityFilterTest { */ @Test public void testAnonymousWithAccessEnabled() throws IOException, ServletException { - when(request.getRequestURI()).thenReturn("/scm/api"); configuration.setAnonymousAccessEnabled(true); // execute - securityFilter.doFilter(request, response, chain); + propagatePrincipleFilter.doFilter(request, response, chain); // verify and capture - verify(request).setAttribute(SecurityFilter.ATTRIBUTE_REMOTE_USER, SCMContext.USER_ANONYMOUS); + verify(request).setAttribute(PropagatePrincipleFilter.ATTRIBUTE_REMOTE_USER, SCMContext.USER_ANONYMOUS); verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture()); // assert @@ -173,13 +140,12 @@ public class SecurityFilterTest { @Test public void testAuthenticated() throws IOException, ServletException { authenticateUser(UserTestData.createTrillian()); - when(request.getRequestURI()).thenReturn("/scm/api"); - + // execute - securityFilter.doFilter(request, response, chain); + propagatePrincipleFilter.doFilter(request, response, chain); // verify and capture - verify(request).setAttribute(SecurityFilter.ATTRIBUTE_REMOTE_USER, "trillian"); + verify(request).setAttribute(PropagatePrincipleFilter.ATTRIBUTE_REMOTE_USER, "trillian"); verify(chain).doFilter(requestCaptor.capture(), responseCaptor.capture()); // assert @@ -187,42 +153,6 @@ public class SecurityFilterTest { assertEquals("trillian", captured.getRemoteUser()); } - /** - * Tests filter without permissions. - * - * @throws IOException - * @throws ServletException - */ - @Test - public void testForbidden() throws IOException, ServletException { - authenticateUser(UserTestData.createTrillian()); - when(request.getRequestURI()).thenReturn("/scm/api"); - - // execute - securityFilter = new AccessForbiddenSecurityFilter(configuration); - securityFilter.doFilter(request, response, chain); - - // assert - verify(response).sendError(HttpServletResponse.SC_FORBIDDEN); - } - - /** - * Tests filter unauthenticated and without permissions. - * - * @throws IOException - * @throws ServletException - */ - @Test - public void testUnauthorized() throws IOException, ServletException { - when(request.getRequestURI()).thenReturn("/scm/api"); - - // execute - securityFilter = new AccessForbiddenSecurityFilter(configuration); - securityFilter.doFilter(request, response, chain); - - // assert - verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); - } private void authenticateUser(User user) { SimplePrincipalCollection spc = new SimplePrincipalCollection(); @@ -236,18 +166,4 @@ public class SecurityFilterTest { shiro.setSubject(subject); } - - private static class AccessForbiddenSecurityFilter extends SecurityFilter { - - private AccessForbiddenSecurityFilter(ScmConfiguration configuration) { - super(configuration); - } - - @Override - protected boolean hasPermission(Subject subject) { - return false; - } - - } - } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java index 42b038ab17..9e5ebccbfd 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java @@ -42,6 +42,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.Stage; import javax.servlet.ServletContext; import java.io.File; @@ -96,14 +97,12 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase * Method description * * - * @throws MalformedURLException */ @Test - public void testGetResourceFromCache() throws MalformedURLException - { + public void testGetResourceFromCache() { DefaultUberWebResourceLoader resourceLoader = new DefaultUberWebResourceLoader(servletContext, - new ArrayList<PluginWrapper>()); + new ArrayList<PluginWrapper>(), Stage.PRODUCTION); resourceLoader.getCache().put("/myresource", GITHUB); @@ -112,6 +111,15 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase assertSame(GITHUB, resource); } + @Test + public void testGetResourceCacheIsDisableInStageDevelopment() throws MalformedURLException { + DefaultUberWebResourceLoader resourceLoader = new DefaultUberWebResourceLoader(servletContext, new ArrayList<>(), Stage.DEVELOPMENT); + when(servletContext.getResource("/scm")).thenAnswer(invocation -> new URL("https://scm-manager.org")); + URL url = resourceLoader.getResource("/scm"); + URL secondUrl = resourceLoader.getResource("/scm"); + assertNotSame(url, secondUrl); + } + /** * Method description * diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index 448c2561f3..9d03fa02ca 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -184,7 +184,7 @@ private long calculateAverage(List<Long> times) { private Repository createTestRepository(int number) { Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number); - repository.getPermissions().add(new Permission("trillian", PermissionType.READ)); + repository.addPermission(new Permission("trillian", PermissionType.READ)); return repository; } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index dd467752d9..ab7e3aafd9 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -111,7 +111,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { private String mockedNamespace = "default_namespace"; @Test - public void testCreate() throws AlreadyExistsException { + public void testCreate() { Repository heartOfGold = createTestRepository(); Repository dbRepo = manager.get(heartOfGold.getId()); @@ -123,18 +123,18 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { username = "unpriv" ) @Test(expected = UnauthorizedException.class) - public void testCreateWithoutPrivileges() throws AlreadyExistsException { + public void testCreateWithoutPrivileges() { createTestRepository(); } @Test(expected = AlreadyExistsException.class) - public void testCreateExisting() throws AlreadyExistsException { + public void testCreateExisting() { createTestRepository(); createTestRepository(); } @Test - public void testDelete() throws Exception { + public void testDelete() { delete(manager, createTestRepository()); } @@ -142,12 +142,12 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { username = "unpriv" ) @Test(expected = UnauthorizedException.class) - public void testDeleteWithoutPrivileges() throws Exception { + public void testDeleteWithoutPrivileges() { delete(manager, createTestRepository()); } @Test(expected = RepositoryIsNotArchivedException.class) - public void testDeleteNonArchived() throws Exception { + public void testDeleteNonArchived() { configuration.setEnableRepositoryArchive(true); delete(manager, createTestRepository()); } @@ -158,7 +158,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { } @Test - public void testDeleteWithEnabledArchive() throws Exception { + public void testDeleteWithEnabledArchive() { Repository repository = createTestRepository(); repository.setArchived(true); @@ -168,7 +168,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { } @Test - public void testGet() throws AlreadyExistsException { + public void testGet() { Repository heartOfGold = createTestRepository(); String id = heartOfGold.getId(); String description = heartOfGold.getDescription(); @@ -186,7 +186,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { @SubjectAware( username = "crato" ) - public void testGetWithoutRequiredPrivileges() throws AlreadyExistsException { + public void testGetWithoutRequiredPrivileges() { Repository heartOfGold = RepositoryTestData.createHeartOfGold(); manager.create(heartOfGold); @@ -195,7 +195,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { } @Test - public void testGetAll() throws AlreadyExistsException { + public void testGetAll() { Repository heartOfGold = createTestRepository(); Repository happyVerticalPeopleTransporter = createSecondTestRepository(); boolean foundHeart = false; @@ -233,7 +233,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { @Test @SuppressWarnings("unchecked") @SubjectAware(username = "dent") - public void testGetAllWithPermissionsForTwoOrThreeRepos() throws AlreadyExistsException { + public void testGetAllWithPermissionsForTwoOrThreeRepos() { // mock key generator KeyGenerator keyGenerator = mock(KeyGenerator.class); Stack<String> keys = new Stack<>(); @@ -274,7 +274,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { } @Test - public void testEvents() throws Exception { + public void testEvents() { RepositoryManager repoManager = createRepositoryManager(false); repoManager.init(contextProvider); TestListener listener = new TestListener(); @@ -305,7 +305,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { } @Test - public void testModify() throws AlreadyExistsException { + public void testModify() { Repository heartOfGold = createTestRepository(); heartOfGold.setDescription("prototype ship"); @@ -319,7 +319,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { @Test @SubjectAware(username = "crato") - public void testModifyWithoutRequiredPermissions() throws AlreadyExistsException, NotFoundException { + public void testModifyWithoutRequiredPermissions() { Repository heartOfGold = RepositoryTestData.createHeartOfGold(); manager.create(heartOfGold); heartOfGold.setDescription("prototype ship"); @@ -334,7 +334,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { } @Test - public void testRefresh() throws AlreadyExistsException { + public void testRefresh() { Repository heartOfGold = createTestRepository(); String description = heartOfGold.getDescription(); @@ -345,7 +345,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { @Test @SubjectAware(username = "crato") - public void testRefreshWithoutRequiredPermissions() throws AlreadyExistsException, NotFoundException { + public void testRefreshWithoutRequiredPermissions() { Repository heartOfGold = RepositoryTestData.createHeartOfGold(); manager.create(heartOfGold); heartOfGold.setDescription("prototype ship"); @@ -354,13 +354,13 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { manager.refresh(heartOfGold); } - @Test(expected = RepositoryNotFoundException.class) + @Test(expected = NotFoundException.class) public void testRefreshNotFound(){ manager.refresh(createRepositoryWithId()); } @Test - public void testRepositoryHook() throws AlreadyExistsException { + public void testRepositoryHook() { CountingReceiveHook hook = new CountingReceiveHook(); RepositoryManager repoManager = createRepositoryManager(false); @@ -380,23 +380,23 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { } @Test - public void testNamespaceSet() throws Exception { + public void testNamespaceSet() { RepositoryManager repoManager = createRepositoryManager(false); Repository repository = spy(createTestRepository()); repository.setName("Testrepo"); - ((DefaultRepositoryManager) repoManager).create(repository); + repoManager.create(repository); assertEquals("default_namespace", repository.getNamespace()); } @Test - public void shouldSetNamespace() throws AlreadyExistsException { + public void shouldSetNamespace() { Repository repository = new Repository(null, "hg", null, "scm"); manager.create(repository); assertNotNull(repository.getId()); assertNotNull(repository.getNamespace()); } - private void createUriTestRepositories(RepositoryManager m) throws AlreadyExistsException { + private void createUriTestRepositories(RepositoryManager m) { mockedNamespace = "namespace"; createRepository(m, new Repository("1", "hg", "namespace", "scm")); createRepository(m, new Repository("2", "hg", "namespace", "scm-test")); @@ -452,7 +452,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { keyGenerator, repositoryDAO, handlerSet, namespaceStrategy); } - private void createRepository(RepositoryManager m, Repository repository) throws AlreadyExistsException { + private void createRepository(RepositoryManager m, Repository repository) { m.create(repository); } @@ -475,7 +475,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { assertEquals(repo.getLastModified(), other.getLastModified()); } - private Repository createRepository(Repository repository) throws AlreadyExistsException { + private Repository createRepository(Repository repository) { manager.create(repository); assertNotNull(repository.getId()); assertNotNull(manager.get(repository.getId())); @@ -490,12 +490,12 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> { return repository; } - private Repository createSecondTestRepository() throws AlreadyExistsException { + private Repository createSecondTestRepository() { return createRepository( RepositoryTestData.createHappyVerticalPeopleTransporter()); } - private Repository createTestRepository() throws AlreadyExistsException { + private Repository createTestRepository() { return createRepository(RepositoryTestData.createHeartOfGold()); } diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestFilterTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestFilterTest.java new file mode 100644 index 0000000000..8f0223753d --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestFilterTest.java @@ -0,0 +1,63 @@ +package sonia.scm.security; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.authc.AuthenticationException; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ResourceInfo; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") +public class SecurityRequestFilterTest { + + @Rule + public ShiroRule shiroRule = new ShiroRule(); + + @Mock + private ResourceInfo resourceInfo; + @Mock + private ContainerRequestContext context; + @InjectMocks + private SecurityRequestFilter securityRequestFilter; + + @Test + public void shouldAllowUnauthenticatedAccessForAnnotatedMethod() throws NoSuchMethodException { + when(resourceInfo.getResourceMethod()).thenReturn(SecurityTestClass.class.getMethod("anonymousAccessAllowed")); + + securityRequestFilter.filter(context); + } + + @Test(expected = AuthenticationException.class) + public void shouldRejectUnauthenticatedAccessForAnnotatedMethod() throws NoSuchMethodException { + when(resourceInfo.getResourceMethod()).thenReturn(SecurityTestClass.class.getMethod("loginRequired")); + + securityRequestFilter.filter(context); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldAllowAuthenticatedAccessForMethodWithoutAnnotation() throws NoSuchMethodException { + when(resourceInfo.getResourceMethod()).thenReturn(SecurityTestClass.class.getMethod("loginRequired")); + + securityRequestFilter.filter(context); + } + + private static class SecurityTestClass { + @AllowAnonymousAccess + public void anonymousAccessAllowed() { + } + + public void loginRequired() { + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/AuthenticationITCase.java b/scm-webapp/src/test/java/sonia/scm/selenium/AuthenticationITCase.java deleted file mode 100644 index 34013e7e76..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/AuthenticationITCase.java +++ /dev/null @@ -1,60 +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.selenium; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.selenium.page.Pages; -import sonia.scm.selenium.page.MainPage; -import sonia.scm.selenium.page.LoginPage; -import static org.junit.Assert.*; -import org.junit.Test; - -/** - * Authentication related selenium integration tests. - * - * @author Sebastian Sdorra - */ -public class AuthenticationITCase extends SeleniumITCaseBase { - - /** - * Authenticates an user and call logout function. - */ - @Test - public void testAuthentication() { - MainPage main = Pages.get(driver, LoginPage.class).login("scmadmin", "scmadmin"); - assertEquals("scmadmin", main.getUserInfo()); - main.logout(); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/RepositoriesITCase.java b/scm-webapp/src/test/java/sonia/scm/selenium/RepositoriesITCase.java deleted file mode 100644 index 05898f6529..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/RepositoriesITCase.java +++ /dev/null @@ -1,88 +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.selenium; - -//~--- non-JDK imports -------------------------------------------------------- - -import sonia.scm.selenium.page.Pages; -import sonia.scm.selenium.page.MainPage; -import sonia.scm.selenium.page.LoginPage; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import sonia.scm.repository.Repository; - -/** - * Repository related selenium integration tests. - * - * @author Sebastian Sdorra - */ -public class RepositoriesITCase extends SeleniumITCaseBase { - - private MainPage main; - - /** - * Authenticates admin user, before each test. - */ - @Before - public void login() { - main = Pages.get(driver, LoginPage.class) - .login("scmadmin", "scmadmin"); - } - - /** - * Creates, select and removes a repository. - */ - @Test - public void createRepository() { - Repository repository = new Repository(); - repository.setName("scm"); - repository.setType("git"); - repository.setContact("scmadmin@scm-manager.org"); - repository.setDescription("SCM-Manager"); - - main.repositories() - .add(repository) - .select(repository.getName()) - .remove(); - } - - /** - * Logs the user out, after each test. - */ - @After - public void logout() { - main.logout(); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumITCaseBase.java b/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumITCaseBase.java deleted file mode 100644 index fde04d9cad..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/SeleniumITCaseBase.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ -package sonia.scm.selenium; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.firefox.FirefoxDriver; - -/** - * Base class for selenium integration tests. - * - * @author Sebastian Sdorra - */ -public class SeleniumITCaseBase { - - /** - * Selenium test driver. - */ - protected static WebDriver driver; - - /** - * Setup selenium test driver. - */ - @BeforeClass - public static void setUpDriver() { - // DesiredCapabilities capa = DesiredCapabilities.chrome(); - // capa.setBrowserName("firefox"); - // capa.setPlatform(Platform.ANY); - // RemoteWebDriver driver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), capa); - - driver = new FirefoxDriver(); - driver.get("http://localhost:8082/scm/index.html"); - } - - /** - * Closes the selenium test driver. - */ - @AfterClass - public static void tearDownDriver() { - driver.close(); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/BasePage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/BasePage.java deleted file mode 100644 index 28c27457a8..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/page/BasePage.java +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ -package sonia.scm.selenium.page; - -import com.google.common.base.Throwables; -import com.google.common.io.Files; -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.openqa.selenium.By; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; - -/** - * Abstract selenium base page. - * - * @author Sebastian Sdorra - * - * @param <P> concrete page implementation - */ -public abstract class BasePage<P extends BasePage> { - - /** - * Selenium test driver. - */ - protected final WebDriver driver; - - /** - * Constructs a new base page. - * - * @param driver selenium test driver - */ - protected BasePage(WebDriver driver) { - this.driver = driver; - } - - /** - * Performs a {@link Thread#sleep(long)} for the given timeout. - * - * @param time timeout - * @param unit time unit of timeout - */ - protected void sleep(long time, TimeUnit unit) { - try { - unit.sleep(time); - } catch (InterruptedException ex) { - throw Throwables.propagate(ex); - } - } - - /** - * Wait for the element until it is clickable. - * - * @param by element selector - * - * @return web element - */ - protected WebElement waitToBeClickable(By by){ - return waitToBeClickable(driver.findElement(by)); - } - - /** - * Waits for the element until it is clickable. - * - * @param element web element - * - * @return web element - */ - protected WebElement waitToBeClickable(WebElement element) { - WebDriverWait wait = new WebDriverWait(driver, 5); - - return wait.until(ExpectedConditions.elementToBeClickable(element)); - } - - /** - * Waits until the element is present. - * - * @param by element locator - * - * @return web element - */ - protected WebElement waitFor(By by){ - WebDriverWait wait = new WebDriverWait(driver, 1); - return wait.until(ExpectedConditions.presenceOfElementLocated(by)); - } - - /** - * Waits until the elements are present. - * - * @param by element selector - * - * @return list of web elements - */ - protected List<WebElement> waitForAll(By by){ - WebDriverWait wait = new WebDriverWait(driver, 1); - return wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(by)); - } - - /** - * Creates a screenshot of the current browser content and stores it at the given path. - * - * @param target target file path - * - * @return {@code this} - */ - public P screenshot(String target) { - return screenshot(new File(target)); - } - - /** - * Creates a screenshot of the current browser content and stores it at the file. - * - * @param target target file - * - * @return {@code this} - */ - public P screenshot(File target) { - try { - File scrFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); - - Files.copy(scrFile, target); - } catch (IOException ex) { - throw Throwables.propagate(ex); - } - return self(); - } - - /** - * Returns {@code this}. - * - * @return {@code this} - */ - protected abstract P self(); - -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/LoginPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/LoginPage.java deleted file mode 100644 index cf9d231510..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/page/LoginPage.java +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ -package sonia.scm.selenium.page; - -import java.util.concurrent.TimeUnit; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; - -/** - * Page object for the scm-manager login page. - * - * @author Sebastian Sdorra - */ -public class LoginPage extends BasePage<LoginPage> { - - @FindBy(css = "input[name=username]") - private WebElement usernameInput; - - @FindBy(css = "input[name=password]") - private WebElement passwordInput; - - @FindBy(css = "#loginButton button") - private WebElement loginButton; - - /** - * Constructs a new page. This constructor should only be called from {@link Pages}. - * - * @param driver selenium test driver - */ - LoginPage(WebDriver driver) { - super(driver); - } - - @Override - protected LoginPage self() { - return this; - } - - /** - * Authenticates the user and returns the {@link MainPage}. - * - * @param username username - * @param password password - * - * @return {@link MainPage} after successful authentication - */ - public MainPage login(String username, String password) { - usernameInput.clear(); - usernameInput.sendKeys(username); - - passwordInput.clear(); - passwordInput.sendKeys(password); - - sleep(250, TimeUnit.MILLISECONDS); - - waitToBeClickable(loginButton).click(); - - return Pages.get(driver, MainPage.class); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/MainPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/MainPage.java deleted file mode 100644 index 42fc807e67..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/page/MainPage.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ -package sonia.scm.selenium.page; - -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; - -/** - * Page object for scm-manager's main page. - * - * @author Sebastian Sdorra - */ -public class MainPage extends BasePage<MainPage> { - - @FindBy(css = "#navLogout a") - private WebElement logoutLink; - - @FindBy(linkText = "Repositories") - private WebElement repositoriesLink; - - @FindBy(css = "#scm-userinfo-tip") - private WebElement userInfoTip; - - /** - * Constructs a new page. This constructor should only be called from {@link Pages}. - * - * @param driver selenium test driver - */ - MainPage(WebDriver driver) { - super(driver); - } - - @Override - protected MainPage self() { - return this; - } - - /** - * Returns the name of the current authenticated user from the user info tip. - * - * @return name of the current authenticated user - */ - public String getUserInfo(){ - return userInfoTip.getText(); - } - - /** - * Navigates to the repositories page and returns the page object for this page. - * - * @return page object for repositories page - */ - public RepositoriesPage repositories(){ - repositoriesLink.click(); - return Pages.get(driver, RepositoriesPage.class); - } - - /** - * Logs the current user out. - * - * @return page object for the login - */ - public LoginPage logout(){ - waitToBeClickable(logoutLink).click(); - return Pages.get(driver, LoginPage.class); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/Pages.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/Pages.java deleted file mode 100644 index b8d85ea08b..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/page/Pages.java +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ -package sonia.scm.selenium.page; - -import com.google.common.base.Throwables; -import com.google.common.collect.Lists; -import java.lang.reflect.Constructor; -import java.util.Arrays; -import java.util.List; -import org.openqa.selenium.By; -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.PageFactory; -import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; - -/** - * Helper class for selenium page objects. - * - * @author Sebastian Sdorra - */ -public final class Pages { - - private Pages() { - } - - /** - * Creates an instance of the given page object. - * - * @param <T> page object type - * @param driver selenium driver - * @param clazz page object type - * @param otherArguments other constructor arguments - * - * @return instance of page object - */ - public static <T extends BasePage> T get(WebDriver driver, Class<T> clazz, Object... otherArguments) - { - T page = null; - try { - List<Class<?>> argumentTypes = Lists.newArrayList(); - argumentTypes.add(WebDriver.class); - for (Object argument : otherArguments) { - argumentTypes.add(argument.getClass()); - } - - List<Object> arguments = Lists.newArrayList(); - arguments.add(driver); - arguments.addAll(Arrays.asList(otherArguments)); - - Constructor<T> constructor = clazz.getDeclaredConstructor( - argumentTypes.toArray(new Class<?>[argumentTypes.size()]) - ); - page = constructor.newInstance(arguments.toArray(new Object[arguments.size()])); - - PageFactory.initElements(new DefaultElementLocatorFactory(new WaitingSearchContext(driver)), page); - } catch (Exception ex) { - throw Throwables.propagate(ex); - } - return page; - } - - private static class WaitingSearchContext implements SearchContext { - - private final WebDriver driver; - private final WebDriverWait wait; - - private WaitingSearchContext(WebDriver driver) { - this.driver = driver; - this.wait = new WebDriverWait(driver, 1); - } - - @Override - public List<WebElement> findElements(By by) { - return wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(by)); - } - - @Override - public WebElement findElement(By by) { - return wait.until(ExpectedConditions.presenceOfElementLocated(by)); - } - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesAddPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesAddPage.java deleted file mode 100644 index a6b6798b7f..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesAddPage.java +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ -package sonia.scm.selenium.page; - -import com.google.common.base.MoreObjects; -import org.openqa.selenium.By; -import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.NotFoundException; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.ui.WebDriverWait; -import sonia.scm.repository.Repository; - -import java.util.List; - -/** - * Page object for scm-manager's repository creation page. - * - * @author Sebastian Sdorra - */ -public class RepositoriesAddPage extends BasePage<RepositoriesAddPage> { - - @FindBy(css = "input[name=name]") - private WebElement nameInput; - - @FindBy(css = "input[name=contact]") - private WebElement contactInput; - - @FindBy(css = "#x-form-el-repositoryType img") - private WebElement typeInput; - - @FindBy(css = "textarea[name=description]") - private WebElement descriptionInput; - - @FindBy(css = "div.x-panel-btns button:nth-of-type(1)") - private WebElement okButton; - - private final RepositoriesPage repositoriesPage; - - /** - * Constructs a new page. This constructor should only be called from {@link Pages}. - * - * @param driver selenium test driver - * @param repositoriesPage repositories page object - */ - RepositoriesAddPage(WebDriver driver, RepositoriesPage repositoriesPage) { - super(driver); - this.repositoriesPage = repositoriesPage; - } - - @Override - protected RepositoriesAddPage self() { - return this; - } - - /** - * Creates a new {@link Repository}. - * - * @param repository repository for creation - * - * @return repositories overview page - */ - public RepositoriesPage add(Repository repository) { - nameInput.sendKeys(repository.getName()); - - selectType(repository.getType()); - - contactInput.sendKeys(repository.getContact()); - descriptionInput.sendKeys(repository.getDescription()); - - waitToBeClickable(okButton).click(); - - return repositoriesPage; - } - - private void selectType(String type) { - typeInput.click(); - - String displayName = findDisplayName(type); - - WebDriverWait wait = new WebDriverWait(driver, 1); - List<WebElement> elements = waitForAll(By.className("x-combo-list-item")); - WebElement typeElement = null; - for (WebElement te : elements){ - if (te.getText().trim().equalsIgnoreCase(displayName)){ - typeElement = te; - break; - } - } - - if (typeElement == null){ - throw new NotFoundException("could not find type element with type " + displayName); - } - - typeElement.click(); - } - - private String findDisplayName(String type) { - String displayName = null; - if (driver instanceof JavascriptExecutor) { - // TODO seams not to work - String script = "Sonia.repository.getTypeByName('" + type + "').displayName;"; - displayName = (String) ((JavascriptExecutor)driver).executeScript(script); - } - return MoreObjects.firstNonNull(displayName, type); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesPage.java deleted file mode 100644 index fef75683ff..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoriesPage.java +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ -package sonia.scm.selenium.page; - -import java.util.List; -import java.util.Locale; -import org.openqa.selenium.By; -import org.openqa.selenium.NotFoundException; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import sonia.scm.repository.Repository; - -/** - * Page object for scm-manager's repositories overview page. - * - * @author Sebastian Sdorra - */ -public class RepositoriesPage extends BasePage<RepositoriesPage> { - - @FindBy(id = "repositoryAddButton") - private WebElement addButton; - - /** - * Constructs a new page. This constructor should only be called from {@link Pages}. - * - * @param driver selenium test driver - */ - RepositoriesPage(WebDriver driver) { - super(driver); - } - - @Override - protected RepositoriesPage self() { - return this; - } - - /** - * Creates a new {@link Repository}. - * - * @param repository repository for creation - * - * @return {@link this} - */ - public RepositoriesPage add(Repository repository){ - addButton.click(); - RepositoriesAddPage addPage = Pages.get(driver, RepositoriesAddPage.class, this); - return addPage.add(repository); - } - - /** - * Selects the repository with the given name and returns the detail page object for the selected repository. - * - * @param repositoryName name of the repository - * - * @return page object for selected repository - */ - public RepositoryPage select(String repositoryName){ - WebElement repositoryNameColumn = null; - - List<WebElement> elements = waitForAll(By.className("x-grid3-col-name")); - for (WebElement element : elements){ - if (element.getText().trim().toLowerCase(Locale.ENGLISH).equals(repositoryName)){ - repositoryNameColumn = element; - break; - } - } - - if ( repositoryNameColumn == null ) { - throw new NotFoundException("could not find repository " + repositoryName); - } - - return Pages.get(driver, RepositoryPage.class, this); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoryPage.java b/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoryPage.java deleted file mode 100644 index d09a0debb5..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/selenium/page/RepositoryPage.java +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2014, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ -package sonia.scm.selenium.page; - -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; - -/** - * Page object for scm-manager's repository detail page. - * - * @author Sebastian Sdorra - */ -public class RepositoryPage extends BasePage<RepositoryPage> { - - @FindBy(css = "#repoRmButton button") - private WebElement removeButton; - - private final RepositoriesPage repositoriesPage; - - /** - * Constructs a new page. This constructor should only be called from {@link Pages}. - * - * @param driver selenium test driver - * @param repositoriesPage repositories page object - */ - RepositoryPage(WebDriver driver, RepositoriesPage repositoriesPage) { - super(driver); - this.repositoriesPage = repositoriesPage; - } - - @Override - protected RepositoryPage self() { - return this; - } - - /** - * Removes the selected repository. - * - * @return repositories overview page object - */ - public RepositoriesPage remove(){ - removeButton.click(); - waitToBeClickable(By.cssSelector("div.x-window button:nth-of-type(1)")).click(); - return repositoriesPage; - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java index df32528e5e..1614bf790a 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/DefaultUserManagerTest.java @@ -40,7 +40,6 @@ import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.Lists; import org.assertj.core.api.Assertions; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -48,7 +47,6 @@ import org.mockito.ArgumentCaptor; import sonia.scm.NotFoundException; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.user.xml.XmlUserDAO; -import sonia.scm.util.MockUtil; import static org.mockito.Mockito.*; @@ -57,7 +55,6 @@ import static org.mockito.Mockito.*; import java.util.Collections; import java.util.List; import org.junit.Rule; -import sonia.scm.store.ConfigurationStoreFactory; /** * diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java index 077020f60c..1bd6358c95 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java @@ -4,11 +4,11 @@ import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import sonia.scm.NotFoundException; import sonia.scm.PushStateDispatcher; import sonia.scm.repository.DefaultRepositoryProvider; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HttpScmProtocol; @@ -58,12 +58,12 @@ public class HttpProtocolServletTest { private HttpScmProtocol protocol; @Before - public void init() throws RepositoryNotFoundException { + public void init() { initMocks(this); when(userAgentParser.parse(request)).thenReturn(userAgent); when(userAgent.isBrowser()).thenReturn(false); NamespaceAndName existingRepo = new NamespaceAndName("space", "repo"); - when(serviceFactory.create(not(eq(existingRepo)))).thenThrow(RepositoryNotFoundException.class); + when(serviceFactory.create(not(eq(existingRepo)))).thenThrow(new NotFoundException("Test", "a")); when(serviceFactory.create(existingRepo)).thenReturn(repositoryService); when(requestProvider.get()).thenReturn(httpServletRequest); } @@ -97,7 +97,7 @@ public class HttpProtocolServletTest { } @Test - public void shouldDelegateToProvider() throws RepositoryNotFoundException, IOException, ServletException { + public void shouldDelegateToProvider() throws IOException, ServletException { when(request.getPathInfo()).thenReturn("/space/name"); NamespaceAndName namespaceAndName = new NamespaceAndName("space", "name"); doReturn(repositoryService).when(serviceFactory).create(namespaceAndName);