diff --git a/CHANGELOG.md b/CHANGELOG.md index 9523f17b4f..0beb76d3e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased - ### Added - Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278)) - Anonymous mode for the web ui ([#1284](https://github.com/scm-manager/scm-manager/pull/1284)) - Add link to source file in diff sections ([#1267](https://github.com/scm-manager/scm-manager/pull/1267)) - Check versions of plugin dependencies on plugin installation ([#1283](https://github.com/scm-manager/scm-manager/pull/1283)) +- Sign PR merges and commits performed through ui with generated private key ([#1285](https://github.com/scm-manager/scm-manager/pull/1285)) +- Add generic popover component to ui-components ([#1285](https://github.com/scm-manager/scm-manager/pull/1285)) +- Show changeset signatures in ui and add public keys ([#1273](https://github.com/scm-manager/scm-manager/pull/1273)) ### Fixed - Repository names may not end with ".git" ([#1277](https://github.com/scm-manager/scm-manager/pull/1277)) diff --git a/docs/de/user/repo/assets/repository-code-changesetsView.png b/docs/de/user/repo/assets/repository-code-changesetsView.png index 4974525c4e..446babec5c 100644 Binary files a/docs/de/user/repo/assets/repository-code-changesetsView.png and b/docs/de/user/repo/assets/repository-code-changesetsView.png differ diff --git a/docs/de/user/repo/code.md b/docs/de/user/repo/code.md index 9ec3f29938..a779564ea0 100644 --- a/docs/de/user/repo/code.md +++ b/docs/de/user/repo/code.md @@ -17,6 +17,8 @@ Die Übersicht der Changesets/Commits zeigt die Änderungshistorie je Branch an. Über den Details-Button kann man sich den Inhalt / die Änderungen dieses Changesets ansehen. +Der Schlüssel Icon zeigt an, ob ein Changeset signiert wurde. Um die Signatur zu validieren, können die Benutzer ihre öffentlichen Schlüssel (Public Keys) im SCM-Manager hinterlegen. Ein grüner Schlüssel bedeutet die Signatur konnte erfolgreich gegen einen hinterlegten öffentlichen Schlüssel im SCM-Manager verifiziert werden. Ein grauer Schlüssel heißt, dass die Signatur zu keinem Schlüssel im SCM-Manager passt. Und ein roter Schlüssel warnt vor einer ungültigen (möglicherweise gefälschten) Signatur. + Über den Sources-Button gelangt man zur Sources-Übersicht und es wird der Datenstand zum Zeitpunkt nach diesem Commit angezeigt. ![Repository-Code-Changesets](assets/repository-code-changesetsView.png) diff --git a/docs/de/user/user/assets/user-settings-publickeys.png b/docs/de/user/user/assets/user-settings-publickeys.png new file mode 100644 index 0000000000..1608e46b04 Binary files /dev/null and b/docs/de/user/user/assets/user-settings-publickeys.png differ diff --git a/docs/de/user/user/settings.md b/docs/de/user/user/settings.md index d9ec76446b..d09de0b98e 100644 --- a/docs/de/user/user/settings.md +++ b/docs/de/user/user/settings.md @@ -19,3 +19,8 @@ Hier werden die globalen (nicht-Repository-bezogenen) Berechtigungen für einen Für die einzelnen Rechte sind Tooltips verfügbar, welche Auskunft über die Auswirkungen der jeweiligen Berechtigung geben. ![Benutzer Berechtigungen](assets/user-settings-permissions.png) + +### Öffentliche Schlüssel +Es können öffentliche Schlüssel (Public Keys) zu Benutzern hinzugefügt werden, um die Changeset Signaturen damit zu verifizieren. + +![Öffentliche Schlüssel](assets/user-settings-publickeys.png) diff --git a/docs/en/user/repo/assets/repository-code-changesetsView.png b/docs/en/user/repo/assets/repository-code-changesetsView.png index 4b466543d6..4cb8f63736 100644 Binary files a/docs/en/user/repo/assets/repository-code-changesetsView.png and b/docs/en/user/repo/assets/repository-code-changesetsView.png differ diff --git a/docs/en/user/repo/code.md b/docs/en/user/repo/code.md index b0a8995c88..b0037d563b 100644 --- a/docs/en/user/repo/code.md +++ b/docs/en/user/repo/code.md @@ -17,6 +17,8 @@ The changesets/commits overview shows the change history of the branch. Each ent The Details button leads to the content/changes of a changeset. +The key icon shows if the changeset was signed. The users can add their public keys to SCM-Manager for signature verification. The green key means that the signature could be verified successfully against an existing public key. The grey key shows that no matching key could be found for the signature. The red key warns you about an invalid (possible faked) signature. + The Sources button leads to the sources overview that shows the state from after this commit. ![Repository-Code-Changesets](assets/repository-code-changesetsView.png) diff --git a/docs/en/user/user/assets/user-settings-publickeys.png b/docs/en/user/user/assets/user-settings-publickeys.png new file mode 100644 index 0000000000..498fe1e336 Binary files /dev/null and b/docs/en/user/user/assets/user-settings-publickeys.png differ diff --git a/docs/en/user/user/settings.md b/docs/en/user/user/settings.md index ec37893bab..328fe414af 100644 --- a/docs/en/user/user/settings.md +++ b/docs/en/user/user/settings.md @@ -19,3 +19,8 @@ In the permissions section, the global, therefore not repository-specific permis There is a tooltip for each permission that provide some more details about the option. ![User Permissions](assets/user-settings-permissions.png) + +### Public keys +Add public keys to users to enable changeset signature verification. + +![Public keys](assets/user-settings-publickeys.png) diff --git a/pom.xml b/pom.xml index c3b5a43318..11ad3f3ff2 100644 --- a/pom.xml +++ b/pom.xml @@ -525,6 +525,26 @@ 1.14 + + + + org.bouncycastle + bcpg-jdk15on + ${bouncycastle.version} + + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + + @@ -595,7 +615,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0-M1 + 3.0.0-M3 enforce-java @@ -639,7 +659,7 @@ org.codehaus.mojo extra-enforcer-rules - 1.0-beta-7 + 1.3 @@ -899,6 +919,7 @@ 4.2.3 2.3.3 6.1.5.Final + 1.65 1.6.2 diff --git a/scm-core/pom.xml b/scm-core/pom.xml index f7df981723..9d4f7e388f 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -112,6 +112,12 @@ ${guice.version} + + com.google.inject.extensions + guice-assistedinject + ${guice.version} + + diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java index bd94f9e33f..823df43582 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java @@ -61,6 +61,8 @@ public class ChangesetDto extends HalRepresentation { private List contributors; + private List signatures; + public ChangesetDto(Links links, Embedded embedded) { super(links, embedded); } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/SignatureDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/SignatureDto.java new file mode 100644 index 0000000000..e25943e025 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/SignatureDto.java @@ -0,0 +1,54 @@ +/* + * 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 de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import sonia.scm.repository.Person; +import sonia.scm.repository.SignatureStatus; + +import java.util.Optional; +import java.util.Set; + +@Getter +@Setter +@NoArgsConstructor +@SuppressWarnings("squid:S2160") +public class SignatureDto extends HalRepresentation { + + private String keyId; + private String type; + private SignatureStatus status; + private Optional owner; + private Set contacts; + + public SignatureDto(Links links) { + super(links); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java index e2c980db37..914fd8f90a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java +++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java @@ -32,6 +32,7 @@ import sonia.scm.util.ValidationUtil; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -85,6 +86,8 @@ public class Changeset extends BasicPropertiesAware implements ModelObject { */ private Collection contributors; + private List signatures = new ArrayList<>(); + public Changeset() {} public Changeset(String id, Long date, Person author) @@ -348,4 +351,31 @@ public class Changeset extends BasicPropertiesAware implements ModelObject { this.contributors.addAll(contributors); } } + + /** + * Sets a collection of signatures which belong to this changeset. + * @param signatures collection of signatures + * @since 2.4.0 + */ + public void setSignatures(Collection signatures) { + this.signatures = new ArrayList<>(signatures); + } + + /** + * Returns a immutable list of signatures. + * @return signatures + * @since 2.4.0 + */ + public List getSignatures() { + return Collections.unmodifiableList(signatures); + } + + /** + * Adds a signature to the list of signatures. + * @param signature + * @since 2.4.0 + */ + public void addSignature(Signature signature) { + signatures.add(signature); + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/Signature.java b/scm-core/src/main/java/sonia/scm/repository/Signature.java new file mode 100644 index 0000000000..f0f3e1e492 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/Signature.java @@ -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. + */ + +package sonia.scm.repository; + +import lombok.Value; + +import java.io.Serializable; +import java.util.Optional; +import java.util.Set; + +/** + * Signature is the output of a signature verification. + * + * @since 2.4.0 + */ +@Value +public class Signature implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String keyId; + private final String type; + private final SignatureStatus status; + private final String owner; + private final Set contacts; + + public Optional getOwner() { + return Optional.ofNullable(owner); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java b/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java new file mode 100644 index 0000000000..73961353ea --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java @@ -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; + +/** + * @since 2.4.0 + */ +public enum SignatureStatus { + VERIFIED, NOT_FOUND, INVALID; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java index f704b16b3d..55e1f44f3c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java @@ -21,7 +21,7 @@ * 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.base.Preconditions; @@ -137,6 +137,16 @@ public class MergeCommandBuilder { return this; } + /** + * Disables adding a verifiable signature to the merge commit. + * @return This builder instance. + * @since 2.4.0 + */ + public MergeCommandBuilder disableSigning() { + request.setSign(false); + return this; + } + /** * Use this to set the strategy of the merge commit manually. * diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 3067d7a054..aeccb24b99 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -164,6 +164,16 @@ public class ModifyCommandBuilder { return this; } + /** + * Disables adding a verifiable signature to the modification commit. + * @return This builder instance. + * @since 2.4.0 + */ + public ModifyCommandBuilder disableSigning() { + request.setSign(false); + return this; + } + /** * Set the expected revision of the branch, before the changes are applied. If the branch does not have the * expected revision, a concurrent modification exception will be thrown when the command is executed and no diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index 31ca9ca15d..d7074b7f4b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -55,6 +55,8 @@ import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceResolver; import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.security.PublicKeyCreatedEvent; +import sonia.scm.security.PublicKeyDeletedEvent; import sonia.scm.security.ScmSecurityException; import java.util.Set; @@ -100,14 +102,12 @@ import static sonia.scm.NotFoundException.notFound; * * * @author Sebastian Sdorra - * @since 1.17 - * * @apiviz.landmark * @apiviz.uses sonia.scm.repository.api.RepositoryService + * @since 1.17 */ @Singleton -public final class RepositoryServiceFactory -{ +public final class RepositoryServiceFactory { /** * the logger for RepositoryServiceFactory @@ -122,12 +122,11 @@ public final class RepositoryServiceFactory * should not be called manually, it should only be used by the injection * container. * - * - * @param configuration configuration - * @param cacheManager cache manager + * @param configuration configuration + * @param cacheManager cache manager * @param repositoryManager manager for repositories - * @param resolvers a set of {@link RepositoryServiceResolver} - * @param preProcessorUtil helper object for pre processor handling + * @param resolvers a set of {@link RepositoryServiceResolver} + * @param preProcessorUtil helper object for pre processor handling * @param protocolProviders * @param workdirProvider * @since 1.21 @@ -136,8 +135,7 @@ public final class RepositoryServiceFactory public RepositoryServiceFactory(ScmConfiguration configuration, CacheManager cacheManager, RepositoryManager repositoryManager, Set resolvers, PreProcessorUtil preProcessorUtil, - Set protocolProviders, WorkdirProvider workdirProvider) - { + @SuppressWarnings("rawtypes") Set protocolProviders, WorkdirProvider workdirProvider) { this( configuration, cacheManager, repositoryManager, resolvers, preProcessorUtil, protocolProviders, workdirProvider, ScmEventBus.getInstance() @@ -146,11 +144,10 @@ public final class RepositoryServiceFactory @VisibleForTesting RepositoryServiceFactory(ScmConfiguration configuration, - CacheManager cacheManager, RepositoryManager repositoryManager, - Set resolvers, PreProcessorUtil preProcessorUtil, - Set protocolProviders, WorkdirProvider workdirProvider, - ScmEventBus eventBus) - { + CacheManager cacheManager, RepositoryManager repositoryManager, + Set resolvers, PreProcessorUtil preProcessorUtil, + Set protocolProviders, WorkdirProvider workdirProvider, + ScmEventBus eventBus) { this.configuration = configuration; this.cacheManager = cacheManager; this.repositoryManager = repositoryManager; @@ -167,19 +164,16 @@ public final class RepositoryServiceFactory /** * Creates a new RepositoryService for the given repository. * - * * @param repositoryId id of the repository - * * @return a implementation of RepositoryService - * for the given type of repository - * - * @throws NotFoundException if no repository - * with the given id is available + * for the given type of repository + * @throws NotFoundException if no repository + * with the given id is available * @throws RepositoryServiceNotFoundException if no repository service - * implementation for this kind of repository is available - * @throws IllegalArgumentException if the repository id is null or empty - * @throws ScmSecurityException if current user has not read permissions - * for that repository + * implementation for this kind of repository is available + * @throws IllegalArgumentException if the repository id is null or empty + * @throws ScmSecurityException if current user has not read permissions + * for that repository */ public RepositoryService create(String repositoryId) { Preconditions.checkArgument(!Strings.isNullOrEmpty(repositoryId), @@ -187,8 +181,7 @@ public final class RepositoryServiceFactory Repository repository = repositoryManager.get(repositoryId); - if (repository == null) - { + if (repository == null) { throw new NotFoundException(Repository.class, repositoryId); } @@ -198,29 +191,24 @@ public final class RepositoryServiceFactory /** * Creates a new RepositoryService for the given repository. * - * * @param namespaceAndName namespace and name of the repository - * * @return a implementation of RepositoryService - * for the given type of repository - * - * @throws NotFoundException if no repository - * with the given id is available + * for the given type of repository + * @throws NotFoundException if no repository + * with the given id is available * @throws RepositoryServiceNotFoundException if no repository service - * implementation for this kind of repository is available - * @throws IllegalArgumentException if one of the parameters is null or empty - * @throws ScmSecurityException if current user has not read permissions - * for that repository + * implementation for this kind of repository is available + * @throws IllegalArgumentException if one of the parameters is null or empty + * @throws ScmSecurityException if current user has not read permissions + * for that repository */ - public RepositoryService create(NamespaceAndName namespaceAndName) - { + public RepositoryService create(NamespaceAndName namespaceAndName) { Preconditions.checkArgument(namespaceAndName != null, "a non empty namespace and name is required"); Repository repository = repositoryManager.get(namespaceAndName); - if (repository == null) - { + if (repository == null) { throw notFound(entity(namespaceAndName)); } @@ -230,20 +218,16 @@ public final class RepositoryServiceFactory /** * Creates a new RepositoryService for the given repository. * - * * @param repository the repository - * * @return a implementation of RepositoryService - * for the given type of repository - * + * for the given type of repository * @throws RepositoryServiceNotFoundException if no repository service - * implementation for this kind of repository is available - * @throws NullPointerException if the repository is null - * @throws ScmSecurityException if current user has not read permissions - * for that repository + * implementation for this kind of repository is available + * @throws NullPointerException if the repository is null + * @throws ScmSecurityException if current user has not read permissions + * for that repository */ - public RepositoryService create(Repository repository) - { + public RepositoryService create(Repository repository) { Preconditions.checkNotNull(repository, "repository is required"); // check for read permissions of current user @@ -251,14 +235,11 @@ public final class RepositoryServiceFactory RepositoryService service = null; - for (RepositoryServiceResolver resolver : resolvers) - { + for (RepositoryServiceResolver resolver : resolvers) { RepositoryServiceProvider provider = resolver.resolve(repository); - if (provider != null) - { - if (logger.isDebugEnabled()) - { + if (provider != null) { + if (logger.isDebugEnabled()) { logger.debug( "create new repository service for repository {} of type {}", repository.getName(), repository.getType()); @@ -271,8 +252,7 @@ public final class RepositoryServiceFactory } } - if (service == null) - { + if (service == null) { throw new RepositoryServiceNotFoundException(repository); } @@ -284,8 +264,7 @@ public final class RepositoryServiceFactory /** * Hook and listener to clear all relevant repository caches. */ - private static class CacheClearHook - { + private static class CacheClearHook { private final Set> caches = Sets.newHashSet(); private final CacheManager cacheManager; @@ -296,8 +275,7 @@ public final class RepositoryServiceFactory * * @param cacheManager cache manager */ - public CacheClearHook(CacheManager cacheManager) - { + public CacheClearHook(CacheManager cacheManager) { this.cacheManager = cacheManager; this.caches.add(cacheManager.getCache(BlameCommandBuilder.CACHE_NAME)); this.caches.add(cacheManager.getCache(BrowseCommandBuilder.CACHE_NAME)); @@ -324,12 +302,10 @@ public final class RepositoryServiceFactory * @param event hook event */ @Subscribe(referenceType = ReferenceType.STRONG) - public void onEvent(PostReceiveRepositoryHookEvent event) - { + public void onEvent(PostReceiveRepositoryHookEvent event) { Repository repository = event.getRepository(); - if (repository != null) - { + if (repository != null) { String id = repository.getId(); clearCaches(id); @@ -342,10 +318,8 @@ public final class RepositoryServiceFactory * @param event repository event */ @Subscribe(referenceType = ReferenceType.STRONG) - public void onEvent(RepositoryEvent event) - { - if (event.getEventType() == HandlerEventType.DELETE) - { + public void onEvent(RepositoryEvent event) { + if (event.getEventType() == HandlerEventType.DELETE) { clearCaches(event.getItem().getId()); } } @@ -357,37 +331,53 @@ public final class RepositoryServiceFactory cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME).removeAll(predicate); } + @Subscribe + public void onEvent(PublicKeyDeletedEvent event) { + cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear(); + } + + @Subscribe + public void onEvent(PublicKeyCreatedEvent event) { + cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear(); + } + @SuppressWarnings({"unchecked", "java:S3740", "rawtypes"}) - private void clearCaches(final String repositoryId) - { - if (logger.isDebugEnabled()) - { + private void clearCaches(final String repositoryId) { + if (logger.isDebugEnabled()) { logger.debug("clear caches for repository id {}", repositoryId); } RepositoryCacheKeyPredicate predicate = new RepositoryCacheKeyPredicate(repositoryId); - caches.forEach((cache) -> { - cache.removeAll(predicate); - }); + caches.forEach(cache -> cache.removeAll(predicate)); } } //~--- fields --------------------------------------------------------------- - /** cache manager */ + /** + * cache manager + */ private final CacheManager cacheManager; - /** scm-manager configuration */ + /** + * scm-manager configuration + */ private final ScmConfiguration configuration; - /** pre processor util */ + /** + * pre processor util + */ private final PreProcessorUtil preProcessorUtil; - /** repository manager */ + /** + * repository manager + */ private final RepositoryManager repositoryManager; - /** service resolvers */ + /** + * service resolvers + */ private final Set resolvers; private Set protocolProviders; diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java index 043f0f9648..5fa755668f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java @@ -21,7 +21,7 @@ * 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.MoreObjects; @@ -43,6 +43,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl private Person author; private String messageTemplate; private MergeStrategy mergeStrategy; + private boolean sign = true; public String getBranchToMerge() { return branchToMerge; @@ -84,6 +85,14 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl this.mergeStrategy = mergeStrategy; } + public boolean isSign() { + return sign; + } + + public void setSign(boolean sign) { + this.sign = sign; + } + public boolean isValid() { return !Strings.isNullOrEmpty(getBranchToMerge()) && !Strings.isNullOrEmpty(getTargetBranch()); @@ -92,6 +101,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl public void reset() { this.setBranchToMerge(null); this.setTargetBranch(null); + this.setSign(true); } @Override @@ -109,7 +119,8 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl return Objects.equal(branchToMerge, other.branchToMerge) && Objects.equal(targetBranch, other.targetBranch) && Objects.equal(author, other.author) - && Objects.equal(mergeStrategy, other.mergeStrategy); + && Objects.equal(mergeStrategy, other.mergeStrategy) + && Objects.equal(sign, other.sign); } @Override @@ -124,6 +135,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl .add("targetBranch", targetBranch) .add("author", author) .add("mergeStrategy", mergeStrategy) + .add("sign", sign) .toString(); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java index d0c05fd99d..507af8b514 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index 8f077430df..e82df8c15a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -49,6 +49,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit private String branch; private String expectedRevision; private boolean defaultPath; + private boolean sign = true; @Override public void reset() { @@ -57,6 +58,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit commitMessage = null; branch = null; defaultPath = false; + sign = true; } public void addRequest(PartialRequest request) { @@ -75,6 +77,10 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit this.branch = branch; } + public void setSign(boolean sign) { + this.sign = sign; + } + public List getRequests() { return Collections.unmodifiableList(requests); } @@ -112,6 +118,10 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit this.defaultPath = defaultPath; } + public boolean isSign() { + return sign; + } + public interface PartialRequest { void execute(ModifyCommand.Worker worker) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/security/GPG.java b/scm-core/src/main/java/sonia/scm/security/GPG.java new file mode 100644 index 0000000000..2f75b8d5dc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/GPG.java @@ -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 java.util.Optional; + +/** + * Allows signing and verification using gpg. + * + * @since 2.4.0 + */ +public interface GPG { + + /** + * Returns the id of the key from the given signature. + * + * @param signature signature + * @return public key id + */ + String findPublicKeyId(byte[] signature); + + /** + * Returns the public key with the given id or an empty optional. + * + * @param id id of public + * @return public key or empty optional + */ + Optional findPublicKey(String id); + + /** + * Returns all public keys assigned to the given username + * + * @param username username of the public key owner + * @return collection of public keys + */ + Iterable findPublicKeysByUsername(String username); + + /** + * Returns the default private key of the currently authenticated user. + * + * @return default private key + */ + PrivateKey getPrivateKey(); +} diff --git a/scm-core/src/main/java/sonia/scm/security/NotPublicKeyException.java b/scm-core/src/main/java/sonia/scm/security/NotPublicKeyException.java new file mode 100644 index 0000000000..3d8775a09a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/NotPublicKeyException.java @@ -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.security; + +import sonia.scm.BadRequestException; +import sonia.scm.ContextEntry; + +import java.util.List; + +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class NotPublicKeyException extends BadRequestException { + public NotPublicKeyException(List context, String message) { + super(context, message); + } + + public NotPublicKeyException(List context, String message, Exception cause) { + super(context, message, cause); + } + + @Override + public String getCode() { + return "BxS5wX2v71"; + } +} diff --git a/scm-core/src/main/java/sonia/scm/security/PrivateKey.java b/scm-core/src/main/java/sonia/scm/security/PrivateKey.java new file mode 100644 index 0000000000..0ae639a3db --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/PrivateKey.java @@ -0,0 +1,57 @@ +/* + * 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 java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * Can be used to create signatures of data. + * @since 2.4.0 + */ +public interface PrivateKey { + + /** + * Returns the key's id. + * @return id + */ + String getId(); + + /** + * Creates a signature for the given data. + * @param stream data stream to sign + * @return signature + */ + byte[] sign(InputStream stream); + + /** + * Creates a signature for the given data. + * @param data data to sign + * @return signature + */ + default byte[] sign(byte[] data) { + return sign(new ByteArrayInputStream(data)); + } +} diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKey.java b/scm-core/src/main/java/sonia/scm/security/PublicKey.java new file mode 100644 index 0000000000..003863d696 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/PublicKey.java @@ -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.security; + +import sonia.scm.repository.Person; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Optional; +import java.util.Set; + +/** + * The public key can be used to verify signatures. + * + * @since 2.4.0 + */ +public interface PublicKey { + + /** + * Returns id of the public key. + * + * @return id of key + */ + String getId(); + + /** + * Returns the username of the owner or an empty optional. + * + * @return owner or empty optional + */ + Optional getOwner(); + + /** + * Returns raw of the public key. + * + * @return raw of key + */ + String getRaw(); + + /** + * Returns the contacts of the publickey. + * + * @return owner or empty optional + */ + Set getContacts(); + + /** + * Verifies that the signature is valid for the given data. + * + * @param stream stream of data to verify + * @param signature signature + * @return {@code true} if the signature is valid for the given data + */ + boolean verify(InputStream stream, byte[] signature); + + /** + * Verifies that the signature is valid for the given data. + * + * @param data data to verify + * @param signature signature + * @return {@code true} if the signature is valid for the given data + */ + default boolean verify(byte[] data, byte[] signature) { + return verify(new ByteArrayInputStream(data), signature); + } +} diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java b/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java new file mode 100644 index 0000000000..a598533088 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java @@ -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.security; + +import sonia.scm.event.Event; + +/** + * This event is fired when a public key was created in SCM-Manager. + * @since 2.4.0 + */ +@Event +public final class PublicKeyCreatedEvent { + private final PublicKey key; + + public PublicKeyCreatedEvent(PublicKey key) { + this.key = key; + } + + public PublicKey getKey() { + return key; + } +} diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java b/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java new file mode 100644 index 0000000000..833af8dee2 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java @@ -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.security; + +import sonia.scm.event.Event; + +/** + * This event is fired when a public key was removed from SCM-Manager. + * @since 2.4.0 + */ +@Event +public final class PublicKeyDeletedEvent { + private final PublicKey key; + + public PublicKeyDeletedEvent(PublicKey key) { + this.key = key; + } + + public PublicKey getKey() { + return key; + } +} diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index cd2afb282d..1b0a3b2388 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -50,7 +50,7 @@ import java.security.Principal; @StaticPermissions( value = "user", globalPermissions = {"create", "list", "autocomplete"}, - permissions = {"read", "modify", "delete", "changePassword"}, + permissions = {"read", "modify", "delete", "changePassword", "changePublicKeys"}, custom = true, customGlobal = true ) @XmlRootElement(name = "users") diff --git a/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java b/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java index 6c5efe722c..61c3013f36 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java +++ b/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/ScmTransportProtocol.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package org.eclipse.jgit.transport; //~--- non-JDK imports -------------------------------------------------------- @@ -29,200 +29,106 @@ package org.eclipse.jgit.transport; import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.google.inject.Provider; - import org.eclipse.jgit.errors.NoRemoteRepositoryException; -import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; - +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.web.CollectingPackParserListener; import sonia.scm.web.GitReceiveHook; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; - import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** - * * @author Sebastian Sdorra */ -public class ScmTransportProtocol extends TransportProtocol -{ +public class ScmTransportProtocol extends TransportProtocol { - /** Field description */ public static final String NAME = "scm"; - - /** Field description */ private static final Set SCHEMES = ImmutableSet.of(NAME); - //~--- constructors --------------------------------------------------------- + private Provider converterFactory; + private Provider hookEventFacadeProvider; + private Provider repositoryHandlerProvider; - /** - * Constructs ... - * - */ - public ScmTransportProtocol() {} + public ScmTransportProtocol() { + } - /** - * Constructs ... - * - * - * - * @param hookEventFacadeProvider - * - * @param repositoryHandlerProvider - */ @Inject public ScmTransportProtocol( + Provider converterFactory, Provider hookEventFacadeProvider, - Provider repositoryHandlerProvider) - { + Provider repositoryHandlerProvider) { + this.converterFactory = converterFactory; this.hookEventFacadeProvider = hookEventFacadeProvider; this.repositoryHandlerProvider = repositoryHandlerProvider; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param uri - * @param local - * @param remoteName - * - * @return - */ @Override - public boolean canHandle(URIish uri, Repository local, String remoteName) - { - if ((uri.getPath() == null) || (uri.getPort() > 0) - || (uri.getUser() != null) || (uri.getPass() != null) - || (uri.getHost() != null) - || ((uri.getScheme() != null) &&!getSchemes().contains(uri.getScheme()))) - { - return false; - } - - return true; + public boolean canHandle(URIish uri, Repository local, String remoteName) { + return (uri.getPath() != null) && (uri.getPort() <= 0) + && (uri.getUser() == null) && (uri.getPass() == null) + && (uri.getHost() == null) + && ((uri.getScheme() == null) || getSchemes().contains(uri.getScheme())); } - /** - * Method description - * - * - * @param uri - * @param local - * @param remoteName - * - * @return - * - * @throws NotSupportedException - * @throws TransportException - */ @Override - public Transport open(URIish uri, Repository local, String remoteName) - throws TransportException - { + public Transport open(URIish uri, Repository local, String remoteName) throws TransportException { File localDirectory = local.getDirectory(); File path = local.getFS().resolve(localDirectory, uri.getPath()); File gitDir = RepositoryCache.FileKey.resolve(path, local.getFS()); - if (gitDir == null) - { + if (gitDir == null) { throw new NoRemoteRepositoryException(uri, JGitText.get().notFound); } - //J- return new TransportLocalWithHooks( + converterFactory.get(), hookEventFacadeProvider.get(), repositoryHandlerProvider.get(), local, uri, gitDir ); - //J+ } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override - public String getName() - { + public String getName() { return NAME; } - /** - * Method description - * - * - * @return - */ @Override - public Set getSchemes() - { + public Set getSchemes() { return SCHEMES; } - //~--- inner classes -------------------------------------------------------- + private static class TransportLocalWithHooks extends TransportLocal { - /** - * Class description - * - * - * @version Enter version here..., 13/05/19 - * @author Enter your name here... - */ - private static class TransportLocalWithHooks extends TransportLocal - { + private final GitChangesetConverterFactory converterFactory; + private final GitRepositoryHandler handler; + private final HookEventFacade hookEventFacade; - /** - * Constructs ... - * - * - * - * @param hookEventFacade - * @param handler - * @param local - * @param uri - * @param gitDir - */ - public TransportLocalWithHooks(HookEventFacade hookEventFacade, - GitRepositoryHandler handler, Repository local, URIish uri, File gitDir) - { + public TransportLocalWithHooks( + GitChangesetConverterFactory converterFactory, + HookEventFacade hookEventFacade, + GitRepositoryHandler handler, + Repository local, URIish uri, File gitDir) { super(local, uri, gitDir); + this.converterFactory = converterFactory; this.hookEventFacade = hookEventFacade; this.handler = handler; } - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param dst - * - * @return - */ @Override - ReceivePack createReceivePack(Repository dst) - { + ReceivePack createReceivePack(Repository dst) { ReceivePack pack = new ReceivePack(dst); - if ((hookEventFacade != null) && (handler != null)) - { - GitReceiveHook hook = new GitReceiveHook(hookEventFacade, handler); + if ((hookEventFacade != null) && (handler != null) && (converterFactory != null)) { + GitReceiveHook hook = new GitReceiveHook(converterFactory, hookEventFacade, handler); pack.setPreReceiveHook(hook); pack.setPostReceiveHook(hook); @@ -232,22 +138,6 @@ public class ScmTransportProtocol extends TransportProtocol return pack; } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private GitRepositoryHandler handler; - - /** Field description */ - private HookEventFacade hookEventFacade; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Provider hookEventFacadeProvider; - - /** Field description */ - private Provider repositoryHandlerProvider; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java index 9b18991d1a..028e91009d 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java @@ -21,7 +21,7 @@ * 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; @@ -29,6 +29,7 @@ import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.web.CollectingPackParserListener; @@ -39,9 +40,9 @@ public abstract class BaseReceivePackFactory implements ReceivePackFactory private final GitRepositoryHandler handler; private final GitReceiveHook hook; - protected BaseReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { + protected BaseReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) { this.handler = handler; - this.hook = new GitReceiveHook(hookEventFacade, handler); + this.hook = new GitReceiveHook(converterFactory, hookEventFacade, handler); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java index f15ccfcf99..a999a57d4d 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java @@ -21,21 +21,22 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.protocolcommand.git; import com.google.inject.Inject; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceivePack; import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.spi.HookEventFacade; public class ScmReceivePackFactory extends BaseReceivePackFactory { @Inject - public ScmReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { - super(handler, hookEventFacade); + public ScmReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) { + super(converterFactory, handler, hookEventFacade); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java index 0f08f54aaa..1262353e1d 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java @@ -26,6 +26,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import org.eclipse.jgit.lib.ObjectId; @@ -33,138 +34,55 @@ import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.jgit.util.RawParseUtils; +import sonia.scm.security.GPG; +import sonia.scm.security.PublicKey; import sonia.scm.util.Util; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; //~--- JDK imports ------------------------------------------------------------ /** - * * @author Sebastian Sdorra */ -public class GitChangesetConverter implements Closeable -{ +public class GitChangesetConverter implements Closeable { - /** - * the logger for GitChangesetConverter - */ - private static final Logger logger = - LoggerFactory.getLogger(GitChangesetConverter.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param repository - */ - public GitChangesetConverter(org.eclipse.jgit.lib.Repository repository) - { - this(repository, null); - } - - /** - * Constructs ... - * - * - * @param repository - * @param revWalk - */ - public GitChangesetConverter(org.eclipse.jgit.lib.Repository repository, - RevWalk revWalk) - { - this.repository = repository; - - if (revWalk != null) - { - this.revWalk = revWalk; - - } - else - { - this.revWalk = new RevWalk(repository); - } + private final GPG gpg; + private final Multimap tags; + private final TreeWalk treeWalk; + public GitChangesetConverter(GPG gpg, org.eclipse.jgit.lib.Repository repository, RevWalk revWalk) { + this.gpg = gpg; this.tags = GitUtil.createTagMap(repository, revWalk); - treeWalk = new TreeWalk(repository); + this.treeWalk = new TreeWalk(repository); } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - @Override - public void close() - { - GitUtil.release(treeWalk); - } - - /** - * Method description - * - * - * @param commit - * - * @return - * - * @throws IOException - */ - public Changeset createChangeset(RevCommit commit) - { + public Changeset createChangeset(RevCommit commit) { return createChangeset(commit, Collections.emptyList()); } - /** - * Method description - * - * - * @param commit - * @param branch - * - * @return - * - * @throws IOException - */ - public Changeset createChangeset(RevCommit commit, String branch) - { + public Changeset createChangeset(RevCommit commit, String branch) { return createChangeset(commit, Lists.newArrayList(branch)); } - /** - * Method description - * - * - * - * @param commit - * @param branches - * - * @return - * - * @throws IOException - */ - public Changeset createChangeset(RevCommit commit, List branches) - { + public Changeset createChangeset(RevCommit commit, List branches) { String id = commit.getId().name(); List parentList = null; RevCommit[] parents = commit.getParents(); - if (Util.isNotEmpty(parents)) - { - parentList = new ArrayList(); + if (Util.isNotEmpty(parents)) { + parentList = new ArrayList<>(); - for (RevCommit parent : parents) - { + for (RevCommit parent : parents) { parentList.add(parent.getId().name()); } } @@ -175,8 +93,7 @@ public class GitChangesetConverter implements Closeable Person author = createPersonFor(authorIndent); String message = commit.getFullMessage(); - if (message != null) - { + if (message != null) { message = message.trim(); } @@ -185,41 +102,83 @@ public class GitChangesetConverter implements Closeable changeset.addContributor(new Contributor("Committed-by", createPersonFor(committerIdent))); } - if (parentList != null) - { + if (parentList != null) { changeset.setParents(parentList); } Collection tagCollection = tags.get(commit.getId()); - if (Util.isNotEmpty(tagCollection)) - { - + if (Util.isNotEmpty(tagCollection)) { // create a copy of the tag collection to reduce memory on caching changeset.getTags().addAll(Lists.newArrayList(tagCollection)); } changeset.setBranches(branches); + Signature signature = createSignature(commit); + if (signature != null) { + changeset.addSignature(signature); + } + return changeset; } + private static final byte[] GPG_HEADER = {'g', 'p', 'g', 's', 'i', 'g'}; + + private Signature createSignature(RevCommit commit) { + byte[] raw = commit.getRawBuffer(); + + int start = RawParseUtils.headerStart(GPG_HEADER, raw, 0); + if (start < 0) { + return null; + } + + int end = RawParseUtils.headerEnd(raw, start); + byte[] signature = Arrays.copyOfRange(raw, start, end); + + String publicKeyId = gpg.findPublicKeyId(signature); + if (Strings.isNullOrEmpty(publicKeyId)) { + // key not found + return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()); + } + + Optional publicKeyById = gpg.findPublicKey(publicKeyId); + if (!publicKeyById.isPresent()) { + // key not found + return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()); + } + + PublicKey publicKey = publicKeyById.get(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + byte[] headerPrefix = Arrays.copyOfRange(raw, 0, start - GPG_HEADER.length - 1); + baos.write(headerPrefix); + + byte[] headerSuffix = Arrays.copyOfRange(raw, end + 1, raw.length); + baos.write(headerSuffix); + } catch (IOException ex) { + // this will never happen, because we are writing into memory + throw new IllegalStateException("failed to write into memory", ex); + } + + boolean verified = publicKey.verify(baos.toByteArray(), signature); + return new Signature( + publicKeyId, + "gpg", + verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID, + publicKey.getOwner().orElse(null), + publicKey.getContacts() + ); + } + public Person createPersonFor(PersonIdent personIndent) { return new Person(personIndent.getName(), personIndent.getEmailAddress()); } + @Override + public void close() { + GitUtil.release(treeWalk); + } - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private org.eclipse.jgit.lib.Repository repository; - - /** Field description */ - private RevWalk revWalk; - - /** Field description */ - private Multimap tags; - - /** Field description */ - private TreeWalk treeWalk; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverterFactory.java new file mode 100644 index 0000000000..4f4389fa2e --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverterFactory.java @@ -0,0 +1,50 @@ +/* + * 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.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import sonia.scm.security.GPG; + +import javax.inject.Inject; + +public class GitChangesetConverterFactory { + + private final GPG gpg; + + @Inject + public GitChangesetConverterFactory(GPG gpg) { + this.gpg = gpg; + } + + public GitChangesetConverter create(Repository repository) { + return new GitChangesetConverter(gpg, repository, new RevWalk(repository)); + } + + public GitChangesetConverter create(Repository repository, RevWalk revWalk) { + return new GitChangesetConverter(gpg, repository, revWalk); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java index 3071f590bc..5d7de5ce27 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHookChangesetCollector.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- @@ -72,9 +72,10 @@ public class GitHookChangesetCollector * @param rpack * @param receiveCommands */ - public GitHookChangesetCollector(ReceivePack rpack, + public GitHookChangesetCollector(GitChangesetConverterFactory converterFactory, ReceivePack rpack, List receiveCommands) { + this.converterFactory = converterFactory; this.rpack = rpack; this.receiveCommands = receiveCommands; this.listener = CollectingPackParserListener.get(rpack); @@ -100,14 +101,14 @@ public class GitHookChangesetCollector try { walk = rpack.getRevWalk(); - converter = new GitChangesetConverter(repository, walk); + converter = converterFactory.create(repository, walk); for (ReceiveCommand rc : receiveCommands) { String ref = rc.getRefName(); - + logger.trace("handle receive command, type={}, ref={}, result={}", rc.getType(), ref, rc.getResult()); - + if (rc.getType() == ReceiveCommand.Type.DELETE) { logger.debug("skip delete of ref {}", ref); @@ -130,7 +131,7 @@ public class GitHookChangesetCollector builder.append(rc.getType()).append(", ref="); builder.append(rc.getRefName()).append(", result="); builder.append(rc.getResult()); - + logger.error(builder.toString(), ex); } } @@ -222,5 +223,6 @@ public class GitHookChangesetCollector private final List receiveCommands; + private final GitChangesetConverterFactory converterFactory; private final ReceivePack rpack; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java new file mode 100644 index 0000000000..423cd2aca8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java @@ -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.repository; + +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.transport.CredentialsProvider; +import sonia.scm.security.GPG; + +import javax.inject.Inject; +import java.io.UnsupportedEncodingException; + +public class ScmGpgSigner extends GpgSigner { + + private final GPG gpg; + + @Inject + public ScmGpgSigner(GPG gpg) { + this.gpg = gpg; + } + + @Override + public void sign(CommitBuilder commitBuilder, String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { + try { + final byte[] signature = this.gpg.getPrivateKey().sign(commitBuilder.build()); + commitBuilder.setGpgSignature(new GpgSignature(signature)); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + @Override + public boolean canLocateSigningKey(String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { + return true; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java new file mode 100644 index 0000000000..144601275d --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java @@ -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. + */ + +package sonia.scm.repository; + +import org.eclipse.jgit.lib.GpgSigner; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +@Extension +public class ScmGpgSignerInitializer implements ServletContextListener { + + private final ScmGpgSigner scmGpgSigner; + + @Inject + public ScmGpgSignerInitializer(ScmGpgSigner scmGpgSigner) { + this.scmGpgSigner = scmGpgSigner; + } + + @Override + public void contextInitialized(ServletContextEvent servletContextEvent) { + GpgSigner.setDefault(scmGpgSigner); + } + + @Override + public void contextDestroyed(ServletContextEvent servletContextEvent) { + // Do nothing + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitReceiveHookMergeDetectionProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitReceiveHookMergeDetectionProvider.java index 03a5b59837..bac6eb6348 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitReceiveHookMergeDetectionProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitReceiveHookMergeDetectionProvider.java @@ -28,6 +28,7 @@ import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceiveCommand; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitUtil; import sonia.scm.repository.spi.GitLogComputer; import sonia.scm.repository.spi.HookMergeDetectionProvider; @@ -39,11 +40,13 @@ public class GitReceiveHookMergeDetectionProvider implements HookMergeDetectionP private final Repository repository; private final String repositoryId; private final List receiveCommands; + private final GitChangesetConverterFactory converterFactory; - public GitReceiveHookMergeDetectionProvider(Repository repository, String repositoryId, List receiveCommands) { + public GitReceiveHookMergeDetectionProvider(Repository repository, String repositoryId, List receiveCommands, GitChangesetConverterFactory converterFactory) { this.repository = repository; this.repositoryId = repositoryId; this.receiveCommands = receiveCommands; + this.converterFactory = converterFactory; } @Override @@ -53,7 +56,7 @@ public class GitReceiveHookMergeDetectionProvider implements HookMergeDetectionP request.setAncestorChangeset(findRelevantRevisionForBranchIfToBeUpdated(target)); request.setPagingLimit(1); - return new GitLogComputer(repositoryId, repository).compute(request).getTotal() == 0; + return new GitLogComputer(repositoryId, repository, converterFactory).compute(request).getTotal() == 0; } private String findRelevantRevisionForBranchIfToBeUpdated(String branch) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index 9e13e82971..c083123242 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -63,11 +63,9 @@ import static sonia.scm.repository.GitUtil.getBranchIdOrCurrentHead; //~--- JDK imports ------------------------------------------------------------ /** - * * @author Sebastian Sdorra */ -class AbstractGitCommand -{ +class AbstractGitCommand { /** * the logger for AbstractGitCommand @@ -77,11 +75,9 @@ class AbstractGitCommand /** * Constructs ... * - * @param context - * + * @param context */ - AbstractGitCommand(GitContext context) - { + AbstractGitCommand(GitContext context) { this.repository = context.getRepository(); this.context = context; } @@ -91,19 +87,16 @@ class AbstractGitCommand /** * Method description * - * * @return - * * @throws IOException */ - Repository open() throws IOException - { + Repository open() throws IOException { return context.open(); } ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException { ObjectId commit; - if ( Strings.isNullOrEmpty(requestedCommit) ) { + if (Strings.isNullOrEmpty(requestedCommit)) { commit = getDefaultBranch(gitRepository); } else { commit = gitRepository.resolve(requestedCommit); @@ -121,7 +114,7 @@ class AbstractGitCommand } Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { - if ( Strings.isNullOrEmpty(requestedBranch) ) { + if (Strings.isNullOrEmpty(requestedBranch)) { String defaultBranchName = context.getConfig().getDefaultBranch(); return getBranchIdOrCurrentHead(gitRepository, defaultBranchName); } else { @@ -220,7 +213,7 @@ class AbstractGitCommand } } - Optional doCommit(String message, Person author) { + Optional doCommit(String message, Person author, boolean sign) { Person authorToUse = determineAuthor(author); try { Status status = clone.status().call(); @@ -229,6 +222,8 @@ class AbstractGitCommand .setAuthor(authorToUse.getName(), authorToUse.getMail()) .setCommitter("SCM-Manager", "noreply@scm-manager.org") .setMessage(message) + .setSign(sign) + .setSigningKey(sign ? "SCM-MANAGER-DEFAULT-KEY" : null) .call()); } else { return empty(); @@ -288,9 +283,13 @@ class AbstractGitCommand //~--- fields --------------------------------------------------------------- - /** Field description */ + /** + * Field description + */ protected GitContext context; - /** Field description */ + /** + * Field description + */ protected sonia.scm.repository.Repository repository; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java index 1b03a5a55e..94b1451d68 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java @@ -36,6 +36,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; @@ -58,18 +59,10 @@ public abstract class AbstractGitIncomingOutgoingCommand /** Field description */ private static final String REMOTE_REF_PREFIX = "refs/remote/scm/%s/"; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * @param handler - * @param context - */ - AbstractGitIncomingOutgoingCommand(GitRepositoryHandler handler, GitContext context) - { + AbstractGitIncomingOutgoingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory) { super(context); this.handler = handler; + this.converterFactory = converterFactory; } //~--- methods -------------------------------------------------------------- @@ -132,7 +125,7 @@ public abstract class AbstractGitIncomingOutgoingCommand try { walk = new RevWalk(git.getRepository()); - converter = new GitChangesetConverter(git.getRepository(), walk); + converter = converterFactory.create(git.getRepository(), walk); org.eclipse.jgit.api.LogCommand log = git.log(); @@ -203,4 +196,5 @@ public abstract class AbstractGitIncomingOutgoingCommand /** Field description */ private GitRepositoryHandler handler; + private final GitChangesetConverterFactory converterFactory; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java index 5c1f074a90..d99343b350 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java @@ -41,6 +41,7 @@ import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Person; +import javax.inject.Inject; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -64,6 +65,7 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand //~--- constructors --------------------------------------------------------- + @Inject public GitBlameCommand(GitContext context) { super(context); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java index eb372ff063..43bfb68578 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java @@ -43,6 +43,7 @@ import sonia.scm.repository.api.HookContext; import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookFeature; +import javax.inject.Inject; import java.io.IOException; import java.util.List; import java.util.Set; @@ -57,6 +58,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman private final HookContextFactory hookContextFactory; private final ScmEventBus eventBus; + @Inject GitBranchCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus) { super(context); this.hookContextFactory = hookContextFactory; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java index cd7892461b..d026affd8b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java @@ -38,6 +38,7 @@ import sonia.scm.repository.Branch; import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; +import javax.inject.Inject; import java.io.IOException; import java.util.List; import java.util.Optional; @@ -53,6 +54,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class); + @Inject public GitBranchesCommand(GitContext context) { super(context); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index ab5a7d33b4..05792b9707 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -57,6 +57,7 @@ import sonia.scm.store.BlobStore; import sonia.scm.util.Util; import sonia.scm.web.lfs.LfsBlobStoreFactory; +import javax.inject.Inject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; @@ -111,6 +112,11 @@ public class GitBrowseCommand extends AbstractGitCommand private int resultCount = 0; + @Inject + public GitBrowseCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutorProvider executorProvider) { + this(context, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout()); + } + public GitBrowseCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) { super(context); this.lfsBlobStoreFactory = lfsBlobStoreFactory; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index 4629e4bce2..8c2e3b44d5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -44,6 +44,7 @@ import sonia.scm.util.IOUtil; import sonia.scm.util.Util; import sonia.scm.web.lfs.LfsBlobStoreFactory; +import javax.inject.Inject; import java.io.Closeable; import java.io.FilterInputStream; import java.io.IOException; @@ -61,6 +62,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { private final LfsBlobStoreFactory lfsBlobStoreFactory; + @Inject public GitCatCommand(GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context); this.lfsBlobStoreFactory = lfsBlobStoreFactory; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java new file mode 100644 index 0000000000..04bb37bed0 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java @@ -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.spi; + +import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +class GitContextFactory { + + private final GitRepositoryHandler handler; + private final GitRepositoryConfigStoreProvider storeProvider; + + @Inject + GitContextFactory(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider) { + this.handler = handler; + this.storeProvider = storeProvider; + } + + GitContext create(Repository repository) { + return new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index adb7a7bd0e..c0ed1a53bc 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -29,6 +29,7 @@ import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.util.QuotedString; import sonia.scm.repository.api.DiffCommandBuilder; +import javax.inject.Inject; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -41,6 +42,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; */ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { + @Inject GitDiffCommand(GitContext context) { super(context); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java index fed865c576..e55d8badae 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java @@ -32,6 +32,7 @@ import sonia.scm.repository.api.DiffFile; import sonia.scm.repository.api.DiffResult; import sonia.scm.repository.api.Hunk; +import javax.inject.Inject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Iterator; @@ -39,6 +40,7 @@ import java.util.stream.Collectors; public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand { + @Inject GitDiffResultCommand(GitContext context) { super(context); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookChangesetProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookChangesetProvider.java index 3280e2c5c4..1dbe652371 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookChangesetProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookChangesetProvider.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -29,6 +29,7 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceivePack; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitHookChangesetCollector; //~--- JDK imports ------------------------------------------------------------ @@ -39,56 +40,27 @@ import java.util.List; * * @author Sebastian Sdorra */ -public class GitHookChangesetProvider implements HookChangesetProvider -{ +public class GitHookChangesetProvider implements HookChangesetProvider { - /** - * Constructs ... - * - * - * @param receivePack - * @param receiveCommands - */ - public GitHookChangesetProvider(ReceivePack receivePack, - List receiveCommands) - { + private final GitChangesetConverterFactory converterFactory; + private final ReceivePack receivePack; + private final List receiveCommands; + + private HookChangesetResponse response; + + public GitHookChangesetProvider(GitChangesetConverterFactory converterFactory, ReceivePack receivePack, + List receiveCommands) { + this.converterFactory = converterFactory; this.receivePack = receivePack; this.receiveCommands = receiveCommands; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * - * @return - */ @Override - public synchronized HookChangesetResponse handleRequest( - HookChangesetRequest request) - { - if (response == null) - { - GitHookChangesetCollector collector = - new GitHookChangesetCollector(receivePack, receiveCommands); - + public synchronized HookChangesetResponse handleRequest(HookChangesetRequest request) { + if (response == null) { + GitHookChangesetCollector collector = new GitHookChangesetCollector(converterFactory, receivePack, receiveCommands); response = new HookChangesetResponse(collector.collectChangesets()); } - return response; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private List receiveCommands; - - /** Field description */ - private ReceivePack receivePack; - - /** Field description */ - private HookChangesetResponse response; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java index bc8d633add..13a11007a2 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java @@ -30,6 +30,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceivePack; import sonia.scm.repository.api.GitHookBranchProvider; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.api.GitHookMessageProvider; import sonia.scm.repository.api.GitHookTagProvider; import sonia.scm.repository.api.GitReceiveHookMergeDetectionProvider; @@ -62,14 +63,16 @@ public class GitHookContextProvider extends HookContextProvider //~--- constructors --------------------------------------------------------- + private final GitChangesetConverterFactory converterFactory; + /** * Constructs a new instance * @param receivePack git receive pack * @param receiveCommands received commands */ public GitHookContextProvider( - ReceivePack receivePack, - List receiveCommands, + GitChangesetConverterFactory converterFactory, ReceivePack receivePack, + List receiveCommands, Repository repository, String repositoryId ) { @@ -77,8 +80,9 @@ public class GitHookContextProvider extends HookContextProvider this.receiveCommands = receiveCommands; this.repository = repository; this.repositoryId = repositoryId; - this.changesetProvider = new GitHookChangesetProvider(receivePack, + this.changesetProvider = new GitHookChangesetProvider(converterFactory, receivePack, receiveCommands); + this.converterFactory = converterFactory; } //~--- methods -------------------------------------------------------------- @@ -110,7 +114,7 @@ public class GitHookContextProvider extends HookContextProvider @Override public HookMergeDetectionProvider getMergeDetectionProvider() { - return new GitReceiveHookMergeDetectionProvider(repository, repositoryId, receiveCommands); + return new GitReceiveHookMergeDetectionProvider(repository, repositoryId, receiveCommands, converterFactory); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java index f6e818bcdb..205d9e9e3f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitIncomingCommand.java @@ -29,8 +29,10 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.lib.ObjectId; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; +import javax.inject.Inject; import java.io.IOException; //~--- JDK imports ------------------------------------------------------------ @@ -40,18 +42,11 @@ import java.io.IOException; * @author Sebastian Sdorra */ public class GitIncomingCommand extends AbstractGitIncomingOutgoingCommand - implements IncomingCommand -{ + implements IncomingCommand { - /** - * Constructs ... - * - * @param handler - * @param context - */ - GitIncomingCommand(GitRepositoryHandler handler, GitContext context) - { - super(handler, context); + @Inject + GitIncomingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory) { + super(context, handler, converterFactory); } //~--- get methods ---------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java index 340df65c16..e0f0b2868b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java @@ -36,10 +36,12 @@ import org.slf4j.LoggerFactory; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.util.IOUtil; +import javax.inject.Inject; import java.io.IOException; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -60,6 +62,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand private static final Logger logger = LoggerFactory.getLogger(GitLogCommand.class); public static final String REVISION = "Revision"; + private final GitChangesetConverterFactory converterFactory; //~--- constructors --------------------------------------------------------- @@ -70,9 +73,11 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand * @param context * */ - GitLogCommand(GitContext context) + @Inject + GitLogCommand(GitContext context, GitChangesetConverterFactory converterFactory) { super(context); + this.converterFactory = converterFactory; } //~--- get methods ---------------------------------------------------------- @@ -110,7 +115,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand if (commit != null) { - converter = new GitChangesetConverter(gr, revWalk); + converter = converterFactory.create(gr, revWalk); if (isBranchRequested(request)) { String branch = request.getBranch(); @@ -177,7 +182,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand if (Strings.isNullOrEmpty(request.getBranch())) { request.setBranch(context.getConfig().getDefaultBranch()); } - return new GitLogComputer(this.repository.getId(), gitRepository).compute(request); + return new GitLogComputer(this.repository.getId(), gitRepository, converterFactory).compute(request); } catch (IOException e) { throw new InternalRepositoryException(repository, "could not create change log", e); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java index eae8ea5851..b33460fb37 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java @@ -42,6 +42,7 @@ import sonia.scm.NotFoundException; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.util.IOUtil; @@ -59,10 +60,12 @@ public class GitLogComputer { private final String repositoryId; private final Repository gitRepository; + private final GitChangesetConverterFactory converterFactory; - public GitLogComputer(String repositoryId, Repository repository) { + public GitLogComputer(String repositoryId, Repository repository, GitChangesetConverterFactory converterFactory) { this.repositoryId = repositoryId; this.gitRepository = repository; + this.converterFactory = converterFactory; } public ChangesetPagingResult compute(LogCommandRequest request) { @@ -123,7 +126,7 @@ public class GitLogComputer { revWalk = new RevWalk(gitRepository); - converter = new GitChangesetConverter(gitRepository, revWalk); + converter = converterFactory.create(gitRepository, revWalk); if (!Strings.isNullOrEmpty(request.getPath())) { revWalk.setTreeFilter( diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java index 9f362edeb6..bd4e6b26b5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -36,6 +36,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.filter.PathFilter; +import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.api.MergeCommandResult; @@ -43,6 +44,7 @@ import sonia.scm.repository.api.MergeDryRunCommandResult; import sonia.scm.repository.api.MergeStrategy; import sonia.scm.repository.api.MergeStrategyNotSupportedException; +import javax.inject.Inject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Set; @@ -61,6 +63,11 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand MergeStrategy.SQUASH ); + @Inject + GitMergeCommand(GitContext context, GitRepositoryHandler handler) { + this(context, handler.getWorkingCopyFactory()); + } + GitMergeCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory) { super(context); this.workingCopyFactory = workingCopyFactory; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java index 0b79ec7204..72a3f23b5f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -56,6 +56,7 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker doCommit() { logger.debug("merged branch {} into {}", branchToMerge, targetBranch); - return doCommit(MessageFormat.format(determineMessageTemplate(), branchToMerge, targetBranch), author); + return doCommit(MessageFormat.format(determineMessageTemplate(), branchToMerge, targetBranch), author, sign); } MergeCommandResult createSuccessResult(String newRevision) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java index 918d276148..e907081f2c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java @@ -41,6 +41,7 @@ import sonia.scm.repository.Modified; import sonia.scm.repository.Removed; import sonia.scm.repository.Renamed; +import javax.inject.Inject; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -53,7 +54,8 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity; @Slf4j public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand { - protected GitModificationsCommand(GitContext context) { + @Inject + GitModificationsCommand(GitContext context) { super(context); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index 9e0f5449d3..01271d0fc3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -34,11 +34,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ConcurrentModificationException; import sonia.scm.NoChangesMadeException; +import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.web.lfs.LfsBlobStoreFactory; +import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.nio.file.Path; @@ -53,6 +55,11 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman private final GitWorkingCopyFactory workingCopyFactory; private final LfsBlobStoreFactory lfsBlobStoreFactory; + @Inject + GitModifyCommand(GitContext context, GitRepositoryHandler repositoryHandler, LfsBlobStoreFactory lfsBlobStoreFactory) { + this(context, repositoryHandler.getWorkingCopyFactory(), lfsBlobStoreFactory); + } + GitModifyCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context); this.workingCopyFactory = workingCopyFactory; @@ -86,7 +93,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman r.execute(this); } failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())); - Optional revCommit = doCommit(request.getCommitMessage(), request.getAuthor()); + Optional revCommit = doCommit(request.getCommitMessage(), request.getAuthor(), request.isSign()); push(); return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java index 03acf9e914..30192d4297 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java @@ -29,8 +29,10 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.lib.ObjectId; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; +import javax.inject.Inject; import java.io.IOException; //~--- JDK imports ------------------------------------------------------------ @@ -40,18 +42,12 @@ import java.io.IOException; * @author Sebastian Sdorra */ public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand - implements OutgoingCommand -{ + implements OutgoingCommand { - /** - * Constructs ... - * - * @param handler - * @param context - */ - GitOutgoingCommand(GitRepositoryHandler handler, GitContext context) + @Inject + GitOutgoingCommand(GitContext context, GitRepositoryHandler handler, GitChangesetConverterFactory converterFactory) { - super(handler, context); + super(context, handler, converterFactory); } //~--- get methods ---------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java index 53b7a59916..422391fd19 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java @@ -44,6 +44,7 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.PullResponse; +import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.net.URL; @@ -73,6 +74,7 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand * @param handler * @param context */ + @Inject public GitPullCommand(GitRepositoryHandler handler, GitContext context) { super(handler, context); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java index fd874524f4..ddbfb6a8e1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java @@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.api.PushResponse; +import javax.inject.Inject; import java.io.IOException; //~--- JDK imports ------------------------------------------------------------ @@ -55,8 +56,8 @@ public class GitPushCommand extends AbstractGitPushOrPullCommand * @param handler * @param context */ - public GitPushCommand(GitRepositoryHandler handler, GitContext context) - { + @Inject + public GitPushCommand(GitRepositoryHandler handler, GitContext context) { super(handler, context); this.handler = handler; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index 862631c32c..fae69a47cf 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -25,21 +25,14 @@ package sonia.scm.repository.spi; import com.google.common.collect.ImmutableSet; -import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; -import sonia.scm.event.ScmEventBus; +import com.google.inject.AbstractModule; +import com.google.inject.Injector; import sonia.scm.repository.Feature; -import sonia.scm.repository.GitRepositoryHandler; -import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; -import sonia.scm.repository.api.HookContextFactory; -import sonia.scm.web.lfs.LfsBlobStoreFactory; -import java.io.IOException; import java.util.EnumSet; import java.util.Set; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra @@ -47,8 +40,6 @@ import java.util.Set; public class GitRepositoryServiceProvider extends RepositoryServiceProvider { - /** Field description */ - //J- public static final Set COMMANDS = ImmutableSet.of( Command.BLAME, Command.BROWSE, @@ -66,105 +57,51 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider Command.MERGE, Command.MODIFY ); + protected static final Set FEATURES = EnumSet.of(Feature.INCOMING_REVISION); - //J+ + + private final GitContext context; + private final Injector commandInjector; //~--- constructors --------------------------------------------------------- - public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) { - this.handler = handler; - this.lfsBlobStoreFactory = lfsBlobStoreFactory; - this.hookContextFactory = hookContextFactory; - this.eventBus = eventBus; - this.executorProvider = executorProvider; - this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); + GitRepositoryServiceProvider(Injector injector, GitContext context) { + this.context = context; + commandInjector = injector.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + bind(GitContext.class).toInstance(context); + } + }); } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @throws IOException - */ @Override - public void close() throws IOException - { - context.close(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - @Override - public BlameCommand getBlameCommand() - { + public BlameCommand getBlameCommand() { return new GitBlameCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public BranchesCommand getBranchesCommand() - { + public BranchesCommand getBranchesCommand() { return new GitBranchesCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public BranchCommand getBranchCommand() - { - return new GitBranchCommand(context, hookContextFactory, eventBus); + public BranchCommand getBranchCommand() { + return commandInjector.getInstance(GitBranchCommand.class); } - /** - * Method description - * - * - * @return - */ @Override - public BrowseCommand getBrowseCommand() - { - return new GitBrowseCommand(context, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout()); + public BrowseCommand getBrowseCommand() { + return commandInjector.getInstance(GitBrowseCommand.class); } - /** - * Method description - * - * - * @return - */ @Override - public CatCommand getCatCommand() - { - return new GitCatCommand(context, lfsBlobStoreFactory); + public CatCommand getCatCommand() { + return commandInjector.getInstance(GitCatCommand.class); } - /** - * Method description - * - * - * @return - */ @Override - public DiffCommand getDiffCommand() - { + public DiffCommand getDiffCommand() { return new GitDiffCommand(context); } @@ -173,28 +110,14 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitDiffResultCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public IncomingCommand getIncomingCommand() - { - return new GitIncomingCommand(handler, context); + public IncomingCommand getIncomingCommand() { + return commandInjector.getInstance(GitIncomingCommand.class); } - /** - * Method description - * - * - * @return - */ @Override - public LogCommand getLogCommand() - { - return new GitLogCommand(context); + public LogCommand getLogCommand() { + return commandInjector.getInstance(GitLogCommand.class); } @Override @@ -202,93 +125,48 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitModificationsCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public OutgoingCommand getOutgoingCommand() - { - return new GitOutgoingCommand(handler, context); + public OutgoingCommand getOutgoingCommand() { + return commandInjector.getInstance(GitOutgoingCommand.class); } - /** - * Method description - * - * - * @return - */ @Override - public PullCommand getPullCommand() - { - return new GitPullCommand(handler, context); + public PullCommand getPullCommand() { + return commandInjector.getInstance(GitPullCommand.class); } - /** - * Method description - * - * - * @return - */ @Override - public PushCommand getPushCommand() - { - return new GitPushCommand(handler, context); + public PushCommand getPushCommand() { + return commandInjector.getInstance(GitPushCommand.class); } - /** - * Method description - * - * - * @return - */ @Override - public Set getSupportedCommands() - { - return COMMANDS; - } - - /** - * Method description - * - * - * @return - */ - @Override - public TagsCommand getTagsCommand() - { + public TagsCommand getTagsCommand() { return new GitTagsCommand(context); } @Override public MergeCommand getMergeCommand() { - return new GitMergeCommand(context, handler.getWorkingCopyFactory()); + return commandInjector.getInstance(GitMergeCommand.class); } @Override public ModifyCommand getModifyCommand() { - return new GitModifyCommand(context, handler.getWorkingCopyFactory(), lfsBlobStoreFactory); + return commandInjector.getInstance(GitModifyCommand.class); + } + + @Override + public Set getSupportedCommands() { + return COMMANDS; } @Override public Set getSupportedFeatures() { return FEATURES; } -//~--- fields --------------------------------------------------------------- - /** Field description */ - private final GitContext context; - - /** Field description */ - private final GitRepositoryHandler handler; - - private final LfsBlobStoreFactory lfsBlobStoreFactory; - - private final HookContextFactory hookContextFactory; - - private final ScmEventBus eventBus; - - private final SyncAsyncExecutorProvider executorProvider; + @Override + public void close() { + context.close(); + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 8ffda05ad3..7ff06dd140 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -21,19 +21,16 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; -import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; -import sonia.scm.event.ScmEventBus; +import com.google.inject.Injector; import sonia.scm.plugin.Extension; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; -import sonia.scm.repository.api.HookContextFactory; -import sonia.scm.web.lfs.LfsBlobStoreFactory; /** * @@ -42,31 +39,20 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory; @Extension public class GitRepositoryServiceResolver implements RepositoryServiceResolver { - private final GitRepositoryHandler handler; - private final GitRepositoryConfigStoreProvider storeProvider; - private final LfsBlobStoreFactory lfsBlobStoreFactory; - private final HookContextFactory hookContextFactory; - private final ScmEventBus eventBus; - private final SyncAsyncExecutorProvider executorProvider; + private final Injector injector; + private final GitContextFactory contextFactory; @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) { - this.handler = handler; - this.storeProvider = storeProvider; - this.lfsBlobStoreFactory = lfsBlobStoreFactory; - this.hookContextFactory = hookContextFactory; - this.eventBus = eventBus; - this.executorProvider = executorProvider; + public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory) { + this.injector = injector; + this.contextFactory = contextFactory; } @Override public GitRepositoryServiceProvider resolve(Repository repository) { - GitRepositoryServiceProvider provider = null; - if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus, executorProvider); + return new GitRepositoryServiceProvider(injector, contextFactory.create(repository)); } - - return provider; + return null; } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java index a9d74e0357..a093b5cda3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java @@ -34,6 +34,7 @@ import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceivePack; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.spi.GitHookContextProvider; @@ -66,9 +67,10 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook * @param hookEventFacade * @param handler */ - public GitReceiveHook(HookEventFacade hookEventFacade, - GitRepositoryHandler handler) + public GitReceiveHook(GitChangesetConverterFactory converterFactory, HookEventFacade hookEventFacade, + GitRepositoryHandler handler) { + this.converterFactory = converterFactory; this.hookEventFacade = hookEventFacade; this.handler = handler; } @@ -122,7 +124,7 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook logger.trace("resolved repository to {}", repositoryId); - GitHookContextProvider context = new GitHookContextProvider(rpack, receiveCommands, repository, repositoryId); + GitHookContextProvider context = new GitHookContextProvider(converterFactory, rpack, receiveCommands, repository, repositoryId); hookEventFacade.handle(repositoryId).fireHookEvent(type, context); @@ -187,6 +189,7 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook /** Field description */ private GitRepositoryHandler handler; + private final GitChangesetConverterFactory converterFactory; /** Field description */ private HookEventFacade hookEventFacade; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java index b4b43ea5b5..dc18189da9 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- @@ -34,6 +34,7 @@ import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import sonia.scm.protocolcommand.git.BaseReceivePackFactory; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.spi.HookEventFacade; @@ -53,8 +54,8 @@ public class GitReceivePackFactory extends BaseReceivePackFactory wrapped; @Inject - public GitReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { - super(handler, hookEventFacade); + public GitReceivePackFactory(GitChangesetConverterFactory converterFactory, GitRepositoryHandler handler, HookEventFacade hookEventFacade) { + super(converterFactory, handler, hookEventFacade); this.wrapped = new DefaultReceivePackFactory(); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java index 8fa2ecddaa..cc178c2ef8 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java @@ -21,7 +21,7 @@ * 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.api.Git; @@ -40,6 +40,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.GitTestHelper; import sonia.scm.web.CollectingPackParserListener; import sonia.scm.web.GitReceiveHook; @@ -82,7 +83,7 @@ public class BaseReceivePackFactoryTest { ReceivePack receivePack = new ReceivePack(repository); when(wrappedReceivePackFactory.create(request, repository)).thenReturn(receivePack); - factory = new BaseReceivePackFactory(handler, null) { + factory = new BaseReceivePackFactory(GitTestHelper.createConverterFactory(), handler, null) { @Override protected ReceivePack createBasicReceivePack(Object request, Repository repository) throws ServiceNotEnabledException, ServiceNotAuthorizedException { return wrappedReceivePackFactory.create(request, repository); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java new file mode 100644 index 0000000000..fcaba7a211 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java @@ -0,0 +1,304 @@ +/* + * 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.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.security.GPG; +import sonia.scm.security.PublicKey; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GitChangesetConverterTest { + + private static Git git; + + @BeforeAll + static void setUpRepository(@TempDir Path repositoryPath) throws GitAPIException { + // we use the same repository for all tests to speed things up + git = Git.init().setDirectory(repositoryPath.toFile()).call(); + } + + @AfterAll + static void closeRepository() { + git.close(); + } + + @Test + void shouldConvertChangeset() throws GitAPIException, IOException { + long now = System.currentTimeMillis() - 1000L; + Changeset changeset = commit( + "Tricia McMillan", "trillian@hitchhiker.com", "Added awesome markdown file" + ); + assertThat(changeset.getId()).isNotEmpty(); + assertThat(changeset.getDate()).isGreaterThanOrEqualTo(now); + assertThat(changeset.getDescription()).isEqualTo("Added awesome markdown file"); + + Person author = changeset.getAuthor(); + assertThat(author.getName()).isEqualTo("Tricia McMillan"); + assertThat(author.getMail()).isEqualTo("trillian@hitchhiker.com"); + } + + private Changeset commit(String name, String mail, String message) throws GitAPIException, IOException { + addRandomFileToRepository(); + + RevCommit commit = git.commit() + .setAuthor(name, mail) + .setMessage(message) + .call(); + + GitChangesetConverterFactory converterFactory = GitTestHelper.createConverterFactory(); + return converterFactory.create(git.getRepository()).createChangeset(commit); + } + + private void addRandomFileToRepository() throws IOException, GitAPIException { + File directory = git.getRepository().getWorkTree(); + String name = UUID.randomUUID().toString(); + File file = new File(directory, name + ".md"); + Files.write(file.toPath(), ("# Greetings\n\nFrom " + name).getBytes(StandardCharsets.UTF_8)); + git.add().addFilepattern(name + ".md").call(); + } + + @Nested + class SignatureTests { + + @Mock + private GPG gpg; + @Mock + private PublicKey publicKey; + + private PGPKeyPair keyPair; + private GpgSigner defaultSigner; + + @BeforeEach + void setUpTestingSignerAndCaptureDefault() throws Exception { + defaultSigner = GpgSigner.getDefault(); + // we use the same keypair for all tests to speed things up a little bit + if (keyPair == null) { + keyPair = createKeyPair(); + GpgSigner.setDefault(new TestingGpgSigner(keyPair)); + } + } + + @AfterEach + void restoreDefaultSigner() { + GpgSigner.setDefault(defaultSigner); + } + + @Test + void shouldReturnUnknownSignature() throws Exception { + String identity = "0xAWESOMExBOB"; + when(gpg.findPublicKeyId(any())).thenReturn(identity); + + Signature signature = addSignedCommitAndReturnSignature(identity); + assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet())); + } + + @Test + void shouldReturnKnownButInvalidSignature() throws Exception { + String identity = "0xAWESOMExBOB"; + String owner = "BobTheSigner"; + setPublicKey(identity, owner, false); + + Signature signature = addSignedCommitAndReturnSignature(identity); + assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.INVALID, owner, Collections.emptySet())); + } + + @Test + void shouldReturnValidSignature() throws Exception { + String identity = "0xAWESOMExBOB"; + String owner = "BobTheSigner"; + setPublicKey(identity, owner, true); + + Signature signature = addSignedCommitAndReturnSignature(identity); + assertThat(signature).isEqualTo(new Signature(identity, "gpg", SignatureStatus.VERIFIED, owner, Collections.emptySet())); + } + + @Test + void shouldPassDataAndSignatureForVerification() throws Exception { + setPublicKey("0x42", "Me", true); + addSignedCommitAndReturnSignature("0x42"); + + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor signatureCaptor = ArgumentCaptor.forClass(byte[].class); + verify(publicKey).verify(dataCaptor.capture(), signatureCaptor.capture()); + + String data = new String(dataCaptor.getValue()); + assertThat(data).contains("author Bob The Signer "); + + String signature = new String(signatureCaptor.getValue()); + assertThat(signature).contains("BEGIN PGP SIGNATURE", "END PGP SIGNATURE"); + } + + @Test + void shouldNotReturnSignatureForNonSignedCommit() throws GitAPIException, IOException { + Changeset changeset = commit("Bob", "unsigned@bob.de", "not signed"); + assertThat(changeset.getSignatures()).isEmpty(); + } + + private void setPublicKey(String id, String owner, boolean valid) { + when(gpg.findPublicKeyId(any())).thenReturn(id); + when(gpg.findPublicKey(id)).thenReturn(Optional.of(publicKey)); + + when(publicKey.getOwner()).thenReturn(Optional.of(owner)); + when(publicKey.verify(any(byte[].class), any(byte[].class))).thenReturn(valid); + } + + private Signature addSignedCommitAndReturnSignature(String keyIdentity) throws IOException, GitAPIException { + RevCommit commit = addSignedCommit(keyIdentity); + GitChangesetConverterFactory factory = new GitChangesetConverterFactory(gpg); + GitChangesetConverter converter = factory.create(git.getRepository()); + + List signatures = converter.createChangeset(commit).getSignatures(); + assertThat(signatures).isNotEmpty().hasSize(1); + + return signatures.get(0); + } + + private RevCommit addSignedCommit(String keyIdentity) throws IOException, GitAPIException { + addRandomFileToRepository(); + return git.commit() + .setAuthor("Bob The Signer", "sign@bob.de") + .setMessage("Signed from Bob") + .setSign(true) + .setSigningKey(keyIdentity) + .call(); + } + + + } + + private PGPKeyPair createKeyPair() throws PGPException, NoSuchProviderException, NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + // we use a small key size to speedup test, a much larger size should be used for production + keyPairGenerator.initialize(512); + KeyPair pair = keyPairGenerator.generateKeyPair(); + return new JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, pair, new Date()); + } + + private static class TestingGpgSigner extends GpgSigner { + + private final PGPKeyPair keyPair; + + TestingGpgSigner(PGPKeyPair keyPair) { + this.keyPair = keyPair; + } + + @Override + public boolean canLocateSigningKey(String gpgSigningKey, PersonIdent committer, CredentialsProvider credentialsProvider) { + return true; + } + + @Override + public void sign(CommitBuilder commit, String gpgSigningKey, + PersonIdent committer, CredentialsProvider credentialsProvider) { + try { + if (keyPair == null) { + throw new JGitInternalException(JGitText.get().unableToSignCommitNoSecretKey); + } + + PGPPrivateKey privateKey = keyPair.getPrivateKey(); + + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( + new JcaPGPContentSignerBuilder( + keyPair.getPublicKey().getAlgorithm(), + HashAlgorithmTags.SHA256).setProvider(BouncyCastleProvider.PROVIDER_NAME) + ); + signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) { + signatureGenerator.update(commit.build()); + signatureGenerator.generate().encode(out); + } + commit.setGpgSignature(new GpgSignature(buffer.toByteArray())); + } catch (PGPException | IOException e) { + throw new JGitInternalException(e.getMessage(), e); + } + } + + } + + // register bouncy castle provider on load + static { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } +} + + diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java new file mode 100644 index 0000000000..9a3c179f95 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java @@ -0,0 +1,90 @@ +/* + * 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.api.errors.CanceledException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.transport.CredentialsProvider; +import sonia.scm.security.GPG; +import sonia.scm.security.PrivateKey; +import sonia.scm.security.PublicKey; + +import java.util.Collections; +import java.util.Optional; + +public final class GitTestHelper { + + private GitTestHelper() { + } + + public static GitChangesetConverterFactory createConverterFactory() { + return new GitChangesetConverterFactory(new NoopGPG()); + } + + public static class SimpleGpgSigner extends GpgSigner { + + public static byte[] getSignature() { + return "SIGNATURE".getBytes(); + } + + @Override + public void sign(CommitBuilder commitBuilder, String s, PersonIdent personIdent, CredentialsProvider + credentialsProvider) throws CanceledException { + commitBuilder.setGpgSignature(new GpgSignature(SimpleGpgSigner.getSignature())); + } + + @Override + public boolean canLocateSigningKey(String s, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { + return true; + } + + } + + private static class NoopGPG implements GPG { + + @Override + public String findPublicKeyId(byte[] signature) { + return "secret-key"; + } + + @Override + public Optional findPublicKey(String id) { + return Optional.empty(); + } + + @Override + public Iterable findPublicKeysByUsername(String username) { + return Collections.emptySet(); + } + + @Override + public PrivateKey getPrivateKey() { + return null; + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/ScmGpgSignerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/ScmGpgSignerTest.java new file mode 100644 index 0000000000..04f0a99c6a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/ScmGpgSignerTest.java @@ -0,0 +1,116 @@ +/* + * 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.api.Git; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.security.GPG; +import sonia.scm.security.PrivateKey; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ScmGpgSignerTest { + + @Mock + GPG gpg; + + @Mock + PersonIdent personIdent; + + @Mock + CredentialsProvider credentialsProvider; + + private ScmGpgSigner signer; + + private final PrivateKey privateKey = new PrivateKey() { + @Override + public String getId() { + return "Private Key"; + } + + @Override + public byte[] sign(InputStream stream) { + return "MY FANCY SIGNATURE".getBytes(); + } + }; + + @BeforeEach + void beforeEach() { + signer = new ScmGpgSigner(gpg); + } + + @Test + void sign(@TempDir Path workdir) throws Exception { + + when(gpg.getPrivateKey()).thenReturn(privateKey); + + GpgSigner.setDefault(signer); + + Path repositoryPath = workdir.resolve("repository"); + Git git = Git.init().setDirectory(repositoryPath.toFile()).call(); + + Files.write(repositoryPath.resolve("README.md"), "# Hello".getBytes(StandardCharsets.UTF_8)); + git.add().addFilepattern("README.md").call(); + + git.commit() + .setAuthor("Bob The Signer", "sign@bob.de") + .setMessage("Signed from Bob") + .setSign(true) + .setSigningKey("Private Key") + .call(); + + RevCommit commit = git.log().setMaxCount(1).call().iterator().next(); + + final byte[] rawCommit = commit.getRawBuffer(); + final String commitString = new String(rawCommit); + assertThat(commitString).contains("gpgsig MY FANCY SIGNATURE"); + } + + @Test + void canLocateSigningKey() throws CanceledException { + assertThat(signer.canLocateSigningKey("foo", personIdent, credentialsProvider)).isTrue(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitCommitCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitCommitCommand.java index 01b0112b72..d17267b056 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitCommitCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitCommitCommand.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.client.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -32,6 +32,8 @@ import org.eclipse.jgit.revwalk.RevCommit; import sonia.scm.repository.Changeset; import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.GitChangesetConverterFactory; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.client.api.RepositoryClientException; //~--- JDK imports ------------------------------------------------------------ @@ -71,7 +73,8 @@ public class GitCommitCommand implements CommitCommand @Override public Changeset commit(CommitRequest request) throws IOException { - try (GitChangesetConverter converter = new GitChangesetConverter(git.getRepository())) + GitChangesetConverterFactory converterFactory = GitTestHelper.createConverterFactory(); + try (GitChangesetConverter converter = converterFactory.create(git.getRepository())) { RevCommit commit = git.commit() .setAuthor(request.getAuthor().getName(), request.getAuthor().getMail()) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitMergeCommand.java index 464d32eba6..df8519fc1b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitMergeCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitMergeCommand.java @@ -32,6 +32,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import sonia.scm.repository.Changeset; import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.client.api.RepositoryClientException; import java.io.IOException; @@ -46,7 +47,7 @@ public class GitMergeCommand implements MergeCommand { @Override public Changeset merge(MergeRequest request) throws IOException { - try (GitChangesetConverter converter = new GitChangesetConverter(git.getRepository())) { + try (GitChangesetConverter converter = GitTestHelper.createConverterFactory().create(git.getRepository())) { ObjectId resolved = git.getRepository().resolve(request.getBranch()); org.eclipse.jgit.api.MergeCommand mergeCommand = git.merge() .include(request.getBranch(), resolved) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java index 01477e6bd9..208d9a16bf 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -40,7 +40,9 @@ import org.junit.Before; import org.junit.Rule; import org.junit.rules.TemporaryFolder; import sonia.scm.repository.Changeset; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.Repository; import sonia.scm.user.User; import sonia.scm.user.UserTestData; @@ -110,23 +112,7 @@ public class AbstractRemoteCommandTestBase { // store reference to handle weak references - proto = new ScmTransportProtocol(new Provider() - { - - @Override - public HookEventFacade get() - { - return null; - } - }, new Provider() - { - - @Override - public GitRepositoryHandler get() - { - return null; - } - }); + proto = new ScmTransportProtocol(GitTestHelper::createConverterFactory, () -> null, () -> null); Transport.register(proto); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java index cd2d537038..f859efecae 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java @@ -21,13 +21,15 @@ * 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.ScmTransportProtocol; import org.eclipse.jgit.transport.Transport; import org.junit.rules.ExternalResource; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.HookContextFactory; @@ -42,12 +44,12 @@ public class BindTransportProtocolRule extends ExternalResource { private ScmTransportProtocol scmTransportProtocol; @Override - protected void before() throws Throwable { + protected void before() { HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); RepositoryManager repositoryManager = mock(RepositoryManager.class); HookEventFacade hookEventFacade = new HookEventFacade(of(repositoryManager), hookContextFactory); GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); - scmTransportProtocol = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)); + scmTransportProtocol = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler)); Transport.register(scmTransportProtocol); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index 7a9e9f660e..fea4a33f15 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -24,7 +24,6 @@ package sonia.scm.repository.spi; -import org.assertj.core.api.Assertions; import org.junit.Test; import java.io.ByteArrayOutputStream; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java index 0ffa917a14..5cfab8de24 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java @@ -32,6 +32,8 @@ import org.junit.Ignore; import org.junit.Test; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.GitChangesetConverterFactory; +import sonia.scm.repository.GitTestHelper; import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; @@ -173,14 +175,11 @@ public class GitIncomingCommandTest assertEquals(0, cpr.getTotal()); } - /** - * Method description - * - * - * @return - */ - private GitIncomingCommand createCommand() - { - return new GitIncomingCommand(handler, new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()))); + private GitIncomingCommand createCommand() { + return new GitIncomingCommand( + new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), + handler, + GitTestHelper.createConverterFactory() + ); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandAncestorTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandAncestorTest.java index 6fcb9303cb..38c1516187 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandAncestorTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandAncestorTest.java @@ -27,6 +27,8 @@ package sonia.scm.repository.spi; import org.junit.Test; import sonia.scm.NotFoundException; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.GitChangesetConverterFactory; +import sonia.scm.repository.GitTestHelper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -108,8 +110,7 @@ public class GitLogCommandAncestorTest extends AbstractGitCommandTestBase createCommand().getChangesets(request); } - private GitLogCommand createCommand() - { - return new GitLogCommand(createContext()); + private GitLogCommand createCommand() { + return new GitLogCommand(createContext(), GitTestHelper.createConverterFactory()); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index 2a8c40db58..b15c6ecebd 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -31,7 +31,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.Modifications; import sonia.scm.repository.Person; @@ -293,8 +295,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase return new File(repositoryDirectory, "HEAD"); } - private GitLogCommand createCommand() - { - return new GitLogCommand(createContext()); + private GitLogCommand createCommand() { + return new GitLogCommand(createContext(), GitTestHelper.createConverterFactory()); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index 0bf61a739b..716b172981 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -29,17 +29,24 @@ import com.github.sdorra.shiro.SubjectAware; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.Assertions; import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.repository.Added; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.Person; import sonia.scm.repository.api.MergeCommandResult; @@ -68,6 +75,11 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { @Rule public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + @BeforeClass + public static void setSigner() { + GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); + } + @Test public void shouldDetectMergeableBranches() { GitMergeCommand command = createCommand(); @@ -419,6 +431,48 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { command.dryRun(request); } + @Test + public void shouldSignMergeCommit() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("empty_merge"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getRawGpgSignature()).isNotEmpty(); + assertThat(mergeCommit.getRawGpgSignature()).isEqualTo(GitTestHelper.SimpleGpgSigner.getSignature()); + + } + + @Test + public void shouldNotSignMergeCommitIfSigningIsDisabled() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("empty_merge"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setSign(false); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getRawGpgSignature()).isNullOrEmpty(); + + } + private GitMergeCommand createCommand() { return createCommand(git -> { }); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index e72ad1af7e..473356d8bf 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -27,23 +27,33 @@ package sonia.scm.repository.spi; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.rules.TemporaryFolder; import sonia.scm.AlreadyExistsException; import sonia.scm.BadRequestException; import sonia.scm.ConcurrentModificationException; import sonia.scm.NotFoundException; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.Person; import sonia.scm.repository.work.NoneCachingWorkingCopyPool; import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.security.PublicKey; import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.File; @@ -65,6 +75,11 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + @BeforeClass + public static void setSigner() { + GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); + } + @Test public void shouldCreateCommit() throws IOException, GitAPIException { File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); @@ -306,6 +321,48 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { command.execute(request); } + @Test + public void shouldSignCreatedCommit() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + try (Git git = new Git(createContext().open())) { + + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getRawGpgSignature()).isNotEmpty(); + assertThat(lastCommit.getRawGpgSignature()).isEqualTo(GitTestHelper.SimpleGpgSigner.getSignature()); + } + } + + @Test + public void shouldNotSignCreatedCommitIfSigningDisabled() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.setSign(false); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + try (Git git = new Git(createContext().open())) { + + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getRawGpgSignature()).isNullOrEmpty(); + } + } + private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { try (Git git = new Git(createContext().open())) { RevCommit lastCommit = getLastCommit(git); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java index 5c7a491c72..22ccb94e1c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java @@ -31,6 +31,8 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Test; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.GitChangesetConverterFactory; +import sonia.scm.repository.GitTestHelper; import sonia.scm.store.InMemoryConfigurationStoreFactory; import java.io.IOException; @@ -151,6 +153,10 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase */ private GitOutgoingCommand createCommand() { - return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()))); + return new GitOutgoingCommand( + new GitContext(outgoingDirectory, outgoingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())), + handler, + GitTestHelper.createConverterFactory() + ); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java new file mode 100644 index 0000000000..aac6eaef49 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java @@ -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 com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +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.repository.GitRepositoryHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class GitRepositoryServiceProviderTest { + + @Mock + private GitRepositoryHandler handler; + + @Mock + private GitContext context; + + @Test + void shouldCreatePushCommand() { + GitRepositoryServiceProvider provider = createProvider(); + PushCommand pushCommand = provider.getPushCommand(); + assertThat(pushCommand).isNotNull().isInstanceOf(GitPushCommand.class); + } + + @Test + void shouldDelegateCloseToContext() { + createProvider().close(); + verify(context).close(); + } + + private GitRepositoryServiceProvider createProvider() { + return new GitRepositoryServiceProvider(createParentInjector(), context); + } + + private Injector createParentInjector() { + return Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(GitRepositoryHandler.class).toInstance(handler); + } + }); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceResolverTest.java new file mode 100644 index 0000000000..6c2de8b7b6 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceResolverTest.java @@ -0,0 +1,63 @@ +/* + * 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.inject.Injector; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.RepositoryTestData; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class GitRepositoryServiceResolverTest { + + @Mock + private Injector injector; + + @Mock + private GitContextFactory contextFactory; + + @InjectMocks + private GitRepositoryServiceResolver resolver; + + @Test + void shouldCreateRepositoryServiceProvider() { + GitRepositoryServiceProvider provider = resolver.resolve(RepositoryTestData.createHeartOfGold("git")); + assertThat(provider).isNotNull(); + } + + @ParameterizedTest + @ValueSource(strings = { "hg","svn", "unknown"}) + void shouldReturnNullForNonGitRepositories(String type) { + GitRepositoryServiceProvider provider = resolver.resolve(RepositoryTestData.createHeartOfGold(type)); + assertThat(provider).isNull(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java index 9a32b5d01c..2d814bba2a 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java @@ -34,7 +34,9 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.HookContextFactory; @@ -65,7 +67,7 @@ public class SimpleGitWorkingCopyFactoryTest extends AbstractGitCommandTestBase HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory); GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); - proto = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)); + proto = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler)); Transport.register(proto); workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); } diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 94eec28a16..5754f126fd 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -1716,7 +1716,7 @@ exports[`Storyshots CardColumnSmall Minimal 1`] = ` exports[`Storyshots Changesets Co-Authors with avatar 1`] = `
-

