Merge pull request #1331 from scm-manager/feature/tag_overview

Feature/tag overview
This commit is contained in:
Konstantin Schaper
2020-09-18 11:16:55 +02:00
committed by GitHub
36 changed files with 789 additions and 34 deletions

View File

@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Tags overview for repository [#1331](https://github.com/scm-manager/scm-manager/pull/1331)
### Fixed
- Missing synchronization during repository creation ([#1328](https://github.com/scm-manager/scm-manager/pull/1328))
- Missing BranchCreatedEvent for mercurial ([#1334](https://github.com/scm-manager/scm-manager/pull/1334))

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -6,6 +6,7 @@ partiallyActive: true
Der Bereich Repository umfasst alles auf Basis von Repositories in Namespaces. Dazu zählen alle Operationen auf Branches, der Code und Einstellungen.
* [Branches](branches/)
* [Tags](tags/)
* [Code](code/)
* [Einstellungen](settings/)
<!--- AppendLinkContentEnd -->

13
docs/de/user/repo/tags.md Normal file
View File

@@ -0,0 +1,13 @@
---
title: Repository
subtitle: Tags
---
### Übersicht
Auf der Tags-Übersicht sind die existierenden Tags nach Erstelldatum absteigend aufgeführt. Bei einem Klick auf einen Tag wird der Benutzer zur Detailseite des Tags weitergeleitet.
![Tags Übersicht](assets/repository-tags-overview.png)
### Tag Detailseite
Hier wird ein Befehl zum Arbeiten mit dem Tag auf einer Kommandozeile aufgeführt.
![Tag Detailseite](assets/repository-tag-detailView.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -5,6 +5,7 @@ partiallyActive: true
The Repository area includes everything based on repositories in namespaces. This includes all operations on branches, the code and settings.
* [Branches](branches/)
* [Tags](tags/)
* [Code](code/)
* [Settings](settings/)

13
docs/en/user/repo/tags.md Normal file
View File

@@ -0,0 +1,13 @@
---
title: Repository
subtitle: Tags
---
### Overview
The tag overview shows the tags that exist for this repository. By clicking on a tag, the details page of the tag is shown.
![Tags Overview](assets/repository-tags-overview.png)
### Tag Details Page
This page shows a command to work with the tag on the command line.
![Tag Details Page](assets/repository-tag-detailView.png)

View File

@@ -0,0 +1,48 @@
/*
* 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.
*/
import React, { FC } from "react";
import { Tag } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next";
type Props = {
tag: Tag;
};
const GitTagInformation: FC<Props> = ({ tag }) => {
const [t] = useTranslation("plugins");
return (
<>
<h4>{t("scm-git-plugin.information.checkoutTag")}</h4>
<pre>
<code>
git checkout tags/{tag.name} -b branch/{tag.name}
</code>
</pre>
</>
);
};
export default GitTagInformation;

View File

@@ -32,6 +32,7 @@ import GitGlobalConfiguration from "./GitGlobalConfiguration";
import GitBranchInformation from "./GitBranchInformation";
import GitMergeInformation from "./GitMergeInformation";
import RepositoryConfig from "./RepositoryConfig";
import GitTagInformation from "./GitTagInformation";
// repository
@@ -42,6 +43,7 @@ export const gitPredicate = (props: any) => {
binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate);
binder.bind("repos.branch-details.information", GitBranchInformation, gitPredicate);
binder.bind("repos.tag-details.information", GitTagInformation, gitPredicate);
binder.bind("repos.repository-merge.information", GitMergeInformation, gitPredicate);
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);

View File

@@ -6,6 +6,7 @@
"replace": "Ein bestehendes Repository aktualisieren",
"fetch": "Remote-Änderungen herunterladen",
"checkout": "Branch wechseln",
"checkoutTag": "Tag als neuen Branch auschecken",
"merge": {
"heading": "Merge des Source Branch in den Target Branch",
"checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.",

View File

@@ -6,6 +6,7 @@
"replace": "Push an existing repository",
"fetch": "Get remote changes",
"checkout": "Switch branch",
"checkoutTag": "Checkout tag as new branch",
"merge": {
"heading": "How to merge source branch into target branch",
"checkout": "1. Make sure your workspace is clean and checkout target branch",

View File

@@ -0,0 +1,46 @@
/*
* 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.
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Tag } from "@scm-manager/ui-types";
type Props = {
tag: Tag;
};
const HgTagInformation: FC<Props> = ({ tag }) => {
const [t] = useTranslation("plugins");
return (
<>
<h4>{t("scm-hg-plugin.information.checkoutTag")}</h4>
<pre>
<code>hg update {tag.name}</code>
</pre>
</>
);
};
export default HgTagInformation;

View File

@@ -28,6 +28,7 @@ import HgAvatar from "./HgAvatar";
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
import HgGlobalConfiguration from "./HgGlobalConfiguration";
import HgBranchInformation from "./HgBranchInformation";
import HgTagInformation from "./HgTagInformation";
const hgPredicate = (props: any) => {
return props.repository && props.repository.type === "hg";
@@ -35,6 +36,7 @@ const hgPredicate = (props: any) => {
binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate);
binder.bind("repos.branch-details.information", HgBranchInformation, hgPredicate);
binder.bind("repos.tag-details.information", HgTagInformation, hgPredicate);
binder.bind("repos.repository-avatar", HgAvatar, hgPredicate);
// bind global configuration

View File

@@ -5,7 +5,8 @@
"create" : "Neues Repository erstellen",
"replace" : "Ein bestehendes Repository aktualisieren",
"fetch": "Remote-Änderungen herunterladen",
"checkout": "Branch wechseln"
"checkout": "Branch wechseln",
"checkoutTag": "Tag auschecken"
},
"config": {
"link": "Mercurial",

View File

@@ -5,7 +5,8 @@
"create" : "Create a new repository",
"replace" : "Push an existing repository",
"fetch": "Get remote changes",
"checkout": "Switch branch"
"checkout": "Switch branch",
"checkoutTag": "Checkout tag"
},
"config": {
"link": "Mercurial",

View File

@@ -27,5 +27,6 @@ import { Links } from "./hal";
export type Tag = {
name: string;
revision: string;
date?: Date;
_links: Links;
};

View File

@@ -31,6 +31,7 @@
"navigationLabel": "Repository",
"informationNavLink": "Informationen",
"branchesNavLink": "Branches",
"tagsNavLink": "Tags",
"sourcesNavLink": "Code",
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell",
@@ -69,6 +70,21 @@
"sources": "Sources",
"defaultTag": "Default"
},
"tags": {
"overview": {
"title": "Übersicht aller verfügbaren Tags",
"noTags": "Keine Tags gefunden.",
"created": "Erstellt"
},
"table": {
"tags": "Tags"
}
},
"tag": {
"name": "Name",
"commit": "Commit",
"sources": "Sources"
},
"code": {
"sources": "Sources",
"commits": "Commits",

View File

@@ -31,6 +31,7 @@
"navigationLabel": "Repository",
"informationNavLink": "Information",
"branchesNavLink": "Branches",
"tagsNavLink": "Tags",
"sourcesNavLink": "Code",
"settingsNavLink": "Settings",
"generalNavLink": "General",
@@ -69,6 +70,21 @@
"sources": "Sources",
"defaultTag": "Default"
},
"tags": {
"overview": {
"title": "Overview of all tags",
"noTags": "No tags found.",
"created": "Created"
},
"table": {
"tags": "Tags"
}
},
"tag": {
"name": "Name",
"commit": "Commit",
"sources": "Sources"
},
"code": {
"sources": "Sources",
"commits": "Commits",

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC } from "react";
import { Link } from "react-router-dom";
import { Branch } from "@scm-manager/ui-types";
import DefaultBranchTag from "./DefaultBranchTag";
@@ -31,24 +31,18 @@ type Props = {
branch: Branch;
};
class BranchRow extends React.Component<Props> {
renderLink(to: string, label: string, defaultBranch?: boolean) {
return (
<Link to={to} title={label}>
{label} <DefaultBranchTag defaultBranch={defaultBranch} />
</Link>
);
}
render() {
const { baseUrl, branch } = this.props;
const BranchRow: FC<Props> = ({ baseUrl, branch }) => {
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
return (
<tr>
<td>{this.renderLink(to, branch.name, branch.defaultBranch)}</td>
<td>
<Link to={to} title={branch.name}>
{branch.name}
<DefaultBranchTag defaultBranch={branch.defaultBranch} />
</Link>
</td>
</tr>
);
}
}
};
export default BranchRow;

View File

@@ -24,7 +24,7 @@
import { FAILURE_SUFFIX, PENDING_SUFFIX, RESET_SUFFIX, SUCCESS_SUFFIX } from "../../../modules/types";
import { apiClient } from "@scm-manager/ui-components";
import { Action, Branch, BranchRequest, Repository } from "@scm-manager/ui-types";
import { Action, Branch, BranchRequest, Repository, Link } from "@scm-manager/ui-types";
import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure";
@@ -65,7 +65,7 @@ export function fetchBranches(repository: Repository) {
return function(dispatch: any) {
dispatch(fetchBranchesPending(repository));
return apiClient
.get(repository._links.branches.href)
.get((repository._links.branches as Link).href)
.then(response => response.json())
.then(data => {
dispatch(fetchBranchesSuccess(data, repository));
@@ -77,7 +77,7 @@ export function fetchBranches(repository: Repository) {
}
export function fetchBranch(repository: Repository, name: string) {
let link = repository._links.branches.href;
let link = (repository._links.branches as Link).href;
if (!link.endsWith("/")) {
link += "/";
}

View File

@@ -54,6 +54,8 @@ import CodeOverview from "../codeSection/containers/CodeOverview";
import ChangesetView from "./ChangesetView";
import SourceExtensions from "../sources/containers/SourceExtensions";
import { FileControlFactory, JumpToFileButton } from "@scm-manager/ui-components";
import TagsOverview from "../tags/container/TagsOverview";
import TagRoot from "../tags/container/TagRoot";
type Props = RouteComponentProps &
WithTranslation & {
@@ -99,6 +101,12 @@ class RepositoryRoot extends React.Component<Props> {
return route.location.pathname.match(regex);
};
matchesTags = (route: any) => {
const url = this.matchedUrl();
const regex = new RegExp(`${url}/tag/.+/info`);
return route.location.pathname.match(regex);
};
matchesCode = (route: any) => {
const url = this.matchedUrl();
const regex = new RegExp(`${url}(/code)/.*`);
@@ -245,6 +253,15 @@ class RepositoryRoot extends React.Component<Props> {
render={() => <BranchesOverview repository={repository} baseUrl={`${url}/branch`} />}
/>
<Route path={`${url}/branches/create`} render={() => <CreateBranch repository={repository} />} />
<Route
path={`${url}/tag/:tag`}
render={() => <TagRoot repository={repository} baseUrl={`${url}/tag`} />}
/>
<Route
path={`${url}/tags`}
exact={true}
render={() => <TagsOverview repository={repository} baseUrl={`${url}/tag`} />}
/>
<ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} />
</Switch>
</PrimaryContentColumn>
@@ -267,6 +284,16 @@ class RepositoryRoot extends React.Component<Props> {
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.branchesNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName="tags"
to={`${url}/tags/`}
icon="fas fa-tags"
label={t("repositoryRoot.menu.tagsNavLink")}
activeWhenMatch={this.matchesTags}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.tagsNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName={this.getCodeLinkname()}

View File

@@ -0,0 +1,53 @@
/*
* 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.
*/
import React, { FC } from "react";
import { Tag, Repository } from "@scm-manager/ui-types";
import { Button, ButtonAddons } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
type Props = {
repository: Repository;
tag: Tag;
};
const TagButtonGroup: FC<Props> = ({ repository, tag }) => {
const [t] = useTranslation("repos");
const changesetLink = `/repo/${repository.namespace}/${repository.name}/code/changeset/${encodeURIComponent(
tag.revision
)}`;
const sourcesLink = `/repo/${repository.namespace}/${repository.name}/sources/${encodeURIComponent(tag.revision)}/`;
return (
<>
<ButtonAddons>
<Button link={changesetLink} icon="exchange-alt" label={t("tag.commit")} reducedMobile={true} />
<Button link={sourcesLink} icon="code" label={t("tag.sources")} reducedMobile={true} />
</ButtonAddons>
</>
);
};
export default TagButtonGroup;

View File

@@ -0,0 +1,73 @@
/*
* 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.
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Repository, Tag } from "@scm-manager/ui-types";
import { DateFromNow, Level } from "@scm-manager/ui-components";
import styled from "styled-components";
import TagButtonGroup from "./TagButtonGroup";
type Props = {
repository: Repository;
tag: Tag;
};
const FlexRow = styled.div`
display: flex;
align-items: center;
`;
const Created = styled.div`
margin-left: 0.5rem;
font-size: 0.8rem;
`;
const Label = styled.strong`
margin-right: 0.3rem;
`;
const Date = styled(DateFromNow)`
font-size: 0.8rem;
`;
const TagDetail: FC<Props> = ({ tag, repository }) => {
const [t] = useTranslation("repos");
return (
<div className="media">
<FlexRow className="media-content subtitle">
<Label>{t("tag.name") + ": "} </Label> {tag.name}
<Created className="is-ellipsis-overflow">
{t("tags.overview.created")} <Date date={tag.date} className="has-text-grey" />
</Created>
</FlexRow>
<div className="media-right">
<TagButtonGroup repository={repository} tag={tag} />
</div>
</div>
);
};
export default TagDetail;

View File

@@ -0,0 +1,60 @@
/*
* 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.
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Tag } from "@scm-manager/ui-types";
import styled from "styled-components";
import { DateFromNow } from "@scm-manager/ui-components";
type Props = {
tag: Tag;
baseUrl: string;
};
const Created = styled.span`
margin-left: 1rem;
font-size: 0.8rem;
`;
const TagRow: FC<Props> = ({ tag, baseUrl }) => {
const [t] = useTranslation("repos");
const to = `${baseUrl}/${encodeURIComponent(tag.name)}/info`;
return (
<tr>
<td>
<Link to={to} title={tag.name}>
{tag.name}
<Created className="has-text-grey is-ellipsis-overflow">
{t("tags.overview.created")} <DateFromNow date={tag.date} />
</Created>
</Link>
</td>
</tr>
);
};
export default TagRow;

View File

@@ -0,0 +1,60 @@
/*
* 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.
*/
import React, { FC } from "react";
import { Tag } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next";
import TagRow from "./TagRow";
type Props = {
baseUrl: string;
tags: Tag[];
};
const TagTable: FC<Props> = ({ baseUrl, tags }) => {
const [t] = useTranslation("repos");
const renderRow = () => {
let rowContent = null;
if (tags) {
rowContent = tags.map((tag, index) => {
return <TagRow key={index} baseUrl={baseUrl} tag={tag} />;
});
}
return rowContent;
};
return (
<table className="card-table table is-hoverable is-fullwidth is-word-break">
<thead>
<tr>
<th>{t("tags.table.tags")}</th>
</tr>
</thead>
<tbody>{renderRow()}</tbody>
</table>
);
};
export default TagTable;

View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
import React, { FC } from "react";
import { Repository, Tag } from "@scm-manager/ui-types";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import TagDetail from "./TagDetail";
type Props = {
repository: Repository;
tag: Tag;
};
const TagView: FC<Props> = ({ repository, tag }) => {
return (
<>
<TagDetail tag={tag} repository={repository} />
<hr />
<div className="content">
<ExtensionPoint
name="repos.tag-details.information"
renderAll={true}
props={{
repository,
tag
}}
/>
</div>
</>
);
};
export default TagView;

View File

@@ -0,0 +1,97 @@
/*
* 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.
*/
import React, { FC, useEffect, useState } from "react";
import { Link, Repository, Tag } from "@scm-manager/ui-types";
import { Redirect, Switch, useLocation, useRouteMatch, Route } from "react-router-dom";
import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import TagView from "../components/TagView";
type Props = {
repository: Repository;
baseUrl: string;
};
const TagRoot: FC<Props> = ({ repository, baseUrl }) => {
const match = useRouteMatch();
const [tags, setTags] = useState<Tag[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | undefined>(undefined);
const [tag, setTag] = useState<Tag>();
useEffect(() => {
const link = (repository._links?.tags as Link)?.href;
if (link) {
apiClient
.get(link)
.then(r => r.json())
.then(r => setTags(r._embedded.tags))
.catch(setError);
}
}, [repository]);
useEffect(() => {
const tagName = decodeURIComponent(match?.params?.tag);
const link = tags?.length > 0 && (tags.find(tag => tag.name === tagName)?._links.self as Link).href;
if (link) {
apiClient
.get(link)
.then(r => r.json())
.then(setTag)
.then(() => setLoading(false))
.catch(setError);
}
}, [tags]);
const stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
return url.substring(0, url.length - 1);
}
return url;
};
const matchedUrl = () => {
return stripEndingSlash(match.url);
};
if (error) {
return <ErrorNotification error={error} />;
}
if (loading || !tags) {
return <Loading />;
}
const url = matchedUrl();
return (
<Switch>
<Redirect exact from={url} to={`${url}/info`} />
<Route path={`${url}/info`} component={() => <TagView repository={repository} tag={tag} />} />
</Switch>
);
};
export default TagRoot;

View File

@@ -0,0 +1,75 @@
/*
* 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.
*/
import React, { FC, useEffect, useState } from "react";
import { Repository, Tag, Link } from "@scm-manager/ui-types";
import { ErrorNotification, Loading, Notification, Subtitle, apiClient } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import orderTags from "../orderTags";
import TagTable from "../components/TagTable";
type Props = {
repository: Repository;
baseUrl: string;
};
const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
const [t] = useTranslation("repos");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | undefined>(undefined);
const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => {
const link = (repository._links?.tags as Link)?.href;
if (link) {
setLoading(true);
apiClient
.get(link)
.then(r => r.json())
.then(r => setTags(r._embedded.tags))
.then(() => setLoading(false))
.catch(setError);
}
}, [repository]);
const renderTagsTable = () => {
if (!loading && tags?.length > 0) {
orderTags(tags);
return <TagTable baseUrl={baseUrl} tags={tags} />;
}
return <Notification type="info">{t("tags.overview.noTags")}</Notification>;
};
if (error) {
return <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
return <>{renderTagsTable()}</>;
};
export default TagsOverview;

View File

@@ -0,0 +1,52 @@
/*
* 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.
*/
import orderTags from "./orderTags";
const tag1 = {
name: "tag1",
revision: "revision1",
date: new Date(2020, 1, 1),
_links: {}
};
const tag2 = {
name: "tag2",
revision: "revision2",
date: new Date(2020, 1, 3),
_links: {}
};
const tag3 = {
name: "tag3",
revision: "revision3",
date: new Date(2020, 1, 2),
_links: {}
};
describe("order tags", () => {
it("should order tags descending by date", () => {
const tags = [tag1, tag2, tag3];
orderTags(tags);
expect(tags).toEqual([tag2, tag3, tag1]);
});
});

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
// sort tags by date beginning with latest first
import { Tag } from "@scm-manager/ui-types";
export default (tags: Tag[]) => {
tags.sort((a, b) => {
return new Date(b.date) - new Date(a.date);
});
};

View File

@@ -31,17 +31,23 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Getter @Setter @NoArgsConstructor
@Getter
@Setter
@NoArgsConstructor
@SuppressWarnings("java:S2160") // we do not need this for dto
public class BranchDto extends HalRepresentation {
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
@NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES)
@NotEmpty
@Length(min = 1, max = 100)
@Pattern(regexp = VALID_BRANCH_NAMES)
private String name;
private String revision;
private boolean defaultBranch;

View File

@@ -120,7 +120,11 @@ public class BranchRootResource {
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException {
public Response get(
@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("branch") String branchName
) throws IOException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
Branches branches = repositoryService.getBranchesCommand().getBranches();
@@ -293,7 +297,10 @@ public class BranchRootResource {
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
public Response getAll(
@PathParam("namespace") String namespace,
@PathParam("name") String name
) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Branches branches = repositoryService.getBranchesCommand().getBranches();
return Response.ok(branchCollectionToDtoMapper.map(repositoryService.getRepository(), branches.getBranches())).build();

View File

@@ -62,5 +62,4 @@ class BranchToBranchDtoMapperTest {
BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold"));
assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master");
}
}