Merge pull request #1232 from scm-manager/feature/support_python_3

Support for mercurial on python 3
This commit is contained in:
eheimbuch
2020-07-02 11:28:55 +02:00
committed by GitHub
21 changed files with 570 additions and 696 deletions

View File

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

View File

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

View File

@@ -44,7 +44,7 @@
<dependency>
<groupId>com.aragost.javahg</groupId>
<artifactId>javahg</artifactId>
<version>0.14</version>
<version>0.15-scm1</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>

View File

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

View File

@@ -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<String> ADDITIONAL_PATH = ImmutableList.of(
"/usr/bin",
"/usr/local/bin",
"/opt/local/bin"
);
private final Set<String> 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<String, String> env) {
this(env, ADDITIONAL_PATH);
}
PosixAutoConfigurator(Map<String, String> env, List<String> 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<Path> hg = findInPath("hg");
if (hg.isPresent()) {
return configure(hg.get());
}
return new HgConfig();
}
private Optional<Path> 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<Path> 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<Path> 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<Path> 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<Path> modulePath = findModulePath(hg);
if (modulePath.isPresent()) {
config.setPythonPath(modulePath.get().toAbsolutePath().toString());
} else {
LOG.warn("could not find module path");
}
}
private Optional<Path> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -60,16 +60,16 @@ public class HgBranchesCommand extends AbstractCommand
@Override
public List<Branch> getBranches() {
List<com.aragost.javahg.commands.Branch> hgBranches =
List<com.aragost.javahg.Branch> hgBranches =
com.aragost.javahg.commands.BranchesCommand.on(open()).execute();
List<Branch> branches = Lists.transform(hgBranches,
new Function<com.aragost.javahg.commands.Branch,
new Function<com.aragost.javahg.Branch,
Branch>()
{
@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();

View File

@@ -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<com.aragost.javahg.commands.Tag> tagList = cmd.includeTip().execute();
List<com.aragost.javahg.Tag> tagList = cmd.includeTip().execute();
List<Tag> tags = null;
@@ -97,7 +97,7 @@ public class HgTagsCommand extends AbstractCommand implements TagsCommand
* @author Enter your name here...
*/
private static class TagTransformer
implements Function<com.aragost.javahg.commands.Tag, Tag>
implements Function<com.aragost.javahg.Tag, Tag>
{
/**
@@ -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;

View File

@@ -48,7 +48,7 @@ except ImportError:
from mercurial import util
_parsedate = util.parsedate
FILE_MARKER = '<files>'
FILE_MARKER = b'<files>'
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"])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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