Refactor nearly the whole scm-hg-plugin for new hook implementation

This commit is contained in:
Sebastian Sdorra
2020-11-07 15:52:22 +01:00
parent 23317662f2
commit d518af4ccc
53 changed files with 1230 additions and 2875 deletions

View File

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

View File

@@ -1,391 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
import sonia.scm.web.HgUtil;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class AbstractHgHandler
{
/** Field description */
protected static final String ENV_ID_REVISION = "SCM_ID_REVISION";
/** Field description */
protected static final String ENV_NODE = "HG_NODE";
/** Field description */
protected static final String ENV_PAGE_LIMIT = "SCM_PAGE_LIMIT";
/** Field description */
protected static final String ENV_PAGE_START = "SCM_PAGE_START";
/** Field description */
protected static final String ENV_PATH = "SCM_PATH";
/** Field description */
protected static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
/** Field description */
protected static final String ENV_REVISION = "SCM_REVISION";
/** Field description */
protected static final String ENV_REVISION_END = "SCM_REVISION_END";
/** Field description */
protected static final String ENV_REVISION_START = "SCM_REVISION_START";
/** Field description */
private static final String ENCODING = "UTF-8";
/** mercurial encoding */
private static final String ENV_HGENCODING = "HGENCODING";
/** Field description */
private static final String ENV_PENDING = "HG_PENDING";
/** python encoding */
private static final String ENV_PYTHONIOENCODING = "PYTHONIOENCODING";
/** Field description */
private static final String ENV_PYTHONPATH = "PYTHONPATH";
/**
* the logger for AbstractHgCommand
*/
private static final Logger logger =
LoggerFactory.getLogger(AbstractHgHandler.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
* @param handler
* @param context
* @param repository
*/
protected AbstractHgHandler(HgRepositoryHandler handler, HgContext context,
Repository repository)
{
this(handler, context, repository, handler.getDirectory(repository.getId()));
}
/**
* Constructs ...
*
*
*
* @param handler
* @param context
* @param repository
* @param repositoryDirectory
*/
protected AbstractHgHandler(HgRepositoryHandler handler, HgContext context,
Repository repository, File repositoryDirectory)
{
this.handler = handler;
this.context = context;
this.repository = repository;
this.repositoryDirectory = repositoryDirectory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param revision
* @param path
*
* @return
*/
protected Map<String, String> createEnvironment(String revision, String path)
{
Map<String, String> env = new HashMap<>();
env.put(ENV_REVISION, HgUtil.getRevision(revision));
env.put(ENV_PATH, Util.nonNull(path));
return env;
}
/**
* Method description
*
*
* @param args
*
* @return
*
* @throws IOException
*/
protected Process createHgProcess(String... args) throws IOException
{
return createHgProcess(new HashMap<String, String>(), args);
}
/**
* Method description
*
*
* @param extraEnv
* @param args
*
* @return
*
* @throws IOException
*/
protected Process createHgProcess(Map<String, String> extraEnv,
String... args)
throws IOException
{
return createProcess(extraEnv, handler.getConfig().getHgBinary(), args);
}
/**
* Method description
*
*
* @param script
* @param extraEnv
*
* @return
*
* @throws IOException
*/
protected Process createScriptProcess(HgPythonScript script,
Map<String, String> extraEnv)
throws IOException
{
return createProcess(extraEnv, handler.getConfig().getPythonBinary(),
script.getFile(SCMContext.getContext()).getAbsolutePath());
}
/**
* Method description
*
*
* @param errorStream
*/
protected void handleErrorStream(final InputStream errorStream)
{
if (errorStream != null)
{
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
String content = IOUtil.getContent(errorStream);
if (Util.isNotEmpty(content))
{
logger.error(content.trim());
}
}
catch (IOException ex)
{
logger.error("error during logging", ex);
}
}
}).start();
}
}
//~--- get methods ----------------------------------------------------------
protected <T> T getResultFromScript(Class<T> resultType, HgPythonScript script) throws IOException {
return getResultFromScript(resultType, script,
new HashMap<String, String>());
}
@SuppressWarnings("unchecked")
protected <T> T getResultFromScript(Class<T> resultType,
HgPythonScript script, Map<String, String> extraEnv)
throws IOException
{
Process p = createScriptProcess(script, extraEnv);
handleErrorStream(p.getErrorStream());
try (InputStream input = p.getInputStream()) {
return (T) handler.getJaxbContext().createUnmarshaller().unmarshal(input);
} catch (JAXBException ex) {
logger.error("could not parse result", ex);
throw new InternalRepositoryException(repository, "could not parse result", ex);
}
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param extraEnv
* @param cmd
* @param args
*
* @return
*
* @throws IOException
*/
private Process createProcess(Map<String, String> extraEnv, String cmd,
String... args)
throws IOException
{
HgConfig config = handler.getConfig();
List<String> cmdList = new ArrayList<String>();
cmdList.add(cmd);
if (Util.isNotEmpty(args))
{
cmdList.addAll(Arrays.asList(args));
}
if (logger.isDebugEnabled())
{
StringBuilder msg = new StringBuilder("create process for [");
Iterator<String> it = cmdList.iterator();
while (it.hasNext())
{
msg.append(it.next());
if (it.hasNext())
{
msg.append(", ");
}
}
msg.append("]");
logger.debug(msg.toString());
}
ProcessBuilder pb = new ProcessBuilder(cmdList);
pb.directory(repositoryDirectory);
Map<String, String> env = pb.environment();
// force utf-8 encoding for mercurial and python
env.put(ENV_PYTHONIOENCODING, ENCODING);
env.put(ENV_HGENCODING, ENCODING);
//J-
env.put(ENV_ID_REVISION,
String.valueOf(handler.getConfig().isShowRevisionInId())
);
//J+
if (context.isSystemEnvironment())
{
env.putAll(System.getenv());
}
if (context.isPending())
{
if (logger.isDebugEnabled())
{
logger.debug("enable hg pending for {}",
repositoryDirectory.getAbsolutePath());
}
env.put(ENV_PENDING, repositoryDirectory.getAbsolutePath());
if (extraEnv.containsKey(ENV_REVISION_START))
{
env.put(ENV_NODE, extraEnv.get(ENV_REVISION_START));
}
}
env.put(ENV_PYTHONPATH, HgUtil.getPythonPath(config));
env.put(ENV_REPOSITORY_PATH, repositoryDirectory.getAbsolutePath());
env.putAll(extraEnv);
if (logger.isTraceEnabled())
{
StringBuilder msg = new StringBuilder("start process in directory '");
msg.append(repositoryDirectory.getAbsolutePath()).append(
"' with env: \n");
for (Map.Entry<String, String> e : env.entrySet())
{
msg.append(" ").append(e.getKey());
msg.append(" = ").append(e.getValue());
msg.append("\n");
}
logger.trace(msg.toString());
}
return pb.start();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
protected Repository repository;
/** Field description */
protected File repositoryDirectory;
/** Field description */
private HgContext context;
/** Field description */
private HgRepositoryHandler handler;
}

View File

@@ -0,0 +1,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.repository;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.repository.hooks.HookServer;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.CipherUtil;
import sonia.scm.web.HgUtil;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.Map;
public class DefaultHgEnvironmentBuilder implements HgEnvironmentBuilder {
@VisibleForTesting
static final String ENV_PYTHON_PATH = "PYTHONPATH";
@VisibleForTesting
static final String ENV_HOOK_PORT = "SCM_HOOK_PORT";
@VisibleForTesting
static final String ENV_CHALLENGE = "SCM_CHALLENGE";
@VisibleForTesting
static final String ENV_BEARER_TOKEN = "SCM_BEARER_TOKEN";
@VisibleForTesting
static final String ENV_REPOSITORY_NAME = "REPO_NAME";
@VisibleForTesting
static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
@VisibleForTesting
static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
@VisibleForTesting
static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
private final HgRepositoryHandler repositoryHandler;
private final HookEnvironment hookEnvironment;
private final HookServer server;
private int hookPort = -1;
@Inject
public DefaultHgEnvironmentBuilder(
AccessTokenBuilderFactory accessTokenBuilderFactory, HgRepositoryHandler repositoryHandler,
HookEnvironment hookEnvironment, HookServer server
) {
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
this.repositoryHandler = repositoryHandler;
this.hookEnvironment = hookEnvironment;
this.server = server;
}
@Override
public Map<String, String> read(Repository repository) {
ImmutableMap.Builder<String, String> env = ImmutableMap.builder();
read(env, repository);
return env.build();
}
@Override
public Map<String, String> write(Repository repository) {
ImmutableMap.Builder<String, String> env = ImmutableMap.builder();
read(env, repository);
write(env);
return env.build();
}
private void read(ImmutableMap.Builder<String, String> env, Repository repository) {
HgConfig config = repositoryHandler.getConfig();
env.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(config));
File directory = repositoryHandler.getDirectory(repository.getId());
env.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
env.put(ENV_REPOSITORY_ID, repository.getId());
env.put(ENV_REPOSITORY_PATH, directory.getAbsolutePath());
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
env.put(ENV_HTTP_POST_ARGS, String.valueOf(config.isEnableHttpPostArgs()));
}
private void write(ImmutableMap.Builder<String, String> env) {
env.put(ENV_HOOK_PORT, String.valueOf(getHookPort()));
AccessToken accessToken = accessTokenBuilderFactory.create().build();
env.put(ENV_BEARER_TOKEN, CipherUtil.getInstance().encode(accessToken.compact()));
env.put(ENV_CHALLENGE, hookEnvironment.getChallenge());
}
private synchronized int getHookPort() {
if (hookPort > 0) {
return hookPort;
}
try {
hookPort = server.start();
} catch (IOException ex) {
throw new IllegalStateException("failed to start mercurial hook server");
}
return hookPort;
}
}