- changeset.contributors.authoredBy - - - SCM Administrator - - - commaSeparatedList.lastDivider - - changeset.contributors.coAuthoredBy - - + changeset.contributors.authoredBy + - Ford Prefect + SCM Administrator - - Zaphod Beeblebrox - - - Tricia Marie McMillan - - - -

+ + Ford Prefect + + + Zaphod Beeblebrox + + + Tricia Marie McMillan + + + +

+
@@ -1882,7 +1886,7 @@ exports[`Storyshots Changesets Co-Authors with avatar 1`] = ` exports[`Storyshots Changesets Commiter and Co-Authors with avatar 1`] = `
-

- changeset.contributors.authoredBy - - - SCM Administrator - - , - changeset.contributors.committedBy - - - Zaphod Beeblebrox - - - commaSeparatedList.lastDivider - - changeset.contributors.coAuthoredBy - - - Ford Prefect - - -

+ changeset.contributors.authoredBy + + + SCM Administrator + + , + changeset.contributors.committedBy + + + Zaphod Beeblebrox + + + commaSeparatedList.lastDivider + + changeset.contributors.coAuthoredBy + + + Ford Prefect + + +

+
@@ -2037,7 +2045,7 @@ exports[`Storyshots Changesets Commiter and Co-Authors with avatar 1`] = ` exports[`Storyshots Changesets Default 1`] = `
-

