mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 19:15:52 +01:00
- add global rename repositories permission
- add api call on rename action
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import sonia.scm.HandlerEventType;
|
||||||
|
import sonia.scm.event.AbstractHandlerEvent;
|
||||||
|
import sonia.scm.event.Event;
|
||||||
|
|
||||||
|
@Event
|
||||||
|
public class RepositoryRenamedEvent extends AbstractHandlerEvent<Repository> {
|
||||||
|
|
||||||
|
public RepositoryRenamedEvent(HandlerEventType eventType, Repository item, Repository oldItem) {
|
||||||
|
super(eventType, item, oldItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { Repository } from "@scm-manager/ui-types";
|
import { Repository, Link } from "@scm-manager/ui-types";
|
||||||
|
import { CONTENT_TYPE } from "../modules/repos";
|
||||||
import {
|
import {
|
||||||
ErrorNotification,
|
ErrorNotification,
|
||||||
Level,
|
Level,
|
||||||
@@ -35,6 +36,8 @@ import {
|
|||||||
ButtonGroup
|
ButtonGroup
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { apiClient } from "@scm-manager/ui-components/src";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -42,12 +45,13 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RenameRepository: FC<Props> = ({ repository, renameNamespace }) => {
|
const RenameRepository: FC<Props> = ({ repository, renameNamespace }) => {
|
||||||
|
let history = useHistory();
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
const [error, setError] = useState<Error | undefined>(undefined);
|
const [error, setError] = useState<Error | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [repositoryName, setRepositoryName] = useState(repository.name);
|
const [name, setName] = useState(repository.name);
|
||||||
const [repositoryNamespace, setRepositoryNamespace] = useState(repository.namespace);
|
const [namespace, setNamespace] = useState(repository.namespace);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorNotification error={error} />;
|
return <ErrorNotification error={error} />;
|
||||||
@@ -58,24 +62,37 @@ const RenameRepository: FC<Props> = ({ repository, renameNamespace }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isValid =
|
const isValid =
|
||||||
validation.isNameValid(repositoryName) &&
|
validation.isNameValid(name) &&
|
||||||
validation.isNameValid(repositoryNamespace) &&
|
validation.isNameValid(namespace) &&
|
||||||
(repository.name !== repositoryName || repository.namespace !== repositoryNamespace);
|
(repository.name !== name || repository.namespace !== namespace);
|
||||||
|
|
||||||
|
const rename = () => {
|
||||||
|
setLoading(true);
|
||||||
|
const url = renameNamespace
|
||||||
|
? (repository?._links?.renameWithNamespace as Link).href
|
||||||
|
: (repository?._links?.rename as Link).href;
|
||||||
|
|
||||||
|
apiClient
|
||||||
|
.post(url, { name, namespace }, CONTENT_TYPE)
|
||||||
|
.then(() => setLoading(false))
|
||||||
|
.then(() => history.push(`/repo/${namespace}/${name}`))
|
||||||
|
.catch(setError);
|
||||||
|
};
|
||||||
|
|
||||||
const modalBody = (
|
const modalBody = (
|
||||||
<div>
|
<div>
|
||||||
<InputField
|
<InputField
|
||||||
label={t("renameRepo.modal.label.repoName")}
|
label={t("renameRepo.modal.label.repoName")}
|
||||||
name={t("renameRepo.modal.label.repoName")}
|
name={t("renameRepo.modal.label.repoName")}
|
||||||
value={repositoryName}
|
value={name}
|
||||||
onChange={setRepositoryName}
|
onChange={setName}
|
||||||
/>
|
/>
|
||||||
{renameNamespace && (
|
{renameNamespace && (
|
||||||
<InputField
|
<InputField
|
||||||
label={t("renameRepo.modal.label.repoNamespace")}
|
label={t("renameRepo.modal.label.repoNamespace")}
|
||||||
name={t("renameRepo.modal.label.repoNamespace")}
|
name={t("renameRepo.modal.label.repoNamespace")}
|
||||||
value={repositoryNamespace}
|
value={namespace}
|
||||||
onChange={setRepositoryNamespace}
|
onChange={setNamespace}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -90,8 +107,13 @@ const RenameRepository: FC<Props> = ({ repository, renameNamespace }) => {
|
|||||||
label={t("renameRepo.modal.button.rename")}
|
label={t("renameRepo.modal.button.rename")}
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
title={t("renameRepo.modal.button.rename")}
|
title={t("renameRepo.modal.button.rename")}
|
||||||
|
action={rename}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={t("renameRepo.modal.button.cancel")}
|
||||||
|
title={t("renameRepo.modal.button.cancel")}
|
||||||
|
action={() => setShowModal(false)}
|
||||||
/>
|
/>
|
||||||
<Button label={t("renameRepo.modal.button.cancel")} action={() => setShowModal(false)} />
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
fetchRepoByName(repoLink, namespace, name);
|
fetchRepoByName(repoLink, namespace, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
const { fetchRepoByName, namespace, name, repoLink } = this.props;
|
||||||
|
if (namespace !== prevProps.namespace || name !== prevProps.name) {
|
||||||
|
fetchRepoByName(repoLink, namespace, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stripEndingSlash = (url: string) => {
|
stripEndingSlash = (url: string) => {
|
||||||
if (url.endsWith("/")) {
|
if (url.endsWith("/")) {
|
||||||
return url.substring(0, url.length - 1);
|
return url.substring(0, url.length - 1);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
|
|||||||
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`;
|
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`;
|
||||||
export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`;
|
export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`;
|
||||||
|
|
||||||
const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
|
export const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
|
||||||
|
|
||||||
// fetch repos
|
// fetch repos
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,14 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import sonia.scm.HandlerEventType;
|
||||||
import sonia.scm.config.ScmConfiguration;
|
import sonia.scm.config.ScmConfiguration;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryManager;
|
import sonia.scm.repository.RepositoryManager;
|
||||||
import sonia.scm.repository.RepositoryPermissions;
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
|
import sonia.scm.repository.RepositoryRenamedEvent;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -205,26 +208,31 @@ public class RepositoryResource {
|
|||||||
))
|
))
|
||||||
@ApiResponse(responseCode = "500", description = "internal server error")
|
@ApiResponse(responseCode = "500", description = "internal server error")
|
||||||
public Response rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) {
|
public Response rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) {
|
||||||
Repository repo = loadBy(namespace, name).get();
|
Supplier<Repository> repoSupplier = loadBy(namespace, name);
|
||||||
|
Repository unchangedRepo = repoSupplier.get();
|
||||||
|
Repository changedRepo = unchangedRepo.clone();
|
||||||
|
|
||||||
if (isRenameForbidden(repo)) {
|
if (isRenameForbidden(unchangedRepo, renameDto)) {
|
||||||
return Response.status(403).build();
|
return Response.status(403).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNamespaceOrNameNotChanged(repo, renameDto)) {
|
if (hasNamespaceOrNameNotChanged(unchangedRepo, renameDto)) {
|
||||||
return Response.status(400).build();
|
return Response.status(400).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Strings.isNullOrEmpty(renameDto.getName())) {
|
if (!Strings.isNullOrEmpty(renameDto.getName())) {
|
||||||
repo.setName(renameDto.getName());
|
changedRepo.setName(renameDto.getName());
|
||||||
}
|
}
|
||||||
if (!Strings.isNullOrEmpty(renameDto.getNamespace())) {
|
if (!Strings.isNullOrEmpty(renameDto.getNamespace())) {
|
||||||
repo.setNamespace(renameDto.getNamespace());
|
changedRepo.setNamespace(renameDto.getNamespace());
|
||||||
}
|
}
|
||||||
|
|
||||||
return adapter.update(
|
return adapter.update(
|
||||||
loadBy(namespace, name),
|
repoSupplier,
|
||||||
existing -> repo,
|
existing -> {
|
||||||
|
ScmEventBus.getInstance().post(new RepositoryRenamedEvent(HandlerEventType.MODIFY, changedRepo, unchangedRepo));
|
||||||
|
return changedRepo;
|
||||||
|
},
|
||||||
changed -> true,
|
changed -> true,
|
||||||
r -> r.getNamespaceAndName().logString()
|
r -> r.getNamespaceAndName().logString()
|
||||||
);
|
);
|
||||||
@@ -235,8 +243,9 @@ public class RepositoryResource {
|
|||||||
&& repo.getNamespace().equals(renameDto.getNamespace());
|
&& repo.getNamespace().equals(renameDto.getNamespace());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRenameForbidden(Repository repo) {
|
private boolean isRenameForbidden(Repository repo, RepositoryRenameDto renameDto) {
|
||||||
return !scmConfiguration.getNamespaceStrategy().equals("CustomNamespaceStrategy")
|
return !scmConfiguration.getNamespaceStrategy().equals("CustomNamespaceStrategy")
|
||||||
|
&& !repo.getNamespace().equals(renameDto.getNamespace())
|
||||||
|| !RepositoryPermissions.rename(repo).isPermitted();
|
|| !RepositoryPermissions.rename(repo).isPermitted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@
|
|||||||
<permission>
|
<permission>
|
||||||
<value>repository:read,pull,push:*</value>
|
<value>repository:read,pull,push:*</value>
|
||||||
</permission>
|
</permission>
|
||||||
|
<permission>
|
||||||
|
<value>repository:read,rename:*</value>
|
||||||
|
</permission>
|
||||||
<permission>
|
<permission>
|
||||||
<value>repository:*</value>
|
<value>repository:*</value>
|
||||||
</permission>
|
</permission>
|
||||||
|
|||||||
@@ -103,6 +103,10 @@
|
|||||||
"displayName": "Repository Löschen",
|
"displayName": "Repository Löschen",
|
||||||
"description": "Darf das Repository löschen."
|
"description": "Darf das Repository löschen."
|
||||||
},
|
},
|
||||||
|
"rename": {
|
||||||
|
"displayName": "Repository umbenennen",
|
||||||
|
"description": "Darf das Repository umbenennen."
|
||||||
|
},
|
||||||
"pull": {
|
"pull": {
|
||||||
"displayName": "Pull/Checkout",
|
"displayName": "Pull/Checkout",
|
||||||
"description": "Darf pull/checkout auf das Repository ausführen."
|
"description": "Darf pull/checkout auf das Repository ausführen."
|
||||||
|
|||||||
@@ -28,6 +28,12 @@
|
|||||||
"description": "May see, clone and push to all repositories"
|
"description": "May see, clone and push to all repositories"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"read,rename": {
|
||||||
|
"*": {
|
||||||
|
"displayName": "Rename all repositories",
|
||||||
|
"description": "May see and rename all repositories"
|
||||||
|
}
|
||||||
|
},
|
||||||
"*": {
|
"*": {
|
||||||
"displayName": "Own all repositories",
|
"displayName": "Own all repositories",
|
||||||
"description": "May see, clone, push to, configure and delete all repositories"
|
"description": "May see, clone, push to, configure and delete all repositories"
|
||||||
@@ -103,6 +109,10 @@
|
|||||||
"displayName": "delete repository",
|
"displayName": "delete repository",
|
||||||
"description": "May delete the repository"
|
"description": "May delete the repository"
|
||||||
},
|
},
|
||||||
|
"rename": {
|
||||||
|
"displayName": "rename repository",
|
||||||
|
"description": "May rename the repository."
|
||||||
|
},
|
||||||
"pull": {
|
"pull": {
|
||||||
"displayName": "pull/checkout repository",
|
"displayName": "pull/checkout repository",
|
||||||
"description": "May pull/checkout the repository"
|
"description": "May pull/checkout the repository"
|
||||||
|
|||||||
Reference in New Issue
Block a user