View File

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

View File

@@ -1,121 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class HgContext
{
/**
* Constructs ...
*
*/
public HgContext() {}
/**
* Constructs ...
*
*
* @param pending
*/
public HgContext(boolean pending)
{
this.pending = pending;
}
/**
* Constructs ...
*
*
* @param pending
* @param systemEnvironment
*/
public HgContext(boolean pending, boolean systemEnvironment)
{
this.pending = pending;
this.systemEnvironment = systemEnvironment;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public boolean isPending()
{
return pending;
}
/**
* Method description
*
*
* @return
*/
public boolean isSystemEnvironment()
{
return systemEnvironment;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param pending
*/
public void setPending(boolean pending)
{
this.pending = pending;
}
/**
* Method description
*
*
* @param systemEnvironment
*/
public void setSystemEnvironment(boolean systemEnvironment)
{
this.systemEnvironment = systemEnvironment;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private boolean pending = false;
/** Field description */
private boolean systemEnvironment = true;
}

View File

@@ -1,96 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
/**
* Injection provider for {@link HgContext}.
* This provider returns an instance {@link HgContext} from request scope, if no {@link HgContext} could be found in
* request scope (mostly because the scope is not available) a new {@link HgContext} gets returned.
*
* @author Sebastian Sdorra
*/
public class HgContextProvider implements Provider<HgContext>
{
/**
* the LOG for HgContextProvider
*/
private static final Logger LOG =
LoggerFactory.getLogger(HgContextProvider.class);
//~--- get methods ----------------------------------------------------------
private Provider<HgContextRequestStore> requestStoreProvider;
@Inject
public HgContextProvider(Provider<HgContextRequestStore> requestStoreProvider) {
this.requestStoreProvider = requestStoreProvider;
}
@VisibleForTesting
public HgContextProvider() {
}
@Override
public HgContext get() {
HgContext context = fetchContextFromRequest();
if (context != null) {
LOG.trace("return HgContext from request store");
return context;
}
LOG.trace("could not find context in request scope, returning new instance");
return new HgContext();
}
private HgContext fetchContextFromRequest() {
try {
if (requestStoreProvider != null) {
return requestStoreProvider.get().get();
} else {
LOG.trace("no request store provider defined, could not return context from request");
return null;
}
} catch (ProvisionException ex) {
if (ex.getCause() instanceof OutOfScopeException) {
LOG.trace("we are currently out of request scope, failed to retrieve context");
return null;
} else {
throw ex;
}
}
}
}

View File

@@ -1,127 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.ProvisionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.AccessToken;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Xsrf;
import sonia.scm.web.HgUtil;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public final class HgEnvironment
{
private static final Logger LOG = LoggerFactory.getLogger(HgEnvironment.class);
/** Field description */
public static final String ENV_PYTHON_PATH = "PYTHONPATH";
/** Field description */
private static final String ENV_CHALLENGE = "SCM_CHALLENGE";
/** Field description */
private static final String ENV_URL = "SCM_URL";
private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
private static final String SCM_XSRF = "SCM_XSRF";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private HgEnvironment() {}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param environment
* @param handler
* @param hookManager
*/
public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager)
{
prepareEnvironment(environment, handler, hookManager, null);
}
/**
* Method description
*
*
* @param environment
* @param handler
* @param hookManager
* @param request
*/
public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager,
HttpServletRequest request)
{
String hookUrl;
if (request != null)
{
hookUrl = hookManager.createUrl(request);
}
else
{
hookUrl = hookManager.createUrl();
}
try {
AccessToken accessToken = hookManager.getAccessToken();
environment.put(SCM_BEARER_TOKEN, CipherUtil.getInstance().encode(accessToken.compact()));
extractXsrfKey(environment, accessToken);
} catch (ProvisionException e) {
LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e);
}
environment.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig()));
environment.put(ENV_URL, hookUrl);
environment.put(ENV_CHALLENGE, hookManager.getChallenge());
}
private static void extractXsrfKey(Map<String, String> environment, AccessToken accessToken) {
environment.put(SCM_XSRF, accessToken.<String>getCustom(Xsrf.TOKEN_KEY).orElse("-"));
}
}

View File

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

View File

