Add extension point to branches overview (#1888)

Prepare branches overview to show additional branch details.

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-12-10 11:04:59 +01:00
committed by GitHub
parent b2d7ed88e4
commit b8d6c219ee
21 changed files with 249 additions and 78 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Show additional branch details information ([#1888](https://github.com/scm-manager/scm-manager/pull/1888))

View File

@@ -0,0 +1,87 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import java.util.Optional;
import static java.util.Optional.ofNullable;
/**
* Represents details of a branch that are not computed by default for a {@link Branch}.
*
* @since 2.28.0
*/
public class BranchDetails {
private final String branchName;
private final Integer changesetsAhead;
private final Integer changesetsBehind;
/**
* Create the details object without further details.
*
* @param branchName The name of the branch these details are for.
*/
public BranchDetails(String branchName) {
this(branchName, null, null);
}
/**
* Creates the details object with ahead/behind counts.
*
* @param branchName The name of the branch these details are for.
* @param changesetsAhead The number of changesets this branch is ahead of the default branch (that is
* the number of changesets on this branch that are not reachable from the default branch).
* @param changesetsBehind The number of changesets the default branch is ahead of this branch (that is
*/
public BranchDetails(String branchName, Integer changesetsAhead, Integer changesetsBehind) {
this.branchName = branchName;
this.changesetsAhead = changesetsAhead;
this.changesetsBehind = changesetsBehind;
}
/**
* The name of the branch these details are for.
*/
public String getBranchName() {
return branchName;
}
/**
* The number of changesets this branch is ahead of the default branch (that is
* the number of changesets on this branch that are not reachable from the default branch).
*/
public Optional<Integer> getChangesetsAhead() {
return ofNullable(changesetsAhead);
}
/**
* The number of changesets the default branch is ahead of this branch (that is
* the number of changesets on the default branch that are not reachable from this branch).
*/
public Optional<Integer> getChangesetsBehind() {
return ofNullable(changesetsBehind);
}
}

View File

@@ -24,43 +24,27 @@
package sonia.scm.repository.api;
import java.util.Optional;
import static java.util.Optional.ofNullable;
import sonia.scm.repository.BranchDetails;
/**
* @since 2.28.0
*/
public class BranchDetailsCommandResult {
private final Integer changesetsAhead;
private final Integer changesetsBehind;
private final BranchDetails details;
/**
* Creates the result object
*
* @param changesetsAhead The number of changesets this branch is ahead of the default branch (that is
* the number of changesets on this branch that are not reachable from the default branch).
* @param changesetsBehind The number of changesets the default branch is ahead of this branch (that is
* the number of changesets on the default branch that are not reachable from this branch).
* @param details The details for the branch
*/
public BranchDetailsCommandResult(Integer changesetsAhead, Integer changesetsBehind) {
this.changesetsAhead = changesetsAhead;
this.changesetsBehind = changesetsBehind;
public BranchDetailsCommandResult(BranchDetails details) {
this.details = details;
}
/**
* The number of changesets this branch is ahead of the default branch (that is
* the number of changesets on this branch that are not reachable from the default branch).
* The details for the branch.
*/
public Optional<Integer> getChangesetsAhead() {
return ofNullable(changesetsAhead);
}
/**
* The number of changesets the default branch is ahead of this branch (that is
* the number of changesets on the default branch that are not reachable from this branch).
*/
public Optional<Integer> getChangesetsBehind() {
return ofNullable(changesetsBehind);
public BranchDetails getDetails() {
return details;
}
}

View File

@@ -33,6 +33,7 @@ import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.RevWalkUtils;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchDetails;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.api.BranchDetailsCommandResult;
@@ -52,14 +53,15 @@ public class GitBranchDetailsCommand extends AbstractGitCommand implements Branc
@Override
public BranchDetailsCommandResult execute(BranchDetailsCommandRequest branchDetailsCommandRequest) {
String defaultBranch = context.getConfig().getDefaultBranch();
if (branchDetailsCommandRequest.getBranchName().equals(defaultBranch)) {
return new BranchDetailsCommandResult(0, 0);
String branchName = branchDetailsCommandRequest.getBranchName();
if (branchName.equals(defaultBranch)) {
return new BranchDetailsCommandResult(new BranchDetails(branchName, 0, 0));
}
try {
Repository repository = open();
ObjectId branchCommit = getObjectId(branchDetailsCommandRequest.getBranchName(), repository);
ObjectId branchCommit = getObjectId(branchName, repository);
ObjectId defaultCommit = getObjectId(defaultBranch, repository);
return computeAheadBehind(repository, branchCommit, defaultCommit);
return computeAheadBehind(repository, branchName, branchCommit, defaultCommit);
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "could not compute ahead/behind", e);
}
@@ -73,7 +75,7 @@ public class GitBranchDetailsCommand extends AbstractGitCommand implements Branc
return branchCommit;
}
private BranchDetailsCommandResult computeAheadBehind(Repository repository, ObjectId branchCommit, ObjectId defaultCommit) throws MissingObjectException, IncorrectObjectTypeException {
private BranchDetailsCommandResult computeAheadBehind(Repository repository, String branchName, ObjectId branchCommit, ObjectId defaultCommit) throws MissingObjectException, IncorrectObjectTypeException {
// this implementation is a copy of the implementation in org.eclipse.jgit.lib.BranchTrackingStatus
try (RevWalk walk = new RevWalk(repository)) {
@@ -90,7 +92,7 @@ public class GitBranchDetailsCommand extends AbstractGitCommand implements Branc
int aheadCount = RevWalkUtils.count(walk, localCommit, mergeBase);
int behindCount = RevWalkUtils.count(walk, trackingCommit, mergeBase);
return new BranchDetailsCommandResult(aheadCount, behindCount);
return new BranchDetailsCommandResult(new BranchDetails(branchName, aheadCount, behindCount));
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "could not compute ahead/behind", e);
}

View File

@@ -26,6 +26,7 @@ package sonia.scm.repository.spi;
import org.junit.Test;
import sonia.scm.NotFoundException;
import sonia.scm.repository.BranchDetails;
import sonia.scm.repository.api.BranchDetailsCommandResult;
import static org.assertj.core.api.Assertions.assertThat;
@@ -38,7 +39,7 @@ public class GitBranchDetailsCommandTest extends AbstractGitCommandTestBase {
BranchDetailsCommandRequest request = new BranchDetailsCommandRequest();
request.setBranchName("master");
BranchDetailsCommandResult result = command.execute(request);
BranchDetails result = command.execute(request).getDetails();
assertThat(result.getChangesetsAhead()).get().isEqualTo(0);
assertThat(result.getChangesetsBehind()).get().isEqualTo(0);
@@ -50,7 +51,7 @@ public class GitBranchDetailsCommandTest extends AbstractGitCommandTestBase {
BranchDetailsCommandRequest request = new BranchDetailsCommandRequest();
request.setBranchName("test-branch");
BranchDetailsCommandResult result = command.execute(request);
BranchDetails result = command.execute(request).getDetails();
assertThat(result.getChangesetsAhead()).get().isEqualTo(1);
assertThat(result.getChangesetsBehind()).get().isEqualTo(2);
@@ -62,7 +63,7 @@ public class GitBranchDetailsCommandTest extends AbstractGitCommandTestBase {
BranchDetailsCommandRequest request = new BranchDetailsCommandRequest();
request.setBranchName("partially_merged");
BranchDetailsCommandResult result = command.execute(request);
BranchDetails result = command.execute(request).getDetails();
assertThat(result.getChangesetsAhead()).get().isEqualTo(3);
assertThat(result.getChangesetsBehind()).get().isEqualTo(1);

View File

@@ -28,6 +28,7 @@ import org.javahg.Changeset;
import org.javahg.commands.ExecutionException;
import org.javahg.commands.LogCommand;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchDetails;
import sonia.scm.repository.api.BranchDetailsCommandResult;
import javax.inject.Inject;
@@ -51,18 +52,19 @@ public class HgBranchDetailsCommand implements BranchDetailsCommand {
@Override
public BranchDetailsCommandResult execute(BranchDetailsCommandRequest request) {
if (request.getBranchName().equals(DEFAULT_BRANCH_NAME)) {
return new BranchDetailsCommandResult(0,0);
final String branchName = request.getBranchName();
if (branchName.equals(DEFAULT_BRANCH_NAME)) {
return new BranchDetailsCommandResult(new BranchDetails(branchName, 0, 0));
}
try {
List<Changeset> behind = getChangesetsSolelyOnBranch(DEFAULT_BRANCH_NAME, request.getBranchName());
List<Changeset> ahead = getChangesetsSolelyOnBranch(request.getBranchName(), DEFAULT_BRANCH_NAME);
List<Changeset> behind = getChangesetsSolelyOnBranch(DEFAULT_BRANCH_NAME, branchName);
List<Changeset> ahead = getChangesetsSolelyOnBranch(branchName, DEFAULT_BRANCH_NAME);
return new BranchDetailsCommandResult(ahead.size(), behind.size());
return new BranchDetailsCommandResult(new BranchDetails(branchName, ahead.size(), behind.size()));
} catch (ExecutionException e) {
if (e.getMessage().contains("unknown revision '")) {
throw notFound(entity(Branch.class, request.getBranchName()).in(context.getScmRepository()));
throw notFound(entity(Branch.class, branchName).in(context.getScmRepository()));
}
throw e;
}

View File

@@ -26,7 +26,7 @@ package sonia.scm.repository.spi;
import org.junit.Test;
import sonia.scm.NotFoundException;
import sonia.scm.repository.api.BranchDetailsCommandResult;
import sonia.scm.repository.BranchDetails;
import static org.assertj.core.api.Assertions.assertThat;
@@ -37,7 +37,7 @@ public class HgBranchDetailsCommandTest extends AbstractHgCommandTestBase {
BranchDetailsCommandRequest branchRequest = new BranchDetailsCommandRequest();
branchRequest.setBranchName("testbranch");
BranchDetailsCommandResult result = new HgBranchDetailsCommand(cmdContext).execute(branchRequest);
BranchDetails result = new HgBranchDetailsCommand(cmdContext).execute(branchRequest).getDetails();
assertThat(result.getChangesetsAhead()).get().isEqualTo(1);
assertThat(result.getChangesetsBehind()).get().isEqualTo(3);
@@ -48,7 +48,7 @@ public class HgBranchDetailsCommandTest extends AbstractHgCommandTestBase {
BranchDetailsCommandRequest branchRequest = new BranchDetailsCommandRequest();
branchRequest.setBranchName("with_merge");
BranchDetailsCommandResult result = new HgBranchDetailsCommand(cmdContext).execute(branchRequest);
BranchDetails result = new HgBranchDetailsCommand(cmdContext).execute(branchRequest).getDetails();
assertThat(result.getChangesetsAhead()).get().isEqualTo(5);
assertThat(result.getChangesetsBehind()).get().isEqualTo(1);
@@ -59,7 +59,7 @@ public class HgBranchDetailsCommandTest extends AbstractHgCommandTestBase {
BranchDetailsCommandRequest branchRequest = new BranchDetailsCommandRequest();
branchRequest.setBranchName("next_merge");
BranchDetailsCommandResult result = new HgBranchDetailsCommand(cmdContext).execute(branchRequest);
BranchDetails result = new HgBranchDetailsCommand(cmdContext).execute(branchRequest).getDetails();
assertThat(result.getChangesetsAhead()).get().isEqualTo(3);
assertThat(result.getChangesetsBehind()).get().isEqualTo(0);

View File

@@ -26,7 +26,7 @@ import {
BranchCollection,
BranchCreation,
BranchDetailsCollection,
Link,
Link, NamespaceAndName,
Repository
} from "@scm-manager/ui-types";
import { requiredLink } from "./links";
@@ -61,13 +61,6 @@ export const useBranch = (repository: Repository, name: string): ApiResultWithFe
);
};
export const useBranchDetails = (repository: Repository, branch: string) => {
const link = requiredLink(repository, "branchDetails");
return useQuery<Branch, Error>(branchQueryKey(repository, branch, "details"), () =>
apiClient.get(concat(link, encodeURIComponent(branch))).then(response => response.json())
);
};
function chunkBranches(branches: Branch[]) {
const chunks: Branch[][] = [];
const chunkSize = 5;
@@ -84,6 +77,20 @@ function chunkBranches(branches: Branch[]) {
return chunks;
}
const branchDetailsQueryKey = (
repository: NamespaceAndName,
branch: string | undefined = undefined,
...values: unknown[]
) => {
let branchName;
if (!branch) {
branchName = "_";
} else {
branchName = branch;
}
return [...repoQueryKey(repository), "branch-details", branchName, ...values];
};
export const useBranchDetailsCollection = (repository: Repository, branches: Branch[]) => {
const link = requiredLink(repository, "branchDetailsCollection");
const chunks = chunkBranches(branches);
@@ -93,7 +100,7 @@ export const useBranchDetailsCollection = (repository: Repository, branches: Bra
Error,
BranchDetailsCollection
>(
branchQueryKey(repository, "details"),
branchDetailsQueryKey(repository),
({ pageParam = 0 }) => {
const encodedBranches = chunks[pageParam]?.map(b => encodeURIComponent(b.name)).join("&branches=");
return apiClient.get(concat(link, `?branches=${encodedBranches}`)).then(response => response.json());

View File

@@ -60,7 +60,6 @@
"sass-loader": "^12.3.0",
"storybook-addon-i18next": "^1.3.0",
"storybook-addon-themes": "^6.1.0",
"tabbable": "^5.2.1",
"to-camel-case": "^1.0.0",
"webpack": "^5.61.0",
"worker-plugin": "^3.2.0"
@@ -92,7 +91,8 @@
"remark-gfm": "^1.0.0",
"remark-parse": "^9.0.0",
"remark-rehype": "^8.0.0",
"unified": "^9.2.1"
"unified": "^9.2.1",
"tabbable": "^5.2.1"
},
"babel": {
"presets": [

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.
*/
import { storiesOf } from "@storybook/react";
import React from "react";
import SmallLoadingSpinner from "./SmallLoadingSpinner";
storiesOf("SmallLoading", module).add("Default", () => (
<div>
<SmallLoadingSpinner />
</div>
));

View File

@@ -0,0 +1,34 @@
/*
* 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";
const SmallLoadingSpinner: FC = () => {
return (
<div className="loader-wrapper">
<div className="loader is-loading" />
</div>
);
};
export default SmallLoadingSpinner;

View File

@@ -75216,6 +75216,18 @@ exports[`Storyshots Secondary Navigation Sub Navigation 1`] = `
</div>
`;
exports[`Storyshots SmallLoading Default 1`] = `
<div>
<div
className="loader-wrapper"
>
<div
className="loader is-loading"
/>
</div>
</div>
`;
exports[`Storyshots SplitAndReplace Simple replacement 1`] = `
Array [
<div

View File

@@ -37,7 +37,7 @@ import {
DiffEventHandler,
File,
FileChangeType,
Hunk,
Hunk
} from "./repos";
export { validation, repositories };
@@ -51,6 +51,7 @@ export { default as ErrorPage } from "./ErrorPage";
export { default as Icon } from "./Icon";
export { default as Image } from "./Image";
export { default as Loading } from "./Loading";
export { default as SmallLoadingSpinner } from "./SmallLoadingSpinner";
export { default as Logo } from "./Logo";
export { default as MailLink } from "./MailLink";
export { default as Notification } from "./Notification";
@@ -113,7 +114,7 @@ export {
AnnotationFactory,
AnnotationFactoryContext,
DiffEventHandler,
DiffEventContext,
DiffEventContext
};
// Re-export from ui-api
@@ -130,7 +131,7 @@ export {
MissingLinkError,
createBackendError,
isBackendError,
TOKEN_EXPIRED_ERROR_CODE,
TOKEN_EXPIRED_ERROR_CODE
} from "@scm-manager/ui-api";
export { urls };

View File

@@ -25,13 +25,14 @@
import React from "react";
import {
Branch,
BranchDetails,
File,
IndexResources,
Links,
NamespaceStrategies,
Repository,
RepositoryCreation,
RepositoryTypeCollection,
RepositoryTypeCollection
} from "@scm-manager/ui-types";
import { ExtensionPointDefinition } from "./binder";

View File

@@ -81,7 +81,7 @@ const AheadBehindTag: FC<Props> = ({ branch, details }) => {
message={t("branch.aheadBehind.tooltip", { ahead: details.changesetsAhead, behind: details.changesetsBehind })}
location="top"
>
<div className="columns is-flex is-unselectable is-hidden-mobile">
<div className="columns is-flex is-unselectable is-hidden-mobile mt-1">
<Behind className="column is-half is-flex is-flex-direction-column is-align-items-flex-end p-0">
<Count className="is-size-7 pr-1">{details.changesetsBehind}</Count>
<Bar

View File

@@ -25,19 +25,21 @@ import React, { FC } from "react";
import { Link as ReactLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { Branch, BranchDetails, Link } from "@scm-manager/ui-types";
import { DateFromNow, Icon } from "@scm-manager/ui-components";
import { Branch, BranchDetails, Link, Repository } from "@scm-manager/ui-types";
import { DateFromNow, Icon, SmallLoadingSpinner } from "@scm-manager/ui-components";
import { binder } from "@scm-manager/ui-extensions";
import DefaultBranchTag from "./DefaultBranchTag";
import AheadBehindTag from "./AheadBehindTag";
type Props = {
repository: Repository;
baseUrl: string;
branch: Branch;
onDelete: (branch: Branch) => void;
details?: BranchDetails;
};
const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete, details }) => {
const BranchRow: FC<Props> = ({ repository, baseUrl, branch, onDelete, details }) => {
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
const [t] = useTranslation("repos");
@@ -62,11 +64,7 @@ const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete, details }) => {
if (details) {
return <AheadBehindTag branch={branch} details={details} />;
}
return (
<div className="loader-wrapper">
<div className="loader is-loading" />
</div>
);
return <SmallLoadingSpinner />;
};
const committedAt = (
@@ -86,9 +84,10 @@ const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete, details }) => {
committedAtBy = committedAt;
}
const extensionProps = { repository, branch, details };
return (
<tr>
<td className="is-flex">
<td>
<ReactLink to={to} title={branch.name}>
{branch.name}
</ReactLink>
@@ -99,6 +98,9 @@ const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete, details }) => {
)}
</td>
<td className="has-text-centered">{renderBranchTag()}</td>
{binder.hasExtension("repos.branches.row.details")
? binder.getExtensions("repos.branches.row.details").map(e => <td>{React.createElement(e, extensionProps)}</td>)
: null}
<td className="is-darker has-text-centered">{deleteButton}</td>
</tr>
);

View File

@@ -99,6 +99,7 @@ const BranchTable: FC<Props> = ({ repository, baseUrl, branches, type, branchesD
{(branches || []).map(branch => (
<BranchRow
key={branch.name}
repository={repository}
baseUrl={baseUrl}
branch={branch}
onDelete={onDelete}

View File

@@ -30,27 +30,27 @@ import de.otto.edison.hal.Links;
import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.ObjectFactory;
import sonia.scm.repository.BranchDetails;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.BranchDetailsCommandResult;
import sonia.scm.web.EdisonHalAppender;
import javax.inject.Inject;
import java.util.Optional;
@Mapper
public abstract class BranchDetailsMapper extends BaseMapper<BranchDetailsCommandResult, BranchDetailsDto> {
public abstract class BranchDetailsMapper extends BaseMapper<BranchDetails, BranchDetailsDto> {
@Inject
private ResourceLinks resourceLinks;
abstract BranchDetailsDto map(@Context Repository repository, String branchName, BranchDetailsCommandResult result);
abstract BranchDetailsDto map(@Context Repository repository, String branchName, BranchDetails result);
@ObjectFactory
BranchDetailsDto createDto(@Context Repository repository, String branchName, BranchDetailsCommandResult result) {
BranchDetailsDto createDto(@Context Repository repository, String branchName, BranchDetails result) {
Links.Builder linksBuilder = createLinks(repository, branchName);
Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder();
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), result, branchName, repository);
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), result, repository);
return new BranchDetailsDto(linksBuilder.build(), embeddedBuilder.build());
}

View File

@@ -117,7 +117,7 @@ public class BranchDetailsResource {
) {
try (RepositoryService service = serviceFactory.create(new NamespaceAndName(namespace, name))) {
BranchDetailsCommandResult result = service.getBranchDetailsCommand().execute(branchName);
BranchDetailsDto dto = mapper.map(service.getRepository(), branchName, result);
BranchDetailsDto dto = mapper.map(service.getRepository(), branchName, result.getDetails());
return Response.ok(dto).build();
} catch (CommandNotSupportedException ex) {
return Response.status(Response.Status.BAD_REQUEST).build();
@@ -186,7 +186,7 @@ public class BranchDetailsResource {
for (String branch : branches) {
try {
BranchDetailsCommandResult result = branchDetailsCommand.execute(branch);
dtos.add(mapper.map(service.getRepository(), branch, result));
dtos.add(mapper.map(service.getRepository(), branch, result.getDetails()));
} catch (NotFoundException e) {
// we simply omit details for branches that do not exist
}

View File

@@ -27,9 +27,9 @@ package sonia.scm.api.v2.resources;
import com.google.inject.util.Providers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sonia.scm.repository.BranchDetails;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.api.BranchDetailsCommandResult;
import java.net.URI;
@@ -53,7 +53,7 @@ class BranchDetailsMapperTest {
BranchDetailsDto dto = mapper.map(
repository,
"master",
new BranchDetailsCommandResult(42, 21)
new BranchDetails("master", 42, 21)
);
assertThat(dto.getBranchName()).isEqualTo("master");

View File

@@ -34,6 +34,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.NotFoundException;
import sonia.scm.repository.BranchDetails;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.api.BranchDetailsCommandBuilder;
@@ -49,6 +50,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
@@ -96,7 +98,7 @@ class BranchDetailsResourceTest extends RepositoryTestBase {
when(serviceFactory.create(repository.getNamespaceAndName())).thenReturn(service);
when(service.getRepository()).thenReturn(repository);
when(service.getBranchDetailsCommand()).thenReturn(branchDetailsCommandBuilder);
BranchDetailsCommandResult result = new BranchDetailsCommandResult(42, 21);
BranchDetailsCommandResult result = new BranchDetailsCommandResult(new BranchDetails("master", 42, 21));
when(branchDetailsCommandBuilder.execute("master")).thenReturn(result);
MockHttpRequest request = MockHttpRequest
@@ -141,6 +143,7 @@ class BranchDetailsResourceTest extends RepositoryTestBase {
when(serviceFactory.create(repository.getNamespaceAndName())).thenReturn(service);
when(service.getRepository()).thenReturn(repository);
when(service.getBranchDetailsCommand()).thenReturn(branchDetailsCommandBuilder);
when(branchDetailsCommandBuilder.execute(any())).thenAnswer(invocation -> new BranchDetailsCommandResult(new BranchDetails(invocation.getArgument(0, String.class), null, null)));
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + repository.getNamespaceAndName() + "/branch-details?branches=master&branches=develop&branches=feature%2Fhitchhiker42");