- changeset.contributors.authoredBy - - - SCM Administrator - -

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
@@ -2150,7 +2162,7 @@ exports[`Storyshots Changesets Default 1`] = ` exports[`Storyshots Changesets Replacements 1`] = `
-

- changeset.contributors.authoredBy - - - SCM Administrator - -

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
@@ -2273,7 +2289,7 @@ exports[`Storyshots Changesets Replacements 1`] = ` exports[`Storyshots Changesets With Committer 1`] = `
-

- changeset.contributors.authoredBy - - - SCM Administrator - - - commaSeparatedList.lastDivider - - changeset.contributors.committedBy - - - Zaphod Beeblebrox - - -

+ changeset.contributors.authoredBy + + + SCM Administrator + + + commaSeparatedList.lastDivider + + changeset.contributors.committedBy + + + Zaphod Beeblebrox + + +

+
@@ -2398,7 +2418,7 @@ exports[`Storyshots Changesets With Committer 1`] = ` exports[`Storyshots Changesets With Committer and Co-Author 1`] = `
-

- changeset.contributors.authoredBy - - - SCM Administrator - - , - changeset.contributors.committedBy - - - Zaphod Beeblebrox - - - commaSeparatedList.lastDivider - - changeset.contributors.coAuthoredBy - - - Ford Prefect - - -

