diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8114275b..dd10c59fca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed restart with deb or rpm installation ([#1222](https://github.com/scm-manager/scm-manager/issues/1222) and [#1227](https://github.com/scm-manager/scm-manager/pull/1227)) - Fixed broken migration with empty security.xml ([#1219](https://github.com/scm-manager/scm-manager/issues/1219) and [#1221](https://github.com/scm-manager/scm-manager/pull/1221)) - Added missing architecture to debian installation documentation ([#1230](https://github.com/scm-manager/scm-manager/pull/1230)) +- Mercurial on Python 3 ([#1232](https://github.com/scm-manager/scm-manager/pull/1232)) - Fixed wrong package information for deb and rpm packages ([#1229](https://github.com/scm-manager/scm-manager/pull/1229)) - ## [2.1.1] - 2020-06-23 ### Fixed - Wait until recommended java installation is available for deb packages ([#1209](https://github.com/scm-manager/scm-manager/pull/1209)) diff --git a/scm-packaging/docker/Dockerfile b/scm-packaging/docker/Dockerfile index 86081a941d..b1ec5d7f6d 100644 --- a/scm-packaging/docker/Dockerfile +++ b/scm-packaging/docker/Dockerfile @@ -22,20 +22,6 @@ # SOFTWARE. # -# we need to build mercurial by ourself, -# because we need mercurial with python 2 for now -# TODO remove if python3 support is available for scm-manager hg extensions -FROM adoptopenjdk/openjdk11:jdk-11.0.7_10-alpine-slim as build -WORKDIR /src -RUN apk add --upgrade alpine-sdk python2 python2-dev -RUN set -x \ - && wget https://www.mercurial-scm.org/release/mercurial-4.9.1.tar.gz \ - && tar xvfz mercurial-4.9.1.tar.gz \ - && rm -f mercurial-4.9.1.tar.gz -WORKDIR /src/mercurial-4.9.1 -RUN make build -RUN make PREFIX=/usr install-bin - FROM adoptopenjdk/openjdk11:jdk-11.0.7_10-alpine-slim ENV SCM_HOME=/var/lib/scm ENV CACHE_DIR=/var/cache/scm/work @@ -43,17 +29,13 @@ ENV CACHE_DIR=/var/cache/scm/work COPY . / RUN set -x \ - && apk add --no-cache python2 bash ca-certificates \ + && apk add --no-cache mercurial bash ca-certificates \ && addgroup -S -g 1000 scm \ && adduser -S -s /bin/false -G scm -h ${SCM_HOME} -D -H -u 1000 scm \ && mkdir -p ${SCM_HOME} ${CACHE_DIR} \ && chmod +x /opt/scm-server/bin/scm-server \ && chown scm:scm ${SCM_HOME} ${CACHE_DIR} -# copy mercurial installation -COPY --from=build /usr/bin/hg /usr/bin/hg -COPY --from=build /usr/lib/python2.7 /usr/lib/python2.7 - WORKDIR "/opt/scm-server" VOLUME ["${SCM_HOME}", "${CACHE_DIR}"] EXPOSE 8080 diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index dbf85955d8..b2b55e0ffc 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -44,7 +44,7 @@ com.aragost.javahg javahg - 0.14 + 0.15-scm1 com.google.guava diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/autoconfig/AutoConfigurator.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/autoconfig/AutoConfigurator.java new file mode 100644 index 0000000000..534166b4ad --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/autoconfig/AutoConfigurator.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.autoconfig; + +import sonia.scm.Platform; +import sonia.scm.repository.HgConfig; +import sonia.scm.util.SystemUtil; + +import java.nio.file.Path; +import java.util.Optional; + +public interface AutoConfigurator { + + HgConfig configure(); + + HgConfig configure(Path hg); + + static Optional get() { + // at the moment we have only support for posix based systems + Platform platform = SystemUtil.getPlatform(); + if (platform.isPosix()) { + return Optional.of(new PosixAutoConfigurator(System.getenv())); + } + return Optional.empty(); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/autoconfig/PosixAutoConfigurator.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/autoconfig/PosixAutoConfigurator.java new file mode 100644 index 0000000000..ff27fa6af1 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/autoconfig/PosixAutoConfigurator.java @@ -0,0 +1,212 @@ +/* + * 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.autoconfig; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.io.MoreFiles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.HgConfig; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class PosixAutoConfigurator implements AutoConfigurator { + + private static final Logger LOG = LoggerFactory.getLogger(PosixAutoConfigurator.class); + + private static final List ADDITIONAL_PATH = ImmutableList.of( + "/usr/bin", + "/usr/local/bin", + "/opt/local/bin" + ); + + private final Set fsPaths; + + private Executor executor = (Path binary, String... args) -> { + ProcessBuilder builder = new ProcessBuilder( + Lists.asList(binary.toString(), args).toArray(new String[0]) + ); + Process process = builder.start(); + int rc = process.waitFor(); + if (rc != 0) { + throw new IOException(binary.toString() + " failed with return code " + rc); + } + return process.getInputStream(); + }; + + PosixAutoConfigurator(Map env) { + this(env, ADDITIONAL_PATH); + } + + PosixAutoConfigurator(Map env, List additionalPaths) { + String path = env.getOrDefault("PATH", ""); + fsPaths = new LinkedHashSet<>(); + fsPaths.addAll(Splitter.on(File.pathSeparator).splitToList(path)); + fsPaths.addAll(additionalPaths); + } + + @VisibleForTesting + void setExecutor(Executor executor) { + this.executor = executor; + } + + @Override + public HgConfig configure() { + Optional hg = findInPath("hg"); + if (hg.isPresent()) { + return configure(hg.get()); + } + return new HgConfig(); + } + + private Optional findInPath(String binary) { + for (String directory : fsPaths) { + Path binaryPath = Paths.get(directory, binary); + if (Files.exists(binaryPath)) { + return Optional.of(binaryPath); + } + } + return Optional.empty(); + } + + + private Optional findModulePath(Path hg) { + if (!Files.isExecutable(hg)) { + LOG.warn("{} is not executable", hg); + return Optional.empty(); + } + try { + InputStream debuginstall = executor.execute(hg, "debuginstall"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(debuginstall))) { + while (reader.ready()) { + String line = reader.readLine(); + if (line.contains("installed modules")) { + int start = line.indexOf("("); + int end = line.indexOf(")"); + Path modulePath = Paths.get(line.substring(start + 1, end)); + if (Files.exists(modulePath)) { + // installed modules contains the path to the mercurial module, + // but we need the parent for the python path + return Optional.of(modulePath.getParent()); + } else { + LOG.warn("could not find module path at {}", modulePath); + } + } + } + } + } catch (IOException ex) { + LOG.warn("failed to parse debuginstall of {}", hg); + } catch (InterruptedException e) { + LOG.warn("interrupted during debuginstall parsing of {}", hg); + Thread.currentThread().interrupt(); + } + return Optional.empty(); + } + + @Override + public HgConfig configure(Path hg) { + HgConfig config = new HgConfig(); + try { + if (Files.exists(hg)) { + configureWithExistingHg(hg, config); + } else { + LOG.warn("{} does not exists", hg); + } + } catch (IOException e) { + LOG.warn("failed to read first line of {}", hg); + } + return config; + } + + private void configureWithExistingHg(Path hg, HgConfig config) throws IOException { + config.setHgBinary(hg.toAbsolutePath().toString()); + Optional pythonFromShebang = findPythonFromShebang(hg); + if (pythonFromShebang.isPresent()) { + config.setPythonBinary(pythonFromShebang.get().toAbsolutePath().toString()); + } else { + LOG.warn("could not find python from shebang, searching for python in path"); + Optional python = findInPath("python"); + if (!python.isPresent()) { + LOG.warn("could not find python in path, searching for python3 instead"); + python = findInPath("python3"); + } + if (python.isPresent()) { + config.setPythonBinary(python.get().toAbsolutePath().toString()); + } else { + LOG.warn("could not find python in path"); + } + } + + Optional modulePath = findModulePath(hg); + if (modulePath.isPresent()) { + config.setPythonPath(modulePath.get().toAbsolutePath().toString()); + } else { + LOG.warn("could not find module path"); + } + + } + + private Optional findPythonFromShebang(Path hg) throws IOException { + String shebang = MoreFiles.asCharSource(hg, StandardCharsets.UTF_8).readFirstLine(); + if (shebang != null && shebang.startsWith("#!")) { + String substring = shebang.substring(2); + String[] parts = substring.split("\\s+"); + if (parts.length > 1) { + return findInPath(parts[1]); + } else { + Path python = Paths.get(parts[0]); + if (Files.exists(python)) { + return Optional.of(python); + } else { + LOG.warn("python binary from shebang {} does not exists", python); + } + } + } else { + LOG.warn("first line does not look like a shebang: {}", shebang); + } + return Optional.empty(); + } + + @FunctionalInterface + interface Executor { + InputStream execute(Path binary, String... args) throws IOException, InterruptedException; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/MacOSHgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/MacOSHgInstaller.java index 2d33eb9f15..52a48d1cad 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/MacOSHgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/MacOSHgInstaller.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.installer; //~--- non-JDK imports -------------------------------------------------------- @@ -40,7 +40,9 @@ import java.io.IOException; /** * * @author Sebastian Sdorra + * @deprecated use {@link sonia.scm.autoconfig.AutoConfigurator} */ +@Deprecated public class MacOSHgInstaller extends UnixHgInstaller { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/UnixHgInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/UnixHgInstaller.java index 470f53de5c..ba65b943e8 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/UnixHgInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/UnixHgInstaller.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.installer; //~--- non-JDK imports -------------------------------------------------------- @@ -40,7 +40,9 @@ import java.util.List; /** * * @author Sebastian Sdorra + * @deprecated use {@link sonia.scm.autoconfig.AutoConfigurator} */ +@Deprecated public class UnixHgInstaller extends AbstractHgInstaller { 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 35f95b9d11..a7c5ef6069 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 @@ -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 -------------------------------------------------------- @@ -36,11 +36,9 @@ import java.io.File; * * @author Sebastian Sdorra */ -public enum HgPythonScript -{ - BLAME("blame.py"), CHANGELOG("changelog.py"), FILELOG("filelog.py"), - LOG("log.py"), UTIL("util.py"), HOOK("scmhooks.py"), HGWEB("hgweb.py"), - VERSION("version.py"); +public enum HgPythonScript { + + HOOK("scmhooks.py"), HGWEB("hgweb.py"), VERSION("version.py"); /** Field description */ private static final String BASE_DIRECTORY = 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 6c33607750..bdc36eb54b 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 @@ -33,6 +33,7 @@ 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; import sonia.scm.installer.HgInstallerFactory; import sonia.scm.io.ExtendedCommand; @@ -55,6 +56,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.MessageFormat; +import java.util.Optional; @Singleton @Extension @@ -91,14 +93,11 @@ public class HgRepositoryHandler this.hgContextProvider = hgContextProvider; this.workingCopyFactory = workingCopyFactory; - try - { + try { this.jaxbContext = JAXBContext.newInstance(BrowserResult.class, BlameResult.class, Changeset.class, ChangesetPagingResult.class, HgVersion.class); - } - catch (JAXBException ex) - { + } catch (JAXBException ex) { throw new ConfigurationException("could not create jaxbcontext", ex); } } @@ -139,7 +138,19 @@ public class HgRepositoryHandler super.loadConfig(); if (config == null) { - doAutoConfiguration(new HgConfig()); + HgConfig config = null; + Optional autoConfigurator = AutoConfigurator.get(); + if (autoConfigurator.isPresent()) { + config = autoConfigurator.get().configure(); + } + + if (config != null && config.isValid()) { + this.config = config; + storeConfig(); + } else { + // do the old configuration + doAutoConfiguration(config != null ? config : new HgConfig()); + } } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBlameCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBlameCommand.java index 83732bb879..474d092840 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBlameCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBlameCommand.java @@ -28,7 +28,7 @@ package sonia.scm.repository.spi; import com.aragost.javahg.Changeset; import com.aragost.javahg.commands.AnnotateCommand; -import com.aragost.javahg.commands.AnnotateLine; +import com.aragost.javahg.AnnotateLine; import com.google.common.base.Strings; import com.google.common.collect.Lists; import org.slf4j.Logger; 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 48b8ebc0a5..22cccbc8ae 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 @@ -60,16 +60,16 @@ public class HgBranchesCommand extends AbstractCommand @Override public List getBranches() { - List hgBranches = + List hgBranches = com.aragost.javahg.commands.BranchesCommand.on(open()).execute(); List branches = Lists.transform(hgBranches, - new Function() { @Override - public Branch apply(com.aragost.javahg.commands.Branch hgBranch) + public Branch apply(com.aragost.javahg.Branch hgBranch) { String node = null; Changeset changeset = hgBranch.getBranchTip(); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagsCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagsCommand.java index 20bdac4ec1..309265617c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagsCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagsCommand.java @@ -69,7 +69,7 @@ public class HgTagsCommand extends AbstractCommand implements TagsCommand com.aragost.javahg.commands.TagsCommand cmd = com.aragost.javahg.commands.TagsCommand.on(open()); - List tagList = cmd.includeTip().execute(); + List tagList = cmd.includeTip().execute(); List tags = null; @@ -97,7 +97,7 @@ public class HgTagsCommand extends AbstractCommand implements TagsCommand * @author Enter your name here... */ private static class TagTransformer - implements Function + implements Function { /** @@ -109,7 +109,7 @@ public class HgTagsCommand extends AbstractCommand implements TagsCommand * @return */ @Override - public Tag apply(com.aragost.javahg.commands.Tag f) + public Tag apply(com.aragost.javahg.Tag f) { Tag t = null; diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 44cb661385..28e88f2ece 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -48,7 +48,7 @@ except ImportError: from mercurial import util _parsedate = util.parsedate -FILE_MARKER = '' +FILE_MARKER = b'' class File_Collector: @@ -62,7 +62,7 @@ class File_Collector: self.attach(self.extract_name_without_parent(path, p), self.structure, dir_only) def attach(self, branch, trunk, dir_only = False): - parts = branch.split('/', 1) + parts = branch.split(b'/', 1) if len(parts) == 1: # branch is a file if dir_only: trunk[parts[0]] = defaultdict(dict, ((FILE_MARKER, []),)) @@ -78,7 +78,7 @@ class File_Collector: def extract_name_without_parent(self, parent, name_with_parent): if len(parent) > 0: name_without_parent = name_with_parent[len(parent):] - if name_without_parent.startswith("/"): + if name_without_parent.startswith(b"/"): name_without_parent = name_without_parent[1:] return name_without_parent return name_with_parent @@ -91,11 +91,11 @@ class File_Object: self.sub_repository = None def get_name(self): - parts = self.path.split("/") + parts = self.path.split(b"/") return parts[len(parts) - 1] def get_parent(self): - idx = self.path.rfind("/") + idx = self.path.rfind(b"/") if idx > 0: return self.path[0:idx] return "" @@ -112,7 +112,7 @@ class File_Object: def __repr__(self): result = self.path if self.directory: - result += "/" + result += b"/" return result class File_Walker: @@ -143,11 +143,15 @@ class File_Walker: def create_path(self, parent, path): if len(parent) > 0: - return parent + "/" + path + return parent + b"/" + path return path - def walk(self, structure, parent = ""): - sortedItems = sorted(structure.iteritems(), key = lambda item: self.sortKey(item)) + def walk(self, structure, parent = b""): + if hasattr(structure, "iteritems"): + items = structure.iteritems() + else: + items = structure.items() + sortedItems = sorted(items, key = lambda item: self.sortKey(item)) for key, value in sortedItems: if key == FILE_MARKER: if value: @@ -162,9 +166,9 @@ class File_Walker: def sortKey(self, item): if (item[0] == FILE_MARKER): - return "2" + return b"2" else: - return "1" + item[0] + return b"1" + item[0] class SubRepository: url = None @@ -173,9 +177,9 @@ class SubRepository: def collect_sub_repositories(revCtx): subrepos = {} try: - hgsub = revCtx.filectx('.hgsub').data().split('\n') + hgsub = revCtx.filectx(b'.hgsub').data().split('\n') for line in hgsub: - parts = line.split('=') + parts = line.split(b'=') if len(parts) > 1: subrepo = SubRepository() subrepo.url = parts[1].strip() @@ -184,7 +188,7 @@ def collect_sub_repositories(revCtx): pass try: - hgsubstate = revCtx.filectx('.hgsubstate').data().split('\n') + hgsubstate = revCtx.filectx(b'.hgsubstate').data().split('\n') for line in hgsubstate: parts = line.split(' ') if len(parts) > 1: @@ -219,31 +223,31 @@ class File_Printer: def print_directory(self, path): if not self.initial_path_printed or self.offset == 0 or self.shouldPrintResult(): self.initial_path_printed = True - format = '%s/\n' + format = b'%s/\n' if self.transport: - format = 'd%s/\0' + format = b'd%s/\0' self.writer.write( format % path) def print_file(self, path): self.result_count += 1 if self.shouldPrintResult(): file = self.revCtx[path] - date = '0 0' - description = 'n/a' + date = b'0 0' + description = b'n/a' if not self.disableLastCommit: linkrev = self.repo[file.linkrev()] - date = '%d %d' % _parsedate(linkrev.date()) + date = b'%d %d' % _parsedate(linkrev.date()) description = linkrev.description() - format = '%s %i %s %s\n' + format = b'%s %i %s %s\n' if self.transport: - format = 'f%s\n%i %s %s\0' + format = b'f%s\n%i %s %s\0' self.writer.write( format % (file.path(), file.size(), date, description) ) def print_sub_repository(self, path, subrepo): if self.shouldPrintResult(): - format = '%s/ %s %s\n' + format = b'%s/ %s %s\n' if self.transport: - format = 's%s/\n%s %s\0' + format = b's%s/\n%s %s\0' self.writer.write( format % (path, subrepo.revision, subrepo.url)) def visit(self, file): @@ -263,9 +267,9 @@ class File_Printer: def finish(self): if self.isTruncated(): if self.transport: - self.writer.write( "t") + self.writer.write(b"t") else: - self.writer.write("truncated") + self.writer.write(b"truncated") class File_Viewer: def __init__(self, revCtx, visitor): @@ -275,11 +279,11 @@ class File_Viewer: self.recursive = False def remove_ending_slash(self, path): - if path.endswith("/"): + if path.endswith(b"/"): return path[:-1] return path - def view(self, path = ""): + def view(self, path = b""): manifest = self.revCtx.manifest() if len(path) > 0 and path in manifest: self.visitor.visit(File_Object(False, path)) @@ -294,15 +298,15 @@ class File_Viewer: collector.collect(self.sub_repositories.keys(), p, True) walker.walk(collector.structure, p) -@command('fileview', [ - ('r', 'revision', 'tip', 'revision to print'), - ('p', 'path', '', 'path to print'), - ('c', 'recursive', False, 'browse repository recursive'), - ('d', 'disableLastCommit', False, 'disables last commit description and date'), - ('s', 'disableSubRepositoryDetection', False, 'disables detection of sub repositories'), - ('t', 'transport', False, 'format the output for command server'), - ('l', 'limit', 100, 'limit the number of results'), - ('o', 'offset', 0, 'proceed from the given result number (zero based)'), +@command(b'fileview', [ + (b'r', b'revision', b'tip', b'revision to print'), + (b'p', b'path', b'', b'path to print'), + (b'c', b'recursive', False, b'browse repository recursive'), + (b'd', b'disableLastCommit', False, b'disables last commit description and date'), + (b's', b'disableSubRepositoryDetection', False, b'disables detection of sub repositories'), + (b't', b'transport', False, b'format the output for command server'), + (b'l', b'limit', 100, b'limit the number of results'), + (b'o', b'offset', 0, b'proceed from the given result number (zero based)'), ]) def fileview(ui, repo, **opts): revCtx = scmutil.revsingle(repo, opts["revision"]) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/blame.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/blame.py deleted file mode 100644 index bbf01552ab..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/blame.py +++ /dev/null @@ -1,57 +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 os -from util import * -from xml.dom.minidom import Document - -def appendBlameLine(doc, parent, lineCtx, lineNumber): - ctx = lineCtx[0].changectx() - lineNode = createChildNode(doc, parent, 'blameline') - appendTextNode(doc, lineNode, 'lineNumber', str(lineNumber)) - appendTextNode(doc, lineNode, 'revision', getId(ctx)) - appendDateNode(doc, lineNode, 'when', ctx.date()) - appendAuthorNodes(doc, lineNode, ctx) - appendTextNode(doc, lineNode, 'description', ctx.description()) - appendTextNode(doc, lineNode, 'code', lineCtx[1][:-1]) - -def appendBlameLines(doc, repo, revision, path): - blameResult = createChildNode(doc, doc, 'blame-result') - blameLines = createChildNode(doc, blameResult, 'blamelines') - linesCtx = repo[revision][path].annotate() - lineNumber = 0 - for lineCtx in linesCtx: - lineNumber += 1 - appendBlameLine(doc, blameLines, lineCtx, lineNumber) - appendTextNode(doc, blameResult, 'total', str(lineNumber)) - -# main method - -repo = openRepository() -revision = os.environ['SCM_REVISION'] -path = os.environ['SCM_PATH'] - -doc = Document() -appendBlameLines(doc, repo, revision, path) -writeXml(doc) \ No newline at end of file diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/changelog.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/changelog.py deleted file mode 100644 index 1a6b762713..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/changelog.py +++ /dev/null @@ -1,143 +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 os -from util import * -from xml.dom.minidom import Document - -# changeset methods - -def appendIdNode(doc, parentNode, ctx): - id = getId(ctx) - appendTextNode(doc, parentNode, 'id', id) - -def appendParentNodes(doc, parentNode, ctx): - parents = ctx.parents() - if parents: - for parent in parents: - parentId = getId(parent) - appendTextNode(doc, parentNode, 'parents', parentId) - -def appendBranchesNode(doc, parentNode, ctx): - branch = ctx.branch() - if branch != 'default': - appendTextNode(doc, parentNode, 'branches', branch) - -def appendModifications(doc, parentNode, ctx): - status = repo.status(ctx.p1().node(), ctx.node()) - if status: - modificationsNode = createChildNode(doc, parentNode, 'modifications') - appendWrappedListNodes(doc, modificationsNode, 'added', 'file', status[1]) - appendWrappedListNodes(doc, modificationsNode, 'modified', 'file', status[0]) - appendWrappedListNodes(doc, modificationsNode, 'removed', 'file', status[2]) - -def appendChangesetNode(doc, parentNode, ctx): - changesetNode = createChildNode(doc, parentNode, 'changeset') - appendIdNode(doc, changesetNode, ctx) - appendParentNodes(doc, changesetNode, ctx) - appendTextNode(doc, changesetNode, 'description', ctx.description()) - appendDateNode(doc, changesetNode, 'date', ctx.date()) - appendAuthorNodes(doc, changesetNode, ctx) - appendBranchesNode(doc, changesetNode, ctx) - appendListNodes(doc, changesetNode, 'tags', ctx.tags()) - appendModifications(doc, changesetNode, ctx) - -# changeset methods end - -# change log methods - -def createBasicNodes(doc, ctxs): - rootNode = doc.createElement('changeset-paging') - doc.appendChild(rootNode) - total = str(len(repo)) - appendTextNode(doc, rootNode, 'total', total) - return createChildNode(doc, rootNode, 'changesets') - -def appendChangesetsForPath(doc, repo, rev, path): - if len(rev) <= 0: - rev = "tip" - fctxs = repo[rev].filectx(path) - maxRev = fctxs.rev() - revs = [] - for i in fctxs.filelog(): - fctx = fctxs.filectx(i) - if fctx.rev() <= maxRev: - revs.append(fctx.changectx()) - # reverse changesets - revs.reverse() - # handle paging - start = os.environ['SCM_PAGE_START'] - limit = os.environ['SCM_PAGE_LIMIT'] - if len(start) > 0: - revs = revs[int(start):] - if len(limit) > 0: - revs = revs[:int(limit)] - # output - changesets = createBasicNodes(doc, revs) - for ctx in revs: - appendChangesetNode(doc, changesets, ctx) - -def appendChangesetsForStartAndEnd(doc, repo, startRev, endRev): - changesets = createBasicNodes(doc, repo) - for i in range(endRev, startRev, -1): - appendChangesetNode(doc, changesets, repo[i]) - -# change log methods - -# main method -repo = openRepository() -doc = Document() - -path = os.environ['SCM_PATH'] -startNode = os.environ['SCM_REVISION_START'] -endNode = os.environ['SCM_REVISION_END'] -rev = os.environ['SCM_REVISION'] - -if len(path) > 0: - appendChangesetsForPath(doc, repo, rev, path) -elif len(rev) > 0: - ctx = repo[rev] - appendChangesetNode(doc, doc, ctx) -else: - if len(startNode) > 0 and len(endNode) > 0: - # start and end revision - startRev = repo[startNode].rev() -1 - endRev = repo[endNode].rev() - else: - # paging - start = os.environ['SCM_PAGE_START'] - limit = os.environ['SCM_PAGE_LIMIT'] - limit = int(limit) - end = int(start) - endRev = len(repo) - end - 1 - startRev = endRev - limit - # fix negative start revisions - if startRev < -1: - startRev = -1 - # print - appendChangesetsForStartAndEnd(doc, repo, startRev, endRev) - -# write document -writeXml(doc) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/filelog.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/filelog.py deleted file mode 100644 index 07f1898be3..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/filelog.py +++ /dev/null @@ -1,159 +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 os -from util import * -from xml.dom.minidom import Document - -class SubRepository: - url = None - revision = None - -def getName(path): - parts = path.split('/') - length = len(parts) - if path.endswith('/'): - length =- 1 - return parts[length - 1] - -def removeTrailingSlash(path): - if path.endswith('/'): - path = path[0:-1] - return path - -def appendSubRepositoryNode(doc, parentNode, path, subRepositories): - if path in subRepositories: - subRepository = subRepositories[path] - subRepositoryNode = createChildNode(doc, parentNode, 'subrepository') - if subRepository.revision != None: - appendTextNode(doc, subRepositoryNode, 'revision', subRepository.revision) - appendTextNode(doc, subRepositoryNode, 'repository-url', subRepository.url) - -def createBasicFileNode(doc, parentNode, path, directory): - fileNode = createChildNode(doc, parentNode, 'file') - appendTextNode(doc, fileNode, 'name', getName(path)) - appendTextNode(doc, fileNode, 'path', removeTrailingSlash(path)) - appendTextNode(doc, fileNode, 'directory', directory) - return fileNode - -def appendDirectoryNode(doc, parentNode, path, subRepositories): - fileNode = createBasicFileNode(doc, parentNode, path, 'true') - appendSubRepositoryNode(doc, fileNode, path, subRepositories) - -def appendFileNode(doc, parentNode, repo, file): - linkrev = repo[file.linkrev()] - fileNode = createBasicFileNode(doc, parentNode, file.path(), 'false') - appendTextNode(doc, fileNode, 'length', str(file.size())) - appendDateNode(doc, fileNode, 'lastModified', linkrev.date()) - appendTextNode(doc, fileNode, 'description', linkrev.description()) - -def createSubRepositoryMap(revCtx): - subrepos = {} - try: - hgsub = revCtx.filectx('.hgsub').data().split('\n') - for line in hgsub: - parts = line.split('=') - if len(parts) > 1: - subrepo = SubRepository() - subrepo.url = parts[1].strip() - subrepos[parts[0].strip()] = subrepo - except Exception: - pass - - try: - hgsubstate = revCtx.filectx('.hgsubstate').data().split('\n') - for line in hgsubstate: - parts = line.split(' ') - if len(parts) > 1: - subrev = parts[0].strip() - subrepo = subrepos[parts[1].strip()] - subrepo.revision = subrev - except Exception: - pass - - return subrepos - -def appendSubRepositoryDirectories(directories, subRepositories): - for k, v in subRepositories.iteritems(): - if k.startswith(path): - directories.append(k) - -def collectFiles(repo, revCtx, files, directories): - length = 0 - paths = [] - mf = revCtx.manifest() - if path is "": - length = 1 - for f in mf: - paths.append(f) - else: - length = len(path.split('/')) + 1 - directory = path - if not directory.endswith('/'): - directory += '/' - - for f in mf: - if f.startswith(directory): - paths.append(f) - - for p in paths: - parts = p.split('/') - depth = len(parts) - if depth is length: - file = repo[revision][p] - files.append(file) - elif depth > length: - dirpath = '' - for i in range(0, length): - dirpath += parts[i] + '/' - if not dirpath in directories: - directories.append(dirpath) - -def appendFileNodes(doc, parentNode, repo, revision): - files = [] - directories = [] - revCtx = repo[revision] - subRepositories = createSubRepositoryMap(revCtx) - appendSubRepositoryDirectories(directories, subRepositories) - collectFiles(repo, revCtx, files, directories) - for dir in directories: - appendDirectoryNode(doc, parentNode, dir, subRepositories) - for file in files: - appendFileNode(doc, parentNode, repo, file) - - -# main method - -repo = openRepository() -revision = os.environ['SCM_REVISION'] -path = os.environ['SCM_PATH'] - -# create document and append nodes - -doc = Document() -browserResultNode = createChildNode(doc, doc, 'browser-result') -appendTextNode(doc, browserResultNode, 'revision', revision) -filesNode = createChildNode(doc, browserResultNode, 'files') -appendFileNodes(doc, filesNode, repo, revision) -writeXml(doc) 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 5d7d8e41ea..81b02c7818 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 @@ -35,20 +35,20 @@ except AttributeError: # For installations earlier than Mercurial 4.1 u = uimod.ui() -u.setconfig('web', 'push_ssl', 'false') -u.setconfig('web', 'allow_read', '*') -u.setconfig('web', 'allow_push', '*') +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('hooks', 'changegroup.scm', 'python:scmhooks.postHook') -u.setconfig('hooks', 'pretxnchangegroup.scm', 'python:scmhooks.preHook') +u.setconfig(b'hooks', b'changegroup.scm', b'python:scmhooks.postHook') +u.setconfig(b'hooks', b'pretxnchangegroup.scm', b'python:scmhooks.preHook') # pass SCM_HTTP_POST_ARGS to enable experimental httppostargs protocol of mercurial # SCM_HTTP_POST_ARGS is set by HgCGIServlet # Issue 970: https://goo.gl/poascp -u.setconfig('experimental', 'httppostargs', os.environ['SCM_HTTP_POST_ARGS']) +u.setconfig(b'experimental', b'httppostargs', os.environ['SCM_HTTP_POST_ARGS'].encode()) # open repository # SCM_REPOSITORY_PATH contains the repository path and is set by HgCGIServlet -r = hg.repository(u, os.environ['SCM_REPOSITORY_PATH']) +r = hg.repository(u, os.environ['SCM_REPOSITORY_PATH'].encode()) application = hgweb(r) wsgicgi.launch(application) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/log.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/log.py deleted file mode 100644 index 8d0c81782e..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/log.py +++ /dev/null @@ -1,140 +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 os -from util import * -from xml.dom.minidom import Document - -# changeset methods - -def appendIdNode(doc, parentNode, ctx): - id = getId(ctx) - appendTextNode(doc, parentNode, 'id', id) - -def appendParentNodes(doc, parentNode, ctx): - parents = ctx.parents() - if parents: - for parent in parents: - parentId = getId(parent) - appendTextNode(doc, parentNode, 'parents', parentId) - -def appendBranchesNode(doc, parentNode, ctx): - branch = ctx.branch() - if branch != 'default': - appendTextNode(doc, parentNode, 'branches', branch) - -def appendModifications(doc, parentNode, ctx): - status = repo.status(ctx.p1().node(), ctx.node()) - if status: - modificationsNode = createChildNode(doc, parentNode, 'modifications') - appendWrappedListNodes(doc, modificationsNode, 'added', 'file', status[1]) - appendWrappedListNodes(doc, modificationsNode, 'modified', 'file', status[0]) - appendWrappedListNodes(doc, modificationsNode, 'removed', 'file', status[2]) - -def appendChangesetNode(doc, parentNode, ctx): - changesetNode = createChildNode(doc, parentNode, 'changeset') - appendIdNode(doc, changesetNode, ctx) - appendParentNodes(doc, changesetNode, ctx) - appendTextNode(doc, changesetNode, 'description', ctx.description()) - appendDateNode(doc, changesetNode, 'date', ctx.date()) - appendAuthorNodes(doc, changesetNode, ctx) - appendBranchesNode(doc, changesetNode, ctx) - appendListNodes(doc, changesetNode, 'tags', ctx.tags()) - appendModifications(doc, changesetNode, ctx) - -# changeset methods end - -# change log methods - -def createBasicNodes(doc, ctxs): - rootNode = doc.createElement('changeset-paging') - doc.appendChild(rootNode) - total = str(len(ctxs)) - appendTextNode(doc, rootNode, 'total', total) - return createChildNode(doc, rootNode, 'changesets') - -def collectChangesets(repo, path, startNode, endNode): - start = 'tip' - end = '0' - if len(startNode) > 0: - start = startNode - if len(endNode) > 0: - end = endNode - - ctxs = [] - startRev = repo[start].rev() - endRev = repo[end].rev() - 1 - - onlyWithPath = len(path) > 0 - - for i in range(startRev, endRev, -1): - ctx = repo[i] - if onlyWithPath: - if path in ctx.files(): - ctxs.append(ctx) - else: - ctxs.append(ctx) - - return ctxs - -def stripChangesets(ctxs, start, limit): - if limit < 0: - ctxs = ctxs[start:] - else: - limit = limit + start - if limit > len(ctxs): - ctxs = ctxs[start:] - else: - ctxs = ctxs[start:limit] - return ctxs - -# change log methods - -# main method -repo = openRepository() -doc = Document() - -# parameter -path = os.environ['SCM_PATH'] -startNode = os.environ['SCM_REVISION_START'] -endNode = os.environ['SCM_REVISION_END'] -rev = os.environ['SCM_REVISION'] -# paging parameter -start = int(os.environ['SCM_PAGE_START']) -limit = int(os.environ['SCM_PAGE_LIMIT']) - -if len(rev) > 0: - ctx = repo[rev] - appendChangesetNode(doc, doc, ctx) -else: - ctxs = collectChangesets(repo, path, startNode, endNode) - changesetsNode = createBasicNodes(doc, ctxs) - ctxs = stripChangesets(ctxs, start, limit) - for ctx in ctxs: - appendChangesetNode(doc, changesetsNode, ctx) - - -# write document -writeXml(doc) 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 0a301e854f..eb7718bb87 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,8 +29,45 @@ # changegroup.scm = python:scmhooks.callback # -import os, urllib, urllib2 +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() + +# read environment baseUrl = os.environ['SCM_URL'] challenge = os.environ['SCM_CHALLENGE'] token = os.environ['SCM_BEARER_TOKEN'] @@ -38,45 +75,43 @@ xsrf = os.environ['SCM_XSRF'] repositoryId = os.environ['SCM_REPOSITORY_ID'] def printMessages(ui, msgs): - for line in msgs: - if line.startswith("_e") or line.startswith("_n"): - line = line[2:]; - ui.warn('%s\n' % line.rstrip()) + 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 callHookUrl(ui, repo, hooktype, node): abort = True try: - url = baseUrl + hooktype - ui.debug( "send scm-hook to " + url + " and " + node + "\n" ) - data = urllib.urlencode({'node': node, 'challenge': challenge, 'token': token, 'repositoryPath': repo.root, 'repositoryId': repositoryId}) - # 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) - conn = opener.open(req) + 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( "scm-hook " + hooktype + " success with status code " + str(conn.code) + "\n" ) + 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( "ERROR: scm-hook failed with error code " + str(conn.code) + "\n" ) - except urllib2.URLError, e: + 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 " + str(e.code) + "\n" + 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( "ERROR: scm-hook failed with an unknown error\n" ) + ui.warn( b"ERROR: scm-hook failed with an unknown error\n" ) ui.traceback() except ValueError: - ui.warn( "scm-hook failed with an exception\n" ) + ui.warn( b"scm-hook failed with an exception\n" ) ui.traceback() return abort @@ -86,10 +121,10 @@ def callback(ui, repo, hooktype, node=None): if len(baseUrl) > 0: abort = callHookUrl(ui, repo, hooktype, node) else: - ui.warn("ERROR: scm-manager hooks are disabled, please check your configuration and the scm-manager log for details\n") + 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("changeset node is not available") + ui.warn(b"changeset node is not available") return abort def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): @@ -106,13 +141,12 @@ def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): tr = repo.currenttransaction() repo.dirstate.write(tr) if tr and not tr.writepending(): - ui.warn("no pending write transaction found") + ui.warn(b"no pending write transaction found") except AttributeError: - ui.debug("mercurial does not support currenttransation") + ui.debug(b"mercurial does not support currenttransation") # do nothing return callback(ui, repo, hooktype, node) def postHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): return callback(ui, repo, hooktype, node) - diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/util.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/util.py deleted file mode 100644 index 12de67da00..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/util.py +++ /dev/null @@ -1,87 +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 basic modules -import sys, os - -# import mercurial modules -from mercurial import hg, ui, commands, encoding -from mercurial.node import hex -from xml.dom.minidom import Document - -# util methods -def openRepository(): - repositoryPath = os.environ['SCM_REPOSITORY_PATH'] - return hg.repository(ui.ui(), path = repositoryPath) - -def writeXml(doc): - # print doc.toprettyxml(indent=" ") - doc.writexml(sys.stdout, encoding='UTF-8') - -def createChildNode(doc, parentNode, name): - node = doc.createElement(name) - parentNode.appendChild(node) - return node - -def appendValue(doc, node, value): - textNode = doc.createTextNode(encoding.tolocal(value)) - node.appendChild(textNode) - -def appendTextNode(doc, parentNode, name, value): - node = createChildNode(doc, parentNode, name) - appendValue(doc, node, value) - -def appendDateNode(doc, parentNode, nodeName, date): - time = int(date[0]) * 1000 - date = str(time).split('.')[0] - appendTextNode(doc, parentNode, nodeName, date) - -def appendListNodes(doc, parentNode, name, values): - if values: - for value in values: - appendTextNode(doc, parentNode, name, value) - -def appendWrappedListNodes(doc, parentNode, wrapperName, name, values): - if values: - wrapperNode = createChildNode(doc, parentNode, wrapperName) - appendListNodes(doc, wrapperNode, name, values) - -def getId(ctx): - id = '' - if os.environ['SCM_ID_REVISION'] == 'true': - id = str(ctx.rev()) + ':' - return id + hex(ctx.node()) - -def appendAuthorNodes(doc, parentNode, ctx): - authorName = ctx.user() - authorMail = None - if authorName: - authorNode = createChildNode(doc, parentNode, 'author') - s = authorName.find('<') - e = authorName.find('>') - if s > 0 and e > 0: - authorMail = authorName[s + 1:e].strip() - authorName = authorName[0:s].strip() - appendTextNode(doc, authorNode, 'mail', authorMail) - appendTextNode(doc, authorNode, 'name', authorName) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/autoconfig/PosixAutoConfiguratorTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/autoconfig/PosixAutoConfiguratorTest.java new file mode 100644 index 0000000000..2eaf69c94f --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/autoconfig/PosixAutoConfiguratorTest.java @@ -0,0 +1,166 @@ +/* + * 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.autoconfig; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import sonia.scm.repository.HgConfig; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class PosixAutoConfiguratorTest { + + @Test + void shouldConfigureWithShebangPath(@TempDir Path directory) throws IOException { + Path hg = directory.resolve("hg"); + Path python = directory.resolve("python"); + + Files.write(hg, ("#!" + python.toAbsolutePath().toString()).getBytes(StandardCharsets.UTF_8)); + Files.createFile(python); + + PosixAutoConfigurator configurator = create(directory); + HgConfig config = configurator.configure(); + + assertThat(config.getHgBinary()).isEqualTo(hg.toString()); + assertThat(config.getPythonBinary()).isEqualTo(python.toString()); + } + + private PosixAutoConfigurator create(@TempDir Path directory) { + return new PosixAutoConfigurator(createEnv(directory), Collections.emptyList()); + } + + private Map createEnv(Path... paths) { + return ImmutableMap.of("PATH", Joiner.on(File.pathSeparator).join(paths)); + } + + @Test + void shouldConfigureWithShebangEnv(@TempDir Path directory) throws IOException { + Path hg = directory.resolve("hg"); + Path python = directory.resolve("python3.8"); + + Files.write(hg, "#!/usr/bin/env python3.8".getBytes(StandardCharsets.UTF_8)); + Files.createFile(python); + + PosixAutoConfigurator configurator = create(directory); + HgConfig config = configurator.configure(); + + assertThat(config.getHgBinary()).isEqualTo(hg.toString()); + assertThat(config.getPythonBinary()).isEqualTo(python.toString()); + } + + @Test + void shouldConfigureWithoutShebang(@TempDir Path directory) throws IOException { + Path hg = directory.resolve("hg"); + Path python = directory.resolve("python"); + + Files.createFile(hg); + Files.createFile(python); + + PosixAutoConfigurator configurator = create(directory); + HgConfig config = configurator.configure(); + + assertThat(config.getHgBinary()).isEqualTo(hg.toString()); + assertThat(config.getPythonBinary()).isEqualTo(python.toString()); + } + + @Test + void shouldConfigureWithoutShebangButWithPython3(@TempDir Path directory) throws IOException { + Path hg = directory.resolve("hg"); + Path python = directory.resolve("python3"); + + Files.createFile(hg); + Files.createFile(python); + + PosixAutoConfigurator configurator = create(directory); + HgConfig config = configurator.configure(); + + assertThat(config.getHgBinary()).isEqualTo(hg.toString()); + assertThat(config.getPythonBinary()).isEqualTo(python.toString()); + } + + @Test + void shouldConfigureFindPythonInAdditionalPath(@TempDir Path directory) throws IOException { + Path def = directory.resolve("default"); + Files.createDirectory(def); + Path additional = directory.resolve("additional"); + Files.createDirectory(additional); + + Path hg = def.resolve("hg"); + Path python = additional.resolve("python"); + + Files.createFile(hg); + Files.createFile(python); + + PosixAutoConfigurator configurator = new PosixAutoConfigurator( + createEnv(def), ImmutableList.of(additional.toAbsolutePath().toString()) + ); + + HgConfig config = configurator.configure(); + assertThat(config.getHgBinary()).isEqualTo(hg.toString()); + assertThat(config.getPythonBinary()).isEqualTo(python.toString()); + } + + @Test + void shouldFindModulePathFromDebuginstallOutput(@TempDir Path directory) throws IOException { + Path hg = directory.resolve("hg"); + Files.createFile(hg); + hg.toFile().setExecutable(true); + + Path modules = directory.resolve("modules"); + Files.createDirectories(modules); + + Path mercurialModule = modules.resolve("mercurial"); + Files.createDirectories(mercurialModule); + + PosixAutoConfigurator configurator = create(directory); + configurator.setExecutor((Path binary, String... args) -> { + String content = String.join("\n", + "checking Python executable (/python3.8)", + "checking Python lib (/python3.8)...", + "checking installed modules (" + mercurialModule.toString() + ")...", + "checking templates (/mercurial/templates)...", + "checking default template (/mercurial/templates/map-cmdline.default))" + ); + return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + }); + HgConfig config = configurator.configure(); + + assertThat(config.getHgBinary()).isEqualTo(hg.toString()); + assertThat(config.getPythonPath()).isEqualTo(modules.toString()); + } + +}