diff --git a/docs/de/user/repo/assets/repository-compare-view.png b/docs/de/user/repo/assets/repository-compare-view.png new file mode 100644 index 0000000000..09e9b7854d Binary files /dev/null and b/docs/de/user/repo/assets/repository-compare-view.png differ diff --git a/docs/de/user/repo/compare.md b/docs/de/user/repo/compare.md new file mode 100644 index 0000000000..4ac07a039b --- /dev/null +++ b/docs/de/user/repo/compare.md @@ -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. + +![Repository-Compare](assets/repository-compare-view.png) diff --git a/docs/de/user/repo/index.md b/docs/de/user/repo/index.md index a8de61a347..c62cde235d 100644 --- a/docs/de/user/repo/index.md +++ b/docs/de/user/repo/index.md @@ -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/) diff --git a/docs/en/user/repo/assets/repository-compare-view.png b/docs/en/user/repo/assets/repository-compare-view.png new file mode 100644 index 0000000000..f3265997e7 Binary files /dev/null and b/docs/en/user/repo/assets/repository-compare-view.png differ diff --git a/docs/en/user/repo/compare.md b/docs/en/user/repo/compare.md new file mode 100644 index 0000000000..ae1cc49a8b --- /dev/null +++ b/docs/en/user/repo/compare.md @@ -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. + +![Repository-Compare](assets/repository-compare-view.png) + diff --git a/docs/en/user/repo/index.md b/docs/en/user/repo/index.md index 60d1ad9e06..7f4411b18d 100644 --- a/docs/en/user/repo/index.md +++ b/docs/en/user/repo/index.md @@ -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 diff --git a/gradle/changelog/compare.yaml b/gradle/changelog/compare.yaml new file mode 100644 index 0000000000..a63caa0005 --- /dev/null +++ b/gradle/changelog/compare.yaml @@ -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)) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/RepositoryLinkEnricher.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/RepositoryLinkEnricher.java index 1fd927abc5..dbf7efe056 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/RepositoryLinkEnricher.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/RepositoryLinkEnricher.java @@ -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)); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 165bc51c21..cfdddf00df 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -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) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java index b33460fb37..83ecadcb34 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogComputer.java @@ -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 iterator = revWalk.iterator(); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/RepositoryLinkEnricherTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/RepositoryLinkEnricherTest.java index 501afedb92..cff10b201f 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/RepositoryLinkEnricherTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/RepositoryLinkEnricherTest.java @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandWithTagsTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandWithTagsTest.java new file mode 100644 index 0000000000..c04d2e55c1 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandWithTagsTest.java @@ -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"; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java index ca6165ef43..03e6ed0d57 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java @@ -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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java index 324be36452..53726a84eb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java @@ -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 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()); + 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 changesets = null; - int total = 0; + List 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 revisionList = null; + List 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()); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index c4ceefcccf..89bd88d97a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -62,7 +62,8 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { public static final Set FEATURES = EnumSet.of( Feature.COMBINED_DEFAULT_BRANCH, - Feature.MODIFICATIONS_BETWEEN_REVISIONS + Feature.MODIFICATIONS_BETWEEN_REVISIONS, + Feature.INCOMING_REVISION ); private final Injector commandInjector; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgDiffCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgDiffCommandTest.java index 9775a5cd42..3884ac1d30 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgDiffCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgDiffCommandTest.java @@ -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); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandAncestorTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandAncestorTest.java new file mode 100644 index 0000000000..0dbc101485 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandAncestorTest.java @@ -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"; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java index d548ebf75e..ecfecc01bf 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java @@ -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); } diff --git a/scm-ui/ui-api/src/compare.ts b/scm-ui/ui-api/src/compare.ts new file mode 100644 index 0000000000..5254079930 --- /dev/null +++ b/scm-ui/ui-api/src/compare.ts @@ -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 => { + 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( + ["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); + }); + } + } + ); +}; diff --git a/scm-ui/ui-api/src/index.ts b/scm-ui/ui-api/src/index.ts index aa058f1453..16413856f5 100644 --- a/scm-ui/ui-api/src/index.ts +++ b/scm-ui/ui-api/src/index.ts @@ -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"; diff --git a/scm-ui/ui-styles/public/_styleguide.html b/scm-ui/ui-styles/public/_styleguide.html index 5a64176328..942caca161 100644 --- a/scm-ui/ui-styles/public/_styleguide.html +++ b/scm-ui/ui-styles/public/_styleguide.html @@ -957,6 +957,39 @@ sync-alt Update + + + + + + + + + bell + Notifications + + + + + + + + + + shield-alt + Alert + + + + + + + + + + retweet + Compare + diff --git a/scm-ui/ui-styles/src/highcontrast.scss b/scm-ui/ui-styles/src/highcontrast.scss index bb5b2c53e1..1c9d4ae05f 100644 --- a/scm-ui/ui-styles/src/highcontrast.scss +++ b/scm-ui/ui-styles/src/highcontrast.scss @@ -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; diff --git a/scm-ui/ui-styles/src/light.scss b/scm-ui/ui-styles/src/light.scss index 7ef5bcd5cf..68766ba5dd 100644 --- a/scm-ui/ui-styles/src/light.scss +++ b/scm-ui/ui-styles/src/light.scss @@ -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; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 3aab7f2dd3..9cabb5a539 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -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", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index bd8552c276..afd12676a8 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -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", diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx index e5defc75ea..b3b43ee9a8 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx @@ -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 = ({ repository, branch }) => { aheadBehind = null; } + const encodedBranch = encodeURIComponent(branch.name); + return ( <>
@@ -69,6 +72,7 @@ const BranchDetail: FC = ({ repository, branch }) => {
+
diff --git a/scm-ui/ui-webapp/src/repos/codeSection/components/CodeActionBar.tsx b/scm-ui/ui-webapp/src/repos/codeSection/components/CodeActionBar.tsx index c8a235c79e..aef7396eaf 100644 --- a/scm-ui/ui-webapp/src/repos/codeSection/components/CodeActionBar.tsx +++ b/scm-ui/ui-webapp/src/repos/codeSection/components/CodeActionBar.tsx @@ -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 = ({ selectedBranch, branches, onSelectBranch, switchViewLink }) => { +const CodeActionBar: FC = ({ selectedBranch, branches, onSelectBranch, switchViewLink, actions }) => { const { t } = useTranslation("repos"); const location = useLocation(); @@ -71,6 +75,7 @@ const CodeActionBar: FC = ({ selectedBranch, branches, onSelectBranch, sw /> ) } + children={actions} right={} /> diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareLink.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareLink.tsx new file mode 100644 index 0000000000..d6b9f77a33 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/CompareLink.tsx @@ -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 = ({ repository, source, sourceType = "b", target, targetType = "b" }) => { + const [t] = useTranslation("repos"); + + const icon = ; + + if (!target) { + return ( + + {icon} + + ); + } + + return ( + + {icon} + + ); +}; + +export default CompareLink; diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareRoot.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareRoot.tsx new file mode 100644 index 0000000000..0716ba5a60 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/CompareRoot.tsx @@ -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 = ({ repository, baseUrl }) => { + const match = useRouteMatch(); + const { data, isLoading, error } = useBranches(repository); + const url = urls.matchedUrlFromMatch(match); + + if (isLoading || !data) { + return ; + } + if (error) { + return ; + } + + return ( + + + + + {data._embedded && ( + b.defaultBranch)[0].name}`} /> + )} + + ); +}; + +export default CompareRoot; diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx new file mode 100644 index 0000000000..b76dc20440 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx @@ -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 = ({ repository, baseUrl }) => { + const [t] = useTranslation("repos"); + const params = useParams(); + const location = useLocation(); + const history = useHistory(); + const [source, setSource] = useState({ + type: params?.sourceType, + name: decodeURIComponent(params?.sourceName) + }); + const [target, setTarget] = useState({ + 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 ( + + setSource({ type, name })} + selected={source} + /> + + setTarget({ type, name })} + selected={target} + /> + + ); +}; + +export default CompareSelectBar; diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx new file mode 100644 index 0000000000..980c003d0c --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx @@ -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 = ({ onSelect, selected, label, repository }) => { + const [t] = useTranslation("repos"); + const [showDropdown, setShowDropdown] = useState(false); + const [filter, setFilter] = useState(""); + const [selection, setSelection] = useState(selected); + const ref = useRef(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 ( + + + + + + + +
+ +
+

{t("compare.selector.title")}

+
+
+
+ setFilter(e.target.value)} + type="search" + /> + +
+
+
+
+
+
+ ); +}; + +export default CompareSelector; diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareSelectorList.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareSelectorList.tsx new file mode 100644 index 0000000000..88217775ce --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/CompareSelectorList.tsx @@ -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 {t("compare.selector.emptyResult")}; +}; + +const BranchTabContent: FC = ({ elements, selection, onSelectEntry }) => { + if (elements.length === 0) { + return ; + } + + return ( + <> + {elements.map(branch => { + return ( + onSelectEntry("b", branch.name)} + key={branch.name} + > + {branch.name} + + + ); + })} + + ); +}; + +type TagTabContentProps = { + elements: Tag[]; + selection: CompareProps; + onSelectEntry: CompareFunction; +}; + +const TagTabContent: FC = ({ elements, selection, onSelectEntry }) => { + if (elements.length === 0) { + return ; + } + + return ( + <> + {elements.map(tag => ( + onSelectEntry("t", tag.name)} + key={tag.name} + > + {tag.name} + + ))} + + ); +}; + +type RevisionTabContentProps = { + selected: CompareProps; + onSelect: CompareFunction; +}; + +const RevisionTabContent: FC = ({ selected, onSelect }) => { + const [t] = useTranslation("repos"); + const defaultValue = selected.type === "r" ? selected.name : ""; + const [revision, setRevision] = useState(defaultValue); + + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault(); + handleSubmit(); + } + }; + + const handleSubmit = () => { + if (revision) { + onSelect("r", revision); + } + }; + + return ( + +
+
+ setRevision(e.target.value.trim())} + onKeyPress={handleKeyPress} + value={revision.trim()} + /> +
+
+ + {t("compare.selector.revision.submit")} + +
+
+
+ ); +}; + +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 ; + } + if (branchesError || tagsError) { + return ; + } + + if (selectedTab !== "r") { + return ( + + {selectedTab === "b" && ( + branch.name.includes(filter))} + selection={selection} + onSelectEntry={onSelectEntry} + /> + )} + {selectedTab === "t" && ( + tag.name.includes(filter))} + selection={selection} + onSelectEntry={onSelectEntry} + /> + )} + + ); + } + return null; +}; + +const CompareSelectorList: FC = ({ onSelect, selected, repository, filter }) => { + const [t] = useTranslation("repos"); + const [selectedTab, setSelectedTab] = useState(selected.type); + const tabs: CompareTypes[] = ["b", "t", "r"]; + + return ( + <> +
+
    + {tabs.map(tab => { + return ( +
  • + setSelectedTab(tab)} + > + {t("compare.selector.tabs." + tab)} + +
  • + ); + })} +
+
+ + {selectedTab === "r" && } + + ); +}; + +export default CompareSelectorList; diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareSelectorListEntry.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareSelectorListEntry.tsx new file mode 100644 index 0000000000..85bb047b59 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/CompareSelectorListEntry.tsx @@ -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 = ({ children, isSelected = false, onClick }) => ( +
  • + + {children} + +
  • +); + +export default CompareSelectorListEntry; diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareTabs.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareTabs.tsx new file mode 100644 index 0000000000..1765149b32 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/CompareTabs.tsx @@ -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 = ({ baseUrl }) => { + const [t] = useTranslation("repos"); + const location = useLocation(); + const match = useRouteMatch(); + + 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 ( +
    +
      +
    • + {t("compare.tabs.diff")} +
    • +
    • + {t("compare.tabs.commits")} +
    • +
    +
    + ); +}; + +export default CompareTabs; diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareView.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareView.tsx new file mode 100644 index 0000000000..2c1c2cbce4 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/CompareView.tsx @@ -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 = ({ repository, baseUrl }) => { + const match = useRouteMatch(); + const url = urls.matchedUrlFromMatch(match); + const source = decodeURIComponent(match.params.sourceName); + const target = decodeURIComponent(match.params.targetName); + + return ( + + + + + + + + + + + + + ); +}; + +const CompareView: FC = ({ repository, baseUrl }) => { + const [t] = useTranslation("repos"); + + if (!repository._links.incomingDiff) { + return null; + } + + return ( + <> + + + + + + ); +}; + +export default CompareView; diff --git a/scm-ui/ui-webapp/src/repos/compare/IncomingChangesets.tsx b/scm-ui/ui-webapp/src/repos/compare/IncomingChangesets.tsx new file mode 100644 index 0000000000..4ae6463493 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/compare/IncomingChangesets.tsx @@ -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 = ({ repository, source, target }) => { + const page = usePage(); + const { data, error, isLoading } = useIncomingChangesets(repository, source, target, { page: page - 1, limit: 25 }); + + return ; +}; + +export default IncomingChangesets; diff --git a/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx b/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx index 8121489afa..9749159803 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx @@ -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 = ({ repository, branch }) => { const page = usePage(); + const { isLoading, error, data } = useChangesets(repository, { branch, page: page - 1 }); + + return ; +}; + +export const ChangesetsPanel: FC = ({ repository, error, isLoading, data }) => { const [t] = useTranslation("repos"); - const changesets = data?._embedded.changesets; + const page = usePage(); + const changesets = data?._embedded?.changesets; if (error) { return ; @@ -59,23 +72,19 @@ const Changesets: FC = ({ repository, branch }) => { return ; } - if (!data || !changesets || changesets.length === 0) { - return ( -
    - {t("changesets.noChangesets")} -
    - ); + if (!changesets || changesets.length === 0) { + return {t("changesets.noChangesets")}; } return ( - <> +
    - +
    ); }; diff --git a/scm-ui/ui-webapp/src/repos/containers/ChangesetsRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/ChangesetsRoot.tsx index 8ebb000b43..e01071ed3a 100644 --- a/scm-ui/ui-webapp/src/repos/containers/ChangesetsRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/ChangesetsRoot.tsx @@ -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 = ({ repository, baseUrl, branches, selectedBranc onSelectBranch={onSelectBranch} switchViewLink={evaluateSwitchViewLink()} /> -
    - - b.name === selectedBranch)[0]} /> - -
    + + b.name === selectedBranch)[0]} /> + ); }; diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 629cc776cc..e2e08de7dd 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -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 = () => { - ( - - )} - /> - } - /> - } - /> + + + + + + + + + - } /> - } - /> - } /> - } - /> - } - /> + + + + + + + + + + + + + + + + + + diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx index c3145a1520..535ccaa499 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx @@ -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 = ({ 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 = ({ repository, branches, selectedBranch, baseUrl }) = branches={branches} onSelectBranch={onSelectBranch} switchViewLink={evaluateSwitchViewLink()} + actions={ + branches && selectedBranch ? ( + + ) : null + } /> )} {renderPanelContent()} diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx index d2f82678f5..663fea4885 100644 --- a/scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx @@ -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 = ({ repository, tag }) => { const [t] = useTranslation("repos"); + const encodedTag = encodeURIComponent(tag.name); + return (
    = ({ repository, tag }) => {
    +