mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-15 17:56:17 +01:00
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:
2
gradle/changelog/branch_details_prs.yaml
Normal file
2
gradle/changelog/branch_details_prs.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Show additional branch details information ([#1888](https://github.com/scm-manager/scm-manager/pull/1888))
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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": [
|
||||
|
||||
32
scm-ui/ui-components/src/SmallLoadingSpinner.stories.tsx
Normal file
32
scm-ui/ui-components/src/SmallLoadingSpinner.stories.tsx
Normal 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>
|
||||
));
|
||||
34
scm-ui/ui-components/src/SmallLoadingSpinner.tsx
Normal file
34
scm-ui/ui-components/src/SmallLoadingSpinner.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user