+ changeset.contributors.authoredBy + + + SCM Administrator + + , + changeset.contributors.committedBy + + + Zaphod Beeblebrox + + + commaSeparatedList.lastDivider + + changeset.contributors.coAuthoredBy + + + Ford Prefect + + +

+
@@ -2532,7 +2556,7 @@ exports[`Storyshots Changesets With Committer and Co-Author 1`] = ` exports[`Storyshots Changesets With avatar 1`] = `
-

- changeset.contributors.authoredBy - - - SCM Administrator - -

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+
+ + +
+
+ +
+
+

+ +

+

+ +

+
+
+ + + +`; + +exports[`Storyshots Changesets With contactless signature 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With invalid signature 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
@@ -2658,7 +2936,7 @@ exports[`Storyshots Changesets With avatar 1`] = ` exports[`Storyshots Changesets With multiple Co-Authors 1`] = `
-

- changeset.contributors.authoredBy - - - SCM Administrator - - - commaSeparatedList.lastDivider - - changeset.contributors.coAuthoredBy - - + SCM Administrator + + + commaSeparatedList.lastDivider + + changeset.contributors.coAuthoredBy + + + changeset.contributors.more + + +

+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With multiple signatures and invalid status 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

- changeset.contributors.more - - -

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With multiple signatures and not found status 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With multiple signatures and valid status 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With unknown signature 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With unowned signature 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With valid signature 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
@@ -43676,6 +44708,24 @@ exports[`Storyshots Layout|Footer Default 1`] = ` footer.user.profile +
  • + + profile.changePasswordNavLink + +
  • +
  • + + footer.user.profile + +
  • +
  • + + footer.user.profile + +
  • +
  • + + profile.changePasswordNavLink + +
  • +
  • + + profile.changePasswordNavLink + +
  • +
  • + + footer.user.profile + +
  • +
  • + + footer.user.profile + +
  • +
  • + + profile.changePasswordNavLink + +
  • `; +exports[`Storyshots Popover Default 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots Popover Link 1`] = ` +
    +`; + exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
    = ({ me, version, links }) => {
    + + {me?._links?.password && } + {me?._links?.publicKeys && } }> diff --git a/scm-ui/ui-components/src/popover/Popover.stories.tsx b/scm-ui/ui-components/src/popover/Popover.stories.tsx new file mode 100644 index 0000000000..c5fac9bcfa --- /dev/null +++ b/scm-ui/ui-components/src/popover/Popover.stories.tsx @@ -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 { storiesOf } from "@storybook/react"; +import React from "react"; +import styled from "styled-components"; +import usePopover from "./usePopover"; +import Popover from "./Popover"; + +const Wrapper = styled.div` + width: 100%; + margin: 20rem; +`; + +storiesOf("Popover", module) + .addDecorator(storyFn => {storyFn()}) + .add("Default", () => React.createElement(() => { + const { triggerProps, popoverProps } = usePopover(); + + return ( +
    + Spaceship Heart of Gold} width={512} {...popoverProps}> +

    + The Heart of Gold is the sleekest, most advanced, coolest spaceship in the galaxy. Its stunning good looks + mirror its awesome speed and power. It is powered by the revolutionary new Infinite Improbability Drive, + which lets the ship pass through every point in every universe simultaneously. +

    +
    + +
    + ); + })) + .add("Link", () => React.createElement(() => { + const { triggerProps, popoverProps } = usePopover(); + + return ( +
    + Spaceship Heart of Gold} width={512} {...popoverProps}> +

    + The Heart of Gold is the sleekest, most advanced, coolest spaceship in the galaxy. Its stunning good looks + mirror its awesome speed and power. It is powered by the revolutionary new Infinite Improbability Drive, + which lets the ship pass through every point in every universe simultaneously. +

    +
    + + Trigger + +
    + ); + })); diff --git a/scm-ui/ui-components/src/popover/Popover.tsx b/scm-ui/ui-components/src/popover/Popover.tsx new file mode 100644 index 0000000000..797d9aef53 --- /dev/null +++ b/scm-ui/ui-components/src/popover/Popover.tsx @@ -0,0 +1,128 @@ +/* + * 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, { Dispatch, FC, ReactNode, useLayoutEffect, useRef, useState } from "react"; +import { Action } from "./usePopover"; +import styled from "styled-components"; + +type Props = { + title: ReactNode; + width?: number; + // props should be defined by usePopover + offsetTop?: number; + offsetLeft?: number; + show: boolean; + dispatch: Dispatch; +}; + +type ContainerProps = { + width: number; +}; + +const PopoverContainer = styled.div` + position: absolute; + z-index: 100; + width: ${props => props.width}px; + display: block; + + &:before { + position: absolute; + content: ""; + border-style: solid; + pointer-events: none; + height: 0; + width: 0; + top: 100%; + left: ${props => props.width / 2}px; + border-color: transparent; + border-bottom-color: white; + border-left-color: white; + border-width: 0.4rem; + margin-left: -0.4rem; + margin-top: -0.4rem; + -webkit-transform-origin: center; + transform-origin: center; + transform: rotate(-45deg); + } +`; + +const SmallHr = styled.hr` + margin: 0.5em 0; +`; + +const PopoverHeading = styled.div` + height: 1.5em; +`; + +const Popover: FC = props => { + if (!props.show) { + return null; + } + return ; +}; + +const InnerPopover: FC = ({ title, show, width, offsetTop, offsetLeft, dispatch, children }) => { + const [height, setHeight] = useState(125); + const ref = useRef(null); + useLayoutEffect(() => { + if (ref.current) { + setHeight(ref.current.clientHeight); + } + }, [ref]); + + const onMouseEnter = () => { + dispatch({ + type: "enter-popover" + }); + }; + + const onMouseLeave = () => { + dispatch({ + type: "leave-popover" + }); + }; + + const top = (offsetTop || 0) - height - 5; + const left = (offsetLeft || 0) - width! / 2; + return ( + + {title} + + {children} + + ); +}; + +Popover.defaultProps = { + width: 120 +}; + +export default Popover; diff --git a/scm-ui/ui-components/src/popover/index.ts b/scm-ui/ui-components/src/popover/index.ts new file mode 100644 index 0000000000..d768334db1 --- /dev/null +++ b/scm-ui/ui-components/src/popover/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { default as Popover } from "./Popover"; +export { default as usePopover } from "./usePopover"; diff --git a/scm-ui/ui-components/src/popover/usePopover.ts b/scm-ui/ui-components/src/popover/usePopover.ts new file mode 100644 index 0000000000..db73194916 --- /dev/null +++ b/scm-ui/ui-components/src/popover/usePopover.ts @@ -0,0 +1,137 @@ +/* + * 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 { Dispatch, useReducer, useRef } from "react"; + +type EnterTrigger = { + type: "enter-trigger"; + offsetTop: number; + offsetLeft: number; +}; + +type LeaveTrigger = { + type: "leave-trigger"; +}; + +type EnterPopover = { + type: "enter-popover"; +}; + +type LeavePopover = { + type: "leave-popover"; +}; + +export type Action = EnterTrigger | LeaveTrigger | EnterPopover | LeavePopover; + +type State = { + offsetTop?: number; + offsetLeft?: number; + onPopover: boolean; + onTrigger: boolean; +}; + +const initialState = { + onPopover: false, + onTrigger: false +}; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "enter-trigger": { + if (state.onPopover) { + return state; + } + return { + offsetTop: action.offsetTop, + offsetLeft: action.offsetLeft, + onTrigger: true, + onPopover: false + }; + } + case "leave-trigger": { + if (state.onPopover) { + return { + ...state, + onTrigger: false + }; + } + return initialState; + } + case "enter-popover": { + return { + ...state, + onPopover: true + }; + } + case "leave-popover": { + if (state.onTrigger) { + return { + ...state, + onPopover: false + }; + } + return initialState; + } + } +}; + +const dispatchDeferred = (dispatch: Dispatch, action: Action) => { + setTimeout(() => dispatch(action), 250); +}; + +const usePopover = () => { + const [state, dispatch] = useReducer(reducer, initialState); + const triggerRef = useRef(null); + + const onMouseOver = () => { + const current = triggerRef.current!; + dispatchDeferred(dispatch, { + type: "enter-trigger", + offsetTop: current.offsetTop, + offsetLeft: current.offsetLeft + current.offsetWidth / 2 + }); + }; + + const onMouseLeave = () => { + dispatchDeferred(dispatch, { + type: "leave-trigger" + }); + }; + + return { + triggerProps: { + onMouseOver, + onMouseLeave, + ref: (node: HTMLElement | null) => (triggerRef.current = node) + }, + popoverProps: { + dispatch, + show: state.onPopover || state.onTrigger, + offsetTop: state.offsetTop, + offsetLeft: state.offsetLeft + } + }; +}; + +export default usePopover; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx index 543de073db..47823b71fe 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx @@ -35,6 +35,7 @@ import ChangesetAuthor from "./ChangesetAuthor"; import ChangesetTags from "./ChangesetTags"; import ChangesetButtonGroup from "./ChangesetButtonGroup"; import ChangesetDescription from "./ChangesetDescription"; +import SignatureIcon from "./SignatureIcon"; type Props = WithTranslation & { repository: Repository; @@ -79,6 +80,11 @@ const VCenteredChildColumn = styled.div` justify-content: flex-end; `; +const FlexRow = styled.div` + display: flex; + flex-direction: row; +`; + class ChangesetRow extends React.Component { createChangesetId = (changeset: Changeset) => { const { repository } = this.props; @@ -124,9 +130,17 @@ class ChangesetRow extends React.Component {

    - - - + + + + + {changeset?.signatures && changeset.signatures.length > 0 && ( + + )} +
    diff --git a/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx b/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx index 2c4f1e48f3..29fb474004 100644 --- a/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx +++ b/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx @@ -22,22 +22,22 @@ * SOFTWARE. */ -import { storiesOf } from "@storybook/react"; +import {storiesOf} from "@storybook/react"; import * as React from "react"; import styled from "styled-components"; -import { MemoryRouter } from "react-router-dom"; +import {MemoryRouter} from "react-router-dom"; import repository from "../../__resources__/repository"; import ChangesetRow from "./ChangesetRow"; -import { one, two, three, four, five } from "../../__resources__/changesets"; -import { Binder, BinderContext } from "@scm-manager/ui-extensions"; +import {one, two, three, four, five} from "../../__resources__/changesets"; +import {Binder, BinderContext} from "@scm-manager/ui-extensions"; // @ts-ignore import hitchhiker from "../../__resources__/hitchhiker.png"; -import { Person } from "../../avatar/Avatar"; -import { Changeset } from "@scm-manager/ui-types"; -import { Replacement } from "../../SplitAndReplace"; +import {Person} from "../../avatar/Avatar"; +import {Changeset} from "@scm-manager/ui-types"; +import {Replacement} from "../../SplitAndReplace"; const Wrapper = styled.div` - margin: 2rem; + margin: 25rem 4rem; `; const robohash = (person: Person) => { @@ -49,7 +49,7 @@ const withAvatarFactory = (factory: (person: Person) => string, changeset: Chang binder.bind("avatar.factory", factory); return ( - + ); }; @@ -62,18 +62,22 @@ const withReplacements = ( replacements.forEach(replacement => binder.bind("changeset.description.tokens", replacement)); return ( - + ); }; +function copy(input: T): T { + return JSON.parse(JSON.stringify(input)); +} + storiesOf("Changesets", module) .addDecorator(story => {story()}) .addDecorator(storyFn => {storyFn()}) - .add("Default", () => ) - .add("With Committer", () => ) - .add("With Committer and Co-Author", () => ) - .add("With multiple Co-Authors", () => ) + .add("Default", () => ) + .add("With Committer", () => ) + .add("With Committer and Co-Author", () => ) + .add("With multiple Co-Authors", () => ) .add("With avatar", () => { return withAvatarFactory(() => hitchhiker, three); }) @@ -88,9 +92,156 @@ storiesOf("Changesets", module) const mail = Arthur; return withReplacements( [ - () => [{ textToReplace: "HOG-42", replacement: link }], - () => [{ textToReplace: "arthur@guide.galaxy", replacement: mail }] + () => [{textToReplace: "HOG-42", replacement: link}], + () => [{textToReplace: "arthur@guide.galaxy", replacement: mail}] ], five ); + }) + .add("With unknown signature", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "NOT_FOUND" + }]; + return ; + }) + .add("With valid signature", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "VERIFIED", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; + }) + .add("With unowned signature", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "VERIFIED", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; + }) + .add("With contactless signature", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "VERIFIED", + owner: "trillian" + }]; + return ; + }) + .add("With invalid signature", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "INVALID", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; + }) + .add("With multiple signatures and invalid status", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x912389FJIQW8W223", + type: "gpg", + status: "INVALID", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "VERIFIED", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x9123891239VFIA33", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; + }) + .add("With multiple signatures and valid status", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x912389FJIQW8W223", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "VERIFIED", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x9123891239VFIA33", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; + }) + .add("With multiple signatures and not found status", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x912389FJIQW8W223", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x9123891239VFIA33", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; }); diff --git a/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx new file mode 100644 index 0000000000..937e19bace --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx @@ -0,0 +1,119 @@ +/* + * 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"; +import {Signature} from "@scm-manager/ui-types"; +import styled from "styled-components"; +import Icon from "../../Icon"; +import {usePopover} from "../../popover"; +import Popover from "../../popover/Popover"; +import classNames from "classnames"; + +type Props = { + signatures: Signature[]; + className: any; +}; + +const StyledIcon = styled(Icon)` + width: 1em; + height: 1em; + vertical-align: middle; + border-radius: 0.25em; + margin-bottom: 0.2em; +`; + +const StyledDiv = styled.div` + > *:not(:last-child) { + margin-bottom: 24px; + } +`; + +const SignatureIcon: FC = ({signatures, className}) => { + const [t] = useTranslation("repos"); + const {popoverProps, triggerProps} = usePopover(); + + if (!signatures.length) { + return null; + } + + const getColor = (signaturesToVerify: Signature[]) => { + const invalid = signaturesToVerify.some(sig => sig.status === "INVALID"); + if (invalid) { + return "danger"; + } + const verified = signaturesToVerify.some(sig => sig.status === "VERIFIED"); + if (verified) { + return "success"; + } + return undefined; + }; + + const createSignatureBlock = (signature: Signature) => { + let status; + if (signature.status === "VERIFIED") { + status = t("changeset.signatureVerified"); + } else if (signature.status === "INVALID") { + status = t("changeset.signatureInvalid"); + } else { + status = t("changeset.signatureNotVerified"); + } + + if (signature.status === "NOT_FOUND") { + return

    +

    {t("changeset.keyId")}: {signature.keyId}
    +
    {t("changeset.signatureStatus")}: {status}
    +

    ; + } + + return

    +

    {t("changeset.keyId")}: { + signature._links?.rawKey ? {signature.keyId} : signature.keyId + }
    +
    {t("changeset.signatureStatus")}: {status}
    +
    {t("changeset.keyOwner")}: {signature.owner || t("changeset.noOwner")}
    + {signature.contacts && signature.contacts.length > 0 && <> +
    {t("changeset.keyContacts")}:
    + {signature.contacts && signature.contacts.map(contact => +
    - {contact.name}{contact.mail && ` <${contact.mail}>`}
    )} + } +

    ; + }; + + const signatureElements = signatures.map(signature => createSignatureBlock(signature)); + + return ( + <> + {t("changeset.signatures")}} width={500} {...popoverProps}> + + {signatureElements} + + +
    + +
    + + ); +}; + +export default SignatureIcon; diff --git a/scm-ui/ui-components/src/repos/changesets/index.ts b/scm-ui/ui-components/src/repos/changesets/index.ts index 3fb6b39c72..5c3324de26 100644 --- a/scm-ui/ui-components/src/repos/changesets/index.ts +++ b/scm-ui/ui-components/src/repos/changesets/index.ts @@ -36,3 +36,4 @@ export { default as ChangesetTag } from "./ChangesetTag"; export { default as ChangesetTags } from "./ChangesetTags"; export { default as ChangesetTagsCollapsed } from "./ChangesetTagsCollapsed"; export { default as ContributorAvatar } from "./ContributorAvatar"; +export { default as SignatureIcon } from "./SignatureIcon"; diff --git a/scm-ui/ui-types/src/Changesets.ts b/scm-ui/ui-types/src/Changesets.ts index f182da296c..81acd09c92 100644 --- a/scm-ui/ui-types/src/Changesets.ts +++ b/scm-ui/ui-types/src/Changesets.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { Collection, Links } from "./hal"; +import {Collection, Link, Links} from "./hal"; import { Tag } from "./Tags"; import { Branch } from "./Branches"; import { Person } from "./Person"; @@ -33,6 +33,7 @@ export type Changeset = Collection & { author: Person; description: string; contributors?: Contributor[]; + signatures?: Signature[]; _links: Links; _embedded: { tags?: Tag[]; @@ -41,6 +42,17 @@ export type Changeset = Collection & { }; }; +export type Signature = { + keyId: string; + type: string; + status: "VERIFIED" | "NOT_FOUND" | "INVALID"; + owner?: string; + contacts?: Person[]; + _links?: { + rawKey?: Link; + }; +} + export type Contributor = { person: Person; type: string; diff --git a/scm-ui/ui-types/src/index.ts b/scm-ui/ui-types/src/index.ts index 6fc5205662..cdaa2fe416 100644 --- a/scm-ui/ui-types/src/index.ts +++ b/scm-ui/ui-types/src/index.ts @@ -36,7 +36,7 @@ export { Branch, BranchRequest } from "./Branches"; export { Person } from "./Person"; -export { Changeset, Contributor, ParentChangeset } from "./Changesets"; +export { Changeset, Contributor, ParentChangeset, Signature } from "./Changesets"; export { AnnotatedSource, AnnotatedLine } from "./Annotate"; diff --git a/scm-ui/ui-webapp/public/locales/de/commons.json b/scm-ui/ui-webapp/public/locales/de/commons.json index 12204387a7..f11e606970 100644 --- a/scm-ui/ui-webapp/public/locales/de/commons.json +++ b/scm-ui/ui-webapp/public/locales/de/commons.json @@ -70,6 +70,7 @@ "navigationLabel": "Profil", "informationNavLink": "Information", "changePasswordNavLink": "Passwort ändern", + "publicKeysNavLink": "Öffentliche Schlüssel", "settingsNavLink": "Einstellungen", "username": "Benutzername", "displayName": "Anzeigename", diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index a0f37eefdf..68e28e5d35 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -88,6 +88,15 @@ "shortSummary": "Committet <0/> <1/>", "tags": "Tags", "diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt", + "keyOwner": "Schlüssel Besitzer", + "signatureStatus": "Status", + "keyId": "Schlüssel-ID", + "keyContacts": "Kontakte", + "noOwner": "Unbekannt", + "signatureVerified": "Verifiziert", + "signatureNotVerified": "Nicht verifiziert", + "signatureInvalid": "Ungültig", + "signatures": "Signaturen", "shortlink": { "title": "Changeset {{id}} aus {{namespace}}/{{name}}" }, diff --git a/scm-ui/ui-webapp/public/locales/de/users.json b/scm-ui/ui-webapp/public/locales/de/users.json index fd744e768d..825876ec47 100644 --- a/scm-ui/ui-webapp/public/locales/de/users.json +++ b/scm-ui/ui-webapp/public/locales/de/users.json @@ -37,7 +37,8 @@ "settingsNavLink": "Einstellungen", "generalNavLink": "Generell", "setPasswordNavLink": "Passwort", - "setPermissionsNavLink": "Berechtigungen" + "setPermissionsNavLink": "Berechtigungen", + "setPublicKeyNavLink": "Öffentliche Schlüssel" } }, "createUser": { @@ -60,5 +61,14 @@ "userForm": { "subtitle": "Benutzer bearbeiten", "button": "Speichern" + }, + "publicKey": { + "noStoredKeys": "Es wurden keine Schlüssel gefunden.", + "displayName": "Anzeigename", + "raw": "Schlüssel", + "created": "Eingetragen an", + "addKey": "Schlüssel hinzufügen", + "delete": "Löschen", + "download": "Herunterladen" } } diff --git a/scm-ui/ui-webapp/public/locales/en/commons.json b/scm-ui/ui-webapp/public/locales/en/commons.json index 1a7a7e9733..da4a68f7b2 100644 --- a/scm-ui/ui-webapp/public/locales/en/commons.json +++ b/scm-ui/ui-webapp/public/locales/en/commons.json @@ -72,6 +72,7 @@ "informationNavLink": "Information", "changePasswordNavLink": "Change password", "settingsNavLink": "Settings", + "publicKeysNavLink": "Public Keys", "username": "Username", "displayName": "Display Name", "mail": "E-Mail", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index f4b9c7c89b..d6e003a499 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -87,6 +87,15 @@ "summary": "Changeset <0/> was committed <1/>", "shortSummary": "Committed <0/> <1/>", "tags": "Tags", + "keyOwner": "Key Owner", + "keyId": "Key ID", + "keyContacts": "Contacts", + "noOwner": "Unknown", + "signatureStatus": "Status", + "signatureVerified": "verified", + "signatureNotVerified": "not verified", + "signatureInvalid": "invalid", + "signatures": "Signatures", "shortlink": { "title": "Changeset {{id}} of {{namespace}}/{{name}}" }, diff --git a/scm-ui/ui-webapp/public/locales/en/users.json b/scm-ui/ui-webapp/public/locales/en/users.json index 3353977a18..d50618bcf0 100644 --- a/scm-ui/ui-webapp/public/locales/en/users.json +++ b/scm-ui/ui-webapp/public/locales/en/users.json @@ -37,7 +37,8 @@ "settingsNavLink": "Settings", "generalNavLink": "General", "setPasswordNavLink": "Password", - "setPermissionsNavLink": "Permissions" + "setPermissionsNavLink": "Permissions", + "setPublicKeyNavLink": "Public Keys" } }, "createUser": { @@ -60,5 +61,14 @@ "userForm": { "subtitle": "Edit User", "button": "Submit" + }, + "publicKey": { + "noStoredKeys": "No keys found.", + "displayName": "Display Name", + "raw": "Key", + "created": "Created on", + "addKey": "Add key", + "delete": "Delete", + "download": "Download" } } diff --git a/scm-ui/ui-webapp/src/containers/Profile.tsx b/scm-ui/ui-webapp/src/containers/Profile.tsx index fd7118f7bb..4fe7bd4b61 100644 --- a/scm-ui/ui-webapp/src/containers/Profile.tsx +++ b/scm-ui/ui-webapp/src/containers/Profile.tsx @@ -42,6 +42,8 @@ import { import ChangeUserPassword from "./ChangeUserPassword"; import ProfileInfo from "./ProfileInfo"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys"; +import SetPublicKeyNavLink from "../users/components/navLinks/SetPublicKeysNavLink"; type Props = RouteComponentProps & WithTranslation & { @@ -66,7 +68,12 @@ class Profile extends React.Component { mayChangePassword = () => { const { me } = this.props; return !!me?._links?.password; - } + }; + + canManagePublicKeys = () => { + const { me } = this.props; + return !!me?._links?.publicKeys; + }; render() { const url = this.matchedUrl(); @@ -100,6 +107,9 @@ class Profile extends React.Component { {this.mayChangePassword() && ( } /> )} + {this.canManagePublicKeys() && ( + } /> + )} @@ -117,6 +127,7 @@ class Profile extends React.Component { title={t("profile.settingsNavLink")} > + )} diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx index 32f000a8c5..8e01cf67e7 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx @@ -26,7 +26,7 @@ import { Trans, useTranslation, WithTranslation, withTranslation } from "react-i import classNames from "classnames"; import styled from "styled-components"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; -import { Changeset, Repository, Tag, ParentChangeset } from "@scm-manager/ui-types"; +import { Changeset, ParentChangeset, Repository, Tag } from "@scm-manager/ui-types"; import { AvatarImage, AvatarWrapper, @@ -38,12 +38,12 @@ import { changesets, ChangesetTag, DateFromNow, - Level, - Icon + Icon, + Level } from "@scm-manager/ui-components"; import ContributorTable from "./ContributorTable"; import { Link as ReactLink } from "react-router-dom"; -import { FileControlFactory } from "@scm-manager/ui-components"; +import { FileControlFactory, SignatureIcon } from "@scm-manager/ui-components"; type Props = WithTranslation & { changeset: Changeset; @@ -65,6 +65,10 @@ const TagsWrapper = styled.div` } `; +const SignedIcon = styled(SignatureIcon)` + padding-left: 1rem; +`; + const BottomMarginLevel = styled(Level)` margin-bottom: 1rem !important; `; @@ -76,6 +80,11 @@ const countContributors = (changeset: Changeset) => { return 1; }; +const FlexRow = styled.div` + display: flex; + flex-direction: row; +`; + const ContributorLine = styled.div` display: flex; cursor: pointer; @@ -122,12 +131,19 @@ const SeparatedParents = styled.div` const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => { const [t] = useTranslation("repos"); const [open, setOpen] = useState(false); + const signatureIcon = changeset?.signatures && changeset.signatures.length > 0 && ( + + ); + if (open) { return ( - setOpen(!open)}> - {t("changeset.contributors.list")} - + + setOpen(!open)} className="is-ellipsis-overflow"> + {t("changeset.contributors.list")} + + {signatureIcon} + ); @@ -136,9 +152,10 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => { return ( <> setOpen(!open)}> - + + {signatureIcon} ( @@ -193,9 +210,9 @@ class ChangesetDetails extends React.Component { -
    +
    - +

    diff --git a/scm-ui/ui-webapp/src/users/components/navLinks/SetPublicKeysNavLink.tsx b/scm-ui/ui-webapp/src/users/components/navLinks/SetPublicKeysNavLink.tsx new file mode 100644 index 0000000000..f3144442f5 --- /dev/null +++ b/scm-ui/ui-webapp/src/users/components/navLinks/SetPublicKeysNavLink.tsx @@ -0,0 +1,43 @@ +/* + * 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 { Link, User, Me } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; + +type Props = { + user: User | Me; + publicKeyUrl: string; +}; + +const SetPublicKeyNavLink: FC = ({ user, publicKeyUrl }) => { + const [t] = useTranslation("users"); + + if ((user?._links?.publicKeys as Link)?.href) { + return ; + } + return null; +}; + +export default SetPublicKeyNavLink; diff --git a/scm-ui/ui-webapp/src/users/components/navLinks/index.ts b/scm-ui/ui-webapp/src/users/components/navLinks/index.ts index 0ccd16b42a..f732ea83ee 100644 --- a/scm-ui/ui-webapp/src/users/components/navLinks/index.ts +++ b/scm-ui/ui-webapp/src/users/components/navLinks/index.ts @@ -25,3 +25,4 @@ export { default as EditUserNavLink } from "./EditUserNavLink"; export { default as SetPasswordNavLink } from "./SetPasswordNavLink"; export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; +export { default as SetPublicKeysNavLink } from "./SetPublicKeysNavLink"; diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx new file mode 100644 index 0000000000..e0129e8245 --- /dev/null +++ b/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx @@ -0,0 +1,89 @@ +/* + * 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, useState } from "react"; +import { User, Link, Links, Collection } from "@scm-manager/ui-types/src"; +import { + ErrorNotification, + InputField, + Level, + Textarea, + SubmitButton, + apiClient, + Loading +} from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import { CONTENT_TYPE_PUBLIC_KEY } from "./SetPublicKeys"; + +type Props = { + createLink: string; + refresh: () => void; +}; + +const AddPublicKey: FC = ({ createLink, refresh }) => { + const [t] = useTranslation("users"); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const [displayName, setDisplayName] = useState(""); + const [raw, setRaw] = useState(""); + + const isValid = () => { + return !!displayName && !!raw; + }; + + const resetForm = () => { + setDisplayName(""); + setRaw(""); + }; + + const addKey = () => { + setLoading(true); + apiClient + .post(createLink, { displayName: displayName, raw: raw }, CONTENT_TYPE_PUBLIC_KEY) + .then(resetForm) + .then(refresh) + .then(() => setLoading(false)) + .catch(setError); + }; + + if (error) { + return ; + } + + if (loading) { + return ; + } + + return ( + <> + +