diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a64a7ebc..3f9ae53037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/de/user/profile/assets/api-key-overview.png b/docs/de/user/profile/assets/api-key-overview.png index d150b8fb1b..d1fbe8f606 100644 Binary files a/docs/de/user/profile/assets/api-key-overview.png and b/docs/de/user/profile/assets/api-key-overview.png differ diff --git a/docs/de/user/profile/index.md b/docs/de/user/profile/index.md index 6e399bfa13..29018292d1 100644 --- a/docs/de/user/profile/index.md +++ b/docs/de/user/profile/index.md @@ -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 diff --git a/docs/de/user/repo/assets/repository-branch-detailView.png b/docs/de/user/repo/assets/repository-branch-detailView.png index d671846fef..f9727d9d4f 100644 Binary files a/docs/de/user/repo/assets/repository-branch-detailView.png and b/docs/de/user/repo/assets/repository-branch-detailView.png differ diff --git a/docs/de/user/repo/assets/repository-branches-overview.png b/docs/de/user/repo/assets/repository-branches-overview.png index 39dcf5e424..c839e6b57c 100644 Binary files a/docs/de/user/repo/assets/repository-branches-overview.png and b/docs/de/user/repo/assets/repository-branches-overview.png differ diff --git a/docs/de/user/repo/assets/repository-overview.png b/docs/de/user/repo/assets/repository-overview.png index 59cf55e4d2..2454f625a3 100644 Binary files a/docs/de/user/repo/assets/repository-overview.png and b/docs/de/user/repo/assets/repository-overview.png differ diff --git a/docs/de/user/repo/branches.md b/docs/de/user/repo/branches.md index 17afe92924..47ac022067 100644 --- a/docs/de/user/repo/branches.md +++ b/docs/de/user/repo/branches.md @@ -3,9 +3,11 @@ title: Repository subtitle: Branches --- ### Übersicht -Auf der Branches-Übersicht sind die bereits existierenden Branches aufgeführt. Bei einem Klick auf einen Branch wird man zur Detailseite des Branches weitergeleitet. +Auf der Branches-Übersicht sind die bereits existierenden Branches aufgeführt. Bei einem Klick auf einen Branch wird man zur Detailseite des Branches weitergeleitet. +Die Branches sind in zwei Listen aufgeteilt: Unter "Aktive Branches" sind Branches aufgelistet, deren letzter Commit +nicht 30 Tage älter als der Stand des Default-Branches ist. Alle älteren Branches sind in der Liste "Stale Branches" zu finden. -Der Tag "Default" gibt an welcher Branch aktuell, als Standard-Branch dieses Repository im SCM-Manager markiert ist. Der Standard-Branch wird immer zuerst angezeigt, wenn man das Repository im SCM-Manager öffnet. +Der Tag "Default" gibt an, welcher Branch aktuell als Standard-Branch dieses Repository im SCM-Manager markiert ist. Der Standard-Branch wird immer zuerst angezeigt, wenn man das Repository im SCM-Manager öffnet. Alle Branches mit Ausnahme des Default Branches können über den Mülleimer-Icon unwiderruflich gelöscht werden. Über den "Branch erstellen"-Button gelangt man zum Formular, um neue Branches anzulegen. diff --git a/docs/de/user/user/assets/user-information.png b/docs/de/user/user/assets/user-information.png index f5573c10e8..b79a4076c7 100644 Binary files a/docs/de/user/user/assets/user-information.png and b/docs/de/user/user/assets/user-information.png differ diff --git a/docs/de/user/user/assets/user-settings-general.png b/docs/de/user/user/assets/user-settings-general.png index 9fe75a3a89..9f63014b71 100644 Binary files a/docs/de/user/user/assets/user-settings-general.png and b/docs/de/user/user/assets/user-settings-general.png differ diff --git a/docs/de/user/user/assets/user-settings-publickeys.png b/docs/de/user/user/assets/user-settings-publickeys.png index 9761a3b044..b40d52e2a8 100644 Binary files a/docs/de/user/user/assets/user-settings-publickeys.png and b/docs/de/user/user/assets/user-settings-publickeys.png differ diff --git a/docs/en/user/profile/assets/api-key-overview.png b/docs/en/user/profile/assets/api-key-overview.png index d150b8fb1b..d052188103 100644 Binary files a/docs/en/user/profile/assets/api-key-overview.png and b/docs/en/user/profile/assets/api-key-overview.png differ diff --git a/docs/en/user/profile/index.md b/docs/en/user/profile/index.md index 50746c2f56..5895b89700 100644 --- a/docs/en/user/profile/index.md +++ b/docs/en/user/profile/index.md @@ -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 diff --git a/docs/en/user/repo/assets/repository-branch-detailView.png b/docs/en/user/repo/assets/repository-branch-detailView.png index 615ba696c1..28f768a6be 100644 Binary files a/docs/en/user/repo/assets/repository-branch-detailView.png and b/docs/en/user/repo/assets/repository-branch-detailView.png differ diff --git a/docs/en/user/repo/assets/repository-branches-overview.png b/docs/en/user/repo/assets/repository-branches-overview.png index e63ebab775..08a2ab19df 100644 Binary files a/docs/en/user/repo/assets/repository-branches-overview.png and b/docs/en/user/repo/assets/repository-branches-overview.png differ diff --git a/docs/en/user/repo/assets/repository-overview.png b/docs/en/user/repo/assets/repository-overview.png index 59cf55e4d2..7e6bfcbab7 100644 Binary files a/docs/en/user/repo/assets/repository-overview.png and b/docs/en/user/repo/assets/repository-overview.png differ diff --git a/docs/en/user/repo/branches.md b/docs/en/user/repo/branches.md index 370165710b..7a9d318786 100644 --- a/docs/en/user/repo/branches.md +++ b/docs/en/user/repo/branches.md @@ -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. diff --git a/docs/en/user/user/assets/user-information.png b/docs/en/user/user/assets/user-information.png index f5573c10e8..666f05f7f8 100644 Binary files a/docs/en/user/user/assets/user-information.png and b/docs/en/user/user/assets/user-information.png differ diff --git a/docs/en/user/user/assets/user-settings-general.png b/docs/en/user/user/assets/user-settings-general.png index 4312fa0e8e..10d3688e47 100644 Binary files a/docs/en/user/user/assets/user-settings-general.png and b/docs/en/user/user/assets/user-settings-general.png differ diff --git a/docs/en/user/user/assets/user-settings-publickeys.png b/docs/en/user/user/assets/user-settings-publickeys.png index b2eae78e8b..286f46d8f2 100644 Binary files a/docs/en/user/user/assets/user-settings-publickeys.png and b/docs/en/user/user/assets/user-settings-publickeys.png differ diff --git a/lerna.json b/lerna.json index 210af935bc..a4385558c7 100644 --- a/lerna.json +++ b/lerna.json @@ -5,5 +5,5 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "2.10.0-SNAPSHOT" + "version": "2.11.0-SNAPSHOT" } diff --git a/pom.xml b/pom.xml index b8192de3a8..534bbceff0 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ sonia.scm scm pom - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT The easiest way to share your Git, Mercurial and Subversion repositories. @@ -464,7 +464,7 @@ org.assertj assertj-core - 3.17.2 + 3.18.0 test @@ -506,7 +506,7 @@ - jakarta.xml.bind + jakarta.xml.bind jakarta.xml.bind-api ${jaxb.version} @@ -580,7 +580,7 @@ sonia.scm.maven smp-maven-plugin - 1.3.0 + 1.4.0 @@ -903,7 +903,7 @@ - 3.5.15 + 3.6.0 2.1 5.7.0 @@ -919,13 +919,13 @@ 4.2.3 2.3.3 6.1.6.Final - 1.66 + 1.67 1.6.2 - 9.4.34.v20201102 + 9.4.35.v20201120 9.4.34.v20201102 @@ -937,7 +937,7 @@ 1.10.1-scm2 - 26.0-jre + 30.0-jre 12.16.1 diff --git a/scm-annotation-processor/pom.xml b/scm-annotation-processor/pom.xml index 62087852cd..87b63417d9 100644 --- a/scm-annotation-processor/pom.xml +++ b/scm-annotation-processor/pom.xml @@ -31,12 +31,12 @@ sonia.scm scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm scm-annotation-processor - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-annotation-processor @@ -46,7 +46,7 @@ sonia.scm scm-annotations - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT diff --git a/scm-annotations/pom.xml b/scm-annotations/pom.xml index 6e70ada1ab..f83dac689c 100644 --- a/scm-annotations/pom.xml +++ b/scm-annotations/pom.xml @@ -31,11 +31,11 @@ sonia.scm scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-annotations - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-annotations diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 85a539c471..e54ff52f01 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -31,11 +31,11 @@ scm sonia.scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-core - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-core @@ -54,7 +54,7 @@ sonia.scm scm-annotations - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT @@ -227,7 +227,7 @@ sonia.scm scm-annotation-processor - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT provided @@ -250,6 +250,12 @@ test + + ch.qos.logback + logback-classic + test + + diff --git a/scm-core/src/main/java/sonia/scm/TransactionId.java b/scm-core/src/main/java/sonia/scm/TransactionId.java new file mode 100644 index 0000000000..68169ba7d4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/TransactionId.java @@ -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 get() { + return Optional.ofNullable(MDC.get(KEY)); + } + + /** + * Removes a bound transaction id from the current thread. + */ + public static void clear() { + MDC.remove(KEY); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/Branch.java b/scm-core/src/main/java/sonia/scm/repository/Branch.java index bdcbc66a82..acfdd6a884 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Branch.java +++ b/scm-core/src/main/java/sonia/scm/repository/Branch.java @@ -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 null */ - Branch(String name, String revision, boolean defaultBranch) - { + Branch(String name, String revision, boolean defaultBranch, Long lastCommitDate) { this.name = name; this.revision = revision; this.defaultBranch = defaultBranch; + this.lastCommitDate = lastCommitDate; } + /** + * @deprecated Use {@link #normalBranch(String, String, Long)} instead to set the date of the last commit, too. + */ + @Deprecated public static Branch normalBranch(String name, String revision) { - return new Branch(name, revision, false); + return normalBranch(name, revision, null); } + public static Branch normalBranch(String name, String revision, Long lastCommitDate) { + return new Branch(name, revision, false, lastCommitDate); + } + + /** + * @deprecated Use {@link #defaultBranch(String, String, Long)} instead to set the date of the last commit, too. + */ + @Deprecated public static Branch defaultBranch(String name, String revision) { - return new Branch(name, revision, true); + return defaultBranch(name, revision, null); } - //~--- methods -------------------------------------------------------------- + public static Branch defaultBranch(String name, String revision, Long lastCommitDate) { + return new Branch(name, revision, true, lastCommitDate); + } + + public void setStale(boolean stale) { + this.stale = stale; + } @Override public boolean isValid() { return VALID_BRANCH_NAME_PATTERN.matcher(name).matches(); } - /** - * {@inheritDoc} - * - * - * @param obj - * - * @return - */ @Override - public boolean equals(Object obj) - { - if (obj == null) - { + public boolean equals(Object obj) { + if (obj == null) { return false; } - if (getClass() != obj.getClass()) - { + if (getClass() != obj.getClass()) { return false; } @@ -120,48 +144,31 @@ public final class Branch implements Serializable, Validateable return Objects.equal(name, other.name) && Objects.equal(revision, other.revision) - && Objects.equal(defaultBranch, other.defaultBranch); + && Objects.equal(defaultBranch, other.defaultBranch) + && Objects.equal(lastCommitDate, other.lastCommitDate); } - /** - * {@inheritDoc} - * - * - * @return - */ @Override - public int hashCode() - { + public int hashCode() { return Objects.hashCode(name, revision); } - /** - * {@inheritDoc} - * - * - * @return - */ @Override - public String toString() - { - //J- + public String toString() { return MoreObjects.toStringHelper(this) .add("name", name) .add("revision", revision) + .add("defaultBranch", defaultBranch) + .add("lastCommitDate", lastCommitDate) .toString(); - //J+ } - //~--- get methods ---------------------------------------------------------- - /** * Returns the name of the branch * - * * @return name of the branch */ - public String getName() - { + public String getName() { return name; } @@ -170,22 +177,27 @@ public final class Branch implements Serializable, Validateable * * @return latest revision of branch */ - public String getRevision() - { + public String getRevision() { return revision; } + /** + * Flag whether this branch is configured as the default branch. + */ public boolean isDefaultBranch() { return defaultBranch; } - //~--- fields --------------------------------------------------------------- + /** + * The date of the commit this branch points to, if this was computed (can be empty). + * + * @since 2.11.0 + */ + public Optional getLastCommitDate() { + return Optional.ofNullable(lastCommitDate); + } - /** name of the branch */ - private String name; - - /** Field description */ - private String revision; - - private boolean defaultBranch; + public boolean isStale() { + return stale; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputer.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputer.java new file mode 100644 index 0000000000..15de6663ec --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputer.java @@ -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(); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java index 5ba45b00a0..af784f287b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; import com.google.common.base.Objects; @@ -165,7 +165,7 @@ public final class BranchesCommandBuilder private Branches getBranchesFromCommand() throws IOException { - return new Branches(branchesCommand.getBranches()); + return new Branches(branchesCommand.getBranchesWithStaleFlags(new BranchXDaysOlderThanDefaultStaleComputer())); } //~--- inner classes -------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BranchStaleComputer.java b/scm-core/src/main/java/sonia/scm/repository/spi/BranchStaleComputer.java new file mode 100644 index 0000000000..c6324fa1cf --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BranchStaleComputer.java @@ -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; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java index ae1468e768..9fa28a49aa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java @@ -21,33 +21,52 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- +package sonia.scm.repository.spi; import sonia.scm.repository.Branch; import java.io.IOException; import java.util.List; - -//~--- JDK imports ------------------------------------------------------------ +import java.util.Optional; /** - * * @author Sebastian Sdorra * @since 1.18 */ -public interface BranchesCommand -{ +public interface BranchesCommand { - /** - * Method description - * - * - * @return - * - * @throws IOException - */ List getBranches() throws IOException; + + default List getBranchesWithStaleFlags(BranchStaleComputer computer) throws IOException { + List branches = getBranches(); + new StaleProcessor(computer, branches).process(); + return branches; + } + + final class StaleProcessor { + + private final BranchStaleComputer computer; + private final List branches; + + private StaleProcessor(BranchStaleComputer computer, List branches) { + this.computer = computer; + this.branches = branches; + } + + private void process() { + Optional 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))); + } + } } diff --git a/scm-core/src/test/java/sonia/scm/TransactionIdTest.java b/scm-core/src/test/java/sonia/scm/TransactionIdTest.java new file mode 100644 index 0000000000..053f2ce86c --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/TransactionIdTest.java @@ -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(); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputerTest.java b/scm-core/src/test/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputerTest.java new file mode 100644 index 0000000000..12fb269162 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputerTest.java @@ -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; + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/BranchesCommandTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/BranchesCommandTest.java new file mode 100644 index 0000000000..4a0f9326c0 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/BranchesCommandTest.java @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.repository.spi; + +import org.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 branches = asList( + Branch.normalBranch("arthur", "42", staleTime), + Branch.normalBranch("marvin", "42", staleTime), + Branch.defaultBranch("hog", "42", now.toEpochMilli()), + Branch.normalBranch("trillian", "42", activeTime) + ); + + List branchesWithStaleFlags = new BranchesCommand() { + @Override + public List getBranches() { + return branches; + } + }.getBranchesWithStaleFlags(new BranchXDaysOlderThanDefaultStaleComputer()); + + Assertions.assertThat(branchesWithStaleFlags) + .extracting("stale") + .containsExactly(true, true, false, false); + } +} diff --git a/scm-dao-xml/pom.xml b/scm-dao-xml/pom.xml index 0f43eafa51..e9001f190e 100644 --- a/scm-dao-xml/pom.xml +++ b/scm-dao-xml/pom.xml @@ -31,11 +31,11 @@ sonia.scm scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-dao-xml - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-dao-xml @@ -50,7 +50,7 @@ sonia.scm scm-core - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT @@ -58,7 +58,7 @@ sonia.scm scm-test - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT test diff --git a/scm-it/pom.xml b/scm-it/pom.xml index 1817845989..acc6971500 100644 --- a/scm-it/pom.xml +++ b/scm-it/pom.xml @@ -31,40 +31,40 @@ sonia.scm scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm scm-it war - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-it sonia.scm scm-core - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm scm-test - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm.plugins scm-git-plugin - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT test sonia.scm.plugins scm-git-plugin - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT tests test @@ -72,14 +72,14 @@ sonia.scm.plugins scm-hg-plugin - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT test sonia.scm.plugins scm-hg-plugin - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT tests test @@ -87,14 +87,14 @@ sonia.scm.plugins scm-svn-plugin - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT test sonia.scm.plugins scm-svn-plugin - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT tests test diff --git a/scm-packaging/deb/pom.xml b/scm-packaging/deb/pom.xml index 569e1d9e73..232841d07d 100644 --- a/scm-packaging/deb/pom.xml +++ b/scm-packaging/deb/pom.xml @@ -32,12 +32,12 @@ sonia.scm.packaging scm-packaging - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT deb deb - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT Packaging for Debian/Ubuntu deb @@ -46,7 +46,7 @@ sonia.scm scm-server - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT diff --git a/scm-packaging/docker/Dockerfile b/scm-packaging/docker/Dockerfile index 9c0451a38e..a69c584d65 100644 --- a/scm-packaging/docker/Dockerfile +++ b/scm-packaging/docker/Dockerfile @@ -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" ] diff --git a/scm-packaging/docker/pom.xml b/scm-packaging/docker/pom.xml index b12f198695..adba6c8271 100644 --- a/scm-packaging/docker/pom.xml +++ b/scm-packaging/docker/pom.xml @@ -32,12 +32,12 @@ sonia.scm.packaging scm-packaging - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT docker pom - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT diff --git a/scm-packaging/helm/pom.xml b/scm-packaging/helm/pom.xml index 2a642dee27..6d53125f2d 100644 --- a/scm-packaging/helm/pom.xml +++ b/scm-packaging/helm/pom.xml @@ -32,12 +32,12 @@ sonia.scm.packaging scm-packaging - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT helm helm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT 3.2.1 diff --git a/scm-packaging/pom.xml b/scm-packaging/pom.xml index faf3e5d404..592139e03f 100644 --- a/scm-packaging/pom.xml +++ b/scm-packaging/pom.xml @@ -31,13 +31,13 @@ sonia.scm scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm.packaging scm-packaging pom - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT packages.scm-manager.org diff --git a/scm-packaging/release-yaml/pom.xml b/scm-packaging/release-yaml/pom.xml index f2bb1e07d2..a8b6dc0a94 100644 --- a/scm-packaging/release-yaml/pom.xml +++ b/scm-packaging/release-yaml/pom.xml @@ -32,12 +32,12 @@ sonia.scm.packaging scm-packaging - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT release-yaml pom - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT diff --git a/scm-packaging/rpm/pom.xml b/scm-packaging/rpm/pom.xml index 98b4dacad7..072a6effed 100644 --- a/scm-packaging/rpm/pom.xml +++ b/scm-packaging/rpm/pom.xml @@ -32,12 +32,12 @@ sonia.scm.packaging scm-packaging - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT rpm rpm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT Packaging for RedHat/Centos/Fedora rpm @@ -52,7 +52,7 @@ sonia.scm scm-server - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT diff --git a/scm-packaging/unix/pom.xml b/scm-packaging/unix/pom.xml index 41063ca51d..a7d09c302c 100644 --- a/scm-packaging/unix/pom.xml +++ b/scm-packaging/unix/pom.xml @@ -31,12 +31,12 @@ sonia.scm.packaging scm-packaging - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT unix pom - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT diff --git a/scm-packaging/windows/pom.xml b/scm-packaging/windows/pom.xml index 617f6a8b34..2fb9a1f595 100644 --- a/scm-packaging/windows/pom.xml +++ b/scm-packaging/windows/pom.xml @@ -32,12 +32,12 @@ sonia.scm.packaging scm-packaging - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT windows pom - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index 33b1061c3f..2c61f07a89 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -31,13 +31,13 @@ sonia.scm scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm.plugins scm-plugins pom - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-plugins @@ -60,7 +60,7 @@ sonia.scm scm-core - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT provided @@ -69,7 +69,7 @@ sonia.scm scm-annotation-processor - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT provided @@ -99,7 +99,7 @@ sonia.scm scm-test - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT test diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index a0ca9cf3ac..4f4087a3f6 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -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" } } diff --git a/scm-plugins/scm-git-plugin/pom.xml b/scm-plugins/scm-git-plugin/pom.xml index 23c7e50527..b5c374758d 100644 --- a/scm-plugins/scm-git-plugin/pom.xml +++ b/scm-plugins/scm-git-plugin/pom.xml @@ -31,7 +31,7 @@ scm-plugins sonia.scm.plugins - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-git-plugin diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java index 893de773da..99ac469166 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java index 71d97f27be..e366a35abb 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -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") diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigDto.java index 92a495669b..46eb671d5c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigDto.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigDto.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java index 3aa348d13a..559dfddae8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitRepositoryConfigResource.java @@ -21,13 +21,16 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + 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" diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateGitConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateGitConfigDto.java new file mode 100644 index 0000000000..66bdcd7278 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateGitConfigDto.java @@ -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(); +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateGitRepositoryConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateGitRepositoryConfigDto.java new file mode 100644 index 0000000000..0da843e1af --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateGitRepositoryConfigDto.java @@ -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(); +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java index d026affd8b..683e5d0068 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java @@ -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 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)) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java index 967226012a..fddf386d92 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java @@ -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 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 branches = branchesCommand.getBranches(); - assertThat(branches).isEmpty(); - } - - @Test - void shouldMapNormalBranch() throws IOException, GitAPIException { - Ref branch = createRef("branch", "1337"); - when(listBranchCommand.call()).thenReturn(asList(branch)); - - List 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 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 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; - } } diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index 107dde3e2e..8aaa4d0b34 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -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" } } diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index a6f125a6f9..6f1563d5a4 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -31,7 +31,7 @@ sonia.scm.plugins scm-plugins - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-hg-plugin @@ -44,15 +44,29 @@ com.aragost.javahg javahg - 0.15-scm1 + 0.16 com.google.guava guava + + org.slf4j + slf4j-simple + + + org.slf4j + slf4j-nop + + + ch.qos.logback + logback-classic + test + + diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java index 2d8c385128..70eb367d3c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java @@ -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" diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java index b5039cbb44..3ddb33052f 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; @@ -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 diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java index cb2ac532ab..0c715ce8c3 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java @@ -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" diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateHgConfigDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateHgConfigDto.java new file mode 100644 index 0000000000..7106c7c85a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateHgConfigDto.java @@ -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(); +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java deleted file mode 100644 index a74fdf9b21..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/AbstractHgHandler.java +++ /dev/null @@ -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 createEnvironment(String revision, String path) - { - Map 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(), args); - } - - /** - * Method description - * - * - * @param extraEnv - * @param args - * - * @return - * - * @throws IOException - */ - protected Process createHgProcess(Map 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 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 getResultFromScript(Class resultType, HgPythonScript script) throws IOException { - return getResultFromScript(resultType, script, - new HashMap()); - } - - @SuppressWarnings("unchecked") - protected T getResultFromScript(Class resultType, - HgPythonScript script, Map 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 extraEnv, String cmd, - String... args) - throws IOException - { - HgConfig config = handler.getConfig(); - List cmdList = new ArrayList(); - - cmdList.add(cmd); - - if (Util.isNotEmpty(args)) - { - cmdList.addAll(Arrays.asList(args)); - } - - if (logger.isDebugEnabled()) - { - StringBuilder msg = new StringBuilder("create process for ["); - Iterator 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 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 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; -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/DefaultHgEnvironmentBuilder.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/DefaultHgEnvironmentBuilder.java new file mode 100644 index 0000000000..1888f0aeae --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/DefaultHgEnvironmentBuilder.java @@ -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 read(Repository repository) { + ImmutableMap.Builder env = ImmutableMap.builder(); + read(env, repository); + return env.build(); + } + + @Override + public Map write(Repository repository) { + ImmutableMap.Builder env = ImmutableMap.builder(); + read(env, repository); + write(env); + return env.build(); + } + + private void read(ImmutableMap.Builder 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 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; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java index daf3414a63..3189239dbd 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; @@ -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 Issue 959 - */ - private boolean disableHookSSLValidation = false; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContext.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContext.java deleted file mode 100644 index 2791fff3c0..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContext.java +++ /dev/null @@ -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; -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextProvider.java deleted file mode 100644 index 6b09416db2..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextProvider.java +++ /dev/null @@ -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 -{ - - /** - * the LOG for HgContextProvider - */ - private static final Logger LOG = - LoggerFactory.getLogger(HgContextProvider.class); - - //~--- get methods ---------------------------------------------------------- - - private Provider requestStoreProvider; - - @Inject - public HgContextProvider(Provider 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; - } - } - } -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java deleted file mode 100644 index a997bb2069..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ /dev/null @@ -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 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 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 environment, AccessToken accessToken) { - environment.put(SCM_XSRF, accessToken.getCustom(Xsrf.TOKEN_KEY).orElse("-")); - } -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextRequestStore.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironmentBuilder.java similarity index 59% rename from scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextRequestStore.java rename to scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironmentBuilder.java index 3fa04bd1e6..3738d92d88 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgContextRequestStore.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironmentBuilder.java @@ -21,28 +21,15 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + 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. - * - *

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.

