mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-04 20:45:52 +01:00
Merge branch 'develop' into bugfix/repository-search-reset
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
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<String> getUrl() {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <T> Optional<T> getEntry(String key, Class<T> 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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<Props, State> {
|
||||
type: "",
|
||||
contact: "",
|
||||
description: "",
|
||||
contextEntries: {},
|
||||
_links: {}
|
||||
},
|
||||
initRepository: false,
|
||||
@@ -85,7 +87,8 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
if (repository) {
|
||||
this.setState({
|
||||
repository: {
|
||||
...repository
|
||||
...repository,
|
||||
contextEntries: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -128,6 +131,18 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
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<Props, State> {
|
||||
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<Props, State> {
|
||||
onChange={this.toggleInitCheckbox}
|
||||
helpText={t("help.initializeRepository")}
|
||||
/>
|
||||
{this.state.initRepository && (
|
||||
<ExtensionPoint name="repos.create.initialize" props={extensionProps} renderAll={true} />
|
||||
)}
|
||||
</CheckboxWrapper>
|
||||
</SpaceBetween>
|
||||
</>
|
||||
|
||||
@@ -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<Props> {
|
||||
};
|
||||
|
||||
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<Props> {
|
||||
submitForm={(repo, initRepository) => {
|
||||
createRepo(repoLink, repo, initRepository, (repo: Repository) => this.repoCreated(repo));
|
||||
}}
|
||||
indexResources={indexResources}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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<String> optionalString) {
|
||||
return optionalString.orElse(null);
|
||||
}
|
||||
|
||||
@AfterMapping
|
||||
void setTransactionId(@MappingTarget ErrorDto dto) {
|
||||
dto.setTransactionId(MDC.get("transaction_id"));
|
||||
|
||||
@@ -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<Repository> 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;
|
||||
}
|
||||
|
||||
@@ -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<String, JsonNode> contextEntries;
|
||||
|
||||
public Map<String, JsonNode> getContextEntries() {
|
||||
if (contextEntries == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return contextEntries;
|
||||
}
|
||||
}
|
||||
@@ -261,6 +261,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
throw new ChangeNamespaceNotAllowedException(repository);
|
||||
}
|
||||
changedRepository.setNamespace(newNamespace);
|
||||
changedRepository.setNamespace(strategy.createNamespace(changedRepository));
|
||||
}
|
||||
|
||||
managerDaoAdapter.modify(
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
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<String, JsonNode> 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<String, JsonNode> contextEntries;
|
||||
|
||||
InitializerContextImpl(Repository repository, ModifyCommandBuilder builder) {
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
InitializerContextImpl(Repository repository, ModifyCommandBuilder builder, Map<String, JsonNode> contextEntries) {
|
||||
this.repository = repository;
|
||||
this.builder = builder;
|
||||
this.contextEntries = contextEntries;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,6 +96,15 @@ public class RepositoryInitializer {
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getEntry(String key, Class<T> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> 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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Repository> captor = ArgumentCaptor.forClass(Repository.class);
|
||||
verify(repositoryInitializer).initialize(captor.capture());
|
||||
verify(repositoryInitializer).initialize(captor.capture(), anyMap());
|
||||
|
||||
Repository repository = captor.getValue();
|
||||
assertEquals("space", repository.getNamespace());
|
||||
|
||||
@@ -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<Repository> {
|
||||
|
||||
@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
|
||||
|
||||
@@ -48,7 +48,7 @@ class ReadmeRepositoryContentInitializerTest {
|
||||
|
||||
private Repository repository;
|
||||
|
||||
private ReadmeRepositoryContentInitializer initializer = new ReadmeRepositoryContentInitializer();
|
||||
private final ReadmeRepositoryContentInitializer initializer = new ReadmeRepositoryContentInitializer();
|
||||
|
||||
@BeforeEach
|
||||
void setUpContext() {
|
||||
|
||||
@@ -24,12 +24,13 @@
|
||||
|
||||
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<String, JsonNode> 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<RepositoryContentInitializer> 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<RepositoryContentInitializer> 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> 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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user