Merge branch 'develop' into feature/hg_hooks_over_tcp
# Conflicts: # CHANGELOG.md
16
CHANGELOG.md
@@ -6,9 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- Add tooltips to short links on repository overview ([#1441](https://github.com/scm-manager/scm-manager/pull/1441))
|
||||
- Show the date of the last commit for branches in the frontend ([#1439](https://github.com/scm-manager/scm-manager/pull/1439))
|
||||
- Unify and add description to key view across user settings ([#1440](https://github.com/scm-manager/scm-manager/pull/1440))
|
||||
|
||||
### Changed
|
||||
- Send mercurial hook callbacks over separate tcp socket instead of http ([#1416](https://github.com/scm-manager/scm-manager/pull/1416))
|
||||
|
||||
## [2.10.1] - 2020-11-24
|
||||
### Fixed
|
||||
- Improved logging of failures during plugin installation ([#1442](https://github.com/scm-manager/scm-manager/pull/1442))
|
||||
- Do not throw exception when plugin file does not exist on cancelled installation ([#1442](https://github.com/scm-manager/scm-manager/pull/1442))
|
||||
|
||||
## [2.10.0] - 2020-11-20
|
||||
### Added
|
||||
- Delete branches directly in the UI ([#1422](https://github.com/scm-manager/scm-manager/pull/1422))
|
||||
@@ -419,3 +430,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[2.6.3]: https://www.scm-manager.org/download/2.6.3
|
||||
[2.7.0]: https://www.scm-manager.org/download/2.7.0
|
||||
[2.7.1]: https://www.scm-manager.org/download/2.7.1
|
||||
[2.8.0]: https://www.scm-manager.org/download/2.8.0
|
||||
[2.9.0]: https://www.scm-manager.org/download/2.9.0
|
||||
[2.9.1]: https://www.scm-manager.org/download/2.9.1
|
||||
[2.10.0]: https://www.scm-manager.org/download/2.10.0
|
||||
[2.10.1]: https://www.scm-manager.org/download/2.10.1
|
||||
|
||||
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 285 KiB |
@@ -13,7 +13,7 @@ eingegeben werden. Danach muss das neue Passwort zweimal eingegeben werden.
|
||||
|
||||
## Öffentliche Schlüssel
|
||||
|
||||
Zum Prüfen von Signaturen für z. B. Commits können hier die entsprechenden öffentlichen Schlüssel hinterlegt werden.
|
||||
Zum Prüfen von Signaturen für z. B. Commits können hier die entsprechenden öffentlichen GPG Schlüssel hinterlegt werden.
|
||||
Zudem können hier die vom SCM-Manager erstellten Signaturschlüssel heruntergeladen werden.
|
||||
|
||||
## API Schlüssel
|
||||
|
||||
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 270 KiB |
@@ -3,9 +3,11 @@ title: Repository
|
||||
subtitle: Branches
|
||||
---
|
||||
### Übersicht
|
||||
Auf der Branches-Übersicht sind die bereits existierenden Branches aufgeführt. Bei einem Klick auf einen Branch wird man zur Detailseite des Branches weitergeleitet.
|
||||
Auf der Branches-Übersicht sind die bereits existierenden Branches aufgeführt. Bei einem Klick auf einen Branch wird man zur Detailseite des Branches weitergeleitet.
|
||||
Die Branches sind in zwei Listen aufgeteilt: Unter "Aktive Branches" sind Branches aufgelistet, deren letzter Commit
|
||||
nicht 30 Tage älter als der Stand des Default-Branches ist. Alle älteren Branches sind in der Liste "Stale Branches" zu finden.
|
||||
|
||||
Der Tag "Default" gibt an welcher Branch aktuell, als Standard-Branch dieses Repository im SCM-Manager markiert ist. Der Standard-Branch wird immer zuerst angezeigt, wenn man das Repository im SCM-Manager öffnet.
|
||||
Der Tag "Default" gibt an, welcher Branch aktuell als Standard-Branch dieses Repository im SCM-Manager markiert ist. Der Standard-Branch wird immer zuerst angezeigt, wenn man das Repository im SCM-Manager öffnet.
|
||||
Alle Branches mit Ausnahme des Default Branches können über den Mülleimer-Icon unwiderruflich gelöscht werden.
|
||||
|
||||
Über den "Branch erstellen"-Button gelangt man zum Formular, um neue Branches anzulegen.
|
||||
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 275 KiB |
@@ -11,9 +11,9 @@ Here the password for the current account can be changed when it is a local acco
|
||||
external system). To authorize the change, the current password has to be put first. Then the new password has to be
|
||||
entered twice.
|
||||
|
||||
## Öffentliche Schlüssel
|
||||
## Public Keys
|
||||
|
||||
To check signatures for example for commits, public keys can be stored here. Additionally the keys created by
|
||||
To check signatures (for example for commits), gpg public keys can be stored here. Additionally the keys created by
|
||||
SCM-Manager can be accessed here, too.
|
||||
|
||||
## API keys
|
||||
|
||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 269 KiB |
@@ -4,6 +4,8 @@ subtitle: Branches
|
||||
---
|
||||
### Overview
|
||||
The branches overview shows the branches that are already existing. By clicking on a branch, the details page of the branch is shown.
|
||||
Branches are split into two lists: Branches whose last commits are at most 30 days older than the head of the default
|
||||
branch are listed in "Active Branches". The older ones can be found in "Stale Branches".
|
||||
|
||||
The tag "Default" shows which branch is currently set as the default branch of the repository in SCM-Manager. The default branch is always shown first when opening the repository in SCM-Manager.
|
||||
All branches except the default branch of the repository can be deleted by clicking on the trash bin icon.
|
||||
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 263 KiB |
6
pom.xml
@@ -580,7 +580,7 @@
|
||||
<plugin>
|
||||
<groupId>sonia.scm.maven</groupId>
|
||||
<artifactId>smp-maven-plugin</artifactId>
|
||||
<version>1.3.0</version>
|
||||
<version>1.4.0</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
@@ -903,7 +903,7 @@
|
||||
|
||||
<properties>
|
||||
<!-- test libraries -->
|
||||
<mockito.version>3.5.15</mockito.version>
|
||||
<mockito.version>3.6.0</mockito.version>
|
||||
<hamcrest.version>2.1</hamcrest.version>
|
||||
<junit.version>5.7.0</junit.version>
|
||||
|
||||
@@ -937,7 +937,7 @@
|
||||
<svnkit.version>1.10.1-scm2</svnkit.version>
|
||||
|
||||
<!-- util libraries -->
|
||||
<guava.version>26.0-jre</guava.version>
|
||||
<guava.version>30.0-jre</guava.version>
|
||||
|
||||
<!-- frontend -->
|
||||
<nodejs.version>12.16.1</nodejs.version>
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import sonia.scm.Validateable;
|
||||
@@ -34,10 +32,9 @@ import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Represents a branch in a repository.
|
||||
*
|
||||
@@ -46,73 +43,100 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
@XmlRootElement(name = "branch")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public final class Branch implements Serializable, Validateable
|
||||
{
|
||||
public final class Branch implements Serializable, Validateable {
|
||||
|
||||
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
|
||||
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
|
||||
public static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
|
||||
public static final Pattern VALID_BRANCH_NAME_PATTERN = Pattern.compile(VALID_BRANCH_NAMES);
|
||||
|
||||
/** Field description */
|
||||
private static final long serialVersionUID = -4602244691711222413L;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
private String name;
|
||||
|
||||
private String revision;
|
||||
|
||||
private boolean defaultBranch;
|
||||
|
||||
private Long lastCommitDate;
|
||||
|
||||
private boolean stale = false;
|
||||
|
||||
/**
|
||||
* Constructs a new instance of branch.
|
||||
* This constructor should only be called from JAXB.
|
||||
*
|
||||
*/
|
||||
Branch() {}
|
||||
|
||||
/**
|
||||
* Constructs a new branch.
|
||||
*
|
||||
* @param name name of the branch
|
||||
* @param revision latest revision of the branch
|
||||
* @param defaultBranch Whether this branch is the default branch for the repository
|
||||
*
|
||||
* @deprecated Use {@link Branch#Branch(String, String, boolean, Long)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
Branch(String name, String revision, boolean defaultBranch) {
|
||||
this(name, revision, defaultBranch, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new branch.
|
||||
*
|
||||
* @param name name of the branch
|
||||
* @param revision latest revision of the branch
|
||||
* @param defaultBranch Whether this branch is the default branch for the repository
|
||||
* @param lastCommitDate The date of the commit this branch points to (if computed). May be <code>null</code>
|
||||
*/
|
||||
Branch(String name, String revision, boolean defaultBranch)
|
||||
{
|
||||
Branch(String name, String revision, boolean defaultBranch, Long lastCommitDate) {
|
||||
this.name = name;
|
||||
this.revision = revision;
|
||||
this.defaultBranch = defaultBranch;
|
||||
this.lastCommitDate = lastCommitDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #normalBranch(String, String, Long)} instead to set the date of the last commit, too.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Branch normalBranch(String name, String revision) {
|
||||
return new Branch(name, revision, false);
|
||||
return normalBranch(name, revision, null);
|
||||
}
|
||||
|
||||
public static Branch normalBranch(String name, String revision, Long lastCommitDate) {
|
||||
return new Branch(name, revision, false, lastCommitDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #defaultBranch(String, String, Long)} instead to set the date of the last commit, too.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Branch defaultBranch(String name, String revision) {
|
||||
return new Branch(name, revision, true);
|
||||
return defaultBranch(name, revision, null);
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
public static Branch defaultBranch(String name, String revision, Long lastCommitDate) {
|
||||
return new Branch(name, revision, true, lastCommitDate);
|
||||
}
|
||||
|
||||
public void setStale(boolean stale) {
|
||||
this.stale = stale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return VALID_BRANCH_NAME_PATTERN.matcher(name).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
*
|
||||
* @param obj
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getClass() != obj.getClass())
|
||||
{
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -120,48 +144,31 @@ public final class Branch implements Serializable, Validateable
|
||||
|
||||
return Objects.equal(name, other.name)
|
||||
&& Objects.equal(revision, other.revision)
|
||||
&& Objects.equal(defaultBranch, other.defaultBranch);
|
||||
&& Objects.equal(defaultBranch, other.defaultBranch)
|
||||
&& Objects.equal(lastCommitDate, other.lastCommitDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(name, revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
//J-
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("revision", revision)
|
||||
.add("defaultBranch", defaultBranch)
|
||||
.add("lastCommitDate", lastCommitDate)
|
||||
.toString();
|
||||
//J+
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the name of the branch
|
||||
*
|
||||
*
|
||||
* @return name of the branch
|
||||
*/
|
||||
public String getName()
|
||||
{
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -170,22 +177,27 @@ public final class Branch implements Serializable, Validateable
|
||||
*
|
||||
* @return latest revision of branch
|
||||
*/
|
||||
public String getRevision()
|
||||
{
|
||||
public String getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag whether this branch is configured as the default branch.
|
||||
*/
|
||||
public boolean isDefaultBranch() {
|
||||
return defaultBranch;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
/**
|
||||
* The date of the commit this branch points to, if this was computed (can be empty).
|
||||
*
|
||||
* @since 2.11.0
|
||||
*/
|
||||
public Optional<Long> getLastCommitDate() {
|
||||
return Optional.ofNullable(lastCommitDate);
|
||||
}
|
||||
|
||||
/** name of the branch */
|
||||
private String name;
|
||||
|
||||
/** Field description */
|
||||
private String revision;
|
||||
|
||||
private boolean defaultBranch;
|
||||
public boolean isStale() {
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.spi.BranchStaleComputer;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static java.time.Instant.ofEpochMilli;
|
||||
|
||||
public class BranchXDaysOlderThanDefaultStaleComputer implements BranchStaleComputer {
|
||||
|
||||
public static final int DEFAULT_AMOUNT_OF_DAYS = 30;
|
||||
|
||||
private final int amountOfDays;
|
||||
|
||||
public BranchXDaysOlderThanDefaultStaleComputer() {
|
||||
this(DEFAULT_AMOUNT_OF_DAYS);
|
||||
}
|
||||
|
||||
public BranchXDaysOlderThanDefaultStaleComputer(int amountOfDays) {
|
||||
this.amountOfDays = amountOfDays;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("java:S3655") // we check "isPresent" for both dates, but due to the third check sonar does not get it
|
||||
public boolean computeStale(Branch branch, StaleContext context) {
|
||||
Branch defaultBranch = context.getDefaultBranch();
|
||||
if (shouldCompute(branch, defaultBranch)) {
|
||||
Instant defaultCommitDate = ofEpochMilli(defaultBranch.getLastCommitDate().get());
|
||||
Instant thisCommitDate = ofEpochMilli(branch.getLastCommitDate().get());
|
||||
return thisCommitDate.plus(amountOfDays, ChronoUnit.DAYS).isBefore(defaultCommitDate);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldCompute(Branch branch, Branch defaultBranch) {
|
||||
return !branch.isDefaultBranch() && branch.getLastCommitDate().isPresent() && defaultBranch.getLastCommitDate().isPresent();
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
@@ -165,7 +165,7 @@ public final class BranchesCommandBuilder
|
||||
private Branches getBranchesFromCommand()
|
||||
throws IOException
|
||||
{
|
||||
return new Branches(branchesCommand.getBranches());
|
||||
return new Branches(branchesCommand.getBranchesWithStaleFlags(new BranchXDaysOlderThanDefaultStaleComputer()));
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 lombok.Data;
|
||||
import sonia.scm.repository.Branch;
|
||||
|
||||
public interface BranchStaleComputer {
|
||||
|
||||
boolean computeStale(Branch branch, StaleContext context);
|
||||
|
||||
@Data
|
||||
class StaleContext {
|
||||
private Branch defaultBranch;
|
||||
}
|
||||
}
|
||||
@@ -21,33 +21,52 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.Branch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.18
|
||||
*/
|
||||
public interface BranchesCommand
|
||||
{
|
||||
public interface BranchesCommand {
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
List<Branch> getBranches() throws IOException;
|
||||
|
||||
default List<Branch> getBranchesWithStaleFlags(BranchStaleComputer computer) throws IOException {
|
||||
List<Branch> branches = getBranches();
|
||||
new StaleProcessor(computer, branches).process();
|
||||
return branches;
|
||||
}
|
||||
|
||||
final class StaleProcessor {
|
||||
|
||||
private final BranchStaleComputer computer;
|
||||
private final List<Branch> branches;
|
||||
|
||||
private StaleProcessor(BranchStaleComputer computer, List<Branch> branches) {
|
||||
this.computer = computer;
|
||||
this.branches = branches;
|
||||
}
|
||||
|
||||
private void process() {
|
||||
Optional<Branch> defaultBranch = branches.stream()
|
||||
.filter(Branch::isDefaultBranch)
|
||||
.findFirst();
|
||||
|
||||
defaultBranch.ifPresent(this::process);
|
||||
}
|
||||
|
||||
private void process(Branch defaultBranch) {
|
||||
BranchStaleComputer.StaleContext staleContext = new BranchStaleComputer.StaleContext();
|
||||
staleContext.setDefaultBranch(defaultBranch);
|
||||
|
||||
branches.forEach(branch -> branch.setStale(computer.computeStale(branch, staleContext)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.spi.BranchStaleComputer;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static java.time.Instant.now;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sonia.scm.repository.Branch.defaultBranch;
|
||||
import static sonia.scm.repository.Branch.normalBranch;
|
||||
|
||||
class BranchXDaysOlderThanDefaultStaleComputerTest {
|
||||
|
||||
Instant now = now();
|
||||
|
||||
BranchXDaysOlderThanDefaultStaleComputer computer = new BranchXDaysOlderThanDefaultStaleComputer(30);
|
||||
|
||||
@Test
|
||||
void shouldTagOldBranchAsStale() {
|
||||
long staleTime =
|
||||
now
|
||||
.minus(30, ChronoUnit.DAYS)
|
||||
.minus(1, ChronoUnit.MINUTES)
|
||||
.toEpochMilli();
|
||||
|
||||
Branch branch = normalBranch("hog", "42", staleTime);
|
||||
boolean stale = computer.computeStale(branch, createStaleContext());
|
||||
|
||||
assertThat(stale).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotTagNotSoOldBranchAsStale() {
|
||||
long activeTime =
|
||||
now
|
||||
.minus(30, ChronoUnit.DAYS)
|
||||
.plus(1, ChronoUnit.MINUTES)
|
||||
.toEpochMilli();
|
||||
|
||||
Branch branch = normalBranch("hog", "42", activeTime);
|
||||
boolean stale = computer.computeStale(branch, createStaleContext());
|
||||
|
||||
assertThat(stale).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotTagDefaultBranchAsStale() {
|
||||
long staleTime =
|
||||
now
|
||||
.minus(30, ChronoUnit.DAYS)
|
||||
.minus(1, ChronoUnit.MINUTES)
|
||||
.toEpochMilli();
|
||||
|
||||
Branch branch = defaultBranch("hog", "42", staleTime);
|
||||
boolean stale = computer.computeStale(branch, createStaleContext());
|
||||
|
||||
assertThat(stale).isFalse();
|
||||
}
|
||||
|
||||
BranchStaleComputer.StaleContext createStaleContext() {
|
||||
BranchStaleComputer.StaleContext staleContext = new BranchStaleComputer.StaleContext();
|
||||
staleContext.setDefaultBranch(defaultBranch("default", "23", now.toEpochMilli()));
|
||||
return staleContext;
|
||||
}
|
||||
}
|
||||
@@ -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.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.api.BranchXDaysOlderThanDefaultStaleComputer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
import static java.time.Instant.now;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
class BranchesCommandTest {
|
||||
|
||||
@Test
|
||||
void shouldMarkEachBranchDependingOnDefaultBranch() throws IOException {
|
||||
Instant now = now();
|
||||
long staleTime =
|
||||
now
|
||||
.minus(30, ChronoUnit.DAYS)
|
||||
.minus(1, ChronoUnit.MINUTES)
|
||||
.toEpochMilli();
|
||||
long activeTime =
|
||||
now
|
||||
.minus(30, ChronoUnit.DAYS)
|
||||
.plus(1, ChronoUnit.MINUTES)
|
||||
.toEpochMilli();
|
||||
|
||||
List<Branch> branches = asList(
|
||||
Branch.normalBranch("arthur", "42", staleTime),
|
||||
Branch.normalBranch("marvin", "42", staleTime),
|
||||
Branch.defaultBranch("hog", "42", now.toEpochMilli()),
|
||||
Branch.normalBranch("trillian", "42", activeTime)
|
||||
);
|
||||
|
||||
List<Branch> branchesWithStaleFlags = new BranchesCommand() {
|
||||
@Override
|
||||
public List<Branch> getBranches() {
|
||||
return branches;
|
||||
}
|
||||
}.getBranchesWithStaleFlags(new BranchXDaysOlderThanDefaultStaleComputer());
|
||||
|
||||
Assertions.assertThat(branchesWithStaleFlags)
|
||||
.extracting("stale")
|
||||
.containsExactly(true, true, false, false);
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,14 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Branch;
|
||||
@@ -44,12 +44,9 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
import static sonia.scm.repository.GitUtil.getCommit;
|
||||
import static sonia.scm.repository.GitUtil.getCommitTime;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class GitBranchesCommand extends AbstractGitCommand implements BranchesCommand {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class);
|
||||
@@ -60,23 +57,22 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
|
||||
super(context);
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public List<Branch> getBranches() throws IOException {
|
||||
Git git = createGit();
|
||||
|
||||
String defaultBranchName = determineDefaultBranchName(git);
|
||||
|
||||
try {
|
||||
Repository repository = git.getRepository();
|
||||
try (RevWalk refWalk = new RevWalk(repository)) {
|
||||
return git
|
||||
.branchList()
|
||||
.call()
|
||||
.stream()
|
||||
.map(ref -> createBranchObject(defaultBranchName, ref))
|
||||
.map(ref -> createBranchObject(repository, refWalk, defaultBranchName, ref))
|
||||
.collect(Collectors.toList());
|
||||
} catch (GitAPIException ex) {
|
||||
throw new InternalRepositoryException(repository, "could not read branches", ex);
|
||||
throw new InternalRepositoryException(this.repository, "could not read branches", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,21 +82,31 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Branch createBranchObject(String defaultBranchName, Ref ref) {
|
||||
private Branch createBranchObject(Repository repository, RevWalk refWalk, String defaultBranchName, Ref ref) {
|
||||
String branchName = GitUtil.getBranch(ref);
|
||||
|
||||
if (branchName == null) {
|
||||
LOG.warn("could not determine branch name for branch name {} at revision {}", ref.getName(), ref.getObjectId());
|
||||
return null;
|
||||
} else {
|
||||
Long lastCommitDate = getCommitDate(repository, refWalk, branchName, ref);
|
||||
if (branchName.equals(defaultBranchName)) {
|
||||
return Branch.defaultBranch(branchName, GitUtil.getId(ref.getObjectId()));
|
||||
return Branch.defaultBranch(branchName, GitUtil.getId(ref.getObjectId()), lastCommitDate);
|
||||
} else {
|
||||
return Branch.normalBranch(branchName, GitUtil.getId(ref.getObjectId()));
|
||||
return Branch.normalBranch(branchName, GitUtil.getId(ref.getObjectId()), lastCommitDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Long getCommitDate(Repository repository, RevWalk refWalk, String branchName, Ref ref) {
|
||||
try {
|
||||
return getCommitTime(getCommit(repository, refWalk, ref));
|
||||
} catch (IOException e) {
|
||||
LOG.info("failed to read commit date of branch {} with revision {}", branchName, ref.getName());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String determineDefaultBranchName(Git git) {
|
||||
String defaultBranchName = context.getConfig().getDefaultBranch();
|
||||
if (Strings.isNullOrEmpty(defaultBranchName)) {
|
||||
|
||||
@@ -24,117 +24,26 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.ListBranchCommand;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.GitRepositoryConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Optional.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GitBranchesCommandTest {
|
||||
|
||||
@Mock
|
||||
GitContext context;
|
||||
@Mock
|
||||
Git git;
|
||||
@Mock
|
||||
ListBranchCommand listBranchCommand;
|
||||
@Mock
|
||||
GitRepositoryConfig gitRepositoryConfig;
|
||||
|
||||
GitBranchesCommand branchesCommand;
|
||||
private Ref master;
|
||||
|
||||
@BeforeEach
|
||||
void initContext() {
|
||||
when(context.getConfig()).thenReturn(gitRepositoryConfig);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initCommand() {
|
||||
master = createRef("master", "0000");
|
||||
branchesCommand = new GitBranchesCommand(context) {
|
||||
@Override
|
||||
Git createGit() {
|
||||
return git;
|
||||
}
|
||||
|
||||
@Override
|
||||
Optional<Ref> getRepositoryHeadRef(Git git) {
|
||||
return of(master);
|
||||
}
|
||||
};
|
||||
when(git.branchList()).thenReturn(listBranchCommand);
|
||||
}
|
||||
public class GitBranchesCommandTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Test
|
||||
void shouldCreateEmptyListWithoutBranches() throws IOException, GitAPIException {
|
||||
when(listBranchCommand.call()).thenReturn(emptyList());
|
||||
public void shouldReadBranches() throws IOException {
|
||||
GitBranchesCommand branchesCommand = new GitBranchesCommand(createContext());
|
||||
|
||||
List<Branch> branches = branchesCommand.getBranches();
|
||||
|
||||
assertThat(branches).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapNormalBranch() throws IOException, GitAPIException {
|
||||
Ref branch = createRef("branch", "1337");
|
||||
when(listBranchCommand.call()).thenReturn(asList(branch));
|
||||
|
||||
List<Branch> branches = branchesCommand.getBranches();
|
||||
|
||||
assertThat(branches).containsExactly(Branch.normalBranch("branch", "1337"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkMasterBranchWithMasterFromConfig() throws IOException, GitAPIException {
|
||||
Ref branch = createRef("branch", "1337");
|
||||
when(listBranchCommand.call()).thenReturn(asList(branch));
|
||||
when(gitRepositoryConfig.getDefaultBranch()).thenReturn("branch");
|
||||
|
||||
List<Branch> branches = branchesCommand.getBranches();
|
||||
|
||||
assertThat(branches).containsExactlyInAnyOrder(Branch.defaultBranch("branch", "1337"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkMasterBranchWithMasterFromHead() throws IOException, GitAPIException {
|
||||
Ref branch = createRef("branch", "1337");
|
||||
when(listBranchCommand.call()).thenReturn(asList(branch, master));
|
||||
|
||||
List<Branch> branches = branchesCommand.getBranches();
|
||||
|
||||
assertThat(branches).containsExactlyInAnyOrder(
|
||||
Branch.normalBranch("branch", "1337"),
|
||||
Branch.defaultBranch("master", "0000")
|
||||
assertThat(branches).contains(
|
||||
Branch.defaultBranch("master", "fcd0ef1831e4002ac43ea539f4094334c79ea9ec", 1339428655000L),
|
||||
Branch.normalBranch("mergeable", "91b99de908fcd04772798a31c308a64aea1a5523", 1541586052000L),
|
||||
Branch.normalBranch("rename", "383b954b27e052db6880d57f1c860dc208795247", 1589203061000L)
|
||||
);
|
||||
}
|
||||
|
||||
private Ref createRef(String branchName, String revision) {
|
||||
Ref ref = mock(Ref.class);
|
||||
lenient().when(ref.getName()).thenReturn("refs/heads/" + branchName);
|
||||
ObjectId objectId = mock(ObjectId.class);
|
||||
lenient().when(objectId.name()).thenReturn(revision);
|
||||
lenient().when(ref.getObjectId()).thenReturn(objectId);
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ package sonia.scm.repository.spi;
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.aragost.javahg.Changeset;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Lists;
|
||||
import sonia.scm.repository.Branch;
|
||||
|
||||
@@ -63,14 +62,8 @@ public class HgBranchesCommand extends AbstractCommand
|
||||
List<com.aragost.javahg.Branch> hgBranches =
|
||||
com.aragost.javahg.commands.BranchesCommand.on(open()).execute();
|
||||
|
||||
List<Branch> branches = Lists.transform(hgBranches,
|
||||
new Function<com.aragost.javahg.Branch,
|
||||
Branch>()
|
||||
{
|
||||
|
||||
@Override
|
||||
public Branch apply(com.aragost.javahg.Branch hgBranch)
|
||||
{
|
||||
return Lists.transform(hgBranches,
|
||||
hgBranch -> {
|
||||
String node = null;
|
||||
Changeset changeset = hgBranch.getBranchTip();
|
||||
|
||||
@@ -79,14 +72,12 @@ public class HgBranchesCommand extends AbstractCommand
|
||||
node = changeset.getNode();
|
||||
}
|
||||
|
||||
long lastCommitDate = changeset.getTimestamp().getDate().getTime();
|
||||
if (DEFAULT_BRANCH_NAME.equals(hgBranch.getName())) {
|
||||
return Branch.defaultBranch(hgBranch.getName(), node);
|
||||
return Branch.defaultBranch(hgBranch.getName(), node, lastCommitDate);
|
||||
} else {
|
||||
return Branch.normalBranch(hgBranch.getName(), node);
|
||||
return Branch.normalBranch(hgBranch.getName(), node, lastCommitDate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return branches;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.Branch;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sonia.scm.repository.Branch.defaultBranch;
|
||||
import static sonia.scm.repository.Branch.normalBranch;
|
||||
|
||||
public class HgBranchesCommandTest extends AbstractHgCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldReadBranches() {
|
||||
HgBranchesCommand command = new HgBranchesCommand(cmdContext);
|
||||
|
||||
List<Branch> branches = command.getBranches();
|
||||
|
||||
assertThat(branches).contains(
|
||||
defaultBranch("default", "2baab8e80280ef05a9aa76c49c76feca2872afb7", 1339586381000L),
|
||||
normalBranch("test-branch", "79b6baf49711ae675568e0698d730b97ef13e84a", 1339586299000L)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,14 @@
|
||||
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import { BranchSelector } from "./index";
|
||||
import { Branch } from "@scm-manager/ui-types/src";
|
||||
import { Branch } from "@scm-manager/ui-types";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const master = { name: "master", revision: "1", defaultBranch: true, _links: {} };
|
||||
const develop = { name: "develop", revision: "2", defaultBranch: false, _links: {} };
|
||||
|
||||
const branchSelected = (branch?: Branch) => {};
|
||||
const branchSelected = (branch?: Branch) => null;
|
||||
|
||||
const branches = [master, develop];
|
||||
|
||||
@@ -42,6 +42,4 @@ const Wrapper = styled.div`
|
||||
|
||||
storiesOf("BranchSelector", module)
|
||||
.addDecorator(storyFn => <Wrapper>{storyFn()}</Wrapper>)
|
||||
.add("Default", () => (
|
||||
<BranchSelector branches={branches} onSelectBranch={branchSelected} label="Select branch:" />
|
||||
));
|
||||
.add("Default", () => <BranchSelector branches={branches} onSelectBranch={branchSelected} label="Select branch:" />);
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import React, { FC } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { Button, DropDown, urls } from "./index";
|
||||
import { FilterInput } from "./forms";
|
||||
|
||||
type Props = RouteComponentProps & {
|
||||
type Props = {
|
||||
showCreateButton: boolean;
|
||||
currentGroup: string;
|
||||
groups: string[];
|
||||
@@ -35,41 +35,33 @@ type Props = RouteComponentProps & {
|
||||
groupSelected: (namespace: string) => void;
|
||||
label?: string;
|
||||
testId?: string;
|
||||
searchPlaceholder?: string;
|
||||
};
|
||||
|
||||
class OverviewPageActions extends React.Component<Props> {
|
||||
render() {
|
||||
const { history, currentGroup, groups, location, link, testId, groupSelected } = this.props;
|
||||
const groupSelector = groups && (
|
||||
<div className={"column is-flex"}>
|
||||
<DropDown
|
||||
className={"is-fullwidth"}
|
||||
options={groups}
|
||||
preselectedOption={currentGroup}
|
||||
optionSelected={groupSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const OverviewPageActions: FC<Props> = ({
|
||||
groups,
|
||||
currentGroup,
|
||||
showCreateButton,
|
||||
link,
|
||||
groupSelected,
|
||||
label,
|
||||
testId,
|
||||
searchPlaceholder
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const groupSelector = groups && (
|
||||
<div className={"column is-flex"}>
|
||||
<DropDown
|
||||
className={"is-fullwidth"}
|
||||
options={groups}
|
||||
preselectedOption={currentGroup}
|
||||
optionSelected={groupSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"columns is-tablet"}>
|
||||
{groupSelector}
|
||||
<div className={"column"}>
|
||||
<FilterInput
|
||||
value={urls.getQueryStringFromLocation(location)}
|
||||
filter={filter => {
|
||||
history.push(`/${link}/?q=${filter}`);
|
||||
}}
|
||||
testId={testId + "-filter"}
|
||||
/>
|
||||
</div>
|
||||
{this.renderCreateButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCreateButton() {
|
||||
const { showCreateButton, link, label } = this.props;
|
||||
const renderCreateButton = () => {
|
||||
if (showCreateButton) {
|
||||
return (
|
||||
<div className={classNames("input-button", "control", "column")}>
|
||||
@@ -78,7 +70,24 @@ class OverviewPageActions extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default withRouter(OverviewPageActions);
|
||||
return (
|
||||
<div className={"columns is-tablet"}>
|
||||
{groupSelector}
|
||||
<div className={"column"}>
|
||||
<FilterInput
|
||||
placeholder={searchPlaceholder}
|
||||
value={urls.getQueryStringFromLocation(location)}
|
||||
filter={filter => {
|
||||
history.push(`/${link}/?q=${filter}`);
|
||||
}}
|
||||
testId={testId + "-filter"}
|
||||
/>
|
||||
</div>
|
||||
{renderCreateButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewPageActions;
|
||||
|
||||
@@ -50439,36 +50439,70 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
|
||||
href="/repo/hitchhiker/heartOfGold/branches/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.branches"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/tags/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.tags"
|
||||
>
|
||||
<i
|
||||
className="fas fa-tags has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/changesets/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.commits"
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/sources/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.sources"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/settings/general"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.settings"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
@@ -50553,36 +50587,70 @@ exports[`Storyshots RepositoryEntry Before Title EP 1`] = `
|
||||
href="/repo/hitchhiker/heartOfGold/branches/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.branches"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/tags/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.tags"
|
||||
>
|
||||
<i
|
||||
className="fas fa-tags has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/changesets/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.commits"
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/sources/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.sources"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/settings/general"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.settings"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
@@ -50664,36 +50732,70 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
|
||||
href="/repo/hitchhiker/heartOfGold/branches/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.branches"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/tags/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.tags"
|
||||
>
|
||||
<i
|
||||
className="fas fa-tags has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/changesets/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.commits"
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/sources/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.sources"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/settings/general"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.settings"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
@@ -50775,27 +50877,56 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
|
||||
href="/repo/hitchhiker/heartOfGold/branches/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.branches"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code-branch has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/tags/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.tags"
|
||||
>
|
||||
<i
|
||||
className="fas fa-tags has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/changesets/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.commits"
|
||||
>
|
||||
<i
|
||||
className="fas fa-exchange-alt has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
|
||||
href="/repo/hitchhiker/heartOfGold/code/sources/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.sources"
|
||||
>
|
||||
<i
|
||||
className="fas fa-code has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="level-item"
|
||||
@@ -50809,9 +50940,14 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
|
||||
href="/repo/hitchhiker/heartOfGold/settings/general"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
<span
|
||||
className="tooltip has-tooltip-top"
|
||||
data-tooltip="repositoryRoot.tooltip.settings"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog has-text-inherit fa-lg"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -27,10 +27,11 @@ import { CardColumn, DateFromNow } from "@scm-manager/ui-components";
|
||||
import RepositoryEntryLink from "./RepositoryEntryLink";
|
||||
import RepositoryAvatar from "./RepositoryAvatar";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { withTranslation, WithTranslation } from "react-i18next";
|
||||
|
||||
type DateProp = Date | string;
|
||||
|
||||
type Props = {
|
||||
type Props = WithTranslation & {
|
||||
repository: Repository;
|
||||
// @VisibleForTesting
|
||||
// the baseDate is only to avoid failing snapshot tests
|
||||
@@ -43,29 +44,67 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
};
|
||||
|
||||
renderBranchesLink = (repository: Repository, repositoryLink: string) => {
|
||||
const { t } = this.props;
|
||||
if (repository._links["branches"]) {
|
||||
return <RepositoryEntryLink icon="code-branch" to={repositoryLink + "/branches/"} />;
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
icon="code-branch"
|
||||
to={repositoryLink + "/branches/"}
|
||||
tooltip={t("repositoryRoot.tooltip.branches")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderTagsLink = (repository: Repository, repositoryLink: string) => {
|
||||
const { t } = this.props;
|
||||
if (repository._links["tags"]) {
|
||||
return (
|
||||
<RepositoryEntryLink icon="tags" to={repositoryLink + "/tags/"} tooltip={t("repositoryRoot.tooltip.tags")} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderChangesetsLink = (repository: Repository, repositoryLink: string) => {
|
||||
const { t } = this.props;
|
||||
if (repository._links["changesets"]) {
|
||||
return <RepositoryEntryLink icon="exchange-alt" to={repositoryLink + "/code/changesets/"} />;
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
icon="exchange-alt"
|
||||
to={repositoryLink + "/code/changesets/"}
|
||||
tooltip={t("repositoryRoot.tooltip.commits")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderSourcesLink = (repository: Repository, repositoryLink: string) => {
|
||||
const { t } = this.props;
|
||||
if (repository._links["sources"]) {
|
||||
return <RepositoryEntryLink icon="code" to={repositoryLink + "/code/sources/"} />;
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
icon="code"
|
||||
to={repositoryLink + "/code/sources/"}
|
||||
tooltip={t("repositoryRoot.tooltip.sources")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderModifyLink = (repository: Repository, repositoryLink: string) => {
|
||||
const { t } = this.props;
|
||||
if (repository._links["update"]) {
|
||||
return <RepositoryEntryLink icon="cog" to={repositoryLink + "/settings/general"} />;
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
icon="cog"
|
||||
to={repositoryLink + "/settings/general"}
|
||||
tooltip={t("repositoryRoot.tooltip.settings")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -74,6 +113,7 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
{this.renderBranchesLink(repository, repositoryLink)}
|
||||
{this.renderTagsLink(repository, repositoryLink)}
|
||||
{this.renderChangesetsLink(repository, repositoryLink)}
|
||||
{this.renderSourcesLink(repository, repositoryLink)}
|
||||
<ExtensionPoint name={"repository.card.quickLink"} props={{ repository, repositoryLink }} renderAll={true} />
|
||||
@@ -118,4 +158,4 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default RepositoryEntry;
|
||||
export default withTranslation("repos")(RepositoryEntry);
|
||||
|
||||
@@ -25,10 +25,12 @@ import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { Icon } from "@scm-manager/ui-components";
|
||||
import Tooltip from "../Tooltip";
|
||||
|
||||
type Props = {
|
||||
to: string;
|
||||
icon: string;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
const PointerEventsLink = styled(Link)`
|
||||
@@ -37,10 +39,20 @@ const PointerEventsLink = styled(Link)`
|
||||
|
||||
class RepositoryEntryLink extends React.Component<Props> {
|
||||
render() {
|
||||
const { to, icon } = this.props;
|
||||
const { to, icon, tooltip } = this.props;
|
||||
|
||||
let content = <Icon className="fa-lg" name={icon} color="inherit" />;
|
||||
if (tooltip) {
|
||||
content = (
|
||||
<Tooltip message={tooltip} location="top">
|
||||
{content}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PointerEventsLink className="level-item" to={to}>
|
||||
<Icon className="fa-lg" name={icon} color="inherit" />
|
||||
{content}
|
||||
</PointerEventsLink>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ export type Branch = {
|
||||
name: string;
|
||||
revision: string;
|
||||
defaultBranch?: boolean;
|
||||
lastCommitDate?: string;
|
||||
stale?: boolean;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"setPermissionsNavLink": "Berechtigungen"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"searchGroup": "Gruppe suchen"
|
||||
},
|
||||
"add-group": {
|
||||
"title": "Gruppe erstellen",
|
||||
"subtitle": "Erstellen einer neuen Gruppe"
|
||||
|
||||
@@ -36,13 +36,22 @@
|
||||
"settingsNavLink": "Einstellungen",
|
||||
"generalNavLink": "Generell",
|
||||
"permissionsNavLink": "Berechtigungen"
|
||||
},
|
||||
"tooltip": {
|
||||
"branches": "Branches",
|
||||
"tags": "Tags",
|
||||
"commits": "Commits",
|
||||
"sources": "Sources",
|
||||
"settings": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"title": "Repositories",
|
||||
"subtitle": "Übersicht aller verfügbaren Repositories",
|
||||
"noRepositories": "Keine Repositories gefunden.",
|
||||
"createButton": "Repository erstellen"
|
||||
"createButton": "Repository erstellen",
|
||||
"searchRepository": "Repository suchen",
|
||||
"allNamespaces": "Alle Namespaces"
|
||||
},
|
||||
"create": {
|
||||
"title": "Repository erstellen",
|
||||
@@ -55,7 +64,11 @@
|
||||
"createButton": "Branch erstellen"
|
||||
},
|
||||
"table": {
|
||||
"branches": "Branches"
|
||||
"branches": {
|
||||
"active": "Aktive Branches",
|
||||
"stale": "Alte Branches"
|
||||
},
|
||||
"lastCommit": "Letzter Commit"
|
||||
},
|
||||
"create": {
|
||||
"title": "Branch erstellen",
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"noUsers": "Keine Benutzer gefunden.",
|
||||
"createButton": "Benutzer erstellen"
|
||||
},
|
||||
"overview": {
|
||||
"searchUser": "Benutzer suchen"
|
||||
},
|
||||
"singleUser": {
|
||||
"errorTitle": "Fehler",
|
||||
"errorSubtitle": "Unbekannter Benutzer Fehler",
|
||||
@@ -76,15 +79,22 @@
|
||||
}
|
||||
},
|
||||
"publicKey": {
|
||||
"subtitle": "Öffentliche Schlüssel",
|
||||
"description": "Zum Prüfen von Signaturen für z. B. Commits können hier die entsprechenden öffentlichen GPG Schlüssel hinterlegt werden. Zudem können hier die vom SCM-Manager erstellten Signaturschlüssel heruntergeladen werden.",
|
||||
"noStoredKeys": "Es wurden keine Schlüssel gefunden.",
|
||||
"displayName": "Anzeigename",
|
||||
"raw": "Schlüssel",
|
||||
"created": "Eingetragen an",
|
||||
"addKey": "Schlüssel hinzufügen",
|
||||
"raw": "Schlüssel",
|
||||
"download": "Herunterladen",
|
||||
"delete": "Löschen",
|
||||
"download": "Herunterladen"
|
||||
"addSubtitle": "Neuen Schlüssel hinzufügen",
|
||||
"addKey": "Schlüssel hinzufügen"
|
||||
},
|
||||
"apiKey": {
|
||||
"subtitle": "API Schlüssel",
|
||||
"text1": "Erstelle und verwalte Personal Access Token um auf die REST API zuzugreifen oder diese als Passwort für SCM-Clients zu nutzen. Die Rechte der Token sind auf Repositories und die gewählte Rolle beschränkt.",
|
||||
"manageRoles": "Sie können die Rollenberechtigungen in der Administration unter „Berechtigungsrollen“ einsehen und neue Rollen anlegen.",
|
||||
"text2": "Um den Token in REST-Abfragen zu nutzen, übergeben Sie diesen als Cookie mit dem Namen „X-Bearer-Token“. Sie können den Token auch anstelle Ihres Passworts nutzen, um sich mit SCM-Clients anzumelden.",
|
||||
"noStoredKeys": "Es wurden keine Schlüssel gefunden.",
|
||||
"displayName": "Anzeigename",
|
||||
"permissionRole": {
|
||||
@@ -92,12 +102,10 @@
|
||||
"help": "Mit der Rolle können Sie die Berechtigung für diesen Schlüssel einschränken"
|
||||
},
|
||||
"created": "Eingetragen an",
|
||||
"addSubtitle": "Neuen Schlüssel hinzufügen",
|
||||
"addKey": "Schlüssel hinzufügen",
|
||||
"delete": "Löschen",
|
||||
"download": "Herunterladen",
|
||||
"text1": "Erstelle und verwalte Personal Access Token um auf die REST API zuzugreifen oder diese als Passwort für SCM-Clients zu nutzen. Die Rechte der Token sind auf Repositories und die gewählte Rolle beschränkt.",
|
||||
"manageRoles": "Sie können die Rollenberechtigungen in der Administration unter „Berechtigungsrollen“ einsehen und neue Rollen anlegen.",
|
||||
"text2": "Um den Token in REST-Abfragen zu nutzen, übergeben Sie diesen als Cookie mit dem Namen „X-Bearer-Token“. Sie können den Token auch anstelle Ihres Passworts nutzen, um sich mit SCM-Clients anzumelden.",
|
||||
"modal": {
|
||||
"title": "Schlüssel erzeugt",
|
||||
"text1": "Ihr neuer API-Schlüssel ist bereit. Sie können diesen als Token für Zugriffe auf die REST-Schnittstelle nutzen oder anstelle Ihres Passworts zum Login mit SCM-Clients nutzen.",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"setPermissionsNavLink": "Permissions"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"searchGroup": "Search group"
|
||||
},
|
||||
"add-group": {
|
||||
"title": "Create Group",
|
||||
"subtitle": "Create a new group"
|
||||
|
||||
@@ -36,13 +36,22 @@
|
||||
"settingsNavLink": "Settings",
|
||||
"generalNavLink": "General",
|
||||
"permissionsNavLink": "Permissions"
|
||||
},
|
||||
"tooltip": {
|
||||
"branches": "Branches",
|
||||
"tags": "Tags",
|
||||
"commits": "Commits",
|
||||
"sources": "Sources",
|
||||
"settings": "Settings"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"title": "Repositories",
|
||||
"subtitle": "Overview of available repositories",
|
||||
"noRepositories": "No repositories found.",
|
||||
"createButton": "Create Repository"
|
||||
"createButton": "Create Repository",
|
||||
"searchRepository": "Search repository",
|
||||
"allNamespaces": "All namespaces"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Repository",
|
||||
@@ -55,7 +64,11 @@
|
||||
"createButton": "Create Branch"
|
||||
},
|
||||
"table": {
|
||||
"branches": "Branches"
|
||||
"branches": {
|
||||
"active": "Active Branches",
|
||||
"stale": "Stale Branches"
|
||||
},
|
||||
"lastCommit": "Last commit"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Branch",
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
"setApiKeyNavLink": "API Keys"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"searchUser": "Search user"
|
||||
},
|
||||
"createUser": {
|
||||
"title": "Create User",
|
||||
"subtitle": "Create a new user"
|
||||
@@ -76,15 +79,22 @@
|
||||
}
|
||||
},
|
||||
"publicKey": {
|
||||
"subtitle": "Public Keys",
|
||||
"description": "To check signatures (for example for commits), gpg public keys can be stored here. Additionally the keys created by SCM-Manager can be accessed here, too.",
|
||||
"noStoredKeys": "No keys found.",
|
||||
"displayName": "Display Name",
|
||||
"raw": "Key",
|
||||
"created": "Created on",
|
||||
"addKey": "Add key",
|
||||
"raw": "Key",
|
||||
"download": "Download",
|
||||
"delete": "Delete",
|
||||
"download": "Download"
|
||||
"addSubtitle": "Add new key",
|
||||
"addKey": "Add key"
|
||||
},
|
||||
"apiKey": {
|
||||
"subtitle": "API Keys",
|
||||
"text1": "Create and manage personal access tokens to access the REST API or use as a password for SCM clients. The privileges of these tokens are limited to repositories and the selected role.",
|
||||
"manageRoles": "You may view and create roles in the administration view “Permission Roles”.",
|
||||
"text2": "To use the token in a REST request, pass it as a cookie named “X-Bearer-Token”. You may use the token as your password for SCM clients, too.",
|
||||
"noStoredKeys": "No keys found.",
|
||||
"displayName": "Display Name",
|
||||
"permissionRole": {
|
||||
@@ -92,12 +102,10 @@
|
||||
"help": "The api key will be restricted to permissions of this role"
|
||||
},
|
||||
"created": "Created on",
|
||||
"addSubtitle": "Add new key",
|
||||
"addKey": "Add key",
|
||||
"delete": "Delete",
|
||||
"download": "Download",
|
||||
"text1": "Create and manage personal access tokens to access the REST API or use as a password for SCM clients. The privileges of these tokens are limited to repositories and the selected role.",
|
||||
"manageRoles": "You may view and create roles in the administration view “Permission Roles”.",
|
||||
"text2": "To use the token in a REST request, pass it as a cookie named “X-Bearer-Token”. You may use the token as your password for SCM clients, too.",
|
||||
"modal": {
|
||||
"title": "Key created",
|
||||
"text1": "Your new API key is ready. You can use it as a bearer token for REST calls or as a password for SCM clients.",
|
||||
|
||||
@@ -92,7 +92,12 @@ class Groups extends React.Component<Props> {
|
||||
{this.renderGroupTable()}
|
||||
{this.renderCreateButton()}
|
||||
<PageActions>
|
||||
<OverviewPageActions showCreateButton={canAddGroups} link="groups" label={t("create-group-button.label")} />
|
||||
<OverviewPageActions
|
||||
showCreateButton={canAddGroups}
|
||||
link="groups"
|
||||
label={t("create-group-button.label")}
|
||||
searchPlaceholder={t("overview.searchGroup")}
|
||||
/>
|
||||
</PageActions>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -26,21 +26,45 @@ import { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import BranchButtonGroup from "./BranchButtonGroup";
|
||||
import DefaultBranchTag from "./DefaultBranchTag";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
repository: Repository;
|
||||
branch: Branch;
|
||||
};
|
||||
|
||||
const FlexRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const Created = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
|
||||
const Label = styled.strong`
|
||||
margin-right: 0.3rem;
|
||||
`;
|
||||
|
||||
const Date = styled(DateFromNow)`
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
|
||||
class BranchDetail extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, branch, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className="media">
|
||||
<div className="media-content subtitle">
|
||||
<strong>{t("branch.name")}</strong> {branch.name} <DefaultBranchTag defaultBranch={branch.defaultBranch} />
|
||||
</div>
|
||||
<FlexRow className="media-content subtitle">
|
||||
<Label>{t("branch.name")}</Label> {branch.name} <DefaultBranchTag defaultBranch={branch.defaultBranch} />
|
||||
<Created className="is-ellipsis-overflow">
|
||||
{t("tags.overview.created")} <Date date={branch.lastCommitDate} className="has-text-grey" />
|
||||
</Created>
|
||||
</FlexRow>
|
||||
<div className="media-right">
|
||||
<BranchButtonGroup repository={repository} branch={branch} />
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,9 @@ import React, { FC } from "react";
|
||||
import { Link as ReactLink } from "react-router-dom";
|
||||
import { Branch, Link } from "@scm-manager/ui-types";
|
||||
import DefaultBranchTag from "./DefaultBranchTag";
|
||||
import { Icon } from "@scm-manager/ui-components";
|
||||
import { DateFromNow, Icon } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string;
|
||||
@@ -34,6 +35,11 @@ type Props = {
|
||||
onDelete: (branch: Branch) => void;
|
||||
};
|
||||
|
||||
const Created = styled.span`
|
||||
margin-left: 1rem;
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
|
||||
const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete }) => {
|
||||
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
|
||||
const [t] = useTranslation("repos");
|
||||
@@ -56,6 +62,11 @@ const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete }) => {
|
||||
{branch.name}
|
||||
<DefaultBranchTag defaultBranch={branch.defaultBranch} />
|
||||
</ReactLink>
|
||||
{branch.lastCommitDate && (
|
||||
<Created className="has-text-grey is-ellipsis-overflow">
|
||||
{t("branches.table.lastCommit")} <DateFromNow date={branch.lastCommitDate} />
|
||||
</Created>
|
||||
)}
|
||||
</td>
|
||||
<td className="is-darker">{deleteButton}</td>
|
||||
</tr>
|
||||
|
||||
@@ -30,10 +30,11 @@ import { apiClient, ConfirmAlert, ErrorNotification } from "@scm-manager/ui-comp
|
||||
type Props = {
|
||||
baseUrl: string;
|
||||
branches: Branch[];
|
||||
type: string;
|
||||
fetchBranches: () => void;
|
||||
};
|
||||
|
||||
const BranchTable: FC<Props> = ({ baseUrl, branches, fetchBranches }) => {
|
||||
const BranchTable: FC<Props> = ({ baseUrl, branches, type, fetchBranches }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
@@ -92,7 +93,7 @@ const BranchTable: FC<Props> = ({ baseUrl, branches, fetchBranches }) => {
|
||||
<table className="card-table table is-hoverable is-fullwidth is-word-break">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("branches.table.branches")}</th>
|
||||
<th>{t(`branches.table.branches.${type}`)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{renderRow()}</tbody>
|
||||
|
||||
@@ -84,7 +84,28 @@ class BranchesOverview extends React.Component<Props> {
|
||||
const { baseUrl, branches, repository, fetchBranches, t } = this.props;
|
||||
if (branches && branches.length > 0) {
|
||||
orderBranches(branches);
|
||||
return <BranchTable baseUrl={baseUrl} branches={branches} fetchBranches={() => fetchBranches(repository)} />;
|
||||
const staleBranches = branches.filter(b => b.stale);
|
||||
const activeBranches = branches.filter(b => !b.stale);
|
||||
return (
|
||||
<>
|
||||
{activeBranches.length > 0 && (
|
||||
<BranchTable
|
||||
baseUrl={baseUrl}
|
||||
type={"active"}
|
||||
branches={activeBranches}
|
||||
fetchBranches={() => fetchBranches(repository)}
|
||||
/>
|
||||
)}
|
||||
{staleBranches.length > 0 && (
|
||||
<BranchTable
|
||||
baseUrl={baseUrl}
|
||||
type={"stale"}
|
||||
branches={staleBranches}
|
||||
fetchBranches={() => fetchBranches(repository)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Notification type="info">{t("branches.overview.noBranches")}</Notification>;
|
||||
}
|
||||
|
||||
@@ -47,9 +47,14 @@ const developBranch = {
|
||||
revision: "revision5",
|
||||
defaultBranch: false
|
||||
};
|
||||
const mainBranch = {
|
||||
name: "main",
|
||||
revision: "revision6",
|
||||
defaultBranch: false
|
||||
};
|
||||
const masterBranch = {
|
||||
name: "master",
|
||||
revision: "revision6",
|
||||
revision: "revision7",
|
||||
defaultBranch: false
|
||||
};
|
||||
|
||||
@@ -66,10 +71,10 @@ describe("order branches", () => {
|
||||
expect(branches).toEqual([branch3, branch1, branch2]);
|
||||
});
|
||||
|
||||
it("should order special branches as follows: master > default > develop", () => {
|
||||
const branches = [defaultBranch, developBranch, masterBranch];
|
||||
it("should order special branches as follows: main > master > default > develop", () => {
|
||||
const branches = [defaultBranch, mainBranch, developBranch, masterBranch];
|
||||
orderBranches(branches);
|
||||
expect(branches).toEqual([masterBranch, defaultBranch, developBranch]);
|
||||
expect(branches).toEqual([mainBranch, masterBranch, defaultBranch, developBranch]);
|
||||
});
|
||||
|
||||
it("should order special branches but starting with defaultBranch", () => {
|
||||
|
||||
@@ -32,10 +32,14 @@ export function orderBranches(branches: Branch[]) {
|
||||
return -20;
|
||||
} else if (!a.defaultBranch && b.defaultBranch) {
|
||||
return 20;
|
||||
} else if (a.name === "master" && b.name !== "master") {
|
||||
} else if (a.name === "main" && b.name !== "main") {
|
||||
return -10;
|
||||
} else if (a.name !== "master" && b.name === "master") {
|
||||
} else if (a.name !== "main" && b.name === "main") {
|
||||
return 10;
|
||||
} else if (a.name === "master" && b.name !== "master") {
|
||||
return -9;
|
||||
} else if (a.name !== "master" && b.name === "master") {
|
||||
return 9;
|
||||
} else if (a.name === "default" && b.name !== "default") {
|
||||
return -10;
|
||||
} else if (a.name !== "default" && b.name === "default") {
|
||||
|
||||
@@ -23,21 +23,25 @@
|
||||
*/
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { CardColumnGroup, RepositoryEntry } from "@scm-manager/ui-components";
|
||||
import { CardColumnGroup, Icon, RepositoryEntry } from "@scm-manager/ui-components";
|
||||
import { RepositoryGroup } from "@scm-manager/ui-types";
|
||||
import { Icon } from "@scm-manager/ui-components";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
group: RepositoryGroup;
|
||||
};
|
||||
|
||||
const SizedIcon = styled(Icon)`
|
||||
font-size: 1.33rem;
|
||||
`;
|
||||
|
||||
class RepositoryGroupEntry extends React.Component<Props> {
|
||||
render() {
|
||||
const { group, t } = this.props;
|
||||
const settingsLink = group.namespace?._links?.permissions && (
|
||||
<Link to={`/namespace/${group.name}/settings`}>
|
||||
<Icon color={"is-link"} name={"cog"} title={t("repositoryOverview.settings.tooltip")} />
|
||||
<SizedIcon color={"is-link"} name={"cog"} title={t("repositoryOverview.settings.tooltip")} />
|
||||
</Link>
|
||||
);
|
||||
const namespaceHeader = (
|
||||
|
||||
@@ -101,8 +101,12 @@ class Overview extends React.Component<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
getNamespaceFilterPlaceholder = () => {
|
||||
return this.props.t("overview.allNamespaces");
|
||||
};
|
||||
|
||||
namespaceSelected = (newNamespace: string) => {
|
||||
if (newNamespace === "") {
|
||||
if (newNamespace === this.getNamespaceFilterPlaceholder()) {
|
||||
this.props.history.push("/repos/");
|
||||
} else {
|
||||
this.props.history.push(`/repos/${newNamespace}/`);
|
||||
@@ -111,8 +115,10 @@ class Overview extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { error, loading, showCreateButton, namespace, namespaces, t } = this.props;
|
||||
|
||||
const namespacesToRender = namespaces ? ["", ...namespaces._embedded.namespaces.map(n => n.namespace).sort()] : [];
|
||||
const namespaceFilterPlaceholder = this.getNamespaceFilterPlaceholder();
|
||||
const namespacesToRender = namespaces
|
||||
? [namespaceFilterPlaceholder, ...namespaces._embedded.namespaces.map(n => n.namespace).sort()]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Page title={t("overview.title")} subtitle={t("overview.subtitle")} loading={loading} error={error}>
|
||||
@@ -126,6 +132,7 @@ class Overview extends React.Component<Props> {
|
||||
link="repos"
|
||||
label={t("overview.createButton")}
|
||||
testId="repository-overview"
|
||||
searchPlaceholder={t("overview.searchRepository")}
|
||||
/>
|
||||
</PageActions>
|
||||
</Page>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { apiClient, ErrorNotification, InputField, Level, Loading, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { apiClient, ErrorNotification, InputField, Level, Loading, SubmitButton, Subtitle } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CONTENT_TYPE_API_KEY } from "./SetApiKeys";
|
||||
import { connect } from "react-redux";
|
||||
@@ -105,6 +105,8 @@ const AddApiKey: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<Subtitle subtitle={t("apiKey.addSubtitle")} />
|
||||
{newKeyModal}
|
||||
<InputField label={t("apiKey.displayName")} value={displayName} onChange={setDisplayName} />
|
||||
<RoleSelector
|
||||
|
||||
@@ -39,8 +39,8 @@ export const ApiKeyEntry: FC<Props> = ({ apiKey, onDelete }) => {
|
||||
if (apiKey?._links?.delete) {
|
||||
deleteButton = (
|
||||
<a className="level-item" onClick={() => onDelete((apiKey._links.delete as Link).href)}>
|
||||
<span className="icon is-small">
|
||||
<Icon name="trash" className="fas" title={t("apiKey.delete")} />
|
||||
<span className="icon">
|
||||
<Icon name="trash" title={t("apiKey.delete")} color="inherit" />
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
@@ -52,7 +52,7 @@ export const ApiKeyEntry: FC<Props> = ({ apiKey, onDelete }) => {
|
||||
<td>{apiKey.displayName}</td>
|
||||
<td>{apiKey.permissionRole}</td>
|
||||
<td className="is-hidden-mobile">
|
||||
<DateFromNow date={apiKey.created}/>
|
||||
<DateFromNow date={apiKey.created} />
|
||||
</td>
|
||||
<td className="is-darker">{deleteButton}</td>
|
||||
</tr>
|
||||
|
||||
@@ -25,11 +25,10 @@
|
||||
import { Collection, Links, User, Me } from "@scm-manager/ui-types";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import { apiClient, ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components";
|
||||
import ApiKeyTable from "./ApiKeyTable";
|
||||
import AddApiKey from "./AddApiKey";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
|
||||
export type ApiKeysCollection = Collection & {
|
||||
_embedded: {
|
||||
@@ -51,10 +50,6 @@ type Props = {
|
||||
user: User | Me;
|
||||
};
|
||||
|
||||
const Subtitle = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const SetApiKeys: FC<Props> = ({ user }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const [error, setError] = useState<undefined | Error>();
|
||||
@@ -94,14 +89,13 @@ const SetApiKeys: FC<Props> = ({ user }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"media-content"}>
|
||||
<p>{t("apiKey.text1")} <Link to={"/admin/roles/"}>{t("apiKey.manageRoles")}</Link></p>
|
||||
<p>{t("apiKey.text2")}</p>
|
||||
</div>
|
||||
<hr />
|
||||
<Subtitle subtitle={t("apiKey.subtitle")} />
|
||||
<p>
|
||||
{t("apiKey.text1")} <Link to={"/admin/roles/"}>{t("apiKey.manageRoles")}</Link>
|
||||
</p>
|
||||
<p>{t("apiKey.text2")}</p>
|
||||
<br />
|
||||
<ApiKeyTable apiKeys={apiKeys} onDelete={onDelete} />
|
||||
<hr />
|
||||
<Subtitle className={"media-content"}><h2 className={"title is-4"}>Create new key</h2></Subtitle>
|
||||
{createLink && <AddApiKey createLink={createLink} refresh={fetchApiKeys} />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from "react";
|
||||
import { User, Link, Links, Collection } from "@scm-manager/ui-types/src";
|
||||
import {
|
||||
ErrorNotification,
|
||||
InputField,
|
||||
@@ -31,7 +30,8 @@ import {
|
||||
Textarea,
|
||||
SubmitButton,
|
||||
apiClient,
|
||||
Loading
|
||||
Loading,
|
||||
Subtitle
|
||||
} from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CONTENT_TYPE_PUBLIC_KEY } from "./SetPublicKeys";
|
||||
@@ -77,6 +77,8 @@ const AddPublicKey: FC<Props> = ({ createLink, refresh }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<Subtitle subtitle={t("publicKey.addSubtitle")} />
|
||||
<InputField label={t("publicKey.displayName")} value={displayName} onChange={setDisplayName} />
|
||||
<Textarea name="raw" label={t("publicKey.raw")} value={raw} onChange={setRaw} />
|
||||
<Level
|
||||
|
||||
@@ -22,24 +22,28 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, {FC} from "react";
|
||||
import {DateFromNow, DeleteButton} from "@scm-manager/ui-components";
|
||||
import {PublicKey} from "./SetPublicKeys";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link} from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { DateFromNow, Icon } from "@scm-manager/ui-components";
|
||||
import { PublicKey } from "./SetPublicKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
publicKey: PublicKey;
|
||||
onDelete: (link: string) => void;
|
||||
};
|
||||
|
||||
export const PublicKeyEntry: FC<Props> = ({publicKey, onDelete}) => {
|
||||
export const PublicKeyEntry: FC<Props> = ({ publicKey, onDelete }) => {
|
||||
const [t] = useTranslation("users");
|
||||
|
||||
let deleteButton;
|
||||
if (publicKey?._links?.delete) {
|
||||
deleteButton = (
|
||||
<DeleteButton label={t("publicKey.delete")} action={() => onDelete((publicKey._links.delete as Link).href)}/>
|
||||
<a className="level-item" onClick={() => onDelete((publicKey._links.delete as Link).href)}>
|
||||
<span className="icon">
|
||||
<Icon name="trash" title={t("publicKey.delete")} color="inherit" />
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,11 +52,18 @@ export const PublicKeyEntry: FC<Props> = ({publicKey, onDelete}) => {
|
||||
<tr>
|
||||
<td>{publicKey.displayName}</td>
|
||||
<td className="is-hidden-mobile">
|
||||
<DateFromNow date={publicKey.created}/>
|
||||
<DateFromNow date={publicKey.created} />
|
||||
</td>
|
||||
<td className="is-hidden-mobile">{publicKey._links?.raw ?
|
||||
<a href={(publicKey._links.raw as Link).href}>{publicKey.id}</a> : publicKey.id}</td>
|
||||
<td>{deleteButton}</td>
|
||||
<td className="is-hidden-mobile">
|
||||
{publicKey._links?.raw ? (
|
||||
<a title={t("publicKey.download")} href={(publicKey._links.raw as Link).href}>
|
||||
{publicKey.id}
|
||||
</a>
|
||||
) : (
|
||||
publicKey.id
|
||||
)}
|
||||
</td>
|
||||
<td className="is-darker">{deleteButton}</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,9 +24,10 @@
|
||||
|
||||
import { Collection, Link, Links, User, Me } from "@scm-manager/ui-types";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AddPublicKey from "./AddPublicKey";
|
||||
import PublicKeyTable from "./PublicKeyTable";
|
||||
import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import { apiClient, ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components";
|
||||
|
||||
export type PublicKeysCollection = Collection & {
|
||||
_embedded: {
|
||||
@@ -49,6 +50,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const SetPublicKeys: FC<Props> = ({ user }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const [error, setError] = useState<undefined | Error>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [publicKeys, setPublicKeys] = useState<PublicKeysCollection | undefined>(undefined);
|
||||
@@ -86,6 +88,9 @@ const SetPublicKeys: FC<Props> = ({ user }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("publicKey.subtitle")} />
|
||||
<p>{t("publicKey.description")}</p>
|
||||
<br />
|
||||
<PublicKeyTable publicKeys={publicKeys} onDelete={onDelete} />
|
||||
{createLink && <AddPublicKey createLink={createLink} refresh={fetchPublicKeys} />}
|
||||
</>
|
||||
|
||||
@@ -93,7 +93,12 @@ class Users extends React.Component<Props> {
|
||||
{this.renderUserTable()}
|
||||
{this.renderCreateButton()}
|
||||
<PageActions>
|
||||
<OverviewPageActions showCreateButton={canAddUsers} link="users" label={t("users.createButton")} />
|
||||
<OverviewPageActions
|
||||
showCreateButton={canAddUsers}
|
||||
link="users"
|
||||
label={t("users.createButton")}
|
||||
searchPlaceholder={t("overview.searchUser")}
|
||||
/>
|
||||
</PageActions>
|
||||
</Page>
|
||||
);
|
||||
|
||||
56
scm-ui/ui-webapp/translationResources.test.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const dirs = fs.readdirSync("public/locales");
|
||||
|
||||
describe("locales folder", () => {
|
||||
it("should contain folder en", () => {
|
||||
expect(dirs).toContain("en");
|
||||
});
|
||||
});
|
||||
|
||||
dirs.forEach(languageDirName => {
|
||||
const languageDir = `public/locales/${languageDirName}`;
|
||||
const languageDirFiles = fs.readdirSync(languageDir);
|
||||
languageDirFiles
|
||||
.filter(fileName => fileName.endsWith(".json"))
|
||||
.forEach(translationFileName => {
|
||||
const translationFile = `${languageDir}/${translationFileName}`;
|
||||
describe(`translation file ${translationFile}`, () => {
|
||||
it("should contain only valid json", () => {
|
||||
const jsonContent = fs.readFileSync(translationFile);
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(jsonContent);
|
||||
} catch (ex) {
|
||||
// eslint-disable-next-line no-undef
|
||||
fail(new Error(`invalid json: ${ex}`));
|
||||
}
|
||||
expect(json).not.toBe({});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -295,7 +295,7 @@
|
||||
<dependency>
|
||||
<groupId>com.cronutils</groupId>
|
||||
<artifactId>cron-utils</artifactId>
|
||||
<version>8.1.1</version>
|
||||
<version>9.1.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- template engine -->
|
||||
@@ -691,7 +691,7 @@
|
||||
<jjwt.version>0.11.2</jjwt.version>
|
||||
<selenium.version>2.53.1</selenium.version>
|
||||
<wagon.version>1.0</wagon.version>
|
||||
<mustache.version>0.9.6-scm1</mustache.version>
|
||||
<mustache.version>0.9.7</mustache.version>
|
||||
<netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server>
|
||||
<sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria>
|
||||
<sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
@@ -31,11 +32,14 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import sonia.scm.repository.Branch;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
|
||||
import java.time.Instant;
|
||||
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -45,10 +49,13 @@ public class BranchDto extends HalRepresentation {
|
||||
|
||||
@NotEmpty
|
||||
@Length(min = 1, max = 100)
|
||||
@Pattern(regexp = VALID_BRANCH_NAMES)
|
||||
@Pattern(regexp = Branch.VALID_BRANCH_NAMES)
|
||||
private String name;
|
||||
private String revision;
|
||||
private boolean defaultBranch;
|
||||
@JsonInclude(NON_NULL)
|
||||
private Instant lastCommitDate;
|
||||
private boolean stale;
|
||||
|
||||
BranchDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
|
||||
@@ -38,11 +38,14 @@ import sonia.scm.web.EdisonHalAppender;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.otto.edison.hal.Link.linkBuilder;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
@Mapper
|
||||
public abstract class BranchToBranchDtoMapper extends HalAppenderMapper {
|
||||
public abstract class BranchToBranchDtoMapper extends HalAppenderMapper implements InstantAttributeMapper {
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
@@ -68,4 +71,8 @@ public abstract class BranchToBranchDtoMapper extends HalAppenderMapper {
|
||||
|
||||
return new BranchDto(linksBuilder.build(), embeddedBuilder.build());
|
||||
}
|
||||
|
||||
Instant mapOptionalTime(Optional<Long> date) {
|
||||
return date.map(this::mapTime).orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +192,16 @@ public class DefaultPluginManager implements PluginManager {
|
||||
dependencyTracker.addInstalled(plugin.getDescriptor());
|
||||
pendingInstallations.add(pending);
|
||||
eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLED, plugin));
|
||||
} catch (PluginInstallException ex) {
|
||||
cancelPending(pendingInstallations);
|
||||
eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLATION_FAILED, plugin));
|
||||
throw ex;
|
||||
} catch (PluginInstallException installException) {
|
||||
try {
|
||||
cancelPending(pendingInstallations);
|
||||
} catch (PluginFailedToCancelInstallationException cancelInstallationException) {
|
||||
LOG.error("could not install plugin {}; uninstallation failed (see next exception)", plugin.getDescriptor().getInformation().getName(), installException);
|
||||
throw cancelInstallationException;
|
||||
} finally {
|
||||
eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLATION_FAILED, plugin));
|
||||
}
|
||||
throw installException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.plugin;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -50,10 +50,14 @@ class PendingPluginInstallation {
|
||||
void cancel() {
|
||||
String name = plugin.getDescriptor().getInformation().getName();
|
||||
LOG.info("cancel installation of plugin {}", name);
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException ex) {
|
||||
throw new PluginFailedToCancelInstallationException("failed to cancel plugin installation ", name, ex);
|
||||
if (Files.exists(file)) {
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException ex) {
|
||||
throw new PluginFailedToCancelInstallationException("failed to cancel plugin installation ", name, ex);
|
||||
}
|
||||
} else {
|
||||
LOG.info("plugin file {} did not exists for plugin {}; nothing deleted", file, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,8 @@ public final class PluginTree
|
||||
throw new PluginConditionFailedException(
|
||||
condition,
|
||||
String.format(
|
||||
"could not load plugin %s, the plugin condition does not match",
|
||||
plugin.getInformation().getId()
|
||||
"could not load plugin %s, the plugin condition does not match: %s",
|
||||
plugin.getInformation().getId(), condition
|
||||
)
|
||||
);
|
||||
//J+
|
||||
|
||||
@@ -59,8 +59,13 @@ class PendingPluginInstallationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionIfCancelFailed(@TempDir Path directory) {
|
||||
void shouldThrowExceptionIfCancelFailed(@TempDir Path directory) throws IOException {
|
||||
Path file = directory.resolve("file");
|
||||
Files.createDirectory(file);
|
||||
|
||||
Path makeFileNotDeletable = file.resolve("not_deletable");
|
||||
Files.write(makeFileNotDeletable, "42".getBytes());
|
||||
|
||||
when(plugin.getDescriptor().getInformation().getName()).thenReturn("scm-awesome-plugin");
|
||||
|
||||
PendingPluginInstallation installation = new PendingPluginInstallation(plugin, file);
|
||||
|
||||
@@ -48,12 +48,15 @@ import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -239,6 +242,30 @@ class I18nServletTest {
|
||||
assertJson(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotHaveInvalidPluginsJsonFiles() throws IOException {
|
||||
String path = getClass().getClassLoader().getResource("locales/en/plugins.json").getPath();
|
||||
assertThat(path).isNotNull();
|
||||
|
||||
Path filePath = Paths.get(path);
|
||||
Path translationRootPath = filePath.getParent().getParent();
|
||||
assertThat(translationRootPath).isDirectoryContaining("glob:**/en");
|
||||
|
||||
Files
|
||||
.list(translationRootPath)
|
||||
.filter(Files::isDirectory)
|
||||
.map(localePath -> localePath.resolve("plugins.json"))
|
||||
.forEach(this::validatePluginsJson);
|
||||
}
|
||||
|
||||
private void validatePluginsJson(Path path) {
|
||||
try {
|
||||
new ObjectMapper().readTree(path.toFile());
|
||||
} catch (IOException e) {
|
||||
fail("error while parsing translation file " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyHeaders(HttpServletResponse response) {
|
||||
verify(response).setCharacterEncoding("UTF-8");
|
||||
verify(response).setContentType("application/json");
|
||||
|
||||