- * - *

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.

- */ -@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 read(Repository repository); + Map write(Repository repository); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java deleted file mode 100644 index 3d333015ee..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java +++ /dev/null @@ -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 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 httpServletRequestProvider; - - private final AccessTokenBuilderFactory accessTokenBuilderFactory; -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgPythonScript.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgPythonScript.java index a7c5ef6069..0e4b05b101 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgPythonScript.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgPythonScript.java @@ -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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryFactory.java new file mode 100644 index 0000000000..5b17ad8bd4 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryFactory.java @@ -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 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 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 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; + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index bdc36eb54b..14cd54866d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -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 { - 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 hgContextProvider; - private final HgWorkingCopyFactory workingCopyFactory; - private final JAXBContext jaxbContext; - @Inject public HgRepositoryHandler(ConfigurationStoreFactory storeFactory, - Provider 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()) { - 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; + String getVersionInformation(HgVersionCommand command) { + String version = getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION); + HgVersion hgVersion = command.get(); + logger.debug("mercurial/python informations: {}", hgVersion); + 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; - } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersion.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersion.java index 36bd6abfa3..a517c2fe08 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersion.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersion.java @@ -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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookMessage.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookMessage.java index ea1671d7ab..0cd78ac932 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookMessage.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookMessage.java @@ -21,75 +21,31 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + 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 } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/DefaultHookHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/DefaultHookHandler.java new file mode 100644 index 0000000000..c189299d72 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/DefaultHookHandler.java @@ -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 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 messages; + private boolean abort; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/AbstractHgCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookContextProviderFactory.java similarity index 52% rename from scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/AbstractHgCommand.java rename to scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookContextProviderFactory.java index 259996e2ed..04f88189ce 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/AbstractHgCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookContextProviderFactory.java @@ -21,60 +21,37 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- +package sonia.scm.repository.hooks; -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 -------------------------------------------------------------- - - /** - * Method description - * - * - * @param revision - * @param path - * - * @param request - * - * @return - */ - protected Map createEnvironment(FileBaseCommandRequest request) - { - return createEnvironment(request.getRevision(), request.getPath()); + 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); } + } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookEnvironment.java new file mode 100644 index 0000000000..a1d205a9d5 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookEnvironment.java @@ -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 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; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookHandler.java new file mode 100644 index 0000000000..f2f7e7ccdd --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookHandler.java @@ -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 { +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookHandlerFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookHandlerFactory.java new file mode 100644 index 0000000000..4eaf205bdd --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookHandlerFactory.java @@ -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); + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookModule.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookModule.java new file mode 100644 index 0000000000..2eed226a09 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookModule.java @@ -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) + ); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookServer.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookServer.java new file mode 100644 index 0000000000..90a15c6fb4 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookServer.java @@ -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(); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/Sockets.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/Sockets.java new file mode 100644 index 0000000000..4a0389a33c --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/Sockets.java @@ -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 receive(InputStream in, Class 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); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java index 22cccbc8ae..a2285bad11 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java @@ -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 hgBranches = com.aragost.javahg.commands.BranchesCommand.on(open()).execute(); - List branches = Lists.transform(hgBranches, - new Function() - { - - @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; + }); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java index 39181d3d67..5169d3c389 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java @@ -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> 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; + @Override + public void close() { + if (repository != null) { + repository.close(); + } + } - /** 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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookChangesetProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookChangesetProvider.java index 10fe43bcf7..98bddd8c7c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookChangesetProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookChangesetProvider.java @@ -21,85 +21,56 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- +package sonia.scm.repository.spi; import 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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java index 1f72d56e24..0282729baa 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgHookContextProvider.java @@ -21,14 +21,14 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -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; @@ -45,55 +44,40 @@ import java.util.Set; /** * Mercurial implementation of {@link HookContextProvider}. - * + * * @author Sebastian Sdorra */ -public class HgHookContextProvider extends HookContextProvider -{ +public class HgHookContextProvider extends HookContextProvider { - private static final Set SUPPORTED_FEATURES = - EnumSet.of(HookFeature.CHANGESET_PROVIDER, HookFeature.MESSAGE_PROVIDER, - HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER); + private static final Set 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; } @@ -102,14 +86,11 @@ 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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java index 302bea6d4f..aaea12eeac 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java @@ -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 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 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 pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy workingCopy) { + LOG.trace("pull changes from working copy"); try { com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); workingCopyFactory.configure(pullCommand); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index b7eece2b9f..df40fdf4ae 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -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 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 FEATURES = - EnumSet.of(Feature.COMBINED_DEFAULT_BRANCH); + public static final Set 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 * diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java index 4af799f30a..87a5d6d6fb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java @@ -21,12 +21,12 @@ * 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.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; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgVersionCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgVersionCommand.java new file mode 100644 index 0000000000..2d4e44f5a7 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgVersionCommand.java @@ -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 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 command) throws IOException; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactory.java index 4e4c4cf80c..8c01c369ff 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactory.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactory.java @@ -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 implements HgWorkingCopyFactory { - private final Provider hgRepositoryEnvironmentBuilder; - @Inject - public SimpleHgWorkingCopyFactory(Provider hgRepositoryEnvironmentBuilder, WorkingCopyPool workdirProvider) { + public SimpleHgWorkingCopyFactory(WorkingCopyPool workdirProvider) { super(workdirProvider); - this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder; } + @Override public ParentAndClone 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 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> 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 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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java deleted file mode 100644 index 5b0609cc72..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java +++ /dev/null @@ -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 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 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 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 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 contextProvider; - - /** Field description */ - private final HgRepositoryHandler handler; - - /** Field description */ - private final HookEventFacade hookEventFacade; - - /** Field description */ - private final HgHookManager hookManager; -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java deleted file mode 100644 index 6b626b5c33..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java +++ /dev/null @@ -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 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 - ); - } -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java index 5271751faa..64a27990a8 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java @@ -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); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java index 234580ed6f..3ba797aff2 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java @@ -21,189 +21,48 @@ * 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.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> 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); - } } diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.tsx b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.tsx index 6b72cb9477..bbcf11c012 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.tsx +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.tsx @@ -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 { {this.checkbox("showRevisionInId")}
- {this.checkbox("disableHookSSLValidation")} {this.checkbox("enableHttpPostArgs")}
diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json index 6a4a639812..2d4b08cd2d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json @@ -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" diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json index 87f4d80ac4..9082862174 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json @@ -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" diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py index 81b02c7818..de7907f403 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py @@ -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 diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py index eb7718bb87..492b6f53ba 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py @@ -29,112 +29,84 @@ # changegroup.scm = python:scmhooks.callback # -import os, sys - -client = None - -# compatibility layer between python 2 and 3 urllib implementations -if sys.version_info[0] < 3: - import urllib, urllib2 - # create type alias for url error - URLError = urllib2.URLError - - class Python2Client: - def post(self, url, values): - data = urllib.urlencode(values) - # open url but ignore proxy settings - proxy_handler = urllib2.ProxyHandler({}) - opener = urllib2.build_opener(proxy_handler) - req = urllib2.Request(url, data) - req.add_header("X-XSRF-Token", xsrf) - return opener.open(req) - - client = Python2Client() -else: - import urllib.parse, urllib.request, urllib.error - # create type alias for url error - URLError = urllib.error.URLError - - class Python3Client: - def post(self, url, values): - data = urllib.parse.urlencode(values) - # open url but ignore proxy settings - proxy_handler = urllib.request.ProxyHandler({}) - opener = urllib.request.build_opener(proxy_handler) - req = urllib.request.Request(url, data.encode()) - req.add_header("X-XSRF-Token", xsrf) - return opener.open(req) - - client = Python3Client() +import os, sys, json, socket, struct # read environment -baseUrl = os.environ['SCM_URL'] +port = os.environ['SCM_HOOK_PORT'] challenge = os.environ['SCM_CHALLENGE'] token = os.environ['SCM_BEARER_TOKEN'] -xsrf = os.environ['SCM_XSRF'] repositoryId = os.environ['SCM_REPOSITORY_ID'] +transactionId = os.environ['SCM_TRANSACTION_ID'] -def printMessages(ui, msgs): - for raw in msgs: - line = raw - if hasattr(line, "encode"): - line = line.encode() - if line.startswith(b"_e") or line.startswith(b"_n"): - line = line[2:] - ui.warn(b'%s\n' % line.rstrip()) +def print_messages(ui, messages): + for message in messages: + msg = "[SCM]" + if message['severity'] == "ERROR": + msg += " Error" + msg += ": " + message['message'] + "\n" + ui.warn(msg.encode('utf-8')) -def callHookUrl(ui, repo, hooktype, node): +def read_bytes(connection, length): + received = bytearray() + while len(received) < length: + buffer = connection.recv(length - len(received)) + received = received + buffer + return received + +def read_int(connection): + data = read_bytes(connection, 4) + return struct.unpack('>i', bytearray(data))[0] + +def fire_hook(ui, repo, hooktype, node): abort = True + ui.debug( b"send scm-hook for " + node + b"\n" ) + connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - url = baseUrl + hooktype.decode("utf-8") - ui.debug( b"send scm-hook to " + url.encode() + b" and " + node + b"\n" ) - values = {'node': node.decode("utf-8"), 'challenge': challenge, 'token': token, 'repositoryPath': repo.root, 'repositoryId': repositoryId} - conn = client.post(url, values) - if 200 <= conn.code < 300: - ui.debug( b"scm-hook " + hooktype + b" success with status code " + str(conn.code).encode() + b"\n" ) - printMessages(ui, conn) - abort = False - else: - ui.warn( b"ERROR: scm-hook failed with error code " + str(conn.code).encode() + b"\n" ) - except URLError as e: - msg = None - # some URLErrors have no read method - if hasattr(e, "read"): - msg = e.read() - elif hasattr(e, "code"): - msg = "scm-hook failed with error code " + e.code + "\n" - else: - msg = str(e) - if len(msg) > 0: - printMessages(ui, msg.splitlines(True)) - else: - ui.warn( b"ERROR: scm-hook failed with an unknown error\n" ) - ui.traceback() + values = {'token': token, 'type': hooktype, 'repositoryId': repositoryId, 'transactionId': transactionId, 'challenge': challenge, 'node': node.decode('utf8') } + + connection.connect(("127.0.0.1", int(port))) + + data = json.dumps(values).encode('utf-8') + connection.send(struct.pack('>i', len(data))) + connection.sendall(data) + + length = read_int(connection) + if length > 8192: + ui.warn( b"scm-hook received message which exceeds the limit of 8192\n" ) + return True + + d = read_bytes(connection, length) + response = json.loads(d.decode("utf-8")) + + abort = response['abort'] + print_messages(ui, response['messages']) + except ValueError: ui.warn( b"scm-hook failed with an exception\n" ) ui.traceback() + finally: + connection.close() return abort def callback(ui, repo, hooktype, node=None): abort = True if node != None: - if len(baseUrl) > 0: - abort = callHookUrl(ui, repo, hooktype, node) + if len(port) > 0: + abort = fire_hook(ui, repo, hooktype, node) else: ui.warn(b"ERROR: scm-manager hooks are disabled, please check your configuration and the scm-manager log for details\n") - abort = False else: ui.warn(b"changeset node is not available") return abort -def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): +def pre_hook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): # older mercurial versions if pending != None: pending() # newer mercurial version # we have to make in-memory changes visible to external process - # this does not happen automatically, because mercurial treat our hooks as internal hooks + # this does not happen automatically, because mercurial treat our hooks as internal hook # see hook.py at mercurial sources _exthook try: if repo is not None: @@ -143,10 +115,10 @@ def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): if tr and not tr.writepending(): ui.warn(b"no pending write transaction found") except AttributeError: - ui.debug(b"mercurial does not support currenttransation") + ui.debug(b"mercurial does not support currenttransaction") # do nothing - return callback(ui, repo, hooktype, node) + return callback(ui, repo, "PRE_RECEIVE", node) -def postHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): - return callback(ui, repo, hooktype, node) +def post_hook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): + return callback(ui, repo, "POST_RECEIVE", node) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/version.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/version.py deleted file mode 100644 index 25c35fe8a2..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/version.py +++ /dev/null @@ -1,45 +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. -# - -import sys -from mercurial import util -from xml.dom.minidom import Document - -pyVersion = sys.version_info -pyVersion = str(pyVersion.major) + "." + str(pyVersion.minor) + "." + str(pyVersion.micro) -hgVersion = util.version() - -doc = Document() -root = doc.createElement('version') - -pyNode = doc.createElement('python') -pyNode.appendChild(doc.createTextNode(pyVersion)) -root.appendChild(pyNode) - -hgNode = doc.createElement('mercurial') -hgNode.appendChild(doc.createTextNode(hgVersion)) -root.appendChild(hgNode) - -doc.appendChild(root) -doc.writexml(sys.stdout, encoding='UTF-8') \ No newline at end of file diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java index 028276cd62..7779380638 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java @@ -21,7 +21,7 @@ * 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.junit.Test; @@ -52,7 +52,6 @@ public class HgConfigDtoToHgConfigMapperTest { assertEquals("/etc/", config.getPythonPath()); assertTrue(config.isShowRevisionInId()); assertTrue(config.isUseOptimizedBytecode()); - assertTrue(config.isDisableHookSSLValidation()); assertTrue(config.isEnableHttpPostArgs()); } @@ -65,7 +64,6 @@ public class HgConfigDtoToHgConfigMapperTest { configDto.setPythonPath("/etc/"); configDto.setShowRevisionInId(true); configDto.setUseOptimizedBytecode(true); - configDto.setDisableHookSSLValidation(true); configDto.setEnableHttpPostArgs(true); return configDto; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/DefaultHgEnvironmentBuilderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/DefaultHgEnvironmentBuilderTest.java new file mode 100644 index 0000000000..7bf3a3c5bf --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/DefaultHgEnvironmentBuilderTest.java @@ -0,0 +1,159 @@ +/* + * 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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContext; +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 javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static sonia.scm.repository.DefaultHgEnvironmentBuilder.*; + + +@ExtendWith(MockitoExtension.class) +class DefaultHgEnvironmentBuilderTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private AccessTokenBuilderFactory accessTokenBuilderFactory; + + @Mock + private HgRepositoryHandler repositoryHandler; + + @Mock + private HookEnvironment hookEnvironment; + + @Mock + private HookServer server; + + @InjectMocks + private DefaultHgEnvironmentBuilder builder; + + private Path directory; + + @BeforeEach + void setBaseDir(@TempDir Path directory) { + this.directory = directory; + TempSCMContextProvider context = (TempSCMContextProvider) SCMContext.getContext(); + context.setBaseDirectory(directory.resolve("home").toFile()); + } + + @Test + void shouldReturnReadEnvironment() { + Repository heartOfGold = prepareForRead("/usr/lib/python", "42"); + + Map env = builder.read(heartOfGold); + assertReadEnv(env, "/usr/lib/python", "42"); + } + + @Test + void shouldReturnWriteEnvironment() throws IOException { + Repository heartOfGold = prepareForWrite("/opt/python", "21"); + + Map env = builder.write(heartOfGold); + assertReadEnv(env, "/opt/python", "21"); + + String bearer = CipherUtil.getInstance().decode(env.get(ENV_BEARER_TOKEN)); + assertThat(bearer).isEqualTo("secretAC"); + assertThat(env) + .containsEntry(ENV_CHALLENGE, "challenge") + .containsEntry(ENV_HOOK_PORT, "2042"); + } + + @Test + void shouldSetTransactionId() throws IOException { + TransactionId.set("ti42"); + Repository heartOfGold = prepareForWrite("/opt/python", "21"); + Map env = builder.write(heartOfGold); + assertThat(env).containsEntry(ENV_TRANSACTION_ID, "ti42"); + } + + @Test + void shouldThrowIllegalStateIfServerCouldNotBeStarted() throws IOException { + when(server.start()).thenThrow(new IOException("failed to start")); + Repository repository = prepareForRead("/usr", "42"); + assertThrows(IllegalStateException.class, () -> builder.write(repository)); + } + + private Repository prepareForWrite(String pythonPath, String id) throws IOException { + Repository heartOfGold = prepareForRead(pythonPath, id); + applyAccessToken("secretAC"); + when(server.start()).thenReturn(2042); + when(hookEnvironment.getChallenge()).thenReturn("challenge"); + return heartOfGold; + } + + private void applyAccessToken(String compact) { + AccessToken accessToken = mock(AccessToken.class); + when(accessTokenBuilderFactory.create().custom(Xsrf.TOKEN_KEY, null).build()).thenReturn(accessToken); + when(accessToken.compact()).thenReturn(compact); + } + + + private void assertReadEnv(Map env, String pythonPath, String repositoryId) { + assertThat(env) + .containsEntry(ENV_REPOSITORY_ID, repositoryId) + .containsEntry(ENV_REPOSITORY_NAME, "hitchhiker/HeartOfGold") + .containsEntry(ENV_HTTP_POST_ARGS, "false") + .containsEntry(ENV_REPOSITORY_PATH, directory.resolve("repo").toAbsolutePath().toString()) + .containsEntry(ENV_PYTHON_PATH, pythonPath + File.pathSeparator + directory.resolve("home/lib/python")); + } + + @Nonnull + private Repository prepareForRead(String pythonPath, String id) { + when(repositoryHandler.getDirectory(id)).thenReturn(directory.resolve("repo").toFile()); + + HgConfig config = new HgConfig(); + config.setPythonPath(pythonPath); + when(repositoryHandler.getConfig()).thenReturn(config); + + Repository heartOfGold = RepositoryTestData.createHeartOfGold(); + heartOfGold.setId(id); + + return heartOfGold; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersionHandler.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/EmptyHgEnvironmentBuilder.java similarity index 66% rename from scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersionHandler.java rename to scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/EmptyHgEnvironmentBuilder.java index 7ee499741f..140802ce7f 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgVersionHandler.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/EmptyHgEnvironmentBuilder.java @@ -21,30 +21,20 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; -//~--- JDK imports ------------------------------------------------------------ +import java.util.Collections; +import java.util.Map; -import java.io.File; -import java.io.IOException; - -/** - * - * @author Sebastian Sdorra - */ -public class HgVersionHandler extends AbstractHgHandler -{ - - public HgVersionHandler(HgRepositoryHandler handler, HgContext context, - File directory) - { - super(handler, context, null, directory); +public class EmptyHgEnvironmentBuilder implements HgEnvironmentBuilder { + @Override + public Map read(Repository repository) { + return Collections.emptyMap(); } - //~--- get methods ---------------------------------------------------------- - - public HgVersion getVersion() throws IOException { - return getResultFromScript(HgVersion.class, HgPythonScript.VERSION); + @Override + public Map write(Repository repository) { + return Collections.emptyMap(); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgContextProviderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgContextProviderTest.java deleted file mode 100644 index a65e4cd8e2..0000000000 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgContextProviderTest.java +++ /dev/null @@ -1,111 +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; - -import com.google.inject.AbstractModule; -import com.google.inject.Guice; -import com.google.inject.Injector; -import com.google.inject.Key; -import com.google.inject.OutOfScopeException; -import com.google.inject.Provider; -import com.google.inject.ProvisionException; -import com.google.inject.Scope; -import com.google.inject.servlet.RequestScoped; -import com.google.inject.util.Providers; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class HgContextProviderTest { - - @Mock - private Scope scope; - - @Test - void shouldThrowNonOutOfScopeProvisionExceptions() { - Provider provider = () -> { - throw new RuntimeException("something different"); - }; - - when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider); - - Injector injector = Guice.createInjector(new HgContextModule(scope)); - - assertThrows(ProvisionException.class, () -> injector.getInstance(HgContext.class)); - } - - @Test - void shouldCreateANewInstanceIfOutOfRequestScope() { - Provider provider = () -> { - throw new OutOfScopeException("no request"); - }; - when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider); - - Injector injector = Guice.createInjector(new HgContextModule(scope)); - - HgContext contextOne = injector.getInstance(HgContext.class); - HgContext contextTwo = injector.getInstance(HgContext.class); - - assertThat(contextOne).isNotSameAs(contextTwo); - } - - @Test - void shouldInjectFromRequestScope() { - HgContextRequestStore requestStore = new HgContextRequestStore(); - Provider provider = Providers.of(requestStore); - - when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider); - - Injector injector = Guice.createInjector(new HgContextModule(scope)); - - HgContext contextOne = injector.getInstance(HgContext.class); - HgContext contextTwo = injector.getInstance(HgContext.class); - - assertThat(contextOne).isSameAs(contextTwo); - } - - private static class HgContextModule extends AbstractModule { - - private Scope scope; - - private HgContextModule(Scope scope) { - this.scope = scope; - } - - @Override - protected void configure() { - bindScope(RequestScoped.class, scope); - bind(HgContextRequestStore.class); - bind(HgContext.class).toProvider(HgContextProvider.class); - } - } -} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryFactoryTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryFactoryTest.java new file mode 100644 index 0000000000..ffeb42f613 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryFactoryTest.java @@ -0,0 +1,125 @@ +/* + * 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.Repository; +import com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.hooks.HookEnvironment; + +import javax.annotation.Nonnull; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class HgRepositoryFactoryTest { + + private HgRepositoryHandler handler; + + @Mock + private HookEnvironment hookEnvironment; + + @Mock + private HgEnvironmentBuilder environmentBuilder; + + private HgRepositoryFactory factory; + + private sonia.scm.repository.Repository heartOfGold; + + @BeforeEach + void setUpFactory(@TempDir Path directory) { + handler = HgTestUtil.createHandler(directory.toFile()); + assumeTrue(handler.isConfigured()); + + factory = new HgRepositoryFactory(handler, hookEnvironment, environmentBuilder); + heartOfGold = createRepository(); + } + + @Test + void shouldOpenRepositoryForRead() { + Repository repository = factory.openForRead(heartOfGold); + + assertThat(repository).isNotNull(); + verify(environmentBuilder).read(heartOfGold); + } + + @Test + void shouldOpenRepositoryForWrite() { + Repository repository = factory.openForWrite(heartOfGold); + + assertThat(repository).isNotNull(); + verify(environmentBuilder).write(heartOfGold); + } + + @Test + void shouldFallbackToUTF8OnUnknownEncoding() { + handler.getConfig().setEncoding("unknown"); + + Repository repository = factory.openForRead(heartOfGold); + + assertThat(repository.getBaseRepository().getConfiguration().getEncoding()).isEqualTo(StandardCharsets.UTF_8); + } + + @Test + void shouldSetPendingChangesetState() { + when(hookEnvironment.isPending()).thenReturn(true); + + Repository repository = factory.openForRead(heartOfGold); + + assertThat(repository.getBaseRepository().getConfiguration().isEnablePendingChangesets()) + .isTrue(); + } + + @Test + void shouldPassEnvironment() { + when(environmentBuilder.read(heartOfGold)).thenReturn(ImmutableMap.of("spaceship", "heartOfGold")); + + Repository repository = factory.openForRead(heartOfGold); + + assertThat(repository.getBaseRepository().getConfiguration().getEnvironment()) + .containsEntry("spaceship", "heartOfGold"); + } + + @Nonnull + private sonia.scm.repository.Repository createRepository() { + sonia.scm.repository.Repository heartOfGold = RepositoryTestData.createHeartOfGold("hg"); + heartOfGold.setId("42"); + + handler.create(heartOfGold); + return heartOfGold; + } + + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java index e91659e841..7d0dc6f6db 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- @@ -32,13 +32,17 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.spi.HgVersionCommand; import sonia.scm.store.ConfigurationStoreFactory; import java.io.File; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ @@ -52,9 +56,6 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Mock private ConfigurationStoreFactory factory; - @Mock - private com.google.inject.Provider provider; - @Override protected void checkDirectory(File directory) { File hgDirectory = new File(directory, ".hg"); @@ -70,7 +71,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Override protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { - HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null, null); + HgRepositoryHandler handler = new HgRepositoryHandler(factory, locationResolver, null, null); handler.init(contextProvider); HgTestUtil.checkForSkip(handler); @@ -80,7 +81,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { @Test public void getDirectory() { - HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null, null); + HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, locationResolver, null, null); HgConfig hgConfig = new HgConfig(); hgConfig.setHgBinary("hg"); @@ -91,4 +92,20 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { File path = repositoryHandler.getDirectory(repository.getId()); assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath()); } + + @Test + public void shouldReturnVersionInformation() { + PluginLoader pluginLoader = mock(PluginLoader.class); + when(pluginLoader.getUberClassLoader()).thenReturn(HgRepositoryHandler.class.getClassLoader()); + + HgVersionCommand versionCommand = mock(HgVersionCommand.class); + when(versionCommand.get()).thenReturn(new HgVersion("5.2.0", "3.7.2")); + + HgRepositoryHandler handler = new HgRepositoryHandler( + factory, locationResolver, pluginLoader, null + ); + + String versionInformation = handler.getVersionInformation(versionCommand); + assertThat(versionInformation).startsWith("scm-hg-version/").endsWith("python/3.7.2 mercurial/5.2.0"); + } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index 72d276b2b6..48b8e4c506 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- @@ -29,16 +29,13 @@ package sonia.scm.repository; import org.junit.Assume; import sonia.scm.SCMContext; import sonia.scm.TempDirRepositoryLocationResolver; -import sonia.scm.security.AccessToken; +import sonia.scm.repository.hooks.HookEnvironment; import sonia.scm.store.InMemoryConfigurationStoreFactory; -import javax.servlet.http.HttpServletRequest; import java.io.File; -import java.nio.file.Path; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ @@ -80,49 +77,26 @@ public final class HgTestUtil } } - /** - * Method description - * - * - * @param directory - * - * @return - */ - public static HgRepositoryHandler createHandler(File directory) { - TempSCMContextProvider context = - (TempSCMContextProvider) SCMContext.getContext(); + public static HgRepositoryHandler createHandler(File directory) { + TempSCMContextProvider context = (TempSCMContextProvider) SCMContext.getContext(); context.setBaseDirectory(directory); - RepositoryDAO repoDao = mock(RepositoryDAO.class); - RepositoryLocationResolver repositoryLocationResolver = new TempDirRepositoryLocationResolver(directory); - HgRepositoryHandler handler = - new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null); + HgRepositoryHandler handler = new HgRepositoryHandler( + new InMemoryConfigurationStoreFactory(), + repositoryLocationResolver, + null, + null + ); handler.init(context); return handler; } - /** - * Method description - * - * - * @return - */ - public static HgHookManager createHookManager() - { - HgHookManager hookManager = mock(HgHookManager.class); - - when(hookManager.getChallenge()).thenReturn("challenge"); - when(hookManager.createUrl()).thenReturn( - "http://localhost:8081/scm/hook/hg/"); - when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn( - "http://localhost:8081/scm/hook/hg/"); - AccessToken accessToken = mock(AccessToken.class); - when(accessToken.compact()).thenReturn(""); - when(hookManager.getAccessToken()).thenReturn(accessToken); - - return hookManager; + public static HgRepositoryFactory createFactory(HgRepositoryHandler handler, File directory) { + return new HgRepositoryFactory( + handler, new HookEnvironment(), new EmptyHgEnvironmentBuilder(), repository -> directory + ); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/DefaultHookHandlerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/DefaultHookHandlerTest.java new file mode 100644 index 0000000000..eed006b6ce --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/DefaultHookHandlerTest.java @@ -0,0 +1,318 @@ +/* + * 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 org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.AfterEach; +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 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.api.HgHookMessageProvider; +import sonia.scm.repository.spi.HgHookContextProvider; +import sonia.scm.repository.spi.HookEventFacade; +import sonia.scm.security.CipherUtil; + +import javax.annotation.Nonnull; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DefaultHookHandlerTest { + + @Mock + private HookContextProviderFactory hookContextProviderFactory; + + @Mock + private HgHookContextProvider contextProvider; + + @Mock + private HookEventFacade hookEventFacade; + + @Mock + private HookEventFacade.HookEventHandler hookEventHandler; + + @Mock + private Socket socket; + + private HookEnvironment hookEnvironment; + + private DefaultHookHandler handler; + + @Mock + private Subject subject; + + @BeforeEach + void setUp() { + ThreadContext.bind(subject); + + hookEnvironment = new HookEnvironment(); + + handler = new DefaultHookHandler(hookContextProviderFactory, hookEventFacade, hookEnvironment, socket); + } + + private void mockMessageProvider() { + mockMessageProvider(new HgHookMessageProvider()); + } + + private void mockMessageProvider(HgHookMessageProvider messageProvider) { + when(hookContextProviderFactory.create("42", "abc")).thenReturn(contextProvider); + when(contextProvider.getHgMessageProvider()).thenReturn(messageProvider); + } + + @AfterEach + void tearDown() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldFireHook() throws IOException { + mockMessageProvider(); + when(hookEventFacade.handle("42")).thenReturn(hookEventHandler); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE); + DefaultHookHandler.Response response = send(request); + + assertSuccess(response, RepositoryHookType.POST_RECEIVE); + assertThat(hookEnvironment.isPending()).isFalse(); + } + + @Test + void shouldSetPendingStateOnPreReceiveHooks() throws IOException { + mockMessageProvider(); + when(hookEventFacade.handle("42")).thenReturn(hookEventHandler); + + // we have to capture the pending state, when the hook is fired + // because the state is cleared before the method ends + AtomicReference ref = new AtomicReference<>(Boolean.FALSE); + doAnswer(ic -> { + ref.set(hookEnvironment.isPending()); + return null; + }).when(hookEventHandler).fireHookEvent(RepositoryHookType.PRE_RECEIVE, contextProvider); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.PRE_RECEIVE); + DefaultHookHandler.Response response = send(request); + + assertSuccess(response, RepositoryHookType.PRE_RECEIVE); + assertThat(ref.get()).isTrue(); + } + + @Test + void shouldHandleUnknownFailure() throws IOException { + mockMessageProvider(); + + doThrow(new IllegalStateException("Something went wrong")) + .when(hookEventFacade) + .handle("42"); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE); + DefaultHookHandler.Response response = send(request); + + assertError(response, "unknown error"); + } + + @Test + void shouldHandleExceptionWithContext() throws IOException { + mockMessageProvider(); + + doThrow(new TestingException("Exception with Context")) + .when(hookEventFacade) + .handle("42"); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE); + DefaultHookHandler.Response response = send(request); + + assertError(response, "Exception with Context"); + } + + @Test + void shouldSendMessagesOnUnknownException() throws IOException { + mockMessageProviderWithMessages(); + + doThrow(new IllegalStateException("Abort it")) + .when(hookEventFacade) + .handle("42"); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE); + DefaultHookHandler.Response response = send(request); + + assertMessages(response, "unknown error"); + } + + @Test + void shouldSendMessagesOnExceptionWithContext() throws IOException { + mockMessageProviderWithMessages(); + + doThrow(new TestingException("Exception with Context")) + .when(hookEventFacade) + .handle("42"); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE); + DefaultHookHandler.Response response = send(request); + + assertMessages(response, "Exception with Context"); + } + + private void assertMessages(DefaultHookHandler.Response response, String errorMessage) { + List received = response.getMessages() + .stream() + .map(HgHookMessage::getMessage) + .collect(Collectors.toList()); + + assertThat(received).containsExactly("Some note", "Some error", errorMessage); + } + + private void mockMessageProviderWithMessages() { + HgHookMessageProvider messageProvider = new HgHookMessageProvider(); + messageProvider.sendMessage("Some note"); + messageProvider.sendMessage("Some error"); + mockMessageProvider(messageProvider); + } + + @Test + void shouldSetAndClearTransactionId() throws IOException { + mockMessageProvider(); + + AtomicReference ref = new AtomicReference<>(); + doAnswer(ic -> { + TransactionId.get().ifPresent(ref::set); + return null; + }).when(hookEventFacade).handle("42"); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE); + send(request); + + assertThat(ref).hasValue("ti21"); + assertThat(TransactionId.get()).isEmpty(); + } + + @Test + void shouldHandleAuthenticationFailure() throws IOException { + doThrow(AuthenticationException.class) + .when(subject) + .login(any(AuthenticationToken.class)); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE); + DefaultHookHandler.Response response = send(request); + + assertError(response, "authentication"); + } + + @Test + void shouldHandleNotFoundException() throws IOException { + doThrow(NotFoundException.class) + .when(hookEventFacade) + .handle("42"); + + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE); + DefaultHookHandler.Response response = send(request); + + assertError(response, "not found"); + } + + @Test + void shouldReturnErrorWithInvalidChallenge() throws IOException { + DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE, "something-different"); + DefaultHookHandler.Response response = send(request); + + assertError(response, "challenge"); + } + + private void assertSuccess(DefaultHookHandler.Response response, RepositoryHookType type) { + assertThat(response.getMessages()).isEmpty(); + assertThat(response.isAbort()).isFalse(); + + verify(hookEventHandler).fireHookEvent(eq(type), any(HgHookContextProvider.class)); + } + + private void assertError(DefaultHookHandler.Response response, String message) { + assertThat(response.isAbort()).isTrue(); + assertThat(response.getMessages()).hasSize(1); + HgHookMessage hgHookMessage = response.getMessages().get(0); + assertThat(hgHookMessage.getSeverity()).isEqualTo(HgHookMessage.Severity.ERROR); + assertThat(hgHookMessage.getMessage()).contains(message); + } + + @Nonnull + private DefaultHookHandler.Request createRequest(RepositoryHookType type) { + return createRequest(type, hookEnvironment.getChallenge()); + } + + @Nonnull + private DefaultHookHandler.Request createRequest(RepositoryHookType type, String challenge) { + String secret = CipherUtil.getInstance().encode("secret"); + return new DefaultHookHandler.Request( + secret, type, "ti21", "42", challenge, "abc" + ); + } + + private DefaultHookHandler.Response send(DefaultHookHandler.Request request) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + Sockets.send(buffer, request); + + ByteArrayInputStream input = new ByteArrayInputStream(buffer.toByteArray()); + when(socket.getInputStream()).thenReturn(input); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + when(socket.getOutputStream()).thenReturn(output); + + handler.run(); + + return Sockets.receive(new ByteArrayInputStream(output.toByteArray()), DefaultHookHandler.Response.class); + } + + private static class TestingException extends ExceptionWithContext { + + private TestingException(String message) { + super(Collections.emptyList(), message); + } + + @Override + public String getCode() { + return "42"; + } + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookContextProviderFactoryTest.java similarity index 52% rename from scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java rename to scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookContextProviderFactoryTest.java index b290eb229a..b5b74509ee 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgEnvironmentTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookContextProviderFactoryTest.java @@ -21,58 +21,50 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository; +package sonia.scm.repository.hooks; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.security.AccessToken; -import sonia.scm.security.Xsrf; +import sonia.scm.NotFoundException; +import sonia.scm.repository.HgRepositoryFactory; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryTestData; +import sonia.scm.repository.spi.HgHookContextProvider; -import java.util.HashMap; -import java.util.Map; - -import static java.util.Optional.empty; -import static java.util.Optional.of; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.Mockito.mock; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class HgEnvironmentTest { +class HookContextProviderFactoryTest { @Mock - HgRepositoryHandler handler; + private RepositoryManager repositoryManager; + @Mock - HgHookManager hookManager; + private HgRepositoryHandler repositoryHandler; + + @Mock + private HgRepositoryFactory repositoryFactory; + + @InjectMocks + private HookContextProviderFactory factory; @Test - void shouldExtractXsrfTokenWhenSet() { - AccessToken accessToken = mock(AccessToken.class); - when(accessToken.compact()).thenReturn(""); - when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(of("XSRF Token")); - when(hookManager.getAccessToken()).thenReturn(accessToken); - - Map environment = new HashMap<>(); - HgEnvironment.prepareEnvironment(environment, handler, hookManager); - - assertThat(environment).contains(entry("SCM_XSRF", "XSRF Token")); + void shouldCreateHookContextProvider() { + when(repositoryManager.get("42")).thenReturn(RepositoryTestData.create42Puzzle()); + HgHookContextProvider provider = factory.create("42", "xyz"); + assertThat(provider).isNotNull(); } @Test - void shouldIgnoreXsrfWhenNotSetButStillContainDummy() { - AccessToken accessToken = mock(AccessToken.class); - when(accessToken.compact()).thenReturn(""); - when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(empty()); - when(hookManager.getAccessToken()).thenReturn(accessToken); - - Map environment = new HashMap<>(); - HgEnvironment.prepareEnvironment(environment, handler, hookManager); - - assertThat(environment).containsKeys("SCM_XSRF"); + void shouldThrowNotFoundExceptionWithoutRepository() { + assertThrows(NotFoundException.class, () -> factory.create("42", "xyz")); } + } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookServerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookServerTest.java new file mode 100644 index 0000000000..ccffd36b04 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookServerTest.java @@ -0,0 +1,135 @@ +/* + * 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 lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.mgt.DefaultSecurityManager; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class HookServerTest { + + private static final Logger LOG = LoggerFactory.getLogger(HookServerTest.class); + + @BeforeEach + void setUp() { + DefaultSecurityManager securityManager = new DefaultSecurityManager(); + ThreadContext.bind(securityManager); + Subject subject = new Subject.Builder().principals(new SimplePrincipalCollection("Tricia", "Testing")).buildSubject(); + ThreadContext.bind(subject); + } + + @AfterEach + void tearDown() { + ThreadContext.unbindSubject(); + ThreadContext.unbindSecurityManager(); + } + + @Test + void shouldStartHookServer() throws IOException { + Response response = send(new Request("Joe")); + assertThat(response.getGreeting()).isEqualTo("Hello Joe"); + assertThat(response.getGreeter()).isEqualTo("Tricia"); + } + + private Response send(Request request) throws IOException { + try (HookServer server = new HookServer(HelloHandler::new)) { + int port = server.start(); + try ( + Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); + InputStream input = socket.getInputStream(); + OutputStream output = socket.getOutputStream() + ) { + Sockets.send(output, request); + return Sockets.receive(input, Response.class); + } catch (IOException ex) { + throw new RuntimeException("failed", ex); + } + } + } + + public static class HelloHandler implements HookHandler { + + private final Socket socket; + + private HelloHandler(Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + try (InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream()) { + Request request = Sockets.receive(input, Request.class); + Subject subject = SecurityUtils.getSubject(); + Sockets.send(output, new Response("Hello " + request.getName(), subject.getPrincipal().toString())); + } catch (IOException ex) { + throw new RuntimeException("failed", ex); + } finally { + try { + socket.close(); + } catch (IOException e) { + LOG.error("failed to close socket", e); + } + } + } + } + + @Data + @AllArgsConstructor + public static class Request { + + private String name; + + } + + @Data + @AllArgsConstructor + public static class Response { + + private String greeting; + private String greeter; + + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/SocketsTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/SocketsTest.java new file mode 100644 index 0000000000..bb5b48a420 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/SocketsTest.java @@ -0,0 +1,112 @@ +/* + * 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 lombok.AllArgsConstructor; +import lombok.Data; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Stream.generate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SocketsTest { + + @Test + void shouldSendAndReceive() throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Sockets.send(output, new TestValue("awesome")); + ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); + TestValue value = Sockets.receive(input, TestValue.class); + assertThat(value.value).isEqualTo("awesome"); + } + + @Test + void shouldFailWithTooFewBytesForLength() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write((512 >>> 24) & 0xFF); + output.write((512 >>> 16) & 0xFF); + + ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); + assertThrows(EOFException.class, () -> Sockets.receive(input, TestValue.class)); + } + + @Test + void shouldFailWithTooFewBytesForData() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write((16 >>> 24) & 0xFF); + output.write((16 >>> 16) & 0xFF); + output.write((16 >>> 8) & 0xFF); + output.write(16 & 0xFF); + + ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); + assertThrows(EOFException.class, () -> Sockets.receive(input, TestValue.class)); + } + + @Test + void shouldFailIfLimitIsExceeded() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write((9216 >>> 24) & 0xFF); + output.write((9216 >>> 16) & 0xFF); + output.write((9216 >>> 8) & 0xFF); + output.write(9216 & 0xFF); + + ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); + IOException ex = assertThrows(IOException.class, () -> Sockets.receive(input, TestValue.class)); + assertThat(ex.getMessage()).contains("9216"); + } + + @Test + void shouldSendAndReceiveWithChunks() throws IOException { + String stringValue = generate(() -> "a").limit(1024).collect(joining()); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Sockets.send(output, new TestValue(stringValue)); + InputStream input = new ByteArrayInputStream(output.toByteArray()) { + @Override + public synchronized int read(byte[] b, int off, int len) { + return super.read(b, off, Math.min(8, len)); + } + }; + TestValue value = Sockets.receive(input, TestValue.class); + assertThat(value.value).hasSize(1024); + } + + @Data + @AllArgsConstructor + public static class TestValue { + + private String value; + + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java index f026169f87..d115123c52 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -29,13 +29,16 @@ package sonia.scm.repository.spi; import org.junit.After; import org.junit.Before; +import sonia.scm.repository.HgRepositoryFactory; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgTestUtil; import sonia.scm.repository.RepositoryTestData; +import sonia.scm.repository.hooks.HookEnvironment; import sonia.scm.util.MockUtil; //~--- JDK imports ------------------------------------------------------------ +import java.io.File; import java.io.IOException; /** @@ -49,31 +52,22 @@ public class AbstractHgCommandTestBase extends ZippedRepositoryTestBase * Method description * * - * @throws IOException */ @After - public void close() throws IOException - { + public void close() { if (cmdContext != null) { cmdContext.close(); } } - /** - * Method description - * - * - * @throws IOException - */ @Before public void initHgHandler() throws IOException { this.handler = HgTestUtil.createHandler(tempFolder.newFolder()); - HgTestUtil.checkForSkip(handler); - cmdContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, - RepositoryTestData.createHeartOfGold(), repositoryDirectory); + HgRepositoryFactory factory = HgTestUtil.createFactory(handler, repositoryDirectory); + cmdContext = new HgCommandContext(handler, factory, RepositoryTestData.createHeartOfGold()); } //~--- set methods ---------------------------------------------------------- diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java index f3a632fa59..241d416d2c 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java @@ -29,12 +29,10 @@ import com.google.inject.util.Providers; import org.junit.Before; import org.junit.Test; import sonia.scm.repository.Branch; -import sonia.scm.repository.HgTestUtil; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.api.BranchRequest; import sonia.scm.repository.work.NoneCachingWorkingCopyPool; import sonia.scm.repository.work.WorkdirProvider; -import sonia.scm.web.HgRepositoryEnvironmentBuilder; import java.util.List; @@ -47,10 +45,8 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase { @Before public void initWorkingCopyFactory() { - HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder = - new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager()); - workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(hgRepositoryEnvironmentBuilder), new NoneCachingWorkingCopyPool(new WorkdirProvider())) { + workingCopyFactory = new SimpleHgWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())) { @Override public void configure(PullCommand pullCommand) { // we do not want to configure http hooks in this unit test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchesCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchesCommandTest.java new file mode 100644 index 0000000000..a8f43eb435 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchesCommandTest.java @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.repository.spi; + +import org.junit.Test; +import sonia.scm.repository.Branch; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.Branch.defaultBranch; +import static sonia.scm.repository.Branch.normalBranch; + +public class HgBranchesCommandTest extends AbstractHgCommandTestBase { + + @Test + public void shouldReadBranches() { + HgBranchesCommand command = new HgBranchesCommand(cmdContext); + + List branches = command.getBranches(); + + assertThat(branches).contains( + defaultBranch("default", "2baab8e80280ef05a9aa76c49c76feca2872afb7", 1339586381000L), + normalBranch("test-branch", "79b6baf49711ae675568e0698d730b97ef13e84a", 1339586299000L) + ); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgIncomingCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgIncomingCommandTest.java index a128e23075..9d04255027 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgIncomingCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgIncomingCommandTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -110,17 +110,10 @@ public class HgIncomingCommandTest extends IncomingOutgoingTestBase cmd.getIncomingChangesets(request); } - /** - * Method description - * - * - * @return - */ - private HgIncomingCommand createIncomingCommand() - { + private HgIncomingCommand createIncomingCommand() { return new HgIncomingCommand( - new HgCommandContext( - HgTestUtil.createHookManager(), handler, incomingRepository, - incomingDirectory), handler); + new HgCommandContext(handler, HgTestUtil.createFactory(handler, incomingDirectory), incomingRepository), + handler + ); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java index 914a063790..2e645c04c7 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java @@ -40,12 +40,11 @@ import static org.assertj.core.api.Assertions.assertThat; public class HgModificationsCommandTest extends IncomingOutgoingTestBase { - private HgModificationsCommand outgoingModificationsCommand; @Before public void init() { - HgCommandContext outgoingContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, outgoingRepository, outgoingDirectory); + HgCommandContext outgoingContext = new HgCommandContext(handler, HgTestUtil.createFactory(handler, outgoingDirectory), outgoingRepository); outgoingModificationsCommand = new HgModificationsCommand(outgoingContext); } @@ -116,10 +115,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase { assertThat(modifications).isNotNull(); assertThat(modifications.getAdded()) .as("added files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getModified()) .as("modified files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getRemoved()) .as("removed files modifications") .hasSize(1) @@ -136,10 +135,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase { assertThat(modifications).isNotNull(); assertThat(modifications.getAdded()) .as("added files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getModified()) .as("modified files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getRemoved()) .as("removed files modifications") .isEmpty(); @@ -161,10 +160,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase { assertThat(modifications).isNotNull(); assertThat(modifications.getAdded()) .as("added files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getModified()) .as("modified files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getRemoved()) .as("removed files modifications") .isEmpty(); @@ -189,7 +188,7 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase { assertThat(modifications).isNotNull(); assertThat(modifications.getAdded()) .as("added files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getModified()) .as("modified files modifications") .hasSize(1) @@ -197,10 +196,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase { .containsOnly(file); assertThat(modifications.getRemoved()) .as("removed files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getRenamed()) .as("renamed files modifications") - .hasSize(0); + .isEmpty(); }; } @@ -214,13 +213,13 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase { .containsOnly(addedFile); assertThat(modifications.getModified()) .as("modified files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getRemoved()) .as("removed files modifications") - .hasSize(0); + .isEmpty(); assertThat(modifications.getRenamed()) .as("renamed files modifications") - .hasSize(0); + .isEmpty(); }; } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java index a2323f69e7..168d741bcb 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java @@ -24,7 +24,6 @@ package sonia.scm.repository.spi; -import com.google.inject.util.Providers; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -32,12 +31,9 @@ import org.junit.rules.TemporaryFolder; import sonia.scm.AlreadyExistsException; import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; -import sonia.scm.repository.HgHookManager; -import sonia.scm.repository.HgTestUtil; import sonia.scm.repository.Person; import sonia.scm.repository.work.NoneCachingWorkingCopyPool; import sonia.scm.repository.work.WorkdirProvider; -import sonia.scm.web.HgRepositoryEnvironmentBuilder; import java.io.File; import java.io.FileOutputStream; @@ -55,9 +51,7 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase { @Before public void initHgModifyCommand() { - HgHookManager hookManager = HgTestUtil.createHookManager(); - HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager); - SimpleHgWorkingCopyFactory workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(environmentBuilder), new NoneCachingWorkingCopyPool(new WorkdirProvider())) { + SimpleHgWorkingCopyFactory workingCopyFactory = new SimpleHgWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())) { @Override public void configure(com.aragost.javahg.commands.PullCommand pullCommand) { // we do not want to configure http hooks in this unit test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgOutgoingCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgOutgoingCommandTest.java index 192b425e6c..426eff8537 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgOutgoingCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgOutgoingCommandTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -106,17 +106,10 @@ public class HgOutgoingCommandTest extends IncomingOutgoingTestBase System.out.println(cpr.getChangesets()); } - /** - * Method description - * - * - * @return - */ - private HgOutgoingCommand createOutgoingCommand() - { + private HgOutgoingCommand createOutgoingCommand() { return new HgOutgoingCommand( - new HgCommandContext( - HgTestUtil.createHookManager(), handler, outgoingRepository, - outgoingDirectory), handler); + new HgCommandContext(handler, HgTestUtil.createFactory(handler, outgoingDirectory), outgoingRepository), + handler + ); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgVersionCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgVersionCommandTest.java new file mode 100644 index 0000000000..404e004fc2 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgVersionCommandTest.java @@ -0,0 +1,137 @@ +/* + * 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.base.Joiner; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgVersion; + +import javax.annotation.Nonnull; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class HgVersionCommandTest { + + private static final String PYTHON_OUTPUT = String.join("\n", + "3.9.0 (default, Oct 27 2020, 14:15:17)", + "[Clang 12.0.0 (clang-1200.0.32.21)]" + ); + + private Map outputs; + + @BeforeEach + void setUpOutputs() { + outputs = new HashMap<>(); + } + + @Test + void shouldReturnHgVersion() throws InterruptedException { + command("/usr/local/bin/hg", HgVersionCommand.HG_ARGS, "5.5.2", 0); + command("/opt/python/bin/python", HgVersionCommand.PYTHON_ARGS, PYTHON_OUTPUT, 0); + + HgVersion hgVersion = getVersion("/usr/local/bin/hg", "/opt/python/bin/python"); + assertThat(hgVersion.getMercurial()).isEqualTo("5.5.2"); + assertThat(hgVersion.getPython()).isEqualTo("3.9.0"); + } + + @Test + void shouldReturnUnknownMercurialVersionOnNonZeroExitCode() throws InterruptedException { + command("hg", HgVersionCommand.HG_ARGS, "", 1); + command("python", HgVersionCommand.PYTHON_ARGS, PYTHON_OUTPUT, 0); + + HgVersion hgVersion = getVersion("hg", "python"); + assertThat(hgVersion.getMercurial()).isEqualTo(HgVersion.UNKNOWN); + assertThat(hgVersion.getPython()).isEqualTo("3.9.0"); + } + + @Test + void shouldReturnUnknownPythonVersionOnNonZeroExitCode() throws InterruptedException { + command("hg", HgVersionCommand.HG_ARGS, "4.4.2", 0); + command("python", HgVersionCommand.PYTHON_ARGS, "", 1); + + HgVersion hgVersion = getVersion("hg", "python"); + assertThat(hgVersion.getMercurial()).isEqualTo("4.4.2"); + assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN); + } + + @Test + void shouldReturnUnknownForInvalidPythonOutput() throws InterruptedException { + command("hg", HgVersionCommand.HG_ARGS, "1.0.0", 0); + command("python", HgVersionCommand.PYTHON_ARGS, "abcdef", 0); + + HgVersion hgVersion = getVersion("hg", "python"); + assertThat(hgVersion.getMercurial()).isEqualTo("1.0.0"); + assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN); + } + + @Test + void shouldReturnUnknownForIOException() { + HgVersionCommand command = new HgVersionCommand(new HgConfig(), cmd -> { + throw new IOException("failed"); + }); + + HgVersion hgVersion = command.get(); + assertThat(hgVersion.getMercurial()).isEqualTo(HgVersion.UNKNOWN); + assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN); + } + + private Process command(String command, String[] args, String content, int exitValue) throws InterruptedException { + Process process = mock(Process.class); + when(process.getInputStream()).thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))); + when(process.waitFor()).thenReturn(exitValue); + + List cmdLine = new ArrayList<>(); + cmdLine.add(command); + cmdLine.addAll(Arrays.asList(args)); + + outputs.put(Joiner.on(' ').join(cmdLine), process); + + return process; + } + + @Nonnull + private HgVersion getVersion(String hg, String python) { + HgConfig config = new HgConfig(); + config.setHgBinary(hg); + config.setPythonBinary(python); + return new HgVersionCommand(config, command -> outputs.get(Joiner.on(' ').join(command))).get(); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java index 6162351148..a5b103ed93 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/IncomingOutgoingTestBase.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -39,7 +39,6 @@ import org.junit.Rule; import org.junit.rules.TemporaryFolder; import sonia.scm.AbstractTestBase; import sonia.scm.repository.HgConfig; -import sonia.scm.repository.HgContext; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgTestUtil; import sonia.scm.user.User; @@ -88,7 +87,6 @@ public abstract class IncomingOutgoingTestBase extends AbstractTestBase when(handler.getDirectory(outgoingRepository.getId())).thenReturn( outgoingDirectory); when(handler.getConfig()).thenReturn(temp.getConfig()); - when(handler.getHgContext()).thenReturn(new HgContext()); } //~--- set methods ---------------------------------------------------------- diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactoryTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactoryTest.java index 8a8164e072..d429371a04 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactoryTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/SimpleHgWorkingCopyFactoryTest.java @@ -30,12 +30,11 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import sonia.scm.repository.HgHookManager; +import sonia.scm.repository.HgEnvironmentBuilder; import sonia.scm.repository.HgTestUtil; import sonia.scm.repository.work.SimpleCachingWorkingCopyPool; import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.repository.work.WorkingCopy; -import sonia.scm.web.HgRepositoryEnvironmentBuilder; import java.io.File; import java.io.IOException; @@ -57,9 +56,7 @@ public class SimpleHgWorkingCopyFactoryTest extends AbstractHgCommandTestBase { @Before public void bindScmProtocol() throws IOException { workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); - HgHookManager hookManager = HgTestUtil.createHookManager(); - HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager); - workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(environmentBuilder), new SimpleCachingWorkingCopyPool(workdirProvider)) { + workingCopyFactory = new SimpleHgWorkingCopyFactory(new SimpleCachingWorkingCopyPool(workdirProvider)) { @Override public void configure(com.aragost.javahg.commands.PullCommand pullCommand) { // we do not want to configure http hooks in this unit test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java deleted file mode 100644 index b7dd2346c5..0000000000 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java +++ /dev/null @@ -1,60 +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 org.junit.Test; -import sonia.scm.repository.HgRepositoryHandler; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static sonia.scm.web.HgHookCallbackServlet.PARAM_REPOSITORYID; - -public class HgHookCallbackServletTest { - - @Test - public void shouldExtractCorrectRepositoryId() throws ServletException, IOException { - HgRepositoryHandler handler = mock(HgRepositoryHandler.class); - HgHookCallbackServlet servlet = new HgHookCallbackServlet(null, handler, null, null); - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - - when(request.getContextPath()).thenReturn("http://example.com/scm"); - when(request.getRequestURI()).thenReturn("http://example.com/scm/hook/hg/pretxnchangegroup"); - String path = "/tmp/hg/12345"; - when(request.getParameter(PARAM_REPOSITORYID)).thenReturn(path); - - servlet.doPost(request, response); - - verify(response, never()).sendError(anyInt()); - } -} diff --git a/scm-plugins/scm-hg-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-plugins/scm-hg-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/scm-plugins/scm-integration-test-plugin/pom.xml b/scm-plugins/scm-integration-test-plugin/pom.xml index e4575edcf6..3c54fdefbb 100644 --- a/scm-plugins/scm-integration-test-plugin/pom.xml +++ b/scm-plugins/scm-integration-test-plugin/pom.xml @@ -29,12 +29,12 @@ sonia.scm.plugins scm-plugins - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-integration-test-plugin Add functions for integration tests. This is not intended for production systems. - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT smp diff --git a/scm-plugins/scm-legacy-plugin/package.json b/scm-plugins/scm-legacy-plugin/package.json index 2772a2c2b2..6d7aed0b46 100644 --- a/scm-plugins/scm-legacy-plugin/package.json +++ b/scm-plugins/scm-legacy-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-legacy-plugin", "private": true, - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "license": "MIT", "main": "./src/main/js/index.tsx", "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" } } diff --git a/scm-plugins/scm-legacy-plugin/pom.xml b/scm-plugins/scm-legacy-plugin/pom.xml index 30fe94e9a8..9b8115c172 100644 --- a/scm-plugins/scm-legacy-plugin/pom.xml +++ b/scm-plugins/scm-legacy-plugin/pom.xml @@ -29,12 +29,12 @@ sonia.scm.plugins scm-plugins - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-legacy-plugin Support migrated repository urls and v1 passwords - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT smp diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index b8dbeed962..6533d2242b 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-svn-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" } } diff --git a/scm-plugins/scm-svn-plugin/pom.xml b/scm-plugins/scm-svn-plugin/pom.xml index f127b00c6d..722755991a 100644 --- a/scm-plugins/scm-svn-plugin/pom.xml +++ b/scm-plugins/scm-svn-plugin/pom.xml @@ -31,7 +31,7 @@ scm-plugins sonia.scm.plugins - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-svn-plugin diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java index 72f7513bb8..9be64e6a3b 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; @@ -34,7 +34,7 @@ import sonia.scm.repository.Compatibility; @NoArgsConstructor @Getter @Setter -public class SvnConfigDto extends HalRepresentation { +public class SvnConfigDto extends HalRepresentation implements UpdateSvnConfigDto { private boolean disabled; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java index 3e45bb6c43..394000c48a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java @@ -21,13 +21,15 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + 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; @@ -112,7 +114,23 @@ public class SvnConfigResource { @PUT @Path("") @Consumes(SvnVndMediaType.SVN_CONFIG) - @Operation(summary = "Modify svn configuration", description = "Modifies the global subversion configuration.", tags = "Subversion", operationId = "svn_put_config") + @Operation( + summary = "Modify svn configuration", + description = "Modifies the global subversion configuration.", + tags = "Subversion", + operationId = "svn_put_config", + requestBody = @RequestBody( + content = @Content( + mediaType = SvnVndMediaType.SVN_CONFIG, + schema = @Schema(implementation = UpdateSvnConfigDto.class), + examples = @ExampleObject( + name = "Overwrites current configuration with this one.", + value = "{\n \"disabled\":false,\n \"compatibility\":\"NONE\",\n \"enabledGZip\":false\n}", + summary = "Simple update configuration" + ) + ) + ) + ) @ApiResponse( responseCode = "204", description = "update success" diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateSvnConfigDto.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateSvnConfigDto.java new file mode 100644 index 0000000000..87de7d5ec8 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/UpdateSvnConfigDto.java @@ -0,0 +1,36 @@ +/* + * 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 sonia.scm.repository.Compatibility; + +interface UpdateSvnConfigDto { + + boolean isDisabled(); + + Compatibility getCompatibility(); + + boolean isEnabledGZip(); +} diff --git a/scm-server/pom.xml b/scm-server/pom.xml index 6016d0d537..854d17d347 100644 --- a/scm-server/pom.xml +++ b/scm-server/pom.xml @@ -31,12 +31,12 @@ scm sonia.scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm scm-server - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-server jar diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 3d155f8630..fbfc202396 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -31,12 +31,12 @@ scm sonia.scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm scm-test - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-test @@ -50,7 +50,7 @@ sonia.scm scm-core - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT diff --git a/scm-ui/babel-preset/package.json b/scm-ui/babel-preset/package.json index a37d1989f0..9cfea3931b 100644 --- a/scm-ui/babel-preset/package.json +++ b/scm-ui/babel-preset/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/babel-preset", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "license": "MIT", "description": "Babel configuration for scm-manager and its plugins", "main": "index.js", diff --git a/scm-ui/e2e-tests/package.json b/scm-ui/e2e-tests/package.json index 0fbc576449..252d3dfbe7 100644 --- a/scm-ui/e2e-tests/package.json +++ b/scm-ui/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/e2e-tests", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "End to end Tests for SCM-Manager", "main": "index.js", "author": "Eduard Heimbuch ", diff --git a/scm-ui/eslint-config/package.json b/scm-ui/eslint-config/package.json index 44b318c1b0..ea2bf6c5e9 100644 --- a/scm-ui/eslint-config/package.json +++ b/scm-ui/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/eslint-config", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "ESLint configuration for scm-manager and its plugins", "main": "src/index.js", "author": "Sebastian Sdorra ", diff --git a/scm-ui/jest-preset/package.json b/scm-ui/jest-preset/package.json index de6e233d3c..86528da5f4 100644 --- a/scm-ui/jest-preset/package.json +++ b/scm-ui/jest-preset/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/jest-preset", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "Jest presets for SCM-Manager and its plugins", "main": "src/index.js", "author": "Sebastian Sdorra ", diff --git a/scm-ui/pom.xml b/scm-ui/pom.xml index 0e9633053d..7ef7daae70 100644 --- a/scm-ui/pom.xml +++ b/scm-ui/pom.xml @@ -32,13 +32,13 @@ sonia.scm scm - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT sonia.scm scm-ui war - 2.10.0-SNAPSHOT + 2.11.0-SNAPSHOT scm-ui diff --git a/scm-ui/prettier-config/package.json b/scm-ui/prettier-config/package.json index 9594fb3819..6d27467ac8 100644 --- a/scm-ui/prettier-config/package.json +++ b/scm-ui/prettier-config/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/prettier-config", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "license": "MIT", "description": "Prettier configuration", "author": "Sebastian Sdorra ", diff --git a/scm-ui/tsconfig/package.json b/scm-ui/tsconfig/package.json index 1f9230a619..bd292addca 100644 --- a/scm-ui/tsconfig/package.json +++ b/scm-ui/tsconfig/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/tsconfig", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "license": "MIT", "description": "TypeScript configuration", "author": "Sebastian Sdorra ", diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 8990acba03..0a8499a875 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-components", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "UI Components for SCM-Manager and its plugins", "main": "src/index.ts", "files": [ @@ -18,7 +18,7 @@ "update-storyshots": "jest --testPathPattern=\"storyshots.test.ts\" --collectCoverage=false -u" }, "devDependencies": { - "@scm-manager/ui-tests": "^2.10.0-SNAPSHOT", + "@scm-manager/ui-tests": "^2.11.0-SNAPSHOT", "@storybook/addon-actions": "^6.0.28", "@storybook/addon-storyshots": "^6.0.28", "@storybook/react": "^6.0.28", @@ -50,8 +50,8 @@ "worker-plugin": "^3.2.0" }, "dependencies": { - "@scm-manager/ui-extensions": "^2.10.0-SNAPSHOT", - "@scm-manager/ui-types": "^2.10.0-SNAPSHOT", + "@scm-manager/ui-extensions": "^2.11.0-SNAPSHOT", + "@scm-manager/ui-types": "^2.11.0-SNAPSHOT", "classnames": "^2.2.6", "date-fns": "^2.4.1", "gitdiff-parser": "^0.1.2", diff --git a/scm-ui/ui-components/src/BranchSelector.stories.tsx b/scm-ui/ui-components/src/BranchSelector.stories.tsx index 5abc1a2202..8f59b379f0 100644 --- a/scm-ui/ui-components/src/BranchSelector.stories.tsx +++ b/scm-ui/ui-components/src/BranchSelector.stories.tsx @@ -24,14 +24,14 @@ import { storiesOf } from "@storybook/react"; import { BranchSelector } from "./index"; -import { Branch } from "@scm-manager/ui-types/src"; +import { Branch } from "@scm-manager/ui-types"; import * as React from "react"; import styled from "styled-components"; const master = { name: "master", revision: "1", defaultBranch: true, _links: {} }; const develop = { name: "develop", revision: "2", defaultBranch: false, _links: {} }; -const branchSelected = (branch?: Branch) => {}; +const branchSelected = (branch?: Branch) => null; const branches = [master, develop]; @@ -42,6 +42,4 @@ const Wrapper = styled.div` storiesOf("BranchSelector", module) .addDecorator(storyFn => {storyFn()}) - .add("Default", () => ( - -)); + .add("Default", () => ); diff --git a/scm-ui/ui-components/src/OverviewPageActions.tsx b/scm-ui/ui-components/src/OverviewPageActions.tsx index 4cbffb32ef..633bc07d52 100644 --- a/scm-ui/ui-components/src/OverviewPageActions.tsx +++ b/scm-ui/ui-components/src/OverviewPageActions.tsx @@ -21,13 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React from "react"; -import { withRouter, RouteComponentProps } from "react-router-dom"; +import React, { FC } from "react"; +import { useHistory, useLocation } from "react-router-dom"; import classNames from "classnames"; import { Button, DropDown, urls } from "./index"; import { FilterInput } from "./forms"; -type Props = RouteComponentProps & { +type Props = { showCreateButton: boolean; currentGroup: string; groups: string[]; @@ -35,41 +35,33 @@ type Props = RouteComponentProps & { groupSelected: (namespace: string) => void; label?: string; testId?: string; + searchPlaceholder?: string; }; -class OverviewPageActions extends React.Component { - render() { - const { history, currentGroup, groups, location, link, testId, groupSelected } = this.props; - const groupSelector = groups && ( -
- -
- ); +const OverviewPageActions: FC = ({ + groups, + currentGroup, + showCreateButton, + link, + groupSelected, + label, + testId, + searchPlaceholder +}) => { + const history = useHistory(); + const location = useLocation(); + const groupSelector = groups && ( +
+ +
+ ); - return ( -
- {groupSelector} -
- { - history.push(`/${link}/?q=${filter}`); - }} - testId={testId + "-filter"} - /> -
- {this.renderCreateButton()} -
- ); - } - - renderCreateButton() { - const { showCreateButton, link, label } = this.props; + const renderCreateButton = () => { if (showCreateButton) { return (
@@ -78,7 +70,24 @@ class OverviewPageActions extends React.Component { ); } return null; - } -} + }; -export default withRouter(OverviewPageActions); + return ( +
+ {groupSelector} +
+ { + history.push(`/${link}/?q=${filter}`); + }} + testId={testId + "-filter"} + /> +
+ {renderCreateButton()} +
+ ); +}; + +export default OverviewPageActions; diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index f2fd0d93d8..eb97f1ef92 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -50439,36 +50439,70 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = ` href="/repo/hitchhiker/heartOfGold/branches/" onClick={[Function]} > - + + + + + + + + - + + + - + + + - + + +
{ }; renderBranchesLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; if (repository._links["branches"]) { - return ; + return ( + + ); + } + return null; + }; + + renderTagsLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; + if (repository._links["tags"]) { + return ( + + ); } return null; }; renderChangesetsLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; if (repository._links["changesets"]) { - return ; + return ( + + ); } return null; }; renderSourcesLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; if (repository._links["sources"]) { - return ; + return ( + + ); } return null; }; renderModifyLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; if (repository._links["update"]) { - return ; + return ( + + ); } return null; }; @@ -74,6 +113,7 @@ class RepositoryEntry extends React.Component { return ( <> {this.renderBranchesLink(repository, repositoryLink)} + {this.renderTagsLink(repository, repositoryLink)} {this.renderChangesetsLink(repository, repositoryLink)} {this.renderSourcesLink(repository, repositoryLink)} @@ -118,4 +158,4 @@ class RepositoryEntry extends React.Component { } } -export default RepositoryEntry; +export default withTranslation("repos")(RepositoryEntry); diff --git a/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx b/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx index f52859c15e..a0adaba7a1 100644 --- a/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx +++ b/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx @@ -25,10 +25,12 @@ import React from "react"; import { Link } from "react-router-dom"; import styled from "styled-components"; import { Icon } from "@scm-manager/ui-components"; +import Tooltip from "../Tooltip"; type Props = { to: string; icon: string; + tooltip?: string; }; const PointerEventsLink = styled(Link)` @@ -37,10 +39,20 @@ const PointerEventsLink = styled(Link)` class RepositoryEntryLink extends React.Component { render() { - const { to, icon } = this.props; + const { to, icon, tooltip } = this.props; + + let content = ; + if (tooltip) { + content = ( + + {content} + + ); + } + return ( - + {content} ); } diff --git a/scm-ui/ui-extensions/package.json b/scm-ui/ui-extensions/package.json index 64ee6367cc..4d9647eefe 100644 --- a/scm-ui/ui-extensions/package.json +++ b/scm-ui/ui-extensions/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-extensions", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "main": "src/index.ts", "license": "MIT", "private": false, diff --git a/scm-ui/ui-plugins/package.json b/scm-ui/ui-plugins/package.json index 97a840f7c8..2dfc6f60da 100644 --- a/scm-ui/ui-plugins/package.json +++ b/scm-ui/ui-plugins/package.json @@ -1,13 +1,13 @@ { "name": "@scm-manager/ui-plugins", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "license": "MIT", "bin": { "ui-plugins": "./bin/ui-plugins.js" }, "dependencies": { - "@scm-manager/ui-components": "^2.10.0-SNAPSHOT", - "@scm-manager/ui-extensions": "^2.10.0-SNAPSHOT", + "@scm-manager/ui-components": "^2.11.0-SNAPSHOT", + "@scm-manager/ui-extensions": "^2.11.0-SNAPSHOT", "classnames": "^2.2.6", "query-string": "^5.0.1", "react": "^16.10.2", @@ -18,14 +18,14 @@ "styled-components": "^5.1.0" }, "devDependencies": { - "@scm-manager/babel-preset": "^2.10.0-SNAPSHOT", - "@scm-manager/eslint-config": "^2.10.0-SNAPSHOT", - "@scm-manager/jest-preset": "^2.10.0-SNAPSHOT", - "@scm-manager/prettier-config": "^2.10.0-SNAPSHOT", - "@scm-manager/tsconfig": "^2.10.0-SNAPSHOT", - "@scm-manager/ui-scripts": "^2.10.0-SNAPSHOT", - "@scm-manager/ui-tests": "^2.10.0-SNAPSHOT", - "@scm-manager/ui-types": "^2.10.0-SNAPSHOT", + "@scm-manager/babel-preset": "^2.11.0-SNAPSHOT", + "@scm-manager/eslint-config": "^2.11.0-SNAPSHOT", + "@scm-manager/jest-preset": "^2.11.0-SNAPSHOT", + "@scm-manager/prettier-config": "^2.11.0-SNAPSHOT", + "@scm-manager/tsconfig": "^2.11.0-SNAPSHOT", + "@scm-manager/ui-scripts": "^2.11.0-SNAPSHOT", + "@scm-manager/ui-tests": "^2.11.0-SNAPSHOT", + "@scm-manager/ui-types": "^2.11.0-SNAPSHOT", "@types/classnames": "^2.2.9", "@types/enzyme": "^3.10.3", "@types/fetch-mock": "^7.3.1", diff --git a/scm-ui/ui-polyfill/package.json b/scm-ui/ui-polyfill/package.json index fd8b63aa29..70b9955dd7 100644 --- a/scm-ui/ui-polyfill/package.json +++ b/scm-ui/ui-polyfill/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-polyfill", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "Polyfills for SCM-Manager UI", "main": "src/index.js", "author": "Sebastian Sdorra ", diff --git a/scm-ui/ui-scripts/package.json b/scm-ui/ui-scripts/package.json index 57ecfd1ac4..07096ea1d3 100644 --- a/scm-ui/ui-scripts/package.json +++ b/scm-ui/ui-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-scripts", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "Build scripts for SCM-Manager", "main": "src/index.js", "author": "Sebastian Sdorra ", diff --git a/scm-ui/ui-styles/package.json b/scm-ui/ui-styles/package.json index 0c0df6b16b..376aff3c95 100644 --- a/scm-ui/ui-styles/package.json +++ b/scm-ui/ui-styles/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-styles", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "Styles for SCM-Manager", "main": "src/scm.scss", "license": "MIT", diff --git a/scm-ui/ui-tests/package.json b/scm-ui/ui-tests/package.json index fcbf2c137b..639d5872e7 100644 --- a/scm-ui/ui-tests/package.json +++ b/scm-ui/ui-tests/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-tests", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "UI-Tests helpers", "author": "Sebastian Sdorra ", "license": "MIT", diff --git a/scm-ui/ui-types/package.json b/scm-ui/ui-types/package.json index e420646d82..729d5b8c55 100644 --- a/scm-ui/ui-types/package.json +++ b/scm-ui/ui-types/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-types", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "description": "Flow types for SCM-Manager related Objects", "main": "src/index.ts", "files": [ diff --git a/scm-ui/ui-types/src/Branches.ts b/scm-ui/ui-types/src/Branches.ts index 87eb5af3dc..949f7b40bc 100644 --- a/scm-ui/ui-types/src/Branches.ts +++ b/scm-ui/ui-types/src/Branches.ts @@ -28,6 +28,8 @@ export type Branch = { name: string; revision: string; defaultBranch?: boolean; + lastCommitDate?: string; + stale?: boolean; _links: Links; }; diff --git a/scm-ui/ui-webapp/package.json b/scm-ui/ui-webapp/package.json index fd43d89102..67b92ecfd2 100644 --- a/scm-ui/ui-webapp/package.json +++ b/scm-ui/ui-webapp/package.json @@ -1,10 +1,10 @@ { "name": "@scm-manager/ui-webapp", - "version": "2.10.0-SNAPSHOT", + "version": "2.11.0-SNAPSHOT", "private": true, "dependencies": { - "@scm-manager/ui-components": "^2.10.0-SNAPSHOT", - "@scm-manager/ui-extensions": "^2.10.0-SNAPSHOT", + "@scm-manager/ui-components": "^2.11.0-SNAPSHOT", + "@scm-manager/ui-extensions": "^2.11.0-SNAPSHOT", "classnames": "^2.2.5", "history": "^4.10.1", "i18next": "^19.6.0", @@ -29,7 +29,7 @@ "test": "jest" }, "devDependencies": { - "@scm-manager/ui-tests": "^2.10.0-SNAPSHOT", + "@scm-manager/ui-tests": "^2.11.0-SNAPSHOT", "@types/classnames": "^2.2.9", "@types/enzyme": "^3.10.3", "@types/fetch-mock": "^7.3.1", diff --git a/scm-ui/ui-webapp/public/locales/de/groups.json b/scm-ui/ui-webapp/public/locales/de/groups.json index 65035781b6..a351a3eda1 100644 --- a/scm-ui/ui-webapp/public/locales/de/groups.json +++ b/scm-ui/ui-webapp/public/locales/de/groups.json @@ -25,6 +25,9 @@ "setPermissionsNavLink": "Berechtigungen" } }, + "overview": { + "searchGroup": "Gruppe suchen" + }, "add-group": { "title": "Gruppe erstellen", "subtitle": "Erstellen einer neuen Gruppe" diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 78371d3e9b..911d6a4bb4 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -36,13 +36,22 @@ "settingsNavLink": "Einstellungen", "generalNavLink": "Generell", "permissionsNavLink": "Berechtigungen" + }, + "tooltip": { + "branches": "Branches", + "tags": "Tags", + "commits": "Commits", + "sources": "Sources", + "settings": "Einstellungen" } }, "overview": { "title": "Repositories", "subtitle": "Übersicht aller verfügbaren Repositories", "noRepositories": "Keine Repositories gefunden.", - "createButton": "Repository erstellen" + "createButton": "Repository erstellen", + "searchRepository": "Repository suchen", + "allNamespaces": "Alle Namespaces" }, "create": { "title": "Repository erstellen", @@ -55,7 +64,11 @@ "createButton": "Branch erstellen" }, "table": { - "branches": "Branches" + "branches": { + "active": "Aktive Branches", + "stale": "Stale Branches" + }, + "lastCommit": "Letzter Commit" }, "create": { "title": "Branch erstellen", diff --git a/scm-ui/ui-webapp/public/locales/de/users.json b/scm-ui/ui-webapp/public/locales/de/users.json index 80d251e6b3..cb2a599aac 100644 --- a/scm-ui/ui-webapp/public/locales/de/users.json +++ b/scm-ui/ui-webapp/public/locales/de/users.json @@ -30,6 +30,9 @@ "noUsers": "Keine Benutzer gefunden.", "createButton": "Benutzer erstellen" }, + "overview": { + "searchUser": "Benutzer suchen" + }, "singleUser": { "errorTitle": "Fehler", "errorSubtitle": "Unbekannter Benutzer Fehler", @@ -76,15 +79,22 @@ } }, "publicKey": { + "subtitle": "Öffentliche Schlüssel", + "description": "Zum Prüfen von Signaturen für z. B. Commits können hier die entsprechenden öffentlichen GPG Schlüssel hinterlegt werden. Zudem können hier die vom SCM-Manager erstellten Signaturschlüssel heruntergeladen werden.", "noStoredKeys": "Es wurden keine Schlüssel gefunden.", "displayName": "Anzeigename", - "raw": "Schlüssel", "created": "Eingetragen an", - "addKey": "Schlüssel hinzufügen", + "raw": "Schlüssel", + "download": "Herunterladen", "delete": "Löschen", - "download": "Herunterladen" + "addSubtitle": "Neuen Schlüssel hinzufügen", + "addKey": "Schlüssel hinzufügen" }, "apiKey": { + "subtitle": "API Schlüssel", + "text1": "Erstelle und verwalte Personal Access Token um auf die REST API zuzugreifen oder diese als Passwort für SCM-Clients zu nutzen. Die Rechte der Token sind auf Repositories und die gewählte Rolle beschränkt.", + "manageRoles": "Sie können die Rollenberechtigungen in der Administration unter „Berechtigungsrollen“ einsehen und neue Rollen anlegen.", + "text2": "Um den Token in REST-Abfragen zu nutzen, übergeben Sie diesen als Cookie mit dem Namen „X-Bearer-Token“. Sie können den Token auch anstelle Ihres Passworts nutzen, um sich mit SCM-Clients anzumelden.", "noStoredKeys": "Es wurden keine Schlüssel gefunden.", "displayName": "Anzeigename", "permissionRole": { @@ -92,12 +102,10 @@ "help": "Mit der Rolle können Sie die Berechtigung für diesen Schlüssel einschränken" }, "created": "Eingetragen an", + "addSubtitle": "Neuen Schlüssel hinzufügen", "addKey": "Schlüssel hinzufügen", "delete": "Löschen", "download": "Herunterladen", - "text1": "Erstelle und verwalte Personal Access Token um auf die REST API zuzugreifen oder diese als Passwort für SCM-Clients zu nutzen. Die Rechte der Token sind auf Repositories und die gewählte Rolle beschränkt.", - "manageRoles": "Sie können die Rollenberechtigungen in der Administration unter „Berechtigungsrollen“ einsehen und neue Rollen anlegen.", - "text2": "Um den Token in REST-Abfragen zu nutzen, übergeben Sie diesen als Cookie mit dem Namen „X-Bearer-Token“. Sie können den Token auch anstelle Ihres Passworts nutzen, um sich mit SCM-Clients anzumelden.", "modal": { "title": "Schlüssel erzeugt", "text1": "Ihr neuer API-Schlüssel ist bereit. Sie können diesen als Token für Zugriffe auf die REST-Schnittstelle nutzen oder anstelle Ihres Passworts zum Login mit SCM-Clients nutzen.", diff --git a/scm-ui/ui-webapp/public/locales/en/groups.json b/scm-ui/ui-webapp/public/locales/en/groups.json index 069013e5d0..944d271071 100644 --- a/scm-ui/ui-webapp/public/locales/en/groups.json +++ b/scm-ui/ui-webapp/public/locales/en/groups.json @@ -25,6 +25,9 @@ "setPermissionsNavLink": "Permissions" } }, + "overview": { + "searchGroup": "Search group" + }, "add-group": { "title": "Create Group", "subtitle": "Create a new group" diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index c789a3253e..b1d87edb6b 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -36,13 +36,22 @@ "settingsNavLink": "Settings", "generalNavLink": "General", "permissionsNavLink": "Permissions" + }, + "tooltip": { + "branches": "Branches", + "tags": "Tags", + "commits": "Commits", + "sources": "Sources", + "settings": "Settings" } }, "overview": { "title": "Repositories", "subtitle": "Overview of available repositories", "noRepositories": "No repositories found.", - "createButton": "Create Repository" + "createButton": "Create Repository", + "searchRepository": "Search repository", + "allNamespaces": "All namespaces" }, "create": { "title": "Create Repository", @@ -55,7 +64,11 @@ "createButton": "Create Branch" }, "table": { - "branches": "Branches" + "branches": { + "active": "Active Branches", + "stale": "Stale Branches" + }, + "lastCommit": "Last commit" }, "create": { "title": "Create Branch", diff --git a/scm-ui/ui-webapp/public/locales/en/users.json b/scm-ui/ui-webapp/public/locales/en/users.json index 627e2b80aa..eb145f440e 100644 --- a/scm-ui/ui-webapp/public/locales/en/users.json +++ b/scm-ui/ui-webapp/public/locales/en/users.json @@ -44,6 +44,9 @@ "setApiKeyNavLink": "API Keys" } }, + "overview": { + "searchUser": "Search user" + }, "createUser": { "title": "Create User", "subtitle": "Create a new user" @@ -76,15 +79,22 @@ } }, "publicKey": { + "subtitle": "Public Keys", + "description": "To check signatures (for example for commits), gpg public keys can be stored here. Additionally the keys created by SCM-Manager can be accessed here, too.", "noStoredKeys": "No keys found.", "displayName": "Display Name", - "raw": "Key", "created": "Created on", - "addKey": "Add key", + "raw": "Key", + "download": "Download", "delete": "Delete", - "download": "Download" + "addSubtitle": "Add new key", + "addKey": "Add key" }, "apiKey": { + "subtitle": "API Keys", + "text1": "Create and manage personal access tokens to access the REST API or use as a password for SCM clients. The privileges of these tokens are limited to repositories and the selected role.", + "manageRoles": "You may view and create roles in the administration view “Permission Roles”.", + "text2": "To use the token in a REST request, pass it as a cookie named “X-Bearer-Token”. You may use the token as your password for SCM clients, too.", "noStoredKeys": "No keys found.", "displayName": "Display Name", "permissionRole": { @@ -92,12 +102,10 @@ "help": "The api key will be restricted to permissions of this role" }, "created": "Created on", + "addSubtitle": "Add new key", "addKey": "Add key", "delete": "Delete", "download": "Download", - "text1": "Create and manage personal access tokens to access the REST API or use as a password for SCM clients. The privileges of these tokens are limited to repositories and the selected role.", - "manageRoles": "You may view and create roles in the administration view “Permission Roles”.", - "text2": "To use the token in a REST request, pass it as a cookie named “X-Bearer-Token”. You may use the token as your password for SCM clients, too.", "modal": { "title": "Key created", "text1": "Your new API key is ready. You can use it as a bearer token for REST calls or as a password for SCM clients.", diff --git a/scm-ui/ui-webapp/src/groups/containers/Groups.tsx b/scm-ui/ui-webapp/src/groups/containers/Groups.tsx index 7fe45d0163..2584239ce0 100644 --- a/scm-ui/ui-webapp/src/groups/containers/Groups.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/Groups.tsx @@ -92,7 +92,12 @@ class Groups extends React.Component { {this.renderGroupTable()} {this.renderCreateButton()} - + ); diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx index 42bfd781b1..224187401f 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx @@ -26,21 +26,45 @@ import { Branch, Repository } from "@scm-manager/ui-types"; import { WithTranslation, withTranslation } from "react-i18next"; import BranchButtonGroup from "./BranchButtonGroup"; import DefaultBranchTag from "./DefaultBranchTag"; +import { DateFromNow } from "@scm-manager/ui-components"; +import styled from "styled-components"; type Props = WithTranslation & { repository: Repository; branch: Branch; }; +const FlexRow = styled.div` + display: flex; + align-items: center; + flex-wrap: wrap; +`; + +const Created = styled.div` + margin-left: 0.5rem; + font-size: 0.8rem; +`; + +const Label = styled.strong` + margin-right: 0.3rem; +`; + +const Date = styled(DateFromNow)` + font-size: 0.8rem; +`; + class BranchDetail extends React.Component { render() { const { repository, branch, t } = this.props; return (
-
- {t("branch.name")} {branch.name} -
+ + {branch.name} + + {t("tags.overview.created")} + +
diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx index 1acaa78159..b13e0d9893 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx @@ -25,8 +25,9 @@ import React, { FC } from "react"; import { Link as ReactLink } from "react-router-dom"; import { Branch, Link } from "@scm-manager/ui-types"; import DefaultBranchTag from "./DefaultBranchTag"; -import { Icon } from "@scm-manager/ui-components"; +import { DateFromNow, Icon } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; +import styled from "styled-components"; type Props = { baseUrl: string; @@ -34,6 +35,11 @@ type Props = { onDelete: (branch: Branch) => void; }; +const Created = styled.span` + margin-left: 1rem; + font-size: 0.8rem; +`; + const BranchRow: FC = ({ baseUrl, branch, onDelete }) => { const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`; const [t] = useTranslation("repos"); @@ -56,6 +62,11 @@ const BranchRow: FC = ({ baseUrl, branch, onDelete }) => { {branch.name} + {branch.lastCommitDate && ( + + {t("branches.table.lastCommit")} + + )} {deleteButton} diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx index 312275b663..f78fc5a447 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx @@ -30,10 +30,11 @@ import { apiClient, ConfirmAlert, ErrorNotification } from "@scm-manager/ui-comp type Props = { baseUrl: string; branches: Branch[]; + type: string; fetchBranches: () => void; }; -const BranchTable: FC = ({ baseUrl, branches, fetchBranches }) => { +const BranchTable: FC = ({ baseUrl, branches, type, fetchBranches }) => { const [t] = useTranslation("repos"); const [showConfirmAlert, setShowConfirmAlert] = useState(false); const [error, setError] = useState(); @@ -92,7 +93,7 @@ const BranchTable: FC = ({ baseUrl, branches, fetchBranches }) => { - + {renderRow()} diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx index 8bbcac53bf..37ff8cf327 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx @@ -84,7 +84,28 @@ class BranchesOverview extends React.Component { const { baseUrl, branches, repository, fetchBranches, t } = this.props; if (branches && branches.length > 0) { orderBranches(branches); - return fetchBranches(repository)} />; + const staleBranches = branches.filter(b => b.stale); + const activeBranches = branches.filter(b => !b.stale); + return ( + <> + {activeBranches.length > 0 && ( + fetchBranches(repository)} + /> + )} + {staleBranches.length > 0 && ( + fetchBranches(repository)} + /> + )} + + ); } return {t("branches.overview.noBranches")}; } diff --git a/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.test.ts b/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.test.ts index afdd325a90..13024ace8a 100644 --- a/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.test.ts +++ b/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.test.ts @@ -47,9 +47,14 @@ const developBranch = { revision: "revision5", defaultBranch: false }; +const mainBranch = { + name: "main", + revision: "revision6", + defaultBranch: false +}; const masterBranch = { name: "master", - revision: "revision6", + revision: "revision7", defaultBranch: false }; @@ -66,10 +71,10 @@ describe("order branches", () => { expect(branches).toEqual([branch3, branch1, branch2]); }); - it("should order special branches as follows: master > default > develop", () => { - const branches = [defaultBranch, developBranch, masterBranch]; + it("should order special branches as follows: main > master > default > develop", () => { + const branches = [defaultBranch, mainBranch, developBranch, masterBranch]; orderBranches(branches); - expect(branches).toEqual([masterBranch, defaultBranch, developBranch]); + expect(branches).toEqual([mainBranch, masterBranch, defaultBranch, developBranch]); }); it("should order special branches but starting with defaultBranch", () => { diff --git a/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.ts b/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.ts index 69102d5d88..827d791fbe 100644 --- a/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.ts +++ b/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.ts @@ -32,10 +32,14 @@ export function orderBranches(branches: Branch[]) { return -20; } else if (!a.defaultBranch && b.defaultBranch) { return 20; - } else if (a.name === "master" && b.name !== "master") { + } else if (a.name === "main" && b.name !== "main") { return -10; - } else if (a.name !== "master" && b.name === "master") { + } else if (a.name !== "main" && b.name === "main") { return 10; + } else if (a.name === "master" && b.name !== "master") { + return -9; + } else if (a.name !== "master" && b.name === "master") { + return 9; } else if (a.name === "default" && b.name !== "default") { return -10; } else if (a.name !== "default" && b.name === "default") { diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx b/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx index 936c5c2ebb..a2c342f51f 100644 --- a/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx +++ b/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx @@ -23,21 +23,25 @@ */ import React from "react"; import { Link } from "react-router-dom"; -import { CardColumnGroup, RepositoryEntry } from "@scm-manager/ui-components"; +import { CardColumnGroup, Icon, RepositoryEntry } from "@scm-manager/ui-components"; import { RepositoryGroup } from "@scm-manager/ui-types"; -import { Icon } from "@scm-manager/ui-components"; import { WithTranslation, withTranslation } from "react-i18next"; +import styled from "styled-components"; type Props = WithTranslation & { group: RepositoryGroup; }; +const SizedIcon = styled(Icon)` + font-size: 1.33rem; +`; + class RepositoryGroupEntry extends React.Component { render() { const { group, t } = this.props; const settingsLink = group.namespace?._links?.permissions && ( - + ); const namespaceHeader = ( diff --git a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx index 0dae2f03d9..de5be31591 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx @@ -101,8 +101,12 @@ class Overview extends React.Component { } }; + getNamespaceFilterPlaceholder = () => { + return this.props.t("overview.allNamespaces"); + }; + namespaceSelected = (newNamespace: string) => { - if (newNamespace === "") { + if (newNamespace === this.getNamespaceFilterPlaceholder()) { this.props.history.push("/repos/"); } else { this.props.history.push(`/repos/${newNamespace}/`); @@ -111,8 +115,10 @@ class Overview extends React.Component { render() { const { error, loading, showCreateButton, namespace, namespaces, t } = this.props; - - const namespacesToRender = namespaces ? ["", ...namespaces._embedded.namespaces.map(n => n.namespace).sort()] : []; + const namespaceFilterPlaceholder = this.getNamespaceFilterPlaceholder(); + const namespacesToRender = namespaces + ? [namespaceFilterPlaceholder, ...namespaces._embedded.namespaces.map(n => n.namespace).sort()] + : []; return ( @@ -126,6 +132,7 @@ class Overview extends React.Component { link="repos" label={t("overview.createButton")} testId="repository-overview" + searchPlaceholder={t("overview.searchRepository")} /> diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx index 216f33d425..fdbda0903b 100644 --- a/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx @@ -23,7 +23,7 @@ */ import React, { FC, useEffect, useState } from "react"; -import { apiClient, ErrorNotification, InputField, Level, Loading, SubmitButton } from "@scm-manager/ui-components"; +import { apiClient, ErrorNotification, InputField, Level, Loading, SubmitButton, Subtitle } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; import { CONTENT_TYPE_API_KEY } from "./SetApiKeys"; import { connect } from "react-redux"; @@ -105,6 +105,8 @@ const AddApiKey: FC = ({ return ( <> +
+ {newKeyModal} = ({ apiKey, onDelete }) => { if (apiKey?._links?.delete) { deleteButton = ( onDelete((apiKey._links.delete as Link).href)}> - - + + ); @@ -52,7 +52,7 @@ export const ApiKeyEntry: FC = ({ apiKey, onDelete }) => {
diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx index 8d80add477..7b9af8a415 100644 --- a/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx @@ -25,11 +25,10 @@ import { Collection, Links, User, Me } from "@scm-manager/ui-types"; import React, { FC, useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components"; +import { apiClient, ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components"; import ApiKeyTable from "./ApiKeyTable"; import AddApiKey from "./AddApiKey"; import { useTranslation } from "react-i18next"; -import styled from "styled-components"; export type ApiKeysCollection = Collection & { _embedded: { @@ -51,10 +50,6 @@ type Props = { user: User | Me; }; -const Subtitle = styled.div` - margin-bottom: 1rem; -`; - const SetApiKeys: FC = ({ user }) => { const [t] = useTranslation("users"); const [error, setError] = useState(); @@ -94,14 +89,13 @@ const SetApiKeys: FC = ({ user }) => { return ( <> -
-

{t("apiKey.text1")} {t("apiKey.manageRoles")}

-

{t("apiKey.text2")}

-
-
+ +

+ {t("apiKey.text1")} {t("apiKey.manageRoles")} +

+

{t("apiKey.text2")}

+
-
-

Create new key

{createLink && } ); diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx index e0129e8245..c693d89130 100644 --- a/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx +++ b/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx @@ -23,7 +23,6 @@ */ import React, { FC, useState } from "react"; -import { User, Link, Links, Collection } from "@scm-manager/ui-types/src"; import { ErrorNotification, InputField, @@ -31,7 +30,8 @@ import { Textarea, SubmitButton, apiClient, - Loading + Loading, + Subtitle } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; import { CONTENT_TYPE_PUBLIC_KEY } from "./SetPublicKeys"; @@ -77,6 +77,8 @@ const AddPublicKey: FC = ({ createLink, refresh }) => { return ( <> +
+
{t("branches.table.branches")}{t(`branches.table.branches.${type}`)}
{apiKey.displayName} {apiKey.permissionRole} - + {deleteButton}