mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-10-31 18:46:07 +01:00
Preferred checkout variant
Add ability to prioritize the repository checkout variants. These are displayed sorted. Co-authored-by: Eduard Heimbuch<eduard.heimbuch@cloudogu.com> Pushed-by: Florian Scholdei<florian.scholdei@cloudogu.com> Co-authored-by: Florian Scholdei<florian.scholdei@cloudogu.com> Pushed-by: Eduard Heimbuch<eduard.heimbuch@cloudogu.com>
This commit is contained in:
2
gradle/changelog/ssh_checkout.yaml
Normal file
2
gradle/changelog/ssh_checkout.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Protocol priority order by user preferences
|
||||||
@@ -34,6 +34,7 @@ import sonia.scm.config.ConfigurationPermissions;
|
|||||||
import sonia.scm.store.ConfigurationStore;
|
import sonia.scm.store.ConfigurationStore;
|
||||||
import sonia.scm.store.ConfigurationStoreFactory;
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
@@ -143,7 +144,10 @@ public abstract class ConfigurationAdapterBase<DAO, DTO extends HalRepresentatio
|
|||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("")
|
@Path("")
|
||||||
public DTO get(@Context UriInfo uriInfo) {
|
public DTO get(@Context UriInfo uriInfo) {
|
||||||
getReadPermission().check();
|
PermissionCheck readPermission = getReadPermission();
|
||||||
|
if (readPermission != null) {
|
||||||
|
readPermission.check();
|
||||||
|
}
|
||||||
return daoToDtoMapper.mapDaoToDto(getConfiguration(), createDtoLinks());
|
return daoToDtoMapper.mapDaoToDto(getConfiguration(), createDtoLinks());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,14 +167,18 @@ public abstract class ConfigurationAdapterBase<DAO, DTO extends HalRepresentatio
|
|||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Path("")
|
@Path("")
|
||||||
public void update(@NotNull @Valid DTO payload) {
|
public void update(@NotNull @Valid DTO payload) {
|
||||||
getWritePermission().check();
|
PermissionCheck writePermission = getWritePermission();
|
||||||
|
if (writePermission != null) {
|
||||||
|
writePermission.check();
|
||||||
|
}
|
||||||
getConfigStore().set(dtoToDaoMapper.mapDtoToDao(payload));
|
getConfigStore().set(dtoToDaoMapper.mapDtoToDao(payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Links.Builder createDtoLinks() {
|
private Links.Builder createDtoLinks() {
|
||||||
Links.Builder builder = Links.linkingTo();
|
Links.Builder builder = Links.linkingTo();
|
||||||
builder.single(Link.link("self", getReadLink()));
|
builder.single(Link.link("self", getReadLink()));
|
||||||
if (getWritePermission().isPermitted()) {
|
PermissionCheck writePermission = getWritePermission();
|
||||||
|
if (writePermission == null || writePermission.isPermitted()) {
|
||||||
builder.single(Link.link("update", getUpdateLink()));
|
builder.single(Link.link("update", getUpdateLink()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,15 +197,18 @@ public abstract class ConfigurationAdapterBase<DAO, DTO extends HalRepresentatio
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void enrich(HalEnricherContext context, HalAppender appender) {
|
public final void enrich(HalEnricherContext context, HalAppender appender) {
|
||||||
if (getReadPermission().isPermitted()) {
|
PermissionCheck readPermission = getReadPermission();
|
||||||
|
if (readPermission == null || readPermission.isPermitted()) {
|
||||||
appender.appendLink(getName(), getReadLink());
|
appender.appendLink(getName(), getReadLink());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
protected PermissionCheck getReadPermission() {
|
protected PermissionCheck getReadPermission() {
|
||||||
return ConfigurationPermissions.read(getName());
|
return ConfigurationPermissions.read(getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
protected PermissionCheck getWritePermission() {
|
protected PermissionCheck getWritePermission() {
|
||||||
return ConfigurationPermissions.write(getName());
|
return ConfigurationPermissions.write(getName());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,4 +47,14 @@ public interface ScmProtocol {
|
|||||||
default boolean isAnonymousEnabled() {
|
default boolean isAnonymousEnabled() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority for frontend evaluation order
|
||||||
|
*
|
||||||
|
* @return priority number
|
||||||
|
* @since 2.48.0
|
||||||
|
*/
|
||||||
|
default int getPriority() {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ public abstract class HttpScmProtocol implements ScmProtocol {
|
|||||||
return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", HttpUtil.encode(repository.getNamespace()), HttpUtil.encode(repository.getName()))).toASCIIString();
|
return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", HttpUtil.encode(repository.getNamespace()), HttpUtil.encode(repository.getName()))).toASCIIString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 150;
|
||||||
|
}
|
||||||
|
|
||||||
public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException {
|
public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException {
|
||||||
serve(request, response, repository, config);
|
serve(request, response, repository, config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,21 +21,20 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React, { FC, useState } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Repository, Link } from "@scm-manager/ui-types";
|
import { Repository, Link } from "@scm-manager/ui-types";
|
||||||
import { ButtonAddons, Button } from "@scm-manager/ui-components";
|
import { Button } from "@scm-manager/ui-buttons";
|
||||||
import CloneInformation from "./CloneInformation";
|
import CloneInformation from "./CloneInformation";
|
||||||
|
|
||||||
const Switcher = styled(ButtonAddons)`
|
const TopRightContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SmallButton = styled(Button).attrs((props) => ({
|
const SmallButton = styled(Button)`
|
||||||
className: "is-small",
|
|
||||||
}))`
|
|
||||||
height: inherit;
|
height: inherit;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -43,79 +42,59 @@ type Props = {
|
|||||||
repository: Repository;
|
repository: Repository;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type Protocol = Link & {
|
||||||
selected?: Link;
|
profile: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function selectHttpOrFirst(repository: Repository) {
|
function selectPreferredProtocolFirst(repository: Repository) {
|
||||||
const protocols = (repository._links["protocol"] as Link[]) || [];
|
const protocols = (repository._links["protocol"] as Protocol[]) || [];
|
||||||
|
|
||||||
for (const protocol of protocols) {
|
if (protocols.length === 0) {
|
||||||
if (protocol.name === "http") {
|
return undefined;
|
||||||
return protocol;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (protocols.length > 0) {
|
protocols.sort((a, b) => Number(b.profile) - Number(a.profile));
|
||||||
return protocols[0];
|
return protocols[0];
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ProtocolInformation extends React.Component<Props, State> {
|
const ProtocolInformation: FC<Props> = ({ repository }) => {
|
||||||
constructor(props: Props) {
|
const [selected, setSelected] = useState<Protocol | undefined>(selectPreferredProtocolFirst(repository));
|
||||||
super(props);
|
|
||||||
this.state = {
|
const protocols = repository._links["protocol"] as Protocol[];
|
||||||
selected: selectHttpOrFirst(props.repository),
|
if (!protocols || protocols.length === 0) {
|
||||||
};
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectProtocol = (protocol: Link) => {
|
if (protocols.length === 1) {
|
||||||
this.setState({
|
return <CloneInformation url={protocols[0].href} repository={repository} />;
|
||||||
selected: protocol,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderProtocolButton = (protocol: Link) => {
|
|
||||||
const name = protocol.name || "unknown";
|
|
||||||
|
|
||||||
let color;
|
|
||||||
|
|
||||||
const { selected } = this.state;
|
|
||||||
if (selected && protocol.name === selected.name) {
|
|
||||||
color = "link is-selected";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SmallButton color={color} action={() => this.selectProtocol(protocol)}>
|
|
||||||
{name.toUpperCase()}
|
|
||||||
</SmallButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { repository } = this.props;
|
|
||||||
|
|
||||||
const protocols = repository._links["protocol"] as Link[];
|
|
||||||
if (!protocols || protocols.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protocols.length === 1) {
|
|
||||||
return <CloneInformation url={protocols[0].href} repository={repository} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { selected } = this.state;
|
|
||||||
let cloneInformation = null;
|
|
||||||
if (selected) {
|
|
||||||
cloneInformation = <CloneInformation repository={repository} url={selected.href} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="content is-relative">
|
|
||||||
<Switcher>{protocols.map(this.renderProtocolButton)}</Switcher>
|
|
||||||
{cloneInformation}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
let cloneInformation = null;
|
||||||
|
if (selected) {
|
||||||
|
cloneInformation = <CloneInformation url={selected?.href} repository={repository} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="content is-relative">
|
||||||
|
<TopRightContainer className="field has-addons">
|
||||||
|
{protocols.map((protocol, index) => {
|
||||||
|
return (
|
||||||
|
<div className="control" key={protocol.name || index}>
|
||||||
|
<SmallButton
|
||||||
|
className={classNames("is-small", {
|
||||||
|
"is-link is-selected": selected && protocol.name === selected.name,
|
||||||
|
})}
|
||||||
|
onClick={() => setSelected(protocol)}
|
||||||
|
>
|
||||||
|
{(protocol.name || "unknown").toUpperCase()}
|
||||||
|
</SmallButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TopRightContainer>
|
||||||
|
{cloneInformation}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtocolInformation;
|
||||||
|
|||||||
@@ -215,6 +215,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Link createProtocolLink(ScmProtocol protocol) {
|
private Link createProtocolLink(ScmProtocol protocol) {
|
||||||
return Link.linkBuilder("protocol", protocol.getUrl()).withName(protocol.getType()).build();
|
return Link.linkBuilder("protocol", protocol.getUrl()).withName(protocol.getType()).withProfile(String.valueOf(protocol.getPriority())).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
|||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
assertEquals(SC_OK, response.getStatus());
|
assertEquals(SC_OK, response.getStatus());
|
||||||
assertTrue(response.getContentAsString().contains("\"protocol\":[{\"href\":\"http://\",\"name\":\"http\"},{\"href\":\"ssh://\",\"name\":\"ssh\"}]"));
|
assertTrue(response.getContentAsString().contains("\"protocol\":[{\"href\":\"http://\",\"name\":\"http\",\"profile\":\"100\"},{\"href\":\"ssh://\",\"name\":\"ssh\",\"profile\":\"100\"}]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user