mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-06 20:30:52 +01:00
Compare branches, tags and revisions (#1920)
Add branch/tag/revision compare to see diffs and changesets between the source and target revisions. This feature is reachable from the branch/tag detail page and also the source code view. Co-authored-by: Florian Scholdei <florian.scholdei@cloudogu.com> Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
BIN
docs/de/user/repo/assets/repository-compare-view.png
Normal file
BIN
docs/de/user/repo/assets/repository-compare-view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
12
docs/de/user/repo/compare.md
Normal file
12
docs/de/user/repo/compare.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
Titel: Repository
|
||||
Untertitel: Vergleichen
|
||||
---
|
||||
### Vergleich von Branches, Tags und Revisionen
|
||||
|
||||
In der Vergleichsansicht können Sie die Diffs und Commits zwischen zwei ausgewählten Repository-Zeigern sehen.
|
||||
Wählen Sie einfach die Quelle und das Ziel aus, worauf Sie den Vergleich durchführen möchten.
|
||||
Dies ist besonders nützlich, wenn Sie sehen möchten, welche Änderungen auf Ihrem Branch vor oder hinter dem Default Branch liegen.
|
||||
Sie können auch sehen, was sich zwischen zwei Versionen geändert hat.
|
||||
|
||||

|
||||
@@ -8,6 +8,7 @@ Der Bereich Repository umfasst alles auf Basis von Repositories in Namespaces. D
|
||||
* [Branches](branches/)
|
||||
* [Tags](tags/)
|
||||
* [Code](code/)
|
||||
* [Compare](compare/)
|
||||
* [Einstellungen](settings/)
|
||||
<!--- AppendLinkContentEnd -->
|
||||
|
||||
|
||||
BIN
docs/en/user/repo/assets/repository-compare-view.png
Normal file
BIN
docs/en/user/repo/assets/repository-compare-view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
12
docs/en/user/repo/compare.md
Normal file
12
docs/en/user/repo/compare.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Repository
|
||||
subtitle: Compare
|
||||
---
|
||||
### Compare Branches, Tags and Revisions
|
||||
|
||||
In the comparison view, you can see the diffs and commits between two selected repository pointers. Simply select the source and target where you want the comparison to take place.
|
||||
This is especially useful if you want to see what changes are ahead or behind the default branch in your branch.
|
||||
You can also see what has changed between two versions.
|
||||
|
||||

|
||||
|
||||
@@ -7,6 +7,7 @@ The Repository area includes everything based on repositories in namespaces. Thi
|
||||
* [Branches](branches/)
|
||||
* [Tags](tags/)
|
||||
* [Code](code/)
|
||||
* [Compare](compare/)
|
||||
* [Settings](settings/)
|
||||
|
||||
### Overview
|
||||
|
||||
2
gradle/changelog/compare.yaml
Normal file
2
gradle/changelog/compare.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Add compare view to see changes between branches, tags and revisions ([#1920](https://github.com/scm-manager/scm-manager/pull/1920))
|
||||
@@ -48,7 +48,7 @@ public class RepositoryLinkEnricher implements HalEnricher {
|
||||
|
||||
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class, GitRepositoryConfigResource.class);
|
||||
|
||||
if (RepositoryPermissions.read(repository).isPermitted()) {
|
||||
if (RepositoryPermissions.read(repository).isPermitted() && repository.getType().equals("git")) {
|
||||
appender.appendLink("defaultBranch", getDefaultBranchLink(repository, linkBuilder));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +627,7 @@ public final class GitUtil {
|
||||
public static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
|
||||
try (RevWalk mergeBaseWalk = new RevWalk(repository)) {
|
||||
mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
|
||||
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1));
|
||||
mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision1));
|
||||
mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2));
|
||||
RevCommit ancestor = mergeBaseWalk.next();
|
||||
if (ancestor == null) {
|
||||
|
||||
@@ -136,13 +136,13 @@ public class GitLogComputer {
|
||||
|
||||
if (branchId != null) {
|
||||
if (startId != null) {
|
||||
revWalk.markStart(revWalk.lookupCommit(startId));
|
||||
revWalk.markStart(revWalk.parseCommit(startId));
|
||||
} else {
|
||||
revWalk.markStart(revWalk.lookupCommit(branchId));
|
||||
revWalk.markStart(revWalk.parseCommit(branchId));
|
||||
}
|
||||
|
||||
if (ancestorId != null) {
|
||||
revWalk.markUninteresting(revWalk.lookupCommit(ancestorId));
|
||||
revWalk.markUninteresting(revWalk.parseCommit(ancestorId));
|
||||
}
|
||||
|
||||
Iterator<RevCommit> iterator = revWalk.iterator();
|
||||
|
||||
@@ -50,7 +50,7 @@ import static org.mockito.Mockito.verify;
|
||||
)
|
||||
class RepositoryLinkEnricherTest {
|
||||
|
||||
private static final Repository REPOSITORY = RepositoryTestData.create42Puzzle();
|
||||
private static final Repository REPOSITORY = RepositoryTestData.create42Puzzle("git");
|
||||
|
||||
private RepositoryLinkEnricher repositoryLinkEnricher;
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.spi;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class GitDiffCommandWithTagsTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void diffBetweenTwoTagsShouldCreateDiff() throws IOException {
|
||||
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext());
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("1.0.0");
|
||||
diffCommandRequest.setAncestorChangeset("test-tag");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
|
||||
assertEquals("diff --git a/a.txt b/a.txt\n" +
|
||||
"index 7898192..2f8bc28 100644\n" +
|
||||
"--- a/a.txt\n" +
|
||||
"+++ b/a.txt\n" +
|
||||
"@@ -1 +1,2 @@\n" +
|
||||
" a\n" +
|
||||
"+line for blame\n", output.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diffBetweenTagAndBranchShouldCreateDiff() throws IOException {
|
||||
GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext());
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("master");
|
||||
diffCommandRequest.setAncestorChangeset("test-tag");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
|
||||
assertEquals("diff --git a/a.txt b/a.txt\n" +
|
||||
"index 7898192..2f8bc28 100644\n" +
|
||||
"--- a/a.txt\n" +
|
||||
"+++ b/a.txt\n" +
|
||||
"@@ -1 +1,2 @@\n" +
|
||||
" a\n" +
|
||||
"+line for blame\n", output.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getZippedRepositoryResource() {
|
||||
return "sonia/scm/repository/spi/scm-git-spi-test-tags.zip";
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class HgDiffCommand extends AbstractCommand implements DiffCommand {
|
||||
@@ -74,7 +73,13 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand {
|
||||
if (format == DiffFormat.GIT) {
|
||||
cmd.git();
|
||||
}
|
||||
cmd.change(HgUtil.getRevision(request.getRevision()));
|
||||
String revision = HgUtil.getRevision(request.getRevision());
|
||||
if (request.getAncestorChangeset() != null) {
|
||||
String ancestor = HgUtil.getRevision(request.getAncestorChangeset());
|
||||
cmd.cmdAppend(String.format("-r ancestor(%s,%s):%s", ancestor, revision, revision));
|
||||
} else {
|
||||
cmd.change(revision);
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,38 +24,22 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
public class HgLogCommand extends AbstractCommand implements LogCommand {
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class HgLogCommand extends AbstractCommand implements LogCommand
|
||||
{
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
* @param context
|
||||
*
|
||||
*/
|
||||
HgLogCommand(HgCommandContext context)
|
||||
{
|
||||
HgLogCommand(HgCommandContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public Changeset getChangeset(String id, LogCommandRequest request) {
|
||||
org.javahg.Repository repository = open();
|
||||
@@ -66,17 +50,15 @@ public class HgLogCommand extends AbstractCommand implements LogCommand
|
||||
|
||||
@Override
|
||||
public ChangesetPagingResult getChangesets(LogCommandRequest request) {
|
||||
ChangesetPagingResult result = null;
|
||||
ChangesetPagingResult result;
|
||||
|
||||
org.javahg.Repository repository = open();
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getPath())
|
||||
||!Strings.isNullOrEmpty(request.getBranch()))
|
||||
{
|
||||
|| !Strings.isNullOrEmpty(request.getBranch())
|
||||
|| !Strings.isNullOrEmpty(request.getAncestorChangeset())) {
|
||||
result = collectSafely(repository, request);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
|
||||
int start = -1;
|
||||
int end = 0;
|
||||
@@ -84,133 +66,107 @@ public class HgLogCommand extends AbstractCommand implements LogCommand
|
||||
String startChangeset = request.getStartChangeset();
|
||||
String endChangeset = request.getEndChangeset();
|
||||
|
||||
if (!Strings.isNullOrEmpty(startChangeset))
|
||||
{
|
||||
if (!Strings.isNullOrEmpty(startChangeset)) {
|
||||
start = on(repository).rev(startChangeset).singleRevision();
|
||||
}
|
||||
else if (!Strings.isNullOrEmpty(endChangeset))
|
||||
{
|
||||
} else if (!Strings.isNullOrEmpty(endChangeset)) {
|
||||
end = on(repository).rev(endChangeset).singleRevision();
|
||||
}
|
||||
|
||||
if (start < 0)
|
||||
{
|
||||
if (start < 0) {
|
||||
start = on(repository).rev("tip").singleRevision();
|
||||
}
|
||||
|
||||
if (start >= 0)
|
||||
{
|
||||
if (start >= 0) {
|
||||
|
||||
int total = start - end + 1;
|
||||
|
||||
if (request.getPagingStart() > 0)
|
||||
{
|
||||
if (request.getPagingStart() > 0) {
|
||||
start -= request.getPagingStart();
|
||||
}
|
||||
|
||||
if (request.getPagingLimit() > 0)
|
||||
{
|
||||
if (request.getPagingLimit() > 0) {
|
||||
end = start - request.getPagingLimit() + 1;
|
||||
}
|
||||
|
||||
if (end < 0)
|
||||
{
|
||||
if (end < 0) {
|
||||
end = 0;
|
||||
}
|
||||
|
||||
List<Changeset> changesets = on(repository).rev(start + ":"
|
||||
+ end).execute();
|
||||
+ end).execute();
|
||||
|
||||
if (request.getBranch() == null) {
|
||||
result = new ChangesetPagingResult(total, changesets);
|
||||
} else {
|
||||
result = new ChangesetPagingResult(total, changesets, request.getBranch());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
|
||||
// empty repository
|
||||
result = new ChangesetPagingResult(0, new ArrayList<Changeset>());
|
||||
result = new ChangesetPagingResult(0, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
* @param request
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private ChangesetPagingResult collectSafely(
|
||||
org.javahg.Repository repository, LogCommandRequest request)
|
||||
{
|
||||
org.javahg.Repository repository, LogCommandRequest request) {
|
||||
HgLogChangesetCommand cmd = on(repository);
|
||||
String startChangeset = request.getStartChangeset();
|
||||
String endChangeset = request.getEndChangeset();
|
||||
String ancestorChangeset = request.getAncestorChangeset();
|
||||
|
||||
if (!Strings.isNullOrEmpty(startChangeset)
|
||||
&&!Strings.isNullOrEmpty(endChangeset))
|
||||
{
|
||||
if (!Strings.isNullOrEmpty(startChangeset) && !Strings.isNullOrEmpty(endChangeset)) {
|
||||
cmd.rev(startChangeset.concat(":").concat(endChangeset));
|
||||
}
|
||||
else if (!Strings.isNullOrEmpty(endChangeset))
|
||||
{
|
||||
} else if (!Strings.isNullOrEmpty(startChangeset) && !Strings.isNullOrEmpty(ancestorChangeset)) {
|
||||
int start = on(repository).rev(startChangeset).singleRevision();
|
||||
int ancestor = on(repository).rev(ancestorChangeset).singleRevision();
|
||||
cmd.rev(String.format("only(%s,%s)", start, ancestor));
|
||||
} else if (!Strings.isNullOrEmpty(endChangeset)) {
|
||||
cmd.rev("tip:".concat(endChangeset));
|
||||
}
|
||||
else if (!Strings.isNullOrEmpty(startChangeset))
|
||||
{
|
||||
} else if (!Strings.isNullOrEmpty(startChangeset)) {
|
||||
cmd.rev(startChangeset.concat(":0"));
|
||||
}
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getBranch()))
|
||||
{
|
||||
if (!Strings.isNullOrEmpty(request.getBranch())) {
|
||||
cmd.branch(request.getBranch());
|
||||
}
|
||||
|
||||
int start = request.getPagingStart();
|
||||
int limit = request.getPagingLimit();
|
||||
|
||||
List<Changeset> changesets = null;
|
||||
int total = 0;
|
||||
List<Changeset> changesets;
|
||||
int total;
|
||||
|
||||
if ((start == 0) && (limit < 0))
|
||||
{
|
||||
if (!Strings.isNullOrEmpty(request.getPath()))
|
||||
{
|
||||
if ((start == 0) && (limit < 0)) {
|
||||
if (!Strings.isNullOrEmpty(request.getPath())) {
|
||||
changesets = cmd.execute(request.getPath());
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
changesets = cmd.execute();
|
||||
}
|
||||
|
||||
total = changesets.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
limit = limit + start;
|
||||
|
||||
List<Integer> revisionList = null;
|
||||
List<Integer> revisionList;
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getPath()))
|
||||
{
|
||||
if (!Strings.isNullOrEmpty(request.getPath())) {
|
||||
revisionList = cmd.loadRevisions(request.getPath());
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
revisionList = cmd.loadRevisions();
|
||||
}
|
||||
|
||||
if ((limit > revisionList.size()) || (limit < 0))
|
||||
{
|
||||
if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) {
|
||||
revisionList = Lists.reverse(revisionList);
|
||||
}
|
||||
|
||||
if (revisionList.isEmpty()) {
|
||||
return new ChangesetPagingResult(0, Collections.emptyList());
|
||||
}
|
||||
|
||||
if ((limit > revisionList.size()) || (limit < 0)) {
|
||||
limit = revisionList.size();
|
||||
}
|
||||
|
||||
@@ -220,8 +176,7 @@ public class HgLogCommand extends AbstractCommand implements LogCommand
|
||||
|
||||
String[] revs = new String[sublist.size()];
|
||||
|
||||
for (int i = 0; i < sublist.size(); i++)
|
||||
{
|
||||
for (int i = 0; i < sublist.size(); i++) {
|
||||
revs[i] = sublist.get(i).toString();
|
||||
}
|
||||
|
||||
@@ -231,16 +186,7 @@ public class HgLogCommand extends AbstractCommand implements LogCommand
|
||||
return new ChangesetPagingResult(total, changesets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private HgLogChangesetCommand on(org.javahg.Repository repository)
|
||||
{
|
||||
private HgLogChangesetCommand on(org.javahg.Repository repository) {
|
||||
return HgLogChangesetCommand.on(repository, getContext().getConfig());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider {
|
||||
|
||||
public static final Set<Feature> FEATURES = EnumSet.of(
|
||||
Feature.COMBINED_DEFAULT_BRANCH,
|
||||
Feature.MODIFICATIONS_BETWEEN_REVISIONS
|
||||
Feature.MODIFICATIONS_BETWEEN_REVISIONS,
|
||||
Feature.INCOMING_REVISION
|
||||
);
|
||||
|
||||
private final Injector commandInjector;
|
||||
|
||||
@@ -53,6 +53,28 @@ public class HgDiffCommandTest extends AbstractHgCommandTestBase {
|
||||
assertThat(content).contains("git");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateDiffComparedToAncestor() throws IOException {
|
||||
DiffCommandRequest request = new DiffCommandRequest();
|
||||
request.setRevision("3049df33fdbbded08b707bac3eccd0f7b453c58b");
|
||||
request.setAncestorChangeset("a9bacaf1b7fa0cebfca71fed4e59ed69a6319427");
|
||||
|
||||
String content = diff(cmdContext, request);
|
||||
assertThat(content)
|
||||
.contains("+++ b/c/d.txt")
|
||||
.contains("+++ b/c/e.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCreateDiffWithAncestorIfNoChangesExists() throws IOException {
|
||||
DiffCommandRequest request = new DiffCommandRequest();
|
||||
request.setRevision("a9bacaf1b7fa0cebfca71fed4e59ed69a6319427");
|
||||
request.setAncestorChangeset("3049df33fdbbded08b707bac3eccd0f7b453c58b");
|
||||
|
||||
String content = diff(cmdContext, request);
|
||||
assertThat(content).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCloseContent() throws IOException {
|
||||
HgCommandContext context = spy(cmdContext);
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.spi;
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
public class HgLogCommandAncestorTest extends AbstractHgCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void testAncestorRange() {
|
||||
LogCommandRequest request = new LogCommandRequest();
|
||||
|
||||
request.setStartChangeset("default");
|
||||
request.setAncestorChangeset("testbranch");
|
||||
|
||||
ChangesetPagingResult result = new HgLogCommand(cmdContext).getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(3, result.getTotal());
|
||||
assertEquals(3, result.getChangesets().size());
|
||||
|
||||
Changeset c1 = result.getChangesets().get(0);
|
||||
Changeset c2 = result.getChangesets().get(1);
|
||||
Changeset c3 = result.getChangesets().get(2);
|
||||
|
||||
assertNotNull(c1);
|
||||
assertEquals("94dd2a4ebc27d30f811d7ac02fbd1cc382386bf3", c1.getId());
|
||||
assertNotNull(c2);
|
||||
assertEquals("aed7afe001a4d5d3111a5916a5656b3032eb4dc2", c2.getId());
|
||||
assertNotNull(c3);
|
||||
assertEquals("03a757fea8b21879d33944b710347c46aa4cfde1", c3.getId());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAncestorReverseRange() {
|
||||
LogCommandRequest request = new LogCommandRequest();
|
||||
|
||||
request.setStartChangeset("testbranch");
|
||||
request.setAncestorChangeset("default");
|
||||
|
||||
ChangesetPagingResult result = new HgLogCommand(cmdContext).getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getTotal());
|
||||
assertEquals(1, result.getChangesets().size());
|
||||
|
||||
Changeset c1 = result.getChangesets().get(0);
|
||||
|
||||
assertNotNull(c1);
|
||||
assertEquals("d2bb8ada6ae68405627d2c757d9d656a4c21799f", c1.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAncestorRangeWithPagination() {
|
||||
LogCommandRequest request = new LogCommandRequest();
|
||||
|
||||
request.setPagingStart(1);
|
||||
request.setPagingLimit(2);
|
||||
request.setStartChangeset("default");
|
||||
request.setAncestorChangeset("testbranch");
|
||||
|
||||
ChangesetPagingResult result = new HgLogCommand(cmdContext).getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(3, result.getTotal());
|
||||
assertEquals(2, result.getChangesets().size());
|
||||
|
||||
Changeset c1 = result.getChangesets().get(0);
|
||||
Changeset c2 = result.getChangesets().get(1);
|
||||
|
||||
assertNotNull(c1);
|
||||
assertEquals("aed7afe001a4d5d3111a5916a5656b3032eb4dc2", c1.getId());
|
||||
assertNotNull(c2);
|
||||
assertEquals("03a757fea8b21879d33944b710347c46aa4cfde1", c2.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAncestorReverseRangeWithPagination() {
|
||||
LogCommandRequest request = new LogCommandRequest();
|
||||
|
||||
request.setPagingStart(0);
|
||||
request.setPagingLimit(2);
|
||||
request.setStartChangeset("testbranch");
|
||||
request.setAncestorChangeset("default");
|
||||
|
||||
ChangesetPagingResult result = new HgLogCommand(cmdContext).getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getTotal());
|
||||
assertEquals(1, result.getChangesets().size());
|
||||
|
||||
Changeset c1 = result.getChangesets().get(0);
|
||||
|
||||
assertNotNull(c1);
|
||||
assertEquals("d2bb8ada6ae68405627d2c757d9d656a4c21799f", c1.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getZippedRepositoryResource() {
|
||||
return "sonia/scm/repository/spi/scm-hg-ahead-behind-test.zip";
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
@Test
|
||||
public void testGetAll() {
|
||||
ChangesetPagingResult result =
|
||||
createComamnd().getChangesets(new LogCommandRequest());
|
||||
createCommand().getChangesets(new LogCommandRequest());
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(5, result.getTotal());
|
||||
@@ -64,7 +64,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
request.setPath("a.txt");
|
||||
|
||||
ChangesetPagingResult result = createComamnd().getChangesets(request);
|
||||
ChangesetPagingResult result = createCommand().getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(3, result.getTotal());
|
||||
@@ -83,7 +83,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
request.setPath("a.txt");
|
||||
|
||||
ChangesetPagingResult result = createComamnd().getChangesets(request);
|
||||
ChangesetPagingResult result = createCommand().getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1,
|
||||
@@ -98,7 +98,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
request.setPagingLimit(2);
|
||||
|
||||
ChangesetPagingResult result = createComamnd().getChangesets(request);
|
||||
ChangesetPagingResult result = createCommand().getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(5, result.getTotal());
|
||||
@@ -122,7 +122,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
request.setPagingStart(1);
|
||||
request.setPagingLimit(2);
|
||||
|
||||
ChangesetPagingResult result = createComamnd().getChangesets(request);
|
||||
ChangesetPagingResult result = createCommand().getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(5, result.getTotal());
|
||||
@@ -141,7 +141,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
@Test
|
||||
public void testGetCommit() throws IOException {
|
||||
HgLogCommand command = createComamnd();
|
||||
HgLogCommand command = createCommand();
|
||||
String revision = "a9bacaf1b7fa0cebfca71fed4e59ed69a6319427";
|
||||
Changeset c =
|
||||
command.getChangeset(revision, null);
|
||||
@@ -173,7 +173,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
request.setStartChangeset("3049df33fdbb");
|
||||
request.setEndChangeset("a9bacaf1b7fa");
|
||||
|
||||
ChangesetPagingResult result = createComamnd().getChangesets(request);
|
||||
ChangesetPagingResult result = createCommand().getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.getTotal());
|
||||
@@ -194,7 +194,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private HgLogCommand createComamnd()
|
||||
private HgLogCommand createCommand()
|
||||
{
|
||||
return new HgLogCommand(cmdContext);
|
||||
}
|
||||
|
||||
90
scm-ui/ui-api/src/compare.ts
Normal file
90
scm-ui/ui-api/src/compare.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 { ChangesetCollection, Link, Repository } from "@scm-manager/ui-types";
|
||||
import { useQuery, useQueryClient } from "react-query";
|
||||
import { ApiResultWithFetching } from "./base";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { changesetQueryKey } from "./changesets";
|
||||
|
||||
function createIncomingUrl(repository: Repository, linkName: string, source: string, target: string) {
|
||||
const link = repository._links[linkName];
|
||||
if ((link as Link)?.templated) {
|
||||
return (link as Link).href
|
||||
.replace("{source}", encodeURIComponent(source))
|
||||
.replace("{target}", encodeURIComponent(target));
|
||||
} else {
|
||||
return (link as Link).href;
|
||||
}
|
||||
}
|
||||
|
||||
export function createChangesetUrl(repository: Repository, source: string, target: string) {
|
||||
return createIncomingUrl(repository, "incomingChangesets", source, target);
|
||||
}
|
||||
|
||||
export function createDiffUrl(repository: Repository, source: string, target: string) {
|
||||
if (repository._links.incomingDiffParsed) {
|
||||
return createIncomingUrl(repository, "incomingDiffParsed", source, target);
|
||||
} else {
|
||||
return createIncomingUrl(repository, "incomingDiff", source, target);
|
||||
}
|
||||
}
|
||||
|
||||
type UseIncomingChangesetsRequest = {
|
||||
page?: string | number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export const useIncomingChangesets = (
|
||||
repository: Repository,
|
||||
source: string,
|
||||
target: string,
|
||||
request?: UseIncomingChangesetsRequest
|
||||
): ApiResultWithFetching<ChangesetCollection> => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
let link = createChangesetUrl(repository, source, target);
|
||||
|
||||
if (request?.page || request?.limit) {
|
||||
if (request?.page && request?.limit) {
|
||||
link = `${link}?page=${request.page}&pageSize=${request.limit}`;
|
||||
} else if (request.page) {
|
||||
link = `${link}?page=${request.page}`;
|
||||
} else if (request.limit) {
|
||||
link = `${link}?pageSize=${request.limit}`;
|
||||
}
|
||||
}
|
||||
|
||||
return useQuery<ChangesetCollection, Error>(
|
||||
["repository", repository.namespace, repository.name, "compare", source, target, "changesets", request?.page || 0],
|
||||
() => apiClient.get(link).then(response => response.json()),
|
||||
{
|
||||
onSuccess: changesetCollection => {
|
||||
changesetCollection._embedded?.changesets.forEach(changeset => {
|
||||
queryClient.setQueryData(changesetQueryKey(repository, changeset.id), changeset);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -62,6 +62,7 @@ export * from "./annotations";
|
||||
export * from "./search";
|
||||
export * from "./loginInfo";
|
||||
export * from "./usePluginCenterAuthInfo";
|
||||
export * from "./compare";
|
||||
|
||||
export { default as ApiProvider } from "./ApiProvider";
|
||||
export * from "./ApiProvider";
|
||||
|
||||
@@ -957,6 +957,39 @@
|
||||
<td><code>sync-alt</code></td>
|
||||
<td>Update</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="level-item">
|
||||
<span class="tooltip has-tooltip-top" data-tooltip="fas fa-bell">
|
||||
<i class="fas fa-lg fa-bell"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td><code>bell</code></td>
|
||||
<td>Notifications</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="level-item">
|
||||
<span class="tooltip has-tooltip-top" data-tooltip="fas fa-shield-alt">
|
||||
<i class="fas fa-lg fa-shield-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td><code>shield-alt</code></td>
|
||||
<td>Alert</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="level-item">
|
||||
<span class="tooltip has-tooltip-top" data-tooltip="fas fa-retweet">
|
||||
<i class="fas fa-lg fa-retweet"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td><code>retweet</code></td>
|
||||
<td>Compare</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -70,6 +70,8 @@ $tooltip-color: $scheme-main;
|
||||
--scm-secondary-background: #{$scheme-main};
|
||||
--scm-secondary-text: #{$white};
|
||||
--scm-border: 2px solid #{$white-ter};
|
||||
--scm-info-color: #{$info};
|
||||
--scm-hover-color: #{$grey};
|
||||
--scm-column-selection: #{$link-dark};
|
||||
|
||||
--sh-base-color: #fff;
|
||||
|
||||
@@ -34,6 +34,8 @@ $button-disabled-opacity: 0.25;
|
||||
--scm-secondary-background: #{$white};
|
||||
--scm-secondary-text: #{$black};
|
||||
--scm-border: 1px solid #dbdbdb;
|
||||
--scm-info-color: #{$info};
|
||||
--scm-hover-color: #{$black-ter};
|
||||
--scm-column-selection: #{$link-25};
|
||||
|
||||
--sh-base-color: #363636;
|
||||
|
||||
@@ -161,6 +161,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"title": "Vergleiche Änderungen",
|
||||
"linkTitle": "Vergleichen mit...",
|
||||
"selector": {
|
||||
"title": "Branch, Tag oder Revision auswählen",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"with": "Vergleiche Änderungen mit...",
|
||||
"filter": "Auswahl filtern...",
|
||||
"emptyResult": "Es wurden keine dem Filter entsprechenden Ergebnisse gefunden.",
|
||||
"tabs": {
|
||||
"b": "Branches",
|
||||
"t": "Tags",
|
||||
"r": "Revision"
|
||||
},
|
||||
"revision": {
|
||||
"input": "Revision eingeben",
|
||||
"submit": "Auswählen"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"diff": "Diff",
|
||||
"commits": "Commits"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"overview": {
|
||||
"title": "Übersicht aller verfügbaren Tags",
|
||||
|
||||
@@ -161,6 +161,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"title": "Compare Changes",
|
||||
"linkTitle": "Compare with...",
|
||||
"selector": {
|
||||
"title": "Select branch, tag or revision",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"with": "Compare changes with...",
|
||||
"filter": "Filter selection...",
|
||||
"emptyResult": "No results matching the filter were found.",
|
||||
"tabs": {
|
||||
"b": "Branches",
|
||||
"t": "Tags",
|
||||
"r": "Revision"
|
||||
},
|
||||
"revision": {
|
||||
"input": "Enter revision",
|
||||
"submit": "Select"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"diff": "Diff",
|
||||
"commits": "Commits"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"overview": {
|
||||
"title": "Overview of All Tags",
|
||||
|
||||
@@ -25,12 +25,13 @@ import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { Subtitle, SmallLoadingSpinner } from "@scm-manager/ui-components";
|
||||
import { SmallLoadingSpinner, Subtitle } from "@scm-manager/ui-components";
|
||||
import BranchButtonGroup from "./BranchButtonGroup";
|
||||
import DefaultBranchTag from "./DefaultBranchTag";
|
||||
import AheadBehindTag from "./AheadBehindTag";
|
||||
import { useBranchDetails } from "@scm-manager/ui-api";
|
||||
import BranchCommitDateCommitter from "./BranchCommitDateCommitter";
|
||||
import CompareLink from "../../compare/CompareLink";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -50,6 +51,8 @@ const BranchDetail: FC<Props> = ({ repository, branch }) => {
|
||||
aheadBehind = null;
|
||||
}
|
||||
|
||||
const encodedBranch = encodeURIComponent(branch.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="media is-align-items-center">
|
||||
@@ -69,6 +72,7 @@ const BranchDetail: FC<Props> = ({ repository, branch }) => {
|
||||
<BranchCommitDateCommitter branch={branch} />
|
||||
</div>
|
||||
</div>
|
||||
<CompareLink repository={repository} source={encodedBranch} target={encodedBranch} />
|
||||
<div className="media-right">
|
||||
<BranchButtonGroup repository={repository} branch={branch} />
|
||||
</div>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Level, BranchSelector } from "@scm-manager/ui-components";
|
||||
import { BranchSelector, Level } from "@scm-manager/ui-components";
|
||||
import CodeViewSwitcher, { SwitchViewLink } from "./CodeViewSwitcher";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Branch } from "@scm-manager/ui-types";
|
||||
@@ -44,6 +44,9 @@ const FlexShrinkLevel = styled(Level)`
|
||||
flex-shrink: 1;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
.level-item {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
@@ -51,9 +54,10 @@ type Props = {
|
||||
branches?: Branch[];
|
||||
onSelectBranch: () => void;
|
||||
switchViewLink: SwitchViewLink;
|
||||
actions?: ReactNode;
|
||||
};
|
||||
|
||||
const CodeActionBar: FC<Props> = ({ selectedBranch, branches, onSelectBranch, switchViewLink }) => {
|
||||
const CodeActionBar: FC<Props> = ({ selectedBranch, branches, onSelectBranch, switchViewLink, actions }) => {
|
||||
const { t } = useTranslation("repos");
|
||||
const location = useLocation();
|
||||
|
||||
@@ -71,6 +75,7 @@ const CodeActionBar: FC<Props> = ({ selectedBranch, branches, onSelectBranch, sw
|
||||
/>
|
||||
)
|
||||
}
|
||||
children={actions}
|
||||
right={<CodeViewSwitcher currentUrl={location.pathname} switchViewLink={switchViewLink} />}
|
||||
/>
|
||||
</ActionBar>
|
||||
|
||||
62
scm-ui/ui-webapp/src/repos/compare/CompareLink.tsx
Normal file
62
scm-ui/ui-webapp/src/repos/compare/CompareLink.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { Icon } from "@scm-manager/ui-components";
|
||||
import { CompareTypes } from "./CompareSelectBar";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
source: string;
|
||||
sourceType?: CompareTypes;
|
||||
target?: string;
|
||||
targetType?: CompareTypes;
|
||||
};
|
||||
|
||||
const CompareLink: FC<Props> = ({ repository, source, sourceType = "b", target, targetType = "b" }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const icon = <Icon name="retweet" title={t("compare.linkTitle")} color="inherit" className="mr-2" />;
|
||||
|
||||
if (!target) {
|
||||
return (
|
||||
<Link to={`/repo/${repository.namespace}/${repository.name}/compare/${sourceType}/${source}`} className="px-1">
|
||||
{icon}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/repo/${repository.namespace}/${repository.name}/compare/${sourceType}/${source}/${targetType}/${target}`}
|
||||
>
|
||||
{icon}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareLink;
|
||||
61
scm-ui/ui-webapp/src/repos/compare/CompareRoot.tsx
Normal file
61
scm-ui/ui-webapp/src/repos/compare/CompareRoot.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { Redirect, Route, Switch, useRouteMatch } from "react-router-dom";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { useBranches } from "@scm-manager/ui-api";
|
||||
import { ErrorNotification, Loading, urls } from "@scm-manager/ui-components";
|
||||
import CompareView, { CompareBranchesParams } from "./CompareView";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
const CompareRoot: FC<Props> = ({ repository, baseUrl }) => {
|
||||
const match = useRouteMatch<CompareBranchesParams>();
|
||||
const { data, isLoading, error } = useBranches(repository);
|
||||
const url = urls.matchedUrlFromMatch(match);
|
||||
|
||||
if (isLoading || !data) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${baseUrl}/:sourceType/:sourceName/:targetType/:targetName`}>
|
||||
<CompareView repository={repository} baseUrl={baseUrl} />
|
||||
</Route>
|
||||
{data._embedded && (
|
||||
<Redirect from={url} to={`${url}/b/${data._embedded.branches.filter(b => b.defaultBranch)[0].name}`} />
|
||||
)}
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareRoot;
|
||||
117
scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx
Normal file
117
scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 { useHistory, useLocation, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { devices, Icon } from "@scm-manager/ui-components";
|
||||
import CompareSelector from "./CompareSelector";
|
||||
import { CompareBranchesParams } from "./CompareView";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
export type CompareTypes = "b" | "t" | "r";
|
||||
|
||||
export type CompareFunction = (type: CompareTypes, name: string) => void;
|
||||
export type CompareProps = {
|
||||
type: CompareTypes;
|
||||
name: string;
|
||||
};
|
||||
const ResponsiveIcon = styled(Icon)`
|
||||
margin: 1rem 0.5rem;
|
||||
transform: rotate(90deg);
|
||||
@media screen and (min-width: ${devices.tablet.width}px) {
|
||||
margin: 1.5rem 1rem 0;
|
||||
transform: rotate(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const ResponsiveBar = styled.div`
|
||||
@media screen and (min-width: ${devices.tablet.width}px) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
}
|
||||
`;
|
||||
|
||||
const CompareSelectBar: FC<Props> = ({ repository, baseUrl }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const params = useParams<CompareBranchesParams>();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const [source, setSource] = useState<CompareProps>({
|
||||
type: params?.sourceType,
|
||||
name: decodeURIComponent(params?.sourceName)
|
||||
});
|
||||
const [target, setTarget] = useState<CompareProps>({
|
||||
type: params?.targetType,
|
||||
name: decodeURIComponent(params?.targetName)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const tabUriComponent = location.pathname.split("/")[9];
|
||||
if (source && target && tabUriComponent) {
|
||||
history.push(
|
||||
baseUrl +
|
||||
"/" +
|
||||
source.type +
|
||||
"/" +
|
||||
encodeURIComponent(source.name) +
|
||||
"/" +
|
||||
target.type +
|
||||
"/" +
|
||||
encodeURIComponent(target.name) +
|
||||
"/" +
|
||||
tabUriComponent +
|
||||
"/" +
|
||||
location.pathname.split("/")[10] || ""
|
||||
);
|
||||
}
|
||||
}, [history, baseUrl, source, target, location.pathname]);
|
||||
|
||||
return (
|
||||
<ResponsiveBar className="is-align-items-center">
|
||||
<CompareSelector
|
||||
repository={repository}
|
||||
label={t("compare.selector.source")}
|
||||
onSelect={(type, name) => setSource({ type, name })}
|
||||
selected={source}
|
||||
/>
|
||||
<ResponsiveIcon name="arrow-right" className="fa-lg" title={t("compare.selector.with")} />
|
||||
<CompareSelector
|
||||
repository={repository}
|
||||
label={t("compare.selector.target")}
|
||||
onSelect={(type, name) => setTarget({ type, name })}
|
||||
selected={target}
|
||||
/>
|
||||
</ResponsiveBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareSelectBar;
|
||||
149
scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx
Normal file
149
scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { devices, Icon } from "@scm-manager/ui-components";
|
||||
import CompareSelectorList from "./CompareSelectorList";
|
||||
import { CompareFunction, CompareProps, CompareTypes } from "./CompareSelectBar";
|
||||
|
||||
type Props = {
|
||||
onSelect: CompareFunction;
|
||||
selected: CompareProps;
|
||||
label: string;
|
||||
repository: Repository;
|
||||
};
|
||||
|
||||
const ResponsiveWrapper = styled.div`
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
@media screen and (min-width: ${devices.tablet.width}px) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
`;
|
||||
|
||||
const BorderedMenu = styled.div`
|
||||
border: var(--scm-border);
|
||||
`;
|
||||
|
||||
const MaxWidthDiv = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const CompareSelector: FC<Props> = ({ onSelect, selected, label, repository }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [selection, setSelection] = useState<CompareProps>(selected);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSelectEntry = (type: CompareTypes, name: string) => {
|
||||
setSelection({ type, name });
|
||||
setShowDropdown(false);
|
||||
onSelect(type, name);
|
||||
};
|
||||
|
||||
const onMousedown = (e: Event) => {
|
||||
if (ref.current && !ref.current.contains(e.target as HTMLElement)) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.which === 27) {
|
||||
// escape
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("mousedown", onMousedown);
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
return () => {
|
||||
window.removeEventListener("mousedown", onMousedown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
};
|
||||
});
|
||||
|
||||
const getActionTypeName = (type: CompareTypes) => {
|
||||
switch (type) {
|
||||
case "b":
|
||||
return "Branch";
|
||||
case "t":
|
||||
return "Tag";
|
||||
case "r":
|
||||
return "Revision";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveWrapper className="field mb-0 is-flex is-flex-direction-column is-fullwidth">
|
||||
<label className="label">{label}</label>
|
||||
<MaxWidthDiv className="control">
|
||||
<MaxWidthDiv className="dropdown is-active" ref={ref}>
|
||||
<MaxWidthDiv className="dropdown-trigger">
|
||||
<button
|
||||
className="button has-text-weight-normal has-text-secondary-more px-4 is-flex is-justify-content-space-between is-fullwidth"
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
>
|
||||
<span className="is-ellipsis-overflow">
|
||||
<strong>{getActionTypeName(selection.type)}:</strong> {selection.name}
|
||||
</span>
|
||||
<span className="icon is-small">
|
||||
<Icon name="angle-down" color="inherit" />
|
||||
</span>
|
||||
</button>
|
||||
</MaxWidthDiv>
|
||||
<div className={classNames("dropdown-menu", { "is-hidden": !showDropdown })} role="menu">
|
||||
<BorderedMenu className="dropdown-content">
|
||||
<div className="dropdown-item">
|
||||
<h3 className="has-text-weight-bold">{t("compare.selector.title")}</h3>
|
||||
</div>
|
||||
<hr className="dropdown-divider my-1" />
|
||||
<div className="dropdown-item px-2">
|
||||
<input
|
||||
className="input is-small"
|
||||
placeholder={t("compare.selector.filter")}
|
||||
onChange={e => setFilter(e.target.value)}
|
||||
type="search"
|
||||
/>
|
||||
<CompareSelectorList
|
||||
onSelect={onSelectEntry}
|
||||
selected={selected}
|
||||
repository={repository}
|
||||
filter={filter}
|
||||
/>
|
||||
</div>
|
||||
</BorderedMenu>
|
||||
</div>
|
||||
</MaxWidthDiv>
|
||||
</MaxWidthDiv>
|
||||
</ResponsiveWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareSelector;
|
||||
272
scm-ui/ui-webapp/src/repos/compare/CompareSelectorList.tsx
Normal file
272
scm-ui/ui-webapp/src/repos/compare/CompareSelectorList.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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, KeyboardEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { Branch, Repository, Tag } from "@scm-manager/ui-types";
|
||||
import { useBranches, useTags } from "@scm-manager/ui-api";
|
||||
import { Button, ErrorNotification, Loading, NoStyleButton, Notification } from "@scm-manager/ui-components";
|
||||
import DefaultBranchTag from "../branches/components/DefaultBranchTag";
|
||||
import CompareSelectorListEntry from "./CompareSelectorListEntry";
|
||||
import { CompareFunction, CompareProps, CompareTypes } from "./CompareSelectBar";
|
||||
|
||||
type Props = {
|
||||
onSelect: CompareFunction;
|
||||
selected: CompareProps;
|
||||
repository: Repository;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
const TabStyleButton = styled(NoStyleButton)`
|
||||
align-items: center;
|
||||
border-bottom: var(--scm-border);
|
||||
color: var(--scm-secondary-text);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: -1px;
|
||||
padding: 0.5rem 1rem;
|
||||
vertical-align: top;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: var(--scm-hover-color);
|
||||
color: var(--scm-hover-color);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-bottom-color: var(--scm-info-color);
|
||||
color: var(--scm-info-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background-color: var(--scm-column-selection);
|
||||
}
|
||||
`;
|
||||
|
||||
const ScrollableUl = styled.ul`
|
||||
max-height: 15.65rem;
|
||||
width: 18.5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const SizedDiv = styled.div`
|
||||
width: 18.5rem;
|
||||
`;
|
||||
|
||||
const SmallButton = styled(Button)`
|
||||
height: 1.875rem;
|
||||
`;
|
||||
|
||||
type BranchTabContentProps = {
|
||||
elements: Branch[];
|
||||
selection: CompareProps;
|
||||
onSelectEntry: CompareFunction;
|
||||
};
|
||||
|
||||
const EmptyResultNotification: FC = () => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
return <Notification type="info">{t("compare.selector.emptyResult")}</Notification>;
|
||||
};
|
||||
|
||||
const BranchTabContent: FC<BranchTabContentProps> = ({ elements, selection, onSelectEntry }) => {
|
||||
if (elements.length === 0) {
|
||||
return <EmptyResultNotification />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{elements.map(branch => {
|
||||
return (
|
||||
<CompareSelectorListEntry
|
||||
isSelected={selection.type === "b" && selection.name === branch.name}
|
||||
onClick={() => onSelectEntry("b", branch.name)}
|
||||
key={branch.name}
|
||||
>
|
||||
<span className="is-ellipsis-overflow">{branch.name}</span>
|
||||
<DefaultBranchTag className="ml-2" defaultBranch={branch.defaultBranch} />
|
||||
</CompareSelectorListEntry>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type TagTabContentProps = {
|
||||
elements: Tag[];
|
||||
selection: CompareProps;
|
||||
onSelectEntry: CompareFunction;
|
||||
};
|
||||
|
||||
const TagTabContent: FC<TagTabContentProps> = ({ elements, selection, onSelectEntry }) => {
|
||||
if (elements.length === 0) {
|
||||
return <EmptyResultNotification />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{elements.map(tag => (
|
||||
<CompareSelectorListEntry
|
||||
isSelected={selection.type === "t" && selection.name === tag.name}
|
||||
onClick={() => onSelectEntry("t", tag.name)}
|
||||
key={tag.name}
|
||||
>
|
||||
<span className="is-ellipsis-overflow">{tag.name}</span>
|
||||
</CompareSelectorListEntry>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type RevisionTabContentProps = {
|
||||
selected: CompareProps;
|
||||
onSelect: CompareFunction;
|
||||
};
|
||||
|
||||
const RevisionTabContent: FC<RevisionTabContentProps> = ({ selected, onSelect }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const defaultValue = selected.type === "r" ? selected.name : "";
|
||||
const [revision, setRevision] = useState(defaultValue);
|
||||
|
||||
const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (revision) {
|
||||
onSelect("r", revision);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SizedDiv className="mt-2">
|
||||
<div className="field has-addons is-justify-content-center">
|
||||
<div className="control">
|
||||
<input
|
||||
className="input is-small"
|
||||
placeholder={t("compare.selector.revision.input")}
|
||||
onChange={e => setRevision(e.target.value.trim())}
|
||||
onKeyPress={handleKeyPress}
|
||||
value={revision.trim()}
|
||||
/>
|
||||
</div>
|
||||
<div className="control">
|
||||
<SmallButton className="is-info is-small" action={handleSubmit} disabled={!revision}>
|
||||
{t("compare.selector.revision.submit")}
|
||||
</SmallButton>
|
||||
</div>
|
||||
</div>
|
||||
</SizedDiv>
|
||||
);
|
||||
};
|
||||
|
||||
const ScrollableList: FC<{ selectedTab: CompareTypes } & Props> = ({
|
||||
selectedTab,
|
||||
onSelect,
|
||||
selected,
|
||||
repository,
|
||||
filter
|
||||
}) => {
|
||||
const { isLoading: branchesIsLoading, error: branchesError, data: branchesData } = useBranches(repository);
|
||||
const branches: Branch[] = (branchesData?._embedded?.branches as Branch[]) || [];
|
||||
const { isLoading: tagsIsLoading, error: tagsError, data: tagsData } = useTags(repository);
|
||||
const tags: Tag[] = (tagsData?._embedded?.tags as Tag[]) || [];
|
||||
const [selection, setSelection] = useState(selected);
|
||||
|
||||
const onSelectEntry = (type: CompareTypes, name: string) => {
|
||||
setSelection({ type, name });
|
||||
onSelect(type, name);
|
||||
};
|
||||
|
||||
if (branchesIsLoading || tagsIsLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (branchesError || tagsError) {
|
||||
return <ErrorNotification error={branchesError || tagsError} />;
|
||||
}
|
||||
|
||||
if (selectedTab !== "r") {
|
||||
return (
|
||||
<ScrollableUl className="py-2 pr-2" aria-expanded="true" role="listbox">
|
||||
{selectedTab === "b" && (
|
||||
<BranchTabContent
|
||||
elements={branches.filter(branch => branch.name.includes(filter))}
|
||||
selection={selection}
|
||||
onSelectEntry={onSelectEntry}
|
||||
/>
|
||||
)}
|
||||
{selectedTab === "t" && (
|
||||
<TagTabContent
|
||||
elements={tags.filter(tag => tag.name.includes(filter))}
|
||||
selection={selection}
|
||||
onSelectEntry={onSelectEntry}
|
||||
/>
|
||||
)}
|
||||
</ScrollableUl>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CompareSelectorList: FC<Props> = ({ onSelect, selected, repository, filter }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [selectedTab, setSelectedTab] = useState<CompareTypes>(selected.type);
|
||||
const tabs: CompareTypes[] = ["b", "t", "r"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tabs is-small mt-3 mb-0">
|
||||
<ul>
|
||||
{tabs.map(tab => {
|
||||
return (
|
||||
<li key={tab}>
|
||||
<TabStyleButton
|
||||
className={classNames({ "is-active": selectedTab === tab })}
|
||||
onClick={() => setSelectedTab(tab)}
|
||||
>
|
||||
{t("compare.selector.tabs." + tab)}
|
||||
</TabStyleButton>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<ScrollableList
|
||||
selectedTab={selectedTab}
|
||||
onSelect={onSelect}
|
||||
selected={selected}
|
||||
repository={repository}
|
||||
filter={filter}
|
||||
/>
|
||||
{selectedTab === "r" && <RevisionTabContent onSelect={onSelect} selected={selected} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareSelectorList;
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { NoStyleButton } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
isSelected?: boolean;
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
};
|
||||
|
||||
const FocusButton = styled(NoStyleButton)`
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:focus:not(.is-active) {
|
||||
background-color: var(--scm-column-selection) !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const CompareSelectorListEntry: FC<Props> = ({ children, isSelected = false, onClick }) => (
|
||||
<li role="option" aria-selected={isSelected}>
|
||||
<FocusButton
|
||||
className={classNames("dropdown-item", "is-flex", "has-text-weight-medium", "px-4", "py-2", {
|
||||
"is-active": isSelected
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</FocusButton>
|
||||
</li>
|
||||
);
|
||||
|
||||
export default CompareSelectorListEntry;
|
||||
60
scm-ui/ui-webapp/src/repos/compare/CompareTabs.tsx
Normal file
60
scm-ui/ui-webapp/src/repos/compare/CompareTabs.tsx
Normal 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 { Link, useLocation, useRouteMatch } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CompareBranchesParams } from "./CompareView";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
const CompareTabs: FC<Props> = ({ baseUrl }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const location = useLocation();
|
||||
const match = useRouteMatch<CompareBranchesParams>();
|
||||
|
||||
const url = `${baseUrl}/${match.params.sourceType}/${match.params.sourceName}/${match.params.targetType}/${match.params.targetName}`;
|
||||
|
||||
const setIsActiveClassName = (path: string) => {
|
||||
const regex = new RegExp(url + path);
|
||||
return location.pathname.match(regex) ? "is-active" : "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tabs mt-5">
|
||||
<ul>
|
||||
<li className={setIsActiveClassName("/diff/")}>
|
||||
<Link to={`${url}/diff/`}>{t("compare.tabs.diff")}</Link>
|
||||
</li>
|
||||
<li className={setIsActiveClassName("/changesets/")}>
|
||||
<Link to={`${url}/changesets/`}>{t("compare.tabs.commits")}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareTabs;
|
||||
86
scm-ui/ui-webapp/src/repos/compare/CompareView.tsx
Normal file
86
scm-ui/ui-webapp/src/repos/compare/CompareView.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 { Redirect, Route, Switch, useRouteMatch } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { createDiffUrl } from "@scm-manager/ui-api";
|
||||
import { LoadingDiff, Subtitle, urls } from "@scm-manager/ui-components";
|
||||
import CompareSelectBar, { CompareTypes } from "./CompareSelectBar";
|
||||
import CompareTabs from "./CompareTabs";
|
||||
import IncomingChangesets from "./IncomingChangesets";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
export type CompareBranchesParams = {
|
||||
sourceType: CompareTypes;
|
||||
sourceName: string;
|
||||
targetType: CompareTypes;
|
||||
targetName: string;
|
||||
};
|
||||
|
||||
const CompareRoutes: FC<Props> = ({ repository, baseUrl }) => {
|
||||
const match = useRouteMatch<CompareBranchesParams>();
|
||||
const url = urls.matchedUrlFromMatch(match);
|
||||
const source = decodeURIComponent(match.params.sourceName);
|
||||
const target = decodeURIComponent(match.params.targetName);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Redirect exact from={url} to={`${url}/diff/`} />
|
||||
<Route path={`${baseUrl}/:sourceType/:sourceName/:targetType/:targetName/diff/`}>
|
||||
<LoadingDiff url={createDiffUrl(repository, source, target) + "?format=GIT"} />
|
||||
</Route>
|
||||
<Route path={`${baseUrl}/:sourceType/:sourceName/:targetType/:targetName/changesets/`} exact>
|
||||
<IncomingChangesets repository={repository} source={source} target={target} />
|
||||
</Route>
|
||||
<Route path={`${baseUrl}/:sourceType/:sourceName/:targetType/:targetName/changesets/:page`} exact>
|
||||
<IncomingChangesets repository={repository} source={source} target={target} />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
const CompareView: FC<Props> = ({ repository, baseUrl }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
if (!repository._links.incomingDiff) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("compare.title")} />
|
||||
<CompareSelectBar repository={repository} baseUrl={baseUrl} />
|
||||
<CompareTabs baseUrl={baseUrl} />
|
||||
<CompareRoutes repository={repository} baseUrl={baseUrl} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareView;
|
||||
43
scm-ui/ui-webapp/src/repos/compare/IncomingChangesets.tsx
Normal file
43
scm-ui/ui-webapp/src/repos/compare/IncomingChangesets.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 } from "@scm-manager/ui-types";
|
||||
import { useIncomingChangesets } from "@scm-manager/ui-api";
|
||||
import { ChangesetsPanel, usePage } from "../containers/Changesets";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
const IncomingChangesets: FC<Props> = ({ repository, source, target }) => {
|
||||
const page = usePage();
|
||||
const { data, error, isLoading } = useIncomingChangesets(repository, source, target, { page: page - 1, limit: 25 });
|
||||
|
||||
return <ChangesetsPanel repository={repository} error={error} isLoading={isLoading} data={data} />;
|
||||
};
|
||||
|
||||
export default IncomingChangesets;
|
||||
@@ -24,14 +24,14 @@
|
||||
import React, { FC } from "react";
|
||||
import { useRouteMatch } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { Branch, ChangesetCollection, Repository } from "@scm-manager/ui-types";
|
||||
import {
|
||||
ChangesetList,
|
||||
ErrorNotification,
|
||||
urls,
|
||||
LinkPaginator,
|
||||
Loading,
|
||||
Notification
|
||||
Notification,
|
||||
urls
|
||||
} from "@scm-manager/ui-components";
|
||||
import { useChangesets } from "@scm-manager/ui-api";
|
||||
|
||||
@@ -40,16 +40,29 @@ type Props = {
|
||||
branch?: Branch;
|
||||
};
|
||||
|
||||
const usePage = () => {
|
||||
type ChangesetProps = Props & {
|
||||
error: Error | null;
|
||||
isLoading: boolean;
|
||||
data?: ChangesetCollection;
|
||||
};
|
||||
|
||||
export const usePage = () => {
|
||||
const match = useRouteMatch();
|
||||
return urls.getPageFromMatch(match);
|
||||
};
|
||||
|
||||
const Changesets: FC<Props> = ({ repository, branch }) => {
|
||||
const page = usePage();
|
||||
|
||||
const { isLoading, error, data } = useChangesets(repository, { branch, page: page - 1 });
|
||||
|
||||
return <ChangesetsPanel repository={repository} branch={branch} error={error} isLoading={isLoading} data={data} />;
|
||||
};
|
||||
|
||||
export const ChangesetsPanel: FC<ChangesetProps> = ({ repository, error, isLoading, data }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const changesets = data?._embedded.changesets;
|
||||
const page = usePage();
|
||||
const changesets = data?._embedded?.changesets;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
@@ -59,23 +72,19 @@ const Changesets: FC<Props> = ({ repository, branch }) => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!data || !changesets || changesets.length === 0) {
|
||||
return (
|
||||
<div className="panel-block">
|
||||
<Notification type="info">{t("changesets.noChangesets")}</Notification>
|
||||
</div>
|
||||
);
|
||||
if (!changesets || changesets.length === 0) {
|
||||
return <Notification type="info">{t("changesets.noChangesets")}</Notification>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="panel">
|
||||
<div className="panel-block">
|
||||
<ChangesetList repository={repository} changesets={changesets} />
|
||||
</div>
|
||||
<div className="panel-footer">
|
||||
<LinkPaginator page={page} collection={data} />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
import React, { FC } from "react";
|
||||
import { Route, useRouteMatch, useHistory } from "react-router-dom";
|
||||
import { Repository, Branch } from "@scm-manager/ui-types";
|
||||
import Changesets from "./Changesets";
|
||||
import CodeActionBar from "../codeSection/components/CodeActionBar";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
import Changesets from "./Changesets";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -74,11 +74,9 @@ const ChangesetRoot: FC<Props> = ({ repository, baseUrl, branches, selectedBranc
|
||||
onSelectBranch={onSelectBranch}
|
||||
switchViewLink={evaluateSwitchViewLink()}
|
||||
/>
|
||||
<div className="panel">
|
||||
<Route path={`${url}/:page?`}>
|
||||
<Changesets repository={repository} branch={branches?.filter(b => b.name === selectedBranch)[0]} />
|
||||
</Route>
|
||||
</div>
|
||||
<Route path={`${url}/:page?`}>
|
||||
<Changesets repository={repository} branch={branches?.filter(b => b.name === selectedBranch)[0]} />
|
||||
</Route>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -57,6 +57,7 @@ import CodeOverview from "../codeSection/containers/CodeOverview";
|
||||
import ChangesetView from "./ChangesetView";
|
||||
import SourceExtensions from "../sources/containers/SourceExtensions";
|
||||
import TagsOverview from "../tags/container/TagsOverview";
|
||||
import CompareRoot from "../compare/CompareRoot";
|
||||
import TagRoot from "../tags/container/TagRoot";
|
||||
import { useIndexLinks, useRepository } from "@scm-manager/ui-api";
|
||||
import styled from "styled-components";
|
||||
@@ -257,41 +258,36 @@ const RepositoryRoot = () => {
|
||||
<Route path={`${url}/settings/permissions`}>
|
||||
<Permissions namespaceOrRepository={repository} />
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
path={`${url}/code/changeset/:id`}
|
||||
render={() => (
|
||||
<ChangesetView repository={repository} fileControlFactoryFactory={fileControlFactoryFactory} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/code/sourceext/:extension`}
|
||||
exact={true}
|
||||
render={() => <SourceExtensions repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/code/sourceext/:extension/:revision/:path*`}
|
||||
render={() => <SourceExtensions repository={repository} baseUrl={`${url}/code/sources`} />}
|
||||
/>
|
||||
<Route exact path={`${url}/code/changeset/:id`}>
|
||||
<ChangesetView repository={repository} fileControlFactoryFactory={fileControlFactoryFactory} />
|
||||
</Route>
|
||||
<Route path={`${url}/code/sourceext/:extension`} exact={true}>
|
||||
<SourceExtensions repository={repository} />
|
||||
</Route>
|
||||
<Route path={`${url}/code/sourceext/:extension/:revision/:path*`}>
|
||||
<SourceExtensions repository={repository} baseUrl={`${url}/code/sources`} />
|
||||
</Route>
|
||||
<Route path={`${url}/code`}>
|
||||
<CodeOverview baseUrl={`${url}/code`} repository={repository} />
|
||||
</Route>
|
||||
<Route path={`${url}/branch/:branch`} render={() => <BranchRoot repository={repository} />} />
|
||||
<Route
|
||||
path={`${url}/branches`}
|
||||
exact={true}
|
||||
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`} />}
|
||||
/>
|
||||
<Route path={`${url}/branch/:branch`}>
|
||||
<BranchRoot repository={repository} />
|
||||
</Route>
|
||||
<Route path={`${url}/branches`} exact={true}>
|
||||
<BranchesOverview repository={repository} baseUrl={`${url}/branch`} />
|
||||
</Route>
|
||||
<Route path={`${url}/branches/create`}>
|
||||
<CreateBranch repository={repository} />
|
||||
</Route>
|
||||
<Route path={`${url}/tag/:tag`}>
|
||||
<TagRoot repository={repository} baseUrl={`${url}/tag`} />
|
||||
</Route>
|
||||
<Route path={`${url}/tags`} exact={true}>
|
||||
<TagsOverview repository={repository} baseUrl={`${url}/tag`} />
|
||||
</Route>
|
||||
<Route path={`${url}/compare/:sourceType/:sourceName`}>
|
||||
<CompareRoot repository={repository} baseUrl={`${url}/compare`} />
|
||||
</Route>
|
||||
<ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} />
|
||||
</Switch>
|
||||
</PrimaryContentColumn>
|
||||
|
||||
@@ -33,6 +33,7 @@ import CodeActionBar from "../../codeSection/components/CodeActionBar";
|
||||
import replaceBranchWithRevision from "../ReplaceBranchWithRevision";
|
||||
import FileSearchButton from "../../codeSection/components/FileSearchButton";
|
||||
import { isEmptyDirectory, isRootFile } from "../utils/files";
|
||||
import CompareLink from "../../compare/CompareLink";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -59,7 +60,7 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [t] = useTranslation("repos");
|
||||
// redirect to default branch is non branch selected
|
||||
// redirect to default branch if no branch selected
|
||||
useEffect(() => {
|
||||
if (branches && branches.length > 0 && !selectedBranch) {
|
||||
const defaultBranch = branches?.filter(b => b.defaultBranch === true)[0];
|
||||
@@ -184,6 +185,11 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
||||
branches={branches}
|
||||
onSelectBranch={onSelectBranch}
|
||||
switchViewLink={evaluateSwitchViewLink()}
|
||||
actions={
|
||||
branches && selectedBranch ? (
|
||||
<CompareLink repository={repository} source={encodeURIComponent(selectedBranch)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{renderPanelContent()}
|
||||
|
||||
@@ -27,6 +27,7 @@ import classNames from "classnames";
|
||||
import { Repository, Tag } from "@scm-manager/ui-types";
|
||||
import { Subtitle, DateFromNow, SignatureIcon } from "@scm-manager/ui-components";
|
||||
import TagButtonGroup from "./TagButtonGroup";
|
||||
import CompareLink from "../../compare/CompareLink";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -36,6 +37,8 @@ type Props = {
|
||||
const TagDetail: FC<Props> = ({ repository, tag }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const encodedTag = encodeURIComponent(tag.name);
|
||||
|
||||
return (
|
||||
<div className="media is-align-items-center">
|
||||
<div
|
||||
@@ -55,6 +58,7 @@ const TagDetail: FC<Props> = ({ repository, tag }) => {
|
||||
<DateFromNow className={classNames("is-size-7", "has-text-secondary")} date={tag.date} />
|
||||
</div>
|
||||
</div>
|
||||
<CompareLink repository={repository} source={encodedTag} sourceType="t" target={encodedTag} targetType="t" />
|
||||
<div className="media-right">
|
||||
<TagButtonGroup repository={repository} tag={tag} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user