From a30721876ebd767416a81d969f1a17d880fb9794 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 2 Sep 2020 11:06:29 +0200 Subject: [PATCH 01/14] enhance repository initializer api with creation context --- .../RepositoryContentInitializer.java | 27 ++++++++++++- .../RepositoryCollectionResource.java | 4 +- .../v2/resources/RepositoryCreationDto.java | 39 +++++++++++++++++++ .../scm/repository/RepositoryInitializer.java | 17 +++++++- ...eadmeRepositoryContentInitializerTest.java | 2 +- 5 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java index c58ffa2402..93e21ac718 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java @@ -21,14 +21,18 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.io.ByteSource; import sonia.scm.plugin.ExtensionPoint; import java.io.IOException; import java.io.InputStream; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; /** * Use this {@link RepositoryContentInitializer} to create new files with custom content @@ -38,12 +42,20 @@ import java.io.InputStream; public interface RepositoryContentInitializer { /** - * * @param context add content to this context in order to commit files in the initial repository commit * @throws IOException */ void initialize(InitializerContext context) throws IOException; + /** + * returns the class to which the creation context will be mapped + * + * @return the class of the creation context + */ + default Optional> getType() { + return Optional.empty(); + } + /** * Use this {@link InitializerContext} to create new files on repository initialization * which will be included in the first commit @@ -57,10 +69,18 @@ public interface RepositoryContentInitializer { /** * create new file which will be included in initial repository commit + * * @param path path of new file * @return */ CreateFile create(String path); + + /** + * @return creation context of repository which is going to be initialized + */ + default Map getCreationContext() { + return Collections.emptyMap(); + } } /** @@ -70,6 +90,7 @@ public interface RepositoryContentInitializer { /** * Applies content to new file + * * @param content content of file as string * @return {@link InitializerContext} * @throws IOException @@ -78,6 +99,7 @@ public interface RepositoryContentInitializer { /** * Applies content to new file + * * @param input content of file as input stream * @return {@link InitializerContext} * @throws IOException @@ -86,6 +108,7 @@ public interface RepositoryContentInitializer { /** * Applies content to new file + * * @param byteSource content of file as byte source * @return {@link InitializerContext} * @throws IOException diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 51f632fa8c..274999d401 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -147,7 +147,7 @@ public class RepositoryCollectionResource { mediaType = VndMediaType.ERROR_TYPE, schema = @Schema(implementation = ErrorDto.class) )) - public Response create(@Valid RepositoryDto repository, @QueryParam("initialize") boolean initialize) { + public Response create(@Valid RepositoryCreationDto repository, @QueryParam("initialize") boolean initialize) { AtomicReference reference = new AtomicReference<>(); Response response = adapter.create(repository, () -> createModelObjectFromDto(repository), @@ -156,7 +156,7 @@ public class RepositoryCollectionResource { return resourceLinks.repository().self(r.getNamespace(), r.getName()); }); if (initialize) { - repositoryInitializer.initialize(reference.get()); + repositoryInitializer.initialize(reference.get(), repository.getCreationContext()); } return response; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java new file mode 100644 index 0000000000..4606ce3060 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java @@ -0,0 +1,39 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Map; + +@NoArgsConstructor +@Getter +@Setter +public class RepositoryCreationDto extends RepositoryDto { + private Map creationContext; +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java index c8ca705a9a..c866b8d7d0 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java @@ -24,6 +24,7 @@ package sonia.scm.repository; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; import org.slf4j.Logger; @@ -38,6 +39,7 @@ import javax.inject.Singleton; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.Set; @Singleton @@ -55,10 +57,14 @@ public class RepositoryInitializer { } public void initialize(Repository repository) { + initialize(repository, null); + } + + public void initialize(Repository repository, Map creationContext) { try (RepositoryService service = serviceFactory.create(repository)) { ModifyCommandBuilder modifyCommandBuilder = service.getModifyCommand(); - InitializerContextImpl initializerContext = new InitializerContextImpl(repository, modifyCommandBuilder); + InitializerContextImpl initializerContext = new InitializerContextImpl(repository, modifyCommandBuilder, creationContext); for (RepositoryContentInitializer initializer : contentInitializers) { initializer.initialize(initializerContext); @@ -77,10 +83,12 @@ public class RepositoryInitializer { private final Repository repository; private final ModifyCommandBuilder builder; + private final Map creationContext; - InitializerContextImpl(Repository repository, ModifyCommandBuilder builder) { + InitializerContextImpl(Repository repository, ModifyCommandBuilder builder, Map creationContext) { this.repository = repository; this.builder = builder; + this.creationContext = creationContext; } @Override @@ -88,6 +96,11 @@ public class RepositoryInitializer { return repository; } + @Override + public Map getCreationContext() { + return creationContext; + } + @Override public RepositoryContentInitializer.CreateFile create(String path) { return new CreateFileImpl(this, builder.useDefaultPath(true).createFile(path).setOverwrite(true)); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/ReadmeRepositoryContentInitializerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/ReadmeRepositoryContentInitializerTest.java index d7e704e76c..d1cbdf7e37 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/ReadmeRepositoryContentInitializerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/ReadmeRepositoryContentInitializerTest.java @@ -48,7 +48,7 @@ class ReadmeRepositoryContentInitializerTest { private Repository repository; - private ReadmeRepositoryContentInitializer initializer = new ReadmeRepositoryContentInitializer(); + private final ReadmeRepositoryContentInitializer initializer = new ReadmeRepositoryContentInitializer(); @BeforeEach void setUpContext() { From ee12e5ceec3cd8c7d65aace1cc683020583cfb08 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Mon, 7 Sep 2020 13:12:10 +0200 Subject: [PATCH 02/14] add new extension point for repository initialization --- scm-ui/ui-types/src/Repositories.ts | 4 +++ scm-ui/ui-types/src/index.ts | 2 +- .../repos/components/form/RepositoryForm.tsx | 35 +++++++++++++++---- .../ui-webapp/src/repos/containers/Create.tsx | 7 ++-- scm-ui/ui-webapp/src/repos/modules/repos.ts | 4 +-- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/scm-ui/ui-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts index 777006496b..68b7d5113a 100644 --- a/scm-ui/ui-types/src/Repositories.ts +++ b/scm-ui/ui-types/src/Repositories.ts @@ -35,6 +35,10 @@ export type Repository = { _links: Links; }; +export type RepositoryCreation = Repository & { + creationContext: { [key: string]: any }; +}; + export type RepositoryCollection = PagedCollection & { _embedded: { repositories: Repository[] | string[]; diff --git a/scm-ui/ui-types/src/index.ts b/scm-ui/ui-types/src/index.ts index cdaa2fe416..f0263afe3b 100644 --- a/scm-ui/ui-types/src/index.ts +++ b/scm-ui/ui-types/src/index.ts @@ -29,7 +29,7 @@ export { Me } from "./Me"; export { DisplayedUser, User } from "./User"; export { Group, Member } from "./Group"; -export { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories"; +export { Repository, RepositoryCollection, RepositoryGroup, RepositoryCreation } from "./Repositories"; export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes"; export { Branch, BranchRequest } from "./Branches"; diff --git a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx index e21ea133bc..b22fd01218 100644 --- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx @@ -23,10 +23,10 @@ */ import React from "react"; import styled from "styled-components"; -import { WithTranslation, withTranslation } from "react-i18next"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; -import { Repository, RepositoryType } from "@scm-manager/ui-types"; -import { Checkbox, InputField, Level, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components"; +import {WithTranslation, withTranslation} from "react-i18next"; +import {ExtensionPoint} from "@scm-manager/ui-extensions"; +import {Repository, RepositoryCreation, RepositoryType} from "@scm-manager/ui-types"; +import {Checkbox, InputField, Level, Select, SubmitButton, Subtitle, Textarea} from "@scm-manager/ui-components"; import * as validator from "./repositoryValidation"; import { CUSTOM_NAMESPACE_STRATEGY } from "../../modules/repos"; @@ -45,7 +45,7 @@ const SpaceBetween = styled.div` `; type Props = WithTranslation & { - submitForm: (repo: Repository, shouldInit: boolean) => void; + submitForm: (repo: RepositoryCreation, shouldInit: boolean) => void; repository?: Repository; repositoryTypes?: RepositoryType[]; namespaceStrategy?: string; @@ -53,7 +53,7 @@ type Props = WithTranslation & { }; type State = { - repository: Repository; + repository: RepositoryCreation; initRepository: boolean; namespaceValidationError: boolean; nameValidationError: boolean; @@ -71,6 +71,7 @@ class RepositoryForm extends React.Component { type: "", contact: "", description: "", + creationContext: {}, _links: {} }, initRepository: false, @@ -85,7 +86,8 @@ class RepositoryForm extends React.Component { if (repository) { this.setState({ repository: { - ...repository + ...repository, + creationContext: {} } }); } @@ -128,6 +130,18 @@ class RepositoryForm extends React.Component { }); }; + setCreationContextEntry = (key: string, value: any) => { + this.setState({ + repository: { + ...this.state.repository, + creationContext: { + ...this.state.repository.creationContext, + [key]: value + } + } + }); + }; + render() { const { loading, t } = this.props; const repository = this.state.repository; @@ -209,6 +223,10 @@ class RepositoryForm extends React.Component { } const { repositoryTypes, t } = this.props; const repository = this.state.repository; + const extensionProps = { + repository, + setCreationContextEntry: this.setCreationContextEntry + }; return ( <> {this.renderNamespaceField()} @@ -239,6 +257,9 @@ class RepositoryForm extends React.Component { /> + {this.state.initRepository && ( + + )} ); } diff --git a/scm-ui/ui-webapp/src/repos/containers/Create.tsx b/scm-ui/ui-webapp/src/repos/containers/Create.tsx index 0ef6121afc..1796e7269f 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Create.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Create.tsx @@ -25,7 +25,7 @@ import React from "react"; import { connect } from "react-redux"; import { WithTranslation, withTranslation } from "react-i18next"; import { History } from "history"; -import { NamespaceStrategies, Repository, RepositoryType } from "@scm-manager/ui-types"; +import { NamespaceStrategies, Repository, RepositoryCreation, RepositoryType } from "@scm-manager/ui-types"; import { Page } from "@scm-manager/ui-components"; import { fetchRepositoryTypesIfNeeded, @@ -56,9 +56,10 @@ type Props = WithTranslation & { fetchRepositoryTypesIfNeeded: () => void; createRepo: ( link: string, - repository: Repository, + repository: RepositoryCreation, initRepository: boolean, - callback: (repo: Repository) => void + callback: (repo: Repository) => void, + initRepositoryContext?: any ) => void; resetForm: () => void; diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index 2cc46f033d..fc20ddd6f0 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -24,7 +24,7 @@ import { apiClient } from "@scm-manager/ui-components"; import * as types from "../../modules/types"; -import { Action, Repository, RepositoryCollection } from "@scm-manager/ui-types"; +import { Action, Repository, RepositoryCollection, RepositoryCreation } from "@scm-manager/ui-types"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; @@ -183,7 +183,7 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error): export function createRepo( link: string, - repository: Repository, + repository: RepositoryCreation, initRepository: boolean, callback?: (repo: Repository) => void ) { From 5c11657858d419fb9e22672a1027ec231df99c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 7 Sep 2020 15:02:59 +0200 Subject: [PATCH 03/14] Validate new namespace on rename --- CHANGELOG.md | 1 + .../repository/DefaultRepositoryManager.java | 1 + .../DefaultRepositoryManagerTest.java | 19 +++++++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c862e998c5..418812c8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Redirection to requested page after login in anonymous mode +- Validate new namespace on repository rename ([#1322](https://github.com/scm-manager/scm-manager/pull/1322)) ## [2.4.1] - 2020-09-01 ### Added diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index c79c8bb865..fa863a83f6 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -261,6 +261,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { throw new ChangeNamespaceNotAllowedException(repository); } changedRepository.setNamespace(newNamespace); + strategy.createNamespace(changedRepository); } managerDaoAdapter.modify( diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index 9a49d86bc9..bab8d873f1 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -46,6 +46,7 @@ import sonia.scm.ManagerTestBase; import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.SCMContext; +import sonia.scm.ScmConstraintViolationException; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.api.HookContext; @@ -63,6 +64,7 @@ import java.util.Map; import java.util.Set; import java.util.Stack; +import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasProperty; @@ -77,6 +79,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -400,12 +403,24 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { @Test public void shouldThrowNoChangesMadeException() { - Repository repository = new Repository("1", "hg", "space", "x"); + Repository repository = createTestRepository(); RepositoryManager repoManager = createManager(); thrown.expect(NoChangesMadeException.class); - repoManager.rename(repository, "space", "x"); + repoManager.rename(repository, "default_namespace", "HeartOfGold"); + } + + @Test + public void shouldThrowValidationException() { + Repository repository = createTestRepository(); + RepositoryManager repoManager = createManager(); + when(namespaceStrategy.canBeChanged()).thenReturn(true); + when(namespaceStrategy.createNamespace(argThat(r -> r.getNamespace().equals("invalid")))).thenThrow(ScmConstraintViolationException.class); + + thrown.expect(ScmConstraintViolationException.class); + + repoManager.rename(repository, "invalid", "splendid"); } @Test From e6c31ec0e13744ffed5c06c017b6e4f9ef014a03 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 7 Sep 2020 15:40:14 +0200 Subject: [PATCH 04/14] add indexResources to extensionProps --- scm-ui/ui-components/src/forms/index.ts | 2 +- .../repos/components/form/RepositoryForm.tsx | 20 ++++++++++--------- .../ui-webapp/src/repos/containers/Create.tsx | 11 +++++++--- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/scm-ui/ui-components/src/forms/index.ts b/scm-ui/ui-components/src/forms/index.ts index 54a5d49b15..de64d40aae 100644 --- a/scm-ui/ui-components/src/forms/index.ts +++ b/scm-ui/ui-components/src/forms/index.ts @@ -33,7 +33,7 @@ export { default as Checkbox } from "./Checkbox"; export { default as Radio } from "./Radio"; export { default as FilterInput } from "./FilterInput"; export { default as InputField } from "./InputField"; -export { default as Select } from "./Select"; +export { default as Select, SelectItem } from "./Select"; export { default as Textarea } from "./Textarea"; export { default as PasswordConfirmation } from "./PasswordConfirmation"; export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon"; diff --git a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx index b22fd01218..a6d90a0ce3 100644 --- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx @@ -23,10 +23,10 @@ */ import React from "react"; import styled from "styled-components"; -import {WithTranslation, withTranslation} from "react-i18next"; -import {ExtensionPoint} from "@scm-manager/ui-extensions"; -import {Repository, RepositoryCreation, RepositoryType} from "@scm-manager/ui-types"; -import {Checkbox, InputField, Level, Select, SubmitButton, Subtitle, Textarea} from "@scm-manager/ui-components"; +import { WithTranslation, withTranslation } from "react-i18next"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { Repository, RepositoryCreation, RepositoryType } from "@scm-manager/ui-types"; +import { Checkbox, InputField, Level, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components"; import * as validator from "./repositoryValidation"; import { CUSTOM_NAMESPACE_STRATEGY } from "../../modules/repos"; @@ -50,6 +50,7 @@ type Props = WithTranslation & { repositoryTypes?: RepositoryType[]; namespaceStrategy?: string; loading?: boolean; + indexResources: any; }; type State = { @@ -221,11 +222,12 @@ class RepositoryForm extends React.Component { if (!this.isCreateMode()) { return null; } - const { repositoryTypes, t } = this.props; + const { repositoryTypes, indexResources, t } = this.props; const repository = this.state.repository; const extensionProps = { repository, - setCreationContextEntry: this.setCreationContextEntry + setCreationContextEntry: this.setCreationContextEntry, + indexResources }; return ( <> @@ -255,11 +257,11 @@ class RepositoryForm extends React.Component { onChange={this.toggleInitCheckbox} helpText={t("help.initializeRepository")} /> + {this.state.initRepository && ( + + )} - {this.state.initRepository && ( - - )} ); } diff --git a/scm-ui/ui-webapp/src/repos/containers/Create.tsx b/scm-ui/ui-webapp/src/repos/containers/Create.tsx index 1796e7269f..b58791a562 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Create.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Create.tsx @@ -50,6 +50,7 @@ type Props = WithTranslation & { createLoading: boolean; error: Error; repoLink: string; + indexResources: any; // dispatch functions fetchNamespaceStrategiesIfNeeded: () => void; @@ -81,7 +82,7 @@ class Create extends React.Component { }; render() { - const { pageLoading, createLoading, repositoryTypes, namespaceStrategies, createRepo, error } = this.props; + const { pageLoading, createLoading, repositoryTypes, namespaceStrategies, createRepo, error, indexResources } = this.props; const { t, repoLink } = this.props; return ( @@ -99,6 +100,7 @@ class Create extends React.Component { submitForm={(repo, initRepository) => { createRepo(repoLink, repo, initRepository, (repo: Repository) => this.repoCreated(repo)); }} + indexResources={indexResources} /> ); @@ -113,13 +115,16 @@ const mapStateToProps = (state: any) => { const error = getFetchRepositoryTypesFailure(state) || getCreateRepoFailure(state) || getFetchNamespaceStrategiesFailure(state); const repoLink = getRepositoriesLink(state); + const indexResources = state?.indexResources; + return { repositoryTypes, namespaceStrategies, pageLoading, createLoading, error, - repoLink + repoLink, + indexResources }; }; @@ -131,7 +136,7 @@ const mapDispatchToProps = (dispatch: any) => { fetchNamespaceStrategiesIfNeeded: () => { dispatch(fetchNamespaceStrategiesIfNeeded()); }, - createRepo: (link: string, repository: Repository, initRepository: boolean, callback: () => void) => { + createRepo: (link: string, repository: RepositoryCreation, initRepository: boolean, callback: () => void) => { dispatch(createRepo(link, repository, initRepository, callback)); }, resetForm: () => { From 80a822932c4ec7f35eb50400fb4f29fe5c8e1506 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 8 Sep 2020 15:46:33 +0200 Subject: [PATCH 05/14] update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c862e998c5..1c4ade5d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Tags now have date information attached ([#1305](https://github.com/scm-manager/scm-manager/pull/1305)) - Add support for scroll anchors in url hash of diff page ([#1304](https://github.com/scm-manager/scm-manager/pull/1304)) - Documentation regarding data and plugin migration from v1 to v2 ([#1321](https://github.com/scm-manager/scm-manager/pull/1321)) +- Add RepositoryCreationDto with creation context and extension-point for repository initialization ([#1324](https://github.com/scm-manager/scm-manager/pull/1324)) ### Fixed - Redirection to requested page after login in anonymous mode From 0abe47f666eadd81fbdbd46cccd4af5f9dde4136 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 9 Sep 2020 08:54:38 +0200 Subject: [PATCH 06/14] simplify core api --- .../RepositoryContentInitializer.java | 23 +++++++------------ .../repos/components/form/RepositoryForm.tsx | 2 +- .../ui-webapp/src/repos/containers/Create.tsx | 3 +-- .../scm/repository/RepositoryInitializer.java | 13 +++++------ .../resources/RepositoryRootResourceTest.java | 5 ++-- .../repository/RepositoryInitializerTest.java | 12 +++++----- 6 files changed, 25 insertions(+), 33 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java index 93e21ac718..eb403d122b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java @@ -24,14 +24,11 @@ package sonia.scm.repository; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.io.ByteSource; import sonia.scm.plugin.ExtensionPoint; import java.io.IOException; import java.io.InputStream; -import java.util.Collections; -import java.util.Map; import java.util.Optional; /** @@ -47,15 +44,6 @@ public interface RepositoryContentInitializer { */ void initialize(InitializerContext context) throws IOException; - /** - * returns the class to which the creation context will be mapped - * - * @return the class of the creation context - */ - default Optional> getType() { - return Optional.empty(); - } - /** * Use this {@link InitializerContext} to create new files on repository initialization * which will be included in the first commit @@ -76,10 +64,15 @@ public interface RepositoryContentInitializer { CreateFile create(String path); /** - * @return creation context of repository which is going to be initialized + * Find the the context object with the given key and unmarshalls it to the given type. + * + * @param key key of the context object + * @param type type of the context object + * @return context object or empty optional + * @since 2.5.0 */ - default Map getCreationContext() { - return Collections.emptyMap(); + default Optional getCreationContext(String key, Class type) { + return Optional.empty(); } } diff --git a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx index a6d90a0ce3..c036cca608 100644 --- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx @@ -258,7 +258,7 @@ class RepositoryForm extends React.Component { helpText={t("help.initializeRepository")} /> {this.state.initRepository && ( - + )} diff --git a/scm-ui/ui-webapp/src/repos/containers/Create.tsx b/scm-ui/ui-webapp/src/repos/containers/Create.tsx index b58791a562..019e130b47 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Create.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Create.tsx @@ -59,8 +59,7 @@ type Props = WithTranslation & { link: string, repository: RepositoryCreation, initRepository: boolean, - callback: (repo: Repository) => void, - initRepositoryContext?: any + callback: (repo: Repository) => void ) => void; resetForm: () => void; diff --git a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java index c866b8d7d0..32a2253017 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java @@ -25,6 +25,7 @@ package sonia.scm.repository; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; import org.slf4j.Logger; @@ -40,6 +41,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.Optional; import java.util.Set; @Singleton @@ -56,10 +58,6 @@ public class RepositoryInitializer { this.contentInitializers = Priorities.sortInstances(contentInitializerSet); } - public void initialize(Repository repository) { - initialize(repository, null); - } - public void initialize(Repository repository, Map creationContext) { try (RepositoryService service = serviceFactory.create(repository)) { ModifyCommandBuilder modifyCommandBuilder = service.getModifyCommand(); @@ -85,6 +83,8 @@ public class RepositoryInitializer { private final ModifyCommandBuilder builder; private final Map creationContext; + private static final ObjectMapper mapper = new ObjectMapper(); + InitializerContextImpl(Repository repository, ModifyCommandBuilder builder, Map creationContext) { this.repository = repository; this.builder = builder; @@ -97,8 +97,8 @@ public class RepositoryInitializer { } @Override - public Map getCreationContext() { - return creationContext; + public Optional getCreationContext(String key, Class type) { + return Optional.of(mapper.convertValue(creationContext.get(key), type)); } @Override @@ -134,5 +134,4 @@ public class RepositoryInitializer { return initializerContext; } } - } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 3c958cae74..d8e22cb33b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -74,6 +74,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; @@ -315,7 +316,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); assertEquals("/v2/repositories/otherspace/repo", response.getOutputHeaders().get("Location").get(0).toString()); verify(repositoryManager).create(any(Repository.class)); - verify(repositoryInitializer, never()).initialize(any(Repository.class)); + verify(repositoryInitializer, never()).initialize(any(Repository.class), anyMap()); } @Test @@ -336,7 +337,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); ArgumentCaptor captor = ArgumentCaptor.forClass(Repository.class); - verify(repositoryInitializer).initialize(captor.capture()); + verify(repositoryInitializer).initialize(captor.capture(), anyMap()); Repository repository = captor.getValue(); assertEquals("space", repository.getNamespace()); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java index efa33feb7f..53a1ea4b02 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.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; import com.google.common.collect.ImmutableSet; @@ -29,7 +29,6 @@ import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Answers; import org.mockito.ArgumentCaptor; @@ -44,6 +43,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -88,7 +88,7 @@ class RepositoryInitializerTest { ); RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); - initializer.initialize(repository); + initializer.initialize(repository, Collections.emptyMap()); verifyFileCreation(readmeContentLoader, "# HeartOfGold"); verifyFileCreation(licenseContentLoader, "MIT"); @@ -108,7 +108,7 @@ class RepositoryInitializerTest { ); RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); - initializer.initialize(repository); + initializer.initialize(repository, Collections.emptyMap()); verifyFileCreationWithStream(contentLoader, "awesome"); @@ -138,7 +138,7 @@ class RepositoryInitializerTest { ); RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); - initializer.initialize(repository); + initializer.initialize(repository, Collections.emptyMap()); assertThat(reference.get()).isEqualTo("MIT"); } @@ -149,7 +149,7 @@ class RepositoryInitializerTest { doThrow(new IOException("epic fail")).when(contentLoader).withData(any(ByteSource.class)); RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, ImmutableSet.of(new ReadmeContentInitializer())); - assertThrows(InternalRepositoryException.class, () -> initializer.initialize(repository)); + assertThrows(InternalRepositoryException.class, () -> initializer.initialize(repository, Collections.emptyMap())); verify(repositoryService).close(); } From b36d6a5da05d1aaebaf7515fdd417b1f4914e743 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Wed, 9 Sep 2020 11:50:41 +0200 Subject: [PATCH 07/14] update gitdiff-parser dependency version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4772263be5..f8d6c968c9 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "resolutions": { "babel-core": "7.0.0-bridge.0", - "gitdiff-parser": "https://github.com/scm-manager/gitdiff-parser#617747460280bf4522bb84d217a9064ac8eb6d3d", + "gitdiff-parser": "https://github.com/scm-manager/gitdiff-parser#420d6cfa17a6a8f9bf1a517a2c629dcb332dbe13", "lowlight": "1.13.1" }, "babel": { From 6c9cba2bdacd039e377ba28919ec5c94642c5105 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Wed, 9 Sep 2020 11:53:30 +0200 Subject: [PATCH 08/14] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c862e998c5..f549f67998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Redirection to requested page after login in anonymous mode +- Diff view for svn now handles whitespaces in filenames properly ([1325](https://github.com/scm-manager/scm-manager/pull/1325)) ## [2.4.1] - 2020-09-01 ### Added From 01e3732c2467c5f4b250543117441cfa6b22f60a Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 9 Sep 2020 14:42:07 +0200 Subject: [PATCH 09/14] Rename getCreationContext to oneByType and make the implementation more robust --- .../RepositoryContentInitializer.java | 2 +- .../scm/repository/RepositoryInitializer.java | 10 ++- .../repository/RepositoryInitializerTest.java | 71 +++++++++++++++++-- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java index eb403d122b..6d89a65e5d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java @@ -71,7 +71,7 @@ public interface RepositoryContentInitializer { * @return context object or empty optional * @since 2.5.0 */ - default Optional getCreationContext(String key, Class type) { + default Optional oneByType(String key, Class type) { return Optional.empty(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java index 32a2253017..59380a0539 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.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; import com.fasterxml.jackson.databind.JsonNode; @@ -97,8 +97,12 @@ public class RepositoryInitializer { } @Override - public Optional getCreationContext(String key, Class type) { - return Optional.of(mapper.convertValue(creationContext.get(key), type)); + public Optional oneByType(String key, Class type) { + JsonNode node = creationContext.get(key); + if (node != null) { + return Optional.of(mapper.convertValue(node, type)); + } + return Optional.empty(); } @Override diff --git a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java index 53a1ea4b02..a8cd0986aa 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java @@ -24,6 +24,8 @@ package sonia.scm.repository; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; @@ -44,6 +46,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -51,15 +54,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class RepositoryInitializerTest { + private final ObjectMapper mapper = new ObjectMapper(); + @Mock private RepositoryServiceFactory repositoryServiceFactory; @@ -154,6 +155,43 @@ class RepositoryInitializerTest { verify(repositoryService).close(); } + @Test + void shouldCallRepositoryContentInitializerWithContext() throws IOException { + ModifyCommandBuilder.WithOverwriteFlagContentLoader slartiContentLoader = mockContentLoader("Slarti.md"); + + Set repositoryContentInitializers = ImmutableSet.of( + new NamedFileInitializer() + ); + + RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); + Named named = new Named(); + named.setName("Slarti"); + initializer.initialize(repository, Collections.singletonMap("named", mapper.valueToTree(named))); + + verifyFileCreation(slartiContentLoader, "# Named file"); + + verify(modifyCommand).setCommitMessage("initialize repository"); + verify(modifyCommand).execute(); + + verify(repositoryService).close(); + } + + @Test + void shouldDoNoInitializationWithoutContextType() { + Set repositoryContentInitializers = ImmutableSet.of( + new NamedFileInitializer() + ); + + RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); + initializer.initialize(repository, Collections.emptyMap()); + + verify(modifyCommand, never()).createFile(any()); + verify(modifyCommand).setCommitMessage("initialize repository"); + verify(modifyCommand).execute(); + + verify(repositoryService).close(); + } + private ModifyCommandBuilder.WithOverwriteFlagContentLoader mockContentLoader(String path) { ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader = mock(ModifyCommandBuilder.WithOverwriteFlagContentLoader.class); doReturn(contentLoader).when(modifyCommand).createFile(path); @@ -175,6 +213,29 @@ class RepositoryInitializerTest { assertThat(new String(bytes, StandardCharsets.UTF_8)).isEqualTo(expectedContent); } + private static class NamedFileInitializer implements RepositoryContentInitializer { + + @Override + public void initialize(InitializerContext context) throws IOException { + Optional named = context.oneByType("named", Named.class); + if (named.isPresent()) { + context.create(named.get().getName() + ".md").from("# Named file"); + } + } + } + + static class Named { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + @Priority(1) private static class ReadmeContentInitializer implements RepositoryContentInitializer { From aa76af4d41f5f304d56c8edc895a39de9c8c4f5f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 9 Sep 2020 15:08:23 +0200 Subject: [PATCH 10/14] Add optional url to ExceptionWithContext --- .../java/sonia/scm/ExceptionWithContext.java | 14 +++- .../ExceptionWithContextToErrorDtoMapper.java | 10 ++- ...eptionWithContextToErrorDtoMapperTest.java | 81 +++++++++++++++++++ .../repository/RepositoryInitializerTest.java | 4 +- 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapperTest.java diff --git a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java index 6d7b31693d..1631aea178 100644 --- a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java +++ b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java @@ -21,10 +21,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm; import java.util.List; +import java.util.Optional; import static java.util.Collections.unmodifiableList; @@ -49,4 +50,15 @@ public abstract class ExceptionWithContext extends RuntimeException { } public abstract String getCode(); + + /** + * Returns an url which gives more information about the exception or an empty optional. + * The methods returns an empty optional by default and can be overwritten. + * + * @return information url or empty + * @since 2.5.0 + */ + public Optional getUrl() { + return Optional.empty(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java index 3c62fe2b0d..bdf14e5bd5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import org.mapstruct.AfterMapping; @@ -31,15 +31,21 @@ import org.mapstruct.MappingTarget; import org.slf4j.MDC; import sonia.scm.ExceptionWithContext; +import java.util.Optional; + @Mapper public abstract class ExceptionWithContextToErrorDtoMapper { @Mapping(target = "errorCode", source = "code") @Mapping(target = "transactionId", ignore = true) @Mapping(target = "violations", ignore = true) - @Mapping(target = "url", ignore = true) public abstract ErrorDto map(ExceptionWithContext exception); + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") // is ok for mapping + public String mapOptional(Optional optionalString) { + return optionalString.orElse(null); + } + @AfterMapping void setTransactionId(@MappingTarget ErrorDto dto) { dto.setTransactionId(MDC.get("transaction_id")); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapperTest.java new file mode 100644 index 0000000000..649493b7e8 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapperTest.java @@ -0,0 +1,81 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.api.v2.resources; + +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; +import sonia.scm.ContextEntry; +import sonia.scm.ExceptionWithContext; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class ExceptionWithContextToErrorDtoMapperTest { + + private final ExceptionWithContextToErrorDtoMapper mapper = Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class); + + @Test + void shouldMapUrl() { + ExceptionWithUrl exception = new ExceptionWithUrl(); + ErrorDto dto = mapper.map(exception); + assertThat(dto.getUrl()).isEqualTo("https://scm-manager.org"); + } + + @Test + void shouldMapUrlToNull() { + ExceptionWithoutUrl exception = new ExceptionWithoutUrl(); + ErrorDto dto = mapper.map(exception); + assertThat(dto.getUrl()).isNull(); + } + + private static class ExceptionWithUrl extends ExceptionWithContext { + public ExceptionWithUrl() { + super(ContextEntry.ContextBuilder.noContext(), "With Url"); + } + + @Override + public String getCode() { + return "42"; + } + + @Override + public Optional getUrl() { + return Optional.of("https://scm-manager.org"); + } + } + + private static class ExceptionWithoutUrl extends ExceptionWithContext { + public ExceptionWithoutUrl() { + super(ContextEntry.ContextBuilder.noContext(), "Without Url"); + } + + @Override + public String getCode() { + return "21"; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java index a8cd0986aa..ba97478bdc 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java @@ -46,6 +46,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -150,7 +151,8 @@ class RepositoryInitializerTest { doThrow(new IOException("epic fail")).when(contentLoader).withData(any(ByteSource.class)); RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, ImmutableSet.of(new ReadmeContentInitializer())); - assertThrows(InternalRepositoryException.class, () -> initializer.initialize(repository, Collections.emptyMap())); + Map creationContext = Collections.emptyMap(); + assertThrows(InternalRepositoryException.class, () -> initializer.initialize(repository, creationContext)); verify(repositoryService).close(); } From 04eb3ade7ce083e758f3766a4f39a78fb4d5ba44 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 9 Sep 2020 15:11:40 +0200 Subject: [PATCH 11/14] ensure creation context map is not null --- .../sonia/scm/api/v2/resources/RepositoryCreationDto.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java index 4606ce3060..05dedab873 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java @@ -29,6 +29,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Collections; import java.util.Map; @NoArgsConstructor @@ -36,4 +37,11 @@ import java.util.Map; @Setter public class RepositoryCreationDto extends RepositoryDto { private Map creationContext; + + public Map getCreationContext() { + if (creationContext == null) { + return Collections.emptyMap(); + } + return creationContext; + } } From a4bd251f90963342b4e8a902a45f2db4c9ce9c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 10 Sep 2020 08:21:15 +0200 Subject: [PATCH 12/14] Accept git change type 'copy', too This one was not expected to exist, though praxis proves otherwise. So we simply accept this one here, too. --- .../java/sonia/scm/repository/spi/GitModificationsCommand.java | 3 +++ 1 file changed, 3 insertions(+) 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 e907081f2c..7581c8462d 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 @@ -33,6 +33,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.EmptyTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import sonia.scm.repository.Added; +import sonia.scm.repository.Copied; import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Modification; @@ -130,6 +131,8 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif return new Removed(entry.getOldPath()); case RENAME: return new Renamed(entry.getOldPath(), entry.getNewPath()); + case COPY: + return new Copied(entry.getOldPath(), entry.getNewPath()); default: throw new UnsupportedModificationTypeException(entity(repository), MessageFormat.format("The modification type: {0} is not supported.", type)); } From e64ddccb13349fbb87f090d1c6a00eb87e760a45 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 10 Sep 2020 08:36:07 +0200 Subject: [PATCH 13/14] Rename creationContext to contextEntries --- .../repository/RepositoryContentInitializer.java | 7 ++++--- scm-ui/ui-types/src/Repositories.ts | 2 +- .../src/repos/components/form/RepositoryForm.tsx | 8 ++++---- .../v2/resources/RepositoryCollectionResource.java | 4 ++-- .../api/v2/resources/RepositoryCreationDto.java | 8 ++++---- .../scm/repository/RepositoryInitializer.java | 14 +++++++------- .../scm/repository/RepositoryInitializerTest.java | 6 +++--- 7 files changed, 25 insertions(+), 24 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java index 6d89a65e5d..ffd3689ebc 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java @@ -64,14 +64,15 @@ public interface RepositoryContentInitializer { CreateFile create(String path); /** - * Find the the context object with the given key and unmarshalls it to the given type. + * Returns the the context entry with the given key and unmarshalls it to the given type. + * It no entry with the given key is available an empty optional is returned. * * @param key key of the context object * @param type type of the context object - * @return context object or empty optional + * @return context entry or empty optional * @since 2.5.0 */ - default Optional oneByType(String key, Class type) { + default Optional getEntry(String key, Class type) { return Optional.empty(); } } diff --git a/scm-ui/ui-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts index 68b7d5113a..7b9d54a3a8 100644 --- a/scm-ui/ui-types/src/Repositories.ts +++ b/scm-ui/ui-types/src/Repositories.ts @@ -36,7 +36,7 @@ export type Repository = { }; export type RepositoryCreation = Repository & { - creationContext: { [key: string]: any }; + contextEntries: { [key: string]: any }; }; export type RepositoryCollection = PagedCollection & { diff --git a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx index c036cca608..091bfdeee8 100644 --- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx @@ -72,7 +72,7 @@ class RepositoryForm extends React.Component { type: "", contact: "", description: "", - creationContext: {}, + contextEntries: {}, _links: {} }, initRepository: false, @@ -88,7 +88,7 @@ class RepositoryForm extends React.Component { this.setState({ repository: { ...repository, - creationContext: {} + contextEntries: {} } }); } @@ -135,8 +135,8 @@ class RepositoryForm extends React.Component { this.setState({ repository: { ...this.state.repository, - creationContext: { - ...this.state.repository.creationContext, + contextEntries: { + ...this.state.repository.contextEntries, [key]: value } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 274999d401..6abbef5ceb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import io.swagger.v3.oas.annotations.Operation; @@ -156,7 +156,7 @@ public class RepositoryCollectionResource { return resourceLinks.repository().self(r.getNamespace(), r.getName()); }); if (initialize) { - repositoryInitializer.initialize(reference.get(), repository.getCreationContext()); + repositoryInitializer.initialize(reference.get(), repository.getContextEntries()); } return response; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java index 05dedab873..7e5125ab8e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java @@ -36,12 +36,12 @@ import java.util.Map; @Getter @Setter public class RepositoryCreationDto extends RepositoryDto { - private Map creationContext; + private Map contextEntries; - public Map getCreationContext() { - if (creationContext == null) { + public Map getContextEntries() { + if (contextEntries == null) { return Collections.emptyMap(); } - return creationContext; + return contextEntries; } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java index 59380a0539..250f20566f 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java @@ -58,11 +58,11 @@ public class RepositoryInitializer { this.contentInitializers = Priorities.sortInstances(contentInitializerSet); } - public void initialize(Repository repository, Map creationContext) { + public void initialize(Repository repository, Map contextEntries) { try (RepositoryService service = serviceFactory.create(repository)) { ModifyCommandBuilder modifyCommandBuilder = service.getModifyCommand(); - InitializerContextImpl initializerContext = new InitializerContextImpl(repository, modifyCommandBuilder, creationContext); + InitializerContextImpl initializerContext = new InitializerContextImpl(repository, modifyCommandBuilder, contextEntries); for (RepositoryContentInitializer initializer : contentInitializers) { initializer.initialize(initializerContext); @@ -81,14 +81,14 @@ public class RepositoryInitializer { private final Repository repository; private final ModifyCommandBuilder builder; - private final Map creationContext; + private final Map contextEntries; private static final ObjectMapper mapper = new ObjectMapper(); - InitializerContextImpl(Repository repository, ModifyCommandBuilder builder, Map creationContext) { + InitializerContextImpl(Repository repository, ModifyCommandBuilder builder, Map contextEntries) { this.repository = repository; this.builder = builder; - this.creationContext = creationContext; + this.contextEntries = contextEntries; } @Override @@ -97,8 +97,8 @@ public class RepositoryInitializer { } @Override - public Optional oneByType(String key, Class type) { - JsonNode node = creationContext.get(key); + public Optional getEntry(String key, Class type) { + JsonNode node = contextEntries.get(key); if (node != null) { return Optional.of(mapper.convertValue(node, type)); } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java index ba97478bdc..d0cc9cdd29 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java @@ -151,8 +151,8 @@ class RepositoryInitializerTest { doThrow(new IOException("epic fail")).when(contentLoader).withData(any(ByteSource.class)); RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, ImmutableSet.of(new ReadmeContentInitializer())); - Map creationContext = Collections.emptyMap(); - assertThrows(InternalRepositoryException.class, () -> initializer.initialize(repository, creationContext)); + Map contextEntries = Collections.emptyMap(); + assertThrows(InternalRepositoryException.class, () -> initializer.initialize(repository, contextEntries)); verify(repositoryService).close(); } @@ -219,7 +219,7 @@ class RepositoryInitializerTest { @Override public void initialize(InitializerContext context) throws IOException { - Optional named = context.oneByType("named", Named.class); + Optional named = context.getEntry("named", Named.class); if (named.isPresent()) { context.create(named.get().getName() + ".md").from("# Named file"); } From 2db9d55ed989a56ea02e1e98784514bebcec34f1 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Thu, 10 Sep 2020 09:30:30 +0200 Subject: [PATCH 14/14] set created namespace on rename --- .../java/sonia/scm/repository/DefaultRepositoryManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index fa863a83f6..4886e81ea1 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -261,7 +261,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { throw new ChangeNamespaceNotAllowedException(repository); } changedRepository.setNamespace(newNamespace); - strategy.createNamespace(changedRepository); + changedRepository.setNamespace(strategy.createNamespace(changedRepository)); } managerDaoAdapter.modify(