Merge branch 'develop' of github.com:scm-manager/scm-manager into develop

This commit is contained in:
Eduard Heimbuch
2020-12-17 10:15:50 +01:00
65 changed files with 1164 additions and 4327 deletions

View File

@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Send mercurial hook callbacks over separate tcp socket instead of http ([#1416](https://github.com/scm-manager/scm-manager/pull/1416))
- Implement mercurial cgi protocol as extension ([#1458](https://github.com/scm-manager/scm-manager/pull/1458))
### Fixed
- Language detection of files with interpreter parameters e.g.: `#!/usr/bin/make -f` ([#1450](https://github.com/scm-manager/scm-manager/issues/1450))

View File

@@ -21,12 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web.cgi;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.util.Collections;
import java.util.List;
/**
*
@@ -233,8 +235,20 @@ public abstract class AbstractCGIExecutor implements CGIExecutor
this.workDirectory = workDirectory;
}
@Override
public void setArgs(List<String> args) {
this.args = args;
}
@Override
public List<String> getArgs() {
return args;
}
//~--- fields ---------------------------------------------------------------
protected List<String> args = Collections.emptyList();
/** Field description */
protected int bufferSize;

View File

@@ -21,103 +21,112 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web.cgi;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletException;
import java.util.Collections;
import java.util.List;
/**
*
* @author Sebastian Sdorra
*/
public interface CGIExecutor
{
public interface CGIExecutor {
/** Field description */
public static final String ENV_AUTH_TYPE = "AUTH_TYPE";
String ENV_AUTH_TYPE = "AUTH_TYPE";
/** Field description */
public static final String ENV_CONTENT_LENGTH = "CONTENT_LENGTH";
String ENV_CONTENT_LENGTH = "CONTENT_LENGTH";
/** Field description */
public static final String ENV_CONTENT_TYPE = "CONTENT_TYPE";
String ENV_CONTENT_TYPE = "CONTENT_TYPE";
/** Field description */
public static final String ENV_GATEWAY_INTERFACE = "GATEWAY_INTERFACE";
String ENV_GATEWAY_INTERFACE = "GATEWAY_INTERFACE";
/** Field description */
public static final String ENV_HTTPS = "HTTPS";
String ENV_HTTPS = "HTTPS";
/** Field description */
public static final String ENV_HTTPS_VALUE_OFF = "OFF";
String ENV_HTTPS_VALUE_OFF = "OFF";
/** Field description */
public static final String ENV_HTTPS_VALUE_ON = "ON";
String ENV_HTTPS_VALUE_ON = "ON";
/** Field description */
public static final String ENV_HTTP_HEADER_PREFIX = "HTTP_";
String ENV_HTTP_HEADER_PREFIX = "HTTP_";
/** Field description */
public static final String ENV_PATH_INFO = "PATH_INFO";
String ENV_PATH_INFO = "PATH_INFO";
/** Field description */
public static final String ENV_PATH_TRANSLATED = "PATH_TRANSLATED";
String ENV_PATH_TRANSLATED = "PATH_TRANSLATED";
/** Field description */
public static final String ENV_QUERY_STRING = "QUERY_STRING";
String ENV_QUERY_STRING = "QUERY_STRING";
/** Field description */
public static final String ENV_REMOTE_ADDR = "REMOTE_ADDR";
String ENV_REMOTE_ADDR = "REMOTE_ADDR";
/** Field description */
public static final String ENV_REMOTE_HOST = "REMOTE_HOST";
String ENV_REMOTE_HOST = "REMOTE_HOST";
/** Field description */
public static final String ENV_REMOTE_USER = "REMOTE_USER";
String ENV_REMOTE_USER = "REMOTE_USER";
/** Field description */
public static final String ENV_REQUEST_METHOD = "REQUEST_METHOD";
String ENV_REQUEST_METHOD = "REQUEST_METHOD";
/** Field description */
public static final String ENV_SCRIPT_FILENAME = "SCRIPT_FILENAME";
String ENV_SCRIPT_FILENAME = "SCRIPT_FILENAME";
/** Field description */
public static final String ENV_SCRIPT_NAME = "SCRIPT_NAME";
String ENV_SCRIPT_NAME = "SCRIPT_NAME";
/** Field description */
public static final String ENV_SERVER_NAME = "SERVER_NAME";
String ENV_SERVER_NAME = "SERVER_NAME";
/** Field description */
public static final String ENV_SERVER_PORT = "SERVER_PORT";
String ENV_SERVER_PORT = "SERVER_PORT";
/** Field description */
public static final String ENV_SERVER_PROTOCOL = "SERVER_PROTOCOL";
String ENV_SERVER_PROTOCOL = "SERVER_PROTOCOL";
/** Field description */
public static final String ENV_SERVER_SOFTWARE = "SERVER_SOFTWARE";
String ENV_SERVER_SOFTWARE = "SERVER_SOFTWARE";
/** Field description */
public static final String ENV_SYSTEM_ROOT = "SystemRoot";
String ENV_SYSTEM_ROOT = "SystemRoot";
/**
* Content type header of response.
* @since 2.12.0
*/
String RESPONSE_HEADER_CONTENT_TYPE = "Content-Type";
/**
* @deprecated use {@link #RESPONSE_HEADER_CONTENT_TYPE} instead.
*/
@Deprecated
String REPSONSE_HEADER_CONTENT_TYPE = RESPONSE_HEADER_CONTENT_TYPE;
/** Field description */
public static final String REPSONSE_HEADER_CONTENT_TYPE = "Content-Type";
String RESPONSE_HEADER_CONTENT_LENGTH = "Content-Length";
/** Field description */
public static final String RESPONSE_HEADER_CONTENT_LENGTH = "Content-Length";
String RESPONSE_HEADER_HTTP_PREFIX = "HTTP";
/** Field description */
public static final String RESPONSE_HEADER_HTTP_PREFIX = "HTTP";
String RESPONSE_HEADER_LOCATION = "Location";
/** Field description */
public static final String RESPONSE_HEADER_LOCATION = "Location";
/** Field description */
public static final String RESPONSE_HEADER_STATUS = "Status";
String RESPONSE_HEADER_STATUS = "Status";
//~--- methods --------------------------------------------------------------
@@ -130,7 +139,7 @@ public interface CGIExecutor
* @throws IOException
* @throws ServletException
*/
public void execute(String cmd) throws IOException, ServletException;
void execute(String cmd) throws IOException, ServletException;
//~--- get methods ----------------------------------------------------------
@@ -140,7 +149,7 @@ public interface CGIExecutor
*
* @return
*/
public int getBufferSize();
int getBufferSize();
/**
* Method description
@@ -148,7 +157,7 @@ public interface CGIExecutor
*
* @return
*/
public EnvList getEnvironment();
EnvList getEnvironment();
/**
* Returns the cgi exception handler.
@@ -157,7 +166,7 @@ public interface CGIExecutor
* @return cgi exception handler
* @since 1.8
*/
public CGIExceptionHandler getExceptionHandler();
CGIExceptionHandler getExceptionHandler();
/**
* Method description
@@ -165,7 +174,7 @@ public interface CGIExecutor
*
* @return
*/
public String getInterpreter();
String getInterpreter();
/**
* Returns the status code handler.
@@ -174,7 +183,7 @@ public interface CGIExecutor
* @return status code handler
* @since 1.15
*/
public CGIStatusCodeHandler getStatusCodeHandler();
CGIStatusCodeHandler getStatusCodeHandler();
/**
* Method description
@@ -182,7 +191,7 @@ public interface CGIExecutor
*
* @return
*/
public File getWorkDirectory();
File getWorkDirectory();
/**
* Method description
@@ -191,7 +200,7 @@ public interface CGIExecutor
* @return
* @since 1.12
*/
public boolean isContentLengthWorkaround();
boolean isContentLengthWorkaround();
/**
* Method description
@@ -199,7 +208,7 @@ public interface CGIExecutor
*
* @return
*/
public boolean isIgnoreExitCode();
boolean isIgnoreExitCode();
/**
* Method description
@@ -207,7 +216,17 @@ public interface CGIExecutor
*
* @return
*/
public boolean isPassShellEnvironment();
boolean isPassShellEnvironment();
/**
* Returns command args as list.
*
* @return list of command args
* @since 2.12.0
*/
default List<String> getArgs() {
return Collections.emptyList();
}
//~--- set methods ----------------------------------------------------------
@@ -217,7 +236,7 @@ public interface CGIExecutor
*
* @param bufferSize
*/
public void setBufferSize(int bufferSize);
void setBufferSize(int bufferSize);
/**
* Method description
@@ -225,7 +244,7 @@ public interface CGIExecutor
*
* @param contentLengthWorkaround
*/
public void setContentLengthWorkaround(boolean contentLengthWorkaround);
void setContentLengthWorkaround(boolean contentLengthWorkaround);
/**
* Method description
@@ -233,7 +252,7 @@ public interface CGIExecutor
*
* @param environment
*/
public void setEnvironment(EnvList environment);
void setEnvironment(EnvList environment);
/**
* Sets the cgi exception handler.
@@ -242,7 +261,7 @@ public interface CGIExecutor
* @param exceptionHandler cgi exception handler
* @since 1.8
*/
public void setExceptionHandler(CGIExceptionHandler exceptionHandler);
void setExceptionHandler(CGIExceptionHandler exceptionHandler);
/**
* Method description
@@ -250,7 +269,7 @@ public interface CGIExecutor
*
* @param ignoreExitCode
*/
public void setIgnoreExitCode(boolean ignoreExitCode);
void setIgnoreExitCode(boolean ignoreExitCode);
/**
* Method description
@@ -258,7 +277,7 @@ public interface CGIExecutor
*
* @param interpreter
*/
public void setInterpreter(String interpreter);
void setInterpreter(String interpreter);
/**
* Method description
@@ -266,7 +285,7 @@ public interface CGIExecutor
*
* @param passShellEnvironment
*/
public void setPassShellEnvironment(boolean passShellEnvironment);
void setPassShellEnvironment(boolean passShellEnvironment);
/**
* Sets the status code handler.
@@ -275,7 +294,7 @@ public interface CGIExecutor
* @param statusCodeHandler the handler to set
* @since 1.15
*/
public void setStatusCodeHandler(CGIStatusCodeHandler statusCodeHandler);
void setStatusCodeHandler(CGIStatusCodeHandler statusCodeHandler);
/**
* Method description
@@ -283,5 +302,13 @@ public interface CGIExecutor
*
* @param workDirectory
*/
public void setWorkDirectory(File workDirectory);
void setWorkDirectory(File workDirectory);
/**
* Set command arguments.
* @param args command arguments
* @since 2.12.0
*/
default void setArgs(List<String> args) {
}
}

View File

@@ -21,76 +21,51 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web.cgi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableSet;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
*
* @author Sebastian Sdorra
*/
public class EnvList
{
public class EnvList {
/** Field description */
private static final ImmutableSet<String> SENSITIVE =
ImmutableSet.of("HTTP_AUTHORIZATION", "SCM_CHALLENGE", "SCM_CREDENTIALS");
ImmutableSet.of("HTTP_AUTHORIZATION", "SCM_CHALLENGE", "SCM_CREDENTIALS", "SCM_BEARER_TOKEN");
//~--- constructors ---------------------------------------------------------
private final Map<String, String> envMap;
/**
* Constructs ...
*
*/
public EnvList()
{
public EnvList() {
envMap = new HashMap<>();
}
/**
* Constructs ...
*
*
* @param l
*/
public EnvList(EnvList l)
{
envMap = new HashMap<>(l.envMap);
public EnvList(EnvList list) {
envMap = new HashMap<>(list.envMap);
}
//~--- methods --------------------------------------------------------------
/**
* Returns environment as mutable map.
* Set a name/value pair, null values will be treated as an empty String
*
* @return environment as mutable map
* @since 1.31
* @param name name of environment variable
* @param value value of environment variable
*/
public Map<String, String> asMutableMap()
{
return new MapDelegate(envMap);
public void set(String name, String value) {
envMap.put(name, Strings.nullToEmpty(value));
}
/**
* Method description
* Return {@code true} if the list contains an environment variable with the given key.
*
*
* @param key
*
* @return
* @param key name of environment variable
* @return {@code true} if contains environment variable
*/
public boolean containsKey(String key)
{
@@ -98,143 +73,69 @@ public class EnvList
}
/**
* Method description
* Representation suitable for passing to exec.
*
*
* @return
* @return array of environment variables
* @since 2.12.0
*/
public String[] asArray() {
return envMap.entrySet()
.stream()
.map(e -> e.getKey() + "=" + e.getValue())
.toArray(String[]::new);
}
/**
* Representation suitable for passing to process builder.
*
* @return environment as immutable map
* @since 2.12.0
*/
public Map<String, String> asMap() {
return Collections.unmodifiableMap(envMap);
}
@Override
public String toString()
{
public String toString() {
String s = System.getProperty("line.separator");
StringBuilder out = new StringBuilder("Environment:");
Iterator<String> it = envMap.values().iterator();
String v;
while (it.hasNext())
{
v = converSensitive(it.next());
out.append(s).append(" ").append(v);
for (Map.Entry<String, String> e : envMap.entrySet()) {
out
.append(s).append(" ")
.append(e.getKey()).append("=").append(convertSensitive(e.getKey(), e.getValue()));
}
return out.toString();
}
//~--- get methods ----------------------------------------------------------
private String convertSensitive(String name, String value) {
if (SENSITIVE.contains(name)) {
return "(is set)";
}
return value;
}
/**
* Get representation suitable for passing to exec.
*
* @return
* @return array of environment variables
* @deprecated use {@link #asArray()} instead
*/
public String[] getEnvArray()
{
return envMap.values().toArray(new String[envMap.size()]);
@Deprecated
public String[] getEnvArray() {
return asArray();
}
//~--- set methods ----------------------------------------------------------
/**
* Set a name/value pair, null values will be treated as an empty String
* Returns environment as mutable map.
*
* @param name
* @param value
* @return environment as mutable map
* @since 1.31
*
* @deprecated the environment should only be modified by {@link #set(String, String)}.
* Of a {@link Map} is required, a immutable {@link Map} can be created with {@link #asMap()}.
*/
public void set(String name, String value)
{
envMap.put(name, name.concat("=").concat(Util.nonNull(value)));
@Deprecated
public Map<String, String> asMutableMap() {
return envMap;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param v
*
* @return
*/
private String converSensitive(String v)
{
String result = v;
for (String s : SENSITIVE)
{
if (v.startsWith(s))
{
result = s.concat("=(is set)");
break;
}
}
return result;
}
//~--- inner classes --------------------------------------------------------
/**
* Class description
*
*
* @version Enter version here..., 13/05/15
* @author Enter your name here...
*/
private static class MapDelegate extends ForwardingMap<String, String>
{
/**
* Constructs ...
*
*
* @param delegate
*/
private MapDelegate(Map<String, String> delegate)
{
this.delegate = delegate;
}
//~--- methods ------------------------------------------------------------
/**
* Method description
*
*
* @param key
* @param value
*
* @return
*/
@Override
public String put(String key, String value)
{
return super.put(key, key.concat("=").concat(Strings.nullToEmpty(value)));
}
/**
* Method description
*
*
* @return
*/
@Override
protected Map<String, String> delegate()
{
return delegate;
}
//~--- fields -------------------------------------------------------------
/** Field description */
private Map<String, String> delegate;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private Map<String, String> envMap;
}

View File

@@ -21,38 +21,39 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web.cgi;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
class EnvListTest {
import static org.junit.Assert.*;
/**
*
* @author Sebastian Sdorra
*/
public class EnvListTest
{
/**
* Method description
*
*/
@Test
public void testToString()
{
EnvList envList = new EnvList();
void shouldNotPrintAuthorizationValue() {
EnvList env = new EnvList();
env.set("HTTP_AUTHORIZATION", "Basic xxx");
env.set("SOME_OTHER", "other");
env.set("SCM_BEARER_TOKEN", "secret");
envList.set("HTTP_AUTHORIZATION", "Basic xxx");
envList.set("SOME_OTHER", "other");
String value = env.toString();
String value = envList.toString();
assertThat(value)
.contains("SOME_OTHER=other")
.contains("HTTP_AUTHORIZATION=(is set)")
.contains("SCM_BEARER_TOKEN=(is set)")
.doesNotContain("HTTP_AUTHORIZATION=Basic xxx")
.doesNotContain("SCM_BEARER_TOKEN=secret");
}
assertTrue(value.contains("SOME_OTHER=other"));
assertFalse(value.contains("HTTP_AUTHORIZATION=Basic xxx"));
assertTrue(value.contains("HTTP_AUTHORIZATION=(is set)"));
@Test
void shouldReturnAsArray() {
EnvList env = new EnvList();
env.set("SPACESHIPT", "Heart of Gold");
env.set("DOMAIN", "hitchhiker.com");
assertThat(env.getEnvArray())
.contains("SPACESHIPT=Heart of Gold")
.contains("DOMAIN=hitchhiker.com");
}
}

View File

@@ -41,9 +41,6 @@ public class HgConfigDto extends HalRepresentation implements UpdateHgConfigDto
private String encoding;
private String hgBinary;
private String pythonBinary;
private String pythonPath;
private boolean useOptimizedBytecode;
private boolean showRevisionInId;
private boolean enableHttpPostArgs;

View File

@@ -1,117 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.installer.HgInstallerFactory;
import sonia.scm.repository.HgConfig;
import sonia.scm.web.HgVndMediaType;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
public class HgConfigInstallationsResource {
public static final String PATH_HG = "hg";
public static final String PATH_PYTHON = "python";
private final HgConfigInstallationsToDtoMapper hgConfigInstallationsToDtoMapper;
@Inject
public HgConfigInstallationsResource(HgConfigInstallationsToDtoMapper hgConfigInstallationsToDtoMapper) {
this.hgConfigInstallationsToDtoMapper = hgConfigInstallationsToDtoMapper;
}
/**
* Returns the hg installations.
*/
@GET
@Path(PATH_HG)
@Produces(HgVndMediaType.INSTALLATIONS)
@Operation(summary = "Hg installations", description = "Returns the mercurial installations.", tags = "Mercurial")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = HgVndMediaType.INSTALLATIONS,
schema = @Schema(implementation = HgConfigInstallationsDto.class)
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public HalRepresentation getHgInstallations() {
ConfigurationPermissions.read(HgConfig.PERMISSION).check();
return hgConfigInstallationsToDtoMapper.map(
HgInstallerFactory.createInstaller().getHgInstallations(), PATH_HG);
}
/**
* Returns the python installations.
*/
@GET
@Path(PATH_PYTHON)
@Produces(HgVndMediaType.INSTALLATIONS)
@Operation(summary = "Python installations", description = "Returns the python installations.", tags = "Mercurial")
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = HgVndMediaType.INSTALLATIONS,
schema = @Schema(implementation = HgConfigInstallationsDto.class)
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public HalRepresentation getPythonInstallations() {
ConfigurationPermissions.read(HgConfig.PERMISSION).check();
return hgConfigInstallationsToDtoMapper.map(
HgInstallerFactory.createInstaller().getPythonInstallations(), PATH_PYTHON);
}
}

View File

@@ -1,142 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.SCMContext;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.installer.HgInstallerFactory;
import sonia.scm.installer.HgPackage;
import sonia.scm.installer.HgPackageReader;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.web.HgVndMediaType;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
public class HgConfigPackageResource {
private final HgPackageReader pkgReader;
private final AdvancedHttpClient client;
private final HgRepositoryHandler handler;
private final HgConfigPackagesToDtoMapper configPackageCollectionToDtoMapper;
@Inject
public HgConfigPackageResource(HgPackageReader pkgReader, AdvancedHttpClient client, HgRepositoryHandler handler,
HgConfigPackagesToDtoMapper hgConfigPackagesToDtoMapper) {
this.pkgReader = pkgReader;
this.client = client;
this.handler = handler;
this.configPackageCollectionToDtoMapper = hgConfigPackagesToDtoMapper;
}
/**
* Returns all mercurial packages.
*/
@GET
@Path("")
@Produces(HgVndMediaType.PACKAGES)
@Operation(summary = "Hg configuration packages", description = "Returns all mercurial packages.", tags = "Mercurial")
@ApiResponse(
responseCode = "204",
description = "update success"
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:read:hg\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public HalRepresentation getPackages() {
ConfigurationPermissions.read(HgConfig.PERMISSION).check();
return configPackageCollectionToDtoMapper.map(pkgReader.getPackages());
}
/**
* Installs a mercurial package
*
* @param pkgId Identifier of the package to install
*/
@PUT
@Path("{pkgId}")
@Operation(summary = "Modifies hg configuration package", description = "Installs a mercurial package.", tags = "Mercurial")
@ApiResponse(
responseCode = "204",
description = "update success"
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege")
@ApiResponse(
responseCode = "404",
description = "no package found for id",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public Response installPackage(@PathParam("pkgId") String pkgId) {
Response response;
ConfigurationPermissions.write(HgConfig.PERMISSION).check();
HgPackage pkg = pkgReader.getPackage(pkgId);
if (pkg != null) {
if (HgInstallerFactory.createInstaller()
.installPackage(client, handler, SCMContext.getContext().getBaseDirectory(), pkg)) {
response = Response.noContent().build();
} else {
response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
} else {
response = Response.status(Response.Status.NOT_FOUND).build();
}
return response;
}
}

View File

@@ -1,83 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import lombok.Getter;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import sonia.scm.installer.HgPackage;
import sonia.scm.installer.HgPackages;
import javax.inject.Inject;
import java.util.List;
import static de.otto.edison.hal.Links.linkingTo;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306")
@Mapper
public abstract class HgConfigPackagesToDtoMapper {
@Inject
private ScmPathInfoStore scmPathInfoStore;
public HgConfigPackagesDto map(HgPackages hgpackages) {
return map(new HgPackagesNonIterable(hgpackages));
}
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
/* Favor warning "Unmapped target property: "attributes", to packages[].hgConfigTemplate"
Over error "Unknown property "packages[].hgConfigTemplate.attributes"
@Mapping(target = "packages[].hgConfigTemplate.attributes", ignore = true) // Also not for nested DTOs
*/
protected abstract HgConfigPackagesDto map(HgPackagesNonIterable hgPackagesNonIterable);
@AfterMapping
void appendLinks(@MappingTarget HgConfigPackagesDto target) {
Links.Builder linksBuilder = linkingTo().self(createSelfLink());
target.add(linksBuilder.build());
}
private String createSelfLink() {
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class);
return linkBuilder.method("getPackagesResource").parameters().href();
}
/**
* Unfortunately, HgPackages is iterable, HgConfigPackagesDto does not need to be iterable and MapStruct refuses to
* map an iterable to a non-iterable. So use this little non-iterable "proxy".
*/
@Getter
static class HgPackagesNonIterable {
private List<HgPackage> packages;
HgPackagesNonIterable(HgPackages hgPackages) {
this.packages = hgPackages.getPackages();
}
}
}

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.api.v2.resources;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
@@ -60,21 +60,17 @@ public class HgConfigResource {
private final HgConfigDtoToHgConfigMapper dtoToConfigMapper;
private final HgConfigToHgConfigDtoMapper configToDtoMapper;
private final HgRepositoryHandler repositoryHandler;
private final Provider<HgConfigPackageResource> packagesResource;
private final Provider<HgConfigAutoConfigurationResource> autoconfigResource;
private final Provider<HgConfigInstallationsResource> installationsResource;
@Inject
public HgConfigResource(HgConfigDtoToHgConfigMapper dtoToConfigMapper, HgConfigToHgConfigDtoMapper configToDtoMapper,
HgRepositoryHandler repositoryHandler, Provider<HgConfigPackageResource> packagesResource,
Provider<HgConfigAutoConfigurationResource> autoconfigResource,
Provider<HgConfigInstallationsResource> installationsResource) {
public HgConfigResource(HgConfigDtoToHgConfigMapper dtoToConfigMapper,
HgConfigToHgConfigDtoMapper configToDtoMapper,
HgRepositoryHandler repositoryHandler,
Provider<HgConfigAutoConfigurationResource> autoconfigResource) {
this.dtoToConfigMapper = dtoToConfigMapper;
this.configToDtoMapper = configToDtoMapper;
this.repositoryHandler = repositoryHandler;
this.packagesResource = packagesResource;
this.autoconfigResource = autoconfigResource;
this.installationsResource = installationsResource;
}
/**
@@ -134,7 +130,7 @@ public class HgConfigResource {
schema = @Schema(implementation = UpdateHgConfigDto.class),
examples = @ExampleObject(
name = "Overwrites current configuration with this one.",
value = "{\n \"disabled\":false,\n \"hgBinary\":\"hg\",\n \"pythonBinary\":\"python\",\n \"pythonPath\":\"\",\n \"encoding\":\"UTF-8\",\n \"useOptimizedBytecode\":false,\n \"showRevisionInId\":false,\n \"disableHookSSLValidation\":false,\n \"enableHttpPostArgs\":false\n}",
value = "{\n \"disabled\":false,\n \"hgBinary\":\"hg\",\n \"encoding\":\"UTF-8\",\n \"showRevisionInId\":false,\n \"enableHttpPostArgs\":false\n}",
summary = "Simple update configuration"
)
)
@@ -165,18 +161,8 @@ public class HgConfigResource {
return Response.noContent().build();
}
@Path("packages")
public HgConfigPackageResource getPackagesResource() {
return packagesResource.get();
}
@Path("auto-configuration")
public HgConfigAutoConfigurationResource getAutoConfigurationResource() {
return autoconfigResource.get();
}
@Path("installations")
public HgConfigInstallationsResource getInstallationsResource() {
return installationsResource.get();
}
}

View File

@@ -29,14 +29,8 @@ interface UpdateHgConfigDto {
String getHgBinary();
String getPythonBinary();
String getPythonPath();
String getEncoding();
boolean isUseOptimizedBytecode();
boolean isShowRevisionInId();
boolean isEnableHttpPostArgs();

View File

@@ -21,30 +21,16 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
package sonia.scm.autoconfig;
import javax.inject.Inject;
import java.util.List;
import com.google.inject.AbstractModule;
import sonia.scm.plugin.Extension;
import static de.otto.edison.hal.Links.linkingTo;
public class HgConfigInstallationsToDtoMapper {
private ScmPathInfoStore scmPathInfoStore;
@Inject
public HgConfigInstallationsToDtoMapper(ScmPathInfoStore scmPathInfoStore) {
this.scmPathInfoStore = scmPathInfoStore;
}
public HgConfigInstallationsDto map(List<String> installations, String path) {
return new HgConfigInstallationsDto(linkingTo().self(createSelfLink(path)).build(), installations);
}
private String createSelfLink(String path) {
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class);
return linkBuilder.method("getInstallationsResource").parameters().href() + '/' + path;
@Extension
public class AutoConfigModule extends AbstractModule {
@Override
protected void configure() {
bind(AutoConfigurator.class).toProvider(AutoConfiguratorProvider.class);
}
}

View File

@@ -24,26 +24,10 @@
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();
}
void configure(HgConfig config);
}

View File

@@ -21,42 +21,41 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
package sonia.scm.autoconfig;
import java.util.List;
import com.google.common.annotations.VisibleForTesting;
import sonia.scm.Platform;
import sonia.scm.repository.HgVerifier;
import sonia.scm.util.SystemUtil;
@NoArgsConstructor
@Getter
@Setter
public class HgConfigPackagesDto extends HalRepresentation {
import javax.inject.Inject;
import javax.inject.Provider;
private List<HgConfigPackageDto> packages;
public class AutoConfiguratorProvider implements Provider<AutoConfigurator> {
private final HgVerifier verifier;
private final Platform platform;
@Inject
public AutoConfiguratorProvider(HgVerifier verifier) {
this(verifier, SystemUtil.getPlatform());
}
@VisibleForTesting
AutoConfiguratorProvider(HgVerifier verifier, Platform platform) {
this.verifier = verifier;
this.platform = platform;
}
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
@NoArgsConstructor
@Getter
@Setter
public static class HgConfigPackageDto {
private String arch;
private HgConfigDto hgConfigTemplate;
private String hgVersion;
private String id;
private String platform;
private String pythonVersion;
private long size;
private String url;
public AutoConfigurator get() {
if (platform.isPosix()) {
return new PosixAutoConfigurator(verifier, System.getenv());
} else if (platform.isWindows()) {
return new WindowsAutoConfigurator(verifier, new WindowsRegistry(), System.getenv());
} else {
return new NoOpAutoConfigurator();
}
}
}

View File

@@ -21,51 +21,23 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
package sonia.scm.autoconfig;
import sonia.scm.util.SystemUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgConfig;
/**
*
* @author Sebastian Sdorra
*/
public final class HgInstallerFactory
{
public class NoOpAutoConfigurator implements AutoConfigurator {
/**
* Constructs ...
*
*/
private HgInstallerFactory() {}
private static final Logger LOG = LoggerFactory.getLogger(NoOpAutoConfigurator.class);
//~--- methods --------------------------------------------------------------
NoOpAutoConfigurator() {
}
/**
* Method description
*
*
* @return
*/
public static HgInstaller createInstaller()
{
HgInstaller installer = null;
if (SystemUtil.isWindows())
{
installer = new WindowsHgInstaller();
}
else if (SystemUtil.isMac())
{
installer = new MacOSHgInstaller();
}
else
{
installer = new UnixHgInstaller();
}
return installer;
@Override
public void configure(HgConfig config) {
// if we do not know the environment, we could not configure mercurial
LOG.debug("no mercurial autoconfiguration available on this platform");
}
}

View File

@@ -27,19 +27,12 @@ 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 sonia.scm.repository.HgVerifier;
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;
@@ -58,155 +51,40 @@ public class PosixAutoConfigurator implements AutoConfigurator {
"/opt/local/bin"
);
private final HgVerifier verifier;
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(HgVerifier verifier, Map<String, String> env) {
this(verifier, env, ADDITIONAL_PATH);
}
PosixAutoConfigurator(Map<String, String> env, List<String> additionalPaths) {
@VisibleForTesting
PosixAutoConfigurator(HgVerifier verifier, Map<String, String> env, List<String> additionalPaths) {
this.verifier = verifier;
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");
public void configure(HgConfig config) {
Optional<Path> hg = findInPath();
if (hg.isPresent()) {
return configure(hg.get());
config.setHgBinary(hg.get().toAbsolutePath().toString());
} else {
LOG.warn("could not find valid mercurial installation");
}
return new HgConfig();
}
private Optional<Path> findInPath(String binary) {
private Optional<Path> findInPath() {
for (String directory : fsPaths) {
Path binaryPath = Paths.get(directory, binary);
if (Files.exists(binaryPath)) {
Path binaryPath = Paths.get(directory, "hg");
if (verifier.isValid(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

@@ -0,0 +1,129 @@
/*
* 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVerifier;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class WindowsAutoConfigurator implements AutoConfigurator {
private static final Logger LOG = LoggerFactory.getLogger(WindowsAutoConfigurator.class);
@VisibleForTesting
static final String REGISTRY_KEY_TORTOISE_HG = "HKEY_LOCAL_MACHINE\\Software\\TortoiseHg";
@VisibleForTesting
static final String REGISTRY_KEY_MERCURIAL = "HKEY_LOCAL_MACHINE\\Software\\Mercurial\\InstallDir";
private static final String[] REGISTRY_KEYS = {REGISTRY_KEY_TORTOISE_HG, REGISTRY_KEY_MERCURIAL};
@VisibleForTesting
static final String BINARY_HG_EXE = "hg.exe";
@VisibleForTesting
static final String BINARY_HG_BAT = "hg.bat";
private static final String[] BINARIES = {BINARY_HG_EXE, BINARY_HG_BAT};
@VisibleForTesting
static final String ENV_PATH = "Path";
private final HgVerifier verifier;
private final WindowsRegistry registry;
private final Map<String, String> env;
WindowsAutoConfigurator(HgVerifier verifier, WindowsRegistry registry, Map<String, String> env) {
this.verifier = verifier;
this.registry = registry;
this.env = env;
}
@Override
public void configure(HgConfig config) {
Set<String> fsPaths = new LinkedHashSet<>(pathFromEnv());
resolveRegistryKeys(fsPaths);
Optional<String> hg = findInPath(fsPaths);
if (hg.isPresent()) {
String hgBinary = hg.get();
LOG.info("found hg at {}", hgBinary);
config.setHgBinary(hgBinary);
} else {
LOG.warn("could not find valid mercurial installation");
}
}
private void resolveRegistryKeys(Set<String> fsPaths) {
for (String registryKey : REGISTRY_KEYS) {
Optional<String> registryValue = registry.get(registryKey);
if (registryValue.isPresent()) {
String directory = registryValue.get();
LOG.trace("resolved registry key {} to directory {}", registryKey, directory);
fsPaths.add(directory);
} else {
LOG.trace("could not find value for registry key {}", registryKey);
}
}
}
private Collection<String> pathFromEnv() {
String path = env.getOrDefault(ENV_PATH, "");
LOG.trace("try to find hg in PATH {}", path);
return Splitter.on(File.pathSeparator).splitToList(path);
}
private Optional<String> findInPath(Set<String> fsPaths) {
for (String directory : fsPaths) {
Optional<String> binaryPath = findInDirectory(directory);
if (binaryPath.isPresent()) {
return binaryPath;
}
}
return Optional.empty();
}
private Optional<String> findInDirectory(String directory) {
LOG.trace("check directory {} for mercurial installations", directory);
for (String binary : BINARIES) {
Path hg = Paths.get(directory, binary);
if (verifier.isValid(hg)) {
return Optional.of(hg.toAbsolutePath().toString());
}
}
return Optional.empty();
}
}

View File

@@ -21,25 +21,19 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.Setter;
package sonia.scm.autoconfig;
import java.util.List;
import sonia.scm.util.RegistryUtil;
@Getter
@Setter
public class HgConfigInstallationsDto extends HalRepresentation {
import java.util.Optional;
private List<String> paths;
public class WindowsRegistry {
public HgConfigInstallationsDto(Links links, List<String> paths) {
super(links);
this.paths = paths;
public Optional<String> get(String key) {
return Optional.ofNullable(
RegistryUtil.getRegistryValue(key)
);
}
}

View File

@@ -1,65 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.HgRepositoryHandler;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import sonia.scm.net.ahc.AdvancedHttpClient;
/**
*
* @author Sebastian Sdorra
*/
public abstract class AbstractHgInstaller implements HgInstaller
{
/**
* Method description
*
*
*
*
* @param client
* @param handler
* @param baseDirectory
* @param pkg
*
* @return
*/
@Override
public boolean installPackage(AdvancedHttpClient client, HgRepositoryHandler handler,
File baseDirectory, HgPackage pkg)
{
return new HgPackageInstaller(client, handler, baseDirectory,
pkg).install();
}
}

View File

@@ -1,104 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
*
* @author Sebastian Sdorra
*/
public interface HgInstaller
{
/**
* Method description
*
*
*
* @param baseDirectory
* @param config
*
* @throws IOException
*/
public void install(File baseDirectory, HgConfig config) throws IOException;
/**
* Method description
*
*
*
*
* @param client
* @param handler
* @param baseDirectory
* @param pkg
*
* @return
*/
public boolean installPackage(AdvancedHttpClient client,
HgRepositoryHandler handler, File baseDirectory, HgPackage pkg);
/**
* Method description
*
*
*
* @param baseDirectory
* @param config
*
* @throws IOException
*/
public void update(File baseDirectory, HgConfig config) throws IOException;
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public List<String> getHgInstallations();
/**
* Method description
*
*
* @return
*/
public List<String> getPythonInstallations();
}

View File

@@ -1,253 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.HgConfig;
//~--- JDK imports ------------------------------------------------------------
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "package")
@XmlAccessorType(XmlAccessType.FIELD)
public class HgPackage
{
/**
* Method description
*
*
* @return
*/
public String getArch()
{
return arch;
}
/**
* Method description
*
*
* @return
*/
public HgConfig getHgConfigTemplate()
{
return hgConfigTemplate;
}
/**
* Method description
*
*
* @return
*/
public String getHgVersion()
{
return hgVersion;
}
/**
* Method description
*
*
* @return
*/
public String getId()
{
return id;
}
/**
* Method description
*
*
* @return
*/
public String getPlatform()
{
return platform;
}
/**
* Method description
*
*
* @return
*/
public String getPythonVersion()
{
return pythonVersion;
}
/**
* Method description
*
*
* @return
*/
public long getSize()
{
return size;
}
/**
* Method description
*
*
* @return
*/
public String getUrl()
{
return url;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param arch
*/
public void setArch(String arch)
{
this.arch = arch;
}
/**
* Method description
*
*
* @param hgConfigTemplate
*/
public void setHgConfigTemplate(HgConfig hgConfigTemplate)
{
this.hgConfigTemplate = hgConfigTemplate;
}
/**
* Method description
*
*
* @param hgVersion
*/
public void setHgVersion(String hgVersion)
{
this.hgVersion = hgVersion;
}
/**
* Method description
*
*
* @param id
*/
public void setId(String id)
{
this.id = id;
}
/**
* Method description
*
*
* @param platform
*/
public void setPlatform(String platform)
{
this.platform = platform;
}
/**
* Method description
*
*
* @param pythonVersion
*/
public void setPythonVersion(String pythonVersion)
{
this.pythonVersion = pythonVersion;
}
/**
* Method description
*
*
* @param size
*/
public void setSize(long size)
{
this.size = size;
}
/**
* Method description
*
*
* @param url
*/
public void setUrl(String url)
{
this.url = url;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String arch;
/** Field description */
@XmlElement(name = "hg-config-template")
private HgConfig hgConfigTemplate;
/** Field description */
@XmlElement(name = "hg-version")
private String hgVersion;
/** Field description */
private String id;
/** Field description */
private String platform;
/** Field description */
@XmlElement(name = "python-version")
private String pythonVersion;
/** Field description */
private long size;
/** Field description */
private String url;
}

View File

@@ -1,267 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.io.ZipUnArchiver;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.HgWindowsPackageFix;
import sonia.scm.util.IOUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;
/**
*
* @author Sebastian Sdorra
*/
public class HgPackageInstaller implements Runnable
{
/** the logger for HgPackageInstaller */
private static final Logger logger =
LoggerFactory.getLogger(HgPackageInstaller.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
*
* @param client
* @param handler
* @param baseDirectory
* @param pkg
*/
public HgPackageInstaller(AdvancedHttpClient client,
HgRepositoryHandler handler, File baseDirectory, HgPackage pkg)
{
this.client = client;
this.handler = handler;
this.baseDirectory = baseDirectory;
this.pkg = pkg;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public boolean install()
{
boolean success = false;
File downloadedFile = downloadFile();
if ((downloadedFile != null) && downloadedFile.exists())
{
File directory = extractPackage(downloadedFile);
if ((directory != null) && directory.exists())
{
updateConfig(directory);
success = true;
}
}
return success;
}
/**
* Method description
*
*/
@Override
public void run()
{
if (!install())
{
logger.error("installation of pkg {} failed", pkg.getId());
}
else if (logger.isInfoEnabled())
{
logger.info("successfully installed pkg {}", pkg.getId());
}
}
/**
* Method description
*
*
* @return
*/
private File downloadFile()
{
File file = null;
InputStream input = null;
OutputStream output = null;
try
{
file = File.createTempFile("scm-hg-", ".pkg");
if (logger.isDebugEnabled())
{
logger.debug("download package to {}", file.getAbsolutePath());
}
// TODO error handling
input = client.get(pkg.getUrl()).request().contentAsStream();
output = new FileOutputStream(file);
IOUtil.copy(input, output);
}
catch (IOException ex)
{
logger.error("could not downlaod file ".concat(pkg.getUrl()), ex);
file = null;
}
finally
{
IOUtil.close(input);
IOUtil.close(output);
}
return file;
}
/**
* Method description
*
*
* @param file
*
* @return
*/
private File extractPackage(File file)
{
File directory = new File(baseDirectory,
"pkg".concat(File.separator).concat(pkg.getId()));
IOUtil.mkdirs(directory);
try
{
IOUtil.extract(file, directory, ZipUnArchiver.EXTENSION);
}
catch (IOException ex)
{
directory = null;
logger.error("could not extract pacakge ".concat(pkg.getId()), ex);
}
finally
{
// delete temp file
try
{
IOUtil.delete(file, true);
}
catch (IOException ex)
{
logger.error(ex.getMessage(), ex);
}
}
return directory;
}
/**
* Method description
*
*
* @param directory
*/
private void updateConfig(File directory)
{
String path = directory.getAbsolutePath();
HgConfig template = pkg.getHgConfigTemplate();
HgConfig config = handler.getConfig();
config.setHgBinary(getTemplateValue(template.getHgBinary(), path));
config.setPythonBinary(getTemplateValue(template.getPythonBinary(), path));
config.setPythonPath(getTemplateValue(template.getPythonPath(), path));
config.setUseOptimizedBytecode(template.isUseOptimizedBytecode());
// fix wrong hg.bat
HgWindowsPackageFix.fixHgPackage(SCMContext.getContext(), config);
handler.storeConfig();
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param template
* @param path
*
* @return
*/
private String getTemplateValue(String template, String path)
{
String result = null;
if (template != null)
{
result = MessageFormat.format(template, path);
}
return result;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private File baseDirectory;
/** Field description */
private AdvancedHttpClient client;
/** Field description */
private HgRepositoryHandler handler;
/** Field description */
private HgPackage pkg;
}

View File

@@ -1,235 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.PlatformType;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.util.SystemUtil;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author Sebastian Sdorra
*/
public class HgPackageReader
{
/** Field description */
public static final String CACHENAME = "sonia.scm.hg.packages";
/** Field description */
public static final String PACKAGEURL =
"http://download.scm-manager.org/pkg/mercurial/packages.xml";
/** the logger for HgPackageReader */
private static final Logger logger =
LoggerFactory.getLogger(HgPackageReader.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param cacheManager
* @param httpClient
*/
@Inject
public HgPackageReader(CacheManager cacheManager, AdvancedHttpClient httpClient)
{
this.cache = cacheManager.getCache(CACHENAME);
this.httpClient = httpClient;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param id
*
* @return
*/
public HgPackage getPackage(String id)
{
HgPackage pkg = null;
for (HgPackage p : getPackages())
{
if (id.equals(p.getId()))
{
pkg = p;
break;
}
}
return pkg;
}
/**
* Method description
*
*
* @return
*/
public HgPackages getPackages()
{
HgPackages packages = cache.get(HgPackages.class.getName());
if (packages == null)
{
packages = getRemptePackages();
filterPackage(packages);
cache.put(HgPackages.class.getName(), packages);
}
return packages;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param packages
*/
private void filterPackage(HgPackages packages)
{
List<HgPackage> pkgList = new ArrayList<>();
for (HgPackage pkg : packages)
{
boolean add = true;
if (Util.isNotEmpty(pkg.getPlatform()))
{
PlatformType pt = PlatformType.createPlatformType(pkg.getPlatform());
if (SystemUtil.getPlatform().getType() != pt)
{
if (logger.isDebugEnabled())
{
logger.debug("reject package {}, because of wrong platform {}",
pkg.getId(), pkg.getPlatform());
}
add = false;
}
}
if (add && Util.isNotEmpty(pkg.getArch()))
{
if (!SystemUtil.getArch().equals(pkg.getArch()))
{
if (logger.isDebugEnabled())
{
logger.debug("reject package {}, because of wrong arch {}",
pkg.getId(), pkg.getArch());
}
add = false;
}
}
if (add)
{
if (logger.isDebugEnabled())
{
logger.debug("added HgPackage {}", pkg.getId());
}
pkgList.add(pkg);
}
}
packages.setPackages(pkgList);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
private HgPackages getRemptePackages()
{
if (logger.isInfoEnabled())
{
logger.info("fetch HgPackages from {}", PACKAGEURL);
}
HgPackages packages = null;
try
{
//J-
packages = httpClient.get(PACKAGEURL)
.request()
.contentFromXml(HgPackages.class);
//J+
}
catch (IOException ex)
{
logger.error("could not read HgPackages from ".concat(PACKAGEURL), ex);
}
if (packages == null)
{
packages = new HgPackages();
packages.setPackages(new ArrayList<>());
}
return packages;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final Cache<String, HgPackages> cache;
/** Field description */
private final AdvancedHttpClient httpClient;
}

View File

@@ -1,89 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- JDK imports ------------------------------------------------------------
import java.util.Iterator;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "packages")
@XmlAccessorType(XmlAccessType.FIELD)
public class HgPackages implements Iterable<HgPackage>
{
/**
* Method description
*
*
* @return
*/
@Override
public Iterator<HgPackage> iterator()
{
return packages.iterator();
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public List<HgPackage> getPackages()
{
return packages;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param packages
*/
public void setPackages(List<HgPackage> packages)
{
this.packages = packages;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@XmlElement(name = "package")
private List<HgPackage> packages;
}

View File

@@ -1,220 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.io.DirectoryFileFilter;
import sonia.scm.repository.HgConfig;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
/**
*
* @author Sebastian Sdorra
* @deprecated use {@link sonia.scm.autoconfig.AutoConfigurator}
*/
@Deprecated
public class MacOSHgInstaller extends UnixHgInstaller
{
/** Field description */
public static final String ENV_PATH = "PATH";
/** Field description */
public static final String PATH_HG = "hg";
/** Field description */
public static final String PATH_HG_BREW = "/usr/local/bin/hg";
/** Field description */
public static final String PATH_HG_BREW_INSTALLATION =
"/usr/local/Cellar/mercurial";
/** Field description */
public static final String PATH_PYTHON = "python";
/** the logger for MacOSHgInstaller */
private static final Logger logger =
LoggerFactory.getLogger(MacOSHgInstaller.class);
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param baseDirectory
* @param config
*
* @throws IOException
*/
@Override
public void install(File baseDirectory, HgConfig config) throws IOException
{
super.install(baseDirectory, config);
String hg = config.getHgBinary();
if (PATH_HG.equals(hg))
{
hg = resolvePath(hg);
}
if (PATH_HG_BREW.equals(hg))
{
File file = new File(PATH_HG_BREW);
file = file.getCanonicalFile();
if (file.getAbsolutePath().startsWith(PATH_HG_BREW_INSTALLATION))
{
useHomebrewInstallation(config, file);
}
}
}
/**
* Method description
*
*
* @param parent
*
* @return
*/
private File findPythonDirectory(File parent)
{
File pythonDirectory = null;
for (File d : parent.listFiles(DirectoryFileFilter.instance))
{
if (d.getName().startsWith("python"))
{
pythonDirectory = d;
break;
}
}
return pythonDirectory;
}
/**
* Method description
*
*
*
* @param binaryName
* @return
*/
private String resolvePath(String binaryName)
{
String binary = binaryName;
try
{
String path = System.getenv(ENV_PATH);
for (String p : path.split(":"))
{
File file = new File(p, binaryName);
if (file.exists())
{
binary = file.getAbsolutePath();
if (logger.isDebugEnabled())
{
logger.debug("resolve {} path to {}", binaryName, binary);
}
break;
}
}
}
catch (Exception ex)
{
logger.error("could not resolve binary path", ex);
}
return binary;
}
/**
* Method description
*
*
* @param config
* @param file
*/
private void useHomebrewInstallation(HgConfig config, File file)
{
File parent = file.getParentFile().getParentFile();
File libDirectory = new File(parent, "lib");
if (!libDirectory.exists())
{
libDirectory = new File(parent, "libexec");
}
if (libDirectory.exists())
{
File pythonDirectory = findPythonDirectory(libDirectory);
if (pythonDirectory != null)
{
File sitePackageDirectory = new File(pythonDirectory, "site-packages");
if (sitePackageDirectory.exists())
{
libDirectory = sitePackageDirectory;
}
String pythonPath = libDirectory.getPath();
if (logger.isInfoEnabled())
{
logger.info("found mercurial brew install set python path to {}",
pythonPath);
}
config.setPythonPath(pythonPath);
}
}
String python = config.getPythonBinary();
if (PATH_PYTHON.equals(python))
{
config.setPythonBinary(resolvePath(python));
}
}
}

View File

@@ -1,141 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.HgConfig;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
*
* @author Sebastian Sdorra
* @deprecated use {@link sonia.scm.autoconfig.AutoConfigurator}
*/
@Deprecated
public class UnixHgInstaller extends AbstractHgInstaller
{
/** Field description */
public static final String COMMAND_HG = "hg";
/** Field description */
public static final String COMMAND_PYTHON = "python";
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
*
* @param baseDirectory
* @param config
*
* @throws IOException
*/
@Override
public void install(File baseDirectory, HgConfig config) throws IOException
{
// search mercurial (hg)
if (Util.isEmpty(config.getHgBinary()))
{
String hg = IOUtil.search(COMMAND_HG);
if (Util.isNotEmpty(hg))
{
config.setHgBinary(hg);
// search python in the same folder
File hgFile = new File(hg);
if (hgFile.exists())
{
File pythonFile = new File(hgFile.getParentFile(), COMMAND_PYTHON);
if (pythonFile.exists())
{
config.setPythonBinary(pythonFile.getAbsolutePath());
}
}
}
}
// search python
if (Util.isEmpty(config.getPythonBinary()))
{
config.setPythonBinary(IOUtil.search(COMMAND_PYTHON));
}
}
/**
* Method description
*
*
*
* @param baseDirectory
* @param config
*/
@Override
public void update(File baseDirectory, HgConfig config)
{
// do nothing
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public List<String> getHgInstallations()
{
return IOUtil.searchAll(COMMAND_HG);
}
/**
* Method description
*
*
* @return
*/
@Override
public List<String> getPythonInstallations()
{
return IOUtil.searchAll(COMMAND_PYTHON);
}
}

View File

@@ -1,418 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.installer;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgConfig;
import sonia.scm.util.IOUtil;
import sonia.scm.util.RegistryUtil;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author Sebastian Sdorra
*/
public class WindowsHgInstaller extends AbstractHgInstaller
{
/** Field description */
private static final String FILE_LIBRARY_ZIP = "library.zip";
/** Field description */
private static final String FILE_LIB_MERCURIAL =
"Lib\\site-packages\\mercurial";
/** Field description */
private static final String FILE_MERCURIAL_EXE = "hg.exe";
/** Field description */
private static final String FILE_MERCURIAL_SCRIPT = "hg.bat";
/** Field description */
private static final String FILE_SCRIPTS = "Scripts";
/** Field description */
private static final String FILE_TEMPLATES = "templates";
/** Field description */
private static final String[] REGISTRY_HG = new String[]
{
// TortoiseHg
"HKEY_CURRENT_USER\\Software\\TortoiseHg",
// Mercurial
"HKEY_CURRENT_USER\\Software\\Mercurial\\InstallDir"
};
/** Field description */
private static final String[] REGISTRY_PYTHON = new String[]
{
// .py files
"HKEY_CLASSES_ROOT\\Python.File\\shell\\open\\command"
};
/** the logger for WindowsHgInstaller */
private static final Logger logger =
LoggerFactory.getLogger(WindowsHgInstaller.class);
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
*
* @param baseDirectory
* @param config
*
* @throws IOException
*/
@Override
public void install(File baseDirectory, HgConfig config) throws IOException
{
if (Util.isEmpty(config.getPythonBinary()))
{
String pythonBinary = getPythonBinary();
config.setPythonBinary(pythonBinary);
}
if (Util.isEmpty(config.getHgBinary()))
{
File hgScript = getMercurialScript(config.getPythonBinary());
if (hgScript != null)
{
config.setHgBinary(hgScript.getAbsolutePath());
}
}
File hgDirectory = getMercurialDirectory(config.getHgBinary());
if (hgDirectory != null)
{
installHg(baseDirectory, config, hgDirectory);
}
checkForOptimizedByteCode(config);
}
/**
* Method description
*
*
*
* @param baseDirectory
* @param config
*/
@Override
public void update(File baseDirectory, HgConfig config) {}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public List<String> getHgInstallations()
{
return getInstallations(REGISTRY_HG);
}
/**
* Method description
*
*
* @return
*/
@Override
public List<String> getPythonInstallations()
{
return getInstallations(REGISTRY_PYTHON);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param config
*/
private void checkForOptimizedByteCode(HgConfig config)
{
boolean optimized = false;
String path = config.getPythonPath();
if (Util.isNotEmpty(path))
{
for (String part : path.split(";"))
{
if (checkForOptimizedByteCode(part))
{
optimized = true;
break;
}
}
}
config.setUseOptimizedBytecode(optimized);
}
/**
* Method description
*
*
* @param part
*
* @return
*/
private boolean checkForOptimizedByteCode(String part)
{
File libDir = new File(part);
String[] pyoFiles = libDir.list(new FilenameFilter()
{
@Override
public boolean accept(File file, String name)
{
return name.toLowerCase().endsWith(".pyo");
}
});
return Util.isNotEmpty(pyoFiles);
}
/**
* Method description
*
*
*
* @param baseDirectory
* @param config
* @param hgDirectory
*
* @throws IOException
*/
private void installHg(File baseDirectory, HgConfig config, File hgDirectory)
throws IOException
{
if (logger.isInfoEnabled())
{
logger.info("installing mercurial {}", hgDirectory.getAbsolutePath());
}
File libDir = new File(baseDirectory, "lib\\hg");
IOUtil.mkdirs(libDir);
File libraryZip = new File(hgDirectory, FILE_LIBRARY_ZIP);
if (libraryZip.exists())
{
IOUtil.extract(libraryZip, libDir);
config.setPythonPath(libDir.getAbsolutePath());
}
File templateDirectory = new File(hgDirectory, FILE_TEMPLATES);
if (templateDirectory.exists())
{
IOUtil.copy(templateDirectory, new File(libDir, FILE_TEMPLATES));
}
File hg = new File(hgDirectory, FILE_MERCURIAL_EXE);
if (hg.exists())
{
config.setHgBinary(hg.getAbsolutePath());
}
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param registryKeys
*
* @return
*/
private List<String> getInstallations(String[] registryKeys)
{
List<String> installations = new ArrayList<String>();
for (String registryKey : registryKeys)
{
String path = RegistryUtil.getRegistryValue(registryKey);
if (path != null)
{
File file = new File(path, FILE_MERCURIAL_EXE);
if (file.exists())
{
installations.add(file.getAbsolutePath());
}
}
}
return installations;
}
/**
* Method description
*
*
*
* @param hgBinary
* @return
*/
private File getMercurialDirectory(String hgBinary)
{
File directory = null;
if (Util.isNotEmpty(hgBinary))
{
File hg = new File(hgBinary);
if (hg.exists() && hg.isFile())
{
directory = hg.getParentFile();
}
}
if (directory == null)
{
directory = getMercurialDirectoryFromRegistry();
}
return directory;
}
/**
* Method description
*
*
* @return
*/
private File getMercurialDirectoryFromRegistry()
{
File directory = null;
for (String registryKey : REGISTRY_HG)
{
String path = RegistryUtil.getRegistryValue(registryKey);
if (path != null)
{
directory = new File(path);
if (!directory.exists())
{
directory = null;
}
else
{
break;
}
}
}
return directory;
}
/**
* Returns the location of the script to run Mercurial, if Mercurial is
* installed as a Python package from source. Only packages that include a
* templates directory will be recognized.
*
* @param pythonBinary
*
* @return
*/
private File getMercurialScript(String pythonBinary)
{
File hgScript = null;
if (pythonBinary != null)
{
File pythonBinaryFile = new File(pythonBinary);
if (pythonBinaryFile.exists())
{
File pythonDir = pythonBinaryFile.getParentFile();
File scriptsDir = new File(pythonDir, FILE_SCRIPTS);
File potentialHgScript = new File(scriptsDir, FILE_MERCURIAL_SCRIPT);
File mercurialPackageDir = new File(pythonDir, FILE_LIB_MERCURIAL);
File templatesDir = new File(mercurialPackageDir, FILE_TEMPLATES);
if (potentialHgScript.exists() && templatesDir.exists())
{
hgScript = potentialHgScript;
}
}
}
return hgScript;
}
/**
* Method description
*
*
* @return
*/
private String getPythonBinary()
{
String python = RegistryUtil.getRegistryValue(REGISTRY_PYTHON[0]);
if (python == null)
{
python = IOUtil.search(new String[0], "python");
}
return python;
}
}

View File

@@ -34,7 +34,6 @@ import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Xsrf;
import sonia.scm.web.HgUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -45,8 +44,6 @@ import java.util.Map;
@Singleton
public class DefaultHgEnvironmentBuilder implements HgEnvironmentBuilder {
@VisibleForTesting
static final String ENV_PYTHON_PATH = "PYTHONPATH";
@VisibleForTesting
static final String ENV_HOOK_PORT = "SCM_HOOK_PORT";
@VisibleForTesting
@@ -100,7 +97,6 @@ public class DefaultHgEnvironmentBuilder implements HgEnvironmentBuilder {
private void read(ImmutableMap.Builder<String, String> env, Repository repository) {
HgConfig config = repositoryHandler.getConfig();
env.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(config));
File directory = repositoryHandler.getDirectory(repository.getId());

View File

@@ -69,28 +69,6 @@ public class HgConfig extends RepositoryConfig {
return hgBinary;
}
/**
* Method description
*
*
* @return
*/
public String getPythonBinary()
{
return pythonBinary;
}
/**
* Method description
*
*
* @return
*/
public String getPythonPath()
{
return pythonPath;
}
/**
* Method description
*
@@ -102,17 +80,6 @@ public class HgConfig extends RepositoryConfig {
return showRevisionInId;
}
/**
* Method description
*
*
* @return
*/
public boolean isUseOptimizedBytecode()
{
return useOptimizedBytecode;
}
public boolean isEnableHttpPostArgs() {
return enableHttpPostArgs;
}
@@ -126,8 +93,7 @@ public class HgConfig extends RepositoryConfig {
@Override
public boolean isValid()
{
return super.isValid() && Util.isNotEmpty(hgBinary)
&& Util.isNotEmpty(pythonBinary);
return super.isValid() && Util.isNotEmpty(hgBinary);
}
//~--- set methods ----------------------------------------------------------
@@ -154,28 +120,6 @@ public class HgConfig extends RepositoryConfig {
this.hgBinary = hgBinary;
}
/**
* Method description
*
*
* @param pythonBinary
*/
public void setPythonBinary(String pythonBinary)
{
this.pythonBinary = pythonBinary;
}
/**
* Method description
*
*
* @param pythonPath
*/
public void setPythonPath(String pythonPath)
{
this.pythonPath = pythonPath;
}
/**
* Method description
*
@@ -191,17 +135,6 @@ public class HgConfig extends RepositoryConfig {
this.enableHttpPostArgs = enableHttpPostArgs;
}
/**
* Method description
*
*
* @param useOptimizedBytecode
*/
public void setUseOptimizedBytecode(boolean useOptimizedBytecode)
{
this.useOptimizedBytecode = useOptimizedBytecode;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -210,15 +143,6 @@ public class HgConfig extends RepositoryConfig {
/** Field description */
private String hgBinary;
/** Field description */
private String pythonBinary;
/** Field description */
private String pythonPath = "";
/** Field description */
private boolean useOptimizedBytecode = false;
/** Field description */
private boolean showRevisionInId = false;

View File

@@ -26,6 +26,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
//~--- JDK imports ------------------------------------------------------------
@@ -36,16 +37,19 @@ import java.io.File;
*
* @author Sebastian Sdorra
*/
public enum HgPythonScript {
public enum HgExtensions {
HOOK("scmhooks.py"), HGWEB("hgweb.py");
HOOK("scmhooks.py"),
CGISERVE("cgiserve.py"),
VERSION("scmversion.py"),
FILEVIEW("fileview.py");
private static final String BASE_DIRECTORY = "lib".concat(File.separator).concat("python");
private static final String BASE_RESOURCE = "/sonia/scm/python/";
private final String name;
HgPythonScript(String name) {
HgExtensions(String name) {
this.name = name;
}
@@ -53,6 +57,10 @@ public enum HgPythonScript {
return new File(context.getBaseDirectory(), BASE_DIRECTORY);
}
public File getFile() {
return getFile(SCMContext.getContext());
}
public File getFile(SCMContextProvider context) {
return new File(getScriptDirectory(context), name);
}

View File

@@ -30,10 +30,9 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
import sonia.scm.autoconfig.AutoConfigurator;
import sonia.scm.installer.HgInstaller;
import sonia.scm.installer.HgInstallerFactory;
import sonia.scm.io.ExtendedCommand;
import sonia.scm.io.INIConfiguration;
import sonia.scm.io.INIConfigurationWriter;
@@ -45,7 +44,6 @@ import sonia.scm.repository.spi.HgVersionCommand;
import sonia.scm.repository.spi.HgWorkingCopyFactory;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.IOUtil;
import sonia.scm.util.SystemUtil;
import java.io.File;
import java.io.FileNotFoundException;
@@ -53,15 +51,12 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.Optional;
@Singleton
@Extension
public class HgRepositoryHandler
extends AbstractSimpleRepositoryHandler<HgConfig> {
public static final String RESOURCE_VERSION = "sonia/scm/version/scm-hg-plugin";
public static final String TYPE_DISPLAYNAME = "Mercurial";
public static final String TYPE_NAME = "hg";
public static final RepositoryType TYPE = new RepositoryType(
@@ -78,43 +73,25 @@ public class HgRepositoryHandler
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
private final HgWorkingCopyFactory workingCopyFactory;
private final AutoConfigurator configurator;
@Inject
public HgRepositoryHandler(ConfigurationStoreFactory storeFactory,
RepositoryLocationResolver repositoryLocationResolver,
PluginLoader pluginLoader, HgWorkingCopyFactory workingCopyFactory) {
PluginLoader pluginLoader, HgWorkingCopyFactory workingCopyFactory, AutoConfigurator configurator) {
super(storeFactory, repositoryLocationResolver, pluginLoader);
this.workingCopyFactory = workingCopyFactory;
this.configurator = configurator;
}
public void doAutoConfiguration(HgConfig autoConfig) {
HgInstaller installer = HgInstallerFactory.createInstaller();
try {
if (logger.isDebugEnabled()) {
logger.debug("installing mercurial with {}", installer.getClass().getName());
}
installer.install(baseDirectory, autoConfig);
config = autoConfig;
storeConfig();
} catch (IOException ioe) {
if (logger.isErrorEnabled()) {
logger.error("Could not write Hg CGI for inital config. "
+ "HgWeb may not function until a new Hg config is set", ioe);
}
}
configurator.configure(autoConfig);
}
@Override
public void init(SCMContextProvider context) {
super.init(context);
writePythonScripts(context);
// fix wrong hg.bat from package installation
if (SystemUtil.isWindows()) {
HgWindowsPackageFix.fixHgPackage(context, getConfig());
}
writeHgExtensions(context);
}
@Override
@@ -122,20 +99,18 @@ public class HgRepositoryHandler
super.loadConfig();
if (config == null) {
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());
}
config = new HgConfig();
storeConfig();
}
if (!isConfigValid(config)) {
doAutoConfiguration(config);
storeConfig();
}
}
private boolean isConfigValid(HgConfig config) {
return config.isValid() && new HgVerifier().isValid(config);
}
@Override
@@ -154,10 +129,10 @@ public class HgRepositoryHandler
}
String getVersionInformation(HgVersionCommand command) {
String version = getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION);
HgVersion hgVersion = command.get();
logger.debug("mercurial/python informations: {}", hgVersion);
return MessageFormat.format(version, hgVersion.getPython(), hgVersion.getMercurial());
return String.format("scm-hg-version/%s %s",
SCMContext.getContext().getVersion(),
command.get()
);
}
@Override
@@ -176,8 +151,7 @@ public class HgRepositoryHandler
}
/**
* Writes .hg/hgrc, disables hg access control and added scm hook support.
* see HgPermissionFilter
* Writes repository to .hg/hgrc.
*
* @param repository
* @param directory
@@ -205,10 +179,10 @@ public class HgRepositoryHandler
return HgConfig.class;
}
private void writePythonScripts(SCMContextProvider context) {
IOUtil.mkdirs(HgPythonScript.getScriptDirectory(context));
private void writeHgExtensions(SCMContextProvider context) {
IOUtil.mkdirs(HgExtensions.getScriptDirectory(context));
for (HgPythonScript script : HgPythonScript.values()) {
for (HgExtensions script : HgExtensions.values()) {
if (logger.isDebugEnabled()) {
logger.debug("write python script {}", script.getName());
}
@@ -216,16 +190,16 @@ public class HgRepositoryHandler
try (InputStream content = input(script); OutputStream output = output(context, script)) {
IOUtil.copy(content, output);
} catch (IOException ex) {
logger.error("could not write script", ex);
throw new IllegalStateException("could not write hg extension", ex);
}
}
}
private InputStream input(HgPythonScript script) {
private InputStream input(HgExtensions script) {
return HgRepositoryHandler.class.getResourceAsStream(script.getResourcePath());
}
private OutputStream output(SCMContextProvider context, HgPythonScript script) throws FileNotFoundException {
private OutputStream output(SCMContextProvider context, HgExtensions script) throws FileNotFoundException {
return new FileOutputStream(script.getFile(context));
}

View File

@@ -0,0 +1,118 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.io.SimpleCommand;
import sonia.scm.io.SimpleCommandResult;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class HgVerifier {
private static final Logger LOG = LoggerFactory.getLogger(HgVerifier.class);
private final VersionResolver versionResolver;
public HgVerifier() {
this.versionResolver = defaultVersionResolver();
}
HgVerifier(VersionResolver versionResolver) {
this.versionResolver = versionResolver;
}
public boolean isValid(HgConfig config) {
return isValid(config.getHgBinary());
}
public boolean isValid(String hg) {
return isValid(Paths.get(hg));
}
public boolean isValid(Path hg) {
LOG.trace("check if hg binary {} is valid", hg);
if (!Files.isRegularFile(hg)) {
LOG.warn("{} is not a regular file", hg);
return false;
}
if (!Files.isExecutable(hg)) {
LOG.warn("{} is not executable", hg);
return false;
}
try {
String version = versionResolver.resolveVersion(hg);
return isVersionValid(hg, version);
} catch (IOException ex) {
LOG.warn("failed to resolve version of {}: ", hg, ex);
return false;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
LOG.warn("failed to resolve version of {}: ", hg, ex);
return false;
}
}
private boolean isVersionValid(Path hg, String version) {
String[] parts = version.split("\\.");
if (parts.length < 2) {
LOG.warn("{} returned invalid version: {}", hg, version);
return false;
}
try {
int major = Integer.parseInt(parts[0]);
if (major < 4) {
LOG.warn("{} is too old, we need at least mercurial 4.x", hg);
return false;
}
} catch (NumberFormatException ex) {
LOG.warn("{} returned invalid version {}", hg, version);
return false;
}
return true;
}
private VersionResolver defaultVersionResolver() {
return hg -> {
SimpleCommand command = new SimpleCommand(hg.toString(), "version", "--template", "{ver}");
SimpleCommandResult result = command.execute();
if (!result.isSuccessfull()) {
throw new IOException("failed to get version from hg");
}
return result.getOutput().trim();
};
}
@FunctionalInterface
interface VersionResolver {
String resolveVersion(Path hg) throws IOException, InterruptedException;
}
}

View File

@@ -1,330 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Charsets;
import com.google.common.base.Throwables;
import com.google.common.io.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
import sonia.scm.util.IOUtil;
import sonia.scm.web.HgUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
/**
*
* @author Sebastian Sdorra
*/
public final class HgWindowsPackageFix
{
/** Field description */
static final String MODIFY_MARK_01 = ".setbinary";
/** Field description */
static final String MODIFY_MARK_02 = ".setpythonpath";
/** Field description */
private static final String HG_BAT = "hg.bat";
/** Field description */
private static final String HG_PY = "hg.py";
/** Field description */
private static final String PYTHONPATH_FIXED =
"set PYTHONPATH=%~dp0..\\lib;%PYTHONHOME%\\Lib;%PYTHONPATH%";
/** Field description */
private static final String PYTHONPATH_WRONG =
"set PYTHONPATH=%~dp0..\\lib;%PYTHONHOME%\\Lib";
/**
* the logger for HgUtil
*/
private static final Logger logger = LoggerFactory.getLogger(HgUtil.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private HgWindowsPackageFix() {}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param context
* @param config
*/
public static void fixHgPackage(SCMContextProvider context, HgConfig config)
{
if ((config != null) && config.isValid())
{
String basePath = context.getBaseDirectory().getAbsolutePath();
String hg = config.getHgBinary();
if (hg.startsWith(basePath) && hg.endsWith(HG_BAT))
{
File file = new File(hg);
fixHgBat(file);
file = new File(file.getParentFile(), HG_PY);
fixHgPy(file);
}
}
else if (logger.isDebugEnabled())
{
logger.debug(
"could not fix hg.py, because the configuration is not valid");
}
}
/**
* Visible for testing
*
* @param hgBat
*/
static void fixHgBat(File hgBat)
{
if (hgBat.exists())
{
File binDirectory = hgBat.getParentFile();
File modifyMark = new File(binDirectory, MODIFY_MARK_02);
if (!modifyMark.exists())
{
try
{
String content = Files.toString(hgBat, Charsets.UTF_8);
if (!content.contains(PYTHONPATH_FIXED))
{
content = content.replace(PYTHONPATH_WRONG, PYTHONPATH_FIXED);
Files.write(content, hgBat, Charsets.UTF_8);
}
createModifyMark(modifyMark);
}
catch (IOException ex)
{
logger.error("could not read content of {}", hgBat);
throw Throwables.propagate(ex);
}
}
else
{
logger.debug("hg.bat allready fixed");
}
}
else
{
logger.warn("could not find hg.bat at {}", hgBat);
}
}
/**
* Visible for testing
*
*
*
* @param hgPy
*/
static void fixHgPy(File hgPy)
{
if (hgPy.exists())
{
File binDirectory = hgPy.getParentFile();
File modifyMark = new File(binDirectory, MODIFY_MARK_01);
if (!modifyMark.exists())
{
if (logger.isDebugEnabled())
{
logger.debug("check hg.py for setbinary at {}", hgPy);
}
if (!isSetBinaryAvailable(hgPy))
{
injectSetBinary(hgPy);
}
else
{
createModifyMark(modifyMark);
}
}
else if (logger.isDebugEnabled())
{
logger.debug("hg.py allready fixed");
}
}
else if (logger.isWarnEnabled())
{
logger.warn("could not find hg.py at {}", hgPy);
}
}
//~--- get methods ----------------------------------------------------------
/**
* Visible for testing
*
*
* @param hg
*
* @return
*/
static boolean isSetBinaryAvailable(File hg)
{
boolean setBinaryAvailable = false;
BufferedReader reader = null;
try
{
reader = Files.newReader(hg, Charsets.UTF_8);
String line = reader.readLine();
while (line != null)
{
line = line.trim();
if (line.contains("mercurial.util.setbinary") &&!line.startsWith("#"))
{
setBinaryAvailable = true;
break;
}
line = reader.readLine();
}
}
catch (IOException ex)
{
logger.error("could not check hg.bat", ex);
}
finally
{
IOUtil.close(reader);
}
return setBinaryAvailable;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param modifyMark
*/
private static void createModifyMark(File modifyMark)
{
try
{
if (!modifyMark.createNewFile())
{
throw new RuntimeException("could not create modify mark");
}
}
catch (IOException ex)
{
throw new RuntimeException("could not create modify mark", ex);
}
}
/**
* Method description
*
*
* @param hg
*/
private static void injectSetBinary(File hg)
{
String lineSeparator = System.getProperty("line.separator");
File mod = new File(hg.getParentFile(), MODIFY_MARK_01);
hg.renameTo(mod);
BufferedWriter writer = null;
BufferedReader reader = null;
try
{
writer = Files.newWriter(hg, Charsets.UTF_8);
reader = Files.newReader(mod, Charsets.UTF_8);
String line = reader.readLine();
while (line != null)
{
if (line.trim().equals("mercurial.dispatch.run()"))
{
writer.write("for fp in (sys.stdin, sys.stdout, sys.stderr):");
writer.write(lineSeparator);
writer.write(" mercurial.util.setbinary(fp)");
writer.write(lineSeparator);
writer.write(lineSeparator);
}
writer.write(line);
writer.write(lineSeparator);
line = reader.readLine();
}
}
catch (IOException ex)
{
logger.error("could not check hg.bat", ex);
}
finally
{
IOUtil.close(reader);
IOUtil.close(writer);
}
}
}

View File

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

View File

@@ -32,6 +32,7 @@ import com.aragost.javahg.commands.PullCommand;
import com.aragost.javahg.commands.StatusCommand;
import com.aragost.javahg.commands.UpdateCommand;
import com.aragost.javahg.commands.flags.CloneCommandFlags;
import sonia.scm.repository.HgExtensions;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.work.SimpleWorkingCopyFactory;
import sonia.scm.repository.work.WorkingCopyPool;
@@ -99,7 +100,8 @@ public class SimpleHgWorkingCopyFactory extends SimpleWorkingCopyFactory<Reposit
@Override
public void configure(PullCommand pullCommand) {
pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.post_hook");
pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.pre_hook");
String hooks = HgExtensions.HOOK.getFile().getAbsolutePath();
pullCommand.cmdAppend("--config", String.format("hooks.pretxnchangegroup.scm=python:%s:pre_hook", hooks));
pullCommand.cmdAppend("--config", String.format("hooks.changegroup.scm=python:%s:post_hook", hooks));
}
}

View File

@@ -21,51 +21,29 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi.javahg;
//~--- non-JDK imports --------------------------------------------------------
import com.aragost.javahg.MercurialExtension;
import com.aragost.javahg.internals.Utils;
import sonia.scm.repository.HgExtensions;
/**
*
* @author Sebastian Sdorra
*/
public class HgFileviewExtension extends MercurialExtension
{
public class HgFileviewExtension extends MercurialExtension {
/** Field description */
static final String NAME = "fileview";
/** Field description */
private static final String PATH =
Utils.resourceAsFile("/sonia/scm/hg/ext/fileview.py").getAbsolutePath();
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public String getName()
{
public String getName() {
return NAME;
}
/**
* Method description
*
*
* @return
*/
@Override
public String getPath()
{
return PATH;
public String getPath() {
return HgExtensions.FILEVIEW.getFile().getAbsolutePath();
}
}

View File

@@ -33,14 +33,15 @@ import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgEnvironmentBuilder;
import sonia.scm.repository.HgPythonScript;
import sonia.scm.repository.HgExtensions;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryRequestListenerUtil;
import sonia.scm.repository.spi.ScmProviderHttpServlet;
import sonia.scm.util.AssertUtil;
import sonia.scm.web.cgi.CGIExecutor;
import sonia.scm.web.cgi.CGIExecutorFactory;
import javax.annotation.Nonnull;
import sonia.scm.web.cgi.EnvList;
import javax.servlet.ServletException;
@@ -49,8 +50,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
import java.util.ArrayList;
import java.util.List;
/**
*
@@ -81,7 +82,7 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
this.requestListenerUtil = requestListenerUtil;
this.environmentBuilder = environmentBuilder;
this.exceptionHandler = new HgCGIExceptionHandler();
this.command = HgPythonScript.HGWEB.getFile(SCMContext.getContext());
this.extension = HgExtensions.CGISERVE.getFile(SCMContext.getContext());
}
//~--- methods --------------------------------------------------------------
@@ -108,17 +109,6 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
}
}
/**
* Method description
*
*
* @param request
* @param response
* @param repository
*
* @throws IOException
* @throws ServletException
*/
private void handleRequest(HttpServletRequest request,
HttpServletResponse response, Repository repository)
throws ServletException, IOException
@@ -135,17 +125,6 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
}
}
/**
* Method description
*
*
* @param request
* @param response
* @param repository
*
* @throws IOException
* @throws ServletException
*/
private void process(HttpServletRequest request,
HttpServletResponse response, Repository repository)
throws IOException, ServletException
@@ -162,38 +141,33 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
EnvList env = executor.getEnvironment();
environmentBuilder.write(repository).forEach(env::set);
String interpreter = getInterpreter();
File directory = handler.getDirectory(repository.getId());
executor.setWorkDirectory(directory);
executor.setArgs(createArgs());
if (interpreter != null)
{
executor.setInterpreter(interpreter);
}
executor.execute(command.getAbsolutePath());
HgConfig config = handler.getConfig();
executor.execute(config.getHgBinary());
}
//~--- get methods ----------------------------------------------------------
@Nonnull
private List<String> createArgs() {
List<String> args = new ArrayList<>();
config(args, "extensions.cgiserve", extension.getAbsolutePath());
/**
* Method description
*
*
* @return
*/
private String getInterpreter()
{
HgConfig config = handler.getConfig();
String hooks = HgExtensions.HOOK.getFile().getAbsolutePath();
config(args, "hooks.pretxnchangegroup.scm", String.format("python:%s:pre_hook", hooks));
config(args, "hooks.changegroup.scm", String.format("python:%s:post_hook", hooks));
AssertUtil.assertIsNotNull(config);
config(args, "web.push_ssl", "false");
config(args, "web.allow_read", "*");
config(args, "web.allow_push", "*");
args.add("cgiserve");
return args;
}
String python = config.getPythonBinary();
if ((python != null) && config.isUseOptimizedBytecode())
{
python = python.concat(" -O");
}
return python;
private void config(List<String> args, String key, String value) {
args.add("--config");
args.add(key + "=" + value);
}
//~--- fields ---------------------------------------------------------------
@@ -202,7 +176,7 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
private final CGIExecutorFactory cgiExecutorFactory;
/** Field description */
private final File command;
private final File extension;
/** Field description */
private final ScmConfiguration configuration;

View File

@@ -29,10 +29,7 @@ package sonia.scm.web;
import com.google.inject.servlet.ServletModule;
import org.mapstruct.factory.Mappers;
import sonia.scm.api.v2.resources.HgConfigDtoToHgConfigMapper;
import sonia.scm.api.v2.resources.HgConfigInstallationsToDtoMapper;
import sonia.scm.api.v2.resources.HgConfigPackagesToDtoMapper;
import sonia.scm.api.v2.resources.HgConfigToHgConfigDtoMapper;
import sonia.scm.installer.HgPackageReader;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.spi.HgWorkingCopyFactory;
import sonia.scm.repository.spi.SimpleHgWorkingCopyFactory;
@@ -46,12 +43,8 @@ public class HgServletModule extends ServletModule {
@Override
protected void configureServlets() {
bind(HgPackageReader.class);
bind(HgConfigDtoToHgConfigMapper.class).to(Mappers.getMapper(HgConfigDtoToHgConfigMapper.class).getClass());
bind(HgConfigToHgConfigDtoMapper.class).to(Mappers.getMapper(HgConfigToHgConfigDtoMapper.class).getClass());
bind(HgConfigPackagesToDtoMapper.class).to(Mappers.getMapper(HgConfigPackagesToDtoMapper.class).getClass());
bind(HgConfigInstallationsToDtoMapper.class);
bind(HgWorkingCopyFactory.class).to(SimpleHgWorkingCopyFactory.class);
}

View File

@@ -24,13 +24,8 @@
package sonia.scm.web;
import sonia.scm.SCMContext;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgPythonScript;
import sonia.scm.util.Util;
import java.io.File;
/**
*
* @author Sebastian Sdorra
@@ -41,26 +36,6 @@ public final class HgUtil {
private HgUtil() {}
public static String getPythonPath(HgConfig config) {
String pythonPath = Util.EMPTY_STRING;
if (config != null) {
pythonPath = Util.nonNull(config.getPythonPath());
}
if (Util.isNotEmpty(pythonPath)) {
pythonPath = pythonPath.concat(File.pathSeparator);
}
pythonPath = pythonPath.concat(
HgPythonScript.getScriptDirectory(
SCMContext.getContext()
).getAbsolutePath()
);
return pythonPath;
}
public static String getRevision(String revision) {
return Util.isEmpty(revision) ? REVISION_TIP : revision;
}

View File

@@ -28,10 +28,7 @@ import { InputField, Checkbox } from "@scm-manager/ui-components";
type Configuration = {
hgBinary: string;
pythonBinary: string;
pythonPath?: string;
encoding: string;
useOptimizedBytecode: boolean;
showRevisionInId: boolean;
enableHttpPostArgs: boolean;
_links: Links;
@@ -58,7 +55,7 @@ class HgConfigurationForm extends React.Component<Props, State> {
}
updateValidationStatus = () => {
const requiredFields = ["hgBinary", "pythonBinary", "encoding"];
const requiredFields = ["hgBinary", "encoding"];
const validationErrors = [];
for (const field of requiredFields) {
@@ -130,11 +127,8 @@ class HgConfigurationForm extends React.Component<Props, State> {
return (
<div className="columns is-multiline">
{this.inputField("hgBinary")}
{this.inputField("pythonBinary")}
{this.inputField("pythonPath")}
{this.inputField("encoding")}
<div className="column is-half">
{this.checkbox("useOptimizedBytecode")}
{this.checkbox("showRevisionInId")}
</div>
<div className="column is-half">

View File

@@ -13,14 +13,8 @@
"title": "Mercurial Konfiguration",
"hgBinary": "HG Binary",
"hgBinaryHelpText": "Pfad des Mercurial Binary.",
"pythonBinary": "Python Binary",
"pythonBinaryHelpText": "Pfad des Python binary.",
"pythonPath": "Python Module Such Pfad",
"pythonPathHelpText": "Python Module Such Pfad (PYTHONPATH).",
"encoding": "Encoding",
"encodingHelpText": "Repository Encoding.",
"useOptimizedBytecode": "Optimized Bytecode (.pyo)",
"useOptimizedBytecodeHelpText": "Verwende den Python '-O' Switch.",
"showRevisionInId": "Revision anzeigen",
"showRevisionInIdHelpText": "Die Revision als Teil der Node ID anzeigen.",
"enableHttpPostArgs": "HttpPostArgs Protocol aktivieren",

View File

@@ -13,14 +13,8 @@
"title": "Mercurial Configuration",
"hgBinary": "HG Binary",
"hgBinaryHelpText": "Location of Mercurial binary.",
"pythonBinary": "Python Binary",
"pythonBinaryHelpText": "Location of Python binary.",
"pythonPath": "Python Module Search Path",
"pythonPathHelpText": "Python Module Search Path (PYTHONPATH).",
"encoding": "Encoding",
"encodingHelpText": "Repository Encoding.",
"useOptimizedBytecode": "Optimized Bytecode (.pyo)",
"useOptimizedBytecodeHelpText": "Use the Python '-O' switch.",
"showRevisionInId": "Show Revision",
"showRevisionInIdHelpText": "Show revision as part of the node id.",
"enableHttpPostArgs": "Enable HttpPostArgs Protocol",

View File

@@ -22,33 +22,20 @@
# SOFTWARE.
#
import os
from mercurial import demandimport, ui as uimod, hg
from mercurial import registrar
from mercurial.hgweb import hgweb, wsgicgi
demandimport.enable()
cmdtable = {}
try:
u = uimod.ui.load()
except AttributeError:
# For installations earlier than Mercurial 4.1
u = uimod.ui()
from mercurial import registrar
command = registrar.command(cmdtable)
except (AttributeError, ImportError):
# Fallback to hg < 4.3 support
from mercurial import cmdutil
command = cmdutil.command(cmdtable)
u.setconfig(b'web', b'push_ssl', b'false')
u.setconfig(b'web', b'allow_read', b'*')
u.setconfig(b'web', b'allow_push', b'*')
u.setconfig(b'hooks', b'changegroup.scm', b'python:scmhooks.post_hook')
u.setconfig(b'hooks', b'pretxnchangegroup.scm', b'python:scmhooks.pre_hook')
# pass SCM_HTTP_POST_ARGS to enable experimental httppostargs protocol of mercurial
# SCM_HTTP_POST_ARGS is set by HgCGIServlet
# Issue 970: https://goo.gl/poascp
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'].encode())
application = hgweb(r)
wsgicgi.launch(application)
@command(b'cgiserve')
def cgiserve(ui, repo, **opts):
application = hgweb(repo)
wsgicgi.launch(application)

View File

@@ -0,0 +1,41 @@
#
# 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.
#
from mercurial import util
import platform
cmdtable = {}
try:
from mercurial import registrar
command = registrar.command(cmdtable)
except (AttributeError, ImportError):
# Fallback to hg < 4.3 support
from mercurial import cmdutil
command = cmdutil.command(cmdtable)
@command(b'scmversion', norepo=True)
def scmversion(ui, **opts):
pythonVersion = platform.python_version().encode("utf-8")
ui.write(b"python/%s mercurial/%s\n" % (pythonVersion, util.version()))

View File

@@ -1 +0,0 @@
scm-hg-version/${project.version} python/{0} mercurial/{1}

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.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
@@ -79,8 +79,8 @@ public class HgConfigAutoConfigurationResourceTest {
when(resourceProvider.get()).thenReturn(resource);
dispatcher.addSingletonResource(
new HgConfigResource(null, null, null, null,
resourceProvider, null));
new HgConfigResource(null, null, null,
resourceProvider));
}
@Test

View File

@@ -48,10 +48,7 @@ public class HgConfigDtoToHgConfigMapperTest {
assertEquals("ABC", config.getEncoding());
assertEquals("/etc/hg", config.getHgBinary());
assertEquals("/py", config.getPythonBinary());
assertEquals("/etc/", config.getPythonPath());
assertTrue(config.isShowRevisionInId());
assertTrue(config.isUseOptimizedBytecode());
assertTrue(config.isEnableHttpPostArgs());
}
@@ -60,10 +57,7 @@ public class HgConfigDtoToHgConfigMapperTest {
configDto.setDisabled(true);
configDto.setEncoding("ABC");
configDto.setHgBinary("/etc/hg");
configDto.setPythonBinary("/py");
configDto.setPythonPath("/etc/");
configDto.setShowRevisionInId(true);
configDto.setUseOptimizedBytecode(true);
configDto.setEnableHttpPostArgs(true);
return configDto;

View File

@@ -1,139 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.web.RestDispatcher;
import javax.inject.Provider;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.net.URISyntaxException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
@SubjectAware(
configuration = "classpath:sonia/scm/configuration/shiro.ini",
password = "secret"
)
@RunWith(MockitoJUnitRunner.class)
public class HgConfigInstallationsResourceTest {
@Rule
public ShiroRule shiro = new ShiroRule();
private RestDispatcher dispatcher = new RestDispatcher();
private final URI baseUri = URI.create("/");
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ScmPathInfoStore scmPathInfoStore;
@InjectMocks
private HgConfigInstallationsToDtoMapper mapper;
@Mock
private Provider<HgConfigInstallationsResource> resourceProvider;
@Before
public void prepareEnvironment() {
HgConfigInstallationsResource resource = new HgConfigInstallationsResource(mapper);
when(resourceProvider.get()).thenReturn(resource);
dispatcher.addSingletonResource(
new HgConfigResource(null, null, null, null,
null, resourceProvider));
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
}
@Test
@SubjectAware(username = "readOnly")
public void shouldGetHgInstallations() throws Exception {
MockHttpResponse response = get("hg");
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
String contentAsString = response.getContentAsString();
assertThat(contentAsString).contains("{\"paths\":[");
assertThat(contentAsString).contains("hg");
assertThat(contentAsString).doesNotContain("python");
assertThat(contentAsString).contains("\"self\":{\"href\":\"/v2/config/hg/installations/hg");
}
@Test
@SubjectAware(username = "writeOnly")
public void shouldNotGetHgInstallationsWhenNotAuthorized() throws Exception {
MockHttpResponse response = get("hg");
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
}
@Test
@SubjectAware(username = "readOnly")
public void shouldGetPythonInstallations() throws Exception {
MockHttpResponse response = get("python");
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
String contentAsString = response.getContentAsString();
assertThat(contentAsString).contains("{\"paths\":[");
assertThat(contentAsString).contains("python");
assertThat(contentAsString).contains("\"self\":{\"href\":\"/v2/config/hg/installations/python");
}
@Test
@SubjectAware(username = "writeOnly")
public void shouldNotGetPythonInstallationsWhenNotAuthorized() throws Exception {
MockHttpResponse response = get("python");
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
}
private MockHttpResponse get(String path) throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + HgConfigResource.HG_CONFIG_PATH_V2 + "/installations/" + path);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
return response;
}
}

View File

@@ -1,228 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.installer.HgPackage;
import sonia.scm.installer.HgPackageReader;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.web.RestDispatcher;
import javax.inject.Provider;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import static sonia.scm.api.v2.resources.HgConfigTests.createPackage;
@SubjectAware(
configuration = "classpath:sonia/scm/configuration/shiro.ini",
password = "secret"
)
@RunWith(MockitoJUnitRunner.class)
public class HgConfigPackageResourceTest {
public static final String URI = "/" + HgConfigResource.HG_CONFIG_PATH_V2 + "/packages";
@Rule
public ShiroRule shiro = new ShiroRule();
private RestDispatcher dispatcher = new RestDispatcher();
private final URI baseUri = java.net.URI.create("/");
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ScmPathInfoStore scmPathInfoStore;
@InjectMocks
private HgConfigPackagesToDtoMapperImpl mapper;
@Mock
private HgRepositoryHandler repositoryHandler;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private HgPackageReader hgPackageReader;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AdvancedHttpClient advancedHttpClient;
@Mock
private Provider<HgConfigPackageResource> hgConfigPackageResourceProvider;
@Mock
private HgPackage hgPackage;
@Before
public void prepareEnvironment() {
setupResources();
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
when(hgPackageReader.getPackages().getPackages()).thenReturn(createPackages());
}
@Test
@SubjectAware(username = "readOnly")
public void shouldGetPackages() throws Exception {
MockHttpResponse response = get();
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
String responseString = response.getContentAsString();
ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class);
JsonNode packages = responseJson.get("packages");
assertThat(packages).isNotNull();
assertThat(packages).hasSize(2);
JsonNode package1 = packages.get(0);
assertThat(package1.get("_links")).isNull();
JsonNode hgConfigTemplate = package1.get("hgConfigTemplate");
assertThat(hgConfigTemplate).isNotNull();
assertThat(hgConfigTemplate.get("_links")).isNull();
assertThat(responseString).contains("\"_links\":{\"self\":{\"href\":\"/v2/config/hg/packages");
}
@Test
@SubjectAware(username = "writeOnly")
public void shouldNotGetPackagesWhenNotAuthorized() throws Exception {
MockHttpResponse response = get();
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
}
@Test
@SubjectAware(username = "writeOnly")
public void shouldInstallPackage() throws Exception {
String packgeId = "ourPackage";
String url = "http://url";
setupPackageInstallation(packgeId, url);
when(advancedHttpClient.get(url).request().contentAsStream())
.thenReturn(new ByteArrayInputStream("mockedFile".getBytes()));
MockHttpResponse response = put(packgeId);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
}
@Test
@SubjectAware(username = "writeOnly")
public void shouldHandleFailingInstallation() throws Exception {
String packgeId = "ourPackage";
String url = "http://url";
setupPackageInstallation(packgeId, url);
when(advancedHttpClient.get(url).request().contentAsStream())
.thenThrow(new IOException("mocked Exception"));
MockHttpResponse response = put(packgeId);
assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus());
}
@Test
@SubjectAware(username = "writeOnly")
public void shouldHandlePackagesThatAreNotFound() throws Exception {
String packageId = "this-package-does-not-ex";
when(hgPackageReader.getPackage(packageId)).thenReturn(null);
MockHttpResponse response = put(packageId);
assertEquals(HttpServletResponse.SC_NOT_FOUND, response.getStatus());
}
@Test
@SubjectAware(username = "readOnly")
public void shouldNotInstallPackageWhenNotAuthorized() throws Exception {
MockHttpResponse response = put("don-t-care");
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
}
private List<HgPackage> createPackages() {
return Arrays.asList(createPackage(), new HgPackage());
}
private MockHttpResponse get() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get(URI);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
return response;
}
private MockHttpResponse put(String pckgId) throws URISyntaxException {
String packgeIdParam = "";
if (pckgId != null) {
packgeIdParam = "/" + pckgId;
}
MockHttpRequest request = MockHttpRequest.put(URI + packgeIdParam);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
return response;
}
private void setupResources() {
HgConfigPackageResource hgConfigPackageResource =
new HgConfigPackageResource(hgPackageReader, advancedHttpClient, repositoryHandler, mapper);
when(hgConfigPackageResourceProvider.get()).thenReturn(hgConfigPackageResource);
dispatcher.addSingletonResource(
new HgConfigResource(null, null, null,
hgConfigPackageResourceProvider, null, null));
}
private void setupPackageInstallation(String packgeId, String url) throws IOException {
when(hgPackage.getId()).thenReturn(packgeId);
when(hgPackageReader.getPackage(packgeId)).thenReturn(hgPackage);
when(repositoryHandler.getConfig()).thenReturn(new HgConfig());
when(hgPackage.getHgConfigTemplate()).thenReturn(new HgConfig());
when(hgPackage.getUrl()).thenReturn(url);
}
}

View File

@@ -1,90 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.installer.HgPackage;
import sonia.scm.installer.HgPackages;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import static sonia.scm.api.v2.resources.HgConfigTests.assertEqualsPackage;
import static sonia.scm.api.v2.resources.HgConfigTests.createPackage;
@RunWith(MockitoJUnitRunner.class)
public class HgConfigPackagesToDtoMapperTest {
private URI baseUri = URI.create("http://example.com/base/");
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ScmPathInfoStore scmPathInfoStore;
@InjectMocks
private HgConfigPackagesToDtoMapperImpl mapper;
private URI expectedBaseUri;
@Before
public void init() {
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/packages");
}
@Test
public void shouldMapFields() {
HgPackages hgPackages = new HgPackages();
hgPackages.setPackages(createPackages());
HgConfigPackagesDto dto = mapper.map(hgPackages);
assertThat(dto.getPackages()).hasSize(2);
HgConfigPackagesDto.HgConfigPackageDto hgPackageDto1 = dto.getPackages().get(0);
assertEqualsPackage(hgPackageDto1);
HgConfigPackagesDto.HgConfigPackageDto hgPackageDto2 = dto.getPackages().get(1);
// Just verify a random field
assertThat(hgPackageDto2.getId()).isNull();
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
}
private List<HgPackage> createPackages() {
return Arrays.asList(createPackage(), new HgPackage());
}
}

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.api.v2.resources;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -81,22 +81,15 @@ public class HgConfigResourceTest {
@Mock
private HgRepositoryHandler repositoryHandler;
@Mock
private Provider<HgConfigPackageResource> packagesResource;
@Mock
private Provider<HgConfigAutoConfigurationResource> autoconfigResource;
@Mock
private Provider<HgConfigInstallationsResource> installationsResource;
@Before
public void prepareEnvironment() {
HgConfig gitConfig = createConfiguration();
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
HgConfigResource gitConfigResource =
new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource,
autoconfigResource, installationsResource);
new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, autoconfigResource);
dispatcher.addSingletonResource(gitConfigResource);
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
}

View File

@@ -21,10 +21,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import sonia.scm.installer.HgPackage;
import sonia.scm.repository.HgConfig;
import static org.junit.Assert.assertEquals;
@@ -41,10 +40,7 @@ class HgConfigTests {
config.setEncoding("ABC");
config.setHgBinary("/etc/hg");
config.setPythonBinary("/py");
config.setPythonPath("/etc/");
config.setShowRevisionInId(true);
config.setUseOptimizedBytecode(true);
return config;
}
@@ -54,36 +50,7 @@ class HgConfigTests {
assertEquals("ABC", dto.getEncoding());
assertEquals("/etc/hg", dto.getHgBinary());
assertEquals("/py", dto.getPythonBinary());
assertEquals("/etc/", dto.getPythonPath());
assertTrue(dto.isShowRevisionInId());
assertTrue(dto.isUseOptimizedBytecode());
}
static HgPackage createPackage() {
HgPackage hgPackage= new HgPackage();
hgPackage.setArch("arch");
hgPackage.setId("1");
hgPackage.setHgVersion("2");
hgPackage.setPlatform("someOs");
hgPackage.setPythonVersion("3");
hgPackage.setSize(4);
hgPackage.setUrl("https://package");
hgPackage.setHgConfigTemplate(createConfiguration());
return hgPackage;
}
static void assertEqualsPackage(HgConfigPackagesDto.HgConfigPackageDto dto) {
assertEquals("arch", dto.getArch());
assertEquals("1", dto.getId());
assertEquals("2", dto.getHgVersion());
assertEquals("someOs", dto.getPlatform());
assertEquals("3", dto.getPythonVersion());
assertEquals(4, dto.getSize());
assertEquals("https://package", dto.getUrl());
assertEqualsConfiguration(dto.getHgConfigTemplate());
assertTrue(dto.getHgConfigTemplate().getLinks().isEmpty());
}
}

View File

@@ -22,27 +22,21 @@
* SOFTWARE.
*/
package sonia.scm.repository;
package sonia.scm.autoconfig;
import lombok.AllArgsConstructor;
import lombok.Data;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.junit.jupiter.api.Test;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
* @author Sebastian Sdorra
*/
@Data
@AllArgsConstructor
@XmlRootElement(name = "version")
@XmlAccessorType(XmlAccessType.FIELD)
public class HgVersion {
class AutoConfigModuleTest {
public static final String UNKNOWN = "x.y.z (unknown)";
@Test
void shouldBindAutoConfigurator() {
Injector injector = Guice.createInjector(new AutoConfigModule());
AutoConfigurator configurator = injector.getInstance(AutoConfigurator.class);
assertThat(configurator).isNotNull();
}
private String mercurial;
private String python;
}

View File

@@ -21,55 +21,48 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
package sonia.scm.autoconfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.Platform;
import sonia.scm.repository.HgVerifier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class HgConfigInstallationsToDtoMapperTest {
@ExtendWith(MockitoExtension.class)
class AutoConfiguratorProviderTest {
@Mock
private HgVerifier verifier;
private URI baseUri = URI.create("http://example.com/base/");
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ScmPathInfoStore scmPathInfoStore;
@Mock
private Platform platform;
@InjectMocks
private HgConfigInstallationsToDtoMapper mapper;
private AutoConfiguratorProvider provider;
private URI expectedBaseUri;
@Test
void shouldReturnPosixAutoConfiguration() {
when(platform.isPosix()).thenReturn(true);
private String expectedPath = "path";
@Before
public void init() {
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/installations/" + expectedPath);
assertThat(provider.get()).isInstanceOf(PosixAutoConfigurator.class);
}
@Test
public void shouldMapFields() {
List<String> installations = Arrays.asList("/hg", "/bin/hg");
void shouldReturnWindowsAutoConfiguration() {
when(platform.isWindows()).thenReturn(true);
HgConfigInstallationsDto dto = mapper.map(installations, expectedPath);
assertThat(provider.get()).isInstanceOf(WindowsAutoConfigurator.class);
}
assertThat(dto.getPaths()).isEqualTo(installations);
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
@Test
void shouldReturnNoOpAutoConfiguration() {
assertThat(provider.get()).isInstanceOf(NoOpAutoConfigurator.class);
}
}

View File

@@ -28,139 +28,101 @@ 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.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVerifier;
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;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PosixAutoConfiguratorTest {
@Test
void shouldConfigureWithShebangPath(@TempDir Path directory) throws IOException {
Path hg = directory.resolve("hg");
Path python = directory.resolve("python");
@Mock
private HgVerifier verifier;
Files.write(hg, ("#!" + python.toAbsolutePath().toString()).getBytes(StandardCharsets.UTF_8));
Files.createFile(python);
@Test
void shouldConfigureMercurial(@TempDir Path directory) {
Path hg = directory.resolve("hg");
when(verifier.isValid(hg)).thenReturn(true);
PosixAutoConfigurator configurator = create(directory);
HgConfig config = configurator.configure();
HgConfig config = new HgConfig();
configurator.configure(config);
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 PosixAutoConfigurator create(Path directory) {
return new PosixAutoConfigurator(verifier, 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 {
void shouldFindMercurialInAdditionalPath(@TempDir Path directory) {
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);
when(verifier.isValid(hg)).thenReturn(true);
PosixAutoConfigurator configurator = new PosixAutoConfigurator(
createEnv(def), ImmutableList.of(additional.toAbsolutePath().toString())
verifier, createEnv(def), ImmutableList.of(additional.toAbsolutePath().toString())
);
HgConfig config = configurator.configure();
HgConfig config = new HgConfig();
configurator.configure(config);
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);
void shouldSkipInvalidMercurialInstallations(@TempDir Path directory) {
Path one = directory.resolve("one");
Path two = directory.resolve("two");
Path three = directory.resolve("three");
Path hg = three.resolve("hg");
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));
when(verifier.isValid(any(Path.class))).then(ic -> {
Path path = ic.getArgument(0, Path.class);
return path.equals(hg);
});
HgConfig config = configurator.configure();
PosixAutoConfigurator configurator = new PosixAutoConfigurator(
verifier, createEnv(one), ImmutableList.of(
two.toAbsolutePath().toString(),
three.toAbsolutePath().toString()
)
);
HgConfig config = new HgConfig();
configurator.configure(config);
assertThat(config.getHgBinary()).isEqualTo(hg.toString());
assertThat(config.getPythonPath()).isEqualTo(modules.toString());
}
@Test
void shouldNotConfigureMercurial(@TempDir Path directory) {
PosixAutoConfigurator configurator = create(directory);
HgConfig config = new HgConfig();
configurator.configure(config);
assertThat(config.getHgBinary()).isNull();
}
}

View File

@@ -0,0 +1,143 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.autoconfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVerifier;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static sonia.scm.autoconfig.WindowsAutoConfigurator.*;
@ExtendWith(MockitoExtension.class)
class WindowsAutoConfiguratorTest {
@Mock
private HgVerifier verifier;
@Mock
private WindowsRegistry registry;
@ParameterizedTest
@ValueSource(strings = { BINARY_HG_EXE, BINARY_HG_BAT })
void shouldConfigureHgFromPath(String binary, @TempDir Path directory) {
Path hgPath = directory.resolve(binary);
mockIsValid(hgPath);
String hg = configure(directory);
assertThat(hg).isEqualTo(hgPath.toString());
}
@ParameterizedTest
@ValueSource(strings = { BINARY_HG_EXE, BINARY_HG_BAT })
void shouldConfigureHgFromSecondPath(String binary, @TempDir Path directory) {
Path one = directory.resolve("one");
Path two = directory.resolve("two");
Path hgPath = two.resolve(binary);
mockIsValid(hgPath);
String hg = configure(one, two);
assertThat(hg).isEqualTo(hgPath.toString());
}
@ParameterizedTest
@ValueSource(strings = { REGISTRY_KEY_TORTOISE_HG, REGISTRY_KEY_MERCURIAL })
void shouldConfigureHgFromHgInstallation(String registryKey, @TempDir Path directory) {
Path one = directory.resolve("one");
Path two = directory.resolve("two");
Path hgPath = two.resolve(BINARY_HG_EXE);
mockIsValid(hgPath);
mockRegistryKey(registryKey, two.toString());
String hg = configure(one);
assertThat(hg).isEqualTo(hgPath.toString());
}
@Test
void shouldNotConfigureMercurial(@TempDir Path directory) {
String hg = configure(directory);
assertThat(hg).isNull();
}
private void mockRegistryKey(String key, String value) {
when(registry.get(anyString())).then(ic -> {
String k = ic.getArgument(0, String.class);
if (key.equals(k)) {
return Optional.of(value);
}
return Optional.empty();
});
}
private void mockIsValid(Path path) {
when(verifier.isValid(any(Path.class))).then(ic -> {
Path p = ic.getArgument(0, Path.class);
return path.equals(p);
});
}
private String path(Path... paths) {
return Arrays.stream(paths)
.map(Path::toString)
.collect(
Collectors.joining(File.pathSeparator)
);
}
private String configure(Path... paths) {
return configure(path(paths));
}
private String configure(String path) {
HgConfig config = new HgConfig();
configurator(path).configure(config);
return config.getHgBinary();
}
private WindowsAutoConfigurator configurator(String path) {
Map<String, String> env = new HashMap<>();
env.put(ENV_PATH, path);
return new WindowsAutoConfigurator(verifier, registry, env);
}
}

View File

@@ -53,7 +53,6 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.repository.DefaultHgEnvironmentBuilder.*;
@ExtendWith(MockitoExtension.class)
class DefaultHgEnvironmentBuilderTest {
@@ -83,18 +82,18 @@ class DefaultHgEnvironmentBuilderTest {
@Test
void shouldReturnReadEnvironment() {
Repository heartOfGold = prepareForRead("/usr/lib/python", "42");
Repository heartOfGold = prepareForRead("42");
Map<String, String> env = builder.read(heartOfGold);
assertReadEnv(env, "/usr/lib/python", "42");
assertReadEnv(env, "42");
}
@Test
void shouldReturnWriteEnvironment() throws IOException {
Repository heartOfGold = prepareForWrite("/opt/python", "21");
Repository heartOfGold = prepareForWrite("21");
Map<String, String> env = builder.write(heartOfGold);
assertReadEnv(env, "/opt/python", "21");
assertReadEnv(env, "21");
String bearer = CipherUtil.getInstance().decode(env.get(ENV_BEARER_TOKEN));
assertThat(bearer).isEqualTo("secretAC");
@@ -106,7 +105,7 @@ class DefaultHgEnvironmentBuilderTest {
@Test
void shouldSetTransactionId() throws IOException {
TransactionId.set("ti42");
Repository heartOfGold = prepareForWrite("/opt/python", "21");
Repository heartOfGold = prepareForWrite("21");
Map<String, String> env = builder.write(heartOfGold);
assertThat(env).containsEntry(ENV_TRANSACTION_ID, "ti42");
}
@@ -114,12 +113,12 @@ class DefaultHgEnvironmentBuilderTest {
@Test
void shouldThrowIllegalStateIfServerCouldNotBeStarted() throws IOException {
when(server.start()).thenThrow(new IOException("failed to start"));
Repository repository = prepareForRead("/usr", "42");
Repository repository = prepareForRead("42");
assertThrows(IllegalStateException.class, () -> builder.write(repository));
}
private Repository prepareForWrite(String pythonPath, String id) throws IOException {
Repository heartOfGold = prepareForRead(pythonPath, id);
private Repository prepareForWrite(String id) throws IOException {
Repository heartOfGold = prepareForRead(id);
applyAccessToken("secretAC");
when(server.start()).thenReturn(2042);
when(hookEnvironment.getChallenge()).thenReturn("challenge");
@@ -133,21 +132,19 @@ class DefaultHgEnvironmentBuilderTest {
}
private void assertReadEnv(Map<String, String> env, String pythonPath, String repositoryId) {
private void assertReadEnv(Map<String, String> env, String repositoryId) {
assertThat(env)
.containsEntry(ENV_REPOSITORY_ID, repositoryId)
.containsEntry(ENV_REPOSITORY_NAME, "hitchhiker/HeartOfGold")
.containsEntry(ENV_HTTP_POST_ARGS, "false")
.containsEntry(ENV_REPOSITORY_PATH, directory.resolve("repo").toAbsolutePath().toString())
.containsEntry(ENV_PYTHON_PATH, pythonPath + File.pathSeparator + directory.resolve("home/lib/python"));
.containsEntry(ENV_REPOSITORY_PATH, directory.resolve("repo").toAbsolutePath().toString());
}
@Nonnull
private Repository prepareForRead(String pythonPath, String id) {
private Repository prepareForRead(String id) {
when(repositoryHandler.getDirectory(id)).thenReturn(directory.resolve("repo").toFile());
HgConfig config = new HgConfig();
config.setPythonPath(pythonPath);
when(repositoryHandler.getConfig()).thenReturn(config);
Repository heartOfGold = RepositoryTestData.createHeartOfGold();

View File

@@ -32,10 +32,12 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.autoconfig.AutoConfiguratorProvider;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.HgVersionCommand;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.annotation.Nonnull;
import java.io.File;
import static org.assertj.core.api.Assertions.assertThat;
@@ -71,7 +73,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) {
HgRepositoryHandler handler = new HgRepositoryHandler(factory, locationResolver, null, null);
HgRepositoryHandler handler = createHandler(factory, locationResolver);
handler.init(contextProvider);
HgTestUtil.checkForSkip(handler);
@@ -81,11 +83,10 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, locationResolver, null, null);
HgRepositoryHandler repositoryHandler = createHandler(factory, locationResolver);
HgConfig hgConfig = new HgConfig();
hgConfig.setHgBinary("hg");
hgConfig.setPythonBinary("python");
repositoryHandler.setConfig(hgConfig);
initRepository();
@@ -93,16 +94,22 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
@Nonnull
private HgRepositoryHandler createHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver) {
AutoConfiguratorProvider provider = new AutoConfiguratorProvider(new HgVerifier());
return new HgRepositoryHandler(factory, locationResolver, null, null, provider.get());
}
@Test
public void shouldReturnVersionInformation() {
PluginLoader pluginLoader = mock(PluginLoader.class);
when(pluginLoader.getUberClassLoader()).thenReturn(HgRepositoryHandler.class.getClassLoader());
HgVersionCommand versionCommand = mock(HgVersionCommand.class);
when(versionCommand.get()).thenReturn(new HgVersion("5.2.0", "3.7.2"));
when(versionCommand.get()).thenReturn("python/3.7.2 mercurial/5.2.0");
HgRepositoryHandler handler = new HgRepositoryHandler(
factory, locationResolver, pluginLoader, null
factory, locationResolver, pluginLoader, null, new AutoConfiguratorProvider(new HgVerifier()).get()
);
String versionInformation = handler.getVersionInformation(versionCommand);

View File

@@ -29,6 +29,7 @@ package sonia.scm.repository;
import org.junit.Assume;
import sonia.scm.SCMContext;
import sonia.scm.TempDirRepositoryLocationResolver;
import sonia.scm.autoconfig.AutoConfiguratorProvider;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
@@ -87,7 +88,8 @@ public final class HgTestUtil
new InMemoryConfigurationStoreFactory(),
repositoryLocationResolver,
null,
null
null,
new AutoConfiguratorProvider(new HgVerifier()).get()
);
handler.init(context);

View File

@@ -0,0 +1,163 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.common.base.Splitter;
import org.assertj.core.util.Strings;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import sonia.scm.util.SystemUtil;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
class HgVerifierTest {
@Nested
class WithDefaultVersionResolver {
private final HgVerifier verifier = new HgVerifier();
@Test
void shouldReturnFalseIfFileDoesNotExists(@TempDir Path directory) {
Path hg = directory.resolve("hg");
boolean isValid = verify(hg);
assertThat(isValid).isFalse();
}
@Test
void shouldReturnFalseIfFileIsADirectory(@TempDir Path directory) throws IOException {
Path hg = directory.resolve("hg");
Files.createDirectories(hg);
boolean isValid = verify(hg);
assertThat(isValid).isFalse();
}
@Test
void shouldReturnFalseIfFileIsNotExecutable(@TempDir Path directory) throws IOException {
Path hg = directory.resolve("hg");
Files.createFile(hg);
boolean isValid = verify(hg);
assertThat(isValid).isFalse();
}
@Test
void shouldReturnTrueForIfMercurialIsAvailable() {
Path hg = findHg();
// skip test if we could not find mercurial
Assumptions.assumeTrue(hg != null, "skip test, because we could not find mercurial on path");
boolean isValid = verify(hg);
assertThat(isValid).isTrue();
}
private Path findHg() {
String pathEnv = System.getenv("PATH");
if (Strings.isNullOrEmpty(pathEnv)) {
return null;
}
for (String path : Splitter.on(File.pathSeparator).splitToList(pathEnv)) {
Path hg = Paths.get(path, SystemUtil.isWindows() ? "hg.exe" : "hg");
if (Files.exists(hg)) {
return hg;
}
}
return null;
}
private boolean verify(Path hg) {
HgConfig config = new HgConfig();
config.setHgBinary(hg.toString());
return verifier.isValid(config);
}
}
@ParameterizedTest
@ValueSource(strings = { "3-2-1", "x.y.z", "3.2.0" })
void shouldReturnFalseForInvalidVersions(String version, @TempDir Path directory) throws IOException {
HgVerifier verifier = new HgVerifier(hg -> version);
Path hg = createHg(directory);
boolean isValid = verifier.isValid(hg);
assertThat(isValid).isFalse();
}
@Test
void shouldReturnFalseOnIOException(@TempDir Path directory) throws IOException {
HgVerifier verifier = new HgVerifier(hg -> {
throw new IOException("failed");
});
Path hg = createHg(directory);
boolean isValid = verifier.isValid(hg);
assertThat(isValid).isFalse();
}
@Test
void shouldReturnTrue(@TempDir Path directory) throws IOException {
HgVerifier verifier = new HgVerifier(hg -> "4.2.0");
Path hg = createHg(directory);
boolean isValid = verifier.isValid(hg);
assertThat(isValid).isTrue();
}
@Nonnull
private Path createHg(Path directory) throws IOException {
Path hg = directory.resolve("hg");
Files.createFile(hg);
// skip test if we could not set executable flag
Assumptions.assumeTrue(hg.toFile().setExecutable(true));
return hg;
}
}

View File

@@ -1,139 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.io.Resources;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.*;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
/**
*
* @author Sebastian Sdorra
*/
public class HgWindowsPackageFixTest
{
/**
* Method description
*
*
* @throws IOException
*/
@Test
public void testHgBatModify() throws IOException
{
testModify(createHgBat("01"));
}
/**
* Method description
*
*
* @throws IOException
*/
@Test
public void testHgBatModifyWithComment() throws IOException
{
testModify(createHgBat("02"));
}
/**
* Method description
*
*
* @throws IOException
*/
@Test
public void testHgBatWithoutModify() throws IOException
{
long length = testModify(createHgBat("03")).length();
assertEquals(0, length);
}
/**
* Method description
*
*
* @param number
*
* @return
*
* @throws IOException
*/
private File createHgBat(String number) throws IOException
{
URL url = Resources.getResource("sonia/scm/repository/hg.bat.".concat(number));
File file = tempFolder.newFile(number);
try (FileOutputStream fos = new FileOutputStream(file))
{
Resources.copy(url, fos);
}
return file;
}
/**
* Method description
*
*
* @param file
*
* @return
*/
private File testModify(File file)
{
HgWindowsPackageFix.fixHgPy(file);
assertTrue(HgWindowsPackageFix.isSetBinaryAvailable(file));
File mod = new File(file.getParentFile(), HgWindowsPackageFix.MODIFY_MARK_01);
assertTrue(mod.exists());
return mod;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
}

View File

@@ -24,114 +24,41 @@
package sonia.scm.repository.spi;
import com.google.common.base.Joiner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVersion;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.HgTestUtil;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgVersionCommandTest {
private static final String PYTHON_OUTPUT = String.join("\n",
"3.9.0 (default, Oct 27 2020, 14:15:17)",
"[Clang 12.0.0 (clang-1200.0.32.21)]"
);
private Map<String, Process> outputs;
@BeforeEach
void setUpOutputs() {
outputs = new HashMap<>();
}
@Test
void shouldReturnHgVersion() throws InterruptedException {
command("/usr/local/bin/hg", HgVersionCommand.HG_ARGS, "5.5.2", 0);
command("/opt/python/bin/python", HgVersionCommand.PYTHON_ARGS, PYTHON_OUTPUT, 0);
void shouldReturnVersion(@TempDir Path temp) {
HgRepositoryHandler handler = HgTestUtil.createHandler(temp.toFile());
HgTestUtil.checkForSkip(handler);
HgVersion hgVersion = getVersion("/usr/local/bin/hg", "/opt/python/bin/python");
assertThat(hgVersion.getMercurial()).isEqualTo("5.5.2");
assertThat(hgVersion.getPython()).isEqualTo("3.9.0");
}
@Test
void shouldReturnUnknownMercurialVersionOnNonZeroExitCode() throws InterruptedException {
command("hg", HgVersionCommand.HG_ARGS, "", 1);
command("python", HgVersionCommand.PYTHON_ARGS, PYTHON_OUTPUT, 0);
HgVersion hgVersion = getVersion("hg", "python");
assertThat(hgVersion.getMercurial()).isEqualTo(HgVersion.UNKNOWN);
assertThat(hgVersion.getPython()).isEqualTo("3.9.0");
}
@Test
void shouldReturnUnknownPythonVersionOnNonZeroExitCode() throws InterruptedException {
command("hg", HgVersionCommand.HG_ARGS, "4.4.2", 0);
command("python", HgVersionCommand.PYTHON_ARGS, "", 1);
HgVersion hgVersion = getVersion("hg", "python");
assertThat(hgVersion.getMercurial()).isEqualTo("4.4.2");
assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN);
}
@Test
void shouldReturnUnknownForInvalidPythonOutput() throws InterruptedException {
command("hg", HgVersionCommand.HG_ARGS, "1.0.0", 0);
command("python", HgVersionCommand.PYTHON_ARGS, "abcdef", 0);
HgVersion hgVersion = getVersion("hg", "python");
assertThat(hgVersion.getMercurial()).isEqualTo("1.0.0");
assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN);
HgVersionCommand command = new HgVersionCommand(handler.getConfig());
assertThat(command.get())
.contains("python/")
.contains("mercurial/")
.isNotEqualTo(HgVersionCommand.UNKNOWN);
}
@Test
void shouldReturnUnknownForIOException() {
HgVersionCommand command = new HgVersionCommand(new HgConfig(), cmd -> {
HgVersionCommand command = new HgVersionCommand(new HgConfig(), "/i/dont/know", cmd -> {
throw new IOException("failed");
});
HgVersion hgVersion = command.get();
assertThat(hgVersion.getMercurial()).isEqualTo(HgVersion.UNKNOWN);
assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN);
}
private Process command(String command, String[] args, String content, int exitValue) throws InterruptedException {
Process process = mock(Process.class);
when(process.getInputStream()).thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
when(process.waitFor()).thenReturn(exitValue);
List<String> cmdLine = new ArrayList<>();
cmdLine.add(command);
cmdLine.addAll(Arrays.asList(args));
outputs.put(Joiner.on(' ').join(cmdLine), process);
return process;
}
@Nonnull
private HgVersion getVersion(String hg, String python) {
HgConfig config = new HgConfig();
config.setHgBinary(hg);
config.setPythonBinary(python);
return new HgVersionCommand(config, command -> outputs.get(Joiner.on(' ').join(command))).get();
assertThat(command.get()).isEqualTo(HgVersionCommand.UNKNOWN);
}
}

View File

@@ -21,12 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web.cgi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
@@ -48,7 +49,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
@@ -110,18 +113,8 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
*
*
* @param cmd
*
* @throws IOException
*/
@Override
public void execute(String cmd) throws IOException
public void execute(String cmd)
{
File command = new File(cmd);
EnvList env = new EnvList(environment);
@@ -150,21 +143,16 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
env.set(ENV_PATH_TRANSLATED, pathTranslated);
String execCmd = path;
if ((execCmd.charAt(0) != '"') && (execCmd.indexOf(' ') >= 0))
{
execCmd = "\"".concat(execCmd).concat("\"");
}
if (interpreter != null)
{
execCmd = interpreter.concat(" ").concat(execCmd);
List<String> execCmd = new ArrayList<>();
if (interpreter != null) {
execCmd.add(interpreter);
}
execCmd.add(command.getAbsolutePath());
execCmd.addAll(getArgs());
if (logger.isDebugEnabled())
{
logger.debug("execute cgi: {}", execCmd);
logger.debug("execute cgi: {}", Joiner.on(' ').join(execCmd));
if (logger.isTraceEnabled())
{
@@ -173,10 +161,11 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
}
Process p = null;
try
{
p = Runtime.getRuntime().exec(execCmd, env.getEnvArray(), workDirectory);
try {
ProcessBuilder builder = new ProcessBuilder(execCmd);
builder.directory(workDirectory);
builder.environment().putAll(env.asMap());
p = builder.start();
execute(p);
}
catch (IOException ex)
@@ -329,15 +318,14 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
env.set(ENV_SERVER_SOFTWARE,
SERVER_SOFTWARE_PREFIX.concat(SCMContext.getContext().getVersion()));
Enumeration enm = request.getHeaderNames();
Enumeration<String> enm = request.getHeaderNames();
while (enm.hasMoreElements())
{
String name = (String) enm.nextElement();
String name = enm.nextElement();
String value = request.getHeader(name);
env.set(ENV_HTTP_HEADER_PREFIX + name.toUpperCase().replace('-', '_'),
value);
env.set(ENV_HTTP_HEADER_PREFIX + name.toUpperCase().replace('-', '_'), value);
}
// these extra ones were from printenv on www.dev.nomura.co.uk
@@ -394,6 +382,7 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
*
* @throws IOException
*/
@SuppressWarnings("UnstableApiUsage")
private void execute(Process process) throws IOException
{
InputStream processIS = null;
@@ -418,61 +407,26 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
}
}
/**
* Method description
*
*
* @param is
*
*
* @throws IOException
*/
private void parseHeaders(InputStream is) throws IOException
{
private void parseHeaders(InputStream is) throws IOException {
String line = null;
while ((line = getTextLineFromStream(is)).length() > 0)
{
if (logger.isTraceEnabled())
{
while ((line = getTextLineFromStream(is)).length() > 0) {
if (logger.isTraceEnabled()) {
logger.trace(" ".concat(line));
}
if (!line.startsWith(RESPONSE_HEADER_HTTP_PREFIX))
{
if (!line.startsWith(RESPONSE_HEADER_HTTP_PREFIX)) {
int k = line.indexOf(':');
if (k > 0)
{
if (k > 0) {
String key = line.substring(0, k).trim();
String value = line.substring(k + 1).trim();
if (RESPONSE_HEADER_LOCATION.equalsIgnoreCase(key))
{
if (RESPONSE_HEADER_LOCATION.equalsIgnoreCase(key)) {
response.sendRedirect(response.encodeRedirectURL(value));
}
else if (RESPONSE_HEADER_STATUS.equalsIgnoreCase(key))
{
String[] token = value.split(" ");
int status = Integer.parseInt(token[0]);
if (logger.isDebugEnabled())
{
logger.debug("CGI returned with status {}", status);
}
if (status < 304)
{
response.setStatus(status);
}
else
{
response.sendError(status);
}
}
else
{
} else if (RESPONSE_HEADER_STATUS.equalsIgnoreCase(key)) {
handleStatus(value);
} else {
// add remaining header items to our response header
response.addHeader(key, value);
}
@@ -481,6 +435,19 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
}
}
private void handleStatus(String value) throws IOException {
String[] token = value.split(" ");
int status = Integer.parseInt(token[0]);
logger.debug("CGI returned with status {}", status);
if (status < 304) {
response.setStatus(status);
} else {
response.sendError(status);
}
}
/**
* Method description
*
@@ -513,26 +480,21 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
*/
private void processErrorStreamAsync(final Process process)
{
executor.execute(new Runnable()
{
@Override
public void run()
{
InputStream errorStream = null;
executor.execute(() -> {
InputStream errorStream = null;
try
{
errorStream = process.getErrorStream();
processErrorStream(errorStream);
}
catch (IOException ex)
{
logger.error("could not read errorstream", ex);
}
finally
{
IOUtil.close(errorStream);
}
try
{
errorStream = process.getErrorStream();
processErrorStream(errorStream);
}
catch (IOException ex)
{
logger.error("could not read errorstream", ex);
}
finally
{
IOUtil.close(errorStream);
}
});
}
@@ -615,6 +577,7 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor
catch (InterruptedException ex)
{
getExceptionHandler().handleException(request, response, ex);
Thread.currentThread().interrupt();
}
}