Merge branch 'develop' into feature/rebase

This commit is contained in:
Eduard Heimbuch
2020-09-21 14:01:34 +02:00
45 changed files with 968 additions and 66 deletions

View File

@@ -7,9 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Add support for pr merge with prior rebase ([#1332](https://github.com/scm-manager/scm-manager/pull/1332))
- 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))
- Branch not found right after creation ([#1334](https://github.com/scm-manager/scm-manager/pull/1334))
## [2.5.0] - 2020-09-10
### Added

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

@@ -903,7 +903,7 @@
<properties>
<!-- test libraries -->
<mockito.version>3.5.6</mockito.version>
<mockito.version>3.5.7</mockito.version>
<hamcrest.version>2.1</hamcrest.version>
<junit.version>5.6.2</junit.version>

View File

@@ -24,18 +24,48 @@
package sonia.scm.repository.api;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.BranchCommand;
import java.io.IOException;
/**
* @since 2.0
*/
public final class BranchCommandBuilder {
private static final Logger LOG = LoggerFactory.getLogger(BranchCommandBuilder.class);
private final Repository repository;
private final BranchCommand command;
private final ScmEventBus eventBus;
private final BranchRequest request = new BranchRequest();
public BranchCommandBuilder(Repository repository, BranchCommand command) {
this(repository, command, ScmEventBus.getInstance());
}
/**
* Creates a new {@link BranchCommandBuilder}.
*
* @param command type specific command implementation
*
* @deprecated use {@link #BranchCommandBuilder(Repository, BranchCommand)} instead.
*/
@Deprecated
public BranchCommandBuilder(BranchCommand command) {
this(null, command, ScmEventBus.getInstance());
}
@VisibleForTesting
BranchCommandBuilder(Repository repository, BranchCommand command, ScmEventBus eventBus) {
this.repository = repository;
this.command = command;
this.eventBus = eventBus;
}
/**
@@ -53,17 +83,23 @@ public final class BranchCommandBuilder {
* Execute the command and create a new branch with the given name.
* @param name The name of the new branch.
* @return The created branch.
* @throws IOException
*/
public Branch branch(String name) {
request.setNewBranch(name);
return command.branch(request);
Branch branch = command.branch(request);
fireCreatedEvent(branch);
return branch;
}
private void fireCreatedEvent(Branch branch) {
if (repository != null) {
eventBus.post(new BranchCreatedEvent(repository, branch.getName()));
} else {
LOG.warn("the branch command was created without a repository, so we are not able to fire a BranchCreatedEvent");
}
}
public void delete(String branchName) {
command.deleteOrClose(branchName);
}
private BranchCommand command;
private BranchRequest request = new BranchRequest();
}

View File

@@ -25,8 +25,24 @@
package sonia.scm.repository.api;
public enum MergeStrategy {
MERGE_COMMIT,
FAST_FORWARD_IF_POSSIBLE,
SQUASH,
REBASE
MERGE_COMMIT("merge commit", true),
FAST_FORWARD_IF_POSSIBLE("fast forward if possible", false),
SQUASH("squash", true),
REBASE("rebase", false);
private final String name;
private final boolean commitMessageAllowed;
MergeStrategy(String name, boolean commitMessageAllowed) {
this.name = name;
this.commitMessageAllowed = commitMessageAllowed;
}
public String getName() {
return name;
}
public boolean isCommitMessageAllowed() {
return commitMessageAllowed;
}
}

View File

@@ -177,7 +177,7 @@ public final class RepositoryService implements Closeable {
LOG.debug("create branch command for repository {}",
repository.getNamespaceAndName());
return new BranchCommandBuilder(provider.getBranchCommand());
return new BranchCommandBuilder(repository, provider.getBranchCommand());
}
/**

View File

@@ -298,10 +298,12 @@ public final class RepositoryServiceFactory {
/**
* Clear caches on repository push.
* We do this synchronously, because there are often workflows which are creating branches and fetch them straight
* after the creation.
*
* @param event hook event
*/
@Subscribe(referenceType = ReferenceType.STRONG)
@Subscribe(async = false, referenceType = ReferenceType.STRONG)
public void onEvent(PostReceiveRepositoryHookEvent event) {
Repository repository = event.getRepository();
@@ -324,13 +326,6 @@ public final class RepositoryServiceFactory {
}
}
@Subscribe(async = false)
@SuppressWarnings({"unchecked", "java:S3740", "rawtypes"})
public void onEvent(BranchCreatedEvent event) {
RepositoryCacheKeyPredicate predicate = new RepositoryCacheKeyPredicate(event.getRepository().getId());
cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME).removeAll(predicate);
}
@Subscribe
public void onEvent(PublicKeyDeletedEvent event) {
cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear();

View File

@@ -0,0 +1,105 @@
/*
* 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.api;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.BranchCommand;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class BranchCommandBuilderTest {
@Mock
private BranchCommand command;
@Mock
private ScmEventBus eventBus;
private final Repository repository = new Repository("42", "git", "spaceships", "heart-of-gold");
private final String branchName = "feature/infinite_improbability_drive";
private final Branch branch = Branch.normalBranch(branchName, "42");
@Nested
class Creation {
@BeforeEach
void configureMocks() {
when(command.branch(any())).thenReturn(branch);
}
@Test
void shouldDelegateCreationToCommand() {
BranchCommandBuilder builder = new BranchCommandBuilder(repository, command, eventBus);
Branch returnedBranch = builder.branch(branchName);
assertThat(branch).isSameAs(returnedBranch);
}
@Test
void shouldSendBranchCreatedEvent() {
BranchCommandBuilder builder = new BranchCommandBuilder(repository, command, eventBus);
builder.branch(branchName);
ArgumentCaptor<BranchCreatedEvent> captor = ArgumentCaptor.forClass(BranchCreatedEvent.class);
verify(eventBus).post(captor.capture());
BranchCreatedEvent event = captor.getValue();
assertThat(event.getBranchName()).isEqualTo("feature/infinite_improbability_drive");
}
@Test
void shouldNotSendEventWithoutRepository() {
BranchCommandBuilder builder = new BranchCommandBuilder(null, command, eventBus);
builder.branch(branchName);
verify(eventBus, never()).post(any());
}
}
@Nested
class Deletion {
@Test
void shouldDelegateDeletionToCommand() {
BranchCommandBuilder builder = new BranchCommandBuilder(repository, command, eventBus);
builder.delete(branchName);
verify(command).deleteOrClose(branchName);
}
}
}

View File

@@ -30,7 +30,6 @@ import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
@@ -48,9 +47,7 @@ import java.io.IOException;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static java.util.Collections.*;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class GitBranchCommand extends AbstractGitCommand implements BranchCommand {
@@ -72,8 +69,6 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
Ref ref = git.branchCreate().setStartPoint(request.getParentBranch()).setName(request.getNewBranch()).call();
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
// Clear cache synchronously to avoid branch not found in invalid cache
eventBus.post(new BranchCreatedEvent(repository, request.getNewBranch()));
return Branch.normalBranch(request.getNewBranch(), GitUtil.getId(ref.getObjectId()));
} catch (GitAPIException | IOException ex) {
throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex);

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

@@ -104,7 +104,8 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
@Test
public void shouldThrowExceptionWhenDeletingDefaultBranch() {
String branchToBeDeleted = "master";
assertThrows(CannotDeleteDefaultBranchException.class, () -> createCommand().deleteOrClose(branchToBeDeleted));
GitBranchCommand command = createCommand();
assertThrows(CannotDeleteDefaultBranchException.class, () -> command.deleteOrClose(branchToBeDeleted));
}
private GitBranchCommand createCommand() {
@@ -130,7 +131,6 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
List<Object> events = captor.getAllValues();
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
assertThat(events.get(2)).isInstanceOf(BranchCreatedEvent.class);
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).containsExactly("new_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

@@ -14,7 +14,7 @@
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"file-loader": "^4.2.0",
"mini-css-extract-plugin": "^0.10.0",
"mini-css-extract-plugin": "^0.11.0",
"mustache": "^3.1.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"react-refresh": "^0.8.0",

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");
}
}