merge 2.0.0-m3
22
Jenkinsfile
vendored
@@ -7,12 +7,15 @@ import com.cloudogu.ces.cesbuildlib.*
|
||||
node('docker') {
|
||||
|
||||
// Change this as when we go back to default - necessary for proper SonarQube analysis
|
||||
mainBranch = "2.0.0-m3"
|
||||
mainBranch = '2.0.0-m3'
|
||||
|
||||
properties([
|
||||
// Keep only the last 10 build to preserve space
|
||||
buildDiscarder(logRotator(numToKeepStr: '10')),
|
||||
disableConcurrentBuilds()
|
||||
disableConcurrentBuilds(),
|
||||
parameters([
|
||||
string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image')
|
||||
])
|
||||
])
|
||||
|
||||
timeout(activity: true, time: 30, unit: 'MINUTES') {
|
||||
@@ -51,9 +54,9 @@ node('docker') {
|
||||
|
||||
if (isMainBranch()) {
|
||||
|
||||
// stage('Lifecycle') {
|
||||
// nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build'
|
||||
// }
|
||||
stage('Lifecycle') {
|
||||
nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build'
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
archiveArtifacts 'scm-webapp/target/scm-webapp.war'
|
||||
@@ -66,6 +69,13 @@ node('docker') {
|
||||
docker.withRegistry('', 'hub.docker.com-cesmarvin') {
|
||||
image.push(dockerImageTag)
|
||||
image.push('latest')
|
||||
if (!'latest'.equals(params.dockerTag)) {
|
||||
image.push(params.dockerTag)
|
||||
|
||||
def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}"
|
||||
currentBuild.description = newDockerTag
|
||||
image.push(newDockerTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +102,7 @@ String mainBranch
|
||||
|
||||
Maven setupMavenBuild() {
|
||||
// Keep this version number in sync with .mvn/maven-wrapper.properties
|
||||
Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8")
|
||||
Maven mvn = new MavenInDocker(this, '3.5.2-jdk-8')
|
||||
|
||||
if (isMainBranch()) {
|
||||
// Release starts javadoc, which takes very long, so do only for certain branches
|
||||
|
||||
BIN
docs/logo/favicon_16x16px.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
docs/logo/favicon_16x16px_transparent.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
1
docs/logo/scm-manager_logo.ai
Normal file
@@ -0,0 +1 @@
|
||||
%!PS-Adobe-2.0
|
||||
BIN
docs/logo/scm-manager_logo.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/logo/scm-manager_logo.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/logo/scm-manager_logo_img.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/logo/scm-manager_logo_img.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/logo/scm-manager_logo_img_neg.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/logo/scm-manager_logo_img_neg.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/logo/scm-manager_logo_neg.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
docs/logo/scm-manager_logo_neg.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/logo/scm-manager_logo_neg1.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/logo/scm-manager_logo_neg1.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/logo/scm-manager_logo_pos1.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/logo/scm-manager_logo_pos1.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
@@ -2,6 +2,8 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response.
|
||||
*
|
||||
@@ -34,6 +36,14 @@ public interface HalAppender {
|
||||
*/
|
||||
void appendEmbedded(String rel, HalRepresentation embeddedItem);
|
||||
|
||||
/**
|
||||
* Appends a list of embedded objects to the json response.
|
||||
*
|
||||
* @param rel name of relation
|
||||
* @param embeddedItems embedded objects
|
||||
*/
|
||||
void appendEmbedded(String rel, List<HalRepresentation> embeddedItems);
|
||||
|
||||
/**
|
||||
* Builder for link arrays.
|
||||
*/
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package sonia.scm.group;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* This class represents all associated groups which are provided by external systems for a certain user.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ExternalGroupNames extends GroupNames {
|
||||
public ExternalGroupNames() {
|
||||
}
|
||||
|
||||
public ExternalGroupNames(String groupName, String... groupNames) {
|
||||
super(groupName, groupNames);
|
||||
}
|
||||
|
||||
public ExternalGroupNames(Collection<String> collection) {
|
||||
super(collection);
|
||||
}
|
||||
}
|
||||
10
scm-core/src/main/java/sonia/scm/group/GroupCollector.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.group;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface GroupCollector {
|
||||
|
||||
String AUTHENTICATED = "_authenticated";
|
||||
|
||||
Set<String> collect(String principal);
|
||||
}
|
||||
@@ -1,187 +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.group;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This class represents all associated groups for a user.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.21
|
||||
*/
|
||||
public class GroupNames implements Serializable, Iterable<String>
|
||||
{
|
||||
|
||||
/**
|
||||
* Group for all authenticated users
|
||||
* @since 1.31
|
||||
*/
|
||||
public static final String AUTHENTICATED = "_authenticated";
|
||||
|
||||
/** Field description */
|
||||
private static final long serialVersionUID = 8615685985213897947L;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*/
|
||||
public GroupNames()
|
||||
{
|
||||
this(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param groupName
|
||||
* @param groupNames
|
||||
*/
|
||||
public GroupNames(String groupName, String... groupNames)
|
||||
{
|
||||
this(Lists.asList(groupName, groupNames));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param collection
|
||||
*/
|
||||
public GroupNames(Collection<String> collection)
|
||||
{
|
||||
this.collection = Collections.unmodifiableCollection(collection);
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param groupName
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean contains(String groupName)
|
||||
{
|
||||
return collection.contains(groupName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getClass() != obj.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
final GroupNames other = (GroupNames) obj;
|
||||
|
||||
return Objects.equal(collection, other.collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Iterator<String> iterator()
|
||||
{
|
||||
return collection.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return Joiner.on(", ").join(collection);
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Collection<String> getCollection()
|
||||
{
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
/** Field description */
|
||||
private final Collection<String> collection;
|
||||
}
|
||||
10
scm-core/src/main/java/sonia/scm/group/GroupResolver.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.group;
|
||||
|
||||
import sonia.scm.plugin.ExtensionPoint;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ExtensionPoint
|
||||
public interface GroupResolver {
|
||||
Set<String> resolve(String principal);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package sonia.scm.migration;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface MigrationDAO {
|
||||
Collection<MigrationInfo> getAll();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package sonia.scm.migration;
|
||||
|
||||
public class MigrationInfo {
|
||||
|
||||
private final String id;
|
||||
private final String protocol;
|
||||
private final String originalRepositoryName;
|
||||
private final String namespace;
|
||||
private final String name;
|
||||
|
||||
public MigrationInfo(String id, String protocol, String originalRepositoryName, String namespace, String name) {
|
||||
this.id = id;
|
||||
this.protocol = protocol;
|
||||
this.originalRepositoryName = originalRepositoryName;
|
||||
this.namespace = namespace;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public String getOriginalRepositoryName() {
|
||||
return originalRepositoryName;
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
package sonia.scm.migration;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class UpdateException extends RuntimeException {
|
||||
private static Logger LOG = LoggerFactory.getLogger(UpdateException.class);
|
||||
|
||||
public UpdateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UpdateException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
LOG.error(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
private String namespace;
|
||||
private String name;
|
||||
@XmlElement(name = "permission")
|
||||
private final Set<RepositoryPermission> permissions = new HashSet<>();
|
||||
private Set<RepositoryPermission> permissions = new HashSet<>();
|
||||
@XmlElement(name = "public")
|
||||
private boolean publicReadable = false;
|
||||
private boolean archived = false;
|
||||
@@ -331,6 +331,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
|
||||
try {
|
||||
repository = (Repository) super.clone();
|
||||
// fix permission reference on clone
|
||||
repository.permissions = new HashSet<>(permissions);
|
||||
} catch (CloneNotSupportedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public abstract class RepositoryLocationResolver {
|
||||
|
||||
public abstract boolean supportsLocationType(Class<?> type);
|
||||
@@ -35,5 +37,12 @@ public abstract class RepositoryLocationResolver {
|
||||
* @throws IllegalStateException when there already is a location for the given repository registered.
|
||||
*/
|
||||
void setLocation(String repositoryId, T location);
|
||||
|
||||
/**
|
||||
* Iterates all repository locations known to this resolver instance and calls the consumer giving the repository id
|
||||
* and its location for each repository.
|
||||
* @param consumer This callback will be called for each repository with the repository id and its location.
|
||||
*/
|
||||
void forAllLocations(BiConsumer<String, T> consumer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import sonia.scm.FeatureNotSupportedException;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.spi.DiffCommandRequest;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
abstract class AbstractDiffCommandBuilder <T extends AbstractDiffCommandBuilder> {
|
||||
|
||||
|
||||
/** request for the diff command implementation */
|
||||
final DiffCommandRequest request = new DiffCommandRequest();
|
||||
|
||||
private final Set<Feature> supportedFeatures;
|
||||
|
||||
AbstractDiffCommandBuilder(Set<Feature> supportedFeatures) {
|
||||
this.supportedFeatures = supportedFeatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given
|
||||
* here. In other words: What changes would be new to the ancestor changeset given here when the branch would
|
||||
* be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}!
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public T setAncestorChangeset(String revision)
|
||||
{
|
||||
if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) {
|
||||
throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name());
|
||||
}
|
||||
request.setAncestorChangeset(revision);
|
||||
|
||||
return self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the difference only for the given path.
|
||||
*
|
||||
*
|
||||
* @param path path for difference
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public T setPath(String path)
|
||||
{
|
||||
request.setPath(path);
|
||||
return self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this
|
||||
* and another revision.
|
||||
*
|
||||
*
|
||||
* @param revision revision for difference
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public T setRevision(String revision)
|
||||
{
|
||||
request.setRevision(revision);
|
||||
return self();
|
||||
}
|
||||
|
||||
abstract T self();
|
||||
}
|
||||
@@ -53,11 +53,6 @@ public enum Command
|
||||
*/
|
||||
BRANCHES,
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
BRANCH,
|
||||
|
||||
/**
|
||||
* @since 1.31
|
||||
*/
|
||||
@@ -71,10 +66,5 @@ public enum Command
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
MODIFICATIONS,
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
MERGE
|
||||
MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH;
|
||||
}
|
||||
|
||||
@@ -38,10 +38,8 @@ package sonia.scm.repository.api;
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.FeatureNotSupportedException;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.spi.DiffCommand;
|
||||
import sonia.scm.repository.spi.DiffCommandRequest;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -72,7 +70,7 @@ import java.util.Set;
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.17
|
||||
*/
|
||||
public final class DiffCommandBuilder
|
||||
public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCommandBuilder>
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -81,6 +79,9 @@ public final class DiffCommandBuilder
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(DiffCommandBuilder.class);
|
||||
|
||||
/** implementation of the diff command */
|
||||
private final DiffCommand diffCommand;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -92,8 +93,8 @@ public final class DiffCommandBuilder
|
||||
*/
|
||||
DiffCommandBuilder(DiffCommand diffCommand, Set<Feature> supportedFeatures)
|
||||
{
|
||||
super(supportedFeatures);
|
||||
this.diffCommand = diffCommand;
|
||||
this.supportedFeatures = supportedFeatures;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -162,54 +163,6 @@ public final class DiffCommandBuilder
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the difference only for the given path.
|
||||
*
|
||||
*
|
||||
* @param path path for difference
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public DiffCommandBuilder setPath(String path)
|
||||
{
|
||||
request.setPath(path);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this
|
||||
* and another revision.
|
||||
*
|
||||
*
|
||||
* @param revision revision for difference
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public DiffCommandBuilder setRevision(String revision)
|
||||
{
|
||||
request.setRevision(revision);
|
||||
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given
|
||||
* here. In other words: What changes would be new to the ancestor changeset given here when the branch would
|
||||
* be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}!
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public DiffCommandBuilder setAncestorChangeset(String revision)
|
||||
{
|
||||
if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) {
|
||||
throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name());
|
||||
}
|
||||
request.setAncestorChangeset(revision);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -233,12 +186,8 @@ public final class DiffCommandBuilder
|
||||
diffCommand.getDiffResult(request, outputStream);
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** implementation of the diff command */
|
||||
private final DiffCommand diffCommand;
|
||||
private Set<Feature> supportedFeatures;
|
||||
|
||||
/** request for the diff command implementation */
|
||||
private final DiffCommandRequest request = new DiffCommandRequest();
|
||||
@Override
|
||||
DiffCommandBuilder self() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
public interface DiffFile extends Iterable<Hunk> {
|
||||
|
||||
String getOldRevision();
|
||||
|
||||
String getNewRevision();
|
||||
|
||||
String getOldPath();
|
||||
|
||||
String getNewPath();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import java.util.OptionalInt;
|
||||
|
||||
public interface DiffLine {
|
||||
|
||||
OptionalInt getOldLineNumber();
|
||||
|
||||
OptionalInt getNewLineNumber();
|
||||
|
||||
String getContent();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
public interface DiffResult extends Iterable<DiffFile> {
|
||||
|
||||
String getOldRevision();
|
||||
|
||||
String getNewRevision();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.spi.DiffResultCommand;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder<DiffResultCommandBuilder> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DiffResultCommandBuilder.class);
|
||||
|
||||
private final DiffResultCommand diffResultCommand;
|
||||
|
||||
DiffResultCommandBuilder(DiffResultCommand diffResultCommand, Set<Feature> supportedFeatures) {
|
||||
super(supportedFeatures);
|
||||
this.diffResultCommand = diffResultCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the difference as parsed objects.
|
||||
*
|
||||
* @return content of the difference
|
||||
*/
|
||||
public DiffResult getDiffResult() throws IOException {
|
||||
Preconditions.checkArgument(request.isValid(),
|
||||
"path and/or revision is required");
|
||||
|
||||
LOG.debug("create diff result for {}", request);
|
||||
|
||||
return diffResultCommand.getDiffResult(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
DiffResultCommandBuilder self() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
24
scm-core/src/main/java/sonia/scm/repository/api/Hunk.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
public interface Hunk extends Iterable<DiffLine> {
|
||||
|
||||
default String getRawHeader() {
|
||||
return String.format("@@ -%s +%s @@", getLineMarker(getOldStart(), getOldLineCount()), getLineMarker(getNewStart(), getNewLineCount()));
|
||||
}
|
||||
|
||||
default String getLineMarker(int start, int lineCount) {
|
||||
if (lineCount == 1) {
|
||||
return Integer.toString(start);
|
||||
} else {
|
||||
return String.format("%s,%s", start, lineCount);
|
||||
}
|
||||
}
|
||||
|
||||
int getOldStart();
|
||||
|
||||
int getOldLineCount();
|
||||
|
||||
int getNewStart();
|
||||
|
||||
int getNewLineCount();
|
||||
}
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
@@ -239,6 +238,21 @@ public final class RepositoryService implements Closeable {
|
||||
return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures());
|
||||
}
|
||||
|
||||
/**
|
||||
* The diff command shows differences between revisions for a specified file
|
||||
* or the entire revision.
|
||||
*
|
||||
* @return instance of {@link DiffResultCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public DiffResultCommandBuilder getDiffResultCommand() {
|
||||
LOG.debug("create diff result command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new DiffResultCommandBuilder(provider.getDiffResultCommand(), provider.getSupportedFeatures());
|
||||
}
|
||||
|
||||
/**
|
||||
* The incoming command shows new {@link Changeset}s found in a different
|
||||
* repository location.
|
||||
@@ -379,7 +393,6 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public MergeCommandBuilder getMergeCommand() {
|
||||
RepositoryPermissions.push(getRepository()).check();
|
||||
LOG.debug("create merge command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface DiffResultCommand {
|
||||
DiffResult getDiffResult(DiffCommandRequest request) throws IOException;
|
||||
}
|
||||
@@ -158,6 +158,11 @@ public abstract class RepositoryServiceProvider implements Closeable
|
||||
throw new CommandNotSupportedException(Command.DIFF);
|
||||
}
|
||||
|
||||
public DiffResultCommand getDiffResultCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.DIFF_RESULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -99,15 +99,6 @@ public interface AccessTokenBuilder {
|
||||
*/
|
||||
AccessTokenBuilder scope(Scope scope);
|
||||
|
||||
/**
|
||||
* Define the logged in user as member of the given groups.
|
||||
*
|
||||
* @param groups group names
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
AccessTokenBuilder groups(String... groups);
|
||||
|
||||
/**
|
||||
* Creates a new {@link AccessToken} with the provided settings.
|
||||
*
|
||||
|
||||
@@ -162,7 +162,7 @@ public class AssignedPermission implements PermissionObject, Serializable
|
||||
//J-
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("groupPermisison", groupPermission)
|
||||
.add("groupPermission", groupPermission)
|
||||
.add("permission", permission)
|
||||
.toString();
|
||||
//J+
|
||||
|
||||
@@ -45,7 +45,6 @@ import org.apache.shiro.authc.credential.CredentialsMatcher;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserDAO;
|
||||
|
||||
@@ -71,8 +70,6 @@ public final class DAORealmHelper {
|
||||
|
||||
private final UserDAO userDAO;
|
||||
|
||||
private final GroupCollector groupCollector;
|
||||
|
||||
private final String realm;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
@@ -83,14 +80,12 @@ public final class DAORealmHelper {
|
||||
*
|
||||
* @param loginAttemptHandler login attempt handler for wrapping credentials matcher
|
||||
* @param userDAO user dao
|
||||
* @param groupCollector collect groups for a principal
|
||||
* @param realm name of realm
|
||||
*/
|
||||
public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupCollector groupCollector, String realm) {
|
||||
public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, String realm) {
|
||||
this.loginAttemptHandler = loginAttemptHandler;
|
||||
this.realm = realm;
|
||||
this.userDAO = userDAO;
|
||||
this.groupCollector = groupCollector;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
@@ -120,7 +115,7 @@ public final class DAORealmHelper {
|
||||
UsernamePasswordToken upt = (UsernamePasswordToken) token;
|
||||
String principal = upt.getUsername();
|
||||
|
||||
return getAuthenticationInfo(principal, null, null, Collections.emptySet());
|
||||
return getAuthenticationInfo(principal, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,7 +130,7 @@ public final class DAORealmHelper {
|
||||
}
|
||||
|
||||
|
||||
private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope, Iterable<String> groups) {
|
||||
private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) {
|
||||
checkArgument(!Strings.isNullOrEmpty(principal), "username is required");
|
||||
|
||||
LOG.debug("try to authenticate {}", principal);
|
||||
@@ -153,7 +148,6 @@ public final class DAORealmHelper {
|
||||
|
||||
collection.add(principal, realm);
|
||||
collection.add(user, realm);
|
||||
collection.add(groupCollector.collect(principal, groups), realm);
|
||||
collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm);
|
||||
|
||||
String creds = credentials;
|
||||
@@ -207,17 +201,17 @@ public final class DAORealmHelper {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info.
|
||||
*
|
||||
* @param groups extra groups
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public AuthenticationInfoBuilder withGroups(Iterable<String> groups) {
|
||||
this.groups = groups;
|
||||
return this;
|
||||
}
|
||||
// /**
|
||||
// * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info.
|
||||
// *
|
||||
// * @param groups extra groups
|
||||
// *
|
||||
// * @return {@code this}
|
||||
// */
|
||||
// public AuthenticationInfoBuilder withGroups(Iterable<String> groups) {
|
||||
// this.groups = groups;
|
||||
// return this;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Build creates the authentication info from the given information.
|
||||
@@ -225,7 +219,7 @@ public final class DAORealmHelper {
|
||||
* @return authentication info
|
||||
*/
|
||||
public AuthenticationInfo build() {
|
||||
return getAuthenticationInfo(principal, credentials, scope, groups);
|
||||
return getAuthenticationInfo(principal, credentials, scope);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.user.UserDAO;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -45,20 +45,19 @@ public final class DAORealmHelperFactory {
|
||||
|
||||
private final LoginAttemptHandler loginAttemptHandler;
|
||||
private final UserDAO userDAO;
|
||||
private final GroupCollector groupCollector;
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param loginAttemptHandler login attempt handler
|
||||
* @param userDAO user dao
|
||||
* @param groupDAO group dao
|
||||
* @param cacheManager
|
||||
*/
|
||||
@Inject
|
||||
public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO) {
|
||||
public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, CacheManager cacheManager) {
|
||||
this.loginAttemptHandler = loginAttemptHandler;
|
||||
this.userDAO = userDAO;
|
||||
this.groupCollector = new GroupCollector(groupDAO);
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +68,7 @@ public final class DAORealmHelperFactory {
|
||||
* @return new {@link DAORealmHelper} instance.
|
||||
*/
|
||||
public DAORealmHelper create(String realm) {
|
||||
return new DAORealmHelper(loginAttemptHandler, userDAO, groupCollector, realm);
|
||||
return new DAORealmHelper(loginAttemptHandler, userDAO, realm);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.group.GroupNames;
|
||||
|
||||
/**
|
||||
* Collect groups for a certain principal.
|
||||
* <strong>Warning</strong>: The class is only for internal use and should never used directly.
|
||||
*/
|
||||
class GroupCollector {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GroupCollector.class);
|
||||
|
||||
private final GroupDAO groupDAO;
|
||||
|
||||
GroupCollector(GroupDAO groupDAO) {
|
||||
this.groupDAO = groupDAO;
|
||||
}
|
||||
|
||||
GroupNames collect(String principal, Iterable<String> groupNames) {
|
||||
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
|
||||
|
||||
builder.add(GroupNames.AUTHENTICATED);
|
||||
|
||||
for (String group : groupNames) {
|
||||
builder.add(group);
|
||||
}
|
||||
|
||||
for (Group group : groupDAO.getAll()) {
|
||||
if (group.isMember(principal)) {
|
||||
builder.add(group.getName());
|
||||
}
|
||||
}
|
||||
|
||||
GroupNames groups = new GroupNames(builder.build());
|
||||
LOG.debug("collected following groups for principal {}: {}", principal, groups);
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
@@ -32,24 +32,15 @@ import com.google.inject.Inject;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.AlreadyExistsException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.group.ExternalGroupNames;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
/**
|
||||
* Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated
|
||||
* users with the local database.
|
||||
@@ -60,12 +51,9 @@ import static java.util.Arrays.asList;
|
||||
@Extension
|
||||
public final class SyncingRealmHelper {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SyncingRealmHelper.class);
|
||||
|
||||
private final AdministrationContext ctx;
|
||||
private final UserManager userManager;
|
||||
private final GroupManager groupManager;
|
||||
private final GroupCollector groupCollector;
|
||||
|
||||
/**
|
||||
* Constructs a new SyncingRealmHelper.
|
||||
@@ -73,134 +61,28 @@ public final class SyncingRealmHelper {
|
||||
* @param ctx administration context
|
||||
* @param userManager user manager
|
||||
* @param groupManager group manager
|
||||
* @param groupDAO group dao
|
||||
*/
|
||||
@Inject
|
||||
public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, GroupDAO groupDAO) {
|
||||
public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager) {
|
||||
this.ctx = ctx;
|
||||
this.userManager = userManager;
|
||||
this.groupManager = groupManager;
|
||||
this.groupCollector = new GroupCollector(groupDAO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link AuthenticationInfo} from user and groups.
|
||||
*/
|
||||
public AuthenticationInfoBuilder.ForRealm authenticationInfo() {
|
||||
return new AuthenticationInfoBuilder().new ForRealm();
|
||||
}
|
||||
|
||||
public class AuthenticationInfoBuilder {
|
||||
private String realm;
|
||||
private User user;
|
||||
private Collection<String> groups = Collections.emptySet();
|
||||
private Collection<String> externalGroups = Collections.emptySet();
|
||||
|
||||
private AuthenticationInfo build() {
|
||||
return SyncingRealmHelper.this.createAuthenticationInfo(realm, user, groups, externalGroups);
|
||||
}
|
||||
|
||||
public class ForRealm {
|
||||
private ForRealm() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the realm.
|
||||
* @param realm name of the realm
|
||||
*/
|
||||
public ForUser forRealm(String realm) {
|
||||
AuthenticationInfoBuilder.this.realm = realm;
|
||||
return AuthenticationInfoBuilder.this.new ForUser();
|
||||
}
|
||||
}
|
||||
|
||||
public class ForUser {
|
||||
private ForUser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user.
|
||||
* @param user authenticated user
|
||||
*/
|
||||
public AuthenticationInfoBuilder.WithGroups andUser(User user) {
|
||||
AuthenticationInfoBuilder.this.user = user;
|
||||
return AuthenticationInfoBuilder.this.new WithGroups();
|
||||
}
|
||||
}
|
||||
|
||||
public class WithGroups {
|
||||
private WithGroups() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the internal groups for the user.
|
||||
* @param groups groups of the authenticated user
|
||||
* @return builder step for groups
|
||||
*/
|
||||
public WithGroups withGroups(String... groups) {
|
||||
return withGroups(asList(groups));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the internal groups for the user.
|
||||
* @param groups groups of the authenticated user
|
||||
* @return builder step for groups
|
||||
*/
|
||||
public WithGroups withGroups(Collection<String> groups) {
|
||||
AuthenticationInfoBuilder.this.groups = groups;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the external groups for the user.
|
||||
* @param externalGroups external groups of the authenticated user
|
||||
* @return builder step for groups
|
||||
*/
|
||||
public WithGroups withExternalGroups(String... externalGroups) {
|
||||
return withExternalGroups(asList(externalGroups));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the external groups for the user.
|
||||
* @param externalGroups external groups of the authenticated user
|
||||
* @return builder step for groups
|
||||
*/
|
||||
public WithGroups withExternalGroups(Collection<String> externalGroups) {
|
||||
AuthenticationInfoBuilder.this.externalGroups = externalGroups;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link AuthenticationInfo} from the given options.
|
||||
*
|
||||
* @return complete autentication info
|
||||
*/
|
||||
public AuthenticationInfo build() {
|
||||
return AuthenticationInfoBuilder.this.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create {@link AuthenticationInfo} from user and groups.
|
||||
*
|
||||
*
|
||||
* @param realm name of the realm
|
||||
* @param user authenticated user
|
||||
* @param groups groups of the authenticated user
|
||||
*
|
||||
* @return authentication info
|
||||
*/
|
||||
private AuthenticationInfo createAuthenticationInfo(String realm, User user,
|
||||
Collection<String> groups, Collection<String> externalGroups) {
|
||||
public AuthenticationInfo createAuthenticationInfo(String realm, User user) {
|
||||
SimplePrincipalCollection collection = new SimplePrincipalCollection();
|
||||
|
||||
collection.add(user.getId(), realm);
|
||||
collection.add(user, realm);
|
||||
collection.add(groupCollector.collect(user.getId(), groups), realm);
|
||||
collection.add(new ExternalGroupNames(externalGroups), realm);
|
||||
|
||||
return new SimpleAuthenticationInfo(collection, user.getPassword());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package sonia.scm.update;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface BlobDirectoryAccess {
|
||||
|
||||
void forBlobDirectories(BlobDirectoryConsumer blobDirectoryConsumer) throws IOException;
|
||||
|
||||
void moveToRepositoryBlobStore(Path blobDirectory, String newDirectoryName, String repositoryId) throws IOException;
|
||||
|
||||
interface BlobDirectoryConsumer {
|
||||
void accept(Path directory) throws IOException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package sonia.scm.update;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
/**
|
||||
* Use this in {@link sonia.scm.migration.UpdateStep}s only to read repository objects directly from locations given by
|
||||
* {@link sonia.scm.repository.RepositoryLocationResolver}.
|
||||
*/
|
||||
public interface UpdateStepRepositoryMetadataAccess<T> {
|
||||
Repository read(T location);
|
||||
}
|
||||
@@ -37,14 +37,11 @@ package sonia.scm.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.io.Command;
|
||||
import sonia.scm.io.CommandResult;
|
||||
import sonia.scm.io.SimpleCommand;
|
||||
import sonia.scm.io.ZipUnArchiver;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
@@ -55,12 +52,13 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -471,8 +469,14 @@ public final class IOUtil
|
||||
{
|
||||
if (!directory.exists() &&!directory.mkdirs())
|
||||
{
|
||||
throw new IllegalStateException(
|
||||
"could not create directory ".concat(directory.getPath()));
|
||||
// Sometimes, the previous check simply has the wrong result (either the 'exists()' returnes false though the
|
||||
// directory exists or 'mkdirs()' returns false though the directory was created successfully.
|
||||
// We therefore have to double check here. Funny though, in these cases a second check with 'directory.exists()'
|
||||
// still returns false. As it seems, 'directory.getAbsoluteFile().exists()' creates a new object that fixes this
|
||||
// problem.
|
||||
if (!directory.getAbsoluteFile().exists()) {
|
||||
throw new IllegalStateException("could not create directory ".concat(directory.getPath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
package sonia.scm.web.filter;
|
||||
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.filter.Filters;
|
||||
import sonia.scm.filter.WebElement;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.web.UserAgent;
|
||||
import sonia.scm.web.UserAgentParser;
|
||||
import sonia.scm.web.WebTokenGenerator;
|
||||
import sonia.scm.web.protocol.HttpProtocolServlet;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -18,14 +13,11 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
@Priority(Filters.PRIORITY_AUTHENTICATION)
|
||||
@WebElement(value = HttpProtocolServlet.PATTERN)
|
||||
public class HttpProtocolServletAuthenticationFilter extends AuthenticationFilter {
|
||||
public class HttpProtocolServletAuthenticationFilterBase extends AuthenticationFilter {
|
||||
|
||||
private final UserAgentParser userAgentParser;
|
||||
|
||||
@Inject
|
||||
public HttpProtocolServletAuthenticationFilter(
|
||||
protected HttpProtocolServletAuthenticationFilterBase(
|
||||
ScmConfiguration configuration,
|
||||
Set<WebTokenGenerator> tokenGenerators,
|
||||
UserAgentParser userAgentParser) {
|
||||
@@ -0,0 +1,22 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RepositoryTest {
|
||||
|
||||
@Test
|
||||
void shouldCreateNewPermissionOnClone() {
|
||||
Repository repository = new Repository();
|
||||
repository.setPermissions(Arrays.asList(new RepositoryPermission("one", "role", false)));
|
||||
|
||||
Repository cloned = repository.clone();
|
||||
cloned.setPermissions(Arrays.asList(new RepositoryPermission("two", "role", false)));
|
||||
|
||||
assertThat(repository.getPermissions()).extracting(r -> r.getName()).containsOnly("one");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HunkTest {
|
||||
|
||||
@Test
|
||||
void shouldGetComplexHeader() {
|
||||
String rawHeader = createHunk(2, 3, 4, 5).getRawHeader();
|
||||
|
||||
assertThat(rawHeader).isEqualTo("@@ -2,3 +4,5 @@");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSingleNumberForOne() {
|
||||
String rawHeader = createHunk(42, 1, 5, 1).getRawHeader();
|
||||
|
||||
assertThat(rawHeader).isEqualTo("@@ -42 +5 @@");
|
||||
}
|
||||
|
||||
private Hunk createHunk(int oldStart, int oldLineCount, int newStart, int newLineCount) {
|
||||
return new Hunk() {
|
||||
@Override
|
||||
public int getOldStart() {
|
||||
return oldStart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldLineCount() {
|
||||
return oldLineCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewStart() {
|
||||
return newStart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewLineCount() {
|
||||
return newLineCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<DiffLine> iterator() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.DisabledAccountException;
|
||||
import org.apache.shiro.authc.UnknownAccountException;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserDAO;
|
||||
|
||||
@@ -38,7 +34,7 @@ class DAORealmHelperTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUpObjectUnderTest() {
|
||||
helper = new DAORealmHelper(loginAttemptHandler, userDAO, new GroupCollector(groupDAO), "hitchhiker");
|
||||
helper = new DAORealmHelper(loginAttemptHandler, userDAO, "hitchhiker");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -73,29 +69,9 @@ class DAORealmHelperTest {
|
||||
AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build();
|
||||
PrincipalCollection principals = authenticationInfo.getPrincipals();
|
||||
assertThat(principals.oneByType(User.class)).isSameAs(user);
|
||||
assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated");
|
||||
assertThat(principals.oneByType(Scope.class)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
void shouldReturnAuthenticationInfoWithGroups() {
|
||||
User user = new User("trillian");
|
||||
when(userDAO.get("trillian")).thenReturn(user);
|
||||
|
||||
Group one = new Group("xml", "one", "trillian");
|
||||
Group two = new Group("xml", "two", "trillian");
|
||||
Group six = new Group("xml", "six", "dent");
|
||||
when(groupDAO.getAll()).thenReturn(ImmutableList.of(one, two, six));
|
||||
|
||||
AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian")
|
||||
.withGroups(ImmutableList.of("three"))
|
||||
.build();
|
||||
|
||||
PrincipalCollection principals = authenticationInfo.getPrincipals();
|
||||
assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated", "one", "two", "three");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAuthenticationInfoWithScope() {
|
||||
User user = new User("trillian");
|
||||
@@ -148,7 +124,6 @@ class DAORealmHelperTest {
|
||||
|
||||
PrincipalCollection principals = authenticationInfo.getPrincipals();
|
||||
assertThat(principals.oneByType(User.class)).isSameAs(user);
|
||||
assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated");
|
||||
assertThat(principals.oneByType(Scope.class)).isEmpty();
|
||||
|
||||
assertThat(authenticationInfo.getCredentials()).isNull();
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.group.GroupNames;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GroupCollectorTest {
|
||||
|
||||
@Mock
|
||||
private GroupDAO groupDAO;
|
||||
|
||||
@InjectMocks
|
||||
private GroupCollector collector;
|
||||
|
||||
@Test
|
||||
void shouldAlwaysReturnAuthenticatedGroup() {
|
||||
GroupNames groupNames = collector.collect("trillian", Collections.emptySet());
|
||||
assertThat(groupNames).containsOnly("_authenticated");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithGroupsFromDao {
|
||||
|
||||
@BeforeEach
|
||||
void setUpGroupsDao() {
|
||||
List<Group> groups = Lists.newArrayList(
|
||||
new Group("xml", "heartOfGold", "trillian"),
|
||||
new Group("xml", "g42", "dent", "prefect"),
|
||||
new Group("xml", "fjordsOfAfrican", "dent", "trillian")
|
||||
);
|
||||
when(groupDAO.getAll()).thenReturn(groups);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnGroupsFromDao() {
|
||||
GroupNames groupNames = collector.collect("trillian", Collections.emptySet());
|
||||
assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCombineGivenWithDao() {
|
||||
GroupNames groupNames = collector.collect("trillian", ImmutableList.of("awesome", "incredible"));
|
||||
assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,31 +36,30 @@ package sonia.scm.security;
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.assertj.core.api.Assertions;
|
||||
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.AlreadyExistsException;
|
||||
import sonia.scm.group.ExternalGroupNames;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
import sonia.scm.web.security.PrivilegedAction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -78,9 +77,6 @@ public class SyncingRealmHelperTest {
|
||||
@Mock
|
||||
private UserManager userManager;
|
||||
|
||||
@Mock
|
||||
private GroupDAO groupDAO;
|
||||
|
||||
private SyncingRealmHelper helper;
|
||||
|
||||
/**
|
||||
@@ -106,7 +102,7 @@ public class SyncingRealmHelperTest {
|
||||
}
|
||||
};
|
||||
|
||||
helper = new SyncingRealmHelper(ctx, userManager, groupManager, groupDAO);
|
||||
helper = new SyncingRealmHelper(ctx, userManager, groupManager);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,67 +179,15 @@ public class SyncingRealmHelperTest {
|
||||
verify(userManager, times(1)).modify(user);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderShouldSetInternalGroups() {
|
||||
AuthenticationInfo authenticationInfo = helper
|
||||
.authenticationInfo()
|
||||
.forRealm("unit-test")
|
||||
.andUser(new User("ziltoid"))
|
||||
.withGroups("internal")
|
||||
.build();
|
||||
|
||||
GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class);
|
||||
Assertions.assertThat(groupNames.getCollection()).contains("_authenticated", "internal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderShouldSetExternalGroups() {
|
||||
AuthenticationInfo authenticationInfo = helper
|
||||
.authenticationInfo()
|
||||
.forRealm("unit-test")
|
||||
.andUser(new User("ziltoid"))
|
||||
.withExternalGroups("external")
|
||||
.build();
|
||||
|
||||
ExternalGroupNames groupNames = authenticationInfo.getPrincipals().oneByType(ExternalGroupNames.class);
|
||||
Assertions.assertThat(groupNames.getCollection()).containsOnly("external");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderShouldSetValues() {
|
||||
User user = new User("ziltoid");
|
||||
AuthenticationInfo authInfo = helper
|
||||
.authenticationInfo()
|
||||
.forRealm("unit-test")
|
||||
.andUser(user)
|
||||
.build();
|
||||
AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test", user);
|
||||
|
||||
assertNotNull(authInfo);
|
||||
assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal());
|
||||
assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test"));
|
||||
assertEquals(user, authInfo.getPrincipals().oneByType(User.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnCombinedGroupNames() {
|
||||
User user = new User("tricia");
|
||||
|
||||
List<Group> groups = Lists.newArrayList(new Group("xml", "heartOfGold", "tricia"));
|
||||
when(groupDAO.getAll()).thenReturn(groups);
|
||||
|
||||
AuthenticationInfo authInfo = helper
|
||||
.authenticationInfo()
|
||||
.forRealm("unit-test")
|
||||
.andUser(user)
|
||||
.withGroups("fjordsOfAfrican")
|
||||
.withExternalGroups("g42")
|
||||
.build();
|
||||
|
||||
|
||||
GroupNames groupNames = authInfo.getPrincipals().oneByType(GroupNames.class);
|
||||
Assertions.assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican");
|
||||
|
||||
ExternalGroupNames externalGroupNames = authInfo.getPrincipals().oneByType(ExternalGroupNames.class);
|
||||
Assertions.assertThat(externalGroupNames).contains("g42");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HttpProtocolServletAuthenticationFilterTest {
|
||||
class HttpProtocolServletAuthenticationFilterBaseTest {
|
||||
|
||||
private ScmConfiguration configuration = new ScmConfiguration();
|
||||
|
||||
@@ -32,7 +32,7 @@ class HttpProtocolServletAuthenticationFilterTest {
|
||||
@Mock
|
||||
private UserAgentParser userAgentParser;
|
||||
|
||||
private HttpProtocolServletAuthenticationFilter authenticationFilter;
|
||||
private HttpProtocolServletAuthenticationFilterBase authenticationFilter;
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
@@ -48,7 +48,7 @@ class HttpProtocolServletAuthenticationFilterTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUpObjectUnderTest() {
|
||||
authenticationFilter = new HttpProtocolServletAuthenticationFilter(configuration, tokenGenerators, userAgentParser);
|
||||
authenticationFilter = new HttpProtocolServletAuthenticationFilterBase(configuration, tokenGenerators, userAgentParser);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -0,0 +1 @@
|
||||
mock-maker-inline
|
||||
@@ -5,19 +5,21 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.Marshaller;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class MetadataStore {
|
||||
public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class);
|
||||
|
||||
private final JAXBContext jaxbContext;
|
||||
|
||||
MetadataStore() {
|
||||
public MetadataStore() {
|
||||
try {
|
||||
jaxbContext = JAXBContext.newInstance(Repository.class);
|
||||
} catch (JAXBException ex) {
|
||||
@@ -25,10 +27,10 @@ class MetadataStore {
|
||||
}
|
||||
}
|
||||
|
||||
Repository read(Path path) {
|
||||
public Repository read(Path path) {
|
||||
LOG.trace("read repository metadata from {}", path);
|
||||
try {
|
||||
return (Repository) jaxbContext.createUnmarshaller().unmarshal(path.toFile());
|
||||
return (Repository) jaxbContext.createUnmarshaller().unmarshal(resolveDataPath(path).toFile());
|
||||
} catch (JAXBException ex) {
|
||||
throw new InternalRepositoryException(
|
||||
ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex
|
||||
@@ -41,10 +43,13 @@ class MetadataStore {
|
||||
try {
|
||||
Marshaller marshaller = jaxbContext.createMarshaller();
|
||||
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
||||
marshaller.marshal(repository, path.toFile());
|
||||
marshaller.marshal(repository, resolveDataPath(path).toFile());
|
||||
} catch (JAXBException ex) {
|
||||
throw new InternalRepositoryException(repository, "failed write repository metadata", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveDataPath(Path repositoryPath) {
|
||||
return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,11 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forAllLocations(BiConsumer<String, T> consumer) {
|
||||
pathById.forEach((id, path) -> consumer.accept(id, (T) contextProvider.resolve(path)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,10 +120,6 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
return contextProvider.resolve(removedPath);
|
||||
}
|
||||
|
||||
void forAllPaths(BiConsumer<String, Path> consumer) {
|
||||
pathById.forEach((id, path) -> consumer.accept(id, contextProvider.resolve(path)));
|
||||
}
|
||||
|
||||
void updateModificationDate() {
|
||||
this.writePathDatabase();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package sonia.scm.repository.xml;
|
||||
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -7,9 +9,9 @@ import java.util.function.BiConsumer;
|
||||
public class SingleRepositoryUpdateProcessor {
|
||||
|
||||
@Inject
|
||||
private PathBasedRepositoryLocationResolver locationResolver;
|
||||
private RepositoryLocationResolver locationResolver;
|
||||
|
||||
public void doUpdate(BiConsumer<String, Path> forEachRepository) {
|
||||
locationResolver.forAllPaths(forEachRepository);
|
||||
locationResolver.forClass(Path.class).forAllLocations(forEachRepository);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
@@ -76,18 +76,14 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
}
|
||||
|
||||
private void init() {
|
||||
repositoryLocationResolver.forAllPaths((repositoryId, repositoryPath) -> {
|
||||
Path metadataPath = resolveDataPath(repositoryPath);
|
||||
Repository repository = metadataStore.read(metadataPath);
|
||||
RepositoryLocationResolver.RepositoryLocationResolverInstance<Path> pathRepositoryLocationResolverInstance = repositoryLocationResolver.create(Path.class);
|
||||
pathRepositoryLocationResolverInstance.forAllLocations((repositoryId, repositoryPath) -> {
|
||||
Repository repository = metadataStore.read(repositoryPath);
|
||||
byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
|
||||
byId.put(repositoryId, repository);
|
||||
});
|
||||
}
|
||||
|
||||
private Path resolveDataPath(Path repositoryPath) {
|
||||
return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "xml";
|
||||
@@ -108,8 +104,7 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
Path repositoryPath = (Path) location;
|
||||
|
||||
try {
|
||||
Path metadataPath = resolveDataPath(repositoryPath);
|
||||
metadataStore.write(metadataPath, repository);
|
||||
metadataStore.write(repositoryPath, repository);
|
||||
} catch (Exception e) {
|
||||
repositoryLocationResolver.remove(repository.getId());
|
||||
throw new InternalRepositoryException(repository, "failed to create filesystem", e);
|
||||
@@ -166,9 +161,8 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
Path repositoryPath = repositoryLocationResolver
|
||||
.create(Path.class)
|
||||
.getLocation(repository.getId());
|
||||
Path metadataPath = resolveDataPath(repositoryPath);
|
||||
repositoryLocationResolver.updateModificationDate();
|
||||
metadataStore.write(metadataPath, clone);
|
||||
metadataStore.write(repositoryPath, clone);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package sonia.scm.store;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.update.BlobDirectoryAccess;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class DefaultBlobDirectoryAccess implements BlobDirectoryAccess {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultBlobDirectoryAccess.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
|
||||
@Inject
|
||||
public DefaultBlobDirectoryAccess(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) {
|
||||
this.contextProvider = contextProvider;
|
||||
this.locationResolver = locationResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forBlobDirectories(BlobDirectoryConsumer blobDirectoryConsumer) throws IOException {
|
||||
Path v1blobDir = computeV1BlobDir();
|
||||
if (Files.exists(v1blobDir) && Files.isDirectory(v1blobDir)) {
|
||||
try (Stream<Path> fileStream = Files.list(v1blobDir)) {
|
||||
fileStream.filter(p -> Files.isDirectory(p)).forEach(p -> {
|
||||
try {
|
||||
blobDirectoryConsumer.accept(p);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("could not call consumer for blob directory " + p, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveToRepositoryBlobStore(Path blobDirectory, String newDirectoryName, String repositoryId) throws IOException {
|
||||
Path repositoryLocation;
|
||||
try {
|
||||
repositoryLocation = locationResolver
|
||||
.forClass(Path.class)
|
||||
.getLocation(repositoryId);
|
||||
} catch (IllegalStateException e) {
|
||||
LOG.info("ignoring blob directory {} because there is no repository location for repository id {}", blobDirectory, repositoryId);
|
||||
return;
|
||||
}
|
||||
Path target = repositoryLocation
|
||||
.resolve(Store.BLOB.getRepositoryStoreDirectory());
|
||||
IOUtil.mkdirs(target.toFile());
|
||||
Path resolvedSourceDirectory = computeV1BlobDir().resolve(blobDirectory);
|
||||
Path resolvedTargetDirectory = target.resolve(newDirectoryName);
|
||||
LOG.trace("moving directory {} to {}", resolvedSourceDirectory, resolvedTargetDirectory);
|
||||
Files.move(resolvedSourceDirectory, resolvedTargetDirectory);
|
||||
}
|
||||
|
||||
private Path computeV1BlobDir() {
|
||||
return contextProvider.getBaseDirectory().toPath().resolve("var").resolve("blob");
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JAXBPropertyFileAccess.class);
|
||||
|
||||
public static final String XML_FILENAME_SUFFIX = ".xml";
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
|
||||
@@ -31,8 +30,8 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess {
|
||||
public Target renameGlobalConfigurationFrom(String oldName) {
|
||||
return newName -> {
|
||||
Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
|
||||
Path oldConfigFile = configDir.resolve(oldName + XML_FILENAME_SUFFIX);
|
||||
Path newConfigFile = configDir.resolve(newName + XML_FILENAME_SUFFIX);
|
||||
Path oldConfigFile = configDir.resolve(oldName + StoreConstants.FILE_EXTENSION);
|
||||
Path newConfigFile = configDir.resolve(newName + StoreConstants.FILE_EXTENSION);
|
||||
Files.move(oldConfigFile, newConfigFile);
|
||||
};
|
||||
}
|
||||
@@ -45,7 +44,7 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess {
|
||||
Path v1storeDir = computeV1StoreDir();
|
||||
if (Files.exists(v1storeDir) && Files.isDirectory(v1storeDir)) {
|
||||
try (Stream<Path> fileStream = Files.list(v1storeDir)) {
|
||||
fileStream.filter(p -> p.toString().endsWith(XML_FILENAME_SUFFIX)).forEach(p -> {
|
||||
fileStream.filter(p -> p.toString().endsWith(StoreConstants.FILE_EXTENSION)).forEach(p -> {
|
||||
try {
|
||||
String storeName = extractStoreName(p);
|
||||
storeFileConsumer.accept(p, storeName);
|
||||
@@ -84,7 +83,7 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess {
|
||||
|
||||
private String extractStoreName(Path p) {
|
||||
String fileName = p.getFileName().toString();
|
||||
return fileName.substring(0, fileName.length() - XML_FILENAME_SUFFIX.length());
|
||||
return fileName.substring(0, fileName.length() - StoreConstants.FILE_EXTENSION.length());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class PathBasedRepositoryLocationResolverTest {
|
||||
@Test
|
||||
void shouldInitWithExistingData() {
|
||||
Map<String, Path> foundRepositories = new HashMap<>();
|
||||
resolverWithExistingData.forAllPaths(
|
||||
resolverWithExistingData.forClass(Path.class).forAllLocations(
|
||||
foundRepositories::put
|
||||
);
|
||||
assertThat(foundRepositories)
|
||||
|
||||
@@ -26,15 +26,13 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -47,6 +45,7 @@ class XmlRepositoryDAOTest {
|
||||
|
||||
@Mock
|
||||
private PathBasedRepositoryLocationResolver locationResolver;
|
||||
private Consumer<BiConsumer<String, Path>> triggeredOnForAllLocations = none -> {};
|
||||
|
||||
private FileSystem fileSystem = new DefaultFileSystem();
|
||||
|
||||
@@ -69,6 +68,11 @@ class XmlRepositoryDAOTest {
|
||||
@Override
|
||||
public void setLocation(String repositoryId, Path location) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forAllLocations(BiConsumer<String, Path> consumer) {
|
||||
triggeredOnForAllLocations.accept(consumer);
|
||||
}
|
||||
}
|
||||
);
|
||||
when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation));
|
||||
@@ -332,11 +336,10 @@ class XmlRepositoryDAOTest {
|
||||
@Test
|
||||
void shouldRefreshWithExistingRepositoriesFromPathDatabase() {
|
||||
// given
|
||||
doNothing().when(locationResolver).forAllPaths(any());
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||
|
||||
mockExistingPath();
|
||||
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
|
||||
|
||||
// when
|
||||
dao.refresh();
|
||||
|
||||
@@ -346,12 +349,7 @@ class XmlRepositoryDAOTest {
|
||||
}
|
||||
|
||||
private void mockExistingPath() {
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
((BiConsumer<String, Path>) invocation.getArgument(0)).accept("existing", repositoryPath);
|
||||
return null;
|
||||
}
|
||||
).when(locationResolver).forAllPaths(any());
|
||||
triggeredOnForAllLocations = consumer -> consumer.accept("existing", repositoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,23 +123,6 @@ public class TestData {
|
||||
;
|
||||
}
|
||||
|
||||
public static void createUserPermission(String username, String roleName, String repositoryType) {
|
||||
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
|
||||
LOG.info("create permission with name {} and role {} using the endpoint: {}", username, roleName, defaultPermissionUrl);
|
||||
given(VndMediaType.REPOSITORY_PERMISSION)
|
||||
.when()
|
||||
.content("{\n" +
|
||||
"\t\"role\": " + roleName + ",\n" +
|
||||
"\t\"name\": \"" + username + "\",\n" +
|
||||
"\t\"groupPermission\": false\n" +
|
||||
"\t\n" +
|
||||
"}")
|
||||
.post(defaultPermissionUrl)
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_CREATED)
|
||||
;
|
||||
}
|
||||
|
||||
public static List<Map> getUserPermissions(String username, String password, String repositoryType) {
|
||||
return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK)
|
||||
.extract()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.eclipse.jgit.lib.StoredConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class GitConfigHelper {
|
||||
|
||||
private static final String CONFIG_SECTION_SCMM = "scmm";
|
||||
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
|
||||
|
||||
public void createScmmConfig(Repository repository, org.eclipse.jgit.lib.Repository gitRepository) throws IOException {
|
||||
StoredConfig config = gitRepository.getConfig();
|
||||
config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId());
|
||||
config.save();
|
||||
}
|
||||
|
||||
public String getRepositoryId(StoredConfig gitConfig) {
|
||||
return gitConfig.getString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID);
|
||||
}
|
||||
}
|
||||
@@ -89,8 +89,6 @@ public class GitRepositoryHandler
|
||||
GitRepositoryServiceProvider.COMMANDS);
|
||||
|
||||
private static final Object LOCK = new Object();
|
||||
private static final String CONFIG_SECTION_SCMM = "scmm";
|
||||
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
@@ -185,7 +183,7 @@ public class GitRepositoryHandler
|
||||
}
|
||||
|
||||
public String getRepositoryId(StoredConfig gitConfig) {
|
||||
return gitConfig.getString(GitRepositoryHandler.CONFIG_SECTION_SCMM, null, GitRepositoryHandler.CONFIG_KEY_REPOSITORY_ID);
|
||||
return new GitConfigHelper().getRepositoryId(gitConfig);
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -194,9 +192,7 @@ public class GitRepositoryHandler
|
||||
protected void create(Repository repository, File directory) throws IOException {
|
||||
try (org.eclipse.jgit.lib.Repository gitRepository = build(directory)) {
|
||||
gitRepository.create(true);
|
||||
StoredConfig config = gitRepository.getConfig();
|
||||
config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId());
|
||||
config.save();
|
||||
new GitConfigHelper().createScmmConfig(repository, gitRepository);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.eclipse.jgit.api.FetchCommand;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.diff.DiffFormatter;
|
||||
import org.eclipse.jgit.lib.AnyObjectId;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
@@ -441,7 +442,7 @@ public final class GitUtil
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getId(ObjectId objectId)
|
||||
public static String getId(AnyObjectId objectId)
|
||||
{
|
||||
String id = Util.EMPTY_STRING;
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.eclipse.jgit.diff.DiffEntry;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
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 sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
final class Differ implements AutoCloseable {
|
||||
|
||||
private final RevWalk walk;
|
||||
private final TreeWalk treeWalk;
|
||||
private final RevCommit commit;
|
||||
|
||||
private Differ(RevCommit commit, RevWalk walk, TreeWalk treeWalk) {
|
||||
this.commit = commit;
|
||||
this.walk = walk;
|
||||
this.treeWalk = treeWalk;
|
||||
}
|
||||
|
||||
static Diff diff(Repository repository, DiffCommandRequest request) throws IOException {
|
||||
try (Differ differ = create(repository, request)) {
|
||||
return differ.diff();
|
||||
}
|
||||
}
|
||||
|
||||
private static Differ create(Repository repository, DiffCommandRequest request) throws IOException {
|
||||
RevWalk walk = new RevWalk(repository);
|
||||
|
||||
ObjectId revision = repository.resolve(request.getRevision());
|
||||
RevCommit commit = walk.parseCommit(revision);
|
||||
|
||||
walk.markStart(commit);
|
||||
commit = walk.next();
|
||||
TreeWalk treeWalk = new TreeWalk(repository);
|
||||
treeWalk.reset();
|
||||
treeWalk.setRecursive(true);
|
||||
|
||||
if (Util.isNotEmpty(request.getPath()))
|
||||
{
|
||||
treeWalk.setFilter(PathFilter.create(request.getPath()));
|
||||
}
|
||||
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getAncestorChangeset()))
|
||||
{
|
||||
ObjectId otherRevision = repository.resolve(request.getAncestorChangeset());
|
||||
ObjectId ancestorId = computeCommonAncestor(repository, revision, otherRevision);
|
||||
RevTree tree = walk.parseCommit(ancestorId).getTree();
|
||||
treeWalk.addTree(tree);
|
||||
}
|
||||
else if (commit.getParentCount() > 0)
|
||||
{
|
||||
RevTree tree = commit.getParent(0).getTree();
|
||||
|
||||
if (tree != null)
|
||||
{
|
||||
treeWalk.addTree(tree);
|
||||
}
|
||||
else
|
||||
{
|
||||
treeWalk.addTree(new EmptyTreeIterator());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
treeWalk.addTree(new EmptyTreeIterator());
|
||||
}
|
||||
|
||||
treeWalk.addTree(commit.getTree());
|
||||
|
||||
return new Differ(commit, walk, treeWalk);
|
||||
}
|
||||
|
||||
private static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
|
||||
return GitUtil.computeCommonAncestor(repository, revision1, revision2);
|
||||
}
|
||||
|
||||
private Diff diff() throws IOException {
|
||||
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
|
||||
return new Diff(commit, entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
GitUtil.release(walk);
|
||||
GitUtil.release(treeWalk);
|
||||
}
|
||||
|
||||
public static class Diff {
|
||||
|
||||
private final RevCommit commit;
|
||||
private final List<DiffEntry> entries;
|
||||
|
||||
private Diff(RevCommit commit, List<DiffEntry> entries) {
|
||||
this.commit = commit;
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
public RevCommit getCommit() {
|
||||
return commit;
|
||||
}
|
||||
|
||||
public List<DiffEntry> getEntries() {
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
public class FileRange {
|
||||
|
||||
private final int start;
|
||||
private final int lineCount;
|
||||
|
||||
public FileRange(int start, int lineCount) {
|
||||
this.start = start;
|
||||
this.lineCount = lineCount;
|
||||
}
|
||||
|
||||
public int getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public int getLineCount() {
|
||||
return lineCount;
|
||||
}
|
||||
}
|
||||
@@ -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,9 +24,8 @@
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
@@ -34,148 +33,41 @@ 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;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
|
||||
{
|
||||
public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
|
||||
|
||||
/**
|
||||
* the logger for GitDiffCommand
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GitDiffCommand.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param context
|
||||
* @param repository
|
||||
*/
|
||||
public GitDiffCommand(GitContext context, Repository repository)
|
||||
{
|
||||
GitDiffCommand(GitContext context, Repository repository) {
|
||||
super(context, repository);
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
* @param output
|
||||
*/
|
||||
@Override
|
||||
public void getDiffResult(DiffCommandRequest request, OutputStream output)
|
||||
{
|
||||
RevWalk walk = null;
|
||||
TreeWalk treeWalk = null;
|
||||
DiffFormatter formatter = null;
|
||||
public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException {
|
||||
@SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService
|
||||
org.eclipse.jgit.lib.Repository repository = open();
|
||||
try (DiffFormatter formatter = new DiffFormatter(new BufferedOutputStream(output))) {
|
||||
formatter.setRepository(repository);
|
||||
|
||||
try
|
||||
{
|
||||
org.eclipse.jgit.lib.Repository gr = open();
|
||||
Differ.Diff diff = Differ.diff(repository, request);
|
||||
|
||||
walk = new RevWalk(gr);
|
||||
|
||||
ObjectId revision = gr.resolve(request.getRevision());
|
||||
RevCommit commit = walk.parseCommit(revision);
|
||||
|
||||
walk.markStart(commit);
|
||||
commit = walk.next();
|
||||
treeWalk = new TreeWalk(gr);
|
||||
treeWalk.reset();
|
||||
treeWalk.setRecursive(true);
|
||||
|
||||
if (Util.isNotEmpty(request.getPath()))
|
||||
{
|
||||
treeWalk.setFilter(PathFilter.create(request.getPath()));
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
|
||||
if (tree != null)
|
||||
{
|
||||
treeWalk.addTree(tree);
|
||||
}
|
||||
else
|
||||
{
|
||||
treeWalk.addTree(new EmptyTreeIterator());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
treeWalk.addTree(new EmptyTreeIterator());
|
||||
}
|
||||
|
||||
treeWalk.addTree(commit.getTree());
|
||||
formatter = new DiffFormatter(new BufferedOutputStream(output));
|
||||
formatter.setRepository(gr);
|
||||
|
||||
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
|
||||
|
||||
for (DiffEntry e : entries)
|
||||
{
|
||||
if (!e.getOldId().equals(e.getNewId()))
|
||||
{
|
||||
for (DiffEntry e : diff.getEntries()) {
|
||||
if (!e.getOldId().equals(e.getNewId())) {
|
||||
formatter.format(e);
|
||||
}
|
||||
}
|
||||
|
||||
formatter.flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO throw exception
|
||||
logger.error("could not create diff", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GitUtil.release(walk);
|
||||
GitUtil.release(treeWalk);
|
||||
GitUtil.release(formatter);
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
|
||||
return GitUtil.computeCommonAncestor(repository, revision1, revision2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.diff.DiffEntry;
|
||||
import org.eclipse.jgit.diff.DiffFormatter;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffFile;
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
import sonia.scm.repository.api.Hunk;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand {
|
||||
|
||||
GitDiffResultCommand(GitContext context, Repository repository) {
|
||||
super(context, repository);
|
||||
}
|
||||
|
||||
public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException {
|
||||
org.eclipse.jgit.lib.Repository repository = open();
|
||||
return new GitDiffResult(repository, Differ.diff(repository, diffCommandRequest));
|
||||
}
|
||||
|
||||
private class GitDiffResult implements DiffResult {
|
||||
|
||||
private final org.eclipse.jgit.lib.Repository repository;
|
||||
private final Differ.Diff diff;
|
||||
|
||||
private GitDiffResult(org.eclipse.jgit.lib.Repository repository, Differ.Diff diff) {
|
||||
this.repository = repository;
|
||||
this.diff = diff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOldRevision() {
|
||||
return GitUtil.getId(diff.getCommit().getParent(0).getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNewRevision() {
|
||||
return GitUtil.getId(diff.getCommit().getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<DiffFile> iterator() {
|
||||
return diff.getEntries()
|
||||
.stream()
|
||||
.map(diffEntry -> new GitDiffFile(repository, diffEntry))
|
||||
.collect(Collectors.<DiffFile>toList())
|
||||
.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
private class GitDiffFile implements DiffFile {
|
||||
|
||||
private final org.eclipse.jgit.lib.Repository repository;
|
||||
private final DiffEntry diffEntry;
|
||||
|
||||
private GitDiffFile(org.eclipse.jgit.lib.Repository repository, DiffEntry diffEntry) {
|
||||
this.repository = repository;
|
||||
this.diffEntry = diffEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOldRevision() {
|
||||
return GitUtil.getId(diffEntry.getOldId().toObjectId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNewRevision() {
|
||||
return GitUtil.getId(diffEntry.getNewId().toObjectId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOldPath() {
|
||||
return diffEntry.getOldPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNewPath() {
|
||||
return diffEntry.getNewPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Hunk> iterator() {
|
||||
String content = format(repository, diffEntry);
|
||||
GitHunkParser parser = new GitHunkParser();
|
||||
return parser.parse(content).iterator();
|
||||
}
|
||||
|
||||
private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DiffFormatter formatter = new DiffFormatter(baos)) {
|
||||
formatter.setRepository(repository);
|
||||
formatter.format(entry);
|
||||
return baos.toString();
|
||||
} catch (IOException ex) {
|
||||
throw new InternalRepositoryException(GitDiffResultCommand.this.repository, "failed to format diff entry", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.api.DiffLine;
|
||||
import sonia.scm.repository.api.Hunk;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class GitHunk implements Hunk {
|
||||
|
||||
private final FileRange oldFileRange;
|
||||
private final FileRange newFileRange;
|
||||
private List<DiffLine> lines;
|
||||
|
||||
public GitHunk(FileRange oldFileRange, FileRange newFileRange) {
|
||||
this.oldFileRange = oldFileRange;
|
||||
this.newFileRange = newFileRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldStart() {
|
||||
return oldFileRange.getStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldLineCount() {
|
||||
return oldFileRange.getLineCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewStart() {
|
||||
return newFileRange.getStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewLineCount() {
|
||||
return newFileRange.getLineCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<DiffLine> iterator() {
|
||||
return lines.iterator();
|
||||
}
|
||||
|
||||
void setLines(List<DiffLine> lines) {
|
||||
this.lines = lines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.api.DiffLine;
|
||||
import sonia.scm.repository.api.Hunk;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Scanner;
|
||||
|
||||
import static java.util.OptionalInt.of;
|
||||
|
||||
final class GitHunkParser {
|
||||
private static final int HEADER_PREFIX_LENGTH = "@@ -".length();
|
||||
private static final int HEADER_SUFFIX_LENGTH = " @@".length();
|
||||
|
||||
private GitHunk currentGitHunk = null;
|
||||
private List<DiffLine> collectedLines = null;
|
||||
private int oldLineCounter = 0;
|
||||
private int newLineCounter = 0;
|
||||
|
||||
GitHunkParser() {
|
||||
}
|
||||
|
||||
public List<Hunk> parse(String content) {
|
||||
List<Hunk> hunks = new ArrayList<>();
|
||||
|
||||
try (Scanner scanner = new Scanner(content)) {
|
||||
while (scanner.hasNextLine()) {
|
||||
String line = scanner.nextLine();
|
||||
if (line.startsWith("@@")) {
|
||||
parseHeader(hunks, line);
|
||||
} else if (currentGitHunk != null) {
|
||||
parseDiffLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentGitHunk != null) {
|
||||
currentGitHunk.setLines(collectedLines);
|
||||
}
|
||||
|
||||
return hunks;
|
||||
}
|
||||
|
||||
private void parseHeader(List<Hunk> hunks, String line) {
|
||||
if (currentGitHunk != null) {
|
||||
currentGitHunk.setLines(collectedLines);
|
||||
}
|
||||
String hunkHeader = line.substring(HEADER_PREFIX_LENGTH, line.length() - HEADER_SUFFIX_LENGTH);
|
||||
String[] split = hunkHeader.split("\\s");
|
||||
|
||||
FileRange oldFileRange = createFileRange(split[0]);
|
||||
// TODO merge contains two two block which starts with "-" e.g. -1,3 -2,4 +3,6
|
||||
// check if it is relevant for our use case
|
||||
FileRange newFileRange = createFileRange(split[1]);
|
||||
|
||||
currentGitHunk = new GitHunk(oldFileRange, newFileRange);
|
||||
hunks.add(currentGitHunk);
|
||||
|
||||
collectedLines = new ArrayList<>();
|
||||
oldLineCounter = currentGitHunk.getOldStart();
|
||||
newLineCounter = currentGitHunk.getNewStart();
|
||||
}
|
||||
|
||||
private void parseDiffLine(String line) {
|
||||
String content = line.substring(1);
|
||||
switch (line.charAt(0)) {
|
||||
case ' ':
|
||||
collectedLines.add(new UnchangedGitDiffLine(newLineCounter, oldLineCounter, content));
|
||||
++newLineCounter;
|
||||
++oldLineCounter;
|
||||
break;
|
||||
case '+':
|
||||
collectedLines.add(new AddedGitDiffLine(newLineCounter, content));
|
||||
++newLineCounter;
|
||||
break;
|
||||
case '-':
|
||||
collectedLines.add(new RemovedGitDiffLine(oldLineCounter, content));
|
||||
++oldLineCounter;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("cannot handle diff line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddedGitDiffLine implements DiffLine {
|
||||
private final int newLineNumber;
|
||||
private final String content;
|
||||
|
||||
private AddedGitDiffLine(int newLineNumber, String content) {
|
||||
this.newLineNumber = newLineNumber;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getOldLineNumber() {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getNewLineNumber() {
|
||||
return of(newLineNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RemovedGitDiffLine implements DiffLine {
|
||||
private final int oldLineNumber;
|
||||
private final String content;
|
||||
|
||||
private RemovedGitDiffLine(int oldLineNumber, String content) {
|
||||
this.oldLineNumber = oldLineNumber;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getOldLineNumber() {
|
||||
return of(oldLineNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getNewLineNumber() {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnchangedGitDiffLine implements DiffLine {
|
||||
private final int newLineNumber;
|
||||
private final int oldLineNumber;
|
||||
private final String content;
|
||||
|
||||
private UnchangedGitDiffLine(int newLineNumber, int oldLineNumber, String content) {
|
||||
this.newLineNumber = newLineNumber;
|
||||
this.oldLineNumber = oldLineNumber;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getOldLineNumber() {
|
||||
return of(oldLineNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getNewLineNumber() {
|
||||
return of(newLineNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
private static FileRange createFileRange(String fileRangeString) {
|
||||
int start;
|
||||
int lineCount = 1;
|
||||
int commaIndex = fileRangeString.indexOf(',');
|
||||
if (commaIndex > 0) {
|
||||
start = Integer.parseInt(fileRangeString.substring(0, commaIndex));
|
||||
lineCount = Integer.parseInt(fileRangeString.substring(commaIndex + 1));
|
||||
} else {
|
||||
start = Integer.parseInt(fileRangeString);
|
||||
}
|
||||
|
||||
return new FileRange(start, lineCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package sonia.scm.repository.update;
|
||||
|
||||
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.GitConfigHelper;
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
@Extension
|
||||
public class GitV2UpdateStep implements UpdateStep {
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
private final UpdateStepRepositoryMetadataAccess<Path> repositoryMetadataAccess;
|
||||
|
||||
@Inject
|
||||
public GitV2UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess<Path> repositoryMetadataAccess) {
|
||||
this.locationResolver = locationResolver;
|
||||
this.repositoryMetadataAccess = repositoryMetadataAccess;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() {
|
||||
locationResolver.forClass(Path.class).forAllLocations(
|
||||
(repositoryId, path) -> {
|
||||
Repository repository = repositoryMetadataAccess.read(path);
|
||||
if (isGitDirectory(repository)) {
|
||||
try (org.eclipse.jgit.lib.Repository gitRepository = build(path.resolve("data").toFile())) {
|
||||
new GitConfigHelper().createScmmConfig(repository, gitRepository);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private org.eclipse.jgit.lib.Repository build(File directory) throws IOException {
|
||||
return new FileRepositoryBuilder()
|
||||
.setGitDir(directory)
|
||||
.readEnvironment()
|
||||
.findGitDir()
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isGitDirectory(Repository repository) {
|
||||
return GitRepositoryHandler.TYPE_NAME.equals(repository.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.plugin.git";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sonia.scm.web.lfs;
|
||||
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.update.BlobDirectoryAccess;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Extension
|
||||
public class LfsV1UpdateStep implements UpdateStep {
|
||||
|
||||
private final BlobDirectoryAccess blobDirectoryAccess;
|
||||
|
||||
@Inject
|
||||
public LfsV1UpdateStep(BlobDirectoryAccess blobDirectoryAccess) {
|
||||
this.blobDirectoryAccess = blobDirectoryAccess;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() throws Exception {
|
||||
blobDirectoryAccess.forBlobDirectories(
|
||||
f -> {
|
||||
Path v1Directory = f.getFileName();
|
||||
String v1DirectoryName = v1Directory.toString();
|
||||
if (v1DirectoryName.endsWith("-git-lfs")) {
|
||||
blobDirectoryAccess.moveToRepositoryBlobStore(f, v1DirectoryName, v1DirectoryName.substring(0, v1DirectoryName.length() - "-git-lfs".length()));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return Version.parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.git.lfs";
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package sonia.scm.repository.spi;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -38,7 +39,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
"+f\n";
|
||||
|
||||
@Test
|
||||
public void diffForOneRevisionShouldCreateDiff() {
|
||||
public void diffForOneRevisionShouldCreateDiff() throws IOException {
|
||||
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||
@@ -48,7 +49,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diffForOneBranchShouldCreateDiff() {
|
||||
public void diffForOneBranchShouldCreateDiff() throws IOException {
|
||||
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("test-branch");
|
||||
@@ -58,7 +59,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diffForPathShouldCreateLimitedDiff() {
|
||||
public void diffForPathShouldCreateLimitedDiff() throws IOException {
|
||||
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("test-branch");
|
||||
@@ -69,7 +70,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diffBetweenTwoBranchesShouldCreateDiff() {
|
||||
public void diffBetweenTwoBranchesShouldCreateDiff() throws IOException {
|
||||
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("master");
|
||||
@@ -80,7 +81,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() {
|
||||
public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() throws IOException {
|
||||
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("master");
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.api.DiffFile;
|
||||
import sonia.scm.repository.api.DiffResult;
|
||||
import sonia.scm.repository.api.Hunk;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class GitDiffResultCommandTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldReturnOldAndNewRevision() throws IOException {
|
||||
DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||
|
||||
assertThat(diffResult.getNewRevision()).isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||
assertThat(diffResult.getOldRevision()).isEqualTo("592d797cd36432e591416e8b2b98154f4f163411");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnFilePaths() throws IOException {
|
||||
DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||
Iterator<DiffFile> iterator = diffResult.iterator();
|
||||
DiffFile a = iterator.next();
|
||||
assertThat(a.getNewPath()).isEqualTo("a.txt");
|
||||
assertThat(a.getOldPath()).isEqualTo("a.txt");
|
||||
|
||||
DiffFile b = iterator.next();
|
||||
assertThat(b.getOldPath()).isEqualTo("b.txt");
|
||||
assertThat(b.getNewPath()).isEqualTo("/dev/null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnFileRevisions() throws IOException {
|
||||
DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||
Iterator<DiffFile> iterator = diffResult.iterator();
|
||||
|
||||
DiffFile a = iterator.next();
|
||||
assertThat(a.getOldRevision()).isEqualTo("78981922613b2afb6025042ff6bd878ac1994e85");
|
||||
assertThat(a.getNewRevision()).isEqualTo("1dc60c7504f4326bc83b9b628c384ec8d7e57096");
|
||||
|
||||
DiffFile b = iterator.next();
|
||||
assertThat(b.getOldRevision()).isEqualTo("61780798228d17af2d34fce4cfbdf35556832472");
|
||||
assertThat(b.getNewRevision()).isEqualTo("0000000000000000000000000000000000000000");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnFileHunks() throws IOException {
|
||||
DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||
Iterator<DiffFile> iterator = diffResult.iterator();
|
||||
|
||||
DiffFile a = iterator.next();
|
||||
Iterator<Hunk> hunks = a.iterator();
|
||||
|
||||
Hunk hunk = hunks.next();
|
||||
assertThat(hunk.getOldStart()).isEqualTo(1);
|
||||
assertThat(hunk.getOldLineCount()).isEqualTo(1);
|
||||
|
||||
assertThat(hunk.getNewStart()).isEqualTo(1);
|
||||
assertThat(hunk.getNewLineCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnFileHunksWithFullFileRange() throws IOException {
|
||||
DiffResult diffResult = createDiffResult("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
|
||||
Iterator<DiffFile> iterator = diffResult.iterator();
|
||||
|
||||
DiffFile a = iterator.next();
|
||||
Iterator<Hunk> hunks = a.iterator();
|
||||
|
||||
Hunk hunk = hunks.next();
|
||||
assertThat(hunk.getOldStart()).isEqualTo(1);
|
||||
assertThat(hunk.getOldLineCount()).isEqualTo(1);
|
||||
|
||||
assertThat(hunk.getNewStart()).isEqualTo(1);
|
||||
assertThat(hunk.getNewLineCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
private DiffResult createDiffResult(String s) throws IOException {
|
||||
GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository);
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision(s);
|
||||
|
||||
return gitDiffResultCommand.getDiffResult(diffCommandRequest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.repository.api.DiffLine;
|
||||
import sonia.scm.repository.api.Hunk;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class GitHunkParserTest {
|
||||
|
||||
private static final String DIFF_001 = "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" +
|
||||
"+added line\n";
|
||||
|
||||
private static final String DIFF_002 = "diff --git a/file b/file\n" +
|
||||
"index 5e89957..e8823e1 100644\n" +
|
||||
"--- a/file\n" +
|
||||
"+++ b/file\n" +
|
||||
"@@ -2,6 +2,9 @@\n" +
|
||||
" 2\n" +
|
||||
" 3\n" +
|
||||
" 4\n" +
|
||||
"+5\n" +
|
||||
"+6\n" +
|
||||
"+7\n" +
|
||||
" 8\n" +
|
||||
" 9\n" +
|
||||
" 10\n" +
|
||||
"@@ -15,14 +18,13 @@\n" +
|
||||
" 18\n" +
|
||||
" 19\n" +
|
||||
" 20\n" +
|
||||
"+21\n" +
|
||||
"+22\n" +
|
||||
" 23\n" +
|
||||
" 24\n" +
|
||||
" 25\n" +
|
||||
" 26\n" +
|
||||
" 27\n" +
|
||||
"-a\n" +
|
||||
"-b\n" +
|
||||
"-c\n" +
|
||||
" 28\n" +
|
||||
" 29\n" +
|
||||
" 30";
|
||||
|
||||
private static final String DIFF_003 = "diff --git a/a.txt b/a.txt\n" +
|
||||
"index 7898192..2f8bc28 100644\n" +
|
||||
"--- a/a.txt\n" +
|
||||
"+++ b/a.txt\n" +
|
||||
"@@ -1,2 +1 @@\n" +
|
||||
" a\n" +
|
||||
"-removed line\n";
|
||||
|
||||
private static final String ILLEGAL_DIFF = "diff --git a/a.txt b/a.txt\n" +
|
||||
"index 7898192..2f8bc28 100644\n" +
|
||||
"--- a/a.txt\n" +
|
||||
"+++ b/a.txt\n" +
|
||||
"@@ -1,2 +1 @@\n" +
|
||||
" a\n" +
|
||||
"~illegal line\n";
|
||||
|
||||
@Test
|
||||
void shouldParseHunks() {
|
||||
List<Hunk> hunks = new GitHunkParser().parse(DIFF_001);
|
||||
assertThat(hunks).hasSize(1);
|
||||
assertHunk(hunks.get(0), 1, 1, 1, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseMultipleHunks() {
|
||||
List<Hunk> hunks = new GitHunkParser().parse(DIFF_002);
|
||||
|
||||
assertThat(hunks).hasSize(2);
|
||||
assertHunk(hunks.get(0), 2, 6, 2, 9);
|
||||
assertHunk(hunks.get(1), 15, 14, 18, 13);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseAddedHunkLines() {
|
||||
List<Hunk> hunks = new GitHunkParser().parse(DIFF_001);
|
||||
|
||||
Hunk hunk = hunks.get(0);
|
||||
|
||||
Iterator<DiffLine> lines = hunk.iterator();
|
||||
|
||||
DiffLine line1 = lines.next();
|
||||
assertThat(line1.getOldLineNumber()).hasValue(1);
|
||||
assertThat(line1.getNewLineNumber()).hasValue(1);
|
||||
assertThat(line1.getContent()).isEqualTo("a");
|
||||
|
||||
DiffLine line2 = lines.next();
|
||||
assertThat(line2.getOldLineNumber()).isEmpty();
|
||||
assertThat(line2.getNewLineNumber()).hasValue(2);
|
||||
assertThat(line2.getContent()).isEqualTo("added line");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseRemovedHunkLines() {
|
||||
List<Hunk> hunks = new GitHunkParser().parse(DIFF_003);
|
||||
|
||||
Hunk hunk = hunks.get(0);
|
||||
|
||||
Iterator<DiffLine> lines = hunk.iterator();
|
||||
|
||||
DiffLine line1 = lines.next();
|
||||
assertThat(line1.getOldLineNumber()).hasValue(1);
|
||||
assertThat(line1.getNewLineNumber()).hasValue(1);
|
||||
assertThat(line1.getContent()).isEqualTo("a");
|
||||
|
||||
DiffLine line2 = lines.next();
|
||||
assertThat(line2.getOldLineNumber()).hasValue(2);
|
||||
assertThat(line2.getNewLineNumber()).isEmpty();
|
||||
assertThat(line2.getContent()).isEqualTo("removed line");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForIllegalLine() {
|
||||
assertThrows(IllegalStateException.class, () -> new GitHunkParser().parse(ILLEGAL_DIFF));
|
||||
}
|
||||
|
||||
private void assertHunk(Hunk hunk, int oldStart, int oldLineCount, int newStart, int newLineCount) {
|
||||
assertThat(hunk.getOldStart()).isEqualTo(oldStart);
|
||||
assertThat(hunk.getOldLineCount()).isEqualTo(oldLineCount);
|
||||
|
||||
assertThat(hunk.getNewStart()).isEqualTo(newStart);
|
||||
assertThat(hunk.getNewLineCount()).isEqualTo(newLineCount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,11 +40,10 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.web.HgUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -118,7 +117,7 @@ public final class HgEnvironment
|
||||
String credentials = hookManager.getCredentials();
|
||||
environment.put(SCM_BEARER_TOKEN, credentials);
|
||||
} catch (ProvisionException e) {
|
||||
LOG.debug("could not create bearer token; looks like currently we are not in a request", e);
|
||||
LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e);
|
||||
}
|
||||
environment.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig()));
|
||||
environment.put(ENV_URL, hookUrl);
|
||||
|
||||
@@ -41,13 +41,11 @@ import com.google.inject.Singleton;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ConfigurationException;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.installer.HgInstaller;
|
||||
import sonia.scm.installer.HgInstallerFactory;
|
||||
import sonia.scm.io.ExtendedCommand;
|
||||
import sonia.scm.io.INIConfiguration;
|
||||
import sonia.scm.io.INIConfigurationReader;
|
||||
import sonia.scm.io.INIConfigurationWriter;
|
||||
import sonia.scm.io.INISection;
|
||||
import sonia.scm.plugin.Extension;
|
||||
@@ -347,14 +345,6 @@ public class HgRepositoryHandler
|
||||
writer.write(hgrc, hgrcFile);
|
||||
}
|
||||
|
||||
public String getRepositoryId(File directory) {
|
||||
try {
|
||||
return new INIConfigurationReader().read(new File(directory, PATH_HGRC)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
20
scm-plugins/scm-legacy-plugin/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@scm-manager/legacy-plugin",
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "src/main/js/index.js",
|
||||
"scripts": {
|
||||
"build": "ui-bundler plugin",
|
||||
"watch": "ui-bundler plugin -w",
|
||||
"lint": "ui-bundler lint",
|
||||
"flow": "flow check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-components": "latest",
|
||||
"@scm-manager/ui-extensions": "^0.1.1",
|
||||
"react-redux": "^5.0.7",
|
||||
"@scm-manager/ui-types": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.25"
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,13 @@
|
||||
<version>${servlet.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.ws.rs</groupId>
|
||||
<artifactId>jsr311-api</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import sonia.scm.api.v2.resources.Enrich;
|
||||
import sonia.scm.api.v2.resources.HalAppender;
|
||||
import sonia.scm.api.v2.resources.HalEnricher;
|
||||
import sonia.scm.api.v2.resources.HalEnricherContext;
|
||||
import sonia.scm.api.v2.resources.Index;
|
||||
import sonia.scm.api.v2.resources.LinkBuilder;
|
||||
import sonia.scm.api.v2.resources.ScmPathInfoStore;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
import javax.inject.Provider;
|
||||
|
||||
@Extension
|
||||
@Enrich(Index.class)
|
||||
public class LegacyIndexHalEnricher implements HalEnricher {
|
||||
|
||||
private Provider<ScmPathInfoStore> scmPathInfoStoreProvider;
|
||||
|
||||
@Inject
|
||||
public LegacyIndexHalEnricher(Provider<ScmPathInfoStore> scmPathInfoStoreProvider) {
|
||||
this.scmPathInfoStoreProvider = scmPathInfoStoreProvider;
|
||||
}
|
||||
|
||||
private String createLink() {
|
||||
return new LinkBuilder(scmPathInfoStoreProvider.get().get(), LegacyRepositoryService.class)
|
||||
.method("getNameAndNamespaceForRepositoryId")
|
||||
.parameters("REPOID")
|
||||
.href()
|
||||
.replace("REPOID", "{id}");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(HalEnricherContext context, HalAppender appender) {
|
||||
appender.appendLink("nameAndNamespace", createLink());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import com.google.inject.servlet.ServletModule;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
@Extension
|
||||
public class LegacyModule extends ServletModule {
|
||||
|
||||
@Override
|
||||
protected void configureServlets() {
|
||||
filter("/*").through(RepositoryLegacyProtocolRedirectFilter.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.filter.Filters;
|
||||
import sonia.scm.filter.WebElement;
|
||||
import sonia.scm.web.UserAgentParser;
|
||||
import sonia.scm.web.WebTokenGenerator;
|
||||
import sonia.scm.web.filter.HttpProtocolServletAuthenticationFilterBase;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Set;
|
||||
|
||||
@Priority(Filters.PRIORITY_AUTHENTICATION)
|
||||
@WebElement(value = "/git/*", morePatterns = {"/hg/*", "/svn/*"})
|
||||
public class LegacyProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase {
|
||||
|
||||
@Inject
|
||||
public LegacyProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set<WebTokenGenerator> tokenGenerators, UserAgentParser userAgentParser) {
|
||||
super(configuration, tokenGenerators, userAgentParser);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
@Path("v2/legacy/repository")
|
||||
public class LegacyRepositoryService {
|
||||
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
@Inject
|
||||
public LegacyRepositoryService(RepositoryManager repositoryManager) {
|
||||
this.repositoryManager = repositoryManager;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:read:global\" privilege"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public NamespaceAndNameDto getNameAndNamespaceForRepositoryId(@PathParam("id") String repositoryId) {
|
||||
Repository repo = repositoryManager.get(repositoryId);
|
||||
if (repo == null) {
|
||||
throw new NotFoundException(Repository.class, repositoryId);
|
||||
}
|
||||
return new NamespaceAndNameDto(repo.getName(), repo.getNamespace());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class NamespaceAndNameDto {
|
||||
private String name;
|
||||
private String namespace;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.filter.Filters;
|
||||
import sonia.scm.migration.MigrationDAO;
|
||||
import sonia.scm.migration.MigrationInfo;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.web.filter.HttpFilter;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static org.apache.commons.lang.StringUtils.isEmpty;
|
||||
|
||||
@Priority(Filters.PRIORITY_BASEURL)
|
||||
@Singleton
|
||||
public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter {
|
||||
|
||||
private final ProtocolBasedLegacyRepositoryInfo info;
|
||||
private final RepositoryDAO repositoryDao;
|
||||
|
||||
@Inject
|
||||
public RepositoryLegacyProtocolRedirectFilter(MigrationDAO migrationDAO, RepositoryDAO repositoryDao) {
|
||||
this.info = load(migrationDAO);
|
||||
this.repositoryDao = repositoryDao;
|
||||
}
|
||||
|
||||
private static ProtocolBasedLegacyRepositoryInfo load(MigrationDAO migrationDAO) {
|
||||
return new ProtocolBasedLegacyRepositoryInfo(migrationDAO.getAll());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
new Worker(request, response, chain).doFilter();
|
||||
}
|
||||
|
||||
private class Worker {
|
||||
private final HttpServletRequest request;
|
||||
private final HttpServletResponse response;
|
||||
private final FilterChain chain;
|
||||
|
||||
private Worker(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.chain = chain;
|
||||
}
|
||||
|
||||
void doFilter() throws IOException, ServletException {
|
||||
String servletPath = getServletPathWithoutLeadingSlash();
|
||||
String[] pathElements = servletPath.split("/");
|
||||
if (pathElements.length > 0) {
|
||||
checkPathElements(servletPath, pathElements);
|
||||
} else {
|
||||
noRedirect();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPathElements(String servletPath, String[] pathElements) throws IOException, ServletException {
|
||||
Optional<MigrationInfo> migrationInfo = info.findRepository(asList(pathElements));
|
||||
if (migrationInfo.isPresent()) {
|
||||
doRedirect(servletPath, migrationInfo.get());
|
||||
} else {
|
||||
noRedirect();
|
||||
}
|
||||
}
|
||||
|
||||
private void doRedirect(String servletPath, MigrationInfo migrationInfo) throws IOException, ServletException {
|
||||
if (repositoryDao.get(migrationInfo.getId()) == null) {
|
||||
noRedirect();
|
||||
} else {
|
||||
String furtherPath = servletPath.substring(migrationInfo.getProtocol().length() + 1 + migrationInfo.getOriginalRepositoryName().length());
|
||||
String queryString = request.getQueryString();
|
||||
if (isEmpty(queryString)) {
|
||||
redirectWithoutQueryParameters(migrationInfo, furtherPath);
|
||||
} else {
|
||||
redirectWithQueryParameters(migrationInfo, furtherPath, queryString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void redirectWithoutQueryParameters(MigrationInfo migrationInfo, String furtherPath) throws IOException {
|
||||
response.sendRedirect(String.format("%s/repo/%s/%s%s", request.getContextPath(), migrationInfo.getNamespace(), migrationInfo.getName(), furtherPath));
|
||||
}
|
||||
|
||||
private void redirectWithQueryParameters(MigrationInfo migrationInfo, String furtherPath, String queryString) throws IOException {
|
||||
response.sendRedirect(String.format("%s/repo/%s/%s%s?%s", request.getContextPath(), migrationInfo.getNamespace(), migrationInfo.getName(), furtherPath, queryString));
|
||||
}
|
||||
|
||||
private void noRedirect() throws IOException, ServletException {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String getServletPathWithoutLeadingSlash() {
|
||||
String servletPath = request.getServletPath();
|
||||
if (servletPath.startsWith("/")) {
|
||||
return servletPath.substring(1);
|
||||
} else {
|
||||
return servletPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ProtocolBasedLegacyRepositoryInfo {
|
||||
|
||||
private final Map<String, LegacyRepositoryInfoCollection> infosForProtocol = new HashMap<>();
|
||||
|
||||
ProtocolBasedLegacyRepositoryInfo(Collection<MigrationInfo> all) {
|
||||
all.forEach(this::add);
|
||||
}
|
||||
|
||||
private void add(MigrationInfo migrationInfo) {
|
||||
String protocol = migrationInfo.getProtocol();
|
||||
LegacyRepositoryInfoCollection legacyRepositoryInfoCollection = infosForProtocol.computeIfAbsent(protocol, x -> new LegacyRepositoryInfoCollection());
|
||||
legacyRepositoryInfoCollection.add(migrationInfo);
|
||||
}
|
||||
|
||||
private Optional<MigrationInfo> findRepository(List<String> pathElements) {
|
||||
String protocol = pathElements.get(0);
|
||||
if (!isProtocol(protocol)) {
|
||||
return empty();
|
||||
}
|
||||
return infosForProtocol.get(protocol).findRepository(removeFirstElement(pathElements));
|
||||
}
|
||||
|
||||
boolean isProtocol(String protocol) {
|
||||
return infosForProtocol.containsKey(protocol);
|
||||
}
|
||||
}
|
||||
|
||||
private static class LegacyRepositoryInfoCollection {
|
||||
|
||||
private final Map<String, MigrationInfo> repositories = new HashMap<>();
|
||||
private final Map<String, LegacyRepositoryInfoCollection> next = new HashMap<>();
|
||||
|
||||
Optional<MigrationInfo> findRepository(List<String> pathElements) {
|
||||
String firstPathElement = pathElements.get(0);
|
||||
if (repositories.containsKey(firstPathElement)) {
|
||||
return of(repositories.get(firstPathElement));
|
||||
} else if (next.containsKey(firstPathElement)) {
|
||||
return next.get(firstPathElement).findRepository(removeFirstElement(pathElements));
|
||||
} else {
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void add(MigrationInfo migrationInfo) {
|
||||
String originalRepositoryName = migrationInfo.getOriginalRepositoryName();
|
||||
List<String> originalRepositoryNameParts = asList(originalRepositoryName.split("/"));
|
||||
add(migrationInfo, originalRepositoryNameParts);
|
||||
}
|
||||
|
||||
private void add(MigrationInfo migrationInfo, List<String> originalRepositoryNameParts) {
|
||||
if (originalRepositoryNameParts.isEmpty()) {
|
||||
throw new IllegalArgumentException("cannot handle empty name");
|
||||
} else if (originalRepositoryNameParts.size() == 1) {
|
||||
repositories.put(originalRepositoryNameParts.get(0), migrationInfo);
|
||||
} else {
|
||||
LegacyRepositoryInfoCollection subCollection = next.computeIfAbsent(originalRepositoryNameParts.get(0), x -> new LegacyRepositoryInfoCollection());
|
||||
subCollection.add(migrationInfo, removeFirstElement(originalRepositoryNameParts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> List<T> removeFirstElement(List<T> originalRepositoryNameParts) {
|
||||
return originalRepositoryNameParts.subList(1, originalRepositoryNameParts.size());
|
||||
}
|
||||
}
|
||||
14
scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js
Normal file
@@ -0,0 +1,14 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {withRouter} from "react-router-dom";
|
||||
|
||||
class DummyComponent extends React.Component<Props, State> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(DummyComponent);
|
||||
90
scm-plugins/scm-legacy-plugin/src/main/js/index.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import {binder} from "@scm-manager/ui-extensions";
|
||||
import {apiClient, ErrorBoundary, ErrorNotification, ProtectedRoute} from "@scm-manager/ui-components";
|
||||
import DummyComponent from "./DummyComponent";
|
||||
import type {Links} from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
authenticated?: boolean,
|
||||
links: Links,
|
||||
|
||||
//context objects
|
||||
history: History
|
||||
};
|
||||
|
||||
type State = {
|
||||
error?: Error
|
||||
};
|
||||
|
||||
class LegacyRepositoryRedirect extends React.Component<Props, State> {
|
||||
constructor(props: Props, state: State) {
|
||||
super(props, state);
|
||||
this.state = { error: null };
|
||||
}
|
||||
|
||||
handleError = (error: Error) => {
|
||||
this.setState({
|
||||
error
|
||||
});
|
||||
};
|
||||
|
||||
redirectLegacyRepository() {
|
||||
const { history, links } = this.props;
|
||||
if (location.href && location.href.includes("#diffPanel;")) {
|
||||
let splittedUrl = location.href.split(";");
|
||||
let repoId = splittedUrl[1];
|
||||
let changeSetId = splittedUrl[2];
|
||||
|
||||
apiClient
|
||||
.get(links.nameAndNamespace.href.replace("{id}", repoId))
|
||||
.then(response => response.json())
|
||||
.then(payload =>
|
||||
history.push(
|
||||
"/repo/" +
|
||||
payload.namespace +
|
||||
"/" +
|
||||
payload.name +
|
||||
"/changeset/" +
|
||||
changeSetId
|
||||
)
|
||||
)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { authenticated } = this.props;
|
||||
const { error } = this.state;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<ErrorBoundary>
|
||||
<ErrorNotification error={error} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{authenticated ? (
|
||||
this.redirectLegacyRepository()
|
||||
) : (
|
||||
<ProtectedRoute
|
||||
path="/index.html"
|
||||
component={DummyComponent}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
binder.bind("main.route", withRouter(LegacyRepositoryRedirect));
|
||||
@@ -0,0 +1,43 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
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.NotFoundException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LegacyRepositoryServiceTest {
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
private LegacyRepositoryService legacyRepositoryService;
|
||||
private final Repository repository = new Repository("abc123", "git", "space", "repo");
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
legacyRepositoryService = new LegacyRepositoryService(repositoryManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findRepositoryNameSpaceAndNameForRepositoryId() {
|
||||
when(repositoryManager.get(any(String.class))).thenReturn(repository);
|
||||
NamespaceAndNameDto namespaceAndName = legacyRepositoryService.getNameAndNamespaceForRepositoryId("abc123");
|
||||
assertThat(namespaceAndName.getName()).isEqualToIgnoringCase("repo");
|
||||
assertThat(namespaceAndName.getNamespace()).isEqualToIgnoringCase("space");
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void shouldGetNotFoundExceptionIfRepositoryNotExists() throws NotFoundException {
|
||||
when(repositoryManager.get(any(String.class))).thenReturn(null);
|
||||
legacyRepositoryService.getNameAndNamespaceForRepositoryId("456def");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package sonia.scm.legacy;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.migration.MigrationDAO;
|
||||
import sonia.scm.migration.MigrationInfo;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RepositoryLegacyProtocolRedirectFilterTest {
|
||||
|
||||
@Mock
|
||||
MigrationDAO migrationDAO;
|
||||
@Mock
|
||||
RepositoryDAO repositoryDao;
|
||||
@Mock
|
||||
HttpServletRequest request;
|
||||
@Mock
|
||||
HttpServletResponse response;
|
||||
@Mock
|
||||
FilterChain filterChain;
|
||||
|
||||
@BeforeEach
|
||||
void initRequest() {
|
||||
lenient().when(request.getContextPath()).thenReturn("/scm");
|
||||
lenient().when(request.getQueryString()).thenReturn("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRedirectForEmptyMigrationList() throws IOException, ServletException {
|
||||
when(request.getServletPath()).thenReturn("/git/old/name");
|
||||
|
||||
new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectForExistingRepository() throws IOException, ServletException {
|
||||
when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name")));
|
||||
when(repositoryDao.get("id")).thenReturn(new Repository());
|
||||
when(request.getServletPath()).thenReturn("/git/old/name");
|
||||
|
||||
new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain);
|
||||
|
||||
verify(response).sendRedirect("/scm/repo/namespace/name");
|
||||
verify(filterChain, never()).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectForExistingRepositoryWithFurtherPathElements() throws IOException, ServletException {
|
||||
when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name")));
|
||||
when(repositoryDao.get("id")).thenReturn(new Repository());
|
||||
when(request.getServletPath()).thenReturn("/git/old/name/info/refs");
|
||||
|
||||
new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain);
|
||||
|
||||
verify(response).sendRedirect("/scm/repo/namespace/name/info/refs");
|
||||
verify(filterChain, never()).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectWithQueryParameters() throws IOException, ServletException {
|
||||
when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name")));
|
||||
when(repositoryDao.get("id")).thenReturn(new Repository());
|
||||
when(request.getServletPath()).thenReturn("/git/old/name/info/refs");
|
||||
when(request.getQueryString()).thenReturn("parameter=value");
|
||||
|
||||
new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain);
|
||||
|
||||
verify(response).sendRedirect("/scm/repo/namespace/name/info/refs?parameter=value");
|
||||
verify(filterChain, never()).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRedirectWhenRepositoryHasBeenDeleted() throws IOException, ServletException {
|
||||
when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name")));
|
||||
when(repositoryDao.get("id")).thenReturn(null);
|
||||
when(request.getServletPath()).thenReturn("/git/old/name");
|
||||
|
||||
new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain);
|
||||
|
||||
verify(response, never()).sendRedirect(any());
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.io.INIConfiguration;
|
||||
import sonia.scm.io.INIConfigurationReader;
|
||||
import sonia.scm.io.INIConfigurationWriter;
|
||||
import sonia.scm.io.INISection;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
class SvnConfigHelper {
|
||||
|
||||
private static final String CONFIG_FILE_NAME = "scm-manager.conf";
|
||||
private static final String CONFIG_SECTION_SCMM = "scmm";
|
||||
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
|
||||
|
||||
void writeRepositoryId(Repository repository, File directory) throws IOException {
|
||||
INISection iniSection = new INISection(CONFIG_SECTION_SCMM);
|
||||
iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId());
|
||||
INIConfiguration iniConfiguration = new INIConfiguration();
|
||||
iniConfiguration.addSection(iniSection);
|
||||
new INIConfigurationWriter().write(iniConfiguration, new File(directory, CONFIG_FILE_NAME));
|
||||
}
|
||||
|
||||
String getRepositoryId(File directory) {
|
||||
try {
|
||||
return new INIConfigurationReader().read(new File(directory, CONFIG_FILE_NAME)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,11 +46,6 @@ import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
|
||||
import org.tmatesoft.svn.core.io.SVNRepository;
|
||||
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
|
||||
import org.tmatesoft.svn.util.SVNDebugLog;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.io.INIConfiguration;
|
||||
import sonia.scm.io.INIConfigurationReader;
|
||||
import sonia.scm.io.INIConfigurationWriter;
|
||||
import sonia.scm.io.INISection;
|
||||
import sonia.scm.logging.SVNKitLogger;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
@@ -87,9 +82,6 @@ public class SvnRepositoryHandler
|
||||
public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME,
|
||||
TYPE_DISPLAYNAME,
|
||||
SvnRepositoryServiceProvider.COMMANDS);
|
||||
private static final String CONFIG_FILE_NAME = "scm-manager.conf";
|
||||
private static final String CONFIG_SECTION_SCMM = "scmm";
|
||||
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
|
||||
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(SvnRepositoryHandler.class);
|
||||
@@ -223,18 +215,10 @@ public class SvnRepositoryHandler
|
||||
|
||||
@Override
|
||||
protected void postCreate(Repository repository, File directory) throws IOException {
|
||||
INISection iniSection = new INISection(CONFIG_SECTION_SCMM);
|
||||
iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId());
|
||||
INIConfiguration iniConfiguration = new INIConfiguration();
|
||||
iniConfiguration.addSection(iniSection);
|
||||
new INIConfigurationWriter().write(iniConfiguration, new File(directory, CONFIG_FILE_NAME));
|
||||
new SvnConfigHelper().writeRepositoryId(repository, directory);
|
||||
}
|
||||
|
||||
String getRepositoryId(File directory) {
|
||||
try {
|
||||
return new INIConfigurationReader().read(new File(directory, CONFIG_FILE_NAME)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e);
|
||||
}
|
||||
return new SvnConfigHelper().getRepositoryId(directory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static sonia.scm.version.Version.parse;
|
||||
|
||||
@Extension
|
||||
public class SvnV2UpdateStep implements UpdateStep {
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
private final UpdateStepRepositoryMetadataAccess<Path> repositoryMetadataAccess;
|
||||
|
||||
@Inject
|
||||
public SvnV2UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess<Path> repositoryMetadataAccess) {
|
||||
this.locationResolver = locationResolver;
|
||||
this.repositoryMetadataAccess = repositoryMetadataAccess;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doUpdate() {
|
||||
locationResolver.forClass(Path.class).forAllLocations(
|
||||
(repositoryId, path) -> {
|
||||
Repository repository = repositoryMetadataAccess.read(path);
|
||||
if (isSvnDirectory(repository)) {
|
||||
try {
|
||||
new SvnConfigHelper().writeRepositoryId(repository, path.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY).toFile());
|
||||
} catch (IOException e) {
|
||||
throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isSvnDirectory(Repository repository) {
|
||||
return SvnRepositoryHandler.TYPE_NAME.equals(repository.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version getTargetVersion() {
|
||||
return parse("2.0.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffectedDataType() {
|
||||
return "sonia.scm.plugin.svn";
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import sonia.scm.repository.BasicRepositoryLocationResolver;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationResolver {
|
||||
private final File tempDirectory;
|
||||
@@ -30,6 +31,11 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe
|
||||
public void setLocation(String repositoryId, T location) {
|
||||
throw new UnsupportedOperationException("not implemented for tests");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forAllLocations(BiConsumer<String, T> consumer) {
|
||||
consumer.accept("id", (T) tempDirectory.toPath());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { AsyncCreatable, Async } from "react-select";
|
||||
import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types";
|
||||
import {Async, AsyncCreatable} from "react-select";
|
||||
import type {AutocompleteObject, SelectValue} from "@scm-manager/ui-types";
|
||||
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
|
||||
|
||||
|
||||
type Props = {
|
||||
loadSuggestions: string => Promise<AutocompleteObject>,
|
||||
valueSelected: SelectValue => void,
|
||||
@@ -17,12 +16,9 @@ type Props = {
|
||||
creatable?: boolean
|
||||
};
|
||||
|
||||
|
||||
type State = {};
|
||||
|
||||
class Autocomplete extends React.Component<Props, State> {
|
||||
|
||||
|
||||
static defaultProps = {
|
||||
placeholder: "Type here",
|
||||
loadingMessage: "Loading...",
|
||||
@@ -34,7 +30,11 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
// We overwrite this to avoid running into a bug (https://github.com/JedWatson/react-select/issues/2944)
|
||||
isValidNewOption = (inputValue: string, selectValue: SelectValue, selectOptions: SelectValue[]) => {
|
||||
isValidNewOption = (
|
||||
inputValue: string,
|
||||
selectValue: SelectValue,
|
||||
selectOptions: SelectValue[]
|
||||
) => {
|
||||
const isNotDuplicated = !selectOptions
|
||||
.map(option => option.label)
|
||||
.includes(inputValue);
|
||||
@@ -43,12 +43,21 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions, creatable } = this.props;
|
||||
const {
|
||||
label,
|
||||
helpText,
|
||||
value,
|
||||
placeholder,
|
||||
loadingMessage,
|
||||
noOptionsMessage,
|
||||
loadSuggestions,
|
||||
creatable
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
{creatable?
|
||||
{creatable ? (
|
||||
<AsyncCreatable
|
||||
cacheOptions
|
||||
loadOptions={loadSuggestions}
|
||||
@@ -65,7 +74,7 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
:
|
||||
) : (
|
||||
<Async
|
||||
cacheOptions
|
||||
loadOptions={loadSuggestions}
|
||||
@@ -75,13 +84,11 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
loadingMessage={() => loadingMessage}
|
||||
noOptionsMessage={() => noOptionsMessage}
|
||||
/>
|
||||
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default Autocomplete;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { BackendError, ForbiddenError, UnauthorizedError } from "./errors";
|
||||
import {translate} from "react-i18next";
|
||||
import {BackendError, ForbiddenError, UnauthorizedError} from "./errors";
|
||||
import Notification from "./Notification";
|
||||
import BackendErrorNotification from "./BackendErrorNotification";
|
||||
|
||||
@@ -10,35 +10,33 @@ type Props = {
|
||||
error?: Error
|
||||
};
|
||||
|
||||
|
||||
class ErrorNotification extends React.Component<Props> {
|
||||
render() {
|
||||
const { t, error } = this.props;
|
||||
if (error) {
|
||||
if (error instanceof BackendError) {
|
||||
return <BackendErrorNotification error={error} />
|
||||
return <BackendErrorNotification error={error} />;
|
||||
} else if (error instanceof UnauthorizedError) {
|
||||
return (
|
||||
<Notification type="danger">
|
||||
<strong>{t("error-notification.prefix")}:</strong>{" "}
|
||||
{t("error-notification.timeout")}{" "}
|
||||
<strong>{t("errorNotification.prefix")}:</strong>{" "}
|
||||
{t("errorNotification.timeout")}{" "}
|
||||
<a href="javascript:window.location.reload(true)">
|
||||
{t("error-notification.loginLink")}
|
||||
{t("errorNotification.loginLink")}
|
||||
</a>
|
||||
</Notification>
|
||||
);
|
||||
} else if (error instanceof ForbiddenError) {
|
||||
return (
|
||||
<Notification type="danger">
|
||||
<strong>{t("error-notification.prefix")}:</strong>{" "}
|
||||
{t("error-notification.forbidden")}
|
||||
<strong>{t("errorNotification.prefix")}:</strong>{" "}
|
||||
{t("errorNotification.forbidden")}
|
||||
</Notification>
|
||||
)
|
||||
} else
|
||||
{
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Notification type="danger">
|
||||
<strong>{t("error-notification.prefix")}:</strong> {error.message}
|
||||
<strong>{t("errorNotification.prefix")}:</strong> {error.message}
|
||||
</Notification>
|
||||
);
|
||||
}
|
||||
@@ -47,4 +45,4 @@ class ErrorNotification extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(ErrorNotification);
|
||||
export default translate("commons")(ErrorNotification);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import {translate} from "react-i18next";
|
||||
import type AutocompleteProps from "./UserGroupAutocomplete";
|
||||
import UserGroupAutocomplete from "./UserGroupAutocomplete";
|
||||
|
||||
type Props = AutocompleteProps & {
|
||||
// Context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class GroupAutocomplete extends React.Component<Props> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<UserGroupAutocomplete
|
||||
label={t("autocomplete.group")}
|
||||
noOptionsMessage={t("autocomplete.noGroupOptions")}
|
||||
loadingMessage={t("autocomplete.loading")}
|
||||
placeholder={t("autocomplete.groupPlaceholder")}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(GroupAutocomplete);
|
||||
@@ -2,11 +2,18 @@
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type NotificationType = "primary" | "info" | "success" | "warning" | "danger";
|
||||
type NotificationType =
|
||||
| "primary"
|
||||
| "info"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "inherit";
|
||||
|
||||
type Props = {
|
||||
type: NotificationType,
|
||||
onClose?: () => void,
|
||||
className?: string,
|
||||
children?: React.Node
|
||||
};
|
||||
|
||||
@@ -24,9 +31,12 @@ class Notification extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { type, children } = this.props;
|
||||
const { type, className, children } = this.props;
|
||||
|
||||
const color = type !== "inherit" ? "is-" + type : "";
|
||||
|
||||
return (
|
||||
<div className={classNames("notification", "is-" + type)}>
|
||||
<div className={classNames("notification", color, className)}>
|
||||
{this.renderCloseButton()}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import {translate} from "react-i18next";
|
||||
import type AutocompleteProps from "./UserGroupAutocomplete";
|
||||
import UserGroupAutocomplete from "./UserGroupAutocomplete";
|
||||
|
||||
type Props = AutocompleteProps & {
|
||||
// Context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class UserAutocomplete extends React.Component<Props> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<UserGroupAutocomplete
|
||||
label={t("autocomplete.user")}
|
||||
noOptionsMessage={t("autocomplete.noUserOptions")}
|
||||
loadingMessage={t("autocomplete.loading")}
|
||||
placeholder={t("autocomplete.userPlaceholder")}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(UserAutocomplete);
|
||||