mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 11:35:57 +01:00
Disable repository types (#1908)
Disable repository types via global config for Git, Mercurial and Subversion. It is only possible to disable a type, if no repositories of this type exist. Also prevent repository creation if no type is allowed at all to catch nasty errors. Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
2
gradle/changelog/disable_repository_types.yaml
Normal file
2
gradle/changelog/disable_repository_types.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: Added
|
||||
description: Disable repository types via global config ([#1908](https://github.com/scm-manager/scm-manager/pull/1908))
|
||||
@@ -43,6 +43,7 @@ import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
|
||||
public class GitConfigDto extends HalRepresentation implements UpdateGitConfigDto {
|
||||
|
||||
private boolean disabled = false;
|
||||
private boolean allowDisable;
|
||||
|
||||
private String gcExpression;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.repository.GitConfig;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -44,6 +45,9 @@ public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper<GitConfig
|
||||
@Inject
|
||||
private ScmPathInfoStore scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
@AfterMapping
|
||||
void appendLinks(GitConfig config, @MappingTarget GitConfigDto target) {
|
||||
Links.Builder linksBuilder = linkingTo().self(self());
|
||||
@@ -51,6 +55,7 @@ public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper<GitConfig
|
||||
linksBuilder.single(link("update", update()));
|
||||
}
|
||||
target.add(linksBuilder.build());
|
||||
target.setAllowDisable(repositoryManager.getAll().stream().noneMatch(r -> r.getType().equals("git")));
|
||||
}
|
||||
|
||||
private String self() {
|
||||
|
||||
@@ -33,6 +33,8 @@ type Props = {
|
||||
};
|
||||
|
||||
type Configuration = HalRepresentation & {
|
||||
disabled: boolean;
|
||||
allowDisable: boolean;
|
||||
repositoryDirectory?: string;
|
||||
gcExpression?: string;
|
||||
nonFastForwardDisallowed: boolean;
|
||||
@@ -43,7 +45,7 @@ type Configuration = HalRepresentation & {
|
||||
const GitGlobalConfiguration: FC<Props> = ({ link }) => {
|
||||
const [t] = useTranslation("plugins");
|
||||
|
||||
const { initialConfiguration, isReadOnly, update, ...formProps } = useConfigLink(link);
|
||||
const { initialConfiguration, isReadOnly, update, ...formProps } = useConfigLink<Configuration>(link);
|
||||
const { formState, handleSubmit, register, reset } = useForm<Configuration>({ mode: "onChange" });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,6 +95,12 @@ const GitGlobalConfiguration: FC<Props> = ({ link }) => {
|
||||
errorMessage={t("scm-git-plugin.config.lfsWriteAuthorizationExpirationInMinutesValidationError")}
|
||||
{...register("lfsWriteAuthorizationExpirationInMinutes", { min: 1, required: true })}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("scm-git-plugin.config.disabled")}
|
||||
helpText={t("scm-git-plugin.config.disabledHelpText")}
|
||||
disabled={isReadOnly || !initialConfiguration?.allowDisable}
|
||||
{...register("disabled")}
|
||||
/>
|
||||
</ConfigurationForm>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"lfsWriteAuthorizationExpirationInMinutesHelpText": "Ablaufzeit für den Autorisierungstoken in Minuten, der für LFS Speicheranfragen ausgestellt wird. Wenn der SCM-Manager hinter einem Reverse-Proxy mit Zwischenspeicherung (z. B. Nginx) betrieben wird, sollte dieser Wert auf die Zeit gesetzt werden, die ein LFS-Upload maximal benötigen kann.",
|
||||
"lfsWriteAuthorizationExpirationInMinutesValidationError": "Has to be at least 1 minute",
|
||||
"disabled": "Deaktiviert",
|
||||
"disabledHelpText": "Aktiviere oder deaktiviere das Git Plugin",
|
||||
"disabledHelpText": "Aktiviere oder deaktiviere das Git Plugin. Nur erlaubt, wenn keine Git Repositories existieren.",
|
||||
"submit": "Speichern"
|
||||
},
|
||||
"repoConfig": {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"lfsWriteAuthorizationExpirationInMinutesHelpText": "Expiration time of the authorization token generated for LFS put requests in minutes. If SCM-Manager is run behind a reverse proxy that buffers http requests (eg. Nginx), this should set up to the time, an LFS upload may take at maximum.",
|
||||
"lfsWriteAuthorizationExpirationInMinutesValidationError": "Has to be at least 1 minute",
|
||||
"disabled": "Disabled",
|
||||
"disabledHelpText": "Enable or disable the Git plugin",
|
||||
"disabledHelpText": "Enable or disable the Git plugin. Only allowed if no Git Repositories exist.",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"repoConfig": {
|
||||
|
||||
@@ -37,11 +37,15 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.GitConfig;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -53,6 +57,9 @@ public class GitConfigToGitConfigDtoMapperTest {
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private ScmPathInfoStore scmPathInfoStore;
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
@InjectMocks
|
||||
private GitConfigToGitConfigDtoMapperImpl mapper;
|
||||
|
||||
@@ -78,15 +85,28 @@ public class GitConfigToGitConfigDtoMapperTest {
|
||||
public void shouldMapFields() {
|
||||
GitConfig config = createConfiguration();
|
||||
|
||||
when(repositoryManager.getAll()).thenReturn(Collections.singleton(RepositoryTestData.create42Puzzle("git")));
|
||||
when(subject.isPermitted("configuration:write:git")).thenReturn(true);
|
||||
GitConfigDto dto = mapper.map(config);
|
||||
|
||||
assertFalse(dto.isAllowDisable());
|
||||
assertEquals("express", dto.getGcExpression());
|
||||
assertFalse(dto.isDisabled());
|
||||
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
|
||||
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowDisableIfNoGitRepositoryExist() {
|
||||
GitConfig config = createConfiguration();
|
||||
|
||||
when(repositoryManager.getAll()).thenReturn(Collections.singleton(RepositoryTestData.create42Puzzle("hg")));
|
||||
when(subject.isPermitted("configuration:write:git")).thenReturn(true);
|
||||
GitConfigDto dto = mapper.map(config);
|
||||
|
||||
assertTrue(dto.isAllowDisable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMapFieldsWithoutUpdate() {
|
||||
GitConfig config = createConfiguration();
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.repository.HgGlobalConfig;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -44,12 +45,19 @@ public abstract class HgGlobalConfigToHgGlobalConfigDtoMapper extends BaseMapper
|
||||
|
||||
@Inject
|
||||
private HgConfigLinks links;
|
||||
@Inject
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
@VisibleForTesting
|
||||
void setLinks(HgConfigLinks links) {
|
||||
this.links = links;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setRepositoryManager(RepositoryManager repositoryManager) {
|
||||
this.repositoryManager = repositoryManager;
|
||||
}
|
||||
|
||||
@AfterMapping
|
||||
void appendLinks(HgGlobalConfig config, @MappingTarget HgGlobalGlobalConfigDto target) {
|
||||
HgConfigLinks.GlobalConfigLinks configLinks = links.global();
|
||||
@@ -59,5 +67,6 @@ public abstract class HgGlobalConfigToHgGlobalConfigDtoMapper extends BaseMapper
|
||||
linksBuilder.single(link("autoConfiguration", configLinks.autoConfigure()));
|
||||
}
|
||||
target.add(linksBuilder.build());
|
||||
target.setAllowDisable(repositoryManager.getAll().stream().noneMatch(r -> r.getType().equals("hg")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import lombok.Setter;
|
||||
public class HgGlobalGlobalConfigDto extends HalRepresentation implements UpdateHgGlobalConfigDto {
|
||||
|
||||
private boolean disabled;
|
||||
private boolean allowDisable = false;
|
||||
|
||||
@Encoding
|
||||
private String encoding;
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, Links } from "@scm-manager/ui-types";
|
||||
import { InputField, Checkbox, Button, apiClient } from "@scm-manager/ui-components";
|
||||
import { apiClient, Button, Checkbox, InputField } from "@scm-manager/ui-components";
|
||||
|
||||
type Configuration = {
|
||||
disabled: boolean;
|
||||
allowDisable: boolean;
|
||||
hgBinary: string;
|
||||
encoding: string;
|
||||
showRevisionInId: boolean;
|
||||
@@ -34,130 +36,106 @@ type Configuration = {
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
initialConfiguration: Configuration;
|
||||
readOnly: boolean;
|
||||
|
||||
onConfigurationChange: (p1: Configuration, p2: boolean) => void;
|
||||
};
|
||||
|
||||
type State = Configuration & {
|
||||
validationErrors: string[];
|
||||
};
|
||||
const HgConfigurationForm: FC<Props> = ({ initialConfiguration, onConfigurationChange, readOnly }) => {
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
const [configuration, setConfiguration] = useState(initialConfiguration);
|
||||
const [t] = useTranslation("plugins");
|
||||
|
||||
class HgConfigurationForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...props.initialConfiguration,
|
||||
validationErrors: [],
|
||||
};
|
||||
}
|
||||
useEffect(() => setConfiguration(initialConfiguration), [initialConfiguration]);
|
||||
useEffect(() => onConfigurationChange(configuration, updateValidationStatus()), [configuration]);
|
||||
|
||||
updateValidationStatus = () => {
|
||||
const requiredFields = ["hgBinary", "encoding"];
|
||||
|
||||
const validationErrors = [];
|
||||
for (const field of requiredFields) {
|
||||
// @ts-ignore
|
||||
if (!this.state[field]) {
|
||||
validationErrors.push(field);
|
||||
}
|
||||
const updateValidationStatus = () => {
|
||||
const errors = [];
|
||||
if (!configuration.hgBinary) {
|
||||
errors.push("hgBinary");
|
||||
}
|
||||
if (!configuration.encoding) {
|
||||
errors.push("encoding");
|
||||
}
|
||||
|
||||
this.setState({
|
||||
validationErrors,
|
||||
});
|
||||
|
||||
return validationErrors.length === 0;
|
||||
setValidationErrors(errors);
|
||||
return errors.length === 0;
|
||||
};
|
||||
|
||||
hasValidationError = (name: string) => {
|
||||
return this.state.validationErrors.indexOf(name) >= 0;
|
||||
const hasValidationError = (name: string) => {
|
||||
return validationErrors.indexOf(name) >= 0;
|
||||
};
|
||||
|
||||
handleChange = (value: string | boolean, name?: string) => {
|
||||
if (!name) {
|
||||
throw new Error("name not set");
|
||||
}
|
||||
this.setState(
|
||||
// @ts-ignore
|
||||
{
|
||||
[name]: value,
|
||||
},
|
||||
() => this.props.onConfigurationChange(this.state, this.updateValidationStatus())
|
||||
);
|
||||
};
|
||||
|
||||
triggerAutoConfigure = () => {
|
||||
const triggerAutoConfigure = () => {
|
||||
apiClient
|
||||
.put(
|
||||
(this.props.initialConfiguration._links.autoConfiguration as Link).href,
|
||||
{ ...this.props.initialConfiguration, hgBinary: this.state.hgBinary },
|
||||
(initialConfiguration._links.autoConfiguration as Link).href,
|
||||
{ ...initialConfiguration, hgBinary: configuration.hgBinary },
|
||||
"application/vnd.scmm-hgConfig+json;v=2"
|
||||
)
|
||||
.then(() =>
|
||||
apiClient
|
||||
.get((this.props.initialConfiguration._links.self as Link).href)
|
||||
.then((r) => r.json())
|
||||
.then((config: Configuration) => this.setState({ hgBinary: config.hgBinary }))
|
||||
.get((initialConfiguration._links.self as Link).href)
|
||||
.then(r => r.json())
|
||||
.then((config: Configuration) => setConfiguration({ ...configuration, hgBinary: config.hgBinary }))
|
||||
)
|
||||
.then(() => this.updateValidationStatus());
|
||||
.then(() => onConfigurationChange(configuration, updateValidationStatus()));
|
||||
};
|
||||
|
||||
inputField = (name: string) => {
|
||||
const { readOnly, t } = this.props;
|
||||
return (
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
name={name}
|
||||
label={t("scm-hg-plugin.config." + name)}
|
||||
helpText={t("scm-hg-plugin.config." + name + "HelpText")}
|
||||
// @ts-ignore
|
||||
value={this.state[name]}
|
||||
onChange={this.handleChange}
|
||||
validationError={this.hasValidationError(name)}
|
||||
errorMessage={t("scm-hg-plugin.config.required")}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
checkbox = (name: string) => {
|
||||
const { readOnly, t } = this.props;
|
||||
return (
|
||||
<Checkbox
|
||||
name={name}
|
||||
label={t("scm-hg-plugin.config." + name)}
|
||||
helpText={t("scm-hg-plugin.config." + name + "HelpText")}
|
||||
// @ts-ignore
|
||||
checked={this.state[name]}
|
||||
onChange={this.handleChange}
|
||||
return (
|
||||
<div>
|
||||
<InputField
|
||||
name="hgBinary"
|
||||
label={t("scm-hg-plugin.config.hgBinary")}
|
||||
helpText={t("scm-hg-plugin.config.hgBinary.HelpText")}
|
||||
value={configuration.hgBinary}
|
||||
onChange={value => setConfiguration({ ...configuration, hgBinary: value })}
|
||||
validationError={hasValidationError("hgBinary")}
|
||||
errorMessage={t("scm-hg-plugin.config.required")}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
<InputField
|
||||
name="encoding"
|
||||
label={t("scm-hg-plugin.config.encoding")}
|
||||
helpText={t("scm-hg-plugin.config.encoding.HelpText")}
|
||||
value={configuration.encoding}
|
||||
onChange={value => setConfiguration({ ...configuration, encoding: value })}
|
||||
validationError={hasValidationError("encoding")}
|
||||
errorMessage={t("scm-hg-plugin.config.required")}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<Checkbox
|
||||
name="showRevisionInId"
|
||||
label={t("scm-hg-plugin.config.showRevisionInId")}
|
||||
helpText={t("scm-hg-plugin.config.showRevisionInId.HelpText")}
|
||||
checked={configuration.showRevisionInId}
|
||||
onChange={value => setConfiguration({ ...configuration, showRevisionInId: value })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<Checkbox
|
||||
name="enableHttpPostArgs"
|
||||
label={t("scm-hg-plugin.config.enableHttpPostArgs")}
|
||||
helpText={t("scm-hg-plugin.config.enableHttpPostArgs.HelpText")}
|
||||
checked={configuration.enableHttpPostArgs}
|
||||
onChange={value => setConfiguration({ ...configuration, enableHttpPostArgs: value })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<Checkbox
|
||||
name="disabled"
|
||||
label={t("scm-hg-plugin.config.disabled")}
|
||||
helpText={t("scm-hg-plugin.config.disabledHelpText")}
|
||||
checked={configuration.disabled}
|
||||
onChange={value => {
|
||||
setConfiguration({ ...configuration, disabled: value });
|
||||
}}
|
||||
disabled={readOnly || !configuration.allowDisable}
|
||||
/>
|
||||
<Button disabled={!initialConfiguration?._links?.autoConfiguration} action={() => triggerAutoConfigure()}>
|
||||
{t("scm-hg-plugin.config.autoConfigure")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<div className="columns is-multiline">
|
||||
{this.inputField("hgBinary")}
|
||||
{this.inputField("encoding")}
|
||||
<div className="column is-half">{this.checkbox("showRevisionInId")}</div>
|
||||
<div className="column is-half">{this.checkbox("enableHttpPostArgs")}</div>
|
||||
<div className="column is-full">
|
||||
<Button
|
||||
disabled={!this.props.initialConfiguration?._links?.autoConfiguration}
|
||||
action={() => this.triggerAutoConfigure()}
|
||||
>
|
||||
{t("scm-hg-plugin.config.autoConfigure")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation("plugins")(HgConfigurationForm);
|
||||
export default HgConfigurationForm;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"enableHttpPostArgs": "HttpPostArgs Protocol aktivieren",
|
||||
"enableHttpPostArgsHelpText": "Aktiviert das experimentelle HttpPostArgs Protokoll von Mercurial. Das HttpPostArgs Protokoll verwendet den Post Request Body anstatt des HTTP Headers um Meta Informationen zu versenden. Dieses Vorgehen reduziert die Header Größe der Mercurial Requests. HttpPostArgs wird seit Mercurial 3.8 unterstützt.",
|
||||
"disabled": "Deaktiviert",
|
||||
"disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin.",
|
||||
"disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin. Nur erlaubt, wenn keine Mercurial Repositories existieren.",
|
||||
"required": "Dieser Konfigurationswert wird benötigt",
|
||||
"submit": "Speichern",
|
||||
"success": "Einstellungen wurden erfolgreich geändert",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"enableHttpPostArgs": "Enable HttpPostArgs Protocol",
|
||||
"enableHttpPostArgsHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.",
|
||||
"disabled": "Disabled",
|
||||
"disabledHelpText": "Enable or disable the Mercurial plugin.",
|
||||
"disabledHelpText": "Enable or disable the Mercurial plugin. Only allowed if no Mercurial repositories exist.",
|
||||
"required": "This configuration value is required",
|
||||
"submit": "Submit",
|
||||
"success": "Configuration changed successfully",
|
||||
|
||||
@@ -41,6 +41,8 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.HgGlobalConfig;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.web.HgVndMediaType;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
@@ -50,6 +52,7 @@ import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -68,6 +71,9 @@ public class HgConfigResourceTest {
|
||||
|
||||
private final RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
@InjectMocks
|
||||
private HgGlobalConfigDtoToHgConfigMapperImpl dtoToConfigMapper;
|
||||
|
||||
@@ -84,6 +90,7 @@ public class HgConfigResourceTest {
|
||||
public void prepareEnvironment() {
|
||||
HgGlobalConfig gitConfig = createConfiguration();
|
||||
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
||||
when(repositoryManager.getAll()).thenReturn(Collections.singleton(RepositoryTestData.create42Puzzle()));
|
||||
|
||||
HgConfigResource gitConfigResource = new HgConfigResource(
|
||||
dtoToConfigMapper, createConfigToDtoMapper(), repositoryHandler,
|
||||
@@ -100,6 +107,7 @@ public class HgConfigResourceTest {
|
||||
HgGlobalConfigToHgGlobalConfigDtoMapper.class
|
||||
);
|
||||
mapper.setLinks(links);
|
||||
mapper.setRepositoryManager(repositoryManager);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,12 @@ import org.mapstruct.factory.Mappers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.HgGlobalConfig;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -51,6 +55,9 @@ class HgGlobalConfigToHgGlobalConfigDtoMapperTest {
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
@Mock
|
||||
private RepositoryManager manager;
|
||||
|
||||
private HgGlobalConfigToHgGlobalConfigDtoMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
@@ -62,6 +69,7 @@ class HgGlobalConfigToHgGlobalConfigDtoMapperTest {
|
||||
|
||||
mapper = Mappers.getMapper(HgGlobalConfigToHgGlobalConfigDtoMapper.class);
|
||||
mapper.setLinks(new HgConfigLinks(store));
|
||||
mapper.setRepositoryManager(manager);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -74,9 +82,11 @@ class HgGlobalConfigToHgGlobalConfigDtoMapperTest {
|
||||
HgGlobalConfig config = createConfiguration();
|
||||
|
||||
when(subject.isPermitted("configuration:write:hg")).thenReturn(true);
|
||||
when(manager.getAll()).thenReturn(Collections.singleton(RepositoryTestData.create42Puzzle("hg")));
|
||||
HgGlobalGlobalConfigDto dto = mapper.map(config);
|
||||
|
||||
assertEqualsConfiguration(dto);
|
||||
assertThat(dto.isAllowDisable()).isFalse();
|
||||
|
||||
assertThat(dto.getLinks().getLinkBy("self")).hasValueSatisfying(
|
||||
link -> assertThat(link.getHref()).isEqualTo(expectedBaseUri.toString())
|
||||
@@ -85,10 +95,21 @@ class HgGlobalConfigToHgGlobalConfigDtoMapperTest {
|
||||
link -> assertThat(link.getHref()).isEqualTo(expectedBaseUri.toString())
|
||||
);
|
||||
assertThat(dto.getLinks().getLinkBy("autoConfiguration")).hasValueSatisfying(
|
||||
link -> assertThat(link.getHref()).isEqualTo(expectedBaseUri.toString() + "/auto-configuration")
|
||||
link -> assertThat(link.getHref()).isEqualTo(expectedBaseUri + "/auto-configuration")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowDisableIfNoHgRepositoriesExist() {
|
||||
HgGlobalConfig config = createConfiguration();
|
||||
|
||||
when(subject.isPermitted("configuration:write:hg")).thenReturn(true);
|
||||
when(manager.getAll()).thenReturn(Collections.singleton(RepositoryTestData.create42Puzzle("git")));
|
||||
HgGlobalGlobalConfigDto dto = mapper.map(config);
|
||||
|
||||
assertThat(dto.isAllowDisable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapFieldsWithoutUpdate() {
|
||||
HgGlobalConfig config = createConfiguration();
|
||||
|
||||
@@ -37,6 +37,7 @@ import sonia.scm.repository.Compatibility;
|
||||
public class SvnConfigDto extends HalRepresentation implements UpdateSvnConfigDto {
|
||||
|
||||
private boolean disabled;
|
||||
private boolean allowDisable;
|
||||
|
||||
private boolean enabledGZip;
|
||||
private Compatibility compatibility;
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.SvnConfig;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -43,6 +44,8 @@ public abstract class SvnConfigToSvnConfigDtoMapper extends BaseMapper<SvnConfig
|
||||
|
||||
@Inject
|
||||
private ScmPathInfoStore scmPathInfoStore;
|
||||
@Inject
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
@AfterMapping
|
||||
void appendLinks(SvnConfig config, @MappingTarget SvnConfigDto target) {
|
||||
@@ -51,6 +54,7 @@ public abstract class SvnConfigToSvnConfigDtoMapper extends BaseMapper<SvnConfig
|
||||
linksBuilder.single(link("update", update()));
|
||||
}
|
||||
target.add(linksBuilder.build());
|
||||
target.setAllowDisable(repositoryManager.getAll().stream().noneMatch(r -> r.getType().equals("svn")));
|
||||
}
|
||||
|
||||
private String self() {
|
||||
|
||||
@@ -21,87 +21,65 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { withTranslation, WithTranslation } from "react-i18next";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Links } from "@scm-manager/ui-types";
|
||||
import { Checkbox, Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Configuration = {
|
||||
disabled: boolean;
|
||||
allowDisable: boolean;
|
||||
compatibility: string;
|
||||
enabledGZip: boolean;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
initialConfiguration: Configuration;
|
||||
readOnly: boolean;
|
||||
|
||||
onConfigurationChange: (p1: Configuration, p2: boolean) => void;
|
||||
};
|
||||
|
||||
type State = Configuration;
|
||||
const SvnConfigurationForm: FC<Props> = ({ initialConfiguration, readOnly, onConfigurationChange }) => {
|
||||
const [t] = useTranslation("plugins");
|
||||
const [configuration, setConfiguration] = useState(initialConfiguration);
|
||||
|
||||
class SvnConfigurationForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...props.initialConfiguration,
|
||||
};
|
||||
}
|
||||
useEffect(() => setConfiguration(initialConfiguration), [initialConfiguration]);
|
||||
useEffect(() => onConfigurationChange(configuration, true), [configuration]);
|
||||
|
||||
handleChange = (value: any, name?: string) => {
|
||||
if (!name) {
|
||||
throw new Error("required name not set");
|
||||
}
|
||||
this.setState(
|
||||
// @ts-ignore
|
||||
{
|
||||
[name]: value,
|
||||
},
|
||||
() => this.props.onConfigurationChange(this.state, true)
|
||||
);
|
||||
};
|
||||
const options = ["NONE", "PRE14", "PRE15", "PRE16", "PRE17", "WITH17"].map((option: string) => ({
|
||||
value: option,
|
||||
label: t("scm-svn-plugin.config.compatibility-values." + option.toLowerCase())
|
||||
}));
|
||||
|
||||
compatibilityOptions = (values: string[]) => {
|
||||
const options = [];
|
||||
for (const value of values) {
|
||||
options.push(this.compatibilityOption(value));
|
||||
}
|
||||
return options;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
name="compatibility"
|
||||
label={t("scm-svn-plugin.config.compatibility")}
|
||||
helpText={t("scm-svn-plugin.config.compatibilityHelpText")}
|
||||
value={configuration.compatibility}
|
||||
options={options}
|
||||
onChange={option => setConfiguration({ ...configuration, compatibility: option })}
|
||||
/>
|
||||
<Checkbox
|
||||
name="enabledGZip"
|
||||
label={t("scm-svn-plugin.config.enabledGZip")}
|
||||
helpText={t("scm-svn-plugin.config.enabledGZipHelpText")}
|
||||
checked={configuration.enabledGZip}
|
||||
onChange={value => setConfiguration({ ...configuration, enabledGZip: value })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<Checkbox
|
||||
name="disabled"
|
||||
label={t("scm-svn-plugin.config.disabled")}
|
||||
helpText={t("scm-svn-plugin.config.disabledHelpText")}
|
||||
checked={configuration.disabled}
|
||||
onChange={value => setConfiguration({ ...configuration, disabled: value })}
|
||||
disabled={readOnly || !configuration.allowDisable}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
compatibilityOption = (value: string) => {
|
||||
return {
|
||||
value,
|
||||
label: this.props.t("scm-svn-plugin.config.compatibility-values." + value.toLowerCase()),
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const { readOnly, t } = this.props;
|
||||
const compatibilityOptions = this.compatibilityOptions(["NONE", "PRE14", "PRE15", "PRE16", "PRE17", "WITH17"]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
name="compatibility"
|
||||
label={t("scm-svn-plugin.config.compatibility")}
|
||||
helpText={t("scm-svn-plugin.config.compatibilityHelpText")}
|
||||
value={this.state.compatibility}
|
||||
options={compatibilityOptions}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<Checkbox
|
||||
name="enabledGZip"
|
||||
label={t("scm-svn-plugin.config.enabledGZip")}
|
||||
helpText={t("scm-svn-plugin.config.enabledGZipHelpText")}
|
||||
checked={this.state.enabledGZip}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation("plugins")(SvnConfigurationForm);
|
||||
export default SvnConfigurationForm;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"enabledGZip": "GZip Compression aktivieren",
|
||||
"enabledGZipHelpText": "Aktiviert GZip Kompression für SVN Responses",
|
||||
"disabled": "Deaktiviert",
|
||||
"disabledHelpText": "Aktiviert oder deaktiviert das SVN Plugin",
|
||||
"disabledHelpText": "Aktiviert oder deaktiviert das SVN Plugin. Nur erlaubt, wenn keine Subversion Repositories existieren.",
|
||||
"required": "Dieser Konfigurationswert wird benötigt"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"enabledGZip": "Enable GZip Compression",
|
||||
"enabledGZipHelpText": "Enable GZip compression for SVN responses.",
|
||||
"disabled": "Disabled",
|
||||
"disabledHelpText": "Enable or disable the SVN plugin",
|
||||
"disabledHelpText": "Enable or disable the SVN plugin. Only allowed if no Subversion repositories exist.",
|
||||
"required": "This configuration value is required"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.SvnConfig;
|
||||
import sonia.scm.repository.SvnRepositoryHandler;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
@@ -68,6 +69,9 @@ public class SvnConfigResourceTest {
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
@InjectMocks
|
||||
private SvnConfigDtoToSvnConfigMapperImpl dtoToConfigMapper;
|
||||
|
||||
|
||||
@@ -37,9 +37,12 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.Compatibility;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.SvnConfig;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -50,11 +53,14 @@ import static org.mockito.Mockito.when;
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SvnConfigToSvnConfigDtoMapperTest {
|
||||
|
||||
private URI baseUri = URI.create("http://example.com/base/");
|
||||
private final URI baseUri = URI.create("http://example.com/base/");
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private ScmPathInfoStore scmPathInfoStore;
|
||||
|
||||
@Mock
|
||||
private RepositoryManager manager;
|
||||
|
||||
@InjectMocks
|
||||
private SvnConfigToSvnConfigDtoMapperImpl mapper;
|
||||
|
||||
@@ -80,10 +86,12 @@ public class SvnConfigToSvnConfigDtoMapperTest {
|
||||
public void shouldMapFields() {
|
||||
SvnConfig config = createConfiguration();
|
||||
|
||||
when(manager.getAll()).thenReturn(Collections.singleton(RepositoryTestData.create42Puzzle("svn")));
|
||||
when(subject.isPermitted("configuration:write:svn")).thenReturn(true);
|
||||
SvnConfigDto dto = mapper.map(config);
|
||||
|
||||
assertTrue(dto.isDisabled());
|
||||
assertFalse(dto.isAllowDisable());
|
||||
|
||||
assertEquals(Compatibility.PRE15, dto.getCompatibility());
|
||||
assertTrue(dto.isEnabledGZip());
|
||||
@@ -92,6 +100,17 @@ public class SvnConfigToSvnConfigDtoMapperTest {
|
||||
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowDisableIfNoSubversionRepositoriesExist() {
|
||||
SvnConfig config = createConfiguration();
|
||||
|
||||
when(manager.getAll()).thenReturn(Collections.singleton(RepositoryTestData.create42Puzzle("git")));
|
||||
when(subject.isPermitted("configuration:write:svn")).thenReturn(true);
|
||||
SvnConfigDto dto = mapper.map(config);
|
||||
|
||||
assertTrue(dto.isAllowDisable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMapFieldsWithoutUpdate() {
|
||||
SvnConfig config = createConfiguration();
|
||||
|
||||
@@ -67,7 +67,8 @@
|
||||
},
|
||||
"create": {
|
||||
"title": "Repository hinzufügen",
|
||||
"subtitle": "Neues Repository erstellen"
|
||||
"subtitle": "Neues Repository erstellen",
|
||||
"noTypes": "Es kann kein neues Repository erstellt werden, weil kein Repository Typ verfügbar ist. Bitte stellen Sie sicher, dass mindestens ein Repository Typ erlaubt ist."
|
||||
},
|
||||
"import": {
|
||||
"subtitle": "Bestehendes Repository importieren",
|
||||
|
||||
@@ -67,7 +67,8 @@
|
||||
},
|
||||
"create": {
|
||||
"title": "Add Repository",
|
||||
"subtitle": "Create a new repository"
|
||||
"subtitle": "Create a new repository",
|
||||
"noTypes": "Cannot create new repository because no repository type is available. Please ensure at least one repository type is enabled."
|
||||
},
|
||||
"import": {
|
||||
"subtitle": "Import existing repository",
|
||||
|
||||
@@ -28,7 +28,7 @@ import CreateRepository from "./CreateRepository";
|
||||
import ImportRepository from "./ImportRepository";
|
||||
import { useBinder } from "@scm-manager/ui-extensions";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Page, urls } from "@scm-manager/ui-components";
|
||||
import { Notification, Page, urls } from "@scm-manager/ui-components";
|
||||
import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher";
|
||||
import { useIndex, useNamespaceStrategies, useRepositoryTypes } from "@scm-manager/ui-api";
|
||||
import NamespaceAndNameFields from "../components/NamespaceAndNameFields";
|
||||
@@ -67,7 +67,11 @@ const CreatorRoute: FC<CreatorRouteProps> = ({ creator, creators }) => {
|
||||
loading={isPageLoading}
|
||||
error={pageLoadingError}
|
||||
>
|
||||
{namespaceStrategies && repositoryTypes && index ? (
|
||||
{namespaceStrategies &&
|
||||
repositoryTypes &&
|
||||
repositoryTypes?._embedded?.repositoryTypes &&
|
||||
repositoryTypes._embedded.repositoryTypes.length > 0 &&
|
||||
index ? (
|
||||
<Component
|
||||
namespaceStrategies={namespaceStrategies}
|
||||
repositoryTypes={repositoryTypes}
|
||||
@@ -75,7 +79,9 @@ const CreatorRoute: FC<CreatorRouteProps> = ({ creator, creators }) => {
|
||||
nameForm={NamespaceAndNameFields}
|
||||
informationForm={RepositoryInformationForm}
|
||||
/>
|
||||
) : null}
|
||||
) : (
|
||||
<Notification type="warning">{t("create.noTypes")}</Notification>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user