@@ -1,354 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe;
import com.google.common.base.MoreObjects;
import com.google.inject.Inject;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.config.ScmConfigurationChangedEvent;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class HgHookManager {
@SuppressWarnings("java:S1075") // this url is fixed
private static final String URL_HOOKPATH = "/hook/hg/";
/**
* the logger for HgHookManager
*/
private static final Logger logger =
LoggerFactory.getLogger(HgHookManager.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
* @param configuration
* @param httpServletRequestProvider
* @param httpClient
* @param accessTokenBuilderFactory
*/
@Inject
public HgHookManager(ScmConfiguration configuration,
Provider<HttpServletRequest> httpServletRequestProvider,
AdvancedHttpClient httpClient, AccessTokenBuilderFactory accessTokenBuilderFactory)
{
this.configuration = configuration;
this.httpServletRequestProvider = httpServletRequestProvider;
this.httpClient = httpClient;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param config
*/
@Subscribe(async = false)
public void configChanged(ScmConfigurationChangedEvent config)
{
hookUrl = null;
}
/**
* Method description
*
*
* @param request
*
* @return
*/
public String createUrl(HttpServletRequest request)
{
if (hookUrl == null)
{
synchronized (this)
{
if (hookUrl == null)
{
buildHookUrl(request);
if (logger.isInfoEnabled() && Util.isNotEmpty(hookUrl))
{
logger.info("use {} for mercurial hooks", hookUrl);
}
}
}
}
return hookUrl;
}
/**
* Method description
*
*
* @return
*/
public String createUrl()
{
String url = hookUrl;
if (url == null)
{
HttpServletRequest request = getHttpServletRequest();
if (request != null)
{
url = createUrl(request);
}
else
{
url = createConfiguredUrl();
logger.warn(
"created url {} without request, in some cases this could cause problems",
url);
}
}
return url;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getChallenge()
{
return challenge;
}
/**
* Method description
*
*
* @param challenge
*
* @return
*/
public boolean isAcceptAble(String challenge)
{
return this.challenge.equals(challenge);
}
public AccessToken getAccessToken()
{
return accessTokenBuilderFactory.create().build();
}
private void buildHookUrl(HttpServletRequest request) {
if (configuration.isForceBaseUrl()) {
logger.debug("create hook url from configured base url because force base url is enabled");
hookUrl = createConfiguredUrl();
if (!isUrlWorking(hookUrl)) {
disableHooks();
}
} else {
logger.debug("create hook url from request");
hookUrl = HttpUtil.getCompleteUrl(request, URL_HOOKPATH);
if (!isUrlWorking(hookUrl)) {
logger.warn("hook url {} from request does not work, try now localhost", hookUrl);
hookUrl = createLocalUrl(request);
if (!isUrlWorking(hookUrl)) {
logger.warn("localhost hook url {} does not work, try now from configured base url", hookUrl);
hookUrl = createConfiguredUrl();
if (!isUrlWorking(hookUrl)) {
disableHooks();
}
}
}
}
}
/**
* Method description
*
*
* @return
*/
private String createConfiguredUrl()
{
//J-
return HttpUtil.getUriWithoutEndSeperator(
MoreObjects.firstNonNull(
configuration.getBaseUrl(),
"http://localhost:8080/scm"
)
).concat(URL_HOOKPATH);
//J+
}
/**
* Method description
*
*
* @param request
*
* @return
*/
private String createLocalUrl(HttpServletRequest request)
{
StringBuilder sb = new StringBuilder(request.getScheme());
sb.append("://localhost:").append(request.getLocalPort());
sb.append(request.getContextPath()).append(URL_HOOKPATH);
return sb.toString();
}
/**
* Method description
*
*/
private void disableHooks()
{
if (logger.isErrorEnabled())
{
logger.error(
"disabling mercurial hooks, because hook url {} seems not to work",
hookUrl);
}
hookUrl = Util.EMPTY_STRING;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
private HttpServletRequest getHttpServletRequest()
{
HttpServletRequest request = null;
try
{
request = httpServletRequestProvider.get();
}
catch (ProvisionException | OutOfScopeException ex)
{
logger.debug("http servlet request is not available");
}
return request;
}
/**
* Method description
*
*
* @param url
*
* @return
*/
private boolean isUrlWorking(String url)
{
boolean result = false;
try
{
url = url.concat("?ping=true");
logger.trace("check hook url {}", url);
//J-
int sc = httpClient.get(url)
.disableHostnameValidation(true)
.disableCertificateValidation(true)
.ignoreProxySettings(true)
.disableTracing()
.request()
.getStatus();
//J+
result = sc == 204;
}
catch (IOException ex)
{
if (logger.isTraceEnabled())
{
logger.trace("url test failed for url ".concat(url), ex);
}
}
return result;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String challenge = UUID.randomUUID().toString();
/** Field description */
private ScmConfiguration configuration;
/** Field description */
private volatile String hookUrl;
/** Field description */
private AdvancedHttpClient httpClient;
/** Field description */
private Provider<HttpServletRequest> httpServletRequestProvider;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,7 @@
package sonia.scm.repository.hooks;
import com.google.inject.assistedinject.Assisted;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
@@ -31,7 +32,7 @@ import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.NotFoundException;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.spi.HgHookContextProvider;
@@ -39,9 +40,7 @@ import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil;
import javax.annotation.Nonnull;
import javax.inject.Provider;
import java.io.File;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -50,19 +49,20 @@ import java.util.List;
import static java.util.Collections.singletonList;
class HgHookHandler implements Runnable {
class DefaultHookHandler implements HookHandler {
private static final Logger LOG = LoggerFactory.getLogger(HgHookHandler.class);
private static final Logger LOG = LoggerFactory.getLogger(DefaultHookHandler.class);
private final HgRepositoryHandler handler;
private final HookEventFacade hookEventFacade;
private final Provider<HookEnvironment> environmentProvider;
private final HookEnvironment environment;
private final HookContextProviderFactory hookContextProviderFactory;
private final Socket socket;
HgHookHandler(HgRepositoryHandler handler, HookEventFacade hookEventFacade, Provider<HookEnvironment> environmentProvider, Socket socket) {
this.handler = handler;
@Inject
public DefaultHookHandler(HookContextProviderFactory hookContextProviderFactory, HookEventFacade hookEventFacade, HookEnvironment environment, @Assisted Socket socket) {
this.hookContextProviderFactory = hookContextProviderFactory;
this.hookEventFacade = hookEventFacade;
this.environmentProvider = environmentProvider;
this.environment = environment;
this.socket = socket;
}
@@ -84,7 +84,6 @@ class HgHookHandler implements Runnable {
}
private Response handleHookRequest(Request request) {
HookEnvironment environment = environmentProvider.get();
try {
if (!environment.isAcceptAble(request.getChallenge())) {
return error("invalid hook challenge");
@@ -93,13 +92,16 @@ class HgHookHandler implements Runnable {
authenticate(request);
environment.setPending(request.getType() == RepositoryHookType.PRE_RECEIVE);
HgHookContextProvider context = createHookContextProvider(request);
HgHookContextProvider context = hookContextProviderFactory.create(request.getRepositoryId(), request.getNode());
hookEventFacade.handle(request.getRepositoryId()).fireHookEvent(request.getType(), context);
return new Response(context.getHgMessageProvider().getMessages(), false);
} catch (AuthenticationException ex) {
LOG.warn("hook authentication failed", ex);
return error("hook authentication failed");
} catch (NotFoundException ex) {
LOG.warn("could not find repository with id {}", request.getRepositoryId(), ex);
return error("repository not found");
} catch (Exception ex) {
LOG.warn("unknown error on hook occurred", ex);
return error("unknown error");
@@ -108,14 +110,6 @@ class HgHookHandler implements Runnable {
}
}
@Nonnull
private HgHookContextProvider createHookContextProvider(Request request) {
File repositoryDirectory = handler.getDirectory(request.getRepositoryId());
return new HgHookContextProvider(
handler, repositoryDirectory, null, request.node, request.type
);
}
private void authenticate(Request request) {
String token = CipherUtil.getInstance().decode(request.getToken());
BearerToken bearer = BearerToken.valueOf(token);

View File

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

View File

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

View File

@@ -24,14 +24,11 @@
package sonia.scm.repository.hooks;
import com.google.inject.ImplementedBy;
import java.net.Socket;
@FunctionalInterface
@ImplementedBy(DefaultHookHandlerFactory.class)
interface HookHandlerFactory {
Runnable create(Socket socket);
HookHandler create(Socket socket);
}

View File

@@ -24,28 +24,17 @@
package sonia.scm.repository.hooks;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade;
import javax.inject.Inject;
import javax.inject.Provider;
import java.net.Socket;
public class DefaultHookHandlerFactory implements HookHandlerFactory {
private final HgRepositoryHandler handler;
private final HookEventFacade hookEventFacade;
private final Provider<HookEnvironment> hookEnvironment;
@Inject
public DefaultHookHandlerFactory(HgRepositoryHandler handler, HookEventFacade hookEventFacade, Provider<HookEnvironment> hookEnvironment) {
this.handler = handler;
this.hookEventFacade = hookEventFacade;
this.hookEnvironment = hookEnvironment;
}
import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import sonia.scm.plugin.Extension;
@Extension
public class HookModule extends AbstractModule {
@Override
public HgHookHandler create(Socket socket) {
return new HgHookHandler(handler, hookEventFacade, hookEnvironment, socket);
protected void configure() {
install(new FactoryModuleBuilder()
.implement(HookHandler.class, DefaultHookHandler.class)
.build(HookHandlerFactory.class)
);
}
}

View File

@@ -25,7 +25,9 @@
package sonia.scm.repository.hooks;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.shiro.concurrent.SubjectAwareExecutorService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.util.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,6 +40,7 @@ import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
@Singleton
public class HookServer implements AutoCloseable {
@@ -56,18 +59,24 @@ public class HookServer implements AutoCloseable {
}
private ExecutorService createAcceptor() {
return new SubjectAwareExecutorService(Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder().setNameFormat("HgHookAcceptor").build()
));
return Executors.newSingleThreadExecutor(
createThreadFactory("HgHookAcceptor")
);
}
private ExecutorService createWorkerPool() {
return new SubjectAwareExecutorService(Executors.newCachedThreadPool(
new ThreadFactoryBuilder().setNameFormat("HgHookWorker-%d").build()
)
return Executors.newCachedThreadPool(
createThreadFactory("HgHookWorker-%d")
);
}
@Nonnull
private ThreadFactory createThreadFactory(String hgHookAcceptor) {
return new ThreadFactoryBuilder()
.setNameFormat(hgHookAcceptor)
.build();
}
public int start() throws IOException {
acceptor = createAcceptor();
workerPool = createWorkerPool();
@@ -83,12 +92,14 @@ public class HookServer implements AutoCloseable {
}
private void accept() {
SecurityManager securityManager = SecurityUtils.getSecurityManager();
acceptor.submit(() -> {
while (!serverSocket.isClosed()) {
try {
Socket clientSocket = serverSocket.accept();
LOG.trace("accept incoming hook client from {}", clientSocket.getInetAddress());
workerPool.submit(handlerFactory.create(clientSocket));
HookHandler hookHandler = handlerFactory.create(clientSocket);
workerPool.submit(associateSecurityManager(securityManager, hookHandler));
} catch (IOException ex) {
LOG.debug("failed to accept socket, possible closed", ex);
}
@@ -96,6 +107,18 @@ public class HookServer implements AutoCloseable {
});
}
private Runnable associateSecurityManager(SecurityManager securityManager, HookHandler hookHandler) {
return () -> {
ThreadContext.bind(securityManager);
try {
hookHandler.run();
} finally {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
};
}
@Nonnull
private ServerSocket createServerSocket() throws IOException {
return new ServerSocket(0, 0, InetAddress.getLoopbackAddress());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,114 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVersion;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class HgVersionCommand {
private static final Logger LOG = LoggerFactory.getLogger(HgVersionCommand.class);
@VisibleForTesting
static final String[] HG_ARGS = {
"version", "--template", "{ver}"
};
@VisibleForTesting
static final String[] PYTHON_ARGS = {
"-c", "import sys; print(sys.version)"
};
private final HgConfig config;
private final ProcessExecutor executor;
public HgVersionCommand(HgConfig config) {
this(config, command -> new ProcessBuilder(command).start());
}
HgVersionCommand(HgConfig config, ProcessExecutor executor) {
this.config = config;
this.executor = executor;
}
public HgVersion get() {
return new HgVersion(getHgVersion(), getPythonVersion());
}
@Nonnull
private String getPythonVersion() {
try {
String content = exec(config.getPythonBinary(), PYTHON_ARGS);
int index = content.indexOf(' ');
if (index > 0) {
return content.substring(0, index);
}
} catch (IOException ex) {
LOG.warn("failed to get python version", ex);
}
return HgVersion.UNKNOWN;
}
@Nonnull
private String getHgVersion() {
try {
return exec(config.getHgBinary(), HG_ARGS).trim();
} catch (IOException ex) {
LOG.warn("failed to get mercurial version", ex);
return HgVersion.UNKNOWN;
}
}
@SuppressWarnings("UnstableApiUsage")
private String exec(String command, String[] args) throws IOException {
List<String> cmd = new ArrayList<>();
cmd.add(command);
cmd.addAll(Arrays.asList(args));
Process process = executor.execute(cmd);
byte[] bytes = ByteStreams.toByteArray(process.getInputStream());
int exitCode = process.exitValue();
if (exitCode != 0) {
throw new IOException("process ends with exit code " + exitCode);
}
return new String(bytes, StandardCharsets.UTF_8);
}
@FunctionalInterface
interface ProcessExecutor {
Process execute(List<String> command) throws IOException;
}
}

View File

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

View File

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

View File

@@ -1,446 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.io.Closeables;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.api.HgHookMessage.Severity;
import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
public class HgHookCallbackServlet extends HttpServlet
{
/** Field description */
public static final String HGHOOK_POST_RECEIVE = "changegroup";
/** Field description */
public static final String HGHOOK_PRE_RECEIVE = "pretxnchangegroup";
/** Field description */
public static final String PARAM_REPOSITORYID = "repositoryId";
/** Field description */
private static final String PARAM_CHALLENGE = "challenge";
/** Field description */
private static final String PARAM_TOKEN = "token";
/** Field description */
private static final String PARAM_NODE = "node";
/** Field description */
private static final String PARAM_PING = "ping";
/** Field description */
private static final Pattern REGEX_URL =
Pattern.compile("^/hook/hg/([^/]+)$");
/** the logger for HgHookCallbackServlet */
private static final Logger logger =
LoggerFactory.getLogger(HgHookCallbackServlet.class);
/** Field description */
private static final long serialVersionUID = 3531596724828189353L;
//~--- constructors ---------------------------------------------------------
@Inject
public HgHookCallbackServlet(HookEventFacade hookEventFacade,
HgRepositoryHandler handler, HgHookManager hookManager,
Provider<HgContext> contextProvider)
{
this.hookEventFacade = hookEventFacade;
this.handler = handler;
this.hookManager = hookManager;
this.contextProvider = contextProvider;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param response
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
{
String ping = request.getParameter(PARAM_PING);
if (Util.isNotEmpty(ping) && Boolean.parseBoolean(ping))
{
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
else
{
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
handlePostRequest(request, response);
} catch (IOException ex) {
logger.warn("error in hook callback execution, sending internal server error", ex);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
private void handlePostRequest(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String strippedURI = HttpUtil.getStrippedURI(request);
Matcher m = REGEX_URL.matcher(strippedURI);
if (m.matches())
{
String repositoryId = getRepositoryId(request);
String type = m.group(1);
String challenge = request.getParameter(PARAM_CHALLENGE);
if (Util.isNotEmpty(challenge))
{
String node = request.getParameter(PARAM_NODE);
if (Util.isNotEmpty(node))
{
String token = request.getParameter(PARAM_TOKEN);
if (Util.isNotEmpty(token))
{
authenticate(token);
}
hookCallback(response, type, repositoryId, challenge, node);
}
else if (logger.isDebugEnabled())
{
logger.debug("node parameter not found");
}
}
else if (logger.isDebugEnabled())
{
logger.debug("challenge parameter not found");
}
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("url does not match");
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
private void authenticate(String token)
{
try
{
token = CipherUtil.getInstance().decode(token);
if (Util.isNotEmpty(token))
{
Subject subject = SecurityUtils.getSubject();
AuthenticationToken accessToken = createToken(token);
//J-
subject.login(accessToken);
}
}
catch (Exception ex)
{
logger.error("could not authenticate user", ex);
}
}
private AuthenticationToken createToken(String tokenString)
{
return BearerToken.valueOf(tokenString);
}
private void fireHook(HttpServletResponse response, String repositoryId, String node, RepositoryHookType type)
throws IOException
{
HgHookContextProvider context = null;
try
{
if (type == RepositoryHookType.PRE_RECEIVE)
{
contextProvider.get().setPending(true);
}
File repositoryDirectory = handler.getDirectory(repositoryId);
context = new HgHookContextProvider(handler, repositoryDirectory, hookManager,
node, type);
hookEventFacade.handle(repositoryId).fireHookEvent(type, context);
printMessages(response, context);
}
catch (NotFoundException ex)
{
logger.error(ex.getMessage());
logger.trace("repository not found", ex);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
catch (Exception ex)
{
sendError(response, context, ex);
}
}
private void hookCallback(HttpServletResponse response, String typeName, String repositoryId, String challenge, String node) throws IOException {
if (hookManager.isAcceptAble(challenge))
{
RepositoryHookType type = null;
if (HGHOOK_PRE_RECEIVE.equals(typeName))
{
type = RepositoryHookType.PRE_RECEIVE;
}
else if (HGHOOK_POST_RECEIVE.equals(typeName))
{
type = RepositoryHookType.POST_RECEIVE;
}
if (type != null)
{
fireHook(response, repositoryId, node, type);
}
else
{
if (logger.isWarnEnabled())
{
logger.warn("unknown hook type {}", typeName);
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
else
{
if (logger.isWarnEnabled())
{
logger.warn("hg hook challenge is not accept able");
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
/**
* Method description
*
*
* @param writer
* @param msg
*/
private void printMessage(PrintWriter writer, HgHookMessage msg)
{
writer.append('_');
if (msg.getSeverity() == Severity.ERROR)
{
writer.append("e[SCM] Error: ");
}
else
{
writer.append("n[SCM] ");
}
writer.println(msg.getMessage());
}
/**
* Method description
*
*
* @param response
* @param context
*
* @throws IOException
*/
private void printMessages(HttpServletResponse response,
HgHookContextProvider context)
throws IOException
{
List<HgHookMessage> msgs = context.getHgMessageProvider().getMessages();
if (Util.isNotEmpty(msgs))
{
PrintWriter writer = null;
try
{
writer = response.getWriter();
printMessages(writer, msgs);
}
finally
{
Closeables.close(writer, false);
}
}
}
/**
* Method description
*
*
* @param writer
* @param msgs
*/
private void printMessages(PrintWriter writer, List<HgHookMessage> msgs)
{
for (HgHookMessage msg : msgs)
{
printMessage(writer, msg);
}
}
/**
* Method description
*
*
* @param response
* @param context
* @param ex
*
* @throws IOException
*/
private void sendError(HttpServletResponse response,
HgHookContextProvider context, Exception ex)
throws IOException
{
logger.warn("hook ended with exception", ex);
response.setStatus(HttpServletResponse.SC_CONFLICT);
String msg = ex.getMessage();
List<HgHookMessage> msgs = null;
if (context != null)
{
msgs = context.getHgMessageProvider().getMessages();
}
if (!Strings.isNullOrEmpty(msg) || Util.isNotEmpty(msgs))
{
PrintWriter writer = null;
try
{
writer = response.getWriter();
if (Util.isNotEmpty(msgs))
{
printMessages(writer, msgs);
}
if (!Strings.isNullOrEmpty(msg))
{
printMessage(writer, new HgHookMessage(Severity.ERROR, msg));
}
}
finally
{
Closeables.close(writer, true);
}
}
}
//~--- get methods ----------------------------------------------------------
private String getRepositoryId(HttpServletRequest request)
{
String id = request.getParameter(PARAM_REPOSITORYID);
Preconditions.checkArgument(!Strings.isNullOrEmpty(id), "repository id not found in request");
return id;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final Provider<HgContext> contextProvider;
/** Field description */
private final HgRepositoryHandler handler;
/** Field description */
private final HookEventFacade hookEventFacade;
/** Field description */
private final HgHookManager hookManager;
}

View File

@@ -1,80 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.Map;
public class HgRepositoryEnvironmentBuilder {
private static final String ENV_REPOSITORY_NAME = "REPO_NAME";
private static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
private final HgRepositoryHandler handler;
private final HgHookManager hookManager;
@Inject
public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager) {
this.handler = handler;
this.hookManager = hookManager;
}
public void buildFor(Repository repository, HttpServletRequest request, Map<String, String> environment) {
File directory = handler.getDirectory(repository.getId());
environment.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
environment.put(ENV_REPOSITORY_ID, repository.getId());
environment.put(ENV_REPOSITORY_PATH,
directory.getAbsolutePath());
// add hook environment
if (handler.getConfig().isDisableHookSSLValidation()) {
// disable ssl validation
// Issue 959: https://goo.gl/zH5eY8
environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
}
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
HgEnvironment.prepareEnvironment(
environment,
handler,
hookManager,
request
);
}
}

View File

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

View File

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

View File

@@ -29,87 +29,40 @@
# changegroup.scm = python:scmhooks.callback
#
import os, sys
client = None
# compatibility layer between python 2 and 3 urllib implementations
if sys.version_info[0] < 3:
import urllib, urllib2
# create type alias for url error
URLError = urllib2.URLError
class Python2Client:
def post(self, url, values):
data = urllib.urlencode(values)
# open url but ignore proxy settings
proxy_handler = urllib2.ProxyHandler({})
opener = urllib2.build_opener(proxy_handler)
req = urllib2.Request(url, data)
req.add_header("X-XSRF-Token", xsrf)
return opener.open(req)
client = Python2Client()
else:
import urllib.parse, urllib.request, urllib.error
# create type alias for url error
URLError = urllib.error.URLError
class Python3Client:
def post(self, url, values):
data = urllib.parse.urlencode(values)
# open url but ignore proxy settings
proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(proxy_handler)
req = urllib.request.Request(url, data.encode())
req.add_header("X-XSRF-Token", xsrf)
return opener.open(req)
client = Python3Client()
import os, sys, json, socket
# read environment
baseUrl = os.environ['SCM_URL']
port = os.environ['SCM_HOOK_PORT']
challenge = os.environ['SCM_CHALLENGE']
token = os.environ['SCM_BEARER_TOKEN']
xsrf = os.environ['SCM_XSRF']
repositoryId = os.environ['SCM_REPOSITORY_ID']
def printMessages(ui, msgs):
for raw in msgs:
line = raw
if hasattr(line, "encode"):
line = line.encode()
if line.startswith(b"_e") or line.startswith(b"_n"):
line = line[2:]
ui.warn(b'%s\n' % line.rstrip())
def print_messages(ui, messages):
for message in messages:
ui.warn(b'%s: %s\n' % message['severity'], message['message'])
def callHookUrl(ui, repo, hooktype, node):
def fire_hook(ui, repo, hooktype, node):
abort = True
try:
url = baseUrl + hooktype.decode("utf-8")
ui.debug( b"send scm-hook to " + url.encode() + b" and " + node + b"\n" )
values = {'node': node.decode("utf-8"), 'challenge': challenge, 'token': token, 'repositoryPath': repo.root, 'repositoryId': repositoryId}
conn = client.post(url, values)
if 200 <= conn.code < 300:
ui.debug( b"scm-hook " + hooktype + b" success with status code " + str(conn.code).encode() + b"\n" )
printMessages(ui, conn)
abort = False
else:
ui.warn( b"ERROR: scm-hook failed with error code " + str(conn.code).encode() + b"\n" )
except URLError as e:
msg = None
# some URLErrors have no read method
if hasattr(e, "read"):
msg = e.read()
elif hasattr(e, "code"):
msg = "scm-hook failed with error code " + e.code + "\n"
else:
msg = str(e)
if len(msg) > 0:
printMessages(ui, msg.splitlines(True))
else:
ui.warn( b"ERROR: scm-hook failed with an unknown error\n" )
ui.traceback()
values = {'token': token, 'type': hooktype, 'repositoryId': repositoryId, 'challenge': challenge, 'node': node.decode('utf8') }
ui.debug( b"send scm-hook for " + node + b"\n" )
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", int(port)))
s.sendall(json.dumps(values).encode('utf-8'))
s.sendall(b'\0')
received = []
received = s.recv(1)
while b != b'\0':
received.append(b)
received = s.recv(1)
message = b''.join(bytes).decode('utf-8')
response = json.loads(message)
abort = response['abort']
print_messages(ui, response['messages'])
except ValueError:
ui.warn( b"scm-hook failed with an exception\n" )
ui.traceback()
@@ -118,8 +71,8 @@ def callHookUrl(ui, repo, hooktype, node):
def callback(ui, repo, hooktype, node=None):
abort = True
if node != None:
if len(baseUrl) > 0:
abort = callHookUrl(ui, repo, hooktype, node)
if len(port) > 0:
abort = fire_hook(ui, repo, hooktype, node)
else:
ui.warn(b"ERROR: scm-manager hooks are disabled, please check your configuration and the scm-manager log for details\n")
abort = False
@@ -127,7 +80,7 @@ def callback(ui, repo, hooktype, node=None):
ui.warn(b"changeset node is not available")
return abort
def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
def pre_hook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
# older mercurial versions
if pending != None:
pending()
@@ -146,7 +99,7 @@ def preHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
ui.debug(b"mercurial does not support currenttransation")
# do nothing
return callback(ui, repo, hooktype, node)
return callback(ui, repo, "PRE_RECEIVE", node)
def postHook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
return callback(ui, repo, hooktype, node)
def post_hook(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
return callback(ui, repo, "POST_RECEIVE", node)

View File

@@ -52,7 +52,6 @@ public class HgConfigDtoToHgConfigMapperTest {
assertEquals("/etc/", config.getPythonPath());
assertTrue(config.isShowRevisionInId());
assertTrue(config.isUseOptimizedBytecode());
assertTrue(config.isDisableHookSSLValidation());
assertTrue(config.isEnableHttpPostArgs());
}
@@ -65,7 +64,6 @@ public class HgConfigDtoToHgConfigMapperTest {
configDto.setPythonPath("/etc/");
configDto.setShowRevisionInId(true);
configDto.setUseOptimizedBytecode(true);
configDto.setDisableHookSSLValidation(true);
configDto.setEnableHttpPostArgs(true);
return configDto;

View File

@@ -0,0 +1,149 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContext;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.repository.hooks.HookServer;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.CipherUtil;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.repository.DefaultHgEnvironmentBuilder.*;
@ExtendWith(MockitoExtension.class)
class DefaultHgEnvironmentBuilderTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AccessTokenBuilderFactory accessTokenBuilderFactory;
@Mock
private HgRepositoryHandler repositoryHandler;
@Mock
private HookEnvironment hookEnvironment;
@Mock
private HookServer server;
@InjectMocks
private DefaultHgEnvironmentBuilder builder;
private Path directory;
@BeforeEach
void setBaseDir(@TempDir Path directory) {
this.directory = directory;
TempSCMContextProvider context = (TempSCMContextProvider) SCMContext.getContext();
context.setBaseDirectory(directory.resolve("home").toFile());
}
@Test
void shouldReturnReadEnvironment() {
Repository heartOfGold = prepareForRead("/usr/lib/python", "42");
Map<String, String> env = builder.read(heartOfGold);
assertReadEnv(env, "/usr/lib/python", "42");
}
@Test
void shouldReturnWriteEnvironment() throws IOException {
Repository heartOfGold = prepareForWrite("/opt/python", "21");
Map<String, String> env = builder.write(heartOfGold);
assertReadEnv(env, "/opt/python", "21");
String bearer = CipherUtil.getInstance().decode(env.get(ENV_BEARER_TOKEN));
assertThat(bearer).isEqualTo("secretAC");
assertThat(env)
.containsEntry(ENV_CHALLENGE, "challenge")
.containsEntry(ENV_HOOK_PORT, "2042");
}
@Test
void shouldThrowIllegalStateIfServerCouldNotBeStarted() throws IOException {
when(server.start()).thenThrow(new IOException("failed to start"));
Repository repository = prepareForRead("/usr", "42");
assertThrows(IllegalStateException.class, () -> builder.write(repository));
}
private Repository prepareForWrite(String pythonPath, String id) throws IOException {
Repository heartOfGold = prepareForRead(pythonPath, id);
applyAccessToken("secretAC");
when(server.start()).thenReturn(2042);
when(hookEnvironment.getChallenge()).thenReturn("challenge");
return heartOfGold;
}
private void applyAccessToken(String compact) {
AccessToken accessToken = mock(AccessToken.class);
when(accessTokenBuilderFactory.create().build()).thenReturn(accessToken);
when(accessToken.compact()).thenReturn(compact);
}
private void assertReadEnv(Map<String, String> env, String pythonPath, String repositoryId) {
assertThat(env)
.containsEntry(ENV_REPOSITORY_ID, repositoryId)
.containsEntry(ENV_REPOSITORY_NAME, "hitchhiker/HeartOfGold")
.containsEntry(ENV_HTTP_POST_ARGS, "false")
.containsEntry(ENV_REPOSITORY_PATH, directory.resolve("repo").toAbsolutePath().toString())
.containsEntry(ENV_PYTHON_PATH, pythonPath + File.pathSeparator + directory.resolve("home/lib/python"));
}
@Nonnull
private Repository prepareForRead(String pythonPath, String id) {
when(repositoryHandler.getDirectory(id)).thenReturn(directory.resolve("repo").toFile());
HgConfig config = new HgConfig();
config.setPythonPath(pythonPath);
when(repositoryHandler.getConfig()).thenReturn(config);
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
heartOfGold.setId(id);
return heartOfGold;
}
}

View File

@@ -24,27 +24,17 @@
package sonia.scm.repository;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collections;
import java.util.Map;
import java.io.File;
import java.io.IOException;
/**
*
* @author Sebastian Sdorra
*/
public class HgVersionHandler extends AbstractHgHandler
{
public HgVersionHandler(HgRepositoryHandler handler, HgContext context,
File directory)
{
super(handler, context, null, directory);
public class EmptyHgEnvironmentBuilder implements HgEnvironmentBuilder {
@Override
public Map<String, String> read(Repository repository) {
return Collections.emptyMap();
}
//~--- get methods ----------------------------------------------------------
public HgVersion getVersion() throws IOException {
return getResultFromScript(HgVersion.class, HgPythonScript.VERSION);
@Override
public Map<String, String> write(Repository repository) {
return Collections.emptyMap();
}
}

View File

@@ -1,111 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Scope;
import com.google.inject.servlet.RequestScoped;
import com.google.inject.util.Providers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgContextProviderTest {
@Mock
private Scope scope;
@Test
void shouldThrowNonOutOfScopeProvisionExceptions() {
Provider<HgContextRequestStore> provider = () -> {
throw new RuntimeException("something different");
};
when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider);
Injector injector = Guice.createInjector(new HgContextModule(scope));
assertThrows(ProvisionException.class, () -> injector.getInstance(HgContext.class));
}
@Test
void shouldCreateANewInstanceIfOutOfRequestScope() {
Provider<HgContextRequestStore> provider = () -> {
throw new OutOfScopeException("no request");
};
when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider);
Injector injector = Guice.createInjector(new HgContextModule(scope));
HgContext contextOne = injector.getInstance(HgContext.class);
HgContext contextTwo = injector.getInstance(HgContext.class);
assertThat(contextOne).isNotSameAs(contextTwo);
}
@Test
void shouldInjectFromRequestScope() {
HgContextRequestStore requestStore = new HgContextRequestStore();
Provider<HgContextRequestStore> provider = Providers.of(requestStore);
when(scope.scope(any(Key.class), any(Provider.class))).thenReturn(provider);
Injector injector = Guice.createInjector(new HgContextModule(scope));
HgContext contextOne = injector.getInstance(HgContext.class);
HgContext contextTwo = injector.getInstance(HgContext.class);
assertThat(contextOne).isSameAs(contextTwo);
}
private static class HgContextModule extends AbstractModule {
private Scope scope;
private HgContextModule(Scope scope) {
this.scope = scope;
}
@Override
protected void configure() {
bindScope(RequestScoped.class, scope);
bind(HgContextRequestStore.class);
bind(HgContext.class).toProvider(HgContextProvider.class);
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.aragost.javahg.Repository;
import com.google.common.collect.ImmutableMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.hooks.HookEnvironment;
import javax.annotation.Nonnull;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgRepositoryFactoryTest {
private HgRepositoryHandler handler;
@Mock
private HookEnvironment hookEnvironment;
@Mock
private HgEnvironmentBuilder environmentBuilder;
private HgRepositoryFactory factory;
private sonia.scm.repository.Repository heartOfGold;
@BeforeEach
void setUpFactory(@TempDir Path directory) {
handler = HgTestUtil.createHandler(directory.toFile());
assumeTrue(handler.isConfigured());
factory = new HgRepositoryFactory(handler, hookEnvironment, environmentBuilder);
heartOfGold = createRepository();
}
@Test
void shouldOpenRepositoryForRead() {
Repository repository = factory.openForRead(heartOfGold);
assertThat(repository).isNotNull();
verify(environmentBuilder).read(heartOfGold);
}
@Test
void shouldOpenRepositoryForWrite() {
Repository repository = factory.openForWrite(heartOfGold);
assertThat(repository).isNotNull();
verify(environmentBuilder).write(heartOfGold);
}
@Test
void shouldFallbackToUTF8OnUnknownEncoding() {
handler.getConfig().setEncoding("unknown");
Repository repository = factory.openForRead(heartOfGold);
assertThat(repository.getBaseRepository().getConfiguration().getEncoding()).isEqualTo(StandardCharsets.UTF_8);
}
@Test
void shouldSetPendingChangesetState() {
when(hookEnvironment.isPending()).thenReturn(true);
Repository repository = factory.openForRead(heartOfGold);
assertThat(repository.getBaseRepository().getConfiguration().isEnablePendingChangesets())
.isTrue();
}
@Test
void shouldPassEnvironment() {
when(environmentBuilder.read(heartOfGold)).thenReturn(ImmutableMap.of("spaceship", "heartOfGold"));
Repository repository = factory.openForRead(heartOfGold);
assertThat(repository.getBaseRepository().getConfiguration().getEnvironment())
.containsEntry("spaceship", "heartOfGold");
}
@Nonnull
private sonia.scm.repository.Repository createRepository() {
sonia.scm.repository.Repository heartOfGold = RepositoryTestData.createHeartOfGold("hg");
heartOfGold.setId("42");
handler.create(heartOfGold);
return heartOfGold;
}
}

View File

@@ -32,13 +32,17 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.HgVersionCommand;
import sonia.scm.store.ConfigurationStoreFactory;
import java.io.File;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
@@ -52,9 +56,6 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock
private ConfigurationStoreFactory factory;
@Mock
private com.google.inject.Provider<HgContext> provider;
@Override
protected void checkDirectory(File directory) {
File hgDirectory = new File(directory, ".hg");
@@ -70,7 +71,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) {
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null, null);
HgRepositoryHandler handler = new HgRepositoryHandler(factory, locationResolver, null, null);
handler.init(contextProvider);
HgTestUtil.checkForSkip(handler);
@@ -80,7 +81,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null, null);
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, locationResolver, null, null);
HgConfig hgConfig = new HgConfig();
hgConfig.setHgBinary("hg");
@@ -91,4 +92,20 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
File path = repositoryHandler.getDirectory(repository.getId());
assertEquals(repoPath.toString() + File.separator + RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY, path.getAbsolutePath());
}
@Test
public void shouldReturnVersionInformation() {
PluginLoader pluginLoader = mock(PluginLoader.class);
when(pluginLoader.getUberClassLoader()).thenReturn(HgRepositoryHandler.class.getClassLoader());
HgVersionCommand versionCommand = mock(HgVersionCommand.class);
when(versionCommand.get()).thenReturn(new HgVersion("5.2.0", "3.7.2"));
HgRepositoryHandler handler = new HgRepositoryHandler(
factory, locationResolver, pluginLoader, null
);
String versionInformation = handler.getVersionInformation(versionCommand);
assertThat(versionInformation).startsWith("scm-hg-version/").endsWith("python/3.7.2 mercurial/5.2.0");
}
}

View File

@@ -29,16 +29,13 @@ package sonia.scm.repository;
import org.junit.Assume;
import sonia.scm.SCMContext;
import sonia.scm.TempDirRepositoryLocationResolver;
import sonia.scm.security.AccessToken;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.nio.file.Path;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
@@ -80,49 +77,26 @@ public final class HgTestUtil
}
}
/**
* Method description
*
*
* @param directory
*
* @return
*/
public static HgRepositoryHandler createHandler(File directory) {
TempSCMContextProvider context =
(TempSCMContextProvider) SCMContext.getContext();
TempSCMContextProvider context = (TempSCMContextProvider) SCMContext.getContext();
context.setBaseDirectory(directory);
RepositoryDAO repoDao = mock(RepositoryDAO.class);
RepositoryLocationResolver repositoryLocationResolver = new TempDirRepositoryLocationResolver(directory);
HgRepositoryHandler handler =
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null);
HgRepositoryHandler handler = new HgRepositoryHandler(
new InMemoryConfigurationStoreFactory(),
repositoryLocationResolver,
null,
null
);
handler.init(context);
return handler;
}
/**
* Method description
*
*
* @return
*/
public static HgHookManager createHookManager()
{
HgHookManager hookManager = mock(HgHookManager.class);
when(hookManager.getChallenge()).thenReturn("challenge");
when(hookManager.createUrl()).thenReturn(
"http://localhost:8081/scm/hook/hg/");
when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn(
"http://localhost:8081/scm/hook/hg/");
AccessToken accessToken = mock(AccessToken.class);
when(accessToken.compact()).thenReturn("");
when(hookManager.getAccessToken()).thenReturn(accessToken);
return hookManager;
public static HgRepositoryFactory createFactory(HgRepositoryHandler handler, File directory) {
return new HgRepositoryFactory(
handler, new HookEnvironment(), new EmptyHgEnvironmentBuilder(), repository -> directory
);
}
}

View File

@@ -24,7 +24,6 @@
package sonia.scm.repository.hooks;
import com.google.inject.util.Providers;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
@@ -35,10 +34,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.NotFoundException;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.api.HgHookMessageProvider;
import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.CipherUtil;
@@ -48,6 +47,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -55,10 +55,13 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class HgHookHandlerTest {
class DefaultHookHandlerTest {
@Mock
private HgRepositoryHandler repositoryHandler;
private HookContextProviderFactory hookContextProviderFactory;
@Mock
private HgHookContextProvider contextProvider;
@Mock
private HookEventFacade hookEventFacade;
@@ -71,7 +74,7 @@ class HgHookHandlerTest {
private HookEnvironment hookEnvironment;
private HgHookHandler handler;
private DefaultHookHandler handler;
@Mock
private Subject subject;
@@ -81,11 +84,13 @@ class HgHookHandlerTest {
ThreadContext.bind(subject);
hookEnvironment = new HookEnvironment();
DefaultHookHandlerFactory factory = new DefaultHookHandlerFactory(
repositoryHandler, hookEventFacade, Providers.of(hookEnvironment)
);
handler = factory.create(socket);
handler = new DefaultHookHandler(hookContextProviderFactory, hookEventFacade, hookEnvironment, socket);
}
private void mockMessageProvider() {
when(hookContextProviderFactory.create("42", "abc")).thenReturn(contextProvider);
when(contextProvider.getHgMessageProvider()).thenReturn(new HgHookMessageProvider());
}
@AfterEach
@@ -95,10 +100,11 @@ class HgHookHandlerTest {
@Test
void shouldFireHook() throws IOException {
mockMessageProvider();
when(hookEventFacade.handle("42")).thenReturn(hookEventHandler);
HgHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
HgHookHandler.Response response = send(request);
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertSuccess(response, RepositoryHookType.POST_RECEIVE);
assertThat(hookEnvironment.isPending()).isFalse();
@@ -106,13 +112,22 @@ class HgHookHandlerTest {
@Test
void shouldSetPendingStateOnPreReceiveHooks() throws IOException {
mockMessageProvider();
when(hookEventFacade.handle("42")).thenReturn(hookEventHandler);
HgHookHandler.Request request = createRequest(RepositoryHookType.PRE_RECEIVE);
HgHookHandler.Response response = send(request);
// we have to capture the pending state, when the hook is fired
// because the state is cleared before the method ends
AtomicReference<Boolean> ref = new AtomicReference<>(Boolean.FALSE);
doAnswer(ic -> {
ref.set(hookEnvironment.isPending());
return null;
}).when(hookEventHandler).fireHookEvent(RepositoryHookType.PRE_RECEIVE, contextProvider);
DefaultHookHandler.Request request = createRequest(RepositoryHookType.PRE_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertSuccess(response, RepositoryHookType.PRE_RECEIVE);
assertThat(hookEnvironment.isPending()).isTrue();
assertThat(ref.get()).isTrue();
}
@Test
@@ -121,8 +136,8 @@ class HgHookHandlerTest {
.when(hookEventFacade)
.handle("42");
HgHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
HgHookHandler.Response response = send(request);
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertError(response, "unknown");
}
@@ -133,28 +148,40 @@ class HgHookHandlerTest {
.when(subject)
.login(any(AuthenticationToken.class));
HgHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
HgHookHandler.Response response = send(request);
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertError(response, "authentication");
}
@Test
void shouldHandleNotFoundException() throws IOException {
doThrow(NotFoundException.class)
.when(hookEventFacade)
.handle("42");
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE);
DefaultHookHandler.Response response = send(request);
assertError(response, "not found");
}
@Test
void shouldReturnErrorWithInvalidChallenge() throws IOException {
HgHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE, "something-different");
HgHookHandler.Response response = send(request);
DefaultHookHandler.Request request = createRequest(RepositoryHookType.POST_RECEIVE, "something-different");
DefaultHookHandler.Response response = send(request);
assertError(response, "challenge");
}
private void assertSuccess(HgHookHandler.Response response, RepositoryHookType type) {
private void assertSuccess(DefaultHookHandler.Response response, RepositoryHookType type) {
assertThat(response.getMessages()).isEmpty();
assertThat(response.isAbort()).isFalse();
verify(hookEventHandler).fireHookEvent(eq(type), any(HgHookContextProvider.class));
}
private void assertError(HgHookHandler.Response response, String message) {
private void assertError(DefaultHookHandler.Response response, String message) {
assertThat(response.isAbort()).isTrue();
assertThat(response.getMessages()).hasSize(1);
HgHookMessage hgHookMessage = response.getMessages().get(0);
@@ -163,19 +190,19 @@ class HgHookHandlerTest {
}
@Nonnull
private HgHookHandler.Request createRequest(RepositoryHookType type) {
private DefaultHookHandler.Request createRequest(RepositoryHookType type) {
return createRequest(type, hookEnvironment.getChallenge());
}
@Nonnull
private HgHookHandler.Request createRequest(RepositoryHookType type, String challenge) {
private DefaultHookHandler.Request createRequest(RepositoryHookType type, String challenge) {
String secret = CipherUtil.getInstance().encode("secret");
return new HgHookHandler.Request(
return new DefaultHookHandler.Request(
secret, type, "42", challenge, "abc"
);
}
private HgHookHandler.Response send(HgHookHandler.Request request) throws IOException {
private DefaultHookHandler.Response send(DefaultHookHandler.Request request) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
Sockets.send(buffer, request);
ByteArrayInputStream input = new ByteArrayInputStream(buffer.toByteArray());
@@ -185,7 +212,7 @@ class HgHookHandlerTest {
handler.run();
return Sockets.read(new ByteArrayInputStream(output.toByteArray()), HgHookHandler.Response.class);
return Sockets.read(new ByteArrayInputStream(output.toByteArray()), DefaultHookHandler.Response.class);
}
}

View File

@@ -22,57 +22,49 @@
* SOFTWARE.
*/
package sonia.scm.repository;
package sonia.scm.repository.hooks;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.security.AccessToken;
import sonia.scm.security.Xsrf;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.spi.HgHookContextProvider;
import java.util.HashMap;
import java.util.Map;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgEnvironmentTest {
class HookContextProviderFactoryTest {
@Mock
HgRepositoryHandler handler;
private RepositoryManager repositoryManager;
@Mock
HgHookManager hookManager;
private HgRepositoryHandler repositoryHandler;
@Mock
private HgRepositoryFactory repositoryFactory;
@InjectMocks
private HookContextProviderFactory factory;
@Test
void shouldExtractXsrfTokenWhenSet() {
AccessToken accessToken = mock(AccessToken.class);
when(accessToken.compact()).thenReturn("");
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(of("XSRF Token"));
when(hookManager.getAccessToken()).thenReturn(accessToken);
Map<String, String> environment = new HashMap<>();
HgEnvironment.prepareEnvironment(environment, handler, hookManager);
assertThat(environment).contains(entry("SCM_XSRF", "XSRF Token"));
void shouldCreateHookContextProvider() {
when(repositoryManager.get("42")).thenReturn(RepositoryTestData.create42Puzzle());
HgHookContextProvider provider = factory.create("42", "xyz");
assertThat(provider).isNotNull();
}
@Test
void shouldIgnoreXsrfWhenNotSetButStillContainDummy() {
AccessToken accessToken = mock(AccessToken.class);
when(accessToken.compact()).thenReturn("");
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(empty());
when(hookManager.getAccessToken()).thenReturn(accessToken);
Map<String, String> environment = new HashMap<>();
HgEnvironment.prepareEnvironment(environment, handler, hookManager);
assertThat(environment).containsKeys("SCM_XSRF");
void shouldThrowNotFoundExceptionWithoutRepository() {
assertThrows(NotFoundException.class, () -> factory.create("42", "xyz"));
}
}

View File

@@ -89,7 +89,7 @@ class HookServerTest {
}
}
public static class HelloHandler implements Runnable {
public static class HelloHandler implements HookHandler {
private final Socket socket;

View File

@@ -29,13 +29,16 @@ package sonia.scm.repository.spi;
import org.junit.After;
import org.junit.Before;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.util.MockUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
/**
@@ -49,31 +52,22 @@ public class AbstractHgCommandTestBase extends ZippedRepositoryTestBase
* Method description
*
*
* @throws IOException
*/
@After
public void close() throws IOException
{
public void close() {
if (cmdContext != null)
{
cmdContext.close();
}
}
/**
* Method description
*
*
* @throws IOException
*/
@Before
public void initHgHandler() throws IOException {
this.handler = HgTestUtil.createHandler(tempFolder.newFolder());
HgTestUtil.checkForSkip(handler);
cmdContext = new HgCommandContext(HgTestUtil.createHookManager(), handler,
RepositoryTestData.createHeartOfGold(), repositoryDirectory);
HgRepositoryFactory factory = HgTestUtil.createFactory(handler, repositoryDirectory);
cmdContext = new HgCommandContext(handler, factory, RepositoryTestData.createHeartOfGold());
}
//~--- set methods ----------------------------------------------------------

View File

@@ -29,12 +29,10 @@ import com.google.inject.util.Providers;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.repository.Branch;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.util.List;
@@ -47,10 +45,8 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase {
@Before
public void initWorkingCopyFactory() {
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager());
workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(hgRepositoryEnvironmentBuilder), new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
workingCopyFactory = new SimpleHgWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
@Override
public void configure(PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test

View File

@@ -110,17 +110,10 @@ public class HgIncomingCommandTest extends IncomingOutgoingTestBase
cmd.getIncomingChangesets(request);
}
/**
* Method description
*
*
* @return
*/
private HgIncomingCommand createIncomingCommand()
{
private HgIncomingCommand createIncomingCommand() {
return new HgIncomingCommand(
new HgCommandContext(
HgTestUtil.createHookManager(), handler, incomingRepository,
incomingDirectory), handler);
new HgCommandContext(handler, HgTestUtil.createFactory(handler, incomingDirectory), incomingRepository),
handler
);
}
}

View File

@@ -40,12 +40,11 @@ import static org.assertj.core.api.Assertions.assertThat;
public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
private HgModificationsCommand outgoingModificationsCommand;
@Before
public void init() {
HgCommandContext outgoingContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, outgoingRepository, outgoingDirectory);
HgCommandContext outgoingContext = new HgCommandContext(handler, HgTestUtil.createFactory(handler, outgoingDirectory), outgoingRepository);
outgoingModificationsCommand = new HgModificationsCommand(outgoingContext);
}
@@ -116,10 +115,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(1)
@@ -136,10 +135,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.isEmpty();
@@ -161,10 +160,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.isEmpty();
@@ -189,7 +188,7 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(1)
@@ -197,10 +196,10 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
.containsOnly(file);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.hasSize(0);
.isEmpty();
};
}
@@ -214,13 +213,13 @@ public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
.containsOnly(addedFile);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
.isEmpty();
assertThat(modifications.getRenamed())
.as("renamed files modifications")
.hasSize(0);
.isEmpty();
};
}
}

View File

@@ -24,7 +24,6 @@
package sonia.scm.repository.spi;
import com.google.inject.util.Providers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -32,12 +31,9 @@ import org.junit.rules.TemporaryFolder;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.Person;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.io.File;
import java.io.FileOutputStream;
@@ -54,9 +50,7 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase {
@Before
public void initHgModifyCommand() {
HgHookManager hookManager = HgTestUtil.createHookManager();
HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager);
SimpleHgWorkingCopyFactory workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(environmentBuilder), new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
SimpleHgWorkingCopyFactory workingCopyFactory = new SimpleHgWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
@Override
public void configure(com.aragost.javahg.commands.PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test

View File

@@ -106,17 +106,10 @@ public class HgOutgoingCommandTest extends IncomingOutgoingTestBase
System.out.println(cpr.getChangesets());
}
/**
* Method description
*
*
* @return
*/
private HgOutgoingCommand createOutgoingCommand()
{
private HgOutgoingCommand createOutgoingCommand() {
return new HgOutgoingCommand(
new HgCommandContext(
HgTestUtil.createHookManager(), handler, outgoingRepository,
outgoingDirectory), handler);
new HgCommandContext(handler, HgTestUtil.createFactory(handler, outgoingDirectory), outgoingRepository),
handler
);
}
}

View File

@@ -0,0 +1,137 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import com.google.common.base.Joiner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgVersion;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HgVersionCommandTest {
private static final String PYTHON_OUTPUT = String.join("\n",
"3.9.0 (default, Oct 27 2020, 14:15:17)",
"[Clang 12.0.0 (clang-1200.0.32.21)]"
);
private Map<String, Process> outputs;
@BeforeEach
void setUpOutputs() {
outputs = new HashMap<>();
}
@Test
void shouldReturnHgVersion() {
command("/usr/local/bin/hg", HgVersionCommand.HG_ARGS, "5.5.2", 0);
command("/opt/python/bin/python", HgVersionCommand.PYTHON_ARGS, PYTHON_OUTPUT, 0);
HgVersion hgVersion = getVersion("/usr/local/bin/hg", "/opt/python/bin/python");
assertThat(hgVersion.getMercurial()).isEqualTo("5.5.2");
assertThat(hgVersion.getPython()).isEqualTo("3.9.0");
}
@Test
void shouldReturnUnknownMercurialVersionOnNonZeroExitCode() {
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() {
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() {
command("hg", HgVersionCommand.HG_ARGS, "1.0.0", 0);
command("python", HgVersionCommand.PYTHON_ARGS, "abcdef", 0);
HgVersion hgVersion = getVersion("hg", "python");
assertThat(hgVersion.getMercurial()).isEqualTo("1.0.0");
assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN);
}
@Test
void shouldReturnUnknownForIOException() {
HgVersionCommand command = new HgVersionCommand(new HgConfig(), cmd -> {
throw new IOException("failed");
});
HgVersion hgVersion = command.get();
assertThat(hgVersion.getMercurial()).isEqualTo(HgVersion.UNKNOWN);
assertThat(hgVersion.getPython()).isEqualTo(HgVersion.UNKNOWN);
}
private Process command(String command, String[] args, String content, int exitValue) {
Process process = mock(Process.class);
when(process.getInputStream()).thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
when(process.exitValue()).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();
}
}

View File

@@ -39,7 +39,6 @@ import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import sonia.scm.AbstractTestBase;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.user.User;
@@ -88,7 +87,6 @@ public abstract class IncomingOutgoingTestBase extends AbstractTestBase
when(handler.getDirectory(outgoingRepository.getId())).thenReturn(
outgoingDirectory);
when(handler.getConfig()).thenReturn(temp.getConfig());
when(handler.getHgContext()).thenReturn(new HgContext());
}
//~--- set methods ----------------------------------------------------------

View File

@@ -30,12 +30,11 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgEnvironmentBuilder;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.work.SimpleCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.repository.work.WorkingCopy;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.io.File;
import java.io.IOException;
@@ -57,9 +56,7 @@ public class SimpleHgWorkingCopyFactoryTest extends AbstractHgCommandTestBase {
@Before
public void bindScmProtocol() throws IOException {
workdirProvider = new WorkdirProvider(temporaryFolder.newFolder());
HgHookManager hookManager = HgTestUtil.createHookManager();
HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager);
workingCopyFactory = new SimpleHgWorkingCopyFactory(Providers.of(environmentBuilder), new SimpleCachingWorkingCopyPool(workdirProvider)) {
workingCopyFactory = new SimpleHgWorkingCopyFactory(new SimpleCachingWorkingCopyPool(workdirProvider)) {
@Override
public void configure(com.aragost.javahg.commands.PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test

View File

@@ -1,60 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
import org.junit.Test;
import sonia.scm.repository.HgRepositoryHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sonia.scm.web.HgHookCallbackServlet.PARAM_REPOSITORYID;
public class HgHookCallbackServletTest {
@Test
public void shouldExtractCorrectRepositoryId() throws ServletException, IOException {
HgRepositoryHandler handler = mock(HgRepositoryHandler.class);
HgHookCallbackServlet servlet = new HgHookCallbackServlet(null, handler, null, null);
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
when(request.getContextPath()).thenReturn("http://example.com/scm");
when(request.getRequestURI()).thenReturn("http://example.com/scm/hook/hg/pretxnchangegroup");
String path = "/tmp/hg/12345";
when(request.getParameter(PARAM_REPOSITORYID)).thenReturn(path);
servlet.doPost(request, response);
verify(response, never()).sendError(anyInt());
}
}

View File

@@ -114,14 +114,14 @@ public class ScmSecurityModule extends ShiroWebModule
}
// bind constant
bindConstant().annotatedWith(Names.named("shiro.loginUrl")).to(
"/index.html");
bindConstant().annotatedWith(Names.named("shiro.loginUrl")).to("/index.html");
// disable access to mustache resources
addFilterChain("/**.mustache", filterConfig(ROLES, "nobody"));
// disable session
addFilterChain("/**", NO_SESSION_CREATION);
bindConstant().annotatedWith(Names.named("shiro.sessionStorageEnabled")).to(false);
}
/**