Merge branch 'develop' into feature/import_git_from_url

This commit is contained in:
Eduard Heimbuch
2020-12-01 12:18:13 +01:00
84 changed files with 2721 additions and 3009 deletions

View File

@@ -30,11 +30,13 @@ 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 implements UpdateHgConfigDto {
private boolean disabled;
private String encoding;
@@ -44,7 +46,6 @@ public class HgConfigDto extends HalRepresentation implements UpdateHgConfigDto
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

@@ -39,7 +39,5 @@ interface UpdateHgConfigDto {
boolean isShowRevisionInId();
boolean isDisableHookSSLValidation();
boolean isEnableHttpPostArgs();
}

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,143 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import sonia.scm.TransactionId;
import sonia.scm.repository.hooks.HookEnvironment;
import sonia.scm.repository.hooks.HookServer;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Xsrf;
import sonia.scm.web.HgUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.util.Map;
@Singleton
public class DefaultHgEnvironmentBuilder implements HgEnvironmentBuilder {
@VisibleForTesting
static final String ENV_PYTHON_PATH = "PYTHONPATH";
@VisibleForTesting
static final String ENV_HOOK_PORT = "SCM_HOOK_PORT";
@VisibleForTesting
static final String ENV_CHALLENGE = "SCM_CHALLENGE";
@VisibleForTesting
static final String ENV_BEARER_TOKEN = "SCM_BEARER_TOKEN";
@VisibleForTesting
static final String ENV_REPOSITORY_NAME = "REPO_NAME";
@VisibleForTesting
static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
@VisibleForTesting
static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
@VisibleForTesting
static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
@VisibleForTesting
static final String ENV_TRANSACTION_ID = "SCM_TRANSACTION_ID";
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
private final HgRepositoryHandler repositoryHandler;
private final HookEnvironment hookEnvironment;
private final HookServer server;
private int hookPort = -1;
@Inject
public DefaultHgEnvironmentBuilder(
AccessTokenBuilderFactory accessTokenBuilderFactory, HgRepositoryHandler repositoryHandler,
HookEnvironment hookEnvironment, HookServer server
) {
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
this.repositoryHandler = repositoryHandler;
this.hookEnvironment = hookEnvironment;
this.server = server;
}
@Override
public Map<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()));
env.put(ENV_BEARER_TOKEN, accessToken());
env.put(ENV_CHALLENGE, hookEnvironment.getChallenge());
TransactionId.get().ifPresent(transactionId -> env.put(ENV_TRANSACTION_ID, transactionId));
}
private String accessToken() {
AccessToken accessToken = accessTokenBuilderFactory.create()
// disable xsrf protection, because we can not access the http servlet request for verification
.custom(Xsrf.TOKEN_KEY, null)
.build();
return CipherUtil.getInstance().encode(accessToken.compact());
}
private synchronized int getHookPort() {
if (hookPort > 0) {
return hookPort;
}
try {
hookPort = server.start();
} catch (IOException ex) {
throw new IllegalStateException("failed to start mercurial hook server");
}
return hookPort;
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
@@ -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

@@ -21,30 +21,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- JDK imports ------------------------------------------------------------
import com.google.inject.ImplementedBy;
import java.io.File;
import java.io.IOException;
import java.util.Map;
/**
*
* @author Sebastian Sdorra
*/
public class HgVersionHandler extends AbstractHgHandler
{
public HgVersionHandler(HgRepositoryHandler handler, HgContext context,
File directory)
{
super(handler, context, null, directory);
}
//~--- get methods ----------------------------------------------------------
public HgVersion getVersion() throws IOException {
return getResultFromScript(HgVersion.class, HgPythonScript.VERSION);
}
@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

@@ -38,80 +38,30 @@ import java.io.File;
*/
public enum HgPythonScript {
HOOK("scmhooks.py"), HGWEB("hgweb.py"), VERSION("version.py");
HOOK("scmhooks.py"), HGWEB("hgweb.py");
/** Field description */
private static final String BASE_DIRECTORY =
"lib".concat(File.separator).concat("python");
/** Field description */
private static final String BASE_DIRECTORY = "lib".concat(File.separator).concat("python");
private static final String BASE_RESOURCE = "/sonia/scm/python/";
//~--- constructors ---------------------------------------------------------
private final String name;
/**
* Constructs ...
*
*
* @param name
*/
private HgPythonScript(String name)
{
HgPythonScript(String name) {
this.name = name;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param context
*
* @return
*/
public static File getScriptDirectory(SCMContextProvider context)
{
public static File getScriptDirectory(SCMContextProvider context) {
return new File(context.getBaseDirectory(), BASE_DIRECTORY);
}
/**
* Method description
*
*
* @param context
*
* @return
*/
public File getFile(SCMContextProvider context)
{
public File getFile(SCMContextProvider context) {
return new File(getScriptDirectory(context), name);
}
/**
* Method description
*
*
* @return
*/
public String getName()
{
public String getName() {
return name;
}
/**
* Method description
*
*
* @return
*/
public String getResourcePath()
{
public String getResourcePath() {
return BASE_RESOURCE.concat(name);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String name;
}

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()) {
logger.debug("mercurial/python informations: {}", hgVersion);
}
version = MessageFormat.format(version, hgVersion.getPython(),
hgVersion.getMercurial());
} else if (logger.isWarnEnabled()) {
logger.warn("could not retrieve version informations");
}
} catch (Exception ex) {
logger.error("could not read version informations", ex);
}
return version;
String getVersionInformation(HgVersionCommand command) {
String version = getStringFromResource(RESOURCE_VERSION, DEFAULT_VERSION_INFORMATION);
HgVersion hgVersion = command.get();
logger.debug("mercurial/python informations: {}", hgVersion);
return MessageFormat.format(version, hgVersion.getPython(), hgVersion.getMercurial());
}
@Override
@@ -253,28 +213,24 @@ public class HgRepositoryHandler
logger.debug("write python script {}", script.getName());
}
InputStream content = null;
OutputStream output = null;
try {
content = HgRepositoryHandler.class.getResourceAsStream(
script.getResourcePath());
output = new FileOutputStream(script.getFile(context));
try (InputStream content = input(script); OutputStream output = output(context, script)) {
IOUtil.copy(content, output);
} catch (IOException ex) {
logger.error("could not write script", ex);
} finally {
IOUtil.close(content);
IOUtil.close(output);
}
}
}
private InputStream input(HgPythonScript script) {
return HgRepositoryHandler.class.getResourceAsStream(script.getResourcePath());
}
private OutputStream output(SCMContextProvider context, HgPythonScript script) throws FileNotFoundException {
return new FileOutputStream(script.getFile(context));
}
public HgWorkingCopyFactory getWorkingCopyFactory() {
return workingCopyFactory;
}
public JAXBContext getJaxbContext() {
return jaxbContext;
}
}

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

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

View File

@@ -0,0 +1,188 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import com.google.inject.assistedinject.Assisted;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ExceptionWithContext;
import sonia.scm.NotFoundException;
import sonia.scm.TransactionId;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.HgHookMessage;
import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import static java.util.Collections.singletonList;
class DefaultHookHandler implements HookHandler {
private static final Logger LOG = LoggerFactory.getLogger(DefaultHookHandler.class);
private final HookEventFacade hookEventFacade;
private final HookEnvironment environment;
private final HookContextProviderFactory hookContextProviderFactory;
private final Socket socket;
@Inject
public DefaultHookHandler(HookContextProviderFactory hookContextProviderFactory, HookEventFacade hookEventFacade, HookEnvironment environment, @Assisted Socket socket) {
this.hookContextProviderFactory = hookContextProviderFactory;
this.hookEventFacade = hookEventFacade;
this.environment = environment;
this.socket = socket;
}
@Override
public void run() {
LOG.trace("start handling hook protocol");
try (InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream()) {
handleHookRequest(input, output);
} catch (IOException e) {
LOG.warn("failed to read hook request", e);
} finally {
LOG.trace("close client socket");
TransactionId.clear();
close();
}
}
private void handleHookRequest(InputStream input, OutputStream output) throws IOException {
Request request = Sockets.receive(input, Request.class);
TransactionId.set(request.getTransactionId());
Response response = handleHookRequest(request);
Sockets.send(output, response);
}
private Response handleHookRequest(Request request) {
LOG.trace("process {} hook for node {}", request.getType(), request.getNode());
if (!environment.isAcceptAble(request.getChallenge())) {
LOG.warn("received hook with invalid challenge: {}", request.getChallenge());
return error("invalid hook challenge");
}
try {
authenticate(request);
return fireHook(request);
} catch (AuthenticationException ex) {
LOG.warn("hook authentication failed", ex);
return error("hook authentication failed");
}
}
@Nonnull
private Response fireHook(Request request) {
HgHookContextProvider context = hookContextProviderFactory.create(request.getRepositoryId(), request.getNode());
try {
environment.setPending(request.getType() == RepositoryHookType.PRE_RECEIVE);
hookEventFacade.handle(request.getRepositoryId()).fireHookEvent(request.getType(), context);
return new Response(context.getHgMessageProvider().getMessages(), false);
} catch (NotFoundException ex) {
LOG.warn("could not find repository with id {}", request.getRepositoryId(), ex);
return error("repository not found");
} catch (ExceptionWithContext ex) {
LOG.debug("scm exception on hook occurred", ex);
return error(context, ex.getMessage());
} catch (Exception ex) {
LOG.warn("unknown error on hook occurred", ex);
return error(context, "unknown error");
} finally {
environment.clearPendingState();
}
}
private void authenticate(Request request) {
LOG.trace("authenticate hook request");
String token = CipherUtil.getInstance().decode(request.getToken());
BearerToken bearer = BearerToken.valueOf(token);
Subject subject = SecurityUtils.getSubject();
subject.login(bearer);
}
private Response error(HgHookContextProvider context, String message) {
List<HgHookMessage> messages = new ArrayList<>(context.getHgMessageProvider().getMessages());
messages.add(createErrorMessage(message));
return new Response(messages, true);
}
private Response error(String message) {
return new Response(
singletonList(createErrorMessage(message)),
true
);
}
@Nonnull
private HgHookMessage createErrorMessage(String message) {
return new HgHookMessage(HgHookMessage.Severity.ERROR, message);
}
private void close() {
try {
socket.close();
} catch (IOException e) {
LOG.debug("failed to close hook socket", e);
}
}
@Data
@AllArgsConstructor
public static class Request {
private String token;
private RepositoryHookType type;
private String transactionId;
private String repositoryId;
private String challenge;
private String node;
}
@Data
@AllArgsConstructor
public static class Response {
private List<HgHookMessage> messages;
private boolean abort;
}
}

View File

@@ -21,60 +21,37 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
package sonia.scm.repository.hooks;
import sonia.scm.repository.AbstractHgHandler;
import sonia.scm.repository.HgContext;
import sonia.scm.NotFoundException;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.spi.HgHookContextProvider;
//~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject;
import java.io.File;
public class HookContextProviderFactory {
import java.util.Map;
private final RepositoryManager repositoryManager;
private final HgRepositoryHandler repositoryHandler;
private final HgRepositoryFactory repositoryFactory;
/**
*
* @author Sebastian Sdorra
*/
public class AbstractHgCommand extends AbstractHgHandler
{
/**
* Constructs ...
*
*
* @param handler
* @param context
* @param repository
* @param repositoryDirectory
*/
protected AbstractHgCommand(HgRepositoryHandler handler, HgContext context,
Repository repository, File repositoryDirectory)
{
super(handler, context, repository, repositoryDirectory);
@Inject
public HookContextProviderFactory(RepositoryManager repositoryManager, HgRepositoryHandler repositoryHandler, HgRepositoryFactory repositoryFactory) {
this.repositoryManager = repositoryManager;
this.repositoryHandler = repositoryHandler;
this.repositoryFactory = repositoryFactory;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param revision
* @param path
*
* @param request
*
* @return
*/
protected Map<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

@@ -21,28 +21,40 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.inject.servlet.RequestScoped;
package sonia.scm.repository.hooks;
/**
* 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 {
import javax.inject.Singleton;
import java.util.UUID;
private final HgContext context = new HgContext();
@Singleton
public class HookEnvironment {
public HgContext get() {
return context;
private final ThreadLocal<Boolean> threadEnvironment = new ThreadLocal<>();
private final String challenge = UUID.randomUUID().toString();
public String getChallenge() {
return challenge;
}
public boolean isAcceptAble(String challenge) {
return this.challenge.equals(challenge);
}
void setPending(boolean pending) {
threadEnvironment.set(pending);
}
void clearPendingState() {
threadEnvironment.remove();
}
public boolean isPending() {
Boolean threadState = threadEnvironment.get();
if (threadState != null) {
return threadState;
}
return false;
}
}

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

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

View File

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

View File

@@ -0,0 +1,153 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.util.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
@Singleton
public class HookServer implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(HookServer.class);
private final HookHandlerFactory handlerFactory;
private ExecutorService acceptor;
private ExecutorService workerPool;
private ServerSocket serverSocket;
private SecurityManager securityManager;
@Inject
public HookServer(HookHandlerFactory handlerFactory) {
this.handlerFactory = handlerFactory;
}
public int start() throws IOException {
securityManager = SecurityUtils.getSecurityManager();
acceptor = createAcceptor();
workerPool = createWorkerPool();
serverSocket = createServerSocket();
// set timeout to 2 min, to avoid blocking clients
serverSocket.setSoTimeout(2 * 60 * 1000);
accept();
int port = serverSocket.getLocalPort();
LOG.info("open hg hook server on port {}", port);
return port;
}
private void accept() {
acceptor.submit(() -> {
while (!serverSocket.isClosed()) {
try {
LOG.trace("wait for next hook connection");
Socket clientSocket = serverSocket.accept();
LOG.trace("accept incoming hook client from {}", clientSocket.getInetAddress());
HookHandler hookHandler = handlerFactory.create(clientSocket);
workerPool.submit(associateSecurityManager(hookHandler));
} catch (IOException ex) {
LOG.debug("failed to accept socket, possible closed", ex);
}
}
LOG.warn("ServerSocket is closed");
});
}
private Runnable associateSecurityManager(HookHandler hookHandler) {
return () -> {
ThreadContext.bind(securityManager);
try {
hookHandler.run();
} finally {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
};
}
@Nonnull
private ServerSocket createServerSocket() throws IOException {
return new ServerSocket(0, 0, InetAddress.getLoopbackAddress());
}
private ExecutorService createAcceptor() {
return Executors.newSingleThreadExecutor(
createThreadFactory("HgHookAcceptor")
);
}
private ExecutorService createWorkerPool() {
return Executors.newCachedThreadPool(
createThreadFactory("HgHookWorker-%d")
);
}
@Nonnull
private ThreadFactory createThreadFactory(String hgHookAcceptor) {
return new ThreadFactoryBuilder()
.setNameFormat(hgHookAcceptor)
.build();
}
@Override
public void close() {
closeSocket();
shutdown(acceptor);
shutdown(workerPool);
}
private void closeSocket() {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException ex) {
LOG.warn("failed to close server socket", ex);
}
}
}
private void shutdown(ExecutorService acceptor) {
if (acceptor != null) {
acceptor.shutdown();
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.hooks;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
class Sockets {
private static final Logger LOG = LoggerFactory.getLogger(Sockets.class);
private static final int READ_LIMIT = 8192;
private static final ObjectMapper objectMapper = new ObjectMapper();
private Sockets() {
}
static void send(OutputStream out, Object object) throws IOException {
byte[] bytes = objectMapper.writeValueAsBytes(object);
LOG.trace("send message length of {} to socket", bytes.length);
DataOutputStream dataOutputStream = new DataOutputStream(out);
dataOutputStream.writeInt(bytes.length);
LOG.trace("send message to socket");
dataOutputStream.write(bytes);
LOG.trace("flush socket");
out.flush();
}
static <T> T receive(InputStream in, Class<T> type) throws IOException {
LOG.trace("read {} from socket", type);
DataInputStream dataInputStream = new DataInputStream(in);
int length = dataInputStream.readInt();
LOG.trace("read message length of {} from socket", length);
if (length > READ_LIMIT) {
String message = String.format("received length of %d, which exceeds the limit of %d", length, READ_LIMIT);
throw new IOException(message);
}
byte[] data = new byte[length];
dataInputStream.readFully(data);
LOG.trace("convert message to {}", type);
return objectMapper.readValue(data, type);
}
}

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

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

View File

@@ -21,14 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.HgHookBranchProvider;
import sonia.scm.repository.api.HgHookMessageProvider;
import sonia.scm.repository.api.HgHookTagProvider;
@@ -37,7 +37,6 @@ import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookMessageProvider;
import sonia.scm.repository.api.HookTagProvider;
import java.io.File;
import java.util.EnumSet;
import java.util.Set;
@@ -45,55 +44,40 @@ import java.util.Set;
/**
* Mercurial implementation of {@link HookContextProvider}.
*
*
* @author Sebastian Sdorra
*/
public class HgHookContextProvider extends HookContextProvider
{
public class HgHookContextProvider extends HookContextProvider {
private static final Set<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;
}
@@ -102,14 +86,11 @@ public class HgHookContextProvider extends HookContextProvider
{
return hookChangesetProvider;
}
public HgHookMessageProvider getHgMessageProvider()
{
if (hgMessageProvider == null)
{
public HgHookMessageProvider getHgMessageProvider() {
if (hgMessageProvider == null) {
hgMessageProvider = new HgHookMessageProvider();
}
return hgMessageProvider;
}
@@ -119,21 +100,9 @@ public class HgHookContextProvider extends HookContextProvider
return SUPPORTED_FEATURES;
}
//~--- methods --------------------------------------------------------------
@Override
protected HookMessageProvider createMessageProvider()
{
return getHgMessageProvider();
}
//~--- fields ---------------------------------------------------------------
private final HgHookChangesetProvider hookChangesetProvider;
private HgHookMessageProvider hgMessageProvider;
private HgHookBranchProvider hookBranchProvider;
private HgHookTagProvider hookTagProvider;
}

View File

@@ -31,6 +31,8 @@ import com.aragost.javahg.commands.ExecutionException;
import com.aragost.javahg.commands.PullCommand;
import com.aragost.javahg.commands.RemoveCommand;
import com.aragost.javahg.commands.StatusCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NoChangesMadeException;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.work.WorkingCopy;
@@ -41,11 +43,13 @@ import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
@SuppressWarnings("java:S3252") // it is ok for javahg classes to access static method of subtype
public class HgModifyCommand implements ModifyCommand {
private static final Logger LOG = LoggerFactory.getLogger(HgModifyCommand.class);
static final Pattern HG_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\](?: Error:)? (.*)");
private HgCommandContext context;
private final HgCommandContext context;
private final HgWorkingCopyFactory workingCopyFactory;
public HgModifyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) {
@@ -55,7 +59,6 @@ public class HgModifyCommand implements ModifyCommand {
@Override
public String execute(ModifyCommandRequest request) {
try (WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy = workingCopyFactory.createWorkingCopy(context, request.getBranch())) {
Repository workingRepository = workingCopy.getWorkingRepository();
request.getRequests().forEach(
@@ -100,12 +103,21 @@ public class HgModifyCommand implements ModifyCommand {
}
}
);
if (StatusCommand.on(workingRepository).lines().isEmpty()) {
throw new NoChangesMadeException(context.getScmRepository());
}
CommitCommand.on(workingRepository).user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail())).message(request.getCommitMessage()).execute();
LOG.trace("commit changes in working copy");
CommitCommand.on(workingRepository)
.user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail()))
.message(request.getCommitMessage()).execute();
List<Changeset> execute = pullModifyChangesToCentralRepository(request, workingCopy);
return execute.get(0).getNode();
String node = execute.get(0).getNode();
LOG.debug("successfully pulled changes from working copy, new node {}", node);
return node;
} catch (ExecutionException e) {
throwInternalRepositoryException("could not execute command on repository", e);
return null;
@@ -113,6 +125,7 @@ public class HgModifyCommand implements ModifyCommand {
}
private List<Changeset> pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy) {
LOG.trace("pull changes from working copy");
try {
com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
workingCopyFactory.configure(pullCommand);

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

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

View File

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

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
import com.google.common.base.Stopwatch;
@@ -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

@@ -21,189 +21,48 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.aragost.javahg.Repository;
import com.aragost.javahg.RepositoryConfiguration;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgPythonScript;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.spi.javahg.HgFileviewExtension;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
/**
*
* @author Sebastian Sdorra
*/
public final class HgUtil
{
public final class HgUtil {
/** Field description */
public static final String REVISION_TIP = "tip";
/** Field description */
private static final String USERAGENT_HG = "mercurial/";
/**
* the logger for HgUtil
*/
private static final Logger logger = LoggerFactory.getLogger(HgUtil.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private HgUtil() {}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param handler
* @param hookManager
* @param directory
* @param encoding
* @param pending
*
* @return
*/
public static Repository open(HgRepositoryHandler handler,
HgHookManager hookManager, File directory, String encoding, boolean pending)
{
return open(
handler,
directory,
encoding,
pending,
environment -> HgEnvironment.prepareEnvironment(environment, handler, hookManager)
);
}
public static Repository open(HgRepositoryHandler handler,
File directory, String encoding, boolean pending,
Consumer<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);
}
}