diff --git a/CHANGELOG.md b/CHANGELOG.md index 35553a8c8f..0c5f0dd6e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ 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 - Update filter state on property change ([#1327](https://github.com/scm-manager/scm-manager/pull/1327)) +- Diff view for svn now handles whitespaces in filenames properly ([1325](https://github.com/scm-manager/scm-manager/pull/1325)) +- 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/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": { 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-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java index c58ffa2402..ffd3689ebc 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.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.io.ByteSource; @@ -29,6 +29,7 @@ import sonia.scm.plugin.ExtensionPoint; import java.io.IOException; import java.io.InputStream; +import java.util.Optional; /** * Use this {@link RepositoryContentInitializer} to create new files with custom content @@ -38,7 +39,6 @@ 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 */ @@ -57,10 +57,24 @@ 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); + + /** + * 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 entry or empty optional + * @since 2.5.0 + */ + default Optional getEntry(String key, Class type) { + return Optional.empty(); + } } /** @@ -70,6 +84,7 @@ public interface RepositoryContentInitializer { /** * Applies content to new file + * * @param content content of file as string * @return {@link InitializerContext} * @throws IOException @@ -78,6 +93,7 @@ public interface RepositoryContentInitializer { /** * Applies content to new file + * * @param input content of file as input stream * @return {@link InitializerContext} * @throws IOException @@ -86,6 +102,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-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)); } 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-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts index 777006496b..7b9d54a3a8 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 & { + contextEntries: { [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..091bfdeee8 100644 --- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx @@ -25,7 +25,7 @@ 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 { 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,15 +45,16 @@ 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; loading?: boolean; + indexResources: any; }; type State = { - repository: Repository; + repository: RepositoryCreation; initRepository: boolean; namespaceValidationError: boolean; nameValidationError: boolean; @@ -71,6 +72,7 @@ class RepositoryForm extends React.Component { type: "", contact: "", description: "", + contextEntries: {}, _links: {} }, initRepository: false, @@ -85,7 +87,8 @@ class RepositoryForm extends React.Component { if (repository) { this.setState({ repository: { - ...repository + ...repository, + contextEntries: {} } }); } @@ -128,6 +131,18 @@ class RepositoryForm extends React.Component { }); }; + setCreationContextEntry = (key: string, value: any) => { + this.setState({ + repository: { + ...this.state.repository, + contextEntries: { + ...this.state.repository.contextEntries, + [key]: value + } + } + }); + }; + render() { const { loading, t } = this.props; const repository = this.state.repository; @@ -207,8 +222,13 @@ 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, + indexResources + }; return ( <> {this.renderNamespaceField()} @@ -237,6 +257,9 @@ class RepositoryForm extends React.Component { onChange={this.toggleInitCheckbox} 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 0ef6121afc..019e130b47 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, @@ -50,13 +50,14 @@ type Props = WithTranslation & { createLoading: boolean; error: Error; repoLink: string; + indexResources: any; // dispatch functions fetchNamespaceStrategiesIfNeeded: () => void; fetchRepositoryTypesIfNeeded: () => void; createRepo: ( link: string, - repository: Repository, + repository: RepositoryCreation, initRepository: boolean, callback: (repo: Repository) => void ) => void; @@ -80,7 +81,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 ( @@ -98,6 +99,7 @@ class Create extends React.Component { submitForm={(repo, initRepository) => { createRepo(repoLink, repo, initRepository, (repo: Repository) => this.repoCreated(repo)); }} + indexResources={indexResources} /> ); @@ -112,13 +114,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 }; }; @@ -130,7 +135,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: () => { 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 ) { 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/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 51f632fa8c..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; @@ -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.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 new file mode 100644 index 0000000000..7e5125ab8e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCreationDto.java @@ -0,0 +1,47 @@ +/* + * 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.Collections; +import java.util.Map; + +@NoArgsConstructor +@Getter +@Setter +public class RepositoryCreationDto extends RepositoryDto { + private Map contextEntries; + + public Map getContextEntries() { + if (contextEntries == null) { + return Collections.emptyMap(); + } + return contextEntries; + } +} 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..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,6 +261,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { throw new ChangeNamespaceNotAllowedException(repository); } changedRepository.setNamespace(newNamespace); + changedRepository.setNamespace(strategy.createNamespace(changedRepository)); } managerDaoAdapter.modify( 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..250f20566f 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java @@ -21,9 +21,11 @@ * 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.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; import org.slf4j.Logger; @@ -38,6 +40,8 @@ import javax.inject.Singleton; 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 @@ -54,11 +58,11 @@ public class RepositoryInitializer { this.contentInitializers = Priorities.sortInstances(contentInitializerSet); } - public void initialize(Repository repository) { + public void initialize(Repository repository, Map contextEntries) { try (RepositoryService service = serviceFactory.create(repository)) { ModifyCommandBuilder modifyCommandBuilder = service.getModifyCommand(); - InitializerContextImpl initializerContext = new InitializerContextImpl(repository, modifyCommandBuilder); + InitializerContextImpl initializerContext = new InitializerContextImpl(repository, modifyCommandBuilder, contextEntries); for (RepositoryContentInitializer initializer : contentInitializers) { initializer.initialize(initializerContext); @@ -77,10 +81,14 @@ public class RepositoryInitializer { private final Repository repository; private final ModifyCommandBuilder builder; + private final Map contextEntries; - InitializerContextImpl(Repository repository, ModifyCommandBuilder builder) { + private static final ObjectMapper mapper = new ObjectMapper(); + + InitializerContextImpl(Repository repository, ModifyCommandBuilder builder, Map contextEntries) { this.repository = repository; this.builder = builder; + this.contextEntries = contextEntries; } @Override @@ -88,6 +96,15 @@ public class RepositoryInitializer { return repository; } + @Override + public Optional getEntry(String key, Class type) { + JsonNode node = contextEntries.get(key); + if (node != null) { + return Optional.of(mapper.convertValue(node, type)); + } + return Optional.empty(); + } + @Override public RepositoryContentInitializer.CreateFile create(String path) { return new CreateFileImpl(this, builder.useDefaultPath(true).createFile(path).setOverwrite(true)); @@ -121,5 +138,4 @@ public class RepositoryInitializer { return initializerContext; } } - } 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/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/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 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() { 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..d0cc9cdd29 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java @@ -21,15 +21,16 @@ * 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.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableSet; 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 +45,9 @@ import java.io.ByteArrayInputStream; 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; @@ -51,15 +55,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; @@ -88,7 +90,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 +110,7 @@ class RepositoryInitializerTest { ); RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); - initializer.initialize(repository); + initializer.initialize(repository, Collections.emptyMap()); verifyFileCreationWithStream(contentLoader, "awesome"); @@ -138,7 +140,7 @@ class RepositoryInitializerTest { ); RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); - initializer.initialize(repository); + initializer.initialize(repository, Collections.emptyMap()); assertThat(reference.get()).isEqualTo("MIT"); } @@ -149,7 +151,45 @@ 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)); + Map contextEntries = Collections.emptyMap(); + assertThrows(InternalRepositoryException.class, () -> initializer.initialize(repository, contextEntries)); + + 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(); } @@ -175,6 +215,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.getEntry("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 {