Merge branch 'develop' into feature/manage-tags

# Conflicts:
#	scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java
This commit is contained in:
Konstantin Schaper
2020-11-30 14:18:05 +01:00
228 changed files with 4954 additions and 3576 deletions

View File

@@ -6,11 +6,31 @@ 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))
- Healthcheck for docker image ([#1428](https://github.com/scm-manager/scm-manager/issues/1428) and [#1454](https://github.com/scm-manager/scm-manager/issues/1454))
### Changed
- Send mercurial hook callbacks over separate tcp socket instead of http ([#1416](https://github.com/scm-manager/scm-manager/pull/1416))
### Fixed
- Language detection of files with interpreter parameters e.g.: `#!/usr/bin/make -f` ([#1450](https://github.com/scm-manager/scm-manager/issues/1450))
## [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))
- Lookup command which provides further repository information ([#1415](https://github.com/scm-manager/scm-manager/pull/1415))
- Include messages from scm protocol in modification or merge errors ([#1420](https://github.com/scm-manager/scm-manager/pull/1420))
- Enhance trace api to accepted status codes ([#1430](https://github.com/scm-manager/scm-manager/pull/1430))
- Add examples to core resources to simplify usage of rest api ([#1434](https://github.com/scm-manager/scm-manager/pull/1434))
### Fixed
- Missing close of hg diff command ([#1417](https://github.com/scm-manager/scm-manager/pull/1417))
@@ -414,3 +434,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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 285 KiB

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 270 KiB

View File

@@ -4,8 +4,10 @@ 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.
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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 263 KiB

View File

@@ -5,5 +5,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "2.10.0-SNAPSHOT"
"version": "2.11.0-SNAPSHOT"
}

14
pom.xml
View File

@@ -32,7 +32,7 @@
<groupId>sonia.scm</groupId>
<artifactId>scm</artifactId>
<packaging>pom</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<description>
The easiest way to share your Git, Mercurial
and Subversion repositories.
@@ -464,7 +464,7 @@
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.17.2</version>
<version>3.18.0</version>
<scope>test</scope>
</dependency>
@@ -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>
@@ -919,13 +919,13 @@
<guice.version>4.2.3</guice.version>
<jaxb.version>2.3.3</jaxb.version>
<hibernate-validator.version>6.1.6.Final</hibernate-validator.version>
<bouncycastle.version>1.66</bouncycastle.version>
<bouncycastle.version>1.67</bouncycastle.version>
<!-- event bus -->
<legman.version>1.6.2</legman.version>
<!-- webserver -->
<jetty.version>9.4.34.v20201102</jetty.version>
<jetty.version>9.4.35.v20201120</jetty.version>
<jetty.maven.version>9.4.34.v20201102</jetty.maven.version>
<!-- security libraries -->
@@ -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>

View File

@@ -31,12 +31,12 @@
<parent>
<groupId>sonia.scm</groupId>
<artifactId>scm</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<groupId>sonia.scm</groupId>
<artifactId>scm-annotation-processor</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<name>scm-annotation-processor</name>
<dependencies>
@@ -46,7 +46,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-annotations</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</dependency>
<!-- rest api -->

View File

@@ -31,11 +31,11 @@
<parent>
<groupId>sonia.scm</groupId>
<artifactId>scm</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>scm-annotations</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<name>scm-annotations</name>
<dependencies>

View File

@@ -31,11 +31,11 @@
<parent>
<artifactId>scm</artifactId>
<groupId>sonia.scm</groupId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>scm-core</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<name>scm-core</name>
<dependencies>
@@ -54,7 +54,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-annotations</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</dependency>
<dependency>
@@ -227,7 +227,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-annotation-processor</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
@@ -250,6 +250,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,71 @@
/*
* 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;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.MDC;
import java.util.Optional;
/**
* Id of the current transaction.
* The transaction id is mainly used for logging and debugging.
*
* @since 2.10.0
*/
public final class TransactionId {
@VisibleForTesting
public static final String KEY = "transaction_id";
private TransactionId() {
}
/**
* Binds the given transaction id to the current thread.
*
* @param transactionId transaction id
*/
public static void set(String transactionId) {
MDC.put(KEY, transactionId);
}
/**
* Returns an optional transaction id.
* If there is no transaction id bound to the thread, the method will return an empty optional.
*
* @return optional transaction id
*/
public static Optional<String> get() {
return Optional.ofNullable(MDC.get(KEY));
}
/**
* Removes a bound transaction id from the current thread.
*/
public static void clear() {
MDC.remove(KEY);
}
}

View File

@@ -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 ---------------------------------------------------------------
/** name of the branch */
private String name;
/** Field description */
private String revision;
private boolean defaultBranch;
/**
* 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);
}
public boolean isStale() {
return stale;
}
}

View File

@@ -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();
}
}

View File

@@ -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 --------------------------------------------------------

View File

@@ -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;
}
}

View File

@@ -24,30 +24,49 @@
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
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)));
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TransactionIdTest {
@Test
void shouldSetGetAndClear() {
TransactionId.set("42");
assertThat(TransactionId.get()).contains("42");
TransactionId.clear();
assertThat(TransactionId.get()).isEmpty();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -31,11 +31,11 @@
<parent>
<groupId>sonia.scm</groupId>
<artifactId>scm</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>scm-dao-xml</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<name>scm-dao-xml</name>
<dependencies>
@@ -50,7 +50,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-core</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</dependency>
<!-- test -->
@@ -58,7 +58,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-test</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>

View File

@@ -31,40 +31,40 @@
<parent>
<groupId>sonia.scm</groupId>
<artifactId>scm</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<groupId>sonia.scm</groupId>
<artifactId>scm-it</artifactId>
<!-- we need type war, because the jetty plugin does not work with jar or pom -->
<packaging>war</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<name>scm-it</name>
<dependencies>
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-core</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-test</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-git-plugin</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-git-plugin</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
@@ -72,14 +72,14 @@
<dependency>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-hg-plugin</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-hg-plugin</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
@@ -87,14 +87,14 @@
<dependency>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-svn-plugin</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-svn-plugin</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>

View File

@@ -32,12 +32,12 @@
<parent>
<groupId>sonia.scm.packaging</groupId>
<artifactId>scm-packaging</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>deb</artifactId>
<packaging>deb</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<description>Packaging for Debian/Ubuntu</description>
<name>deb</name>
@@ -46,7 +46,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-server</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</dependency>
</dependencies>

View File

@@ -41,4 +41,9 @@ VOLUME ["${SCM_HOME}", "${CACHE_DIR}"]
EXPOSE 8080
USER scm
# we us a high relative high start period,
# because the start time depends on the number of installed plugins
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/scm/api/v2 || exit 1
ENTRYPOINT [ "/opt/scm-server/bin/scm-server" ]

View File

@@ -32,12 +32,12 @@
<parent>
<groupId>sonia.scm.packaging</groupId>
<artifactId>scm-packaging</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>docker</artifactId>
<packaging>pom</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<dependencies>

View File

@@ -32,12 +32,12 @@
<parent>
<groupId>sonia.scm.packaging</groupId>
<artifactId>scm-packaging</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>helm</artifactId>
<packaging>helm</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<properties>
<helm.version>3.2.1</helm.version>

View File

@@ -31,13 +31,13 @@
<parent>
<groupId>sonia.scm</groupId>
<artifactId>scm</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<groupId>sonia.scm.packaging</groupId>
<artifactId>scm-packaging</artifactId>
<packaging>pom</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<properties>
<deployment.serverId>packages.scm-manager.org</deployment.serverId>

View File

@@ -32,12 +32,12 @@
<parent>
<groupId>sonia.scm.packaging</groupId>
<artifactId>scm-packaging</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>release-yaml</artifactId>
<packaging>pom</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<build>
<plugins>

View File

@@ -32,12 +32,12 @@
<parent>
<groupId>sonia.scm.packaging</groupId>
<artifactId>scm-packaging</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>rpm</artifactId>
<packaging>rpm</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<description>Packaging for RedHat/Centos/Fedora</description>
<name>rpm</name>
@@ -52,7 +52,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-server</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</dependency>
</dependencies>

View File

@@ -31,12 +31,12 @@
<parent>
<groupId>sonia.scm.packaging</groupId>
<artifactId>scm-packaging</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>unix</artifactId>
<packaging>pom</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<dependencies>

View File

@@ -32,12 +32,12 @@
<parent>
<groupId>sonia.scm.packaging</groupId>
<artifactId>scm-packaging</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>windows</artifactId>
<packaging>pom</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<dependencies>

View File

@@ -31,13 +31,13 @@
<parent>
<groupId>sonia.scm</groupId>
<artifactId>scm</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-plugins</artifactId>
<packaging>pom</packaging>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<name>scm-plugins</name>
<modules>
@@ -60,7 +60,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-core</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
@@ -69,7 +69,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-annotation-processor</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
@@ -99,7 +99,7 @@
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-test</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>

View File

@@ -1,7 +1,7 @@
{
"name": "@scm-manager/scm-git-plugin",
"private": true,
"version": "2.10.0-SNAPSHOT",
"version": "2.11.0-SNAPSHOT",
"license": "MIT",
"main": "./src/main/js/index.ts",
"scripts": {
@@ -20,6 +20,6 @@
},
"prettier": "@scm-manager/prettier-config",
"dependencies": {
"@scm-manager/ui-plugins": "^2.10.0-SNAPSHOT"
"@scm-manager/ui-plugins": "^2.11.0-SNAPSHOT"
}
}

View File

@@ -31,7 +31,7 @@
<parent>
<artifactId>scm-plugins</artifactId>
<groupId>sonia.scm.plugins</groupId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>scm-git-plugin</artifactId>

View File

@@ -39,7 +39,7 @@ import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
@NoArgsConstructor
@Getter
@Setter
public class GitConfigDto extends HalRepresentation {
public class GitConfigDto extends HalRepresentation implements UpdateGitConfigDto {
private boolean disabled = false;

View File

@@ -27,7 +27,9 @@ package sonia.scm.api.v2.resources;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import sonia.scm.config.ConfigurationPermissions;
@@ -116,7 +118,23 @@ public class GitConfigResource {
@PUT
@Path("")
@Consumes(GitVndMediaType.GIT_CONFIG)
@Operation(summary = "Modify git configuration", description = "Modifies the global git configuration.", tags = "Git", operationId = "git_put_config")
@Operation(
summary = "Modify git configuration",
description = "Modifies the global git configuration.",
tags = "Git",
operationId = "git_put_config",
requestBody = @RequestBody(
content = @Content(
mediaType = GitVndMediaType.GIT_CONFIG,
schema = @Schema(implementation = UpdateGitConfigDto.class),
examples = @ExampleObject(
name = "Overwrites current configuration with this one.",
value = "{\n \"disabled\":false,\n \"gcExpression\":null,\n \"nonFastForwardDisallowed\":false,\n \"defaultBranch\":\"main\"\n}",
summary = "Simple update configuration"
)
)
)
)
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:git\" privilege")

View File

@@ -36,7 +36,7 @@ import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@SuppressWarnings("squid:S2160") // there is no proper semantic for equals on this dto
public class GitRepositoryConfigDto extends HalRepresentation {
public class GitRepositoryConfigDto extends HalRepresentation implements UpdateGitRepositoryConfigDto {
private String defaultBranch;

View File

@@ -26,8 +26,11 @@ package sonia.scm.api.v2.resources;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitRepositoryConfig;
@@ -106,7 +109,22 @@ public class GitRepositoryConfigResource {
@PUT
@Path("/")
@Consumes(GitVndMediaType.GIT_REPOSITORY_CONFIG)
@Operation(summary = "Modifies git repository configuration", description = "Modifies the repository related git configuration.", tags = "Git")
@Operation(
summary = "Modifies git repository configuration",
description = "Modifies the repository related git configuration.",
tags = "Git",
requestBody = @RequestBody(
content = @Content(
mediaType = GitVndMediaType.GIT_REPOSITORY_CONFIG,
schema = @Schema(implementation = UpdateGitRepositoryConfigDto.class),
examples = @ExampleObject(
name = "Overwrites current configuration with this one.",
value = "{\n \"defaultBranch\":\"main\"\n}",
summary = "Simple update configuration"
)
)
)
)
@ApiResponse(
responseCode = "204",
description = "update success"

View File

@@ -0,0 +1,46 @@
/*
* 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.api.v2.resources;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
interface UpdateGitConfigDto {
boolean isDisabled();
String getGcExpression();
boolean isNonFastForwardDisallowed();
@NotEmpty
@Length(min = 1, max = 100)
@Pattern(regexp = VALID_BRANCH_NAMES)
String getDefaultBranch();
}

View File

@@ -0,0 +1,29 @@
/*
* 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.api.v2.resources;
interface UpdateGitRepositoryConfigDto {
String getDefaultBranch();
}

View File

@@ -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)) {

View File

@@ -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;
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@scm-manager/scm-hg-plugin",
"private": true,
"version": "2.10.0-SNAPSHOT",
"version": "2.11.0-SNAPSHOT",
"license": "MIT",
"main": "./src/main/js/index.ts",
"scripts": {
@@ -19,6 +19,6 @@
},
"prettier": "@scm-manager/prettier-config",
"dependencies": {
"@scm-manager/ui-plugins": "^2.10.0-SNAPSHOT"
"@scm-manager/ui-plugins": "^2.11.0-SNAPSHOT"
}
}

View File

@@ -31,7 +31,7 @@
<parent>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-plugins</artifactId>
<version>2.10.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
</parent>
<artifactId>scm-hg-plugin</artifactId>
@@ -44,15 +44,29 @@
<dependency>
<groupId>com.aragost.javahg</groupId>
<artifactId>javahg</artifactId>
<version>0.15-scm1</version>
<version>0.16</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -27,7 +27,9 @@ package sonia.scm.api.v2.resources;
import com.google.inject.Inject;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.repository.HgConfig;
@@ -83,7 +85,22 @@ public class HgConfigAutoConfigurationResource {
@PUT
@Path("")
@Consumes(HgVndMediaType.CONFIG)
@Operation(summary = "Modifies hg configuration and installs hg binary", description = "Modifies the mercurial config and installs the mercurial binary.", tags = "Mercurial")
@Operation(
summary = "Modifies hg configuration and installs hg binary",
description = "Modifies the mercurial config and installs the mercurial binary.",
tags = "Mercurial",
requestBody = @RequestBody(
content = @Content(
mediaType = HgVndMediaType.CONFIG,
schema = @Schema(implementation = UpdateHgConfigDto.class),
examples = @ExampleObject(
name = "Overwrites current configuration with this one and installs the mercurial binary.",
value = "{\n \"disabled\":false,\n \"hgBinary\":\"hg\",\n \"pythonBinary\":\"python\",\n \"pythonPath\":\"\",\n \"encoding\":\"UTF-8\",\n \"useOptimizedBytecode\":false,\n \"showRevisionInId\":false,\n \"disableHookSSLValidation\":false,\n \"enableHttpPostArgs\":false\n}",
summary = "Simple update configuration and installs binary"
)
)
)
)
@ApiResponse(
responseCode = "204",
description = "update success"

View File

@@ -30,10 +30,12 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@Getter
@Setter
public class HgConfigDto extends HalRepresentation {
@NoArgsConstructor
@SuppressWarnings("java:S2160") // we don't need equals for dto
public class HgConfigDto extends HalRepresentation implements UpdateHgConfigDto {
private boolean disabled;
@@ -44,7 +46,6 @@ public class HgConfigDto extends HalRepresentation {
private boolean useOptimizedBytecode;
private boolean showRevisionInId;
private boolean enableHttpPostArgs;
private boolean disableHookSSLValidation;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package

View File

@@ -27,7 +27,9 @@ package sonia.scm.api.v2.resources;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import sonia.scm.config.ConfigurationPermissions;
@@ -121,7 +123,23 @@ public class HgConfigResource {
@PUT
@Path("")
@Consumes(HgVndMediaType.CONFIG)
@Operation(summary = "Modify hg configuration", description = "Modifies the global mercurial configuration.", tags = "Mercurial", operationId = "hg_put_config")
@Operation(
summary = "Modify hg configuration",
description = "Modifies the global mercurial configuration.",
tags = "Mercurial",
operationId = "hg_put_config",
requestBody = @RequestBody(
content = @Content(
mediaType = HgVndMediaType.CONFIG,
schema = @Schema(implementation = UpdateHgConfigDto.class),
examples = @ExampleObject(
name = "Overwrites current configuration with this one.",
value = "{\n \"disabled\":false,\n \"hgBinary\":\"hg\",\n \"pythonBinary\":\"python\",\n \"pythonPath\":\"\",\n \"encoding\":\"UTF-8\",\n \"useOptimizedBytecode\":false,\n \"showRevisionInId\":false,\n \"disableHookSSLValidation\":false,\n \"enableHttpPostArgs\":false\n}",
summary = "Simple update configuration"
)
)
)
)
@ApiResponse(
responseCode = "204",
description = "update success"

View File

@@ -0,0 +1,43 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
interface UpdateHgConfigDto {
boolean isDisabled();
String getHgBinary();
String getPythonBinary();
String getPythonPath();
String getEncoding();
boolean isUseOptimizedBytecode();
boolean isShowRevisionInId();
boolean isEnableHttpPostArgs();
}

View File

@@ -1,391 +0,0 @@
/*
* 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;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
import sonia.scm.web.HgUtil;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class AbstractHgHandler
{
/** Field description */
protected static final String ENV_ID_REVISION = "SCM_ID_REVISION";
/** Field description */
protected static final String ENV_NODE = "HG_NODE";
/** Field description */
protected static final String ENV_PAGE_LIMIT = "SCM_PAGE_LIMIT";
/** Field description */
protected static final String ENV_PAGE_START = "SCM_PAGE_START";
/** Field description */
protected static final String ENV_PATH = "SCM_PATH";
/** Field description */
protected static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
/** Field description */
protected static final String ENV_REVISION = "SCM_REVISION";
/** Field description */
protected static final String ENV_REVISION_END = "SCM_REVISION_END";
/** Field description */
protected static final String ENV_REVISION_START = "SCM_REVISION_START";
/** Field description */
private static final String ENCODING = "UTF-8";
/** mercurial encoding */
private static final String ENV_HGENCODING = "HGENCODING";
/** Field description */
private static final String ENV_PENDING = "HG_PENDING";
/** python encoding */
private static final String ENV_PYTHONIOENCODING = "PYTHONIOENCODING";
/** Field description */
private static final String ENV_PYTHONPATH = "PYTHONPATH";
/**
* the logger for AbstractHgCommand
*/
private static final Logger logger =
LoggerFactory.getLogger(AbstractHgHandler.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
* @param handler
* @param context
* @param repository
*/
protected AbstractHgHandler(HgRepositoryHandler handler, HgContext context,
Repository repository)
{
this(handler, context, repository, handler.getDirectory(repository.getId()));
}
/**
* Constructs ...
*
*
*
* @param handler
* @param context
* @param repository
* @param repositoryDirectory
*/
protected AbstractHgHandler(HgRepositoryHandler handler, HgContext context,
Repository repository, File repositoryDirectory)
{
this.handler = handler;
this.context = context;
this.repository = repository;
this.repositoryDirectory = repositoryDirectory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param revision
* @param path
*
* @return
*/
protected Map<String, String> createEnvironment(String revision, String path)
{
Map<String, String> env = new HashMap<>();
env.put(ENV_REVISION, HgUtil.getRevision(revision));
env.put(ENV_PATH, Util.nonNull(path));
return env;
}
/**
* Method description
*
*
* @param args
*
* @return
*
* @throws IOException
*/
protected Process createHgProcess(String... args) throws IOException
{
return createHgProcess(new HashMap<String, String>(), args);
}
/**
* Method description
*
*
* @param extraEnv
* @param args
*
* @return
*
* @throws IOException
*/
protected Process createHgProcess(Map<String, String> extraEnv,
String... args)
throws IOException
{
return createProcess(extraEnv, handler.getConfig().getHgBinary(), args);
}
/**
* Method description
*
*
* @param script
* @param extraEnv
*
* @return
*
* @throws IOException
*/
protected Process createScriptProcess(HgPythonScript script,
Map<String, String> extraEnv)
throws IOException
{
return createProcess(extraEnv, handler.getConfig().getPythonBinary(),
script.getFile(SCMContext.getContext()).getAbsolutePath());
}
/**
* Method description
*
*
* @param errorStream
*/
protected void handleErrorStream(final InputStream errorStream)
{
if (errorStream != null)
{
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
String content = IOUtil.getContent(errorStream);
if (Util.isNotEmpty(content))
{
logger.error(content.trim());
}
}
catch (IOException ex)
{
logger.error("error during logging", ex);
}
}
}).start();
}
}
//~--- get methods ----------------------------------------------------------
protected <T> T getResultFromScript(Class<T> resultType, HgPythonScript script) throws IOException {
return getResultFromScript(resultType, script,
new HashMap<String, String>());
}
@SuppressWarnings("unchecked")
protected <T> T getResultFromScript(Class<T> resultType,
HgPythonScript script, Map<String, String> extraEnv)
throws IOException
{
Process p = createScriptProcess(script, extraEnv);
handleErrorStream(p.getErrorStream());
try (InputStream input = p.getInputStream()) {
return (T) handler.getJaxbContext().createUnmarshaller().unmarshal(input);
} catch (JAXBException ex) {
logger.error("could not parse result", ex);
throw new InternalRepositoryException(repository, "could not parse result", ex);
}
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param extraEnv
* @param cmd
* @param args
*
* @return
*
* @throws IOException
*/
private Process createProcess(Map<String, String> extraEnv, String cmd,
String... args)
throws IOException
{
HgConfig config = handler.getConfig();
List<String> cmdList = new ArrayList<String>();
cmdList.add(cmd);
if (Util.isNotEmpty(args))
{
cmdList.addAll(Arrays.asList(args));
}
if (logger.isDebugEnabled())
{
StringBuilder msg = new StringBuilder("create process for [");
Iterator<String> it = cmdList.iterator();
while (it.hasNext())
{
msg.append(it.next());
if (it.hasNext())
{
msg.append(", ");
}
}
msg.append("]");
logger.debug(msg.toString());
}
ProcessBuilder pb = new ProcessBuilder(cmdList);
pb.directory(repositoryDirectory);
Map<String, String> env = pb.environment();
// force utf-8 encoding for mercurial and python
env.put(ENV_PYTHONIOENCODING, ENCODING);
env.put(ENV_HGENCODING, ENCODING);
//J-
env.put(ENV_ID_REVISION,
String.valueOf(handler.getConfig().isShowRevisionInId())
);
//J+
if (context.isSystemEnvironment())
{
env.putAll(System.getenv());
}
if (context.isPending())
{
if (logger.isDebugEnabled())
{
logger.debug("enable hg pending for {}",
repositoryDirectory.getAbsolutePath());
}
env.put(ENV_PENDING, repositoryDirectory.getAbsolutePath());
if (extraEnv.containsKey(ENV_REVISION_START))
{
env.put(ENV_NODE, extraEnv.get(ENV_REVISION_START));
}
}
env.put(ENV_PYTHONPATH, HgUtil.getPythonPath(config));
env.put(ENV_REPOSITORY_PATH, repositoryDirectory.getAbsolutePath());
env.putAll(extraEnv);
if (logger.isTraceEnabled())
{
StringBuilder msg = new StringBuilder("start process in directory '");
msg.append(repositoryDirectory.getAbsolutePath()).append(
"' with env: \n");
for (Map.Entry<String, String> e : env.entrySet())
{
msg.append(" ").append(e.getKey());
msg.append(" = ").append(e.getValue());
msg.append("\n");
}
logger.trace(msg.toString());
}
return pb.start();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
protected Repository repository;
/** Field description */
protected File repositoryDirectory;
/** Field description */
private HgContext context;
/** Field description */
private HgRepositoryHandler handler;
}

View File

@@ -0,0 +1,143 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import sonia.scm.TransactionId;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.repository.hooks.HookServer;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Xsrf;
import sonia.scm.web.HgUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.util.Map;
@Singleton
public class DefaultHgEnvironmentBuilder implements HgEnvironmentBuilder {
@VisibleForTesting
static final String ENV_PYTHON_PATH = "PYTHONPATH";
@VisibleForTesting
static final String ENV_HOOK_PORT = "SCM_HOOK_PORT";
@VisibleForTesting
static final String ENV_CHALLENGE = "SCM_CHALLENGE";
@VisibleForTesting
static final String ENV_BEARER_TOKEN = "SCM_BEARER_TOKEN";
@VisibleForTesting
static final String ENV_REPOSITORY_NAME = "REPO_NAME";
@VisibleForTesting
static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
@VisibleForTesting
static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
@VisibleForTesting
static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
@VisibleForTesting
static final String ENV_TRANSACTION_ID = "SCM_TRANSACTION_ID";
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
private final HgRepositoryHandler repositoryHandler;
private final HookEnvironment hookEnvironment;
private final HookServer server;
private int hookPort = -1;
@Inject
public DefaultHgEnvironmentBuilder(
AccessTokenBuilderFactory accessTokenBuilderFactory, HgRepositoryHandler repositoryHandler,
HookEnvironment hookEnvironment, HookServer server
) {
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
this.repositoryHandler = repositoryHandler;
this.hookEnvironment = hookEnvironment;
this.server = server;
}
@Override
public Map<String, String> read(Repository repository) {
ImmutableMap.Builder<String, String> env = ImmutableMap.builder();
read(env, repository);
return env.build();
}
@Override
public Map<String, String> write(Repository repository) {
ImmutableMap.Builder<String, String> env = ImmutableMap.builder();
read(env, repository);
write(env);
return env.build();
}
private void read(ImmutableMap.Builder<String, String> env, Repository repository) {
HgConfig config = repositoryHandler.getConfig();
env.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(config));
File directory = repositoryHandler.getDirectory(repository.getId());
env.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
env.put(ENV_REPOSITORY_ID, repository.getId());
env.put(ENV_REPOSITORY_PATH, directory.getAbsolutePath());
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
env.put(ENV_HTTP_POST_ARGS, String.valueOf(config.isEnableHttpPostArgs()));
}
private void write(ImmutableMap.Builder<String, String> env) {
env.put(ENV_HOOK_PORT, String.valueOf(getHookPort()));
env.put(ENV_BEARER_TOKEN, accessToken());
env.put(ENV_CHALLENGE, hookEnvironment.getChallenge());
TransactionId.get().ifPresent(transactionId -> env.put(ENV_TRANSACTION_ID, transactionId));
}
private String accessToken() {
AccessToken accessToken = accessTokenBuilderFactory.create()
// disable xsrf protection, because we can not access the http servlet request for verification
.custom(Xsrf.TOKEN_KEY, null)
.build();
return CipherUtil.getInstance().encode(accessToken.compact());
}
private synchronized int getHookPort() {
if (hookPort > 0) {
return hookPort;
}
try {
hookPort = server.start();
} catch (IOException ex) {
throw new IllegalStateException("failed to start mercurial hook server");
}
return hookPort;
}
}

View File

@@ -36,20 +36,10 @@ import javax.xml.bind.annotation.XmlTransient;
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "config")
public class HgConfig extends RepositoryConfig
{
public class HgConfig extends RepositoryConfig {
public static final String PERMISSION = "hg";
/**
* Constructs ...
*
*/
public HgConfig() {}
//~--- get methods ----------------------------------------------------------
@Override
@XmlTransient // Only for permission checks, don't serialize to XML
public String getId() {
@@ -123,10 +113,6 @@ public class HgConfig extends RepositoryConfig
return useOptimizedBytecode;
}
public boolean isDisableHookSSLValidation() {
return disableHookSSLValidation;
}
public boolean isEnableHttpPostArgs() {
return enableHttpPostArgs;
}
@@ -216,10 +202,6 @@ public class HgConfig extends RepositoryConfig
this.useOptimizedBytecode = useOptimizedBytecode;
}
public void setDisableHookSSLValidation(boolean disableHookSSLValidation) {
this.disableHookSSLValidation = disableHookSSLValidation;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -242,9 +224,4 @@ public class HgConfig extends RepositoryConfig
private boolean enableHttpPostArgs = false;
/**
* disable validation of ssl certificates for mercurial hook
* @see <a href="https://goo.gl/zH5eY8">Issue 959</a>
*/
private boolean disableHookSSLValidation = false;
}

View File

@@ -1,121 +0,0 @@
/*
* 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;
//~--- non-JDK imports --------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class HgContext
{
/**
* Constructs ...
*
*/
public HgContext() {}
/**
* Constructs ...
*
*
* @param pending
*/
public HgContext(boolean pending)
{
this.pending = pending;
}
/**
* Constructs ...
*
*
* @param pending
* @param systemEnvironment
*/
public HgContext(boolean pending, boolean systemEnvironment)
{
this.pending = pending;
this.systemEnvironment = systemEnvironment;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public boolean isPending()
{
return pending;
}
/**
* Method description
*
*
* @return
*/
public boolean isSystemEnvironment()
{
return systemEnvironment;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param pending
*/
public void setPending(boolean pending)
{
this.pending = pending;
}
/**
* Method description
*
*
* @param systemEnvironment
*/
public void setSystemEnvironment(boolean systemEnvironment)
{
this.systemEnvironment = systemEnvironment;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private boolean pending = false;
/** Field description */
private boolean systemEnvironment = true;
}

View File

@@ -1,96 +0,0 @@
/*
* 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;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
/**
* Injection provider for {@link HgContext}.
* This provider returns an instance {@link HgContext} from request scope, if no {@link HgContext} could be found in
* request scope (mostly because the scope is not available) a new {@link HgContext} gets returned.
*
* @author Sebastian Sdorra
*/
public class HgContextProvider implements Provider<HgContext>
{
/**
* the LOG for HgContextProvider
*/
private static final Logger LOG =
LoggerFactory.getLogger(HgContextProvider.class);
//~--- get methods ----------------------------------------------------------
private Provider<HgContextRequestStore> requestStoreProvider;
@Inject
public HgContextProvider(Provider<HgContextRequestStore> requestStoreProvider) {
this.requestStoreProvider = requestStoreProvider;
}
@VisibleForTesting
public HgContextProvider() {
}
@Override
public HgContext get() {
HgContext context = fetchContextFromRequest();
if (context != null) {
LOG.trace("return HgContext from request store");
return context;
}
LOG.trace("could not find context in request scope, returning new instance");
return new HgContext();
}
private HgContext fetchContextFromRequest() {
try {
if (requestStoreProvider != null) {
return requestStoreProvider.get().get();
} else {
LOG.trace("no request store provider defined, could not return context from request");
return null;
}
} catch (ProvisionException ex) {
if (ex.getCause() instanceof OutOfScopeException) {
LOG.trace("we are currently out of request scope, failed to retrieve context");
return null;
} else {
throw ex;
}
}
}
}

View File

@@ -1,127 +0,0 @@
/*
* 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;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.ProvisionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.AccessToken;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Xsrf;
import sonia.scm.web.HgUtil;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public final class HgEnvironment
{
private static final Logger LOG = LoggerFactory.getLogger(HgEnvironment.class);
/** Field description */
public static final String ENV_PYTHON_PATH = "PYTHONPATH";
/** Field description */
private static final String ENV_CHALLENGE = "SCM_CHALLENGE";
/** Field description */
private static final String ENV_URL = "SCM_URL";
private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
private static final String SCM_XSRF = "SCM_XSRF";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private HgEnvironment() {}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param environment
* @param handler
* @param hookManager
*/
public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager)
{
prepareEnvironment(environment, handler, hookManager, null);
}
/**
* Method description
*
*
* @param environment
* @param handler
* @param hookManager
* @param request
*/
public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager,
HttpServletRequest request)
{
String hookUrl;
if (request != null)
{
hookUrl = hookManager.createUrl(request);
}
else
{
hookUrl = hookManager.createUrl();
}
try {
AccessToken accessToken = hookManager.getAccessToken();
environment.put(SCM_BEARER_TOKEN, CipherUtil.getInstance().encode(accessToken.compact()));
extractXsrfKey(environment, accessToken);
} catch (ProvisionException e) {
LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e);
}
environment.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig()));
environment.put(ENV_URL, hookUrl);
environment.put(ENV_CHALLENGE, hookManager.getChallenge());
}
private static void extractXsrfKey(Map<String, String> environment, AccessToken accessToken) {
environment.put(SCM_XSRF, accessToken.<String>getCustom(Xsrf.TOKEN_KEY).orElse("-"));
}
}

View File

@@ -24,25 +24,12 @@
package sonia.scm.repository;
import com.google.inject.servlet.RequestScoped;
import com.google.inject.ImplementedBy;
/**
* Holds an instance of {@link HgContext} in the request scope.
*
* <p>The problem seems to be that guice had multiple options for injecting HgContext. {@link HgContextProvider}
* bound via Module and {@link HgContext} bound void {@link RequestScoped} annotation. It looks like that Guice 4
* injects randomly the one or the other, in SCMv1 (Guice 3) everything works as expected.</p>
*
* <p>To fix the problem we have created this class annotated with {@link RequestScoped}, which holds an instance
* of {@link HgContext}. This way only the {@link HgContextProvider} is used for injection.</p>
*/
@RequestScoped
public class HgContextRequestStore {
private final HgContext context = new HgContext();
public HgContext get() {
return context;
}
import java.util.Map;
@ImplementedBy(DefaultHgEnvironmentBuilder.class)
public interface HgEnvironmentBuilder {
Map<String, String> read(Repository repository);
Map<String, String> write(Repository repository);
}

View File

@@ -1,354 +0,0 @@
/*
* 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;
//~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe;
import com.google.common.base.MoreObjects;
import com.google.inject.Inject;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.config.ScmConfigurationChangedEvent;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class HgHookManager {
@SuppressWarnings("java:S1075") // this url is fixed
private static final String URL_HOOKPATH = "/hook/hg/";
/**
* the logger for HgHookManager
*/
private static final Logger logger =
LoggerFactory.getLogger(HgHookManager.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
* @param configuration
* @param httpServletRequestProvider
* @param httpClient
* @param accessTokenBuilderFactory
*/
@Inject
public HgHookManager(ScmConfiguration configuration,
Provider<HttpServletRequest> httpServletRequestProvider,
AdvancedHttpClient httpClient, AccessTokenBuilderFactory accessTokenBuilderFactory)
{
this.configuration = configuration;
this.httpServletRequestProvider = httpServletRequestProvider;
this.httpClient = httpClient;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param config
*/
@Subscribe(async = false)
public void configChanged(ScmConfigurationChangedEvent config)
{
hookUrl = null;
}
/**
* Method description
*
*
* @param request
*
* @return
*/
public String createUrl(HttpServletRequest request)
{
if (hookUrl == null)
{
synchronized (this)
{
if (hookUrl == null)
{
buildHookUrl(request);
if (logger.isInfoEnabled() && Util.isNotEmpty(hookUrl))
{
logger.info("use {} for mercurial hooks", hookUrl);
}
}
}
}
return hookUrl;
}
/**
* Method description
*
*
* @return
*/
public String createUrl()
{
String url = hookUrl;
if (url == null)
{
HttpServletRequest request = getHttpServletRequest();
if (request != null)
{
url = createUrl(request);
}
else
{
url = createConfiguredUrl();
logger.warn(
"created url {} without request, in some cases this could cause problems",
url);
}
}
return url;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getChallenge()
{
return challenge;
}
/**
* Method description
*
*
* @param challenge
*
* @return
*/
public boolean isAcceptAble(String challenge)
{
return this.challenge.equals(challenge);
}
public AccessToken getAccessToken()
{
return accessTokenBuilderFactory.create().build();
}
private void buildHookUrl(HttpServletRequest request) {
if (configuration.isForceBaseUrl()) {
logger.debug("create hook url from configured base url because force base url is enabled");
hookUrl = createConfiguredUrl();
if (!isUrlWorking(hookUrl)) {
disableHooks();
}
} else {
logger.debug("create hook url from request");
hookUrl = HttpUtil.getCompleteUrl(request, URL_HOOKPATH);
if (!isUrlWorking(hookUrl)) {
logger.warn("hook url {} from request does not work, try now localhost", hookUrl);
hookUrl = createLocalUrl(request);
if (!isUrlWorking(hookUrl)) {
logger.warn("localhost hook url {} does not work, try now from configured base url", hookUrl);
hookUrl = createConfiguredUrl();
if (!isUrlWorking(hookUrl)) {
disableHooks();
}
}
}
}
}
/**
* Method description
*
*
* @return
*/
private String createConfiguredUrl()
{
//J-
return HttpUtil.getUriWithoutEndSeperator(
MoreObjects.firstNonNull(
configuration.getBaseUrl(),
"http://localhost:8080/scm"
)
).concat(URL_HOOKPATH);
//J+
}
/**
* Method description
*
*
* @param request
*
* @return
*/
private String createLocalUrl(HttpServletRequest request)
{
StringBuilder sb = new StringBuilder(request.getScheme());
sb.append("://localhost:").append(request.getLocalPort());
sb.append(request.getContextPath()).append(URL_HOOKPATH);
return sb.toString();
}
/**
* Method description
*
*/
private void disableHooks()
{
if (logger.isErrorEnabled())
{
logger.error(
"disabling mercurial hooks, because hook url {} seems not to work",
hookUrl);
}
hookUrl = Util.EMPTY_STRING;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
private HttpServletRequest getHttpServletRequest()
{
HttpServletRequest request = null;
try
{
request = httpServletRequestProvider.get();
}
catch (ProvisionException | OutOfScopeException ex)
{
logger.debug("http servlet request is not available");
}
return request;
}
/**
* Method description
*
*
* @param url
*
* @return
*/
private boolean isUrlWorking(String url)
{
boolean result = false;
try
{
url = url.concat("?ping=true");
logger.trace("check hook url {}", url);
//J-
int sc = httpClient.get(url)
.disableHostnameValidation(true)
.disableCertificateValidation(true)
.ignoreProxySettings(true)
.disableTracing()
.request()
.getStatus();
//J+
result = sc == 204;
}
catch (IOException ex)
{
if (logger.isTraceEnabled())
{
logger.trace("url test failed for url ".concat(url), ex);
}
}
return result;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String challenge = UUID.randomUUID().toString();
/** Field description */
private ScmConfiguration configuration;
/** Field description */
private volatile String hookUrl;
/** Field description */
private AdvancedHttpClient httpClient;
/** Field description */
private Provider<HttpServletRequest> httpServletRequestProvider;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
}

View File

@@ -38,80 +38,30 @@ import java.io.File;
*/
public enum HgPythonScript {
HOOK("scmhooks.py"), HGWEB("hgweb.py"), VERSION("version.py");
HOOK("scmhooks.py"), HGWEB("hgweb.py");
/** Field description */
private static final String BASE_DIRECTORY =
"lib".concat(File.separator).concat("python");
/** Field description */
private static final String BASE_DIRECTORY = "lib".concat(File.separator).concat("python");
private static final String BASE_RESOURCE = "/sonia/scm/python/";
//~--- constructors ---------------------------------------------------------
private final String name;
/**
* Constructs ...
*
*
* @param name
*/
private HgPythonScript(String name)
{
HgPythonScript(String name) {
this.name = name;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param context
*
* @return
*/
public static File getScriptDirectory(SCMContextProvider context)
{
public static File getScriptDirectory(SCMContextProvider context) {
return new File(context.getBaseDirectory(), BASE_DIRECTORY);
}
/**
* Method description
*
*
* @param context
*
* @return
*/
public File getFile(SCMContextProvider context)
{
public File getFile(SCMContextProvider context) {
return new File(getScriptDirectory(context), name);
}
/**
* Method description
*
*
* @return
*/
public String getName()
{
public String getName() {
return name;
}
/**
* Method description
*
*
* @return
*/
public String getResourcePath()
{
public String getResourcePath() {
return BASE_RESOURCE.concat(name);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String name;
}

View File

@@ -0,0 +1,106 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.aragost.javahg.RepositoryConfiguration;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.repository.spi.javahg.HgFileviewExtension;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Map;
import java.util.function.Function;
@Singleton
public class HgRepositoryFactory {
private static final Logger LOG = LoggerFactory.getLogger(HgRepositoryFactory.class);
private final HgRepositoryHandler handler;
private final HookEnvironment hookEnvironment;
private final HgEnvironmentBuilder environmentBuilder;
private final Function<Repository, File> directoryResolver;
@Inject
public HgRepositoryFactory(HgRepositoryHandler handler, HookEnvironment hookEnvironment, HgEnvironmentBuilder environmentBuilder) {
this(
handler, hookEnvironment, environmentBuilder,
repository -> handler.getDirectory(repository.getId())
);
}
@VisibleForTesting
public HgRepositoryFactory(HgRepositoryHandler handler, HookEnvironment hookEnvironment, HgEnvironmentBuilder environmentBuilder, Function<Repository, File> directoryResolver) {
this.handler = handler;
this.hookEnvironment = hookEnvironment;
this.environmentBuilder = environmentBuilder;
this.directoryResolver = directoryResolver;
}
public com.aragost.javahg.Repository openForRead(Repository repository) {
return open(repository, environmentBuilder.read(repository));
}
public com.aragost.javahg.Repository openForWrite(Repository repository) {
return open(repository, environmentBuilder.write(repository));
}
private com.aragost.javahg.Repository open(Repository repository, Map<String, String> environment) {
File directory = directoryResolver.apply(repository);
RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT;
repoConfiguration.getEnvironment().putAll(environment);
repoConfiguration.addExtension(HgFileviewExtension.class);
boolean pending = hookEnvironment.isPending();
repoConfiguration.setEnablePendingChangesets(pending);
Charset encoding = encoding();
repoConfiguration.setEncoding(encoding);
repoConfiguration.setHgBin(handler.getConfig().getHgBinary());
LOG.trace("open hg repository {}: encoding: {}, pending: {}", directory, encoding, pending);
return com.aragost.javahg.Repository.open(repoConfiguration, directory);
}
private Charset encoding() {
String charset = handler.getConfig().getEncoding();
try {
return Charset.forName(charset);
} catch (UnsupportedCharsetException ex) {
LOG.warn("unknown charset {} in hg config, fallback to utf-8", charset);
return StandardCharsets.UTF_8;
}
}
}

View File

@@ -27,11 +27,9 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ConfigurationException;
import sonia.scm.SCMContextProvider;
import sonia.scm.autoconfig.AutoConfigurator;
import sonia.scm.installer.HgInstaller;
@@ -43,14 +41,14 @@ import sonia.scm.io.INISection;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.HgRepositoryServiceProvider;
import sonia.scm.repository.spi.HgVersionCommand;
import sonia.scm.repository.spi.HgWorkingCopyFactory;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.IOUtil;
import sonia.scm.util.SystemUtil;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -63,14 +61,15 @@ import java.util.Optional;
public class HgRepositoryHandler
extends AbstractSimpleRepositoryHandler<HgConfig> {
public static final String PATH_HOOK = ".hook-1.8";
public static final String RESOURCE_VERSION = "sonia/scm/version/scm-hg-plugin";
public static final String TYPE_DISPLAYNAME = "Mercurial";
public static final String TYPE_NAME = "hg";
public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME,
public static final RepositoryType TYPE = new RepositoryType(
TYPE_NAME,
TYPE_DISPLAYNAME,
HgRepositoryServiceProvider.COMMANDS,
HgRepositoryServiceProvider.FEATURES);
HgRepositoryServiceProvider.FEATURES
);
private static final Logger logger = LoggerFactory.getLogger(HgRepositoryHandler.class);
@@ -78,28 +77,14 @@ public class HgRepositoryHandler
private static final String CONFIG_SECTION_SCMM = "scmm";
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
private final Provider<HgContext> hgContextProvider;
private final HgWorkingCopyFactory workingCopyFactory;
private final JAXBContext jaxbContext;
@Inject
public HgRepositoryHandler(ConfigurationStoreFactory storeFactory,
Provider<HgContext> hgContextProvider,
RepositoryLocationResolver repositoryLocationResolver,
PluginLoader pluginLoader, HgWorkingCopyFactory workingCopyFactory) {
super(storeFactory, repositoryLocationResolver, pluginLoader);
this.hgContextProvider = hgContextProvider;
this.workingCopyFactory = workingCopyFactory;
try {
this.jaxbContext = JAXBContext.newInstance(BrowserResult.class,
BlameResult.class, Changeset.class, ChangesetPagingResult.class,
HgVersion.class);
} catch (JAXBException ex) {
throw new ConfigurationException("could not create jaxbcontext", ex);
}
}
public void doAutoConfiguration(HgConfig autoConfig) {
@@ -107,8 +92,7 @@ public class HgRepositoryHandler
try {
if (logger.isDebugEnabled()) {
logger.debug("installing mercurial with {}",
installer.getClass().getName());
logger.debug("installing mercurial with {}", installer.getClass().getName());
}
installer.install(baseDirectory, autoConfig);
@@ -154,16 +138,6 @@ public class HgRepositoryHandler
}
}
public HgContext getHgContext() {
HgContext context = hgContextProvider.get();
if (context == null) {
context = new HgContext();
}
return context;
}
@Override
public ImportHandler getImportHandler() {
return new HgImportHandler(this);
@@ -176,28 +150,14 @@ public class HgRepositoryHandler
@Override
public String getVersionInformation() {
String version = getStringFromResource(RESOURCE_VERSION,
DEFAULT_VERSION_INFORMATION);
return getVersionInformation(new HgVersionCommand(getConfig()));
}
try {
HgVersion hgVersion = new HgVersionHandler(this, hgContextProvider.get(),
baseDirectory).getVersion();
if (hgVersion != null) {
if (logger.isDebugEnabled()) {
String getVersionInformation(HgVersionCommand command) {
String version = getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION);
HgVersion hgVersion = command.get();
logger.debug("mercurial/python informations: {}", hgVersion);
}
version = MessageFormat.format(version, hgVersion.getPython(),
hgVersion.getMercurial());
} else if (logger.isWarnEnabled()) {
logger.warn("could not retrieve version informations");
}
} catch (Exception ex) {
logger.error("could not read version informations", ex);
}
return version;
return MessageFormat.format(version, hgVersion.getPython(), hgVersion.getMercurial());
}
@Override
@@ -253,28 +213,24 @@ public class HgRepositoryHandler
logger.debug("write python script {}", script.getName());
}
InputStream content = null;
OutputStream output = null;
try {
content = HgRepositoryHandler.class.getResourceAsStream(
script.getResourcePath());
output = new FileOutputStream(script.getFile(context));
try (InputStream content = input(script); OutputStream output = output(context, script)) {
IOUtil.copy(content, output);
} catch (IOException ex) {
logger.error("could not write script", ex);
} finally {
IOUtil.close(content);
IOUtil.close(output);
}
}
}
private InputStream input(HgPythonScript script) {
return HgRepositoryHandler.class.getResourceAsStream(script.getResourcePath());
}
private OutputStream output(SCMContextProvider context, HgPythonScript script) throws FileNotFoundException {
return new FileOutputStream(script.getFile(context));
}
public HgWorkingCopyFactory getWorkingCopyFactory() {
return workingCopyFactory;
}
public JAXBContext getJaxbContext() {
return jaxbContext;
}
}

View File

@@ -24,10 +24,8 @@
package sonia.scm.repository;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.AllArgsConstructor;
import lombok.Data;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@@ -37,13 +35,14 @@ import javax.xml.bind.annotation.XmlRootElement;
*
* @author Sebastian Sdorra
*/
@Data
@AllArgsConstructor
@XmlRootElement(name = "version")
@XmlAccessorType(XmlAccessType.FIELD)
@EqualsAndHashCode
@Getter
@Setter
@ToString
public class HgVersion {
public static final String UNKNOWN = "x.y.z (unknown)";
private String mercurial;
private String python;
}

View File

@@ -26,70 +26,26 @@ package sonia.scm.repository.api;
//~--- JDK imports ------------------------------------------------------------
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
*
* @author Sebastian Sdorra
*/
public final class HgHookMessage implements Serializable
{
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class HgHookMessage implements Serializable {
/** Field description */
private static final long serialVersionUID = 1804492842452344326L;
//~--- constant enums -------------------------------------------------------
/**
* Enum description
*
*/
public static enum Severity { NOTE, ERROR; }
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param severity
* @param message
*/
public HgHookMessage(Severity severity, String message)
{
this.severity = severity;
this.message = message;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getMessage()
{
return message;
}
/**
* Method description
*
*
* @return
*/
public Severity getSeverity()
{
return severity;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private Severity severity;
private String message;
/** Field description */
private Severity severity;
public enum Severity { NOTE, ERROR }
}

View File

@@ -0,0 +1,188 @@
/*
* 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.hooks;
import com.google.inject.assistedinject.Assisted;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ExceptionWithContext;
import sonia.scm.NotFoundException;
import sonia.scm.TransactionId;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import static java.util.Collections.singletonList;
class DefaultHookHandler implements HookHandler {
private static final Logger LOG = LoggerFactory.getLogger(DefaultHookHandler.class);
private final HookEventFacade hookEventFacade;
private final HookEnvironment environment;
private final HookContextProviderFactory hookContextProviderFactory;
private final Socket socket;
@Inject
public DefaultHookHandler(HookContextProviderFactory hookContextProviderFactory, HookEventFacade hookEventFacade, HookEnvironment environment, @Assisted Socket socket) {
this.hookContextProviderFactory = hookContextProviderFactory;
this.hookEventFacade = hookEventFacade;
this.environment = environment;
this.socket = socket;
}
@Override
public void run() {
LOG.trace("start handling hook protocol");
try (InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream()) {
handleHookRequest(input, output);
} catch (IOException e) {
LOG.warn("failed to read hook request", e);
} finally {
LOG.trace("close client socket");
TransactionId.clear();
close();
}
}
private void handleHookRequest(InputStream input, OutputStream output) throws IOException {
Request request = Sockets.receive(input, Request.class);
TransactionId.set(request.getTransactionId());
Response response = handleHookRequest(request);
Sockets.send(output, response);
}
private Response handleHookRequest(Request request) {
LOG.trace("process {} hook for node {}", request.getType(), request.getNode());
if (!environment.isAcceptAble(request.getChallenge())) {
LOG.warn("received hook with invalid challenge: {}", request.getChallenge());
return error("invalid hook challenge");
}
try {
authenticate(request);
return fireHook(request);
} catch (AuthenticationException ex) {
LOG.warn("hook authentication failed", ex);
return error("hook authentication failed");
}
}
@Nonnull
private Response fireHook(Request request) {
HgHookContextProvider context = hookContextProviderFactory.create(request.getRepositoryId(), request.getNode());
try {
environment.setPending(request.getType() == RepositoryHookType.PRE_RECEIVE);
hookEventFacade.handle(request.getRepositoryId()).fireHookEvent(request.getType(), context);
return new Response(context.getHgMessageProvider().getMessages(), false);
} catch (NotFoundException ex) {
LOG.warn("could not find repository with id {}", request.getRepositoryId(), ex);
return error("repository not found");
} catch (ExceptionWithContext ex) {
LOG.debug("scm exception on hook occurred", ex);
return error(context, ex.getMessage());
} catch (Exception ex) {
LOG.warn("unknown error on hook occurred", ex);
return error(context, "unknown error");
} finally {
environment.clearPendingState();
}
}
private void authenticate(Request request) {
LOG.trace("authenticate hook request");
String token = CipherUtil.getInstance().decode(request.getToken());
BearerToken bearer = BearerToken.valueOf(token);
Subject subject = SecurityUtils.getSubject();
subject.login(bearer);
}
private Response error(HgHookContextProvider context, String message) {
List<HgHookMessage> messages = new ArrayList<>(context.getHgMessageProvider().getMessages());
messages.add(createErrorMessage(message));
return new Response(messages, true);
}
private Response error(String message) {
return new Response(
singletonList(createErrorMessage(message)),
true
);
}
@Nonnull
private HgHookMessage createErrorMessage(String message) {
return new HgHookMessage(HgHookMessage.Severity.ERROR, message);
}
private void close() {
try {
socket.close();
} catch (IOException e) {
LOG.debug("failed to close hook socket", e);
}
}
@Data
@AllArgsConstructor
public static class Request {
private String token;
private RepositoryHookType type;
private String transactionId;
private String repositoryId;
private String challenge;
private String node;
}
@Data
@AllArgsConstructor
public static class Response {
private List<HgHookMessage> messages;
private boolean abort;
}
}

View File

@@ -22,59 +22,36 @@
* SOFTWARE.
*/
package sonia.scm.repository.spi;
package sonia.scm.repository.hooks;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.AbstractHgHandler;
import sonia.scm.repository.HgContext;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.spi.HgHookContextProvider;
//~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject;
import java.io.File;
public class HookContextProviderFactory {
import java.util.Map;
private final RepositoryManager repositoryManager;
private final HgRepositoryHandler repositoryHandler;
private final HgRepositoryFactory repositoryFactory;
/**
*
* @author Sebastian Sdorra
*/
public class AbstractHgCommand extends AbstractHgHandler
{
/**
* Constructs ...
*
*
* @param handler
* @param context
* @param repository
* @param repositoryDirectory
*/
protected AbstractHgCommand(HgRepositoryHandler handler, HgContext context,
Repository repository, File repositoryDirectory)
{
super(handler, context, repository, repositoryDirectory);
@Inject
public HookContextProviderFactory(RepositoryManager repositoryManager, HgRepositoryHandler repositoryHandler, HgRepositoryFactory repositoryFactory) {
this.repositoryManager = repositoryManager;
this.repositoryHandler = repositoryHandler;
this.repositoryFactory = repositoryFactory;
}
//~--- methods --------------------------------------------------------------
HgHookContextProvider create(String repositoryId, String node) {
Repository repository = repositoryManager.get(repositoryId);
if (repository == null) {
throw new NotFoundException(Repository.class, repositoryId);
}
return new HgHookContextProvider(repositoryHandler, repositoryFactory, repository, node);
}
/**
* Method description
*
*
* @param revision
* @param path
*
* @param request
*
* @return
*/
protected Map<String,
String> createEnvironment(FileBaseCommandRequest request)
{
return createEnvironment(request.getRevision(), request.getPath());
}
}

View File

@@ -0,0 +1,60 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import javax.inject.Singleton;
import java.util.UUID;
@Singleton
public class HookEnvironment {
private final ThreadLocal<Boolean> threadEnvironment = new ThreadLocal<>();
private final String challenge = UUID.randomUUID().toString();
public String getChallenge() {
return challenge;
}
public boolean isAcceptAble(String challenge) {
return this.challenge.equals(challenge);
}
void setPending(boolean pending) {
threadEnvironment.set(pending);
}
void clearPendingState() {
threadEnvironment.remove();
}
public boolean isPending() {
Boolean threadState = threadEnvironment.get();
if (threadState != null) {
return threadState;
}
return false;
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.hooks;
public interface HookHandler extends Runnable {
}

View File

@@ -0,0 +1,34 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import java.net.Socket;
@FunctionalInterface
interface HookHandlerFactory {
HookHandler create(Socket socket);
}

View File

@@ -0,0 +1,40 @@
/*
* 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.hooks;
import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import sonia.scm.plugin.Extension;
@Extension
public class HookModule extends AbstractModule {
@Override
protected void configure() {
install(new FactoryModuleBuilder()
.implement(HookHandler.class, DefaultHookHandler.class)
.build(HookHandlerFactory.class)
);
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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.hooks;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.util.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
@Singleton
public class HookServer implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(HookServer.class);
private final HookHandlerFactory handlerFactory;
private ExecutorService acceptor;
private ExecutorService workerPool;
private ServerSocket serverSocket;
private SecurityManager securityManager;
@Inject
public HookServer(HookHandlerFactory handlerFactory) {
this.handlerFactory = handlerFactory;
}
public int start() throws IOException {
securityManager = SecurityUtils.getSecurityManager();
acceptor = createAcceptor();
workerPool = createWorkerPool();
serverSocket = createServerSocket();
// set timeout to 2 min, to avoid blocking clients
serverSocket.setSoTimeout(2 * 60 * 1000);
accept();
int port = serverSocket.getLocalPort();
LOG.info("open hg hook server on port {}", port);
return port;
}
private void accept() {
acceptor.submit(() -> {
while (!serverSocket.isClosed()) {
try {
LOG.trace("wait for next hook connection");
Socket clientSocket = serverSocket.accept();
LOG.trace("accept incoming hook client from {}", clientSocket.getInetAddress());
HookHandler hookHandler = handlerFactory.create(clientSocket);
workerPool.submit(associateSecurityManager(hookHandler));
} catch (IOException ex) {
LOG.debug("failed to accept socket, possible closed", ex);
}
}
LOG.warn("ServerSocket is closed");
});
}
private Runnable associateSecurityManager(HookHandler hookHandler) {
return () -> {
ThreadContext.bind(securityManager);
try {
hookHandler.run();
} finally {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
};
}
@Nonnull
private ServerSocket createServerSocket() throws IOException {
return new ServerSocket(0, 0, InetAddress.getLoopbackAddress());
}
private ExecutorService createAcceptor() {
return Executors.newSingleThreadExecutor(
createThreadFactory("HgHookAcceptor")
);
}
private ExecutorService createWorkerPool() {
return Executors.newCachedThreadPool(
createThreadFactory("HgHookWorker-%d")
);
}
@Nonnull
private ThreadFactory createThreadFactory(String hgHookAcceptor) {
return new ThreadFactoryBuilder()
.setNameFormat(hgHookAcceptor)
.build();
}
@Override
public void close() {
closeSocket();
shutdown(acceptor);
shutdown(workerPool);
}
private void closeSocket() {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException ex) {
LOG.warn("failed to close server socket", ex);
}
}
}
private void shutdown(ExecutorService acceptor) {
if (acceptor != null) {
acceptor.shutdown();
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.hooks;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
class Sockets {
private static final Logger LOG = LoggerFactory.getLogger(Sockets.class);
private static final int READ_LIMIT = 8192;
private static final ObjectMapper objectMapper = new ObjectMapper();
private Sockets() {
}
static void send(OutputStream out, Object object) throws IOException {
byte[] bytes = objectMapper.writeValueAsBytes(object);
LOG.trace("send message length of {} to socket", bytes.length);
DataOutputStream dataOutputStream = new DataOutputStream(out);
dataOutputStream.writeInt(bytes.length);
LOG.trace("send message to socket");
dataOutputStream.write(bytes);
LOG.trace("flush socket");
out.flush();
}
static <T> T receive(InputStream in, Class<T> type) throws IOException {
LOG.trace("read {} from socket", type);
DataInputStream dataInputStream = new DataInputStream(in);
int length = dataInputStream.readInt();
LOG.trace("read message length of {} from socket", length);
if (length > READ_LIMIT) {
String message = String.format("received length of %d, which exceeds the limit of %d", length, READ_LIMIT);
throw new IOException(message);
}
byte[] data = new byte[length];
dataInputStream.readFully(data);
LOG.trace("convert message to {}", type);
return objectMapper.readValue(data, type);
}
}

View File

@@ -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;
}
}

View File

@@ -27,18 +27,12 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.aragost.javahg.Repository;
import com.google.common.base.Strings;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryProvider;
import sonia.scm.web.HgUtil;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
//~--- JDK imports ------------------------------------------------------------
@@ -46,105 +40,32 @@ import java.util.function.BiConsumer;
*
* @author Sebastian Sdorra
*/
public class HgCommandContext implements Closeable, RepositoryProvider
{
public class HgCommandContext implements Closeable, RepositoryProvider {
/** Field description */
private static final String PROPERTY_ENCODING = "hg.encoding";
private final HgRepositoryHandler handler;
private final HgRepositoryFactory factory;
private final sonia.scm.repository.Repository scmRepository;
//~--- constructors ---------------------------------------------------------
private Repository repository;
/**
* Constructs ...
*
*
* @param hookManager
* @param handler
* @param repository
* @param directory
*/
public HgCommandContext(HgHookManager hookManager,
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
File directory)
{
this(hookManager, handler, repository, directory,
handler.getHgContext().isPending());
}
/**
* Constructs ...
*
*
* @param hookManager
* @param handler
* @param repository
* @param directory
* @param pending
*/
public HgCommandContext(HgHookManager hookManager,
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
File directory, boolean pending)
{
this.hookManager = hookManager;
public HgCommandContext(HgRepositoryHandler handler, HgRepositoryFactory factory, sonia.scm.repository.Repository scmRepository) {
this.handler = handler;
this.directory = directory;
this.scmRepository = repository;
this.encoding = repository.getProperty(PROPERTY_ENCODING);
this.pending = pending;
if (Strings.isNullOrEmpty(encoding))
{
encoding = handler.getConfig().getEncoding();
}
this.factory = factory;
this.scmRepository = scmRepository;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @throws IOException
*/
@Override
public void close() throws IOException
{
if (repository != null)
{
repository.close();
public Repository open() {
if (repository == null) {
repository = factory.openForRead(scmRepository);
}
}
/**
* Method description
*
*
* @return
*/
public Repository open()
{
if (repository == null)
{
repository = HgUtil.open(handler, hookManager, directory, encoding, pending);
}
return repository;
}
public Repository openWithSpecialEnvironment(BiConsumer<sonia.scm.repository.Repository, Map<String, String>> prepareEnvironment)
{
return HgUtil.open(handler, directory, encoding,
pending, environment -> prepareEnvironment.accept(scmRepository, environment));
public Repository openForWrite() {
return factory.openForWrite(scmRepository);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public HgConfig getConfig()
{
return handler.getConfig();
@@ -159,25 +80,12 @@ public class HgCommandContext implements Closeable, RepositoryProvider
return getScmRepository();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private File directory;
/** Field description */
private String encoding;
/** Field description */
private HgRepositoryHandler handler;
/** Field description */
private HgHookManager hookManager;
/** Field description */
private boolean pending;
/** Field description */
private Repository repository;
private final sonia.scm.repository.Repository scmRepository;
@Override
public void close() {
if (repository != null) {
repository.close();
}
}
}

View File

@@ -24,82 +24,53 @@
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.aragost.javahg.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
import sonia.scm.web.HgUtil;
import java.io.File;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class HgHookChangesetProvider implements HookChangesetProvider
{
public class HgHookChangesetProvider implements HookChangesetProvider {
/**
* the logger for HgHookChangesetProvider
*/
private static final Logger logger =
LoggerFactory.getLogger(HgHookChangesetProvider.class);
private static final Logger LOG = LoggerFactory.getLogger(HgHookChangesetProvider.class);
//~--- constructors ---------------------------------------------------------
private final HgRepositoryHandler handler;
private final HgRepositoryFactory factory;
private final sonia.scm.repository.Repository scmRepository;
private final String startRev;
public HgHookChangesetProvider(HgRepositoryHandler handler,
File repositoryDirectory, HgHookManager hookManager, String startRev,
RepositoryHookType type)
{
private HookChangesetResponse response;
public HgHookChangesetProvider(HgRepositoryHandler handler, HgRepositoryFactory factory, sonia.scm.repository.Repository scmRepository, String startRev) {
this.handler = handler;
this.repositoryDirectory = repositoryDirectory;
this.hookManager = hookManager;
this.factory = factory;
this.scmRepository = scmRepository;
this.startRev = startRev;
this.type = type;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
*
* @return
*/
@Override
public synchronized HookChangesetResponse handleRequest(HookChangesetRequest request)
{
if (response == null)
{
public synchronized HookChangesetResponse handleRequest(HookChangesetRequest request) {
if (response == null) {
Repository repository = null;
try
{
repository = open();
try {
repository = factory.openForRead(scmRepository);
HgLogChangesetCommand cmd = HgLogChangesetCommand.on(repository,
handler.getConfig());
HgLogChangesetCommand cmd = HgLogChangesetCommand.on(repository, handler.getConfig());
response = new HookChangesetResponse(
cmd.rev(startRev.concat(":").concat(HgUtil.REVISION_TIP)).execute());
}
catch (Exception ex)
{
logger.error("could not retrieve changesets", ex);
}
finally
{
if (repository != null)
{
cmd.rev(startRev.concat(":").concat(HgUtil.REVISION_TIP)).execute()
);
} catch (Exception ex) {
LOG.error("could not retrieve changesets", ex);
} finally {
if (repository != null) {
repository.close();
}
}
@@ -108,39 +79,4 @@ public class HgHookChangesetProvider implements HookChangesetProvider
return response;
}
/**
* Method description
*
*
* @return
*/
private Repository open()
{
// use HG_PENDING only for pre receive hooks
boolean pending = type == RepositoryHookType.PRE_RECEIVE;
// TODO get repository encoding
return HgUtil.open(handler, hookManager, repositoryDirectory, null,
pending);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private HgRepositoryHandler handler;
/** Field description */
private HgHookManager hookManager;
/** Field description */
private File repositoryDirectory;
/** Field description */
private HookChangesetResponse response;
/** Field description */
private String startRev;
/** Field description */
private RepositoryHookType type;
}

View File

@@ -26,9 +26,9 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.HgHookBranchProvider;
import sonia.scm.repository.api.HgHookMessageProvider;
import sonia.scm.repository.api.HgHookTagProvider;
@@ -37,7 +37,6 @@ import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookMessageProvider;
import sonia.scm.repository.api.HookTagProvider;
import java.io.File;
import java.util.EnumSet;
import java.util.Set;
@@ -48,52 +47,37 @@ import java.util.Set;
*
* @author Sebastian Sdorra
*/
public class HgHookContextProvider extends HookContextProvider
{
public class HgHookContextProvider extends HookContextProvider {
private static final Set<HookFeature> SUPPORTED_FEATURES =
EnumSet.of(HookFeature.CHANGESET_PROVIDER, HookFeature.MESSAGE_PROVIDER,
HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER);
private static final Set<HookFeature> SUPPORTED_FEATURES = EnumSet.of(
HookFeature.CHANGESET_PROVIDER,
HookFeature.MESSAGE_PROVIDER,
HookFeature.BRANCH_PROVIDER,
HookFeature.TAG_PROVIDER
);
//~--- constructors ---------------------------------------------------------
private final HgHookChangesetProvider hookChangesetProvider;
private HgHookMessageProvider hgMessageProvider;
private HgHookBranchProvider hookBranchProvider;
private HgHookTagProvider hookTagProvider;
/**
* Constructs a new instance.
*
* @param handler mercurial repository handler
* @param repositoryDirectory the directory of the changed repository
* @param hookManager mercurial hook manager
* @param startRev start revision
* @param type type of hook
*/
public HgHookContextProvider(HgRepositoryHandler handler,
File repositoryDirectory, HgHookManager hookManager, String startRev,
RepositoryHookType type)
{
this.hookChangesetProvider = new HgHookChangesetProvider(handler, repositoryDirectory, hookManager, startRev, type);
public HgHookContextProvider(HgRepositoryHandler handler, HgRepositoryFactory factory, Repository repository, String startRev) {
this.hookChangesetProvider = new HgHookChangesetProvider(handler, factory, repository, startRev);
}
//~--- get methods ----------------------------------------------------------
@Override
public HookBranchProvider getBranchProvider()
{
if (hookBranchProvider == null)
{
public HookBranchProvider getBranchProvider() {
if (hookBranchProvider == null) {
hookBranchProvider = new HgHookBranchProvider(hookChangesetProvider);
}
return hookBranchProvider;
}
@Override
public HookTagProvider getTagProvider()
{
if (hookTagProvider == null)
{
public HookTagProvider getTagProvider() {
if (hookTagProvider == null) {
hookTagProvider = new HgHookTagProvider(hookChangesetProvider);
}
return hookTagProvider;
}
@@ -103,13 +87,10 @@ public class HgHookContextProvider extends HookContextProvider
return hookChangesetProvider;
}
public HgHookMessageProvider getHgMessageProvider()
{
if (hgMessageProvider == null)
{
public HgHookMessageProvider getHgMessageProvider() {
if (hgMessageProvider == null) {
hgMessageProvider = new HgHookMessageProvider();
}
return hgMessageProvider;
}
@@ -119,21 +100,9 @@ public class HgHookContextProvider extends HookContextProvider
return SUPPORTED_FEATURES;
}
//~--- methods --------------------------------------------------------------
@Override
protected HookMessageProvider createMessageProvider()
{
return getHgMessageProvider();
}
//~--- fields ---------------------------------------------------------------
private final HgHookChangesetProvider hookChangesetProvider;
private HgHookMessageProvider hgMessageProvider;
private HgHookBranchProvider hookBranchProvider;
private HgHookTagProvider hookTagProvider;
}

View File

@@ -31,6 +31,8 @@ import com.aragost.javahg.commands.ExecutionException;
import com.aragost.javahg.commands.PullCommand;
import com.aragost.javahg.commands.RemoveCommand;
import com.aragost.javahg.commands.StatusCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NoChangesMadeException;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.work.WorkingCopy;
@@ -41,11 +43,13 @@ import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
@SuppressWarnings("java:S3252") // it is ok for javahg classes to access static method of subtype
public class HgModifyCommand implements ModifyCommand {
private static final Logger LOG = LoggerFactory.getLogger(HgModifyCommand.class);
static final Pattern HG_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\](?: Error:)? (.*)");
private HgCommandContext context;
private final HgCommandContext context;
private final HgWorkingCopyFactory workingCopyFactory;
public HgModifyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) {
@@ -55,7 +59,6 @@ public class HgModifyCommand implements ModifyCommand {
@Override
public String execute(ModifyCommandRequest request) {
try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workingCopyFactory.createWorkingCopy(context, request.getBranch())) {
Repository workingRepository = workingCopy.getWorkingRepository();
request.getRequests().forEach(
@@ -100,12 +103,21 @@ public class HgModifyCommand implements ModifyCommand {
}
}
);
if (StatusCommand.on(workingRepository).lines().isEmpty()) {
throw new NoChangesMadeException(context.getScmRepository());
}
CommitCommand.on(workingRepository).user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail())).message(request.getCommitMessage()).execute();
LOG.trace("commit changes in working copy");
CommitCommand.on(workingRepository)
.user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail()))
.message(request.getCommitMessage()).execute();
List<Changeset> execute = pullModifyChangesToCentralRepository(request, workingCopy);
return execute.get(0).getNode();
String node = execute.get(0).getNode();
LOG.debug("successfully pulled changes from working copy, new node {}", node);
return node;
} catch (ExecutionException e) {
throwInternalRepositoryException("could not execute command on repository", e);
return null;
@@ -113,6 +125,7 @@ public class HgModifyCommand implements ModifyCommand {
}
private List<Changeset> pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy) {
LOG.trace("pull changes from working copy");
try {
com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
workingCopyFactory.configure(pullCommand);

View File

@@ -26,13 +26,12 @@ package sonia.scm.repository.spi;
import com.google.common.io.Closeables;
import sonia.scm.repository.Feature;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.CommandNotSupportedException;
import java.io.File;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Set;
@@ -41,11 +40,8 @@ import java.util.Set;
*
* @author Sebastian Sdorra
*/
public class HgRepositoryServiceProvider extends RepositoryServiceProvider
{
public class HgRepositoryServiceProvider extends RepositoryServiceProvider {
/** Field description */
//J-
public static final Set<Command> COMMANDS = EnumSet.of(
Command.BLAME,
Command.BROWSE,
@@ -61,25 +57,19 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
Command.PULL,
Command.MODIFY
);
//J+
/** Field description */
public static final Set<Feature> FEATURES =
EnumSet.of(Feature.COMBINED_DEFAULT_BRANCH);
public static final Set<Feature> FEATURES = EnumSet.of(Feature.COMBINED_DEFAULT_BRANCH);
//~--- constructors ---------------------------------------------------------
private final HgRepositoryHandler handler;
private final HgCommandContext context;
HgRepositoryServiceProvider(HgRepositoryHandler handler,
HgHookManager hookManager, Repository repository)
{
HgRepositoryServiceProvider(HgRepositoryHandler handler, HgRepositoryFactory factory, Repository repository) {
this.handler = handler;
this.repositoryDirectory = handler.getDirectory(repository.getId());
this.context = new HgCommandContext(hookManager, handler, repository,
repositoryDirectory);
this.context = new HgCommandContext(handler, factory, repository);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
@@ -91,9 +81,9 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
{
Closeables.close(context, true);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*

View File

@@ -26,7 +26,7 @@ package sonia.scm.repository.spi;
import com.google.inject.Inject;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
@@ -35,18 +35,15 @@ import sonia.scm.repository.Repository;
* @author Sebastian Sdorra
*/
@Extension
public class HgRepositoryServiceResolver implements RepositoryServiceResolver
{
public class HgRepositoryServiceResolver implements RepositoryServiceResolver {
private final HgRepositoryHandler handler;
private final HgHookManager hookManager;
private final HgRepositoryFactory factory;
@Inject
public HgRepositoryServiceResolver(HgRepositoryHandler handler,
HgHookManager hookManager)
{
public HgRepositoryServiceResolver(HgRepositoryHandler handler, HgRepositoryFactory factory) {
this.handler = handler;
this.hookManager = hookManager;
this.factory = factory;
}
@Override
@@ -54,7 +51,7 @@ public class HgRepositoryServiceResolver implements RepositoryServiceResolver
HgRepositoryServiceProvider provider = null;
if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
provider = new HgRepositoryServiceProvider(handler, hookManager, repository);
provider = new HgRepositoryServiceProvider(handler, factory, repository);
}
return provider;

View File

@@ -0,0 +1,120 @@
/*
* 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVersion;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class HgVersionCommand {
private static final Logger LOG = LoggerFactory.getLogger(HgVersionCommand.class);
@VisibleForTesting
static final String[] HG_ARGS = {
"version", "--template", "{ver}"
};
@VisibleForTesting
static final String[] PYTHON_ARGS = {
"-c", "import sys; print(sys.version)"
};
private final HgConfig config;
private final ProcessExecutor executor;
public HgVersionCommand(HgConfig config) {
this(config, command -> new ProcessBuilder(command).start());
}
HgVersionCommand(HgConfig config, ProcessExecutor executor) {
this.config = config;
this.executor = executor;
}
public HgVersion get() {
return new HgVersion(getHgVersion(), getPythonVersion());
}
@Nonnull
private String getPythonVersion() {
try {
String content = exec(config.getPythonBinary(), PYTHON_ARGS);
int index = content.indexOf(' ');
if (index > 0) {
return content.substring(0, index);
}
} catch (IOException ex) {
LOG.warn("failed to get python version", ex);
} catch (InterruptedException ex) {
LOG.warn("failed to get python version", ex);
Thread.currentThread().interrupt();
}
return HgVersion.UNKNOWN;
}
@Nonnull
private String getHgVersion() {
try {
return exec(config.getHgBinary(), HG_ARGS).trim();
} catch (IOException ex) {
LOG.warn("failed to get mercurial version", ex);
} catch (InterruptedException ex) {
LOG.warn("failed to get mercurial version", ex);
Thread.currentThread().interrupt();
}
return HgVersion.UNKNOWN;
}
@SuppressWarnings("UnstableApiUsage")
private String exec(String command, String[] args) throws IOException, InterruptedException {
List<String> cmd = new ArrayList<>();
cmd.add(command);
cmd.addAll(Arrays.asList(args));
Process process = executor.execute(cmd);
byte[] bytes = ByteStreams.toByteArray(process.getInputStream());
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("process ends with exit code " + exitCode);
}
return new String(bytes, StandardCharsets.UTF_8);
}
@FunctionalInterface
interface ProcessExecutor {
Process execute(List<String> command) throws IOException;
}
}

View File

@@ -36,27 +36,21 @@ import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.work.SimpleWorkingCopyFactory;
import sonia.scm.repository.work.WorkingCopyPool;
import sonia.scm.util.IOUtil;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import javax.inject.Inject;
import javax.inject.Provider;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Repository, Repository, HgCommandContext> implements HgWorkingCopyFactory {
private final Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder;
@Inject
public SimpleHgWorkingCopyFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, WorkingCopyPool workdirProvider) {
public SimpleHgWorkingCopyFactory(WorkingCopyPool workdirProvider) {
super(workdirProvider);
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
}
@Override
public ParentAndClone<Repository, Repository> initialize(HgCommandContext context, File target, String initialBranch) {
Repository centralRepository = openCentral(context);
Repository centralRepository = context.openForWrite();
CloneCommand cloneCommand = CloneCommandFlags.on(centralRepository);
if (initialBranch != null) {
cloneCommand.updaterev(initialBranch);
@@ -76,7 +70,7 @@ public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Reposit
// The hg api to create a command is meant to be used from the command classes, not from their "flags" base classes.
@SuppressWarnings("java:S3252")
protected ParentAndClone<Repository, Repository> reclaim(HgCommandContext context, File target, String initialBranch) throws ReclaimFailedException {
Repository centralRepository = openCentral(context);
Repository centralRepository = context.openForWrite();
try {
BaseRepository clone = Repository.open(target);
for (String unknown : StatusCommand.on(clone).execute().getUnknown()) {
@@ -89,12 +83,6 @@ public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Reposit
}
}
public Repository openCentral(HgCommandContext context) {
BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer =
(repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment);
return context.openWithSpecialEnvironment(repositoryMapBiConsumer);
}
private void delete(File directory, String unknownFile) throws IOException {
IOUtil.delete(new File(directory, unknownFile));
}
@@ -111,7 +99,7 @@ public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Reposit
@Override
public void configure(PullCommand pullCommand) {
pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.postHook");
pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.preHook");
pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.post_hook");
pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.pre_hook");
}
}

View File

@@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgEnvironmentBuilder;
import sonia.scm.repository.HgPythonScript;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
@@ -42,29 +43,21 @@ import sonia.scm.web.cgi.CGIExecutor;
import sonia.scm.web.cgi.CGIExecutorFactory;
import sonia.scm.web.cgi.EnvList;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
{
/** Field description */
public static final String ENV_SESSION_PREFIX = "SCM_";
public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet {
/** Field description */
private static final long serialVersionUID = -3492811300905099810L;
@@ -80,13 +73,13 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
ScmConfiguration configuration,
HgRepositoryHandler handler,
RepositoryRequestListenerUtil requestListenerUtil,
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder)
HgEnvironmentBuilder environmentBuilder)
{
this.cgiExecutorFactory = cgiExecutorFactory;
this.configuration = configuration;
this.handler = handler;
this.requestListenerUtil = requestListenerUtil;
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
this.environmentBuilder = environmentBuilder;
this.exceptionHandler = new HgCGIExceptionHandler();
this.command = HgPythonScript.HGWEB.getFile(SCMContext.getContext());
}
@@ -108,11 +101,7 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
{
handleRequest(request, response, repository);
}
catch (ServletException ex)
{
exceptionHandler.handleException(request, response, ex);
}
catch (IOException ex)
catch (ServletException | IOException ex)
{
exceptionHandler.handleException(request, response, ex);
}
@@ -146,29 +135,6 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
}
}
/**
* Method description
*
*
* @param env
* @param session
*/
@SuppressWarnings("unchecked")
private void passSessionAttributes(EnvList env, HttpSession session)
{
Enumeration<String> enm = session.getAttributeNames();
while (enm.hasMoreElements())
{
String key = enm.nextElement();
if (key.startsWith(ENV_SESSION_PREFIX))
{
env.set(key, session.getAttribute(key).toString());
}
}
}
/**
* Method description
*
@@ -192,7 +158,9 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
executor.setExceptionHandler(exceptionHandler);
executor.setStatusCodeHandler(exceptionHandler);
executor.setContentLengthWorkaround(true);
hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment().asMutableMap());
EnvList env = executor.getEnvironment();
environmentBuilder.write(repository).forEach(env::set);
String interpreter = getInterpreter();
@@ -248,5 +216,5 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
/** Field description */
private final RepositoryRequestListenerUtil requestListenerUtil;
private final HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder;
private final HgEnvironmentBuilder environmentBuilder;
}

View File

@@ -1,446 +0,0 @@
/*
* 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.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.io.Closeables;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.api.HgHookMessage.Severity;
import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class HgHookCallbackServlet extends HttpServlet
{
/** Field description */
public static final String HGHOOK_POST_RECEIVE = "changegroup";
/** Field description */
public static final String HGHOOK_PRE_RECEIVE = "pretxnchangegroup";
/** Field description */
public static final String PARAM_REPOSITORYID = "repositoryId";
/** Field description */
private static final String PARAM_CHALLENGE = "challenge";
/** Field description */
private static final String PARAM_TOKEN = "token";
/** Field description */
private static final String PARAM_NODE = "node";
/** Field description */
private static final String PARAM_PING = "ping";
/** Field description */
private static final Pattern REGEX_URL =
Pattern.compile("^/hook/hg/([^/]+)$");
/** the logger for HgHookCallbackServlet */
private static final Logger logger =
LoggerFactory.getLogger(HgHookCallbackServlet.class);
/** Field description */
private static final long serialVersionUID = 3531596724828189353L;
//~--- constructors ---------------------------------------------------------
@Inject
public HgHookCallbackServlet(HookEventFacade hookEventFacade,
HgRepositoryHandler handler, HgHookManager hookManager,
Provider<HgContext> contextProvider)
{
this.hookEventFacade = hookEventFacade;
this.handler = handler;
this.hookManager = hookManager;
this.contextProvider = contextProvider;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param response
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
{
String ping = request.getParameter(PARAM_PING);
if (Util.isNotEmpty(ping) && Boolean.parseBoolean(ping))
{
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
else
{
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
handlePostRequest(request, response);
} catch (IOException ex) {
logger.warn("error in hook callback execution, sending internal server error", ex);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
private void handlePostRequest(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String strippedURI = HttpUtil.getStrippedURI(request);
Matcher m = REGEX_URL.matcher(strippedURI);
if (m.matches())
{
String repositoryId = getRepositoryId(request);
String type = m.group(1);
String challenge = request.getParameter(PARAM_CHALLENGE);
if (Util.isNotEmpty(challenge))
{
String node = request.getParameter(PARAM_NODE);
if (Util.isNotEmpty(node))
{
String token = request.getParameter(PARAM_TOKEN);
if (Util.isNotEmpty(token))
{
authenticate(token);
}
hookCallback(response, type, repositoryId, challenge, node);
}
else if (logger.isDebugEnabled())
{
logger.debug("node parameter not found");
}
}
else if (logger.isDebugEnabled())
{
logger.debug("challenge parameter not found");
}
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("url does not match");
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
private void authenticate(String token)
{
try
{
token = CipherUtil.getInstance().decode(token);
if (Util.isNotEmpty(token))
{
Subject subject = SecurityUtils.getSubject();
AuthenticationToken accessToken = createToken(token);
//J-
subject.login(accessToken);
}
}
catch (Exception ex)
{
logger.error("could not authenticate user", ex);
}
}
private AuthenticationToken createToken(String tokenString)
{
return BearerToken.valueOf(tokenString);
}
private void fireHook(HttpServletResponse response, String repositoryId, String node, RepositoryHookType type)
throws IOException
{
HgHookContextProvider context = null;
try
{
if (type == RepositoryHookType.PRE_RECEIVE)
{
contextProvider.get().setPending(true);
}
File repositoryDirectory = handler.getDirectory(repositoryId);
context = new HgHookContextProvider(handler, repositoryDirectory, hookManager,
node, type);
hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
printMessages(response, context);
}
catch (NotFoundException ex)
{
logger.error(ex.getMessage());
logger.trace("repository not found", ex);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
catch (Exception ex)
{
sendError(response, context, ex);
}
}
private void hookCallback(HttpServletResponse response, String typeName, String repositoryId, String challenge, String node) throws IOException {
if (hookManager.isAcceptAble(challenge))
{
RepositoryHookType type = null;
if (HGHOOK_PRE_RECEIVE.equals(typeName))
{
type = RepositoryHookType.PRE_RECEIVE;
}
else if (HGHOOK_POST_RECEIVE.equals(typeName))
{
type = RepositoryHookType.POST_RECEIVE;
}
if (type != null)
{
fireHook(response, repositoryId, node, type);
}
else
{
if (logger.isWarnEnabled())
{
logger.warn("unknown hook type {}", typeName);
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
else
{
if (logger.isWarnEnabled())
{
logger.warn("hg hook challenge is not accept able");
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
/**
* Method description
*
*
* @param writer
* @param msg
*/
private void printMessage(PrintWriter writer, HgHookMessage msg)
{
writer.append('_');
if (msg.getSeverity() == Severity.ERROR)
{
writer.append("e[SCM] Error: ");
}
else
{
writer.append("n[SCM] ");
}
writer.println(msg.getMessage());
}
/**
* Method description
*
*
* @param response
* @param context
*
* @throws IOException
*/
private void printMessages(HttpServletResponse response,
HgHookContextProvider context)
throws IOException
{
List<HgHookMessage> msgs = context.getHgMessageProvider().getMessages();
if (Util.isNotEmpty(msgs))
{
PrintWriter writer = null;
try
{
writer = response.getWriter();
printMessages(writer, msgs);
}
finally
{
Closeables.close(writer, false);
}
}
}
/**
* Method description
*
*
* @param writer
* @param msgs
*/
private void printMessages(PrintWriter writer, List<HgHookMessage> msgs)
{
for (HgHookMessage msg : msgs)
{
printMessage(writer, msg);
}
}
/**
* Method description
*
*
* @param response
* @param context
* @param ex
*
* @throws IOException
*/
private void sendError(HttpServletResponse response,
HgHookContextProvider context, Exception ex)
throws IOException
{
logger.warn("hook ended with exception", ex);
response.setStatus(HttpServletResponse.SC_CONFLICT);
String msg = ex.getMessage();
List<HgHookMessage> msgs = null;
if (context != null)
{
msgs = context.getHgMessageProvider().getMessages();
}
if (!Strings.isNullOrEmpty(msg) || Util.isNotEmpty(msgs))
{
PrintWriter writer = null;
try
{
writer = response.getWriter();
if (Util.isNotEmpty(msgs))
{
printMessages(writer, msgs);
}
if (!Strings.isNullOrEmpty(msg))
{
printMessage(writer, new HgHookMessage(Severity.ERROR, msg));
}
}
finally
{
Closeables.close(writer, true);
}
}
}
//~--- get methods ----------------------------------------------------------
private String getRepositoryId(HttpServletRequest request)
{
String id = request.getParameter(PARAM_REPOSITORYID);
Preconditions.checkArgument(!Strings.isNullOrEmpty(id), "repository id not found in request");
return id;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final Provider<HgContext> contextProvider;
/** Field description */
private final HgRepositoryHandler handler;
/** Field description */
private final HookEventFacade hookEventFacade;
/** Field description */
private final HgHookManager hookManager;
}

View File

@@ -1,80 +0,0 @@
/*
* 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.web;
import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.Map;
public class HgRepositoryEnvironmentBuilder {
private static final String ENV_REPOSITORY_NAME = "REPO_NAME";
private static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
private final HgRepositoryHandler handler;
private final HgHookManager hookManager;
@Inject
public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager) {
this.handler = handler;
this.hookManager = hookManager;
}
public void buildFor(Repository repository, HttpServletRequest request, Map<String, String> environment) {
File directory = handler.getDirectory(repository.getId());
environment.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
environment.put(ENV_REPOSITORY_ID, repository.getId());
environment.put(ENV_REPOSITORY_PATH,
directory.getAbsolutePath());
// add hook environment
if (handler.getConfig().isDisableHookSSLValidation()) {
// disable ssl validation
// Issue 959: https://goo.gl/zH5eY8
environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
}
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
HgEnvironment.prepareEnvironment(
environment,
handler,
hookManager,
request
);
}
}

View File

@@ -34,9 +34,6 @@ import sonia.scm.api.v2.resources.HgConfigPackagesToDtoMapper;
import sonia.scm.api.v2.resources.HgConfigToHgConfigDtoMapper;
import sonia.scm.installer.HgPackageReader;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgContextProvider;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.spi.HgWorkingCopyFactory;
import sonia.scm.repository.spi.SimpleHgWorkingCopyFactory;
@@ -45,26 +42,10 @@ import sonia.scm.repository.spi.SimpleHgWorkingCopyFactory;
* @author Sebastian Sdorra
*/
@Extension
public class HgServletModule extends ServletModule
{
public class HgServletModule extends ServletModule {
/** Field description */
public static final String MAPPING_HG = "/hg/*";
/** Field description */
public static final String MAPPING_HOOK = "/hook/hg/*";
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*/
@Override
protected void configureServlets()
{
bind(HgContext.class).toProvider(HgContextProvider.class);
bind(HgHookManager.class);
protected void configureServlets() {
bind(HgPackageReader.class);
bind(HgConfigDtoToHgConfigMapper.class).to(Mappers.getMapper(HgConfigDtoToHgConfigMapper.class).getClass());
@@ -72,9 +53,6 @@ public class HgServletModule extends ServletModule
bind(HgConfigPackagesToDtoMapper.class).to(Mappers.getMapper(HgConfigPackagesToDtoMapper.class).getClass());
bind(HgConfigInstallationsToDtoMapper.class);
// bind servlets
serve(MAPPING_HOOK).with(HgHookCallbackServlet.class);
bind(HgWorkingCopyFactory.class).to(SimpleHgWorkingCopyFactory.class);
}
}

View File

@@ -24,186 +24,45 @@
package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.aragost.javahg.Repository;
import com.aragost.javahg.RepositoryConfiguration;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgPythonScript;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.spi.javahg.HgFileviewExtension;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
/**
*
* @author Sebastian Sdorra
*/
public final class HgUtil
{
public final class HgUtil {
/** Field description */
public static final String REVISION_TIP = "tip";
/** Field description */
private static final String USERAGENT_HG = "mercurial/";
/**
* the logger for HgUtil
*/
private static final Logger logger = LoggerFactory.getLogger(HgUtil.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private HgUtil() {}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param handler
* @param hookManager
* @param directory
* @param encoding
* @param pending
*
* @return
*/
public static Repository open(HgRepositoryHandler handler,
HgHookManager hookManager, File directory, String encoding, boolean pending)
{
return open(
handler,
directory,
encoding,
pending,
environment -> HgEnvironment.prepareEnvironment(environment, handler, hookManager)
);
}
public static Repository open(HgRepositoryHandler handler,
File directory, String encoding, boolean pending,
Consumer<Map<String, String>> prepareEnvironment)
{
String enc = encoding;
if (Strings.isNullOrEmpty(enc))
{
enc = handler.getConfig().getEncoding();
}
RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT;
prepareEnvironment.accept(repoConfiguration.getEnvironment());
repoConfiguration.addExtension(HgFileviewExtension.class);
repoConfiguration.setEnablePendingChangesets(pending);
try
{
Charset charset = Charset.forName(enc);
logger.trace("set encoding {} for mercurial", enc);
repoConfiguration.setEncoding(charset);
}
catch (IllegalArgumentException ex)
{
logger.error("could not set encoding for mercurial", ex);
}
repoConfiguration.setHgBin(handler.getConfig().getHgBinary());
logger.debug("open hg repository {}: encoding: {}, pending: {}", directory, enc, pending);
return Repository.open(repoConfiguration, directory);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param config
*
* @return
*/
public static String getPythonPath(HgConfig config)
{
public static String getPythonPath(HgConfig config) {
String pythonPath = Util.EMPTY_STRING;
if (config != null)
{
if (config != null) {
pythonPath = Util.nonNull(config.getPythonPath());
}
if (Util.isNotEmpty(pythonPath))
{
if (Util.isNotEmpty(pythonPath)) {
pythonPath = pythonPath.concat(File.pathSeparator);
}
//J-
pythonPath = pythonPath.concat(
HgPythonScript.getScriptDirectory(
SCMContext.getContext()
).getAbsolutePath()
);
//J+
return pythonPath;
}
/**
* Method description
*
*
* @param revision
*
* @return
*/
public static String getRevision(String revision)
{
return Util.isEmpty(revision)
? REVISION_TIP
: revision;
public static String getRevision(String revision) {
return Util.isEmpty(revision) ? REVISION_TIP : revision;
}
/**
* Returns true if the request comes from a mercurial client.
*
*
* @param request servlet request
*
* @return true if the client is mercurial
*/
public static boolean isHgClient(HttpServletRequest request)
{
return HttpUtil.userAgentStartsWith(request, USERAGENT_HG);
}
}

View File

@@ -33,7 +33,6 @@ type Configuration = {
encoding: string;
useOptimizedBytecode: boolean;
showRevisionInId: boolean;
disableHookSSLValidation: boolean;
enableHttpPostArgs: boolean;
_links: Links;
};
@@ -139,7 +138,6 @@ class HgConfigurationForm extends React.Component<Props, State> {
{this.checkbox("showRevisionInId")}
</div>
<div className="column is-half">
{this.checkbox("disableHookSSLValidation")}
{this.checkbox("enableHttpPostArgs")}
</div>
</div>

View File

@@ -25,8 +25,6 @@
"showRevisionInIdHelpText": "Die Revision als Teil der Node ID anzeigen.",
"enableHttpPostArgs": "HttpPostArgs Protocol aktivieren",
"enableHttpPostArgsHelpText": "Aktiviert das experimentelle HttpPostArgs Protokoll von Mercurial. Das HttpPostArgs Protokoll verwendet den Post Request Body anstatt des HTTP Headers um Meta Informationen zu versenden. Dieses Vorgehen reduziert die Header Größe der Mercurial Requests. HttpPostArgs wird seit Mercurial 3.8 unterstützt.",
"disableHookSSLValidation": "SSL Validierung für Hooks deaktivieren",
"disableHookSSLValidationHelpText": "Deaktiviert die Validierung von SSL Zertifikaten für den Mercurial Hook, der die Repositoryänderungen wieder zurück an den SCM-Manager leitet. Diese Option sollte nur benutzt werden, wenn der SCM-Manager ein selbstsigniertes Zertifikat verwendet.",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin.",
"required": "Dieser Konfigurationswert wird benötigt"

View File

@@ -25,8 +25,6 @@
"showRevisionInIdHelpText": "Show revision as part of the node id.",
"enableHttpPostArgs": "Enable HttpPostArgs Protocol",
"enableHttpPostArgsHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.",
"disableHookSSLValidation": "Disable SSL Validation on Hooks",
"disableHookSSLValidationHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.",
"disabled": "Disabled",
"disabledHelpText": "Enable or disable the Mercurial plugin.",
"required": "This configuration value is required"

View File

@@ -39,8 +39,8 @@ u.setconfig(b'web', b'push_ssl', b'false')
u.setconfig(b'web', b'allow_read', b'*')
u.setconfig(b'web', b'allow_push', b'*')
u.setconfig(b'hooks', b'changegroup.scm', b'python:scmhooks.postHook')
u.setconfig(b'hooks', b'pretxnchangegroup.scm', b'python:scmhooks.preHook')
u.setconfig(b'hooks', b'changegroup.scm', b'python:scmhooks.post_hook')
u.setconfig(b'hooks', b'pretxnchangegroup.scm', b'python:scmhooks.pre_hook')
# pass SCM_HTTP_POST_ARGS to enable experimental httppostargs protocol of mercurial
# SCM_HTTP_POST_ARGS is set by HgCGIServlet

Some files were not shown because too many files have changed in this diff Show More