mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 19:45:51 +01:00
Feature/mirror (#1683)
Add mirror command and extension points. Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com> Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
2
gradle/changelog/git_ssl_context.yaml
Normal file
2
gradle/changelog/git_ssl_context.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: changed
|
||||||
|
description: Inject custom trust manager to git https connections ([#1675](https://github.com/scm-manager/scm-manager/pull/1675))
|
||||||
2
gradle/changelog/mirror_command.yaml
Normal file
2
gradle/changelog/mirror_command.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Add mirror command and extension points ([#1683](https://github.com/scm-manager/scm-manager/pull/1683))
|
||||||
2
gradle/changelog/svn_mirror_command.yaml
Normal file
2
gradle/changelog/svn_mirror_command.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Implement Subversion mirror command ([#1660](https://github.com/scm-manager/scm-manager/pull/1660))
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
|
||||||
|
public interface RepositoryLinkProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the internal api link for the given repository.
|
||||||
|
*
|
||||||
|
* @param namespaceAndName The namespace and name of the repository.
|
||||||
|
* @return Internal api link for the given repository.
|
||||||
|
*/
|
||||||
|
String get(NamespaceAndName namespaceAndName);
|
||||||
|
}
|
||||||
79
scm-core/src/main/java/sonia/scm/collect/EvictingQueue.java
Normal file
79
scm-core/src/main/java/sonia/scm/collect/EvictingQueue.java
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.collect;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.collect.ForwardingQueue;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public final class EvictingQueue<E> extends ForwardingQueue<E> {
|
||||||
|
|
||||||
|
private final ArrayDeque<E> delegate;
|
||||||
|
@VisibleForTesting
|
||||||
|
int maxSize;
|
||||||
|
|
||||||
|
public EvictingQueue() {
|
||||||
|
this.delegate = new ArrayDeque<>();
|
||||||
|
this.maxSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EvictingQueue(int maxSize) {
|
||||||
|
Preconditions.checkArgument(maxSize >= 0, "maxSize (%s) must >= 0", maxSize);
|
||||||
|
this.delegate = new ArrayDeque<>(maxSize);
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E> EvictingQueue<E> create(int maxSize) {
|
||||||
|
return new EvictingQueue<>(maxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Queue<E> delegate() {
|
||||||
|
return this.delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public boolean add(@NotNull E e) {
|
||||||
|
Preconditions.checkNotNull(e);
|
||||||
|
if (this.maxSize == 0) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
while (this.size() >= this.maxSize) {
|
||||||
|
this.delegate.remove();
|
||||||
|
}
|
||||||
|
this.delegate.add(e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ package sonia.scm.repository;
|
|||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -36,6 +37,7 @@ import java.util.function.Supplier;
|
|||||||
/**
|
/**
|
||||||
* Default implementation of {@link RepositoryExportingCheck}. This tracks the exporting status of repositories.
|
* Default implementation of {@link RepositoryExportingCheck}. This tracks the exporting status of repositories.
|
||||||
*/
|
*/
|
||||||
|
@Extension
|
||||||
public final class DefaultRepositoryExportingCheck implements RepositoryExportingCheck {
|
public final class DefaultRepositoryExportingCheck implements RepositoryExportingCheck {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultRepositoryExportingCheck.class);
|
private static final Logger LOG = LoggerFactory.getLogger(DefaultRepositoryExportingCheck.class);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
import com.github.legman.Subscribe;
|
import com.github.legman.Subscribe;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -35,6 +36,7 @@ import java.util.HashSet;
|
|||||||
* {@link RepositoryModificationEvent}s. The initial set of archived repositories is read by
|
* {@link RepositoryModificationEvent}s. The initial set of archived repositories is read by
|
||||||
* {@link EventDrivenRepositoryArchiveCheckInitializer} on startup.
|
* {@link EventDrivenRepositoryArchiveCheckInitializer} on startup.
|
||||||
*/
|
*/
|
||||||
|
@Extension
|
||||||
public final class EventDrivenRepositoryArchiveCheck implements RepositoryArchivedCheck {
|
public final class EventDrivenRepositoryArchiveCheck implements RepositoryArchivedCheck {
|
||||||
|
|
||||||
private static final Collection<String> ARCHIVED_REPOSITORIES = Collections.synchronizedSet(new HashSet<>());
|
private static final Collection<String> ARCHIVED_REPOSITORIES = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import sonia.scm.plugin.ExtensionPoint;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read only check could be used to mark a repository as read only.
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
@ExtensionPoint
|
||||||
|
public interface ReadOnlyCheck {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the reason for the write protection.
|
||||||
|
* @return reason for write protection
|
||||||
|
*/
|
||||||
|
String getReason();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the repository with the given id is read only.
|
||||||
|
* @param repositoryId repository id
|
||||||
|
* @return {@code true} if repository is read only
|
||||||
|
*/
|
||||||
|
boolean isReadOnly(String repositoryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the repository is read only.
|
||||||
|
* @param repository repository
|
||||||
|
* @return {@code true} if repository is read only
|
||||||
|
*/
|
||||||
|
default boolean isReadOnly(Repository repository) {
|
||||||
|
return isReadOnly(repository.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws a {@link ReadOnlyException} if the repository is read only.
|
||||||
|
* @param repository repository
|
||||||
|
*/
|
||||||
|
default void check(Repository repository) {
|
||||||
|
check(repository.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws a {@link ReadOnlyException} if the repository with th id is read only.
|
||||||
|
* @param repositoryId repository id
|
||||||
|
*/
|
||||||
|
default void check(String repositoryId) {
|
||||||
|
if (isReadOnly(repositoryId)) {
|
||||||
|
throw new ReadOnlyException(entity(Repository.class, repositoryId).build(), getReason());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean isReadOnly(String permission, String repositoryId) {
|
||||||
|
return isReadOnly(repositoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,23 +30,28 @@ import sonia.scm.SCMContextProvider;
|
|||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes read only permissions for {@link RepositoryPermissionGuard} at startup.
|
* Initializes read only permissions and their checks at startup.
|
||||||
*/
|
*/
|
||||||
@Extension
|
@Extension
|
||||||
@EagerSingleton
|
@EagerSingleton
|
||||||
final class RepositoryPermissionGuardInitializer implements Initable {
|
final class ReadOnlyCheckInitializer implements Initable {
|
||||||
|
|
||||||
private final PermissionProvider permissionProvider;
|
private final PermissionProvider permissionProvider;
|
||||||
|
private final Set<ReadOnlyCheck> readOnlyChecks;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
RepositoryPermissionGuardInitializer(PermissionProvider permissionProvider) {
|
ReadOnlyCheckInitializer(PermissionProvider permissionProvider, Set<ReadOnlyCheck> readOnlyChecks) {
|
||||||
this.permissionProvider = permissionProvider;
|
this.permissionProvider = permissionProvider;
|
||||||
|
this.readOnlyChecks = readOnlyChecks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(SCMContextProvider context) {
|
public void init(SCMContextProvider context) {
|
||||||
RepositoryPermissionGuard.setReadOnlyVerbs(permissionProvider.readOnlyVerbs());
|
RepositoryPermissionGuard.setReadOnlyVerbs(permissionProvider.readOnlyVerbs());
|
||||||
|
RepositoryPermissionGuard.setReadOnlyChecks(readOnlyChecks);
|
||||||
|
RepositoryReadOnlyChecker.setReadOnlyChecks(readOnlyChecks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import sonia.scm.ContextEntry;
|
||||||
|
import sonia.scm.ExceptionWithContext;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read only exception is thrown if someone tries to execute a write command on a read only repository.
|
||||||
|
*
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
public class ReadOnlyException extends ExceptionWithContext {
|
||||||
|
|
||||||
|
public ReadOnlyException(List<ContextEntry> context, String message) {
|
||||||
|
super(context, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCode() {
|
||||||
|
return "BaSXkAztI1";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ package sonia.scm.repository;
|
|||||||
*
|
*
|
||||||
* @since 2.12.0
|
* @since 2.12.0
|
||||||
*/
|
*/
|
||||||
public interface RepositoryArchivedCheck {
|
public interface RepositoryArchivedCheck extends ReadOnlyCheck {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the repository with the given id is archived or not.
|
* Checks whether the repository with the given id is archived or not.
|
||||||
@@ -47,4 +47,28 @@ public interface RepositoryArchivedCheck {
|
|||||||
default boolean isArchived(Repository repository) {
|
default boolean isArchived(Repository repository) {
|
||||||
return isArchived(repository.getId());
|
return isArchived(repository.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default boolean isReadOnly(String repositoryId) {
|
||||||
|
return isArchived(repositoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default String getReason() {
|
||||||
|
return "repository is archived";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void check(Repository repository) {
|
||||||
|
if (repository.isArchived() || isArchived(repository)) {
|
||||||
|
throw new RepositoryArchivedException(repository);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void check(String repositoryId) {
|
||||||
|
if (isArchived(repositoryId)) {
|
||||||
|
throw new RepositoryArchivedException(repositoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,11 @@
|
|||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
import sonia.scm.ExceptionWithContext;
|
|
||||||
|
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
public class RepositoryArchivedException extends ExceptionWithContext {
|
@SuppressWarnings("java:S110") // large history is ok for exceptions
|
||||||
|
public class RepositoryArchivedException extends ReadOnlyException {
|
||||||
|
|
||||||
public static final String CODE = "3hSIlptme1";
|
public static final String CODE = "3hSIlptme1";
|
||||||
|
|
||||||
@@ -37,6 +36,13 @@ public class RepositoryArchivedException extends ExceptionWithContext {
|
|||||||
super(entity(repository).build(), format("Repository %s is marked as archived and must not be modified", repository));
|
super(entity(repository).build(), format("Repository %s is marked as archived and must not be modified", repository));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RepositoryArchivedException(String repositoryId) {
|
||||||
|
super(
|
||||||
|
entity(Repository.class, repositoryId).build(),
|
||||||
|
format("Repository with id %s is marked as archived and must not be modified", repositoryId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCode() {
|
public String getCode() {
|
||||||
return CODE;
|
return CODE;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import java.util.function.Supplier;
|
|||||||
*
|
*
|
||||||
* @since 2.14.0
|
* @since 2.14.0
|
||||||
*/
|
*/
|
||||||
public interface RepositoryExportingCheck {
|
public interface RepositoryExportingCheck extends ReadOnlyCheck {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the repository with the given id is currently (that is, at this moment) being exported or not.
|
* Checks whether the repository with the given id is currently (that is, at this moment) being exported or not.
|
||||||
@@ -59,4 +59,21 @@ public interface RepositoryExportingCheck {
|
|||||||
* @return The result of the callback.
|
* @return The result of the callback.
|
||||||
*/
|
*/
|
||||||
<T> T withExportingLock(Repository repository, Supplier<T> callback);
|
<T> T withExportingLock(Repository repository, Supplier<T> callback);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default boolean isReadOnly(String repositoryId) {
|
||||||
|
return isExporting(repositoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default String getReason() {
|
||||||
|
return "repository is exporting";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void check(String repositoryId) {
|
||||||
|
if (isExporting(repositoryId)) {
|
||||||
|
throw new RepositoryExportingException(repositoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,11 @@
|
|||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
import sonia.scm.ExceptionWithContext;
|
|
||||||
|
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
public class RepositoryExportingException extends ExceptionWithContext {
|
@SuppressWarnings("java:S110") // large history is ok for exceptions
|
||||||
|
public class RepositoryExportingException extends ReadOnlyException {
|
||||||
|
|
||||||
public static final String CODE = "1mSNlpe1V1";
|
public static final String CODE = "1mSNlpe1V1";
|
||||||
|
|
||||||
@@ -37,6 +36,13 @@ public class RepositoryExportingException extends ExceptionWithContext {
|
|||||||
super(entity(repository).build(), format("Repository %s is currently being exported and must not be modified", repository));
|
super(entity(repository).build(), format("Repository %s is currently being exported and must not be modified", repository));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RepositoryExportingException(String repositoryId) {
|
||||||
|
super(
|
||||||
|
entity(Repository.class, repositoryId).build(),
|
||||||
|
format("Repository with id %s is currently being exported and must not be modified", repositoryId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCode() {
|
public String getCode() {
|
||||||
return CODE;
|
return CODE;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ package sonia.scm.repository;
|
|||||||
|
|
||||||
import com.github.sdorra.ssp.PermissionActionCheckInterceptor;
|
import com.github.sdorra.ssp.PermissionActionCheckInterceptor;
|
||||||
import com.github.sdorra.ssp.PermissionGuard;
|
import com.github.sdorra.ssp.PermissionGuard;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
import org.apache.shiro.authz.AuthorizationException;
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
|
|
||||||
@@ -34,45 +35,62 @@ import java.util.Collections;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
import static sonia.scm.repository.DefaultRepositoryExportingCheck.isRepositoryExporting;
|
|
||||||
import static sonia.scm.repository.EventDrivenRepositoryArchiveCheck.isRepositoryArchived;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This intercepts permission checks for repositories and blocks write permissions for archived repositories.
|
* This intercepts permission checks for repositories and blocks write permissions for archived repositories.
|
||||||
* Read only permissions are set at startup by {@link RepositoryPermissionGuardInitializer}.
|
* Read only permissions are set at startup by {@link ReadOnlyCheckInitializer}.
|
||||||
*/
|
*/
|
||||||
public class RepositoryPermissionGuard implements PermissionGuard<Repository> {
|
public class RepositoryPermissionGuard implements PermissionGuard<Repository> {
|
||||||
|
|
||||||
private static final Collection<String> READ_ONLY_VERBS = Collections.synchronizedSet(new HashSet<>());
|
private static final Collection<String> READ_ONLY_VERBS = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
private static Collection<ReadOnlyCheck> readOnlyChecks = Collections.emptySet();
|
||||||
|
|
||||||
static void setReadOnlyVerbs(Collection<String> readOnlyVerbs) {
|
static void setReadOnlyVerbs(Collection<String> readOnlyVerbs) {
|
||||||
READ_ONLY_VERBS.addAll(readOnlyVerbs);
|
READ_ONLY_VERBS.addAll(readOnlyVerbs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets static read only checks.
|
||||||
|
* @param readOnlyChecks read only checks
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
static void setReadOnlyChecks(Collection<ReadOnlyCheck> readOnlyChecks) {
|
||||||
|
RepositoryPermissionGuard.readOnlyChecks = ImmutableSet.copyOf(readOnlyChecks);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PermissionActionCheckInterceptor<Repository> intercept(String permission) {
|
public PermissionActionCheckInterceptor<Repository> intercept(String permission) {
|
||||||
if (READ_ONLY_VERBS.contains(permission)) {
|
if (READ_ONLY_VERBS.contains(permission)) {
|
||||||
return new PermissionActionCheckInterceptor<Repository>() {};
|
return new PermissionActionCheckInterceptor<Repository>() {};
|
||||||
} else {
|
} else {
|
||||||
return new WriteInterceptor();
|
return new WriteInterceptor(permission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WriteInterceptor implements PermissionActionCheckInterceptor<Repository> {
|
private static class WriteInterceptor implements PermissionActionCheckInterceptor<Repository> {
|
||||||
|
|
||||||
|
private final String permission;
|
||||||
|
|
||||||
|
private WriteInterceptor(String permission) {
|
||||||
|
this.permission = permission;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void check(Subject subject, String id, Runnable delegate) {
|
public void check(Subject subject, String id, Runnable delegate) {
|
||||||
delegate.run();
|
delegate.run();
|
||||||
if (isRepositoryArchived(id)) {
|
for (ReadOnlyCheck check : readOnlyChecks) {
|
||||||
throw new AuthorizationException("repository is archived");
|
if (check.isReadOnly(permission, id)) {
|
||||||
}
|
throw new AuthorizationException(check.getReason());
|
||||||
if (isRepositoryExporting(id)) {
|
}
|
||||||
throw new AuthorizationException("repository is exporting");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPermitted(Subject subject, String id, BooleanSupplier delegate) {
|
public boolean isPermitted(Subject subject, String id, BooleanSupplier delegate) {
|
||||||
return !isRepositoryArchived(id) && !isRepositoryExporting(id) && delegate.getAsBoolean();
|
return isWritable(id) && delegate.getAsBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWritable(String id) {
|
||||||
|
return readOnlyChecks.stream().noneMatch(c -> c.isReadOnly(permission, id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,15 @@
|
|||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks, whether a repository has to be considered read only. Currently, this includes {@link RepositoryArchivedCheck}
|
* Checks, whether a repository has to be considered read only. Currently, this includes {@link RepositoryArchivedCheck}
|
||||||
@@ -34,13 +42,38 @@ import javax.inject.Inject;
|
|||||||
*/
|
*/
|
||||||
public final class RepositoryReadOnlyChecker {
|
public final class RepositoryReadOnlyChecker {
|
||||||
|
|
||||||
private final RepositoryArchivedCheck archivedCheck;
|
private static Set<ReadOnlyCheck> staticChecks = Collections.emptySet();
|
||||||
private final RepositoryExportingCheck exportingCheck;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set static read only checks.
|
||||||
|
*
|
||||||
|
* @param readOnlyChecks static read only checks
|
||||||
|
*/
|
||||||
|
static void setReadOnlyChecks(Collection<ReadOnlyCheck> readOnlyChecks) {
|
||||||
|
staticChecks = ImmutableSet.copyOf(readOnlyChecks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We should use {@link #staticChecks} instead of checks.
|
||||||
|
* Checks exists only for backward compatibility.
|
||||||
|
*/
|
||||||
|
private final Set<ReadOnlyCheck> checks = new HashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new read only checker, which uses only static checks.
|
||||||
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
|
public RepositoryReadOnlyChecker() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new read only checker.
|
||||||
|
*
|
||||||
|
* @deprecated use {@link RepositoryReadOnlyChecker#setReadOnlyChecks(Collection)} instead
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public RepositoryReadOnlyChecker(RepositoryArchivedCheck archivedCheck, RepositoryExportingCheck exportingCheck) {
|
public RepositoryReadOnlyChecker(RepositoryArchivedCheck archivedCheck, RepositoryExportingCheck exportingCheck) {
|
||||||
this.archivedCheck = archivedCheck;
|
this.checks.addAll(Arrays.asList(archivedCheck, exportingCheck));
|
||||||
this.exportingCheck = exportingCheck;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,29 +91,15 @@ public final class RepositoryReadOnlyChecker {
|
|||||||
* @return <code>true</code> if any check locks the repository to read only access.
|
* @return <code>true</code> if any check locks the repository to read only access.
|
||||||
*/
|
*/
|
||||||
public boolean isReadOnly(String repositoryId) {
|
public boolean isReadOnly(String repositoryId) {
|
||||||
return archivedCheck.isArchived(repositoryId) || exportingCheck.isExporting(repositoryId);
|
return Stream.concat(checks.stream(), staticChecks.stream()).anyMatch(check -> check.isReadOnly(repositoryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the repository may be modified.
|
* Checks if the repository may be modified.
|
||||||
*
|
*
|
||||||
* @throws RepositoryArchivedException if the repository is archived
|
* @throws ReadOnlyException if the repository is marked as read only
|
||||||
* @throws RepositoryExportingException if the repository is currently being exported
|
|
||||||
*/
|
*/
|
||||||
public static void checkReadOnly(Repository repository) {
|
public static void checkReadOnly(Repository repository) {
|
||||||
if (isArchived(repository)) {
|
staticChecks.forEach(check -> check.check(repository));
|
||||||
throw new RepositoryArchivedException(repository);
|
|
||||||
}
|
|
||||||
if (isExporting(repository)) {
|
|
||||||
throw new RepositoryExportingException(repository);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isExporting(Repository repository) {
|
|
||||||
return DefaultRepositoryExportingCheck.isRepositoryExporting(repository.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isArchived(Repository repository) {
|
|
||||||
return repository.isArchived() || EventDrivenRepositoryArchiveCheck.isRepositoryArchived(repository.getId());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,17 +30,14 @@ package sonia.scm.repository;
|
|||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
* @since 1.23
|
* @since 1.23
|
||||||
*/
|
*/
|
||||||
public class WrappedRepositoryHookEvent extends RepositoryHookEvent
|
public class WrappedRepositoryHookEvent extends RepositoryHookEvent {
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new WrappedRepositoryHookEvent.
|
* Constructs a new WrappedRepositoryHookEvent.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param wrappedEvent event to wrap
|
* @param wrappedEvent event to wrap
|
||||||
*/
|
*/
|
||||||
protected WrappedRepositoryHookEvent(RepositoryHookEvent wrappedEvent)
|
protected WrappedRepositoryHookEvent(RepositoryHookEvent wrappedEvent) {
|
||||||
{
|
|
||||||
super(wrappedEvent.getContext(), wrappedEvent.getRepository(),
|
super(wrappedEvent.getContext(), wrappedEvent.getRepository(),
|
||||||
wrappedEvent.getType());
|
wrappedEvent.getType());
|
||||||
}
|
}
|
||||||
@@ -50,28 +47,24 @@ public class WrappedRepositoryHookEvent extends RepositoryHookEvent
|
|||||||
/**
|
/**
|
||||||
* Returns a wrapped instance of the {@link RepositoryHookEvent}-
|
* Returns a wrapped instance of the {@link RepositoryHookEvent}-
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param event event to wrap
|
* @param event event to wrap
|
||||||
*
|
|
||||||
* @return wrapper
|
* @return wrapper
|
||||||
*/
|
*/
|
||||||
public static WrappedRepositoryHookEvent wrap(RepositoryHookEvent event)
|
public static WrappedRepositoryHookEvent wrap(RepositoryHookEvent event) {
|
||||||
{
|
|
||||||
WrappedRepositoryHookEvent wrappedEvent = null;
|
WrappedRepositoryHookEvent wrappedEvent = null;
|
||||||
|
|
||||||
switch (event.getType())
|
switch (event.getType()) {
|
||||||
{
|
case POST_RECEIVE:
|
||||||
case POST_RECEIVE :
|
|
||||||
wrappedEvent = new PostReceiveRepositoryHookEvent(event);
|
wrappedEvent = new PostReceiveRepositoryHookEvent(event);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PRE_RECEIVE :
|
case PRE_RECEIVE:
|
||||||
wrappedEvent = new PreReceiveRepositoryHookEvent(event);
|
wrappedEvent = new PreReceiveRepositoryHookEvent(event);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default :
|
default:
|
||||||
throw new IllegalArgumentException("unsupported hook event type");
|
throw new IllegalArgumentException("unsupported hook event type");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,5 +72,10 @@ public enum Command
|
|||||||
/**
|
/**
|
||||||
* @since 2.17.0
|
* @since 2.17.0
|
||||||
*/
|
*/
|
||||||
FULL_HEALTH_CHECK;
|
FULL_HEALTH_CHECK,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
MIRROR;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
public interface Credential {
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.spi.MirrorCommand;
|
||||||
|
import sonia.scm.repository.spi.MirrorCommandRequest;
|
||||||
|
import sonia.scm.security.PublicKey;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public final class MirrorCommandBuilder {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MirrorCommandBuilder.class);
|
||||||
|
|
||||||
|
private final MirrorCommand mirrorCommand;
|
||||||
|
private final Repository targetRepository;
|
||||||
|
|
||||||
|
private String sourceUrl;
|
||||||
|
private Collection<Credential> credentials = emptyList();
|
||||||
|
private List<PublicKey> publicKeys = emptyList();
|
||||||
|
private MirrorFilter filter = new MirrorFilter() {};
|
||||||
|
|
||||||
|
MirrorCommandBuilder(MirrorCommand mirrorCommand, Repository targetRepository) {
|
||||||
|
this.mirrorCommand = mirrorCommand;
|
||||||
|
this.targetRepository = targetRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorCommandBuilder setCredentials(Credential credential, Credential... furtherCredentials) {
|
||||||
|
this.credentials = new ArrayList<>();
|
||||||
|
credentials.add(credential);
|
||||||
|
credentials.addAll(asList(furtherCredentials));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorCommandBuilder setCredentials(Collection<Credential> credentials) {
|
||||||
|
this.credentials = credentials;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorCommandBuilder setPublicKeys(PublicKey... publicKeys) {
|
||||||
|
this.publicKeys = Arrays.asList(publicKeys);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorCommandBuilder setPublicKeys(Collection<PublicKey> publicKeys) {
|
||||||
|
this.publicKeys = new ArrayList<>(publicKeys);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorCommandBuilder setSourceUrl(String sourceUrl) {
|
||||||
|
this.sourceUrl = sourceUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorCommandBuilder setFilter(MirrorFilter filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorCommandResult initialCall() {
|
||||||
|
LOG.info("Creating mirror for {} in repository {}", sourceUrl, targetRepository);
|
||||||
|
MirrorCommandRequest mirrorCommandRequest = createRequest();
|
||||||
|
return mirrorCommand.mirror(mirrorCommandRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorCommandResult update() {
|
||||||
|
LOG.debug("Updating mirror for {} in repository {}", sourceUrl, targetRepository);
|
||||||
|
MirrorCommandRequest mirrorCommandRequest = createRequest();
|
||||||
|
return mirrorCommand.update(mirrorCommandRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandRequest createRequest() {
|
||||||
|
MirrorCommandRequest mirrorCommandRequest = new MirrorCommandRequest();
|
||||||
|
mirrorCommandRequest.setSourceUrl(sourceUrl);
|
||||||
|
mirrorCommandRequest.setCredentials(credentials);
|
||||||
|
mirrorCommandRequest.setFilter(filter);
|
||||||
|
mirrorCommandRequest.setPublicKeys(publicKeys);
|
||||||
|
Preconditions.checkArgument(mirrorCommandRequest.isValid(), "source url has to be specified");
|
||||||
|
return mirrorCommandRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Beta
|
||||||
|
public final class MirrorCommandResult {
|
||||||
|
|
||||||
|
private final ResultType result;
|
||||||
|
private final List<String> log;
|
||||||
|
private final Duration duration;
|
||||||
|
|
||||||
|
public MirrorCommandResult(ResultType result, List<String> log, Duration duration) {
|
||||||
|
this.result = result;
|
||||||
|
this.log = log;
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultType getResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getLog() {
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getDuration() {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ResultType {
|
||||||
|
OK,
|
||||||
|
REJECTED_UPDATES,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import sonia.scm.repository.Changeset;
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
|
||||||
|
@Beta
|
||||||
|
public interface MirrorFilter {
|
||||||
|
|
||||||
|
default Filter getFilter(FilterContext context) {
|
||||||
|
return new Filter() {};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Filter {
|
||||||
|
|
||||||
|
default Result acceptBranch(BranchUpdate branch) {
|
||||||
|
return Result.accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
default Result acceptTag(TagUpdate tag) {
|
||||||
|
return Result.accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterContext {
|
||||||
|
|
||||||
|
default Collection<BranchUpdate> getBranchUpdates() {
|
||||||
|
return emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
default Collection<TagUpdate> getTagUpdates() {
|
||||||
|
return emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Result {
|
||||||
|
private final boolean accepted;
|
||||||
|
private final String rejectReason;
|
||||||
|
|
||||||
|
private Result(boolean accepted, String rejectReason) {
|
||||||
|
this.accepted = accepted;
|
||||||
|
this.rejectReason = rejectReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result reject(String rejectReason) {
|
||||||
|
return new Result(false, rejectReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result reject() {
|
||||||
|
return new Result(false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result accept() {
|
||||||
|
return new Result(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAccepted() {
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getRejectReason() {
|
||||||
|
return Optional.ofNullable(rejectReason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UpdateType {
|
||||||
|
CREATE, DELETE, UPDATE
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BranchUpdate {
|
||||||
|
String getBranchName();
|
||||||
|
|
||||||
|
Optional<Changeset> getChangeset();
|
||||||
|
|
||||||
|
Optional<String> getNewRevision();
|
||||||
|
|
||||||
|
Optional<String> getOldRevision();
|
||||||
|
|
||||||
|
Optional<UpdateType> getUpdateType();
|
||||||
|
|
||||||
|
boolean isForcedUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagUpdate {
|
||||||
|
String getTagName();
|
||||||
|
|
||||||
|
Optional<Tag> getTag();
|
||||||
|
|
||||||
|
Optional<String> getNewRevision();
|
||||||
|
|
||||||
|
Optional<String> getOldRevision();
|
||||||
|
|
||||||
|
Optional<UpdateType> getUpdateType();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class Pkcs12ClientCertificateCredential implements Credential {
|
||||||
|
|
||||||
|
private final byte[] certificate;
|
||||||
|
private final char[] password;
|
||||||
|
}
|
||||||
@@ -158,8 +158,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public BlameCommandBuilder getBlameCommand() {
|
public BlameCommandBuilder getBlameCommand() {
|
||||||
LOG.debug("create blame command for repository {}",
|
LOG.debug("create blame command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(),
|
return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(),
|
||||||
repository, preProcessorUtil);
|
repository, preProcessorUtil);
|
||||||
@@ -173,8 +172,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public BranchesCommandBuilder getBranchesCommand() {
|
public BranchesCommandBuilder getBranchesCommand() {
|
||||||
LOG.debug("create branches command for repository {}",
|
LOG.debug("create branches command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new BranchesCommandBuilder(cacheManager,
|
return new BranchesCommandBuilder(cacheManager,
|
||||||
provider.getBranchesCommand(), repository);
|
provider.getBranchesCommand(), repository);
|
||||||
@@ -190,8 +188,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
public BranchCommandBuilder getBranchCommand() {
|
public BranchCommandBuilder getBranchCommand() {
|
||||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||||
RepositoryPermissions.push(getRepository()).check();
|
RepositoryPermissions.push(getRepository()).check();
|
||||||
LOG.debug("create branch command for repository {}",
|
LOG.debug("create branch command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new BranchCommandBuilder(repository, provider.getBranchCommand());
|
return new BranchCommandBuilder(repository, provider.getBranchCommand());
|
||||||
}
|
}
|
||||||
@@ -204,8 +201,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public BrowseCommandBuilder getBrowseCommand() {
|
public BrowseCommandBuilder getBrowseCommand() {
|
||||||
LOG.debug("create browse command for repository {}",
|
LOG.debug("create browse command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(),
|
return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(),
|
||||||
repository, preProcessorUtil);
|
repository, preProcessorUtil);
|
||||||
@@ -220,8 +216,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 1.43
|
* @since 1.43
|
||||||
*/
|
*/
|
||||||
public BundleCommandBuilder getBundleCommand() {
|
public BundleCommandBuilder getBundleCommand() {
|
||||||
LOG.debug("create bundle command for repository {}",
|
LOG.debug("create bundle command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new BundleCommandBuilder(provider.getBundleCommand(), repositoryExportingCheck, repository);
|
return new BundleCommandBuilder(provider.getBundleCommand(), repositoryExportingCheck, repository);
|
||||||
}
|
}
|
||||||
@@ -234,8 +229,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public CatCommandBuilder getCatCommand() {
|
public CatCommandBuilder getCatCommand() {
|
||||||
LOG.debug("create cat command for repository {}",
|
LOG.debug("create cat command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new CatCommandBuilder(provider.getCatCommand());
|
return new CatCommandBuilder(provider.getCatCommand());
|
||||||
}
|
}
|
||||||
@@ -249,8 +243,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public DiffCommandBuilder getDiffCommand() {
|
public DiffCommandBuilder getDiffCommand() {
|
||||||
LOG.debug("create diff command for repository {}",
|
LOG.debug("create diff command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures());
|
return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures());
|
||||||
}
|
}
|
||||||
@@ -264,8 +257,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public DiffResultCommandBuilder getDiffResultCommand() {
|
public DiffResultCommandBuilder getDiffResultCommand() {
|
||||||
LOG.debug("create diff result command for repository {}",
|
LOG.debug("create diff result command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new DiffResultCommandBuilder(provider.getDiffResultCommand(), provider.getSupportedFeatures());
|
return new DiffResultCommandBuilder(provider.getDiffResultCommand(), provider.getSupportedFeatures());
|
||||||
}
|
}
|
||||||
@@ -280,8 +272,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 1.31
|
* @since 1.31
|
||||||
*/
|
*/
|
||||||
public IncomingCommandBuilder getIncomingCommand() {
|
public IncomingCommandBuilder getIncomingCommand() {
|
||||||
LOG.debug("create incoming command for repository {}",
|
LOG.debug("create incoming command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new IncomingCommandBuilder(cacheManager,
|
return new IncomingCommandBuilder(cacheManager,
|
||||||
provider.getIncomingCommand(), repository, preProcessorUtil);
|
provider.getIncomingCommand(), repository, preProcessorUtil);
|
||||||
@@ -295,8 +286,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public LogCommandBuilder getLogCommand() {
|
public LogCommandBuilder getLogCommand() {
|
||||||
LOG.debug("create log command for repository {}",
|
LOG.debug("create log command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
|
return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
|
||||||
repository, preProcessorUtil, provider.getSupportedFeatures());
|
repository, preProcessorUtil, provider.getSupportedFeatures());
|
||||||
@@ -323,8 +313,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 1.31
|
* @since 1.31
|
||||||
*/
|
*/
|
||||||
public OutgoingCommandBuilder getOutgoingCommand() {
|
public OutgoingCommandBuilder getOutgoingCommand() {
|
||||||
LOG.debug("create outgoing command for repository {}",
|
LOG.debug("create outgoing command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new OutgoingCommandBuilder(cacheManager,
|
return new OutgoingCommandBuilder(cacheManager,
|
||||||
provider.getOutgoingCommand(), repository, preProcessorUtil);
|
provider.getOutgoingCommand(), repository, preProcessorUtil);
|
||||||
@@ -340,8 +329,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
*/
|
*/
|
||||||
public PullCommandBuilder getPullCommand() {
|
public PullCommandBuilder getPullCommand() {
|
||||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||||
LOG.debug("create pull command for repository {}",
|
LOG.debug("create pull command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new PullCommandBuilder(provider.getPullCommand(), repository);
|
return new PullCommandBuilder(provider.getPullCommand(), repository);
|
||||||
}
|
}
|
||||||
@@ -355,8 +343,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 1.31
|
* @since 1.31
|
||||||
*/
|
*/
|
||||||
public PushCommandBuilder getPushCommand() {
|
public PushCommandBuilder getPushCommand() {
|
||||||
LOG.debug("create push command for repository {}",
|
LOG.debug("create push command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new PushCommandBuilder(provider.getPushCommand());
|
return new PushCommandBuilder(provider.getPushCommand());
|
||||||
}
|
}
|
||||||
@@ -378,8 +365,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
*/
|
*/
|
||||||
public TagsCommandBuilder getTagsCommand() {
|
public TagsCommandBuilder getTagsCommand() {
|
||||||
LOG.debug("create tags command for repository {}",
|
LOG.debug("create tags command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(),
|
return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(),
|
||||||
repository);
|
repository);
|
||||||
@@ -406,8 +392,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 1.43
|
* @since 1.43
|
||||||
*/
|
*/
|
||||||
public UnbundleCommandBuilder getUnbundleCommand() {
|
public UnbundleCommandBuilder getUnbundleCommand() {
|
||||||
LOG.debug("create unbundle command for repository {}",
|
LOG.debug("create unbundle command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new UnbundleCommandBuilder(provider.getUnbundleCommand(),
|
return new UnbundleCommandBuilder(provider.getUnbundleCommand(),
|
||||||
repository);
|
repository);
|
||||||
@@ -424,8 +409,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
*/
|
*/
|
||||||
public MergeCommandBuilder getMergeCommand() {
|
public MergeCommandBuilder getMergeCommand() {
|
||||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||||
LOG.debug("create merge command for repository {}",
|
LOG.debug("create merge command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new MergeCommandBuilder(provider.getMergeCommand(), eMail);
|
return new MergeCommandBuilder(provider.getMergeCommand(), eMail);
|
||||||
}
|
}
|
||||||
@@ -446,8 +430,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
*/
|
*/
|
||||||
public ModifyCommandBuilder getModifyCommand() {
|
public ModifyCommandBuilder getModifyCommand() {
|
||||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||||
LOG.debug("create modify command for repository {}",
|
LOG.debug("create modify command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
|
|
||||||
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, repository.getId(), eMail);
|
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, repository.getId(), eMail);
|
||||||
}
|
}
|
||||||
@@ -461,8 +444,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 2.10.0
|
* @since 2.10.0
|
||||||
*/
|
*/
|
||||||
public LookupCommandBuilder getLookupCommand() {
|
public LookupCommandBuilder getLookupCommand() {
|
||||||
LOG.debug("create lookup command for repository {}",
|
LOG.debug("create lookup command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
return new LookupCommandBuilder(provider.getLookupCommand());
|
return new LookupCommandBuilder(provider.getLookupCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,11 +458,25 @@ public final class RepositoryService implements Closeable {
|
|||||||
* @since 2.17.0
|
* @since 2.17.0
|
||||||
*/
|
*/
|
||||||
public FullHealthCheckCommandBuilder getFullCheckCommand() {
|
public FullHealthCheckCommandBuilder getFullCheckCommand() {
|
||||||
LOG.debug("create full check command for repository {}",
|
LOG.debug("create full check command for repository {}", repository);
|
||||||
repository.getNamespaceAndName());
|
|
||||||
return new FullHealthCheckCommandBuilder(provider.getFullHealthCheckCommand());
|
return new FullHealthCheckCommandBuilder(provider.getFullHealthCheckCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mirror command creates a 'mirror' of an existing repository (specified by a URL) by copying all data
|
||||||
|
* to the repository of this service. Therefore this repository has to be empty (otherwise the behaviour is
|
||||||
|
* not specified).
|
||||||
|
*
|
||||||
|
* @return instance of {@link MirrorCommandBuilder}
|
||||||
|
* @throws CommandNotSupportedException if the command is not supported
|
||||||
|
* by the implementation of the repository service provider.
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
public MirrorCommandBuilder getMirrorCommand() {
|
||||||
|
LOG.debug("create mirror command for repository {}", repository);
|
||||||
|
return new MirrorCommandBuilder(provider.getMirrorCommand(), repository);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the command is supported by the repository service.
|
* Returns true if the command is supported by the repository service.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
public class SimpleUsernamePasswordCredential implements UsernamePasswordCredential {
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
private final char[] password;
|
||||||
|
|
||||||
|
public SimpleUsernamePasswordCredential(String username, char[] password) {
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String username() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] password() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
public interface UsernamePasswordCredential extends Credential {
|
||||||
|
|
||||||
|
String username();
|
||||||
|
|
||||||
|
char[] password();
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import sonia.scm.repository.api.MirrorCommandResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public interface MirrorCommand {
|
||||||
|
|
||||||
|
MirrorCommandResult mirror(MirrorCommandRequest mirrorCommandRequest);
|
||||||
|
|
||||||
|
MirrorCommandResult update(MirrorCommandRequest mirrorCommandRequest);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import sonia.scm.repository.api.Credential;
|
||||||
|
import sonia.scm.repository.api.MirrorFilter;
|
||||||
|
import sonia.scm.security.PublicKey;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Collections.unmodifiableCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public final class MirrorCommandRequest {
|
||||||
|
|
||||||
|
private String sourceUrl;
|
||||||
|
private Collection<Credential> credentials = emptyList();
|
||||||
|
private List<PublicKey> publicKeys = emptyList();
|
||||||
|
private MirrorFilter filter = new MirrorFilter() {};
|
||||||
|
|
||||||
|
public String getSourceUrl() {
|
||||||
|
return sourceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourceUrl(String sourceUrl) {
|
||||||
|
this.sourceUrl = sourceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Credential> getCredentials() {
|
||||||
|
return unmodifiableCollection(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Credential> Optional<T> getCredential(Class<T> credentialClass) {
|
||||||
|
return getCredentials()
|
||||||
|
.stream()
|
||||||
|
.filter(credentialClass::isInstance)
|
||||||
|
.map(credentialClass::cast)
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCredentials(Collection<Credential> credentials) {
|
||||||
|
this.credentials = credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MirrorFilter getFilter() {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilter(MirrorFilter filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return StringUtils.isNotBlank(sourceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublicKeys(List<PublicKey> publicKeys) {
|
||||||
|
this.publicKeys = publicKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PublicKey> getPublicKeys() {
|
||||||
|
return Collections.unmodifiableList(publicKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -298,4 +298,11 @@ public abstract class RepositoryServiceProvider implements Closeable
|
|||||||
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
||||||
throw new CommandNotSupportedException(Command.FULL_HEALTH_CHECK);
|
throw new CommandNotSupportedException(Command.FULL_HEALTH_CHECK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
public MirrorCommand getMirrorCommand() {
|
||||||
|
throw new CommandNotSupportedException(Command.MIRROR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
|
|
||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts and decrypts string values.
|
* Encrypts and decrypts string values.
|
||||||
@@ -41,7 +44,23 @@ public interface CipherHandler
|
|||||||
*
|
*
|
||||||
* @return decrypted value
|
* @return decrypted value
|
||||||
*/
|
*/
|
||||||
public String decode(String value);
|
String decode(String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the given value. If not implemented explicitly, this creates a string
|
||||||
|
* from the byte array, decodes this with {@link #decode(String)}, and interprets
|
||||||
|
* this string as base 64 encoded byte array.
|
||||||
|
* <p>
|
||||||
|
* if {@link #encode(byte[])} is overridden by an implementation, this has to be
|
||||||
|
* implemented accordingly.
|
||||||
|
*
|
||||||
|
* @param value encrypted value
|
||||||
|
*
|
||||||
|
* @return decrypted value
|
||||||
|
*/
|
||||||
|
default byte[] decode(byte[] value) {
|
||||||
|
return Base64.getDecoder().decode(decode(new String(value, UTF_8)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts the given value.
|
* Encrypts the given value.
|
||||||
@@ -50,5 +69,21 @@ public interface CipherHandler
|
|||||||
*
|
*
|
||||||
* @return encrypted value
|
* @return encrypted value
|
||||||
*/
|
*/
|
||||||
public String encode(String value);
|
String encode(String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the given value. If not implemented explicitly, this encoded the given
|
||||||
|
* byte array as a base 64 string, encodes this string with {@link #encode(String)},
|
||||||
|
* and returns the bytes of this resulting string.
|
||||||
|
* <p>
|
||||||
|
* if {@link #decode(byte[])} is overridden by an implementation, this has to be
|
||||||
|
* implemented accordingly.
|
||||||
|
*
|
||||||
|
* @param value plain byte array to encrypt.
|
||||||
|
*
|
||||||
|
* @return encrypted value
|
||||||
|
*/
|
||||||
|
default byte[] encode(byte[] value) {
|
||||||
|
return encode(Base64.getEncoder().encodeToString(value)).getBytes(UTF_8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
|
|
||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import sonia.scm.SCMContext;
|
import sonia.scm.SCMContext;
|
||||||
import sonia.scm.util.ServiceUtil;
|
import sonia.scm.util.ServiceUtil;
|
||||||
|
|
||||||
@@ -34,52 +32,31 @@ import sonia.scm.util.ServiceUtil;
|
|||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
* @since 1.7
|
* @since 1.7
|
||||||
*/
|
*/
|
||||||
public final class CipherUtil
|
public final class CipherUtil {
|
||||||
{
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static volatile CipherUtil instance;
|
private static volatile CipherUtil instance;
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
private CipherHandler cipherHandler;
|
||||||
|
private KeyGenerator keyGenerator;
|
||||||
|
|
||||||
/**
|
private CipherUtil() {
|
||||||
* Constructs ...
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private CipherUtil()
|
|
||||||
{
|
|
||||||
keyGenerator = ServiceUtil.getService(KeyGenerator.class);
|
keyGenerator = ServiceUtil.getService(KeyGenerator.class);
|
||||||
|
|
||||||
if (keyGenerator == null)
|
if (keyGenerator == null) {
|
||||||
{
|
|
||||||
keyGenerator = new UUIDKeyGenerator();
|
keyGenerator = new UUIDKeyGenerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
cipherHandler = ServiceUtil.getService(CipherHandler.class);
|
cipherHandler = ServiceUtil.getService(CipherHandler.class);
|
||||||
|
|
||||||
if (cipherHandler == null)
|
if (cipherHandler == null) {
|
||||||
{
|
cipherHandler = new DefaultCipherHandler(SCMContext.getContext(), keyGenerator);
|
||||||
cipherHandler = new DefaultCipherHandler(SCMContext.getContext(),
|
|
||||||
keyGenerator);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
public static CipherUtil getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
/**
|
synchronized (CipherUtil.class) {
|
||||||
* Method description
|
if (instance == null) {
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static CipherUtil getInstance()
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
synchronized (CipherUtil.class)
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new CipherUtil();
|
instance = new CipherUtil();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,63 +65,29 @@ public final class CipherUtil
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
public String decode(String value) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public String decode(String value)
|
|
||||||
{
|
|
||||||
return cipherHandler.decode(value);
|
return cipherHandler.decode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public byte[] decode(byte[] value) {
|
||||||
* Method description
|
return cipherHandler.decode(value);
|
||||||
*
|
}
|
||||||
*
|
|
||||||
* @param value
|
public String encode(String value) {
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public String encode(String value)
|
|
||||||
{
|
|
||||||
return cipherHandler.encode(value);
|
return cipherHandler.encode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
public byte[] encode(byte[] value) {
|
||||||
|
return cipherHandler.encode(value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public CipherHandler getCipherHandler()
|
public CipherHandler getCipherHandler()
|
||||||
{
|
{
|
||||||
return cipherHandler;
|
return cipherHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public KeyGenerator getKeyGenerator()
|
public KeyGenerator getKeyGenerator()
|
||||||
{
|
{
|
||||||
return keyGenerator;
|
return keyGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private CipherHandler cipherHandler;
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private KeyGenerator keyGenerator;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import sonia.scm.repository.Person;
|
|||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -45,6 +46,16 @@ public interface PublicKey {
|
|||||||
*/
|
*/
|
||||||
String getId();
|
String getId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns ids from gpg sub keys.
|
||||||
|
*
|
||||||
|
* @return sub key ids
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
default Set<String> getSubkeys() {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the username of the owner or an empty optional.
|
* Returns the username of the owner or an empty optional.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key parser.
|
||||||
|
*
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
public interface PublicKeyParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the given public key.
|
||||||
|
* @param raw raw representation of public key
|
||||||
|
* @return parsed public key
|
||||||
|
*/
|
||||||
|
PublicKey parse(String raw);
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@
|
|||||||
|
|
||||||
package sonia.scm.store;
|
package sonia.scm.store;
|
||||||
|
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for {@link ConfigurationStore}.
|
* Base class for {@link ConfigurationStore}.
|
||||||
*
|
*
|
||||||
@@ -38,9 +40,9 @@ public abstract class AbstractStore<T> implements ConfigurationStore<T> {
|
|||||||
* stored object
|
* stored object
|
||||||
*/
|
*/
|
||||||
protected T storeObject;
|
protected T storeObject;
|
||||||
private final boolean readOnly;
|
private final BooleanSupplier readOnly;
|
||||||
|
|
||||||
protected AbstractStore(boolean readOnly) {
|
protected AbstractStore(BooleanSupplier readOnly) {
|
||||||
this.readOnly = readOnly;
|
this.readOnly = readOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ public abstract class AbstractStore<T> implements ConfigurationStore<T> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void set(T object) {
|
public void set(T object) {
|
||||||
if (readOnly) {
|
if (readOnly.getAsBoolean()) {
|
||||||
throw new StoreReadOnlyException(object);
|
throw new StoreReadOnlyException(object);
|
||||||
}
|
}
|
||||||
writeObject(object);
|
writeObject(object);
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.xml;
|
||||||
|
|
||||||
|
import sonia.scm.security.CipherUtil;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.19.0
|
||||||
|
*/
|
||||||
|
public class XmlCipherByteArrayAdapter extends XmlAdapter<byte[], byte[]> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] marshal(byte[] v) throws Exception {
|
||||||
|
return CipherUtil.getInstance().encode(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] unmarshal(byte[] v) throws Exception {
|
||||||
|
return CipherUtil.getInstance().decode(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.collect;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class EvictingQueueTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnFalseIfMaxSizeIsZero() {
|
||||||
|
Queue<String> queue = EvictingQueue.create(0);
|
||||||
|
assertThat(queue.add("a")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEvictFirstAddedEntry() {
|
||||||
|
Queue<String> queue = EvictingQueue.create(2);
|
||||||
|
assertThat(queue.add("a")).isTrue();
|
||||||
|
assertThat(queue.add("b")).isTrue();
|
||||||
|
assertThat(queue.add("c")).isTrue();
|
||||||
|
assertThat(queue).containsOnly("b", "c");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateWithDefaultSize() {
|
||||||
|
EvictingQueue<String> queue = new EvictingQueue<>();
|
||||||
|
assertThat(queue.maxSize).isEqualTo(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailWithNegativeSize() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> EvictingQueue.create(-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,10 @@ package sonia.scm.repository;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
class DefaultRepositoryExportingCheckTest {
|
class DefaultRepositoryExportingCheckTest {
|
||||||
|
|
||||||
@@ -62,4 +65,32 @@ class DefaultRepositoryExportingCheckTest {
|
|||||||
boolean readOnly = check.isExporting(EXPORTING_REPOSITORY);
|
boolean readOnly = check.isExporting(EXPORTING_REPOSITORY);
|
||||||
assertThat(readOnly).isFalse();
|
assertThat(readOnly).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExportingException() {
|
||||||
|
RepositoryExportingCheck check = new TestingRepositoryExportingCheck();
|
||||||
|
|
||||||
|
assertThrows(RepositoryExportingException.class, () -> check.check(EXPORTING_REPOSITORY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExportingExceptionWithId() {
|
||||||
|
RepositoryExportingCheck check = new TestingRepositoryExportingCheck();
|
||||||
|
|
||||||
|
assertThrows(RepositoryExportingException.class, () -> check.check("exporting_hog"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestingRepositoryExportingCheck implements RepositoryExportingCheck {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isExporting(String repositoryId) {
|
||||||
|
return "exporting_hog".equals(repositoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T withExportingLock(Repository repository, Supplier<T> callback) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ReadOnlyCheckTest {
|
||||||
|
|
||||||
|
private final ReadOnlyCheck check = new TestingReadOnlyCheck();
|
||||||
|
|
||||||
|
private final Repository repository = new Repository("42", "git", "hitchhiker", "hog");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToMethodWithRepositoryId() {
|
||||||
|
assertThat(check.isReadOnly(repository)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowReadOnlyException() {
|
||||||
|
assertThrows(ReadOnlyException.class, () -> check.check(repository));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowReadOnlyExceptionForId() {
|
||||||
|
assertThrows(ReadOnlyException.class, () -> check.check("42"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotThrowException() {
|
||||||
|
assertDoesNotThrow(() -> check.check("21"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToNormalCheck() {
|
||||||
|
assertThat(check.isReadOnly("any", "42")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestingReadOnlyCheck implements ReadOnlyCheck {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReason() {
|
||||||
|
return "Testing";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly(String repositoryId) {
|
||||||
|
return repositoryId.equals("42");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class RepositoryArchivedCheckTest {
|
||||||
|
|
||||||
|
private final RepositoryArchivedCheck check = new TestingRepositoryArchivedCheck();
|
||||||
|
|
||||||
|
private final Repository repository = new Repository("42", "git", "hitchhiker", "hog");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowForRepositoryMarkedAsArchived() {
|
||||||
|
assertThrows(RepositoryArchivedException.class, () -> check.check(repository));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowForRepositoryMarkedAsArchivedWithId() {
|
||||||
|
assertThrows(RepositoryArchivedException.class, () -> check.check("42"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowForArchivedRepository() {
|
||||||
|
Repository repository = new Repository("21", "hg", "hitchhiker", "puzzle42");
|
||||||
|
repository.setArchived(true);
|
||||||
|
assertThrows(RepositoryArchivedException.class, () -> check.check(repository));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestingRepositoryArchivedCheck implements RepositoryArchivedCheck {
|
||||||
|
@Override
|
||||||
|
public boolean isArchived(String repositoryId) {
|
||||||
|
return "42".equals(repositoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import org.junit.jupiter.api.BeforeAll;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||||
import org.junit.jupiter.api.extension.InvocationInterceptor;
|
import org.junit.jupiter.api.extension.InvocationInterceptor;
|
||||||
@@ -40,6 +41,7 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
@@ -117,12 +119,14 @@ class RepositoryPermissionGuardTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void mockArchivedRepository() {
|
void mockArchivedRepository() {
|
||||||
|
RepositoryPermissionGuard.setReadOnlyChecks(Collections.singleton(new EventDrivenRepositoryArchiveCheck()));
|
||||||
EventDrivenRepositoryArchiveCheck.setAsArchived("1");
|
EventDrivenRepositoryArchiveCheck.setAsArchived("1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void removeArchiveFlag() {
|
void removeArchiveFlag() {
|
||||||
EventDrivenRepositoryArchiveCheck.removeFromArchived("1");
|
EventDrivenRepositoryArchiveCheck.removeFromArchived("1");
|
||||||
|
RepositoryPermissionGuard.setReadOnlyChecks(Collections.emptySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -174,12 +178,14 @@ class RepositoryPermissionGuardTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WrapInExportCheck implements InvocationInterceptor {
|
private static class WrapInExportCheck implements InvocationInterceptor, AfterEachCallback {
|
||||||
|
|
||||||
public void interceptTestMethod(Invocation<Void> invocation,
|
public void interceptTestMethod(Invocation<Void> invocation,
|
||||||
ReflectiveInvocationContext<Method> invocationContext,
|
ReflectiveInvocationContext<Method> invocationContext,
|
||||||
ExtensionContext extensionContext) {
|
ExtensionContext extensionContext) {
|
||||||
new DefaultRepositoryExportingCheck().withExportingLock(new Repository("1", "git", "space", "X"), () -> {
|
DefaultRepositoryExportingCheck check = new DefaultRepositoryExportingCheck();
|
||||||
|
RepositoryPermissionGuard.setReadOnlyChecks(Collections.singleton(check));
|
||||||
|
check.withExportingLock(new Repository("1", "git", "space", "X"), () -> {
|
||||||
try {
|
try {
|
||||||
invocation.proceed();
|
invocation.proceed();
|
||||||
return null;
|
return null;
|
||||||
@@ -188,5 +194,10 @@ class RepositoryPermissionGuardTest {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterEach(ExtensionContext context) {
|
||||||
|
RepositoryPermissionGuard.setReadOnlyChecks(Collections.emptySet());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class exists to bypass the visibility of the {@link RepositoryReadOnlyChecker} methods.
|
||||||
|
* This is necessary to use those methods from test which are located in other packages.
|
||||||
|
*/
|
||||||
|
public class RepositoryReadOnlyCheckerHack {
|
||||||
|
|
||||||
|
private RepositoryReadOnlyCheckerHack() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bypass the visibility of the {@link RepositoryReadOnlyChecker#setReadOnlyChecks(Collection)} method.
|
||||||
|
*
|
||||||
|
* @param readOnlyChecks checks to register
|
||||||
|
*/
|
||||||
|
public static void setReadOnlyChecks(Collection<ReadOnlyCheck> readOnlyChecks) {
|
||||||
|
RepositoryReadOnlyChecker.setReadOnlyChecks(readOnlyChecks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,15 +24,21 @@
|
|||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
class RepositoryReadOnlyCheckerTest {
|
class RepositoryReadOnlyCheckerTest {
|
||||||
|
|
||||||
private final Repository repository = new Repository("1", "git","hitchhiker", "HeartOfGold");
|
private final Repository repository = new Repository("1", "git", "hitchhiker", "HeartOfGold");
|
||||||
|
|
||||||
private boolean archived = false;
|
private boolean archived = false;
|
||||||
private boolean exporting = false;
|
private boolean exporting = false;
|
||||||
@@ -50,30 +56,126 @@ class RepositoryReadOnlyCheckerTest {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final RepositoryReadOnlyChecker checker = new RepositoryReadOnlyChecker(archivedCheck, exportingCheck);
|
@BeforeEach
|
||||||
|
void resetStates() {
|
||||||
@Test
|
archived = false;
|
||||||
void shouldReturnFalseIfAllChecksFalse() {
|
exporting = false;
|
||||||
boolean readOnly = checker.isReadOnly(repository);
|
|
||||||
|
|
||||||
assertThat(readOnly).isFalse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@AfterEach
|
||||||
void shouldReturnTrueIfArchivedIsTrue() {
|
void unregisterStaticChecks() {
|
||||||
archived = true;
|
RepositoryReadOnlyChecker.setReadOnlyChecks(Collections.emptySet());
|
||||||
|
|
||||||
boolean readOnly = checker.isReadOnly(repository);
|
|
||||||
|
|
||||||
assertThat(readOnly).isTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
void shouldReturnTrueIfExportingIsTrue() {
|
class LegacyChecks {
|
||||||
exporting = true;
|
|
||||||
|
|
||||||
boolean readOnly = checker.isReadOnly(repository);
|
private final RepositoryReadOnlyChecker checker = new RepositoryReadOnlyChecker(archivedCheck, exportingCheck);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnFalseIfAllChecksFalse() {
|
||||||
|
boolean readOnly = checker.isReadOnly(repository);
|
||||||
|
|
||||||
|
assertThat(readOnly).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTrueIfArchivedIsTrue() {
|
||||||
|
archived = true;
|
||||||
|
|
||||||
|
boolean readOnly = checker.isReadOnly(repository);
|
||||||
|
|
||||||
|
assertThat(readOnly).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTrueIfExportingIsTrue() {
|
||||||
|
exporting = true;
|
||||||
|
|
||||||
|
boolean readOnly = checker.isReadOnly(repository);
|
||||||
|
|
||||||
|
assertThat(readOnly).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleLegacyAndStatic() {
|
||||||
|
assertThat(checker.isReadOnly(repository)).isFalse();
|
||||||
|
|
||||||
|
RepositoryReadOnlyChecker.setReadOnlyChecks(Collections.singleton(new SampleReadOnlyCheck(true)));
|
||||||
|
assertThat(checker.isReadOnly(repository)).isTrue();
|
||||||
|
|
||||||
|
RepositoryReadOnlyChecker.setReadOnlyChecks(Collections.emptySet());
|
||||||
|
assertThat(checker.isReadOnly(repository)).isFalse();
|
||||||
|
|
||||||
|
exporting = true;
|
||||||
|
assertThat(checker.isReadOnly(repository)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
assertThat(readOnly).isTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class StaticChecks {
|
||||||
|
|
||||||
|
private final RepositoryReadOnlyChecker checker = new RepositoryReadOnlyChecker();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void registerStaticChecks() {
|
||||||
|
RepositoryReadOnlyChecker.setReadOnlyChecks(Arrays.asList(
|
||||||
|
archivedCheck, exportingCheck
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnFalseIfAllChecksFalse() {
|
||||||
|
boolean readOnly = checker.isReadOnly(repository);
|
||||||
|
|
||||||
|
assertThat(readOnly).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTrueIfArchivedIsTrue() {
|
||||||
|
archived = true;
|
||||||
|
|
||||||
|
boolean readOnly = checker.isReadOnly(repository);
|
||||||
|
|
||||||
|
assertThat(readOnly).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTrueIfExportingIsTrue() {
|
||||||
|
exporting = true;
|
||||||
|
|
||||||
|
boolean readOnly = checker.isReadOnly(repository);
|
||||||
|
|
||||||
|
assertThat(readOnly).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowReadOnlyException() {
|
||||||
|
exporting = true;
|
||||||
|
|
||||||
|
assertThrows(ReadOnlyException.class, () -> RepositoryReadOnlyChecker.checkReadOnly(repository));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SampleReadOnlyCheck implements ReadOnlyCheck {
|
||||||
|
|
||||||
|
private final boolean readOnly;
|
||||||
|
|
||||||
|
public SampleReadOnlyCheck(boolean readOnly) {
|
||||||
|
this.readOnly = readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReason() {
|
||||||
|
return "testing purposes";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly(String repositoryId) {
|
||||||
|
return readOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import org.apache.shiro.subject.Subject;
|
|||||||
import org.apache.shiro.util.ThreadContext;
|
import org.apache.shiro.util.ThreadContext;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
@@ -35,9 +36,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import sonia.scm.SCMContext;
|
import sonia.scm.SCMContext;
|
||||||
import sonia.scm.config.ScmConfiguration;
|
import sonia.scm.config.ScmConfiguration;
|
||||||
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||||
|
import sonia.scm.repository.EventDrivenRepositoryArchiveCheck;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryArchivedException;
|
import sonia.scm.repository.RepositoryArchivedException;
|
||||||
import sonia.scm.repository.RepositoryExportingException;
|
import sonia.scm.repository.RepositoryExportingException;
|
||||||
|
import sonia.scm.repository.RepositoryReadOnlyCheckerHack;
|
||||||
import sonia.scm.repository.spi.HttpScmProtocol;
|
import sonia.scm.repository.spi.HttpScmProtocol;
|
||||||
import sonia.scm.repository.spi.RepositoryServiceProvider;
|
import sonia.scm.repository.spi.RepositoryServiceProvider;
|
||||||
import sonia.scm.user.EMail;
|
import sonia.scm.user.EMail;
|
||||||
@@ -59,7 +62,7 @@ import static org.mockito.Mockito.when;
|
|||||||
class RepositoryServiceTest {
|
class RepositoryServiceTest {
|
||||||
|
|
||||||
private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class);
|
private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class);
|
||||||
private final Repository repository = new Repository("", "git", "space", "repo");
|
private final Repository repository = new Repository("42", "git", "space", "repo");
|
||||||
|
|
||||||
private final EMail eMail = new EMail(new ScmConfiguration());
|
private final EMail eMail = new EMail(new ScmConfiguration());
|
||||||
|
|
||||||
@@ -112,34 +115,54 @@ class RepositoryServiceTest {
|
|||||||
assertThrows(IllegalArgumentException.class, () -> repositoryService.getProtocol(UnknownScmProtocol.class));
|
assertThrows(IllegalArgumentException.class, () -> repositoryService.getProtocol(UnknownScmProtocol.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
void shouldFailForArchivedRepository() {
|
class RepositoryReadOnlyCheckerTests {
|
||||||
repository.setArchived(true);
|
|
||||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
|
||||||
|
|
||||||
assertThrows(RepositoryArchivedException.class, repositoryService::getModifyCommand);
|
@AfterEach
|
||||||
assertThrows(RepositoryArchivedException.class, repositoryService::getBranchCommand);
|
void clearChecks() {
|
||||||
assertThrows(RepositoryArchivedException.class, repositoryService::getPullCommand);
|
RepositoryReadOnlyCheckerHack.setReadOnlyChecks(Collections.emptySet());
|
||||||
assertThrows(RepositoryArchivedException.class, repositoryService::getTagCommand);
|
repository.setArchived(false);
|
||||||
assertThrows(RepositoryArchivedException.class, repositoryService::getMergeCommand);
|
}
|
||||||
assertThrows(RepositoryArchivedException.class, repositoryService::getModifyCommand);
|
|
||||||
}
|
@Test
|
||||||
|
void shouldFailForExportingRepository() {
|
||||||
|
DefaultRepositoryExportingCheck check = new DefaultRepositoryExportingCheck();
|
||||||
|
|
||||||
|
RepositoryReadOnlyCheckerHack.setReadOnlyChecks(Collections.singleton(check));
|
||||||
|
|
||||||
|
check.withExportingLock(repository, () -> {
|
||||||
|
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
||||||
|
|
||||||
|
assertThrows(RepositoryExportingException.class, repositoryService::getModifyCommand);
|
||||||
|
assertThrows(RepositoryExportingException.class, repositoryService::getBranchCommand);
|
||||||
|
assertThrows(RepositoryExportingException.class, repositoryService::getPullCommand);
|
||||||
|
assertThrows(RepositoryExportingException.class, repositoryService::getTagCommand);
|
||||||
|
assertThrows(RepositoryExportingException.class, repositoryService::getMergeCommand);
|
||||||
|
assertThrows(RepositoryExportingException.class, repositoryService::getModifyCommand);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailForArchivedRepository() {
|
||||||
|
EventDrivenRepositoryArchiveCheck check = new EventDrivenRepositoryArchiveCheck();
|
||||||
|
RepositoryReadOnlyCheckerHack.setReadOnlyChecks(Collections.singleton(check));
|
||||||
|
|
||||||
|
repository.setArchived(true);
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFailForExportingRepository() {
|
|
||||||
new DefaultRepositoryExportingCheck().withExportingLock(repository, () -> {
|
|
||||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null);
|
||||||
|
|
||||||
assertThrows(RepositoryExportingException.class, repositoryService::getModifyCommand);
|
assertThrows(RepositoryArchivedException.class, repositoryService::getModifyCommand);
|
||||||
assertThrows(RepositoryExportingException.class, repositoryService::getBranchCommand);
|
assertThrows(RepositoryArchivedException.class, repositoryService::getBranchCommand);
|
||||||
assertThrows(RepositoryExportingException.class, repositoryService::getPullCommand);
|
assertThrows(RepositoryArchivedException.class, repositoryService::getPullCommand);
|
||||||
assertThrows(RepositoryExportingException.class, repositoryService::getTagCommand);
|
assertThrows(RepositoryArchivedException.class, repositoryService::getTagCommand);
|
||||||
assertThrows(RepositoryExportingException.class, repositoryService::getMergeCommand);
|
assertThrows(RepositoryArchivedException.class, repositoryService::getMergeCommand);
|
||||||
assertThrows(RepositoryExportingException.class, repositoryService::getModifyCommand);
|
assertThrows(RepositoryArchivedException.class, repositoryService::getModifyCommand);
|
||||||
return null;
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static class DummyHttpProtocol extends HttpScmProtocol {
|
private static class DummyHttpProtocol extends HttpScmProtocol {
|
||||||
|
|
||||||
private final boolean anonymousEnabled;
|
private final boolean anonymousEnabled;
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class CipherHandlerTest {
|
||||||
|
|
||||||
|
public static final String SECRET_TEXT = "secret text";
|
||||||
|
public static final String SECRET_TEXT_AS_BASE64 = "c2VjcmV0IHRleHQ=";
|
||||||
|
public static final String ENCRYPTED_TEXT = "unreadable bytes";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateToStringEncryptionForBytes() {
|
||||||
|
CipherHandler cipherHandler = new CipherHandler() {
|
||||||
|
@Override
|
||||||
|
public String decode(String value) {
|
||||||
|
if (value.equals(ENCRYPTED_TEXT)) {
|
||||||
|
return SECRET_TEXT_AS_BASE64;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("unexpected data: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encode(String value) {
|
||||||
|
if (value.equals(SECRET_TEXT_AS_BASE64)) {
|
||||||
|
return ENCRYPTED_TEXT;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("unexpected data: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
byte[] encodedBytes = cipherHandler.encode(SECRET_TEXT.getBytes(UTF_8));
|
||||||
|
|
||||||
|
byte[] originalBytes = cipherHandler.decode(encodedBytes);
|
||||||
|
|
||||||
|
assertThat(originalBytes).isEqualTo(SECRET_TEXT.getBytes(UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JAXB implementation of {@link ConfigurationStore}.
|
* JAXB implementation of {@link ConfigurationStore}.
|
||||||
@@ -46,7 +47,7 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
|
|||||||
private final Class<T> type;
|
private final Class<T> type;
|
||||||
private final File configFile;
|
private final File configFile;
|
||||||
|
|
||||||
public JAXBConfigurationStore(TypedStoreContext<T> context, Class<T> type, File configFile, boolean readOnly) {
|
public JAXBConfigurationStore(TypedStoreContext<T> context, Class<T> type, File configFile, BooleanSupplier readOnly) {
|
||||||
super(readOnly);
|
super(readOnly);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme
|
|||||||
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION),
|
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION),
|
||||||
storeParameters.getType(),
|
storeParameters.getType(),
|
||||||
storeParameters.getRepositoryId()),
|
storeParameters.getRepositoryId()),
|
||||||
mustBeReadOnly(storeParameters)
|
() -> mustBeReadOnly(storeParameters)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@SuppressWarnings("unstableApiUsage")
|
||||||
class TypedStoreContextTest {
|
class TypedStoreContextTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMarshallAndUnmarshall(@TempDir Path tempDir) {
|
void shouldMarshallAndUnmarshall(@TempDir Path tempDir) {
|
||||||
TypedStoreContext<Sample> context = context(Sample.class);
|
TypedStoreContext<Sample> context = context();
|
||||||
|
|
||||||
File file = tempDir.resolve("test.xml").toFile();
|
File file = tempDir.resolve("test.xml").toFile();
|
||||||
context.marshal(new Sample("awesome"), file);
|
context.marshal(new Sample("awesome"), file);
|
||||||
@@ -62,7 +63,7 @@ class TypedStoreContextTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldWorkWithMarshallerAndUnmarshaller(@TempDir Path tempDir) {
|
void shouldWorkWithMarshallerAndUnmarshaller(@TempDir Path tempDir) {
|
||||||
TypedStoreContext<Sample> context = context(Sample.class);
|
TypedStoreContext<Sample> context = context();
|
||||||
|
|
||||||
File file = tempDir.resolve("test.xml").toFile();
|
File file = tempDir.resolve("test.xml").toFile();
|
||||||
|
|
||||||
@@ -115,10 +116,12 @@ class TypedStoreContextTest {
|
|||||||
assertThat(sample.value).isEqualTo("awesome!!");
|
assertThat(sample.value).isEqualTo("awesome!!");
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> TypedStoreContext<T> context(Class<T> type) {
|
@SuppressWarnings("unchecked")
|
||||||
return TypedStoreContext.of(params(type));
|
private <T> TypedStoreContext<T> context() {
|
||||||
|
return TypedStoreContext.of(params((Class<T>) Sample.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private <T> TypedStoreParameters<T> params(Class<T> type) {
|
private <T> TypedStoreParameters<T> params(Class<T> type) {
|
||||||
TypedStoreParameters<T> params = mock(TypedStoreParameters.class);
|
TypedStoreParameters<T> params = mock(TypedStoreParameters.class);
|
||||||
when(params.getType()).thenReturn(type);
|
when(params.getType()).thenReturn(type);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dependencies {
|
|||||||
implementation "sonia.jgit:org.eclipse.jgit.gpg.bc:${jgitVersion}"
|
implementation "sonia.jgit:org.eclipse.jgit.gpg.bc:${jgitVersion}"
|
||||||
implementation libraries.commonsCompress
|
implementation libraries.commonsCompress
|
||||||
|
|
||||||
|
testImplementation "sonia.jgit:org.eclipse.jgit.junit.http:${jgitVersion}"
|
||||||
testImplementation libraries.shiroUnit
|
testImplementation libraries.shiroUnit
|
||||||
testImplementation libraries.awaitility
|
testImplementation libraries.awaitility
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.protocolcommand.git;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import sonia.scm.repository.spi.GitMirrorCommand;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
final class MirrorRefFilter {
|
||||||
|
|
||||||
|
private MirrorRefFilter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Ref> filterMirrors(Map<String, Ref> refs) {
|
||||||
|
return refs.entrySet()
|
||||||
|
.stream()
|
||||||
|
.filter(entry -> !entry.getKey().startsWith(GitMirrorCommand.MIRROR_REF_PREFIX))
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,8 @@ import sonia.scm.protocolcommand.RepositoryContext;
|
|||||||
public class ScmUploadPackFactory implements UploadPackFactory<RepositoryContext> {
|
public class ScmUploadPackFactory implements UploadPackFactory<RepositoryContext> {
|
||||||
@Override
|
@Override
|
||||||
public UploadPack create(RepositoryContext repositoryContext, Repository repository) {
|
public UploadPack create(RepositoryContext repositoryContext, Repository repository) {
|
||||||
return new UploadPack(repository);
|
UploadPack uploadPack = new UploadPack(repository);
|
||||||
|
uploadPack.setRefFilter(MirrorRefFilter::filterMirrors);
|
||||||
|
return uploadPack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.protocolcommand.git;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.transport.UploadPack;
|
||||||
|
import org.eclipse.jgit.transport.resolver.UploadPackFactory;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
public class ScmUploadPackFactoryForHttpServletRequest implements UploadPackFactory<HttpServletRequest> {
|
||||||
|
@Override
|
||||||
|
public UploadPack create(HttpServletRequest repositoryContext, Repository repository) {
|
||||||
|
UploadPack uploadPack = new UploadPack(repository);
|
||||||
|
uploadPack.setRefFilter(MirrorRefFilter::filterMirrors);
|
||||||
|
return uploadPack;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import sonia.scm.security.GPG;
|
||||||
|
import sonia.scm.security.PublicKey;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
class GPGSignatureResolver {
|
||||||
|
|
||||||
|
private final GPG gpg;
|
||||||
|
private final Map<String, PublicKey> additionalPublicKeys;
|
||||||
|
|
||||||
|
GPGSignatureResolver(GPG gpg, Iterable<PublicKey> additionalPublicKeys) {
|
||||||
|
this.gpg = gpg;
|
||||||
|
this.additionalPublicKeys = createKeyMap(additionalPublicKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, PublicKey> createKeyMap(Iterable<PublicKey> additionalPublicKeys) {
|
||||||
|
ImmutableMap.Builder<String, PublicKey> builder = ImmutableMap.builder();
|
||||||
|
for (PublicKey key : additionalPublicKeys) {
|
||||||
|
appendKey(builder, key);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendKey(ImmutableMap.Builder<String, PublicKey> builder, PublicKey key) {
|
||||||
|
builder.put(key.getId(), key);
|
||||||
|
for (String subkey : key.getSubkeys()) {
|
||||||
|
builder.put(subkey, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String findPublicKeyId(byte[] signature) {
|
||||||
|
return gpg.findPublicKeyId(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<PublicKey> findPublicKey(String id) {
|
||||||
|
PublicKey publicKey = additionalPublicKeys.get(id);
|
||||||
|
if (publicKey != null) {
|
||||||
|
return Optional.of(publicKey);
|
||||||
|
}
|
||||||
|
return gpg.findPublicKey(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,6 @@ import org.eclipse.jgit.revwalk.RevCommit;
|
|||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||||
import org.eclipse.jgit.util.RawParseUtils;
|
import org.eclipse.jgit.util.RawParseUtils;
|
||||||
import sonia.scm.security.GPG;
|
|
||||||
import sonia.scm.security.PublicKey;
|
import sonia.scm.security.PublicKey;
|
||||||
import sonia.scm.util.Util;
|
import sonia.scm.util.Util;
|
||||||
|
|
||||||
@@ -56,11 +55,11 @@ import java.util.Optional;
|
|||||||
*/
|
*/
|
||||||
public class GitChangesetConverter implements Closeable {
|
public class GitChangesetConverter implements Closeable {
|
||||||
|
|
||||||
private final GPG gpg;
|
private final GPGSignatureResolver gpg;
|
||||||
private final Multimap<ObjectId, String> tags;
|
private final Multimap<ObjectId, String> tags;
|
||||||
private final TreeWalk treeWalk;
|
private final TreeWalk treeWalk;
|
||||||
|
|
||||||
public GitChangesetConverter(GPG gpg, org.eclipse.jgit.lib.Repository repository, RevWalk revWalk) {
|
GitChangesetConverter(GPGSignatureResolver gpg, org.eclipse.jgit.lib.Repository repository, RevWalk revWalk) {
|
||||||
this.gpg = gpg;
|
this.gpg = gpg;
|
||||||
this.tags = GitUtil.createTagMap(repository, revWalk);
|
this.tags = GitUtil.createTagMap(repository, revWalk);
|
||||||
this.treeWalk = new TreeWalk(repository);
|
this.treeWalk = new TreeWalk(repository);
|
||||||
|
|||||||
@@ -27,8 +27,13 @@ package sonia.scm.repository;
|
|||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
import sonia.scm.security.GPG;
|
import sonia.scm.security.GPG;
|
||||||
|
import sonia.scm.security.PublicKey;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class GitChangesetConverterFactory {
|
public class GitChangesetConverterFactory {
|
||||||
|
|
||||||
@@ -40,11 +45,52 @@ public class GitChangesetConverterFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public GitChangesetConverter create(Repository repository) {
|
public GitChangesetConverter create(Repository repository) {
|
||||||
return new GitChangesetConverter(gpg, repository, new RevWalk(repository));
|
return builder(repository).create();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GitChangesetConverter create(Repository repository, RevWalk revWalk) {
|
public GitChangesetConverter create(Repository repository, RevWalk revWalk) {
|
||||||
return new GitChangesetConverter(gpg, repository, revWalk);
|
return builder(repository).withRevWalk(revWalk).create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder builder(Repository repository) {
|
||||||
|
return new Builder(gpg, repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private final GPG gpg;
|
||||||
|
private final Repository repository;
|
||||||
|
private RevWalk revWalk;
|
||||||
|
private final List<PublicKey> additionalPublicKeys = new ArrayList<>();
|
||||||
|
|
||||||
|
private Builder(GPG gpg, Repository repository) {
|
||||||
|
this.gpg = gpg;
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withRevWalk(RevWalk revWalk) {
|
||||||
|
this.revWalk = revWalk;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAdditionalPublicKeys(PublicKey... publicKeys) {
|
||||||
|
additionalPublicKeys.addAll(Arrays.asList(publicKeys));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAdditionalPublicKeys(Collection<PublicKey> publicKeys) {
|
||||||
|
additionalPublicKeys.addAll(publicKeys);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GitChangesetConverter create() {
|
||||||
|
return new GitChangesetConverter(
|
||||||
|
new GPGSignatureResolver(gpg, additionalPublicKeys),
|
||||||
|
repository,
|
||||||
|
revWalk != null ? revWalk : new RevWalk(repository)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.transport.HttpTransport;
|
||||||
|
import sonia.scm.web.ScmHttpConnectionFactory;
|
||||||
|
import sonia.scm.plugin.Extension;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.servlet.ServletContextEvent;
|
||||||
|
import javax.servlet.ServletContextListener;
|
||||||
|
|
||||||
|
@Extension
|
||||||
|
public class GitHttpTransportRegistration implements ServletContextListener {
|
||||||
|
|
||||||
|
private final ScmHttpConnectionFactory connectionFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GitHttpTransportRegistration(ScmHttpConnectionFactory connectionFactory) {
|
||||||
|
this.connectionFactory = connectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void contextInitialized(ServletContextEvent servletContextEvent) {
|
||||||
|
// Override default http connection factory to inject our own ssl context
|
||||||
|
HttpTransport.setConnectionFactory(connectionFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void contextDestroyed(ServletContextEvent servletContextEvent) {
|
||||||
|
// Nothing to destroy
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ import org.eclipse.jgit.revwalk.filter.RevFilter;
|
|||||||
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
||||||
import org.eclipse.jgit.transport.FetchResult;
|
import org.eclipse.jgit.transport.FetchResult;
|
||||||
import org.eclipse.jgit.transport.RefSpec;
|
import org.eclipse.jgit.transport.RefSpec;
|
||||||
|
import org.eclipse.jgit.transport.TagOpt;
|
||||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||||
import org.eclipse.jgit.util.FS;
|
import org.eclipse.jgit.util.FS;
|
||||||
import org.eclipse.jgit.util.LfsFactory;
|
import org.eclipse.jgit.util.LfsFactory;
|
||||||
@@ -97,6 +98,7 @@ public final class GitUtil {
|
|||||||
* the logger for GitUtil
|
* the logger for GitUtil
|
||||||
*/
|
*/
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GitUtil.class);
|
private static final Logger logger = LoggerFactory.getLogger(GitUtil.class);
|
||||||
|
private static final String REF_SPEC = "refs/heads/*:refs/heads/*";
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
//~--- constructors ---------------------------------------------------------
|
||||||
|
|
||||||
@@ -691,4 +693,10 @@ public final class GitUtil {
|
|||||||
private static RefSpec createRefSpec(Repository repository) {
|
private static RefSpec createRefSpec(Repository repository) {
|
||||||
return new RefSpec(String.format(REFSPEC, repository.getId()));
|
return new RefSpec(String.format(REFSPEC, repository.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static FetchCommand createFetchCommandWithBranchAndTagUpdate(Git git) {
|
||||||
|
return git.fetch()
|
||||||
|
.setRefSpecs(new RefSpec(REF_SPEC))
|
||||||
|
.setTagOpt(TagOpt.FETCH_TAGS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,567 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.google.common.base.Stopwatch;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import org.eclipse.jgit.api.FetchCommand;
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.Constants;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.RefUpdate;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.eclipse.jgit.revwalk.RevObject;
|
||||||
|
import org.eclipse.jgit.revwalk.RevTag;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.eclipse.jgit.transport.FetchResult;
|
||||||
|
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||||
|
import org.eclipse.jgit.transport.TrackingRefUpdate;
|
||||||
|
import org.eclipse.jgit.transport.TransportHttp;
|
||||||
|
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.Changeset;
|
||||||
|
import sonia.scm.repository.GitChangesetConverter;
|
||||||
|
import sonia.scm.repository.GitChangesetConverterFactory;
|
||||||
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.repository.api.MirrorCommandResult;
|
||||||
|
import sonia.scm.repository.api.MirrorCommandResult.ResultType;
|
||||||
|
import sonia.scm.repository.api.MirrorFilter;
|
||||||
|
import sonia.scm.repository.api.MirrorFilter.Result;
|
||||||
|
import sonia.scm.repository.api.Pkcs12ClientCertificateCredential;
|
||||||
|
import sonia.scm.repository.api.UsernamePasswordCredential;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static java.util.Collections.unmodifiableMap;
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.FAILED;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.OK;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.REJECTED_UPDATES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the mirror command for git. This implementation makes use of a special
|
||||||
|
* "ref" called <code>mirror</code>. A synchronization works in principal in the following way:
|
||||||
|
* <ol>
|
||||||
|
* <li>The mirror reference is updated. This is done by calling the jgit equivalent of
|
||||||
|
* <pre>git fetch -pf <source url> "refs/heads/*:refs/mirror/heads/*" "refs/tags/*:refs/mirror/tags/*"</pre>
|
||||||
|
* </li>
|
||||||
|
* <li>These updates are then presented to the filter. Here single updates can be rejected.
|
||||||
|
* Such rejected updates have to be reverted in the mirror, too.
|
||||||
|
* </li>
|
||||||
|
* <li>Accepted ref updates are copied to the "normal" refs.</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
public class GitMirrorCommand extends AbstractGitCommand implements MirrorCommand {
|
||||||
|
|
||||||
|
public static final String MIRROR_REF_PREFIX = "refs/mirror/";
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(GitMirrorCommand.class);
|
||||||
|
|
||||||
|
private final PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory;
|
||||||
|
private final MirrorHttpConnectionProvider mirrorHttpConnectionProvider;
|
||||||
|
private final GitChangesetConverterFactory converterFactory;
|
||||||
|
private final GitTagConverter gitTagConverter;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
GitMirrorCommand(GitContext context, PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory, MirrorHttpConnectionProvider mirrorHttpConnectionProvider, GitChangesetConverterFactory converterFactory, GitTagConverter gitTagConverter) {
|
||||||
|
super(context);
|
||||||
|
this.mirrorHttpConnectionProvider = mirrorHttpConnectionProvider;
|
||||||
|
this.postReceiveRepositoryHookEventFactory = postReceiveRepositoryHookEventFactory;
|
||||||
|
this.converterFactory = converterFactory;
|
||||||
|
this.gitTagConverter = gitTagConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MirrorCommandResult mirror(MirrorCommandRequest mirrorCommandRequest) {
|
||||||
|
return update(mirrorCommandRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MirrorCommandResult update(MirrorCommandRequest mirrorCommandRequest) {
|
||||||
|
try (Repository repository = context.open(); Git git = Git.wrap(repository)) {
|
||||||
|
return new Worker(mirrorCommandRequest, repository, git).update();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new InternalRepositoryException(context.getRepository(), "error during git fetch", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Worker {
|
||||||
|
|
||||||
|
private final MirrorCommandRequest mirrorCommandRequest;
|
||||||
|
private final List<String> mirrorLog = new ArrayList<>();
|
||||||
|
private final Stopwatch stopwatch;
|
||||||
|
|
||||||
|
private final Repository repository;
|
||||||
|
private final Git git;
|
||||||
|
|
||||||
|
private FetchResult fetchResult;
|
||||||
|
private GitFilterContext filterContext;
|
||||||
|
private MirrorFilter.Filter filter;
|
||||||
|
|
||||||
|
private ResultType result = OK;
|
||||||
|
|
||||||
|
private Worker(MirrorCommandRequest mirrorCommandRequest, Repository repository, Git git) {
|
||||||
|
this.mirrorCommandRequest = mirrorCommandRequest;
|
||||||
|
this.repository = repository;
|
||||||
|
this.git = git;
|
||||||
|
stopwatch = Stopwatch.createStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult update() {
|
||||||
|
try {
|
||||||
|
return doUpdate();
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
result = FAILED;
|
||||||
|
mirrorLog.add("failed to synchronize: " + e.getMessage());
|
||||||
|
return new MirrorCommandResult(FAILED, mirrorLog, stopwatch.stop().elapsed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandResult doUpdate() throws GitAPIException {
|
||||||
|
fetchResult = createFetchCommand().call();
|
||||||
|
filterContext = new GitFilterContext();
|
||||||
|
filter = mirrorCommandRequest.getFilter().getFilter(filterContext);
|
||||||
|
|
||||||
|
if (fetchResult.getTrackingRefUpdates().isEmpty()) {
|
||||||
|
mirrorLog.add("No updates found");
|
||||||
|
} else {
|
||||||
|
handleBranches();
|
||||||
|
handleTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
postReceiveRepositoryHookEventFactory.fireForFetch(git, fetchResult);
|
||||||
|
return new MirrorCommandResult(result, mirrorLog, stopwatch.stop().elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleBranches() {
|
||||||
|
LoggerWithHeader logger = new LoggerWithHeader("Branches:");
|
||||||
|
doForEachRefStartingWith(MIRROR_REF_PREFIX + "heads", ref -> handleBranch(logger, ref));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleBranch(LoggerWithHeader logger, TrackingRefUpdate ref) {
|
||||||
|
MirrorReferenceUpdateHandler refHandler = new MirrorReferenceUpdateHandler(logger, ref, "heads/", "branch");
|
||||||
|
refHandler.handleRef(ref1 -> refHandler.testFilterForBranch());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTags() {
|
||||||
|
LoggerWithHeader logger = new LoggerWithHeader("Tags:");
|
||||||
|
doForEachRefStartingWith(MIRROR_REF_PREFIX + "tags", ref -> handleTag(logger, ref));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTag(LoggerWithHeader logger, TrackingRefUpdate ref) {
|
||||||
|
MirrorReferenceUpdateHandler refHandler = new MirrorReferenceUpdateHandler(logger, ref, "tags/", "tag");
|
||||||
|
refHandler.handleRef(ref1 -> refHandler.testFilterForTag());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MirrorReferenceUpdateHandler {
|
||||||
|
private final LoggerWithHeader logger;
|
||||||
|
private final TrackingRefUpdate ref;
|
||||||
|
private final String refType;
|
||||||
|
private final String typeForLog;
|
||||||
|
|
||||||
|
public MirrorReferenceUpdateHandler(LoggerWithHeader logger, TrackingRefUpdate ref, String refType, String typeForLog) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.ref = ref;
|
||||||
|
this.refType = refType;
|
||||||
|
this.typeForLog = typeForLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRef(Function<TrackingRefUpdate, Result> filter) {
|
||||||
|
Result filterResult = filter.apply(ref);
|
||||||
|
try {
|
||||||
|
String referenceName = ref.getLocalName().substring(MIRROR_REF_PREFIX.length() + refType.length());
|
||||||
|
if (filterResult.isAccepted()) {
|
||||||
|
handleAcceptedReference(referenceName);
|
||||||
|
} else {
|
||||||
|
handleRejectedRef(referenceName, filterResult);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleReferenceUpdateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result testFilterForBranch() {
|
||||||
|
try {
|
||||||
|
return filter.acceptBranch(filterContext.getBranchUpdate(ref.getLocalName()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return handleExceptionFromFilter(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleReferenceUpdateException(Exception e) {
|
||||||
|
LOG.warn("got exception processing ref {} in repository {}", ref.getLocalName(), GitMirrorCommand.this.repository, e);
|
||||||
|
mirrorLog.add(format("got error processing reference %s: %s", ref.getLocalName(), e.getMessage()));
|
||||||
|
mirrorLog.add("mirror may be damaged");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRejectedRef(String referenceName, Result filterResult) throws IOException {
|
||||||
|
result = REJECTED_UPDATES;
|
||||||
|
LOG.trace("{} ref rejected in {}: {}", typeForLog, GitMirrorCommand.this.repository, ref.getLocalName());
|
||||||
|
if (ref.getResult() == NEW) {
|
||||||
|
deleteReference(ref.getLocalName());
|
||||||
|
} else {
|
||||||
|
updateReference(ref.getLocalName(), ref.getOldObjectId());
|
||||||
|
}
|
||||||
|
logger.logChange(ref, referenceName, filterResult.getRejectReason().orElse("rejected due to filter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleAcceptedReference(String referenceName) throws IOException {
|
||||||
|
String targetRef = "refs/" + refType + referenceName;
|
||||||
|
if (isDeletedReference(ref)) {
|
||||||
|
LOG.trace("deleting {} ref in {}: {}", typeForLog, GitMirrorCommand.this.repository, targetRef);
|
||||||
|
deleteReference(targetRef);
|
||||||
|
logger.logChange(ref, referenceName, "deleted");
|
||||||
|
} else {
|
||||||
|
LOG.trace("updating {} ref in {}: {}", typeForLog, GitMirrorCommand.this.repository, targetRef);
|
||||||
|
updateReference(targetRef, ref.getNewObjectId());
|
||||||
|
logger.logChange(ref, referenceName, getUpdateType(ref));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result testFilterForTag() {
|
||||||
|
try {
|
||||||
|
return filter.acceptTag(filterContext.getTagUpdate(ref.getLocalName()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return handleExceptionFromFilter(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result handleExceptionFromFilter(Exception e) {
|
||||||
|
LOG.warn("got exception from filter for ref {} in repository {}", ref.getLocalName(), GitMirrorCommand.this.repository, e);
|
||||||
|
mirrorLog.add("! got error checking filter for update: " + e.getMessage());
|
||||||
|
return Result.reject("exception in filter");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteReference(String targetRef) throws IOException {
|
||||||
|
RefUpdate deleteUpdate = repository.getRefDatabase().newUpdate(targetRef, true);
|
||||||
|
deleteUpdate.setForceUpdate(true);
|
||||||
|
deleteUpdate.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDeletedReference(TrackingRefUpdate ref) {
|
||||||
|
return ref.asReceiveCommand().getType() == ReceiveCommand.Type.DELETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateReference(String reference, ObjectId objectId) throws IOException {
|
||||||
|
LOG.trace("updating ref in {}: {} -> {}", GitMirrorCommand.this.repository, reference, objectId);
|
||||||
|
RefUpdate refUpdate = repository.getRefDatabase().newUpdate(reference, true);
|
||||||
|
refUpdate.setNewObjectId(objectId);
|
||||||
|
refUpdate.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUpdateType(TrackingRefUpdate trackingRefUpdate) {
|
||||||
|
return trackingRefUpdate.getResult().name().toLowerCase(Locale.ENGLISH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LoggerWithHeader {
|
||||||
|
private final String header;
|
||||||
|
private boolean headerWritten = false;
|
||||||
|
|
||||||
|
private LoggerWithHeader(String header) {
|
||||||
|
this.header = header;
|
||||||
|
}
|
||||||
|
|
||||||
|
void logChange(TrackingRefUpdate ref, String branchName, String type) {
|
||||||
|
logLine(
|
||||||
|
format("- %s..%s %s (%s)",
|
||||||
|
ref.getOldObjectId().abbreviate(9).name(),
|
||||||
|
ref.getNewObjectId().abbreviate(9).name(),
|
||||||
|
branchName,
|
||||||
|
type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void logLine(String line) {
|
||||||
|
if (!headerWritten) {
|
||||||
|
headerWritten = true;
|
||||||
|
mirrorLog.add(header);
|
||||||
|
}
|
||||||
|
mirrorLog.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doForEachRefStartingWith(String prefix, RefUpdateConsumer refUpdateConsumer) {
|
||||||
|
fetchResult.getTrackingRefUpdates()
|
||||||
|
.stream()
|
||||||
|
.filter(ref -> ref.getLocalName().startsWith(prefix))
|
||||||
|
.forEach(ref -> {
|
||||||
|
try {
|
||||||
|
refUpdateConsumer.accept(ref);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new InternalRepositoryException(GitMirrorCommand.this.repository, "error updating mirror references", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private FetchCommand createFetchCommand() {
|
||||||
|
FetchCommand fetchCommand = Git.wrap(repository).fetch()
|
||||||
|
.setRefSpecs("refs/heads/*:" + MIRROR_REF_PREFIX + "heads/*", "refs/tags/*:" + MIRROR_REF_PREFIX + "tags/*")
|
||||||
|
.setForceUpdate(true)
|
||||||
|
.setRemoveDeletedRefs(true)
|
||||||
|
.setRemote(mirrorCommandRequest.getSourceUrl());
|
||||||
|
|
||||||
|
mirrorCommandRequest.getCredential(Pkcs12ClientCertificateCredential.class)
|
||||||
|
.ifPresent(c -> fetchCommand.setTransportConfigCallback(transport -> {
|
||||||
|
if (transport instanceof TransportHttp) {
|
||||||
|
TransportHttp transportHttp = (TransportHttp) transport;
|
||||||
|
transportHttp.setHttpConnectionFactory(mirrorHttpConnectionProvider.createHttpConnectionFactory(c, mirrorLog));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
mirrorCommandRequest.getCredential(UsernamePasswordCredential.class)
|
||||||
|
.ifPresent(c -> fetchCommand
|
||||||
|
.setCredentialsProvider(
|
||||||
|
new UsernamePasswordCredentialsProvider(
|
||||||
|
Strings.nullToEmpty(c.username()),
|
||||||
|
Strings.nullToEmpty(new String(c.password()))
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
return fetchCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GitFilterContext implements MirrorFilter.FilterContext {
|
||||||
|
|
||||||
|
private final Map<String, MirrorFilter.BranchUpdate> branchUpdates;
|
||||||
|
private final Map<String, MirrorFilter.TagUpdate> tagUpdates;
|
||||||
|
|
||||||
|
public GitFilterContext() {
|
||||||
|
Map<String, MirrorFilter.BranchUpdate> extractedBranchUpdates = new HashMap<>();
|
||||||
|
Map<String, MirrorFilter.TagUpdate> extractedTagUpdates = new HashMap<>();
|
||||||
|
|
||||||
|
fetchResult.getTrackingRefUpdates().forEach(refUpdate -> {
|
||||||
|
if (refUpdate.getLocalName().startsWith(MIRROR_REF_PREFIX + "heads")) {
|
||||||
|
extractedBranchUpdates.put(refUpdate.getLocalName(), new GitBranchUpdate(refUpdate));
|
||||||
|
}
|
||||||
|
if (refUpdate.getLocalName().startsWith(MIRROR_REF_PREFIX + "tags")) {
|
||||||
|
extractedTagUpdates.put(refUpdate.getLocalName(), new GitTagUpdate(refUpdate));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.branchUpdates = unmodifiableMap(extractedBranchUpdates);
|
||||||
|
this.tagUpdates = unmodifiableMap(extractedTagUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MirrorFilter.BranchUpdate> getBranchUpdates() {
|
||||||
|
return branchUpdates.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MirrorFilter.TagUpdate> getTagUpdates() {
|
||||||
|
return tagUpdates.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorFilter.BranchUpdate getBranchUpdate(String ref) {
|
||||||
|
return branchUpdates.get(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorFilter.TagUpdate getTagUpdate(String ref) {
|
||||||
|
return tagUpdates.get(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GitBranchUpdate implements MirrorFilter.BranchUpdate {
|
||||||
|
|
||||||
|
private final TrackingRefUpdate refUpdate;
|
||||||
|
|
||||||
|
private final String branchName;
|
||||||
|
|
||||||
|
private Changeset changeset;
|
||||||
|
|
||||||
|
public GitBranchUpdate(TrackingRefUpdate refUpdate) {
|
||||||
|
this.refUpdate = refUpdate;
|
||||||
|
this.branchName = refUpdate.getLocalName().substring(MIRROR_REF_PREFIX.length() + "heads/".length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBranchName() {
|
||||||
|
return branchName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Changeset> getChangeset() {
|
||||||
|
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.DELETE)) {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
if (changeset == null) {
|
||||||
|
changeset = computeChangeset();
|
||||||
|
}
|
||||||
|
return of(changeset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> getNewRevision() {
|
||||||
|
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.DELETE)) {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
return of(refUpdate.getNewObjectId().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> getOldRevision() {
|
||||||
|
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.CREATE)) {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
return of(refUpdate.getOldObjectId().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<MirrorFilter.UpdateType> getUpdateType() {
|
||||||
|
return getUpdateTypeFor(refUpdate.asReceiveCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isForcedUpdate() {
|
||||||
|
return refUpdate.getResult() == RefUpdate.Result.FORCED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Changeset computeChangeset() {
|
||||||
|
try (RevWalk revWalk = new RevWalk(repository); GitChangesetConverter gitChangesetConverter = converter(revWalk)) {
|
||||||
|
try {
|
||||||
|
RevCommit revCommit = revWalk.parseCommit(refUpdate.getNewObjectId());
|
||||||
|
return gitChangesetConverter.createChangeset(revCommit, refUpdate.getLocalName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new InternalRepositoryException(context.getRepository(), "got exception while validating branch", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GitChangesetConverter converter(RevWalk revWalk) {
|
||||||
|
return converterFactory.builder(repository)
|
||||||
|
.withRevWalk(revWalk)
|
||||||
|
.withAdditionalPublicKeys(mirrorCommandRequest.getPublicKeys())
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GitTagUpdate implements MirrorFilter.TagUpdate {
|
||||||
|
|
||||||
|
private final TrackingRefUpdate refUpdate;
|
||||||
|
|
||||||
|
private final String tagName;
|
||||||
|
|
||||||
|
private Tag tag;
|
||||||
|
|
||||||
|
public GitTagUpdate(TrackingRefUpdate refUpdate) {
|
||||||
|
this.refUpdate = refUpdate;
|
||||||
|
this.tagName = refUpdate.getLocalName().substring(MIRROR_REF_PREFIX.length() + "tags/".length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTagName() {
|
||||||
|
return tagName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Tag> getTag() {
|
||||||
|
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.DELETE)) {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
if (tag == null) {
|
||||||
|
tag = computeTag();
|
||||||
|
}
|
||||||
|
return of(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> getNewRevision() {
|
||||||
|
return getTag().map(Tag::getRevision);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> getOldRevision() {
|
||||||
|
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.CREATE)) {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
return of(refUpdate.getOldObjectId().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<MirrorFilter.UpdateType> getUpdateType() {
|
||||||
|
return getUpdateTypeFor(refUpdate.asReceiveCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tag computeTag() {
|
||||||
|
try (RevWalk revWalk = new RevWalk(repository)) {
|
||||||
|
try {
|
||||||
|
RevObject revObject = revWalk.parseAny(refUpdate.getNewObjectId());
|
||||||
|
if (revObject.getType() == Constants.OBJ_TAG) {
|
||||||
|
RevTag revTag = revWalk.parseTag(revObject.getId());
|
||||||
|
return gitTagConverter.buildTag(revTag, revWalk);
|
||||||
|
} else if (revObject.getType() == Constants.OBJ_COMMIT) {
|
||||||
|
Ref ref = repository.getRefDatabase().findRef(refUpdate.getLocalName());
|
||||||
|
Tag t = gitTagConverter.buildTag(repository, revWalk, ref);
|
||||||
|
return new Tag(tagName, t.getRevision(), t.getDate().orElse(null), t.getDeletable());
|
||||||
|
} else {
|
||||||
|
throw new InternalRepositoryException(context.getRepository(), "invalid object type for tag");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new InternalRepositoryException(context.getRepository(), "got exception while validating tag", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOfTypeOrEmpty(Optional<MirrorFilter.UpdateType> updateType, MirrorFilter.UpdateType type) {
|
||||||
|
return !updateType.isPresent() || updateType.get() == type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<MirrorFilter.UpdateType> getUpdateTypeFor(ReceiveCommand receiveCommand) {
|
||||||
|
switch (receiveCommand.getType()) {
|
||||||
|
case UPDATE:
|
||||||
|
case UPDATE_NONFASTFORWARD:
|
||||||
|
return of(MirrorFilter.UpdateType.UPDATE);
|
||||||
|
case CREATE:
|
||||||
|
return of(MirrorFilter.UpdateType.CREATE);
|
||||||
|
case DELETE:
|
||||||
|
return of(MirrorFilter.UpdateType.DELETE);
|
||||||
|
default:
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface RefUpdateConsumer {
|
||||||
|
void accept(TrackingRefUpdate refUpdate) throws IOException;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,26 +32,20 @@ import org.eclipse.jgit.api.errors.GitAPIException;
|
|||||||
import org.eclipse.jgit.lib.ObjectId;
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.eclipse.jgit.transport.FetchResult;
|
import org.eclipse.jgit.transport.FetchResult;
|
||||||
import org.eclipse.jgit.transport.RefSpec;
|
|
||||||
import org.eclipse.jgit.transport.TagOpt;
|
|
||||||
import org.eclipse.jgit.transport.TrackingRefUpdate;
|
import org.eclipse.jgit.transport.TrackingRefUpdate;
|
||||||
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.ContextEntry;
|
import sonia.scm.ContextEntry;
|
||||||
import sonia.scm.event.ScmEventBus;
|
|
||||||
import sonia.scm.repository.GitRepositoryHandler;
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
import sonia.scm.repository.GitUtil;
|
import sonia.scm.repository.GitUtil;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.Tag;
|
|
||||||
import sonia.scm.repository.api.ImportFailedException;
|
import sonia.scm.repository.api.ImportFailedException;
|
||||||
import sonia.scm.repository.api.PullResponse;
|
import sonia.scm.repository.api.PullResponse;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
@@ -59,19 +53,15 @@ import java.util.stream.Collectors;
|
|||||||
public class GitPullCommand extends AbstractGitPushOrPullCommand
|
public class GitPullCommand extends AbstractGitPushOrPullCommand
|
||||||
implements PullCommand {
|
implements PullCommand {
|
||||||
|
|
||||||
private static final String REF_SPEC = "refs/heads/*:refs/heads/*";
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(GitPullCommand.class);
|
private static final Logger LOG = LoggerFactory.getLogger(GitPullCommand.class);
|
||||||
private final ScmEventBus eventBus;
|
private final PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory;
|
||||||
private final GitRepositoryHookEventFactory eventFactory;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GitPullCommand(GitRepositoryHandler handler,
|
public GitPullCommand(GitRepositoryHandler handler,
|
||||||
GitContext context,
|
GitContext context,
|
||||||
ScmEventBus eventBus,
|
PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory) {
|
||||||
GitRepositoryHookEventFactory eventFactory) {
|
|
||||||
super(handler, context);
|
super(handler, context);
|
||||||
this.eventBus = eventBus;
|
this.postReceiveRepositoryHookEventFactory = postReceiveRepositoryHookEventFactory;
|
||||||
this.eventFactory = eventFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -158,8 +148,8 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
|||||||
|
|
||||||
org.eclipse.jgit.lib.Repository source = null;
|
org.eclipse.jgit.lib.Repository source = null;
|
||||||
|
|
||||||
try {
|
try (Git git = Git.open(sourceDirectory)) {
|
||||||
source = Git.open(sourceDirectory).getRepository();
|
source = git.getRepository();
|
||||||
response = new PullResponse(push(source, getRemoteUrl(targetDirectory)));
|
response = new PullResponse(push(source, getRemoteUrl(targetDirectory)));
|
||||||
} finally {
|
} finally {
|
||||||
GitUtil.close(source);
|
GitUtil.close(source);
|
||||||
@@ -177,16 +167,14 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
|||||||
FetchResult result;
|
FetchResult result;
|
||||||
try {
|
try {
|
||||||
//J-
|
//J-
|
||||||
result = git.fetch()
|
result = GitUtil.createFetchCommandWithBranchAndTagUpdate(git)
|
||||||
.setCredentialsProvider(
|
.setCredentialsProvider(
|
||||||
new UsernamePasswordCredentialsProvider(
|
new UsernamePasswordCredentialsProvider(
|
||||||
Strings.nullToEmpty(request.getUsername()),
|
Strings.nullToEmpty(request.getUsername()),
|
||||||
Strings.nullToEmpty(request.getPassword())
|
Strings.nullToEmpty(request.getPassword())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.setRefSpecs(new RefSpec(REF_SPEC))
|
|
||||||
.setRemote(request.getRemoteUrl().toExternalForm())
|
.setRemote(request.getRemoteUrl().toExternalForm())
|
||||||
.setTagOpt(TagOpt.FETCH_TAGS)
|
|
||||||
.call();
|
.call();
|
||||||
//J+
|
//J+
|
||||||
|
|
||||||
@@ -206,31 +194,6 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void firePostReceiveRepositoryHookEvent(Git git, FetchResult result) {
|
private void firePostReceiveRepositoryHookEvent(Git git, FetchResult result) {
|
||||||
try {
|
postReceiveRepositoryHookEventFactory.fireForFetch(git, result);
|
||||||
List<String> branches = getBranchesFromFetchResult(result);
|
|
||||||
List<Tag> tags = getTagsFromFetchResult(result);
|
|
||||||
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git);
|
|
||||||
eventBus.post(eventFactory.createEvent(context, branches, tags, changesetResolver));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ImportFailedException(
|
|
||||||
ContextEntry.ContextBuilder.entity(context.getRepository()).build(),
|
|
||||||
"Could not fire post receive repository hook event after unbundle",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Tag> getTagsFromFetchResult(FetchResult result) {
|
|
||||||
return result.getAdvertisedRefs().stream()
|
|
||||||
.filter(r -> r.getName().startsWith("refs/tags/"))
|
|
||||||
.map(r -> new Tag(r.getName().substring("refs/tags/".length()), r.getObjectId().getName()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getBranchesFromFetchResult(FetchResult result) {
|
|
||||||
return result.getAdvertisedRefs().stream()
|
|
||||||
.filter(r -> r.getName().startsWith("refs/heads/"))
|
|
||||||
.map(r -> r.getLeaf().getName().substring("refs/heads/".length()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
|
|||||||
Command.MERGE,
|
Command.MERGE,
|
||||||
Command.MODIFY,
|
Command.MODIFY,
|
||||||
Command.BUNDLE,
|
Command.BUNDLE,
|
||||||
Command.UNBUNDLE
|
Command.UNBUNDLE,
|
||||||
|
Command.MIRROR
|
||||||
);
|
);
|
||||||
|
|
||||||
protected static final Set<Feature> FEATURES = EnumSet.of(Feature.INCOMING_REVISION);
|
protected static final Set<Feature> FEATURES = EnumSet.of(Feature.INCOMING_REVISION);
|
||||||
@@ -171,6 +172,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
|
|||||||
return commandInjector.getInstance(GitUnbundleCommand.class);
|
return commandInjector.getInstance(GitUnbundleCommand.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MirrorCommand getMirrorCommand() {
|
||||||
|
return commandInjector.getInstance(GitMirrorCommand.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<Command> getSupportedCommands() {
|
public Set<Command> getSupportedCommands() {
|
||||||
return COMMANDS;
|
return COMMANDS;
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Constants;
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.eclipse.jgit.revwalk.RevObject;
|
||||||
|
import org.eclipse.jgit.revwalk.RevTag;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.GitUtil;
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.security.GPG;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
class GitTagConverter {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(GitTagConverter.class);
|
||||||
|
|
||||||
|
private final GPG gpg;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
GitTagConverter(GPG gpg) {
|
||||||
|
this.gpg = gpg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tag buildTag(RevTag revTag, RevWalk revWalk) {
|
||||||
|
Tag tag = null;
|
||||||
|
try {
|
||||||
|
RevCommit revCommit = revWalk.parseCommit(revTag.getObject().getId());
|
||||||
|
tag = new Tag(revTag.getTagName(), revCommit.getId().name(), revTag.getTaggerIdent().getWhen().getTime());
|
||||||
|
GitUtil.getTagSignature(revTag, gpg, revWalk).ifPresent(tag::addSignature);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("could not get commit for tag", ex);
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tag buildTag(Repository repository, RevWalk revWalk, Ref ref) {
|
||||||
|
Tag tag = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
RevCommit revCommit = GitUtil.getCommit(repository, revWalk, ref);
|
||||||
|
if (revCommit != null) {
|
||||||
|
String name = GitUtil.getTagName(ref);
|
||||||
|
tag = new Tag(name, revCommit.getId().name(), GitUtil.getTagTime(revWalk, ref.getObjectId()));
|
||||||
|
RevObject revObject = revWalk.parseAny(ref.getObjectId());
|
||||||
|
if (revObject.getType() == Constants.OBJ_TAG) {
|
||||||
|
RevTag revTag = (RevTag) revObject;
|
||||||
|
GitUtil.getTagSignature(revTag, gpg, revWalk)
|
||||||
|
.ifPresent(tag::addSignature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("could not get commit for tag", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -26,28 +26,19 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import com.google.common.base.Function;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
import org.eclipse.jgit.lib.Constants;
|
|
||||||
import org.eclipse.jgit.lib.Ref;
|
import org.eclipse.jgit.lib.Ref;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
|
||||||
import org.eclipse.jgit.revwalk.RevObject;
|
|
||||||
import org.eclipse.jgit.revwalk.RevTag;
|
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import sonia.scm.repository.GitUtil;
|
|
||||||
import sonia.scm.repository.InternalRepositoryException;
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
import sonia.scm.security.GPG;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +46,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
||||||
|
|
||||||
private final GPG gpg;
|
private final GitTagConverter gitTagConverter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs ...
|
* Constructs ...
|
||||||
@@ -63,108 +54,23 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
|||||||
* @param context
|
* @param context
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
public GitTagsCommand(GitContext context, GPG gpp) {
|
public GitTagsCommand(GitContext context, GitTagConverter gitTagConverter) {
|
||||||
super(context);
|
super(context);
|
||||||
this.gpg = gpp;
|
this.gitTagConverter = gitTagConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Tag> getTags() throws IOException {
|
public List<Tag> getTags() throws IOException {
|
||||||
List<Tag> tags;
|
try (Git git = new Git(open()); RevWalk revWalk = new RevWalk(git.getRepository())) {
|
||||||
|
|
||||||
RevWalk revWalk = null;
|
|
||||||
|
|
||||||
try (Git git = new Git(open())) {
|
|
||||||
revWalk = new RevWalk(git.getRepository());
|
|
||||||
|
|
||||||
List<Ref> tagList = git.tagList().call();
|
List<Ref> tagList = git.tagList().call();
|
||||||
|
|
||||||
tags = Lists.transform(tagList,
|
return tagList.stream()
|
||||||
new TransformFunction(git.getRepository(), revWalk, gpg));
|
.map(ref -> gitTagConverter.buildTag(git.getRepository(), revWalk, ref))
|
||||||
|
.collect(toList());
|
||||||
} catch (GitAPIException ex) {
|
} catch (GitAPIException ex) {
|
||||||
throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
|
throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
|
||||||
} finally {
|
|
||||||
GitUtil.release(revWalk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
//~--- inner classes --------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class description
|
|
||||||
*
|
|
||||||
* @author Enter your name here...
|
|
||||||
* @version Enter version here..., 12/07/06
|
|
||||||
*/
|
|
||||||
private static class TransformFunction implements Function<Ref, Tag> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the logger for TransformFuntion
|
|
||||||
*/
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(TransformFunction.class);
|
|
||||||
|
|
||||||
//~--- constructors -------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs ...
|
|
||||||
* @param repository
|
|
||||||
* @param revWalk
|
|
||||||
*/
|
|
||||||
public TransformFunction(Repository repository,
|
|
||||||
RevWalk revWalk,
|
|
||||||
GPG gpg) {
|
|
||||||
this.repository = repository;
|
|
||||||
this.revWalk = revWalk;
|
|
||||||
this.gpg = gpg;
|
|
||||||
}
|
|
||||||
|
|
||||||
//~--- methods ------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
* @param ref
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Tag apply(Ref ref) {
|
|
||||||
Tag tag = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
RevCommit revCommit = GitUtil.getCommit(repository, revWalk, ref);
|
|
||||||
if (revCommit != null) {
|
|
||||||
String name = GitUtil.getTagName(ref);
|
|
||||||
tag = new Tag(name, revCommit.getId().name(), GitUtil.getTagTime(revWalk, ref.getObjectId()));
|
|
||||||
RevObject revObject = revWalk.parseAny(ref.getObjectId());
|
|
||||||
if (revObject.getType() == Constants.OBJ_TAG) {
|
|
||||||
RevTag revTag = (RevTag) revObject;
|
|
||||||
GitUtil.getTagSignature(revTag, gpg, revWalk)
|
|
||||||
.ifPresent(tag::addSignature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException ex) {
|
|
||||||
logger.error("could not get commit for tag", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
//~--- fields -------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Field description
|
|
||||||
*/
|
|
||||||
private final org.eclipse.jgit.lib.Repository repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Field description
|
|
||||||
*/
|
|
||||||
private final RevWalk revWalk;
|
|
||||||
private final GPG gpg;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.transport.http.HttpConnectionFactory2;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.api.Pkcs12ClientCertificateCredential;
|
||||||
|
import sonia.scm.web.ScmHttpConnectionFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.net.ssl.KeyManager;
|
||||||
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
class MirrorHttpConnectionProvider {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MirrorHttpConnectionProvider.class);
|
||||||
|
|
||||||
|
private final Provider<TrustManager> trustManagerProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MirrorHttpConnectionProvider(Provider<TrustManager> trustManagerProvider) {
|
||||||
|
this.trustManagerProvider = trustManagerProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpConnectionFactory2 createHttpConnectionFactory(Pkcs12ClientCertificateCredential credential, List<String> log) {
|
||||||
|
return new ScmHttpConnectionFactory(trustManagerProvider, createKeyManagers(credential, log));
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyManager[] createKeyManagers(Pkcs12ClientCertificateCredential credential, List<String> log) {
|
||||||
|
try {
|
||||||
|
KeyStore pkcs12 = KeyStore.getInstance("PKCS12");
|
||||||
|
pkcs12.load(new ByteArrayInputStream(credential.getCertificate()), credential.getPassword());
|
||||||
|
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||||
|
keyManagerFactory.init(pkcs12, credential.getPassword());
|
||||||
|
log.add("added pkcs12 certificate");
|
||||||
|
return keyManagerFactory.getKeyManagers();
|
||||||
|
} catch (IOException | GeneralSecurityException e) {
|
||||||
|
LOG.info("could not create key store from pkcs12 credential", e);
|
||||||
|
log.add("failed to add pkcs12 certificate: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeyManager[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.transport.FetchResult;
|
||||||
|
import sonia.scm.ContextEntry;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
|
||||||
|
import sonia.scm.repository.RepositoryHookEvent;
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.repository.WrappedRepositoryHookEvent;
|
||||||
|
import sonia.scm.repository.api.ImportFailedException;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
class PostReceiveRepositoryHookEventFactory {
|
||||||
|
|
||||||
|
private final ScmEventBus eventBus;
|
||||||
|
private final GitRepositoryHookEventFactory eventFactory;
|
||||||
|
private final GitContext context;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
PostReceiveRepositoryHookEventFactory(ScmEventBus eventBus, GitRepositoryHookEventFactory eventFactory, GitContext context) {
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.eventFactory = eventFactory;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fireForFetch(Git git, FetchResult result) {
|
||||||
|
PostReceiveRepositoryHookEvent event;
|
||||||
|
try {
|
||||||
|
List<String> branches = getBranchesFromFetchResult(result);
|
||||||
|
List<Tag> tags = getTagsFromFetchResult(result);
|
||||||
|
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git);
|
||||||
|
event = new PostReceiveRepositoryHookEvent(WrappedRepositoryHookEvent.wrap(eventFactory.createEvent(context, branches, tags, changesetResolver)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ImportFailedException(
|
||||||
|
ContextEntry.ContextBuilder.entity(context.getRepository()).build(),
|
||||||
|
"Could not fire post receive repository hook event after fetch",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eventBus.post(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Tag> getTagsFromFetchResult(FetchResult result) {
|
||||||
|
return result.getAdvertisedRefs().stream()
|
||||||
|
.filter(r -> r.getName().startsWith("refs/tags/"))
|
||||||
|
.map(r -> new Tag(r.getName().substring("refs/tags/".length()), r.getObjectId().getName()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getBranchesFromFetchResult(FetchResult result) {
|
||||||
|
return result.getAdvertisedRefs().stream()
|
||||||
|
.filter(r -> r.getName().startsWith("refs/heads/"))
|
||||||
|
.map(r -> r.getLeaf().getName().substring("refs/heads/".length()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,16 +38,13 @@ import sonia.scm.repository.spi.SimpleGitWorkingCopyFactory;
|
|||||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@Extension
|
@Extension
|
||||||
public class GitServletModule extends ServletModule
|
public class GitServletModule extends ServletModule {
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configureServlets()
|
protected void configureServlets() {
|
||||||
{
|
|
||||||
bind(GitRepositoryViewer.class);
|
bind(GitRepositoryViewer.class);
|
||||||
bind(GitRepositoryResolver.class);
|
bind(GitRepositoryResolver.class);
|
||||||
bind(GitReceivePackFactory.class);
|
bind(GitReceivePackFactory.class);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.google.inject.Singleton;
|
|||||||
import org.eclipse.jgit.http.server.GitServlet;
|
import org.eclipse.jgit.http.server.GitServlet;
|
||||||
import org.eclipse.jgit.lfs.lib.Constants;
|
import org.eclipse.jgit.lfs.lib.Constants;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
import sonia.scm.protocolcommand.git.ScmUploadPackFactoryForHttpServletRequest;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryRequestListenerUtil;
|
import sonia.scm.repository.RepositoryRequestListenerUtil;
|
||||||
import sonia.scm.repository.spi.ScmProviderHttpServlet;
|
import sonia.scm.repository.spi.ScmProviderHttpServlet;
|
||||||
@@ -71,6 +72,7 @@ public class ScmGitServlet extends GitServlet implements ScmProviderHttpServlet
|
|||||||
@Inject
|
@Inject
|
||||||
public ScmGitServlet(GitRepositoryResolver repositoryResolver,
|
public ScmGitServlet(GitRepositoryResolver repositoryResolver,
|
||||||
GitReceivePackFactory receivePackFactory,
|
GitReceivePackFactory receivePackFactory,
|
||||||
|
ScmUploadPackFactoryForHttpServletRequest scmUploadPackFactory,
|
||||||
GitRepositoryViewer repositoryViewer,
|
GitRepositoryViewer repositoryViewer,
|
||||||
RepositoryRequestListenerUtil repositoryRequestListenerUtil,
|
RepositoryRequestListenerUtil repositoryRequestListenerUtil,
|
||||||
LfsServletFactory lfsServletFactory)
|
LfsServletFactory lfsServletFactory)
|
||||||
@@ -81,6 +83,7 @@ public class ScmGitServlet extends GitServlet implements ScmProviderHttpServlet
|
|||||||
|
|
||||||
setRepositoryResolver(repositoryResolver);
|
setRepositoryResolver(repositoryResolver);
|
||||||
setReceivePackFactory(receivePackFactory);
|
setReceivePackFactory(receivePackFactory);
|
||||||
|
setUploadPackFactory(scmUploadPackFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.web;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.internal.JGitText;
|
||||||
|
import org.eclipse.jgit.transport.http.HttpConnection;
|
||||||
|
import org.eclipse.jgit.transport.http.JDKHttpConnection;
|
||||||
|
import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory;
|
||||||
|
import org.eclipse.jgit.transport.http.NoCheckX509TrustManager;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.net.ssl.KeyManager;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
|
public class ScmHttpConnectionFactory extends JDKHttpConnectionFactory {
|
||||||
|
|
||||||
|
private final Provider<TrustManager> trustManagerProvider;
|
||||||
|
private final KeyManager[] keyManagers;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ScmHttpConnectionFactory(Provider<TrustManager> trustManagerProvider) {
|
||||||
|
this(trustManagerProvider, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScmHttpConnectionFactory(Provider<TrustManager> trustManagerProvider, KeyManager[] keyManagers) {
|
||||||
|
this.trustManagerProvider = trustManagerProvider;
|
||||||
|
this.keyManagers = keyManagers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GitSession newSession() {
|
||||||
|
return new ScmConnectionSession(trustManagerProvider.get(), keyManagers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ScmConnectionSession implements GitSession {
|
||||||
|
|
||||||
|
private final TrustManager trustManager;
|
||||||
|
private final KeyManager[] keyManagers;
|
||||||
|
|
||||||
|
private ScmConnectionSession(TrustManager trustManager, KeyManager[] keyManagers) {
|
||||||
|
this.trustManager = trustManager;
|
||||||
|
this.keyManagers = keyManagers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("java:S5527")
|
||||||
|
public JDKHttpConnection configure(HttpConnection connection,
|
||||||
|
boolean sslVerify) throws GeneralSecurityException {
|
||||||
|
if (!(connection instanceof JDKHttpConnection)) {
|
||||||
|
throw new IllegalArgumentException(MessageFormat.format(
|
||||||
|
JGitText.get().httpWrongConnectionType,
|
||||||
|
JDKHttpConnection.class.getName(),
|
||||||
|
connection.getClass().getName()));
|
||||||
|
}
|
||||||
|
JDKHttpConnection conn = (JDKHttpConnection) connection;
|
||||||
|
String scheme = conn.getURL().getProtocol();
|
||||||
|
if ("https".equals(scheme) && sslVerify) { //$NON-NLS-1$
|
||||||
|
// sslVerify == true: use the JDK defaults
|
||||||
|
conn.configure(keyManagers, new TrustManager[]{trustManager}, null);
|
||||||
|
} else if ("https".equals(scheme)) {
|
||||||
|
conn.configure(keyManagers, new TrustManager[]{new NoCheckX509TrustManager()}, null);
|
||||||
|
conn.setHostnameVerifier((name, value) -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.protocolcommand.git;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
class MirrorRefFilterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRemoveMirrorRefs() {
|
||||||
|
Map<String, Ref> refs = new HashMap<>();
|
||||||
|
Ref master = mock(Ref.class);
|
||||||
|
Ref mirror = mock(Ref.class);
|
||||||
|
refs.put("refs/heads/master", master);
|
||||||
|
refs.put("refs/mirror/some/other", mirror);
|
||||||
|
|
||||||
|
Map<String, Ref> filteredRefs = MirrorRefFilter.filterMirrors(refs);
|
||||||
|
|
||||||
|
assertThat(filteredRefs).containsOnly(entry("refs/heads/master", master));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
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.security.GPG;
|
||||||
|
import sonia.scm.security.PublicKey;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptySet;
|
||||||
|
import static java.util.Collections.singleton;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class GPGSignatureResolverTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GPG gpg;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDelegateFindPublicKeyId() {
|
||||||
|
GPGSignatureResolver signatureResolver = new GPGSignatureResolver(gpg, emptySet());
|
||||||
|
|
||||||
|
byte[] signature = new byte[]{4, 2};
|
||||||
|
when(gpg.findPublicKeyId(signature)).thenReturn("0x42");
|
||||||
|
|
||||||
|
assertThat(signatureResolver.findPublicKeyId(signature)).isEqualTo("0x42");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveStoredGpgKey() {
|
||||||
|
GPGSignatureResolver signatureResolver = new GPGSignatureResolver(gpg, emptySet());
|
||||||
|
|
||||||
|
PublicKey publicKey = createPublicKey("0x42");
|
||||||
|
when(gpg.findPublicKey("0x42")).thenReturn(Optional.of(publicKey));
|
||||||
|
|
||||||
|
Optional<PublicKey> resolverPublicKey = signatureResolver.findPublicKey("0x42");
|
||||||
|
assertThat(resolverPublicKey).contains(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveKeyFormList() {
|
||||||
|
PublicKey publicKey = createPublicKey("0x21");
|
||||||
|
GPGSignatureResolver signatureResolver = new GPGSignatureResolver(gpg, singleton(publicKey));
|
||||||
|
|
||||||
|
Optional<PublicKey> resolverPublicKey = signatureResolver.findPublicKey("0x21");
|
||||||
|
assertThat(resolverPublicKey).contains(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveSubkeyFormList() {
|
||||||
|
PublicKey publicKey = createPublicKey("0x21", "0x42");
|
||||||
|
GPGSignatureResolver signatureResolver = new GPGSignatureResolver(gpg, singleton(publicKey));
|
||||||
|
|
||||||
|
Optional<PublicKey> resolverPublicKey = signatureResolver.findPublicKey("0x42");
|
||||||
|
assertThat(resolverPublicKey).contains(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKey createPublicKey(String id, String... subkeys) {
|
||||||
|
PublicKey publicKey = mock(PublicKey.class);
|
||||||
|
lenient().when(publicKey.getId()).thenReturn(id);
|
||||||
|
lenient().when(publicKey.getSubkeys()).thenReturn(ImmutableSet.copyOf(subkeys));
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -93,12 +93,13 @@ public class GitIncomingCommandTest
|
|||||||
|
|
||||||
commit(outgoing, "added a");
|
commit(outgoing, "added a");
|
||||||
|
|
||||||
|
GitContext context = new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()), new GitConfig());
|
||||||
|
PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory = new PostReceiveRepositoryHookEventFactory(eventBus, eventFactory, context);
|
||||||
|
|
||||||
GitPullCommand pull = new GitPullCommand(
|
GitPullCommand pull = new GitPullCommand(
|
||||||
handler,
|
handler,
|
||||||
new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()), new GitConfig()),
|
context,
|
||||||
eventBus,
|
postReceiveRepositoryHookEventFactory);
|
||||||
eventFactory
|
|
||||||
);
|
|
||||||
PullCommandRequest req = new PullCommandRequest();
|
PullCommandRequest req = new PullCommandRequest();
|
||||||
req.setRemoteRepository(outgoingRepository);
|
req.setRemoteRepository(outgoingRepository);
|
||||||
pull.pull(req);
|
pull.pull(req);
|
||||||
|
|||||||
@@ -0,0 +1,796 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.junit.http.AppServer;
|
||||||
|
import org.eclipse.jgit.junit.http.SimpleHttpServer;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.revwalk.RevObject;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
|
import sonia.scm.repository.GitChangesetConverterFactory;
|
||||||
|
import sonia.scm.repository.GitConfig;
|
||||||
|
import sonia.scm.repository.api.MirrorCommandResult;
|
||||||
|
import sonia.scm.repository.api.MirrorFilter;
|
||||||
|
import sonia.scm.repository.api.SimpleUsernamePasswordCredential;
|
||||||
|
import sonia.scm.security.GPG;
|
||||||
|
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.FAILED;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.OK;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.REJECTED_UPDATES;
|
||||||
|
|
||||||
|
public class GitMirrorCommandTest extends AbstractGitCommandTestBase {
|
||||||
|
|
||||||
|
public static final Consumer<MirrorCommandRequest> ACCEPT_ALL = r -> {
|
||||||
|
};
|
||||||
|
public static final Consumer<MirrorCommandRequest> REJECT_ALL = r -> r.setFilter(new DenyAllMirrorFilter());
|
||||||
|
private final PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory = mock(PostReceiveRepositoryHookEventFactory.class);
|
||||||
|
private final MirrorHttpConnectionProvider mirrorHttpConnectionProvider = mock(MirrorHttpConnectionProvider.class);
|
||||||
|
private final GPG gpg = mock(GPG.class);
|
||||||
|
private final GitChangesetConverterFactory gitChangesetConverterFactory = new GitChangesetConverterFactory(gpg);
|
||||||
|
private final GitTagConverter gitTagConverter = new GitTagConverter(gpg);
|
||||||
|
|
||||||
|
private File clone;
|
||||||
|
private GitMirrorCommand command;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bendContextToNewRepository() throws IOException, GitAPIException {
|
||||||
|
clone = tempFolder.newFolder();
|
||||||
|
Git.init().setBare(true).setDirectory(clone).call();
|
||||||
|
|
||||||
|
GitContext emptyContext = createMirrorContext(clone);
|
||||||
|
command = new GitMirrorCommand(emptyContext, postReceiveRepositoryHookEventFactory, mirrorHttpConnectionProvider, gitChangesetConverterFactory, gitTagConverter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateInitialMirror() throws IOException, GitAPIException {
|
||||||
|
MirrorCommandResult result = callMirrorCommand();
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).contains("Branches:")
|
||||||
|
.contains("- 000000000..fcd0ef183 master (new)")
|
||||||
|
.contains("- 000000000..3f76a12f0 test-branch (new)")
|
||||||
|
.contains("Tags:")
|
||||||
|
.contains("- 000000000..86a6645ec test-tag (new)");
|
||||||
|
|
||||||
|
try (Git createdMirror = Git.open(clone)) {
|
||||||
|
assertThat(createdMirror.branchList().call()).isNotEmpty();
|
||||||
|
assertThat(createdMirror.tagList().call()).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(postReceiveRepositoryHookEventFactory).fireForFetch(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateEmptyLogWhenNoChangesFound() {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(ACCEPT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).containsExactly("No updates found");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateMirrorWithNewBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchCreate().setName("added-branch").call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(ACCEPT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Branches:",
|
||||||
|
"- 000000000..fcd0ef183 added-branch (new)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> addedBranch = findBranch(updatedMirror, "added-branch");
|
||||||
|
assertThat(addedBranch).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// event should be thrown two times, once for the initial clone, and once for the update
|
||||||
|
verify(postReceiveRepositoryHookEventFactory, times(2)).fireForFetch(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateMirrorWithForcedBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchCreate().setStartPoint("9e93d8631675a89615fac56b09209686146ff3c0").setName("test-branch").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(ACCEPT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Branches:",
|
||||||
|
"- 3f76a12f0..9e93d8631 test-branch (forced)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> updatedBranch = findBranch(updatedMirror, "test-branch");
|
||||||
|
assertThat(updatedBranch).hasValueSatisfying(ref -> assertThat(ref.getObjectId().getName()).isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateMirrorWithDeletedBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchDelete().setBranchNames("test-branch").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(ACCEPT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Branches:",
|
||||||
|
"- 3f76a12f0..000000000 test-branch (deleted)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> deletedBranch = findBranch(updatedMirror, "test-branch");
|
||||||
|
assertThat(deletedBranch).isNotPresent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateMirrorWithNewTag() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
RevObject revObject = getRevObject(existingClone, "9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
existingClone.tag().setName("added-tag").setAnnotated(false).setObjectId(revObject).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(ACCEPT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Tags:",
|
||||||
|
"- 000000000..9e93d8631 added-tag (new)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> addedTag = findTag(updatedMirror, "added-tag");
|
||||||
|
assertThat(addedTag).hasValueSatisfying(ref -> assertThat(ref.getObjectId().getName()).isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// event should be thrown two times, once for the initial clone, and once for the update
|
||||||
|
verify(postReceiveRepositoryHookEventFactory, times(2)).fireForFetch(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateMirrorWithChangedTag() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
RevObject revObject = getRevObject(existingClone, "9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
existingClone.tag().setName("test-tag").setObjectId(revObject).setForceUpdate(true).setAnnotated(false).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(ACCEPT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Tags:",
|
||||||
|
"- 86a6645ec..9e93d8631 test-tag (forced)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> updatedTag = findTag(updatedMirror, "test-tag");
|
||||||
|
assertThat(updatedTag).hasValueSatisfying(ref -> assertThat(ref.getObjectId().getName()).isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateMirrorWithDeletedTag() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.tagDelete().setTags("test-tag").call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(ACCEPT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Tags:",
|
||||||
|
"- 86a6645ec..000000000 test-tag (deleted)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> deletedTag = findTag(updatedMirror, "test-tag");
|
||||||
|
assertThat(deletedTag).isNotPresent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRevertRejectedAddedBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchCreate().setName("added-branch").call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(REJECT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(REJECTED_UPDATES);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Branches:",
|
||||||
|
"- 000000000..fcd0ef183 added-branch (rejected due to filter)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> rejectedBranch = findBranch(updatedMirror, "added-branch");
|
||||||
|
assertThat(rejectedBranch).isNotPresent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRevertRejectedChangedBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchCreate().setStartPoint("9e93d8631675a89615fac56b09209686146ff3c0").setName("test-branch").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(REJECT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(REJECTED_UPDATES);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Branches:",
|
||||||
|
"- 3f76a12f0..9e93d8631 test-branch (rejected due to filter)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> rejectedBranch = findBranch(updatedMirror, "test-branch");
|
||||||
|
assertThat(rejectedBranch).get().extracting("objectId.name").hasToString("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRevertRejectedDeletedBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchDelete().setBranchNames("test-branch").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(REJECT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(REJECTED_UPDATES);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Branches:",
|
||||||
|
"- 3f76a12f0..000000000 test-branch (rejected due to filter)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> rejectedBranch = findBranch(updatedMirror, "test-branch");
|
||||||
|
assertThat(rejectedBranch).get().extracting("objectId.name").hasToString("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRevertRejectedNewTag() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
RevObject revObject = getRevObject(existingClone, "9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
existingClone.tag().setName("added-tag").setAnnotated(false).setObjectId(revObject).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(REJECT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(REJECTED_UPDATES);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Tags:",
|
||||||
|
"- 000000000..9e93d8631 added-tag (rejected due to filter)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> rejectedTag = findTag(updatedMirror, "added-tag");
|
||||||
|
assertThat(rejectedTag).isNotPresent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRevertRejectedChangedTag() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
RevObject revObject = getRevObject(existingClone, "9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
existingClone.tag().setName("test-tag").setObjectId(revObject).setForceUpdate(true).setAnnotated(false).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(REJECT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(REJECTED_UPDATES);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Tags:",
|
||||||
|
"- 86a6645ec..9e93d8631 test-tag (rejected due to filter)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> rejectedTag = findTag(updatedMirror, "test-tag");
|
||||||
|
assertThat(rejectedTag).hasValueSatisfying(ref -> assertThat(ref.getObjectId().getName()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRevertRejectedDeletedTag() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.tagDelete().setTags("test-tag").call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(REJECT_ALL);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(REJECTED_UPDATES);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Tags:",
|
||||||
|
"- 86a6645ec..000000000 test-tag (rejected due to filter)"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Git updatedMirror = Git.open(clone)) {
|
||||||
|
Optional<Ref> rejectedTag = findTag(updatedMirror, "test-tag");
|
||||||
|
assertThat(rejectedTag).hasValueSatisfying(ref -> assertThat(ref.getObjectId().getName()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRejectWithCustomMessage() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.tagDelete().setTags("test-tag").call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(r -> r.setFilter(new DenyAllWithReasonMirrorFilter("thou shalt not pass")));
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(REJECTED_UPDATES);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"Tags:",
|
||||||
|
"- 86a6645ec..000000000 test-tag (thou shalt not pass)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldLogExceptionsFromFilter() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.tagDelete().setTags("test-tag").call();
|
||||||
|
}
|
||||||
|
|
||||||
|
MirrorCommandResult result = callUpdate(r -> r.setFilter(new ErroneousMirrorFilterThrowingExceptions()));
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(REJECTED_UPDATES);
|
||||||
|
assertThat(result.getLog()).containsExactly(
|
||||||
|
"! got error checking filter for update: this tag creates an exception",
|
||||||
|
"Tags:",
|
||||||
|
"- 86a6645ec..000000000 test-tag (exception in filter)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldMarkForcedBranchUpdate() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchCreate().setStartPoint("9e93d8631675a89615fac56b09209686146ff3c0").setName("test-branch").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
InvocationCheck filterInvokedCheck = mock(InvocationCheck.class);
|
||||||
|
|
||||||
|
callUpdate(r -> r.setFilter(new MirrorFilter() {
|
||||||
|
@Override
|
||||||
|
public Filter getFilter(FilterContext context) {
|
||||||
|
filterInvokedCheck.invoked();
|
||||||
|
context.getBranchUpdates().forEach(branchUpdate -> {
|
||||||
|
assertThat(branchUpdate.getBranchName()).isEqualTo("test-branch");
|
||||||
|
assertThat(branchUpdate.isForcedUpdate()).isTrue();
|
||||||
|
});
|
||||||
|
return MirrorFilter.super.getFilter(context);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
verify(filterInvokedCheck).invoked();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotMarkFastForwardBranchUpdateAsForced() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchCreate().setStartPoint("d81ad6c63d7e2162308d69637b339dedd1d9201c").setName("master").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
InvocationCheck filterInvokedCheck = mock(InvocationCheck.class);
|
||||||
|
|
||||||
|
callUpdate(r -> r.setFilter(new MirrorFilter() {
|
||||||
|
@Override
|
||||||
|
public Filter getFilter(FilterContext context) {
|
||||||
|
filterInvokedCheck.invoked();
|
||||||
|
context.getBranchUpdates().forEach(branchUpdate -> {
|
||||||
|
assertThat(branchUpdate.getBranchName()).isEqualTo("master");
|
||||||
|
assertThat(branchUpdate.isForcedUpdate()).isFalse();
|
||||||
|
});
|
||||||
|
return MirrorFilter.super.getFilter(context);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
verify(filterInvokedCheck).invoked();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUseCredentials() throws Exception {
|
||||||
|
SimpleHttpServer simpleHttpServer = new SimpleHttpServer(Git.open(repositoryDirectory).getRepository());
|
||||||
|
simpleHttpServer.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
MirrorCommandResult result =
|
||||||
|
callMirrorCommand(
|
||||||
|
simpleHttpServer.getUri().toASCIIString(),
|
||||||
|
createCredential(AppServer.username, AppServer.password));
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).contains("Branches:")
|
||||||
|
.contains("- 000000000..fcd0ef183 master (new)")
|
||||||
|
.contains("- 000000000..3f76a12f0 test-branch (new)")
|
||||||
|
.contains("Tags:")
|
||||||
|
.contains("- 000000000..86a6645ec test-tag (new)");
|
||||||
|
} finally {
|
||||||
|
simpleHttpServer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailWithIncorrectCredentials() throws Exception {
|
||||||
|
SimpleHttpServer simpleHttpServer = new SimpleHttpServer(Git.open(repositoryDirectory).getRepository());
|
||||||
|
simpleHttpServer.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
MirrorCommandResult result =
|
||||||
|
callMirrorCommand(
|
||||||
|
simpleHttpServer.getUri().toASCIIString(),
|
||||||
|
createCredential("wrong", "credentials"));
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(FAILED);
|
||||||
|
|
||||||
|
verify(postReceiveRepositoryHookEventFactory, never()).fireForFetch(any(), any());
|
||||||
|
} finally {
|
||||||
|
simpleHttpServer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateUpdateObjectForCreatedTags() throws IOException, GitAPIException {
|
||||||
|
try (Git updatedSource = Git.open(repositoryDirectory)) {
|
||||||
|
RevObject revObject = getRevObject(updatedSource, "9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
updatedSource.tag().setAnnotated(true).setName("42").setMessage("annotated tag").setObjectId(revObject).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MirrorFilter.TagUpdate> collectedTagUpdates = callMirrorAndCollectUpdates().tagUpdates;
|
||||||
|
|
||||||
|
assertThat(collectedTagUpdates)
|
||||||
|
.anySatisfy(update -> {
|
||||||
|
assertThat(update.getUpdateType()).get().isEqualTo(MirrorFilter.UpdateType.CREATE);
|
||||||
|
assertThat(update.getTagName()).isEqualTo("42");
|
||||||
|
assertThat(update.getNewRevision()).get().isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
assertThat(update.getOldRevision()).isEmpty();
|
||||||
|
assertThat(update.getTag()).get().extracting("name").isEqualTo("42");
|
||||||
|
assertThat(update.getTag()).get().extracting("revision").isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
})
|
||||||
|
.anySatisfy(tagUpdate -> {
|
||||||
|
assertThat(tagUpdate.getUpdateType()).get().isEqualTo(MirrorFilter.UpdateType.CREATE);
|
||||||
|
assertThat(tagUpdate.getTagName()).isEqualTo("test-tag");
|
||||||
|
assertThat(tagUpdate.getNewRevision()).get().isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
||||||
|
assertThat(tagUpdate.getOldRevision()).isEmpty();
|
||||||
|
assertThat(tagUpdate.getTag()).get().extracting("name").isEqualTo("test-tag");
|
||||||
|
assertThat(tagUpdate.getTag()).get().extracting("revision").isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateUpdateObjectForDeletedTags() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git updatedSource = Git.open(repositoryDirectory)) {
|
||||||
|
updatedSource.tagDelete().setTags("test-tag").call();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MirrorFilter.TagUpdate> collectedTagUPdates = callMirrorAndCollectUpdates().tagUpdates;
|
||||||
|
|
||||||
|
assertThat(collectedTagUPdates)
|
||||||
|
.anySatisfy(update -> {
|
||||||
|
assertThat(update.getUpdateType()).get().isEqualTo(MirrorFilter.UpdateType.DELETE);
|
||||||
|
assertThat(update.getTagName()).isEqualTo("test-tag");
|
||||||
|
assertThat(update.getNewRevision()).isEmpty();
|
||||||
|
assertThat(update.getOldRevision()).get().isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
||||||
|
assertThat(update.getTag()).isEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateUpdateObjectForUpdatedTags() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git updatedSource = Git.open(repositoryDirectory)) {
|
||||||
|
RevObject revObject = getRevObject(updatedSource, "9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
updatedSource.tag().setName("test-tag").setObjectId(revObject).setForceUpdate(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MirrorFilter.TagUpdate> collectedTagUpdates = callMirrorAndCollectUpdates().tagUpdates;
|
||||||
|
|
||||||
|
assertThat(collectedTagUpdates)
|
||||||
|
.anySatisfy(update -> {
|
||||||
|
assertThat(update.getUpdateType()).get().isEqualTo(MirrorFilter.UpdateType.UPDATE);
|
||||||
|
assertThat(update.getTagName()).isEqualTo("test-tag");
|
||||||
|
assertThat(update.getNewRevision()).get().isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
assertThat(update.getOldRevision()).get().isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
||||||
|
assertThat(update.getTag()).get().extracting("name").isEqualTo("test-tag");
|
||||||
|
assertThat(update.getTag()).get().extracting("revision").isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateUpdateObjectForNewBranch() {
|
||||||
|
List<MirrorFilter.BranchUpdate> collectedBranchUpdates = callMirrorAndCollectUpdates().branchUpdates;
|
||||||
|
|
||||||
|
assertThat(collectedBranchUpdates)
|
||||||
|
.anySatisfy(update -> {
|
||||||
|
assertThat(update.getUpdateType()).get().isEqualTo(MirrorFilter.UpdateType.CREATE);
|
||||||
|
assertThat(update.getBranchName()).isEqualTo("test-branch");
|
||||||
|
assertThat(update.getNewRevision()).get().isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||||
|
assertThat(update.getOldRevision()).isEmpty();
|
||||||
|
assertThat(update.getChangeset()).get().extracting("id").isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateUpdateObjectForForcedUpdatedBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchCreate().setStartPoint("9e93d8631675a89615fac56b09209686146ff3c0").setName("test-branch").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MirrorFilter.BranchUpdate> collectedBranchUpdates = callMirrorAndCollectUpdates().branchUpdates;
|
||||||
|
|
||||||
|
assertThat(collectedBranchUpdates)
|
||||||
|
.anySatisfy(update -> {
|
||||||
|
assertThat(update.getUpdateType()).get().isEqualTo(MirrorFilter.UpdateType.UPDATE);
|
||||||
|
assertThat(update.isForcedUpdate()).isTrue();
|
||||||
|
assertThat(update.getBranchName()).isEqualTo("test-branch");
|
||||||
|
assertThat(update.getNewRevision()).get().isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
assertThat(update.getOldRevision()).get().isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||||
|
assertThat(update.getChangeset()).get().extracting("id").isEqualTo("9e93d8631675a89615fac56b09209686146ff3c0");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateUpdateObjectForFastForwardUpdatedBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git existingClone = Git.open(repositoryDirectory)) {
|
||||||
|
existingClone.branchCreate().setStartPoint("a8495c0335a13e6e432df90b3727fa91943189a7").setName("master").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MirrorFilter.BranchUpdate> collectedBranchUpdates = callMirrorAndCollectUpdates().branchUpdates;
|
||||||
|
|
||||||
|
assertThat(collectedBranchUpdates)
|
||||||
|
.anySatisfy(update -> {
|
||||||
|
assertThat(update.getUpdateType()).get().isEqualTo(MirrorFilter.UpdateType.UPDATE);
|
||||||
|
assertThat(update.isForcedUpdate()).isFalse();
|
||||||
|
assertThat(update.getBranchName()).isEqualTo("master");
|
||||||
|
assertThat(update.getNewRevision()).get().isEqualTo("a8495c0335a13e6e432df90b3727fa91943189a7");
|
||||||
|
assertThat(update.getOldRevision()).get().isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
|
||||||
|
assertThat(update.getChangeset()).get().extracting("id").isEqualTo("a8495c0335a13e6e432df90b3727fa91943189a7");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateUpdateObjectForDeletedBranch() throws IOException, GitAPIException {
|
||||||
|
callMirrorCommand();
|
||||||
|
|
||||||
|
try (Git updatedSource = Git.open(repositoryDirectory)) {
|
||||||
|
updatedSource.branchDelete().setBranchNames("test-branch").setForce(true).call();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MirrorFilter.BranchUpdate> collectedBranchUpdates = callMirrorAndCollectUpdates().branchUpdates;
|
||||||
|
|
||||||
|
assertThat(collectedBranchUpdates)
|
||||||
|
.anySatisfy(update -> {
|
||||||
|
assertThat(update.getBranchName()).isEqualTo("test-branch");
|
||||||
|
assertThat(update.getNewRevision()).isEmpty();
|
||||||
|
assertThat(update.getOldRevision()).get().isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||||
|
assertThat(update.getChangeset()).isEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Updates callMirrorAndCollectUpdates() {
|
||||||
|
Updates updates = new Updates();
|
||||||
|
|
||||||
|
MirrorCommandRequest request = new MirrorCommandRequest();
|
||||||
|
request.setSourceUrl(repositoryDirectory.getAbsolutePath());
|
||||||
|
request.setFilter(new MirrorFilter() {
|
||||||
|
@Override
|
||||||
|
public Filter getFilter(FilterContext context) {
|
||||||
|
return new Filter() {
|
||||||
|
@Override
|
||||||
|
public Result acceptTag(TagUpdate tagUpdate) {
|
||||||
|
updates.tagUpdates.add(tagUpdate);
|
||||||
|
return Result.accept();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Result acceptBranch(BranchUpdate branchUpdate) {
|
||||||
|
updates.branchUpdates.add(branchUpdate);
|
||||||
|
return Result.accept();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
command.mirror(request);
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Updates {
|
||||||
|
private final List<MirrorFilter.BranchUpdate> branchUpdates = new ArrayList<>();
|
||||||
|
private final List<MirrorFilter.TagUpdate> tagUpdates = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RevObject getRevObject(Git existingClone, String revision) throws IOException {
|
||||||
|
RevWalk walk = new RevWalk(existingClone.getRepository());
|
||||||
|
ObjectId id = existingClone.getRepository().resolve(revision);
|
||||||
|
return walk.parseAny(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandResult callUpdate(Consumer<MirrorCommandRequest> requestModifier) {
|
||||||
|
MirrorCommandRequest request = new MirrorCommandRequest();
|
||||||
|
request.setSourceUrl(repositoryDirectory.getAbsolutePath());
|
||||||
|
requestModifier.accept(request);
|
||||||
|
return command.update(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Ref> findBranch(Git git, String branchName) throws GitAPIException {
|
||||||
|
return git.branchList().call().stream().filter(ref -> ref.getName().equals("refs/heads/" + branchName)).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Ref> findTag(Git git, String tagName) throws GitAPIException {
|
||||||
|
return git.tagList().call().stream().filter(ref -> ref.getName().equals("refs/tags/" + tagName)).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandResult callMirrorCommand() {
|
||||||
|
return callMirrorCommand(repositoryDirectory.getAbsolutePath(), c -> {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandResult callMirrorCommand(String source, Consumer<MirrorCommandRequest> requestConsumer) {
|
||||||
|
MirrorCommandRequest request = new MirrorCommandRequest();
|
||||||
|
request.setSourceUrl(source);
|
||||||
|
requestConsumer.accept(request);
|
||||||
|
return command.mirror(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<MirrorCommandRequest> createCredential(String wrong, String credentials) {
|
||||||
|
return request -> request.setCredentials(singletonList(new SimpleUsernamePasswordCredential(wrong, credentials.toCharArray())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private GitContext createMirrorContext(File clone) {
|
||||||
|
return new GitContext(clone, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()), new GitConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DenyAllMirrorFilter implements MirrorFilter {
|
||||||
|
@Override
|
||||||
|
public Filter getFilter(FilterContext context) {
|
||||||
|
return new Filter() {
|
||||||
|
@Override
|
||||||
|
public Result acceptBranch(BranchUpdate branch) {
|
||||||
|
return Result.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result acceptTag(TagUpdate tag) {
|
||||||
|
return Result.reject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DenyAllWithReasonMirrorFilter implements MirrorFilter {
|
||||||
|
|
||||||
|
private final String reason;
|
||||||
|
|
||||||
|
private DenyAllWithReasonMirrorFilter(String reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Filter getFilter(FilterContext context) {
|
||||||
|
return new Filter() {
|
||||||
|
@Override
|
||||||
|
public Result acceptBranch(BranchUpdate branch) {
|
||||||
|
return Result.reject(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result acceptTag(TagUpdate tag) {
|
||||||
|
return Result.reject(reason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ErroneousMirrorFilterThrowingExceptions implements MirrorFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Filter getFilter(FilterContext context) {
|
||||||
|
return new Filter() {
|
||||||
|
@Override
|
||||||
|
public Result acceptBranch(BranchUpdate branch) {
|
||||||
|
throw new RuntimeException("this branch creates an exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result acceptTag(TagUpdate tag) {
|
||||||
|
throw new RuntimeException("this tag creates an exception");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface InvocationCheck {
|
||||||
|
void invoked();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,14 +28,13 @@ import org.eclipse.jgit.revwalk.RevCommit;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import sonia.scm.repository.GitConfig;
|
import sonia.scm.repository.GitConfig;
|
||||||
import sonia.scm.repository.GitTestHelper;
|
|
||||||
import sonia.scm.repository.Modifications;
|
import sonia.scm.repository.Modifications;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.assertj.core.api.Java6Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
|
public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
|
||||||
|
|
||||||
@@ -112,12 +111,12 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
|
|||||||
PushCommandRequest request = new PushCommandRequest();
|
PushCommandRequest request = new PushCommandRequest();
|
||||||
request.setRemoteRepository(incomingRepository);
|
request.setRemoteRepository(incomingRepository);
|
||||||
cmd.push(request);
|
cmd.push(request);
|
||||||
|
GitContext context = new GitContext(incomingDirectory, incomingRepository, null, new GitConfig());
|
||||||
|
PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory = new PostReceiveRepositoryHookEventFactory(eventBus, eventFactory, context);
|
||||||
GitPullCommand pullCommand = new GitPullCommand(
|
GitPullCommand pullCommand = new GitPullCommand(
|
||||||
handler,
|
handler,
|
||||||
new GitContext(incomingDirectory, incomingRepository, null, new GitConfig()),
|
context,
|
||||||
eventBus,
|
postReceiveRepositoryHookEventFactory);
|
||||||
eventFactory
|
|
||||||
);
|
|
||||||
PullCommandRequest pullRequest = new PullCommandRequest();
|
PullCommandRequest pullRequest = new PullCommandRequest();
|
||||||
pullRequest.setRemoteRepository(incomingRepository);
|
pullRequest.setRemoteRepository(incomingRepository);
|
||||||
pullCommand.pull(pullRequest);
|
pullCommand.pull(pullRequest);
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Tag> readTags(GitContext context) throws IOException {
|
private List<Tag> readTags(GitContext context) throws IOException {
|
||||||
return new GitTagsCommand(context, gpg).getTags();
|
return new GitTagsCommand(context, new GitTagConverter(gpg)).getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Tag> findTag(GitContext context, String name) throws IOException {
|
private Optional<Tag> findTag(GitContext context, String name) throws IOException {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class GitTagsCommandTest extends AbstractGitCommandTestBase {
|
|||||||
@Test
|
@Test
|
||||||
public void shouldGetDatesCorrectly() throws IOException {
|
public void shouldGetDatesCorrectly() throws IOException {
|
||||||
final GitContext gitContext = createContext();
|
final GitContext gitContext = createContext();
|
||||||
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, gpg);
|
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, new GitTagConverter(gpg));
|
||||||
final List<Tag> tags = tagsCommand.getTags();
|
final List<Tag> tags = tagsCommand.getTags();
|
||||||
assertThat(tags).hasSize(3);
|
assertThat(tags).hasSize(3);
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ public class GitTagsCommandTest extends AbstractGitCommandTestBase {
|
|||||||
when(publicKey.verify(signedContent.getBytes(), signature.getBytes())).thenReturn(true);
|
when(publicKey.verify(signedContent.getBytes(), signature.getBytes())).thenReturn(true);
|
||||||
|
|
||||||
final GitContext gitContext = createContext();
|
final GitContext gitContext = createContext();
|
||||||
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, gpg);
|
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, new GitTagConverter(gpg));
|
||||||
final List<Tag> tags = tagsCommand.getTags();
|
final List<Tag> tags = tagsCommand.getTags();
|
||||||
|
|
||||||
assertThat(tags).hasSize(3);
|
assertThat(tags).hasSize(3);
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -50,40 +48,28 @@ import java.io.IOException;
|
|||||||
|
|
||||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
@Extension
|
@Extension
|
||||||
public class SvnRepositoryHandler
|
public class SvnRepositoryHandler extends AbstractSimpleRepositoryHandler<SvnConfig> {
|
||||||
extends AbstractSimpleRepositoryHandler<SvnConfig>
|
|
||||||
{
|
|
||||||
|
|
||||||
public static final String PROPERTY_UUID = "svn.uuid";
|
public static final String PROPERTY_UUID = "svn.uuid";
|
||||||
|
|
||||||
public static final String RESOURCE_VERSION = "sonia/scm/version/scm-svn-plugin";
|
public static final String RESOURCE_VERSION = "sonia/scm/version/scm-svn-plugin";
|
||||||
|
|
||||||
public static final String TYPE_DISPLAYNAME = "Subversion";
|
public static final String TYPE_DISPLAYNAME = "Subversion";
|
||||||
|
|
||||||
public static final String TYPE_NAME = "svn";
|
public static final String TYPE_NAME = "svn";
|
||||||
|
|
||||||
public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME,
|
public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, SvnRepositoryServiceProvider.COMMANDS);
|
||||||
TYPE_DISPLAYNAME,
|
|
||||||
SvnRepositoryServiceProvider.COMMANDS);
|
|
||||||
|
|
||||||
private static final Logger logger =
|
private static final Logger LOG = LoggerFactory.getLogger(SvnRepositoryHandler.class);
|
||||||
LoggerFactory.getLogger(SvnRepositoryHandler.class);
|
|
||||||
private SvnRepositoryHook hook;
|
private SvnRepositoryHook hook;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory,
|
public SvnRepositoryHandler(ConfigurationStoreFactory storeFactory,
|
||||||
HookEventFacade eventFacade,
|
HookEventFacade eventFacade,
|
||||||
RepositoryLocationResolver repositoryLocationResolver,
|
RepositoryLocationResolver repositoryLocationResolver,
|
||||||
PluginLoader pluginLoader)
|
PluginLoader pluginLoader) {
|
||||||
{
|
|
||||||
super(storeFactory, repositoryLocationResolver, pluginLoader);
|
super(storeFactory, repositoryLocationResolver, pluginLoader);
|
||||||
|
|
||||||
// register logger
|
// register logger
|
||||||
@@ -93,116 +79,95 @@ public class SvnRepositoryHandler
|
|||||||
FSRepositoryFactory.setup();
|
FSRepositoryFactory.setup();
|
||||||
|
|
||||||
// register hook
|
// register hook
|
||||||
if (eventFacade != null)
|
if (eventFacade != null) {
|
||||||
{
|
|
||||||
hook = new SvnRepositoryHook(eventFacade, this);
|
hook = new SvnRepositoryHook(eventFacade, this);
|
||||||
FSHooks.registerHook(hook);
|
FSHooks.registerHook(hook);
|
||||||
}
|
} else if (LOG.isWarnEnabled()) {
|
||||||
else if (logger.isWarnEnabled())
|
LOG.warn(
|
||||||
{
|
|
||||||
logger.warn(
|
|
||||||
"unable to register hook, beacause of missing repositorymanager");
|
"unable to register hook, beacause of missing repositorymanager");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ImportHandler getImportHandler()
|
public ImportHandler getImportHandler() {
|
||||||
{
|
|
||||||
return new SvnImportHandler(this);
|
return new SvnImportHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RepositoryType getType()
|
public RepositoryType getType() {
|
||||||
{
|
|
||||||
return TYPE;
|
return TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getVersionInformation()
|
public String getVersionInformation() {
|
||||||
{
|
|
||||||
return getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION);
|
return getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void create(Repository repository, File directory) throws InternalRepositoryException {
|
protected void create(Repository repository, File directory) throws InternalRepositoryException {
|
||||||
Compatibility comp = config.getCompatibility();
|
|
||||||
|
|
||||||
if (logger.isDebugEnabled())
|
|
||||||
{
|
|
||||||
StringBuilder log = new StringBuilder("create svn repository \"");
|
|
||||||
|
|
||||||
log.append(directory.getName()).append("\": pre14Compatible=");
|
|
||||||
log.append(comp.isPre14Compatible()).append(", pre15Compatible=");
|
|
||||||
log.append(comp.isPre15Compatible()).append(", pre16Compatible=");
|
|
||||||
log.append(comp.isPre16Compatible()).append(", pre17Compatible=");
|
|
||||||
log.append(comp.isPre17Compatible()).append(", with17Compatible=");
|
|
||||||
log.append(comp.isWith17Compatible());
|
|
||||||
logger.debug(log.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
SVNRepository svnRepository = null;
|
SVNRepository svnRepository = null;
|
||||||
|
|
||||||
try
|
try {
|
||||||
{
|
SVNURL url = createSvnUrl(directory);
|
||||||
SVNURL url = SVNRepositoryFactory.createLocalRepository(directory, null,
|
|
||||||
true, false, comp.isPre14Compatible(),
|
|
||||||
comp.isPre15Compatible(), comp.isPre16Compatible(),
|
|
||||||
comp.isPre17Compatible(), comp.isWith17Compatible());
|
|
||||||
|
|
||||||
svnRepository = SVNRepositoryFactory.create(url);
|
svnRepository = SVNRepositoryFactory.create(url);
|
||||||
|
|
||||||
String uuid = svnRepository.getRepositoryUUID(true);
|
String uuid = svnRepository.getRepositoryUUID(true);
|
||||||
|
|
||||||
if (Util.isNotEmpty(uuid))
|
if (Util.isNotEmpty(uuid)) {
|
||||||
{
|
if (LOG.isDebugEnabled()) {
|
||||||
if (logger.isDebugEnabled())
|
LOG.debug("store repository uuid {} for {}", uuid,
|
||||||
{
|
|
||||||
logger.debug("store repository uuid {} for {}", uuid,
|
|
||||||
repository.getName());
|
repository.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
repository.setProperty(PROPERTY_UUID, uuid);
|
repository.setProperty(PROPERTY_UUID, uuid);
|
||||||
}
|
} else if (LOG.isWarnEnabled()) {
|
||||||
else if (logger.isWarnEnabled())
|
LOG.warn("could not read repository uuid for {}",
|
||||||
{
|
|
||||||
logger.warn("could not read repository uuid for {}",
|
|
||||||
repository.getName());
|
repository.getName());
|
||||||
}
|
}
|
||||||
}
|
} catch (SVNException ex) {
|
||||||
catch (SVNException ex)
|
throw new InternalRepositoryException(repository, "could not create repository", ex);
|
||||||
{
|
} finally {
|
||||||
logger.error("could not create svn repository", ex);
|
|
||||||
throw new InternalRepositoryException(entity(repository), "could not create repository", ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
SvnUtil.closeSession(svnRepository);
|
SvnUtil.closeSession(svnRepository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public SVNURL createSvnUrl(File directory) {
|
||||||
* Method description
|
Compatibility comp = config.getCompatibility();
|
||||||
*
|
|
||||||
*
|
if (LOG.isDebugEnabled()) {
|
||||||
* @return
|
|
||||||
*/
|
LOG.debug("create svn repository \"{}\": " +
|
||||||
|
"pre14Compatible={}, " +
|
||||||
|
"pre15Compatible={}, " +
|
||||||
|
"pre16Compatible={}, " +
|
||||||
|
"pre17Compatible={}, " +
|
||||||
|
"with17Compatible={}",
|
||||||
|
directory.getName(),
|
||||||
|
comp.isPre14Compatible(),
|
||||||
|
comp.isPre15Compatible(),
|
||||||
|
comp.isPre16Compatible(),
|
||||||
|
comp.isPre17Compatible(),
|
||||||
|
comp.isWith17Compatible());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return SVNRepositoryFactory.createLocalRepository(directory, null,
|
||||||
|
true, false, comp.isPre14Compatible(),
|
||||||
|
comp.isPre15Compatible(), comp.isPre16Compatible(),
|
||||||
|
comp.isPre17Compatible(), comp.isWith17Compatible());
|
||||||
|
} catch (SVNException ex) {
|
||||||
|
throw new InternalRepositoryException(entity(File.class, directory.toString()), "could not create svn url", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SvnConfig createInitialConfig()
|
protected SvnConfig createInitialConfig() {
|
||||||
{
|
|
||||||
return new SvnConfig();
|
return new SvnConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected Class<SvnConfig> getConfigClass()
|
protected Class<SvnConfig> getConfigClass() {
|
||||||
{
|
|
||||||
return SvnConfig.class;
|
return SvnConfig.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.google.common.base.Stopwatch;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.tmatesoft.svn.core.SVNException;
|
||||||
|
import org.tmatesoft.svn.core.SVNURL;
|
||||||
|
import org.tmatesoft.svn.core.auth.BasicAuthenticationManager;
|
||||||
|
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
|
||||||
|
import org.tmatesoft.svn.core.auth.SVNAuthentication;
|
||||||
|
import org.tmatesoft.svn.core.auth.SVNPasswordAuthentication;
|
||||||
|
import org.tmatesoft.svn.core.auth.SVNSSLAuthentication;
|
||||||
|
import org.tmatesoft.svn.core.wc.SVNWCUtil;
|
||||||
|
import org.tmatesoft.svn.core.wc.admin.SVNAdminClient;
|
||||||
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
|
import sonia.scm.repository.api.MirrorCommandResult;
|
||||||
|
import sonia.scm.repository.api.Pkcs12ClientCertificateCredential;
|
||||||
|
import sonia.scm.repository.api.UsernamePasswordCredential;
|
||||||
|
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.FAILED;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.OK;
|
||||||
|
|
||||||
|
public class SvnMirrorCommand extends AbstractSvnCommand implements MirrorCommand {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SvnMirrorCommand.class);
|
||||||
|
|
||||||
|
private final TrustManager trustManager;
|
||||||
|
|
||||||
|
SvnMirrorCommand(SvnContext context, TrustManager trustManager) {
|
||||||
|
super(context);
|
||||||
|
this.trustManager = trustManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MirrorCommandResult mirror(MirrorCommandRequest mirrorCommandRequest) {
|
||||||
|
SVNURL url = createUrlForLocalRepository();
|
||||||
|
return withAdminClient(mirrorCommandRequest, admin -> {
|
||||||
|
SVNURL source = SVNURL.parseURIEncoded(mirrorCommandRequest.getSourceUrl());
|
||||||
|
admin.doCompleteSynchronize(source, url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MirrorCommandResult update(MirrorCommandRequest mirrorCommandRequest) {
|
||||||
|
SVNURL url = createUrlForLocalRepository();
|
||||||
|
return withAdminClient(mirrorCommandRequest, admin -> admin.doSynchronize(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandResult withAdminClient(MirrorCommandRequest mirrorCommandRequest, AdminConsumer consumer) {
|
||||||
|
Stopwatch stopwatch = Stopwatch.createStarted();
|
||||||
|
long beforeUpdate;
|
||||||
|
long afterUpdate;
|
||||||
|
try {
|
||||||
|
beforeUpdate = context.open().getLatestRevision();
|
||||||
|
SVNURL url = createUrlForLocalRepository();
|
||||||
|
SVNAdminClient admin = createAdminClient(url, mirrorCommandRequest);
|
||||||
|
|
||||||
|
consumer.accept(admin);
|
||||||
|
afterUpdate = context.open().getLatestRevision();
|
||||||
|
} catch (SVNException e) {
|
||||||
|
LOG.info("Could not mirror svn repository", e);
|
||||||
|
return new MirrorCommandResult(
|
||||||
|
FAILED,
|
||||||
|
asList(
|
||||||
|
"failed to synchronize. See following error message for more details:",
|
||||||
|
e.getMessage()
|
||||||
|
),
|
||||||
|
stopwatch.stop().elapsed());
|
||||||
|
}
|
||||||
|
return new MirrorCommandResult(
|
||||||
|
OK,
|
||||||
|
ImmutableList.of("Updated from revision " + beforeUpdate + " to revision " + afterUpdate),
|
||||||
|
stopwatch.stop().elapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SVNURL createUrlForLocalRepository() {
|
||||||
|
try {
|
||||||
|
return SVNURL.fromFile(context.getDirectory());
|
||||||
|
} catch (SVNException e) {
|
||||||
|
throw new InternalRepositoryException(repository, "could not create svn url for local repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SVNAdminClient createAdminClient(SVNURL url, MirrorCommandRequest mirrorCommandRequest) {
|
||||||
|
Collection<SVNAuthentication> authentications = new ArrayList<>();
|
||||||
|
mirrorCommandRequest.getCredential(Pkcs12ClientCertificateCredential.class)
|
||||||
|
.map(c -> createTlsAuth(url, c))
|
||||||
|
.ifPresent(authentications::add);
|
||||||
|
mirrorCommandRequest.getCredential(UsernamePasswordCredential.class)
|
||||||
|
.map(c -> SVNPasswordAuthentication.newInstance(c.username(), c.password(), false, url, false))
|
||||||
|
.ifPresent(authentications::add);
|
||||||
|
ISVNAuthenticationManager authManager = new BasicAuthenticationManager(
|
||||||
|
authentications.toArray(new SVNAuthentication[authentications.size()])) {
|
||||||
|
@Override
|
||||||
|
public TrustManager getTrustManager(SVNURL url) {
|
||||||
|
return trustManager;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SVNAdminClient(authManager, SVNWCUtil.createDefaultOptions(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SVNSSLAuthentication createTlsAuth(SVNURL url, Pkcs12ClientCertificateCredential c) {
|
||||||
|
return SVNSSLAuthentication.newInstance(
|
||||||
|
c.getCertificate(),
|
||||||
|
c.getPassword(),
|
||||||
|
false,
|
||||||
|
url,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface AdminConsumer {
|
||||||
|
void accept(SVNAdminClient adminClient) throws SVNException;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import sonia.scm.repository.SvnWorkingCopyFactory;
|
|||||||
import sonia.scm.repository.api.Command;
|
import sonia.scm.repository.api.Command;
|
||||||
import sonia.scm.repository.api.HookContextFactory;
|
import sonia.scm.repository.api.HookContextFactory;
|
||||||
|
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -51,7 +52,8 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
|
|||||||
Command.UNBUNDLE,
|
Command.UNBUNDLE,
|
||||||
Command.MODIFY,
|
Command.MODIFY,
|
||||||
Command.LOOKUP,
|
Command.LOOKUP,
|
||||||
Command.FULL_HEALTH_CHECK
|
Command.FULL_HEALTH_CHECK,
|
||||||
|
Command.MIRROR
|
||||||
);
|
);
|
||||||
//J+
|
//J+
|
||||||
|
|
||||||
@@ -59,14 +61,17 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
|
|||||||
private final SvnContext context;
|
private final SvnContext context;
|
||||||
private final SvnWorkingCopyFactory workingCopyFactory;
|
private final SvnWorkingCopyFactory workingCopyFactory;
|
||||||
private final HookContextFactory hookContextFactory;
|
private final HookContextFactory hookContextFactory;
|
||||||
|
private final TrustManager trustManager;
|
||||||
|
|
||||||
SvnRepositoryServiceProvider(SvnRepositoryHandler handler,
|
SvnRepositoryServiceProvider(SvnRepositoryHandler handler,
|
||||||
Repository repository,
|
Repository repository,
|
||||||
SvnWorkingCopyFactory workingCopyFactory,
|
SvnWorkingCopyFactory workingCopyFactory,
|
||||||
HookContextFactory hookContextFactory) {
|
HookContextFactory hookContextFactory,
|
||||||
|
TrustManager trustManager) {
|
||||||
this.context = new SvnContext(repository, handler.getDirectory(repository.getId()));
|
this.context = new SvnContext(repository, handler.getDirectory(repository.getId()));
|
||||||
this.workingCopyFactory = workingCopyFactory;
|
this.workingCopyFactory = workingCopyFactory;
|
||||||
this.hookContextFactory = hookContextFactory;
|
this.hookContextFactory = hookContextFactory;
|
||||||
|
this.trustManager = trustManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -133,4 +138,9 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
|
|||||||
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
||||||
return new SvnFullHealthCheckCommand(context);
|
return new SvnFullHealthCheckCommand(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MirrorCommand getMirrorCommand() {
|
||||||
|
return new SvnMirrorCommand(context, trustManager);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,20 +31,25 @@ import sonia.scm.repository.SvnRepositoryHandler;
|
|||||||
import sonia.scm.repository.SvnWorkingCopyFactory;
|
import sonia.scm.repository.SvnWorkingCopyFactory;
|
||||||
import sonia.scm.repository.api.HookContextFactory;
|
import sonia.scm.repository.api.HookContextFactory;
|
||||||
|
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
|
||||||
@Extension
|
@Extension
|
||||||
public class SvnRepositoryServiceResolver implements RepositoryServiceResolver {
|
public class SvnRepositoryServiceResolver implements RepositoryServiceResolver {
|
||||||
|
|
||||||
private final SvnRepositoryHandler handler;
|
private final SvnRepositoryHandler handler;
|
||||||
private final SvnWorkingCopyFactory workingCopyFactory;
|
private final SvnWorkingCopyFactory workingCopyFactory;
|
||||||
private final HookContextFactory hookContextFactory;
|
private final HookContextFactory hookContextFactory;
|
||||||
|
private final TrustManager trustManager;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public SvnRepositoryServiceResolver(SvnRepositoryHandler handler,
|
public SvnRepositoryServiceResolver(SvnRepositoryHandler handler,
|
||||||
SvnWorkingCopyFactory workingCopyFactory,
|
SvnWorkingCopyFactory workingCopyFactory,
|
||||||
HookContextFactory hookContextFactory) {
|
HookContextFactory hookContextFactory,
|
||||||
|
TrustManager trustManager) {
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
this.workingCopyFactory = workingCopyFactory;
|
this.workingCopyFactory = workingCopyFactory;
|
||||||
this.hookContextFactory = hookContextFactory;
|
this.hookContextFactory = hookContextFactory;
|
||||||
|
this.trustManager = trustManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -52,7 +57,7 @@ public class SvnRepositoryServiceResolver implements RepositoryServiceResolver {
|
|||||||
SvnRepositoryServiceProvider provider = null;
|
SvnRepositoryServiceProvider provider = null;
|
||||||
|
|
||||||
if (SvnRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
|
if (SvnRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
|
||||||
provider = new SvnRepositoryServiceProvider(handler, repository, workingCopyFactory, hookContextFactory);
|
provider = new SvnRepositoryServiceProvider(handler, repository, workingCopyFactory, hookContextFactory, trustManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
|
|||||||
@@ -24,78 +24,36 @@
|
|||||||
|
|
||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
public class AbstractSvnCommandTestBase extends ZippedRepositoryTestBase {
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public class AbstractSvnCommandTestBase extends ZippedRepositoryTestBase
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
@After
|
@After
|
||||||
public void close() throws IOException
|
public void close() throws IOException {
|
||||||
{
|
if (context != null) {
|
||||||
if (context != null)
|
|
||||||
{
|
|
||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public SvnContext createContext() {
|
||||||
* Method description
|
if (context == null) {
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public SvnContext createContext()
|
|
||||||
{
|
|
||||||
if (context == null)
|
|
||||||
{
|
|
||||||
context = new SvnContext(repository, repositoryDirectory);
|
context = new SvnContext(repository, repositoryDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected String getType()
|
protected String getType() {
|
||||||
{
|
|
||||||
return "svn";
|
return "svn";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected String getZippedRepositoryResource()
|
protected String getZippedRepositoryResource() {
|
||||||
{
|
|
||||||
return "sonia/scm/repository/spi/scm-svn-spi-test.zip";
|
return "sonia/scm/repository/spi/scm-svn-spi-test.zip";
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private SvnContext context;
|
private SvnContext context;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import org.tmatesoft.svn.core.SVNException;
|
||||||
|
import org.tmatesoft.svn.core.SVNURL;
|
||||||
|
import org.tmatesoft.svn.core.auth.BasicAuthenticationManager;
|
||||||
|
import org.tmatesoft.svn.core.auth.SVNAuthentication;
|
||||||
|
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
|
||||||
|
import org.tmatesoft.svn.core.wc.SVNWCUtil;
|
||||||
|
import org.tmatesoft.svn.core.wc.admin.SVNAdminClient;
|
||||||
|
import sonia.scm.repository.RepositoryTestData;
|
||||||
|
import sonia.scm.repository.api.MirrorCommandResult;
|
||||||
|
import sonia.scm.repository.api.SimpleUsernamePasswordCredential;
|
||||||
|
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.OK;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class SvnMirrorCommandTest extends AbstractSvnCommandTestBase {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private X509TrustManager trustManager;
|
||||||
|
|
||||||
|
private SvnContext emptyContext;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bendContextToNewRepository() throws IOException, SVNException {
|
||||||
|
emptyContext = createEmptyContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldDoInitialMirror() {
|
||||||
|
MirrorCommandResult result = callMirror(emptyContext, repositoryDirectory, c -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldDoMirrorUpdate() throws SVNException {
|
||||||
|
// Initialize destination repo before update
|
||||||
|
SVNAdminClient svnAdminClient = new SVNAdminClient(new BasicAuthenticationManager(new SVNAuthentication[]{}), SVNWCUtil.createDefaultOptions(false));
|
||||||
|
svnAdminClient.doInitialize(SVNURL.fromFile(repositoryDirectory), emptyContext.createUrl());
|
||||||
|
|
||||||
|
MirrorCommandResult result = callMirrorUpdate(emptyContext, repositoryDirectory);
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
assertThat(result.getLog()).contains("Updated from revision 0 to revision 5");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUseCredentials() {
|
||||||
|
MirrorCommandResult result = callMirror(emptyContext, repositoryDirectory, createCredential("svnadmin", "secret"));
|
||||||
|
|
||||||
|
assertThat(result.getResult()).isEqualTo(OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandResult callMirrorUpdate(SvnContext context, File source) {
|
||||||
|
MirrorCommandRequest request = createRequest(source);
|
||||||
|
return createMirrorCommand(context).update(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandResult callMirror(SvnContext context, File source, Consumer<MirrorCommandRequest> consumer) {
|
||||||
|
MirrorCommandRequest request = createRequest(source);
|
||||||
|
consumer.accept(request);
|
||||||
|
return createMirrorCommand(context).mirror(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MirrorCommandRequest createRequest(File source) {
|
||||||
|
MirrorCommandRequest request = new MirrorCommandRequest();
|
||||||
|
request.setSourceUrl("file://" + source.getAbsolutePath());
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SvnMirrorCommand createMirrorCommand(SvnContext context) {
|
||||||
|
return new SvnMirrorCommand(context, trustManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<MirrorCommandRequest> createCredential(String username, String password) {
|
||||||
|
return request -> request.setCredentials(singletonList(new SimpleUsernamePasswordCredential(username, password.toCharArray())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SvnContext createEmptyContext() throws SVNException, IOException {
|
||||||
|
File dir = tempFolder.newFolder();
|
||||||
|
SVNRepositoryFactory.createLocalRepository(dir, true, true);
|
||||||
|
return new SvnContext(RepositoryTestData.createHappyVerticalPeopleTransporter(), dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
238
scm-test/src/main/java/sonia/scm/web/JsonMockHttpRequest.java
Normal file
238
scm-test/src/main/java/sonia/scm/web/JsonMockHttpRequest.java
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.web;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
|
import org.jboss.resteasy.specimpl.ResteasyUriInfo;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.jboss.resteasy.spi.ResteasyAsynchronousContext;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
public class JsonMockHttpRequest implements HttpRequest {
|
||||||
|
|
||||||
|
private final MockHttpRequest delegate;
|
||||||
|
private boolean contentTypeSet = false;
|
||||||
|
|
||||||
|
private JsonMockHttpRequest(MockHttpRequest delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see MockHttpRequest#post(String)
|
||||||
|
*/
|
||||||
|
public static JsonMockHttpRequest post(String url) throws URISyntaxException {
|
||||||
|
return new JsonMockHttpRequest(MockHttpRequest.post(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see MockHttpRequest#put(String)
|
||||||
|
*/
|
||||||
|
public static JsonMockHttpRequest put(String url) throws URISyntaxException {
|
||||||
|
return new JsonMockHttpRequest(MockHttpRequest.put(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see MockHttpRequest#setHttpMethod(String)
|
||||||
|
*/
|
||||||
|
public void setHttpMethod(String method) {
|
||||||
|
delegate.setHttpMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see MockHttpRequest#getAsynchronousContext()
|
||||||
|
*/
|
||||||
|
public ResteasyAsynchronousContext getAsynchronousContext() {
|
||||||
|
return delegate.getAsynchronousContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see MockHttpRequest#setAsynchronousContext(ResteasyAsynchronousContext)
|
||||||
|
*/
|
||||||
|
public void setAsynchronousContext(ResteasyAsynchronousContext asynchronousContext) {
|
||||||
|
delegate.setAsynchronousContext(asynchronousContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest header(String name, String value) {
|
||||||
|
delegate.header(name, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest accept(List<MediaType> accepts) {
|
||||||
|
delegate.accept(accepts);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest accept(MediaType accept) {
|
||||||
|
delegate.accept(accept);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest accept(String type) {
|
||||||
|
delegate.accept(type);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest language(String language) {
|
||||||
|
delegate.language(language);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest cookie(String name, String value) {
|
||||||
|
delegate.cookie(name, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest contentType(String type) {
|
||||||
|
contentTypeSet = true;
|
||||||
|
delegate.contentType(type);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest contentType(MediaType type) {
|
||||||
|
contentTypeSet = true;
|
||||||
|
delegate.contentType(type);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest content(byte[] bytes) {
|
||||||
|
delegate.content(bytes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest json(String json) {
|
||||||
|
if (!contentTypeSet) {
|
||||||
|
contentType("application/json");
|
||||||
|
}
|
||||||
|
return content(json.replaceAll("'", "\"").getBytes(UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest content(InputStream stream) {
|
||||||
|
delegate.content(stream);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMockHttpRequest addFormHeader(String name, String value) {
|
||||||
|
delegate.addFormHeader(name, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpHeaders getHttpHeaders() {
|
||||||
|
return delegate.getHttpHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MultivaluedMap<String, String> getMutableHeaders() {
|
||||||
|
return delegate.getMutableHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return delegate.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInputStream(InputStream stream) {
|
||||||
|
delegate.setInputStream(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResteasyUriInfo getUri() {
|
||||||
|
return delegate.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHttpMethod() {
|
||||||
|
return delegate.getHttpMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialRequestThreadFinished() {
|
||||||
|
delegate.initialRequestThreadFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getAttribute(String attribute) {
|
||||||
|
return delegate.getAttribute(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttribute(String name, Object value) {
|
||||||
|
delegate.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeAttribute(String name) {
|
||||||
|
delegate.removeAttribute(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Enumeration<String> getAttributeNames() {
|
||||||
|
return delegate.getAttributeNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResteasyAsynchronousContext getAsyncContext() {
|
||||||
|
return delegate.getAsyncContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forward(String path) {
|
||||||
|
delegate.forward(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean wasForwarded() {
|
||||||
|
return delegate.wasForwarded();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemoteHost() {
|
||||||
|
return delegate.getRemoteHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemoteAddress() {
|
||||||
|
return delegate.getRemoteAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean formParametersRead() {
|
||||||
|
return delegate.formParametersRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MultivaluedMap<String, String> getFormParameters() {
|
||||||
|
return delegate.getFormParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MultivaluedMap<String, String> getDecodedFormParameters() {
|
||||||
|
return delegate.getDecodedFormParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInitial() {
|
||||||
|
return delegate.isInitial();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestUri(URI requestUri) throws IllegalStateException {
|
||||||
|
delegate.setRequestUri(requestUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestUri(URI baseUri, URI requestUri) throws IllegalStateException {
|
||||||
|
delegate.setRequestUri(baseUri, requestUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
scm-test/src/main/java/sonia/scm/web/JsonMockHttpResponse.java
Normal file
155
scm-test/src/main/java/sonia/scm/web/JsonMockHttpResponse.java
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.web;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
|
import org.jboss.resteasy.spi.AsyncOutputStream;
|
||||||
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.NewCookie;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class JsonMockHttpResponse implements HttpResponse {
|
||||||
|
|
||||||
|
private final MockHttpResponse delegate = new MockHttpResponse();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStatus() {
|
||||||
|
return delegate.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setStatus(int status) {
|
||||||
|
delegate.setStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MultivaluedMap<String, Object> getOutputHeaders() {
|
||||||
|
return delegate.getOutputHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() throws IOException {
|
||||||
|
return delegate.getOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOutputStream(OutputStream os) {
|
||||||
|
delegate.setOutputStream(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getOutput() {
|
||||||
|
return delegate.getOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentAsString() throws UnsupportedEncodingException {
|
||||||
|
return delegate.getContentAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addNewCookie(NewCookie cookie) {
|
||||||
|
delegate.addNewCookie(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendError(int status) throws IOException {
|
||||||
|
delegate.sendError(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendError(int status, String message) throws IOException {
|
||||||
|
delegate.sendError(status, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NewCookie> getNewCookies() {
|
||||||
|
return delegate.getNewCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return delegate.getErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isErrorSent() {
|
||||||
|
return delegate.isErrorSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCommitted() {
|
||||||
|
return delegate.isCommitted();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
delegate.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flushBuffer() throws IOException {
|
||||||
|
delegate.flushBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncOutputStream getAsyncOutputStream() throws IOException {
|
||||||
|
return delegate.getAsyncOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
delegate.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSuppressExceptionDuringChunkedTransfer(boolean suppressExceptionDuringChunkedTransfer) {
|
||||||
|
delegate.setSuppressExceptionDuringChunkedTransfer(suppressExceptionDuringChunkedTransfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean suppressExceptionDuringChunkedTransfer() {
|
||||||
|
return delegate.suppressExceptionDuringChunkedTransfer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getContentAs(Class<T> clazz) {
|
||||||
|
try {
|
||||||
|
return new ObjectMapper().readValue(getContentAsString(), clazz);
|
||||||
|
} catch (JsonProcessingException | UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("could not unmarshal content for class " + clazz, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonNode getContentAsJson() {
|
||||||
|
try {
|
||||||
|
return new ObjectMapper().readTree(getContentAsString());
|
||||||
|
} catch (JsonProcessingException | UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("could not unmarshal json content", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.web;
|
||||||
|
|
||||||
|
import sonia.scm.api.v2.resources.ScmPathInfoStore;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import static com.google.inject.util.Providers.of;
|
||||||
|
|
||||||
|
public class MockScmPathInfoStore {
|
||||||
|
|
||||||
|
public static Provider<ScmPathInfoStore> forUri(String uri) {
|
||||||
|
ScmPathInfoStore store = new ScmPathInfoStore();
|
||||||
|
store.set(() -> URI.create(uri));
|
||||||
|
return of(store);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import org.apache.shiro.authz.AuthorizationException;
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
import org.apache.shiro.authz.UnauthorizedException;
|
import org.apache.shiro.authz.UnauthorizedException;
|
||||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||||
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
import org.jboss.resteasy.spi.Dispatcher;
|
import org.jboss.resteasy.spi.Dispatcher;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.jboss.resteasy.spi.HttpResponse;
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
|
|||||||
53
scm-ui/ui-components/src/Duration.stories.tsx
Normal file
53
scm-ui/ui-components/src/Duration.stories.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import Duration from "./Duration";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
storiesOf("Duration", module).add("Duration", () => (
|
||||||
|
<div className="m-5 p-5">
|
||||||
|
<p>
|
||||||
|
<Duration duration={500} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Duration duration={2000} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Duration duration={42 * 1000 * 60} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Duration duration={21 * 1000 * 60 * 60} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Duration duration={5 * 1000 * 60 * 60 * 24} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Duration duration={3 * 1000 * 60 * 60 * 24 * 7} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Duration duration={12 * 1000 * 60 * 60 * 24} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
73
scm-ui/ui-components/src/Duration.tsx
Normal file
73
scm-ui/ui-components/src/Duration.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type Unit = "ms" | "s" | "m" | "h" | "d" | "w";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parse = (duration: number) => {
|
||||||
|
let value = duration;
|
||||||
|
let unit: Unit = "ms";
|
||||||
|
if (value > 1000) {
|
||||||
|
unit = "s";
|
||||||
|
value /= 1000;
|
||||||
|
if (value > 60) {
|
||||||
|
unit = "m";
|
||||||
|
value /= 60;
|
||||||
|
if (value > 60) {
|
||||||
|
unit = "h";
|
||||||
|
value /= 60;
|
||||||
|
if (value > 24) {
|
||||||
|
unit = "d";
|
||||||
|
value /= 24;
|
||||||
|
if (value > 7) {
|
||||||
|
unit = "w";
|
||||||
|
value /= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
duration: Math.round(value),
|
||||||
|
unit,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Duration: FC<Props> = ({ duration }) => {
|
||||||
|
const [t] = useTranslation("commons");
|
||||||
|
const parsed = parse(duration);
|
||||||
|
return (
|
||||||
|
<time dateTime={`${parsed.duration}${parsed.unit}`}>
|
||||||
|
{t(`duration.${parsed.unit}`, { count: parsed.duration })}
|
||||||
|
</time>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Duration;
|
||||||
@@ -74,7 +74,7 @@ const OverviewPageActions: FC<Props> = ({
|
|||||||
if (showCreateButton) {
|
if (showCreateButton) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames("input-button", "control", "column")}>
|
<div className={classNames("input-button", "control", "column")}>
|
||||||
<Button label={label} link={createLink || `${link}create`} color="primary" />
|
<Button label={label} link={createLink || `${link}create/`} color="primary" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,10 @@
|
|||||||
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { storiesOf } from "@storybook/react";
|
import { storiesOf } from "@storybook/react";
|
||||||
import * as React from "react";
|
import React, { ReactNode } from "react";
|
||||||
import Tag from "./Tag";
|
import Tag from "./Tag";
|
||||||
import { ReactNode } from "react";
|
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
import { Color, colors, sizes } from "./styleConstants";
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
@@ -35,26 +35,43 @@ const Wrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Spacing = styled.div`
|
const Spacing = styled.div`
|
||||||
padding: 1em;
|
padding: 0.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const colors = ["primary", "link", "info", "success", "warning", "danger"];
|
|
||||||
|
|
||||||
const RoutingDecorator = (story: () => ReactNode) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>;
|
const RoutingDecorator = (story: () => ReactNode) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>;
|
||||||
|
|
||||||
storiesOf("Tag", module)
|
storiesOf("Tag", module)
|
||||||
.addDecorator(RoutingDecorator)
|
.addDecorator(RoutingDecorator)
|
||||||
.addDecorator(storyFn => <Wrapper>{storyFn()}</Wrapper>)
|
.addDecorator((storyFn) => <Wrapper>{storyFn()}</Wrapper>)
|
||||||
.add("Default", () => <Tag label="Default tag" />)
|
.add("Default", () => <Tag label="Default tag" />)
|
||||||
|
.add("Rounded", () => <Tag label="Rounded tag" color="dark" rounded={true} />)
|
||||||
.add("With Icon", () => <Tag label="System" icon="bolt" />)
|
.add("With Icon", () => <Tag label="System" icon="bolt" />)
|
||||||
.add("Colors", () => (
|
.add("Colors", () => (
|
||||||
<div>
|
<div>
|
||||||
{colors.map(color => (
|
{colors.map((color) => (
|
||||||
<Spacing>
|
<Spacing key={color}>
|
||||||
<Tag color={color} label={color} />
|
<Tag color={color} label={color} />
|
||||||
</Spacing>
|
</Spacing>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.add("With title", () => <Tag label="hover me" title="good job"/>)
|
.add("Outlined", () => (
|
||||||
.add("Clickable", () => <Tag label="Click here" onClick={() => alert("Not so fast")}/>);
|
<div>
|
||||||
|
{(["success", "black", "danger"] as Color[]).map((color) => (
|
||||||
|
<Spacing key={color}>
|
||||||
|
<Tag color={color} label={color} outlined={true} />
|
||||||
|
</Spacing>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
.add("With title", () => <Tag label="hover me" title="good job" />)
|
||||||
|
.add("Clickable", () => <Tag label="Click here" onClick={() => alert("Not so fast")} />)
|
||||||
|
.add("Sizes", () => (
|
||||||
|
<div>
|
||||||
|
{sizes.map((size) => (
|
||||||
|
<Spacing key={size}>
|
||||||
|
<Tag size={size} label={size} />
|
||||||
|
</Spacing>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|||||||
@@ -21,50 +21,84 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import * as React from "react";
|
import React, { FC, HTMLAttributes } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { Color, Size } from "./styleConstants";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
color: string;
|
color?: Color;
|
||||||
|
outlined?: boolean;
|
||||||
|
rounded?: boolean;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
label: string;
|
label?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
size?: Size;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Tag extends React.Component<Props> {
|
type InnerTagProps = HTMLAttributes<HTMLSpanElement> & {
|
||||||
static defaultProps = {
|
small: boolean;
|
||||||
color: "light"
|
};
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
const smallMixin = css`
|
||||||
const { className, color, icon, label, title, onClick, onRemove } = this.props;
|
font-size: 0.7rem !important;
|
||||||
let showIcon = null;
|
padding: 0.25rem !important;
|
||||||
if (icon) {
|
font-weight: bold;
|
||||||
showIcon = (
|
`;
|
||||||
<>
|
|
||||||
<i className={classNames("fas", `fa-${icon}`)} />
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let showDelete = null;
|
|
||||||
if (onRemove) {
|
|
||||||
showDelete = <a className="tag is-delete" onClick={onRemove} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const InnerTag = styled.span<InnerTagProps>`
|
||||||
|
${(props) => props.small && smallMixin};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Tag: FC<Props> = ({
|
||||||
|
className,
|
||||||
|
color = "light",
|
||||||
|
outlined,
|
||||||
|
size = "normal",
|
||||||
|
rounded,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
onRemove,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
let showIcon = null;
|
||||||
|
if (icon) {
|
||||||
|
showIcon = (
|
||||||
<>
|
<>
|
||||||
<span className={classNames("tag", `is-${color}`, className)} title={title} onClick={onClick}>
|
<i className={classNames("fas", `fa-${icon}`)} />
|
||||||
{showIcon}
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
{showDelete}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
let showDelete = null;
|
||||||
|
if (onRemove) {
|
||||||
|
showDelete = <a className="tag is-delete" onClick={onRemove} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InnerTag
|
||||||
|
className={classNames("tag", `is-${color}`, `is-${size}`, className, {
|
||||||
|
"is-outlined": outlined,
|
||||||
|
"is-rounded": rounded,
|
||||||
|
"has-cursor-pointer": onClick,
|
||||||
|
})}
|
||||||
|
title={title}
|
||||||
|
onClick={onClick}
|
||||||
|
small={size === "small"}
|
||||||
|
>
|
||||||
|
{showIcon}
|
||||||
|
{label}
|
||||||
|
{children}
|
||||||
|
</InnerTag>
|
||||||
|
{showDelete}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Tag;
|
export default Tag;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
86
scm-ui/ui-components/src/forms/FileInput.tsx
Normal file
86
scm-ui/ui-components/src/forms/FileInput.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
import React, { ChangeEvent, FC, FocusEvent } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||||
|
import { createAttributesForTesting } from "../devBuild";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name?: string;
|
||||||
|
className?: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
helpText?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
testId?: string;
|
||||||
|
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
|
||||||
|
ref?: React.Ref<HTMLInputElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileInput: FC<Props> = ({
|
||||||
|
name,
|
||||||
|
testId,
|
||||||
|
helpText,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
ref,
|
||||||
|
onBlur,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (onChange && event.target.files) {
|
||||||
|
onChange(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
||||||
|
if (onBlur && event.target.files) {
|
||||||
|
onBlur(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames("field", className)}>
|
||||||
|
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
name={name}
|
||||||
|
className={classNames("input", "p-1", className)}
|
||||||
|
type="file"
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileInput;
|
||||||
@@ -56,7 +56,7 @@ export default class TagGroup extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div className="control" key={key}>
|
<div className="control" key={key}>
|
||||||
<div className="tags has-addons">
|
<div className="tags has-addons">
|
||||||
<Tag color="info is-outlined" label={item.displayName} onRemove={() => this.removeEntry(item)} />
|
<Tag color="info" outlined={true} label={item.displayName} onRemove={() => this.removeEntry(item)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -66,7 +66,7 @@ export default class TagGroup extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeEntry = (item: DisplayedUser) => {
|
removeEntry = (item: DisplayedUser) => {
|
||||||
const newItems = this.props.items.filter(name => name !== item);
|
const newItems = this.props.items.filter((name) => name !== item);
|
||||||
this.props.onRemove(newItems);
|
this.props.onRemove(newItems);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,3 +39,4 @@ export { default as PasswordConfirmation } from "./PasswordConfirmation";
|
|||||||
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon";
|
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon";
|
||||||
export { default as DropDown } from "./DropDown";
|
export { default as DropDown } from "./DropDown";
|
||||||
export { default as FileUpload } from "./FileUpload";
|
export { default as FileUpload } from "./FileUpload";
|
||||||
|
export { default as FileInput } from "./FileInput";
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export { validation, repositories };
|
|||||||
|
|
||||||
export { default as DateFromNow } from "./DateFromNow";
|
export { default as DateFromNow } from "./DateFromNow";
|
||||||
export { default as DateShort } from "./DateShort";
|
export { default as DateShort } from "./DateShort";
|
||||||
|
export { default as Duration } from "./Duration";
|
||||||
export { default as ErrorNotification } from "./ErrorNotification";
|
export { default as ErrorNotification } from "./ErrorNotification";
|
||||||
export { default as ErrorPage } from "./ErrorPage";
|
export { default as ErrorPage } from "./ErrorPage";
|
||||||
export { default as Icon } from "./Icon";
|
export { default as Icon } from "./Icon";
|
||||||
|
|||||||
@@ -396,10 +396,17 @@ class DiffFile extends React.Component<Props, State> {
|
|||||||
if (key === value) {
|
if (key === value) {
|
||||||
value = file.type;
|
value = file.type;
|
||||||
}
|
}
|
||||||
const color =
|
|
||||||
value === "added" ? "success is-outlined" : value === "deleted" ? "danger is-outlined" : "info is-outlined";
|
|
||||||
|
|
||||||
return <ChangeTypeTag className={classNames("is-rounded", "has-text-weight-normal")} color={color} label={value} />;
|
const color = value === "added" ? "success" : value === "deleted" ? "danger" : "info";
|
||||||
|
return (
|
||||||
|
<ChangeTypeTag
|
||||||
|
className={classNames("has-text-weight-normal")}
|
||||||
|
rounded={true}
|
||||||
|
outlined={true}
|
||||||
|
color={color}
|
||||||
|
label={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
hasContent = (file: FileDiff) => file && !file.isBinary && file.hunks && file.hunks.length > 0;
|
hasContent = (file: FileDiff) => file && !file.isBinary && file.hunks && file.hunks.length > 0;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import { Repository } from "@scm-manager/ui-types";
|
|||||||
import Image from "../Image";
|
import Image from "../Image";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
import { Color } from "../styleConstants";
|
||||||
|
import RepositoryFlag from "./RepositoryFlag";
|
||||||
|
|
||||||
const baseDate = "2020-03-26T12:13:42+02:00";
|
const baseDate = "2020-03-26T12:13:42+02:00";
|
||||||
|
|
||||||
@@ -48,6 +50,14 @@ const bindAvatar = (binder: Binder, avatar: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bindFlag = (binder: Binder, color: Color, label: string) => {
|
||||||
|
binder.bind("repository.card.flags", () => (
|
||||||
|
<RepositoryFlag title={label} color={color}>
|
||||||
|
{label}
|
||||||
|
</RepositoryFlag>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
const bindBeforeTitle = (binder: Binder, extension: ReactNode) => {
|
const bindBeforeTitle = (binder: Binder, extension: ReactNode) => {
|
||||||
binder.bind("repository.card.beforeTitle", () => {
|
binder.bind("repository.card.beforeTitle", () => {
|
||||||
return extension;
|
return extension;
|
||||||
@@ -76,6 +86,17 @@ const QuickLink = (
|
|||||||
|
|
||||||
const archivedRepository = { ...repository, archived: true };
|
const archivedRepository = { ...repository, archived: true };
|
||||||
const exportingRepository = { ...repository, exporting: true };
|
const exportingRepository = { ...repository, exporting: true };
|
||||||
|
const healthCheckFailedRepository = {
|
||||||
|
...repository,
|
||||||
|
healthCheckFailures: [
|
||||||
|
{
|
||||||
|
id: "4211",
|
||||||
|
summary: "Something failed",
|
||||||
|
description: "Something realy bad happend",
|
||||||
|
url: "https://something-realy-bad.happend"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
const archivedExportingRepository = { ...repository, archived: true, exporting: true };
|
const archivedExportingRepository = { ...repository, archived: true, exporting: true };
|
||||||
|
|
||||||
storiesOf("RepositoryEntry", module)
|
storiesOf("RepositoryEntry", module)
|
||||||
@@ -109,6 +130,18 @@ storiesOf("RepositoryEntry", module)
|
|||||||
bindAvatar(binder, Git);
|
bindAvatar(binder, Git);
|
||||||
return withBinder(binder, exportingRepository);
|
return withBinder(binder, exportingRepository);
|
||||||
})
|
})
|
||||||
|
.add("HealthCheck Failure", () => {
|
||||||
|
const binder = new Binder("title");
|
||||||
|
bindAvatar(binder, Git);
|
||||||
|
return withBinder(binder, healthCheckFailedRepository);
|
||||||
|
})
|
||||||
|
.add("RepositoryFlag EP", () => {
|
||||||
|
const binder = new Binder("title");
|
||||||
|
bindAvatar(binder, Git);
|
||||||
|
bindFlag(binder, "success", "awesome");
|
||||||
|
bindFlag(binder, "warning", "ouhhh...");
|
||||||
|
return withBinder(binder, healthCheckFailedRepository);
|
||||||
|
})
|
||||||
.add("MultiRepositoryTags", () => {
|
.add("MultiRepositoryTags", () => {
|
||||||
const binder = new Binder("title");
|
const binder = new Binder("title");
|
||||||
bindAvatar(binder, Git);
|
bindAvatar(binder, Git);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
|||||||
import { withTranslation, WithTranslation } from "react-i18next";
|
import { withTranslation, WithTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import HealthCheckFailureDetail from "./HealthCheckFailureDetail";
|
import HealthCheckFailureDetail from "./HealthCheckFailureDetail";
|
||||||
|
import RepositoryFlag from "./RepositoryFlag";
|
||||||
|
|
||||||
type DateProp = Date | string;
|
type DateProp = Date | string;
|
||||||
|
|
||||||
@@ -44,37 +45,23 @@ type State = {
|
|||||||
showHealthCheck: boolean;
|
showHealthCheck: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RepositoryTag = styled.span`
|
const Title = styled.span`
|
||||||
margin-left: 0.2rem;
|
display: flex;
|
||||||
background-color: #9a9a9a;
|
align-items: center;
|
||||||
padding: 0.25rem;
|
|
||||||
border-radius: 5px;
|
& > * {
|
||||||
color: white;
|
margin-right: 0.25rem;
|
||||||
overflow: visible;
|
}
|
||||||
pointer-events: all;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
`;
|
|
||||||
const RepositoryWarnTag = styled.span`
|
|
||||||
margin-left: 0.2rem;
|
|
||||||
background-color: #f14668;
|
|
||||||
padding: 0.25rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: white;
|
|
||||||
overflow: visible;
|
|
||||||
pointer-events: all;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
cursor: help;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class RepositoryEntry extends React.Component<Props, State> {
|
class RepositoryEntry extends React.Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showHealthCheck: false
|
showHealthCheck: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createLink = (repository: Repository) => {
|
createLink = (repository: Repository) => {
|
||||||
return `/repo/${repository.namespace}/${repository.name}`;
|
return `/repo/${repository.namespace}/${repository.name}`;
|
||||||
};
|
};
|
||||||
@@ -170,31 +157,33 @@ class RepositoryEntry extends React.Component<Props, State> {
|
|||||||
const { repository, t } = this.props;
|
const { repository, t } = this.props;
|
||||||
const repositoryFlags = [];
|
const repositoryFlags = [];
|
||||||
if (repository.archived) {
|
if (repository.archived) {
|
||||||
repositoryFlags.push(<RepositoryTag title={t("archive.tooltip")}>{t("repository.archived")}</RepositoryTag>);
|
repositoryFlags.push(<RepositoryFlag title={t("archive.tooltip")}>{t("repository.archived")}</RepositoryFlag>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repository.exporting) {
|
if (repository.exporting) {
|
||||||
repositoryFlags.push(<RepositoryTag title={t("exporting.tooltip")}>{t("repository.exporting")}</RepositoryTag>);
|
repositoryFlags.push(<RepositoryFlag title={t("exporting.tooltip")}>{t("repository.exporting")}</RepositoryFlag>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repository.healthCheckFailures && repository.healthCheckFailures.length > 0) {
|
if (repository.healthCheckFailures && repository.healthCheckFailures.length > 0) {
|
||||||
repositoryFlags.push(
|
repositoryFlags.push(
|
||||||
<RepositoryWarnTag
|
<RepositoryFlag
|
||||||
|
color="danger"
|
||||||
title={t("healthCheckFailure.tooltip")}
|
title={t("healthCheckFailure.tooltip")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({ showHealthCheck: true });
|
this.setState({ showHealthCheck: true });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("repository.healthCheckFailure")}
|
{t("repository.healthCheckFailure")}
|
||||||
</RepositoryWarnTag>
|
</RepositoryFlag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Title>
|
||||||
<ExtensionPoint name="repository.card.beforeTitle" props={{ repository }} />
|
<ExtensionPoint name="repository.card.beforeTitle" props={{ repository }} />
|
||||||
<strong>{repository.name}</strong> {repositoryFlags.map(flag => flag)}
|
<strong>{repository.name}</strong> {repositoryFlags}
|
||||||
</>
|
<ExtensionPoint name="repository.flags" props={{ repository }} renderAll={true} />
|
||||||
|
</Title>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
42
scm-ui/ui-components/src/repos/RepositoryFlag.tsx
Normal file
42
scm-ui/ui-components/src/repos/RepositoryFlag.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import Tag from "../Tag";
|
||||||
|
import { Color, Size } from "../styleConstants";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
color?: Color;
|
||||||
|
title?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
size?: Size;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RepositoryFlag: FC<Props> = ({ children, size = "small", ...props }) => (
|
||||||
|
<Tag size={size} {...props}>
|
||||||
|
{children}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RepositoryFlag;
|
||||||
@@ -29,10 +29,11 @@ import {
|
|||||||
AnnotationFactory,
|
AnnotationFactory,
|
||||||
AnnotationFactoryContext,
|
AnnotationFactoryContext,
|
||||||
DiffEventHandler,
|
DiffEventHandler,
|
||||||
DiffEventContext
|
DiffEventContext,
|
||||||
} from "./DiffTypes";
|
} from "./DiffTypes";
|
||||||
|
|
||||||
import { FileDiff as File, FileChangeType, Hunk, Change, ChangeType } from "@scm-manager/ui-types";
|
import { FileDiff as File, FileChangeType, Hunk, Change, ChangeType } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
export { diffs };
|
export { diffs };
|
||||||
|
|
||||||
export * from "./annotate";
|
export * from "./annotate";
|
||||||
@@ -46,6 +47,7 @@ export { default as LoadingDiff } from "./LoadingDiff";
|
|||||||
export { DefaultCollapsed, DefaultCollapsedFunction } from "./defaultCollapsed";
|
export { DefaultCollapsed, DefaultCollapsedFunction } from "./defaultCollapsed";
|
||||||
export { default as RepositoryAvatar } from "./RepositoryAvatar";
|
export { default as RepositoryAvatar } from "./RepositoryAvatar";
|
||||||
export { default as RepositoryEntry } from "./RepositoryEntry";
|
export { default as RepositoryEntry } from "./RepositoryEntry";
|
||||||
|
export { default as RepositoryFlag } from "./RepositoryFlag";
|
||||||
export { default as RepositoryEntryLink } from "./RepositoryEntryLink";
|
export { default as RepositoryEntryLink } from "./RepositoryEntryLink";
|
||||||
export { default as JumpToFileButton } from "./JumpToFileButton";
|
export { default as JumpToFileButton } from "./JumpToFileButton";
|
||||||
export { default as CommitAuthor } from "./CommitAuthor";
|
export { default as CommitAuthor } from "./CommitAuthor";
|
||||||
@@ -61,5 +63,5 @@ export {
|
|||||||
AnnotationFactory,
|
AnnotationFactory,
|
||||||
AnnotationFactoryContext,
|
AnnotationFactoryContext,
|
||||||
DiffEventHandler,
|
DiffEventHandler,
|
||||||
DiffEventContext
|
DiffEventContext,
|
||||||
};
|
};
|
||||||
|
|||||||
40
scm-ui/ui-components/src/styleConstants.ts
Normal file
40
scm-ui/ui-components/src/styleConstants.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const colors = [
|
||||||
|
"black",
|
||||||
|
"dark",
|
||||||
|
"light",
|
||||||
|
"white",
|
||||||
|
"primary",
|
||||||
|
"link",
|
||||||
|
"info",
|
||||||
|
"success",
|
||||||
|
"warning",
|
||||||
|
"danger",
|
||||||
|
] as const;
|
||||||
|
export type Color = typeof colors[number];
|
||||||
|
|
||||||
|
export const sizes = ["small", "normal", "medium", "large"] as const;
|
||||||
|
export type Size = typeof sizes[number];
|
||||||
@@ -26,8 +26,9 @@ import { ExtensionPointDefinition } from "./binder";
|
|||||||
import {
|
import {
|
||||||
IndexResources,
|
IndexResources,
|
||||||
NamespaceStrategies,
|
NamespaceStrategies,
|
||||||
|
Repository,
|
||||||
RepositoryCreation,
|
RepositoryCreation,
|
||||||
RepositoryTypeCollection
|
RepositoryTypeCollection,
|
||||||
} from "@scm-manager/ui-types";
|
} from "@scm-manager/ui-types";
|
||||||
|
|
||||||
type RepositoryCreatorSubFormProps = {
|
type RepositoryCreatorSubFormProps = {
|
||||||
@@ -55,3 +56,5 @@ export type RepositoryCreatorExtension = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RepositoryCreator = ExtensionPointDefinition<"repos.creator", RepositoryCreatorExtension>;
|
export type RepositoryCreator = ExtensionPointDefinition<"repos.creator", RepositoryCreatorExtension>;
|
||||||
|
|
||||||
|
export type RepositoryFlags = ExtensionPointDefinition<"repository.flags", { repository: Repository }>;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user