merge with branch 1.x

This commit is contained in:
Sebastian Sdorra
2017-06-25 19:01:33 +02:00
81 changed files with 4617 additions and 1319 deletions

View File

@@ -81,7 +81,10 @@ public class GitRepositoryHandler
/** Field description */
public static final String TYPE_NAME = "git";
public static final String DOT_GIT = ".git";
private static final Logger logger = LoggerFactory.getLogger(GitRepositoryHandler.class);
/** Field description */

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
import sonia.scm.plugin.Extension;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
/**
* Matches git repositories with ".git" and without ".git".
*
* @author Sebastian Sdorra
* @since 1.54
*/
@Extension
public class GitRepositoryPathMatcher implements RepositoryPathMatcher {
@Override
public boolean isPathMatching(Repository repository, String path) {
String repositoryName = repository.getName();
if (path.startsWith(repositoryName)) {
String pathPart = path.substring(repositoryName.length());
// git repository may also be named <<repo-name>>.git by convention
if (pathPart.startsWith(GitRepositoryHandler.DOT_GIT)) {
// if this is the case, just also cut it away
pathPart = pathPart.substring(GitRepositoryHandler.DOT_GIT.length());
}
return Util.isEmpty(pathPart) || pathPart.startsWith(HttpUtil.SEPARATOR_PATH);
}
return false;
}
@Override
public String getType() {
return GitRepositoryHandler.TYPE_NAME;
}
}

View File

@@ -70,6 +70,7 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import sonia.scm.web.GitUserAgentProvider;
/**
*
@@ -77,6 +78,8 @@ import javax.servlet.http.HttpServletRequest;
*/
public final class GitUtil
{
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
/** Field description */
public static final String REF_HEAD = "HEAD";
@@ -696,7 +699,7 @@ public final class GitUtil
*/
public static boolean isGitClient(HttpServletRequest request)
{
return HttpUtil.userAgentStartsWith(request, USERAGENT_GIT);
return GIT_USER_AGENT_PROVIDER.parseUserAgent(request.getHeader(HttpUtil.HEADER_USERAGENT)) != null;
}
/**

View File

@@ -35,6 +35,7 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -57,7 +58,8 @@ import sonia.scm.filter.Filters;
import sonia.scm.filter.WebElement;
/**
*
* GitPermissionFilter decides if a git request requires write or read privileges.
*
* @author Sebastian Sdorra
*/
@Priority(Filters.PRIORITY_AUTHORIZATION)
@@ -65,79 +67,60 @@ import sonia.scm.filter.WebElement;
public class GitPermissionFilter extends ProviderPermissionFilter
{
/** Field description */
public static final String PARAMETER_SERVICE = "service";
private static final String PARAMETER_SERVICE = "service";
/** Field description */
public static final String PARAMETER_VALUE_RECEIVE = "git-receive-pack";
private static final String PARAMETER_VALUE_RECEIVE = "git-receive-pack";
/** Field description */
public static final String URI_RECEIVE_PACK = "git-receive-pack";
private static final String URI_RECEIVE_PACK = "git-receive-pack";
/** Field description */
public static final String URI_REF_INFO = "/info/refs";
private static final String URI_REF_INFO = "/info/refs";
private static final String METHOD_LFS_UPLOAD = "PUT";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
* Constructs a new instance of the GitPermissionFilter.
*
* @param configuration
* @param repositoryProvider
* @param configuration scm main configuration
* @param repositoryProvider repository provider
*/
@Inject
public GitPermissionFilter(ScmConfiguration configuration,
RepositoryProvider repositoryProvider)
{
public GitPermissionFilter(ScmConfiguration configuration, RepositoryProvider repositoryProvider) {
super(configuration, repositoryProvider);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param response
*
* @throws IOException
*/
@Override
protected void sendNotEnoughPrivilegesError(HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
if (GitUtil.isGitClient(request))
{
protected void sendNotEnoughPrivilegesError(HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (GitUtil.isGitClient(request)) {
GitSmartHttpTools.sendError(request, response,
HttpServletResponse.SC_FORBIDDEN,
ClientMessages.get(request).notEnoughPrivileges());
}
else
{
} else {
super.sendNotEnoughPrivilegesError(request, response);
}
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param request
*
* @return
*/
@Override
protected boolean isWriteRequest(HttpServletRequest request)
{
String uri = request.getRequestURI();
return uri.endsWith(URI_RECEIVE_PACK)
|| (uri.endsWith(URI_REF_INFO)
&& PARAMETER_VALUE_RECEIVE.equals(
request.getParameter(PARAMETER_SERVICE)));
protected boolean isWriteRequest(HttpServletRequest request) {
return isReceivePackRequest(request) ||
isReceiveServiceRequest(request) ||
isLfsFileUpload(request);
}
private boolean isReceivePackRequest(HttpServletRequest request) {
return request.getRequestURI().endsWith(URI_RECEIVE_PACK);
}
private boolean isReceiveServiceRequest(HttpServletRequest request) {
return request.getRequestURI().endsWith(URI_REF_INFO)
&& PARAMETER_VALUE_RECEIVE.equals(request.getParameter(PARAMETER_SERVICE));
}
@VisibleForTesting
private static boolean isLfsFileUpload(HttpServletRequest request) {
return METHOD_LFS_UPLOAD.equalsIgnoreCase(request.getMethod());
}
}

View File

@@ -35,6 +35,7 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -63,13 +64,11 @@ import javax.servlet.http.HttpServletRequest;
*
* @author Sebastian Sdorra
*/
public class GitRepositoryResolver
implements RepositoryResolver<HttpServletRequest>
public class GitRepositoryResolver implements RepositoryResolver<HttpServletRequest>
{
/** the logger for GitRepositoryResolver */
private static final Logger logger =
LoggerFactory.getLogger(GitRepositoryResolver.class);
private static final Logger logger = LoggerFactory.getLogger(GitRepositoryResolver.class);
//~--- constructors ---------------------------------------------------------
@@ -114,20 +113,14 @@ public class GitRepositoryResolver
if (config.isValid())
{
File gitdir = new File(config.getRepositoryDirectory(), repositoryName);
if (logger.isDebugEnabled())
{
logger.debug("try to open git repository at {}", gitdir);
}
if (!gitdir.exists())
{
File gitdir = findRepository(config.getRepositoryDirectory(), repositoryName);
if (gitdir == null) {
throw new RepositoryNotFoundException(repositoryName);
}
logger.debug("try to open git repository at {}", gitdir);
repository = RepositoryCache.open(FileKey.lenient(gitdir, FS.DETECTED),
true);
repository = RepositoryCache.open(FileKey.lenient(gitdir, FS.DETECTED), true);
}
else
{
@@ -139,17 +132,39 @@ public class GitRepositoryResolver
throw new ServiceNotEnabledException();
}
}
catch (RuntimeException e)
{
throw new RepositoryNotFoundException(repositoryName, e);
}
catch (IOException e)
catch (RuntimeException | IOException e)
{
throw new RepositoryNotFoundException(repositoryName, e);
}
return repository;
}
@VisibleForTesting
File findRepository(File parentDirectory, String repositoryName) {
File repositoryDirectory = new File(parentDirectory, repositoryName);
if (repositoryDirectory.exists()) {
return repositoryDirectory;
}
if (endsWithDotGit(repositoryName)) {
String repositoryNameWithoutDotGit = repositoryNameWithoutDotGit(repositoryName);
repositoryDirectory = new File(parentDirectory, repositoryNameWithoutDotGit);
if (repositoryDirectory.exists()) {
return repositoryDirectory;
}
}
return null;
}
private boolean endsWithDotGit(String repositoryName) {
return repositoryName.endsWith(GitRepositoryHandler.DOT_GIT);
}
private String repositoryNameWithoutDotGit(String repositoryName) {
return repositoryName.substring(0, repositoryName.length() - GitRepositoryHandler.DOT_GIT.length());
}
//~--- fields ---------------------------------------------------------------

View File

@@ -41,6 +41,8 @@ import org.eclipse.jgit.transport.ScmTransportProtocol;
import sonia.scm.plugin.Extension;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
*
* @author Sebastian Sdorra
@@ -49,8 +51,11 @@ import sonia.scm.plugin.Extension;
public class GitServletModule extends ServletModule
{
public static final String GIT_PATH = "/git";
/** Field description */
public static final String PATTERN_GIT = "/git/*";
public static final String PATTERN_GIT = GIT_PATH + "/*";
//~--- methods --------------------------------------------------------------
@@ -65,6 +70,8 @@ public class GitServletModule extends ServletModule
bind(GitRepositoryResolver.class);
bind(GitReceivePackFactory.class);
bind(ScmTransportProtocol.class);
bind(LfsBlobStoreFactory.class);
// serlvelts and filters
serve(PATTERN_GIT).with(ScmGitServlet.class);

View File

@@ -35,63 +35,89 @@ package sonia.scm.web;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import java.util.Locale;
import sonia.scm.plugin.Extension;
/**
*
* UserAgent provider for git related clients.
* @author Sebastian Sdorra <sebastian.sdorra@gmail.com>
* @since 1.45
*/
@Extension
public class GitUserAgentProvider implements UserAgentProvider
{
public class GitUserAgentProvider implements UserAgentProvider {
private static final String PREFIX_JGIT = "jgit/";
/** Field description */
@VisibleForTesting
static final UserAgent GIT = UserAgent.builder("Git").browser(
false).basicAuthenticationCharset(
Charsets.UTF_8).build();
/** Field description */
static final UserAgent JGIT = UserAgent.builder("JGit")
.browser(false)
.basicAuthenticationCharset(Charsets.UTF_8)
.build();
private static final String PREFIX_REGULAR = "git/";
@VisibleForTesting
static final UserAgent MSYSGIT = UserAgent.builder("msysGit").browser(
false).basicAuthenticationCharset(
Charsets.UTF_8).build();
static final UserAgent GIT = UserAgent.builder("Git")
.browser(false)
.basicAuthenticationCharset(Charsets.UTF_8)
.build();
private static final String PREFIX_LFS = "git-lfs/";
@VisibleForTesting
static final UserAgent GIT_LFS = UserAgent.builder("Git Lfs")
.browser(false)
.basicAuthenticationCharset(Charsets.UTF_8)
.build();
private static final String SUFFIX_MSYSGIT = "msysgit";
@VisibleForTesting
static final UserAgent MSYSGIT = UserAgent.builder("msysGit")
.browser(false)
.basicAuthenticationCharset(Charsets.UTF_8)
.build();
/** Field description */
private static final String PREFIX = "git/";
/** Field description */
private static final String SUFFIX = "msysgit";
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param userAgentString
*
* @return
*/
@Override
public UserAgent parseUserAgent(String userAgentString)
{
UserAgent ua = null;
if (userAgentString.startsWith(PREFIX))
{
if (userAgentString.contains(SUFFIX))
{
ua = MSYSGIT;
}
else
{
ua = GIT;
}
public UserAgent parseUserAgent(String userAgentString) {
String lowerUserAgent = toLower(userAgentString);
if (isJGit(lowerUserAgent)) {
return JGIT;
} else if (isMsysGit(lowerUserAgent)) {
return MSYSGIT;
} else if (isGitLFS(lowerUserAgent)) {
return GIT_LFS;
} else if (isGit(lowerUserAgent)) {
return GIT;
} else {
return null;
}
return ua;
}
private String toLower(String value) {
return Strings.nullToEmpty(value).toLowerCase(Locale.ENGLISH);
}
private boolean isJGit(String userAgent) {
return userAgent.startsWith(PREFIX_JGIT);
}
private boolean isMsysGit(String userAgent) {
return userAgent.startsWith(PREFIX_REGULAR) && userAgent.contains(SUFFIX_MSYSGIT);
}
private boolean isGitLFS(String userAgent) {
return userAgent.startsWith(PREFIX_LFS);
}
private boolean isGit(String userAgent) {
return userAgent.startsWith(PREFIX_REGULAR);
}
}

View File

@@ -35,23 +35,32 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.http.server.GitServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.slf4j.LoggerFactory.getLogger;
import org.eclipse.jgit.lfs.lib.Constants;
import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryProvider;
import sonia.scm.repository.RepositoryRequestListenerUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.lfs.servlet.LfsServletFactory;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sonia.scm.repository.RepositoryException;
@@ -65,15 +74,15 @@ public class ScmGitServlet extends GitServlet
{
/** Field description */
public static final String REGEX_GITHTTPBACKEND =
"(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$";
public static final Pattern REGEX_GITHTTPBACKEND = Pattern.compile(
"(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$"
);
/** Field description */
private static final long serialVersionUID = -7712897339207470674L;
/** the logger for ScmGitServlet */
private static final Logger logger =
LoggerFactory.getLogger(ScmGitServlet.class);
private static final Logger logger = getLogger(ScmGitServlet.class);
//~--- constructors ---------------------------------------------------------
@@ -87,17 +96,21 @@ public class ScmGitServlet extends GitServlet
* @param repositoryViewer
* @param repositoryProvider
* @param repositoryRequestListenerUtil
* @param lfsServletFactory
*/
@Inject
public ScmGitServlet(GitRepositoryResolver repositoryResolver,
GitReceivePackFactory receivePackFactory,
GitRepositoryViewer repositoryViewer,
RepositoryProvider repositoryProvider,
RepositoryRequestListenerUtil repositoryRequestListenerUtil)
GitReceivePackFactory receivePackFactory,
GitRepositoryViewer repositoryViewer,
RepositoryProvider repositoryProvider,
RepositoryRequestListenerUtil repositoryRequestListenerUtil,
LfsServletFactory lfsServletFactory)
{
this.repositoryProvider = repositoryProvider;
this.repositoryViewer = repositoryViewer;
this.repositoryRequestListenerUtil = repositoryRequestListenerUtil;
this.lfsServletFactory = lfsServletFactory;
setRepositoryResolver(repositoryResolver);
setReceivePackFactory(receivePackFactory);
}
@@ -118,74 +131,165 @@ public class ScmGitServlet extends GitServlet
protected void service(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
String uri = HttpUtil.getStrippedURI(request);
if (uri.matches(REGEX_GITHTTPBACKEND))
{
sonia.scm.repository.Repository repository = repositoryProvider.get();
if (repository != null)
{
if (repositoryRequestListenerUtil.callListeners(request, response,
repository))
{
super.service(request, response);
}
else if (logger.isDebugEnabled())
{
logger.debug("request aborted by repository request listener");
}
}
else
{
super.service(request, response);
}
}
else
{
printGitInformation(request, response);
{
Repository repository = repositoryProvider.get();
if (repository != null) {
handleRequest(request, response, repository);
} else {
// logger
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
/**
* Decides the type request being currently made and delegates it accordingly.
* <ul>
* <li>Batch API:</li>
* <ul>
* <li>used to provide the client with information on how handle the large files of a repository.</li>
* <li>response contains the information where to perform the actual upload and download of the large objects.</li>
* </ul>
* <li>Transfer API:</li>
* <ul>
* <li>receives and provides the actual large objects (resolves the pointer placed in the file of the working copy).</li>
* <li>invoked only after the Batch API has been questioned about what to do with the large files</li>
* </ul>
* <li>Regular Git Http API:</li>
* <ul>
* <li>regular git http wire protocol, use by normal git clients.</li>
* </ul>
* <li>Browser Overview:<li>
* <ul>
* <li>short repository overview for browser clients.</li>
* </ul>
* </li>
* </ul>
*/
private void handleRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
logger.trace("handle git repository at {}", repository.getName());
if (isLfsBatchApiRequest(request, repository.getName())) {
HttpServlet servlet = lfsServletFactory.createProtocolServletFor(repository, request);
logger.trace("handle lfs batch api request");
handleGitLfsRequest(servlet, request, response, repository);
} else if (isLfsFileTransferRequest(request, repository.getName())) {
HttpServlet servlet = lfsServletFactory.createFileLfsServletFor(repository, request);
logger.trace("handle lfs file transfer request");
handleGitLfsRequest(servlet, request, response, repository);
} else if (isRegularGitAPIRequest(request)) {
logger.trace("handle regular git request");
// continue with the regular git Backend
handleRegularGitRequest(request, response, repository);
} else {
logger.trace("handle browser request");
handleBrowserRequest(request, response, repository);
}
}
private boolean isRegularGitAPIRequest(HttpServletRequest request) {
return REGEX_GITHTTPBACKEND.matcher(HttpUtil.getStrippedURI(request)).matches();
}
private void handleGitLfsRequest(HttpServlet servlet, HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
if (repositoryRequestListenerUtil.callListeners(request, response, repository)) {
servlet.service(request, response);
} else if (logger.isDebugEnabled()) {
logger.debug("request aborted by repository request listener");
}
}
private void handleRegularGitRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
if (repositoryRequestListenerUtil.callListeners(request, response, repository)) {
super.service(request, response);
} else if (logger.isDebugEnabled()) {
logger.debug("request aborted by repository request listener");
}
}
/**
* Method description
*
*
*
* This method renders basic information about the repository into the response. The result is meant to be viewed by
* browser.
* @param request
* @param response
*
* @throws IOException
* @throws ServletException
*/
private void printGitInformation(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
sonia.scm.repository.Repository scmRepository = repositoryProvider.get();
if (scmRepository != null)
{
try
{
repositoryViewer.handleRequest(request, response, scmRepository);
}
catch (RepositoryException ex)
{
throw new ServletException("could not create repository view", ex);
}
catch (IOException ex)
{
throw new ServletException("could not create repository view", ex);
}
}
else
{
response.sendError(HttpServletResponse.SC_NOT_FOUND);
private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
try {
repositoryViewer.handleRequest(request, response, repository);
} catch (RepositoryException | IOException ex) {
throw new ServletException("could not create repository view", ex);
}
}
/**
* Decides whether or not a request is for the LFS Batch API,
* <p>
* - PUT or GET
* - exactly for this repository
* - Content Type is {@link Constants#HDR_APPLICATION_OCTET_STREAM}.
*
* @return Returns {@code false} if either of the conditions does not match. Returns true if all match.
*/
private static boolean isLfsFileTransferRequest(HttpServletRequest request, String repository) {
String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), GitServletModule.GIT_PATH, repository);
boolean pathMatches = request.getRequestURI().matches(regex);
boolean methodMatches = request.getMethod().equals("PUT") || request.getMethod().equals("GET");
return pathMatches && methodMatches;
}
/**
* Decides whether or not a request is for the LFS Batch API,
* <p>
* - POST
* - exactly for this repository
* - Content Type is {@link Constants#CONTENT_TYPE_GIT_LFS_JSON}.
*
* @return Returns {@code false} if either of the conditions does not match. Returns true if all match.
*/
private static boolean isLfsBatchApiRequest(HttpServletRequest request, String repository) {
String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), GitServletModule.GIT_PATH, repository);
boolean pathMatches = request.getRequestURI().matches(regex);
boolean methodMatches = "POST".equals(request.getMethod());
boolean headerContentTypeMatches = isLfsContentHeaderField(request.getContentType(), CONTENT_TYPE_GIT_LFS_JSON);
boolean headerAcceptMatches = isLfsContentHeaderField(request.getHeader("Accept"), CONTENT_TYPE_GIT_LFS_JSON);
return pathMatches && methodMatches && headerContentTypeMatches && headerAcceptMatches;
}
/**
* Checks whether request is of the specific content type.
*
* @param request The HTTP request header value to be examined.
* @param expectedContentType The expected content type.
* @return Returns {@code true} if the request has the expected content type. Return {@code false} otherwise.
*/
@VisibleForTesting
static boolean isLfsContentHeaderField(String request, String expectedContentType) {
if (request == null || request.isEmpty()) {
return false;
}
String[] parts = request.split(" ");
for (String part : parts) {
if (part.startsWith(expectedContentType)) {
return true;
}
}
return false;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -194,6 +298,11 @@ public class ScmGitServlet extends GitServlet
/** Field description */
private final RepositoryRequestListenerUtil repositoryRequestListenerUtil;
/** Field description */
/**
* Field description
*/
private final GitRepositoryViewer repositoryViewer;
private final LfsServletFactory lfsServletFactory;
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import sonia.scm.repository.Repository;
import sonia.scm.store.BlobStore;
import sonia.scm.store.BlobStoreFactory;
/**
* Creates {@link BlobStore} objects to store lfs objects.
*
* @author Sebastian Sdorra
* @since 1.54
*/
@Singleton
public class LfsBlobStoreFactory {
private static final String GIT_LFS_REPOSITORY_POSTFIX = "-git-lfs";
private final BlobStoreFactory blobStoreFactory;
/**
* Create a new instance.
*
* @param blobStoreFactory blob store factory
*/
@Inject
public LfsBlobStoreFactory(BlobStoreFactory blobStoreFactory) {
this.blobStoreFactory = blobStoreFactory;
}
/**
* Provides a {@link BlobStore} corresponding to the SCM Repository.
* <p>
* git-lfs repositories should generally carry the same name as their regular SCM repository counterparts. However,
* we have decided to store them under their IDs instead of their names, since the names might change and provide
* other drawbacks, as well.
* <p>
* These repositories will have {@linkplain #GIT_LFS_REPOSITORY_POSTFIX} appended to their IDs.
*
* @param repository The SCM Repository to provide a LFS {@link BlobStore} for.
*
* @return blob store for the corresponding scm repository
*/
public BlobStore getLfsBlobStore(Repository repository) {
return blobStoreFactory.getBlobStore(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX);
}
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.EagerSingleton;
import sonia.scm.HandlerEventType;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
/**
* Listener which removes all lfs objects from a blob store, whenever its corresponding git repository gets deleted.
*
* @author Sebastian Sdorra
* @since 1.54
*/
@Extension
@EagerSingleton
public class LfsStoreRemoveListener {
private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreFactory.class);
private final LfsBlobStoreFactory lfsBlobStoreFactory;
@Inject
public LfsStoreRemoveListener(LfsBlobStoreFactory lfsBlobStoreFactory) {
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
}
/**
* Remove all object from the blob store, if the event is an delete event and the repository is a git repository.
*
* @param event repository event
*/
@Subscribe
public void handleRepositoryEvent(RepositoryEvent event) {
if ( isDeleteEvent(event) && isGitRepositoryEvent(event) ) {
removeLfsStore(event.getItem());
}
}
private boolean isDeleteEvent(RepositoryEvent event) {
return HandlerEventType.DELETE == event.getEventType();
}
private boolean isGitRepositoryEvent(RepositoryEvent event) {
return event.getItem() != null
&& event.getItem().getType().equals(GitRepositoryHandler.TYPE_NAME);
}
private void removeLfsStore(Repository repository) {
LOG.debug("remove all blobs from store, because corresponding git repository {} was removed", repository.getName());
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
for ( Blob blob : blobStore.getAll() ) {
LOG.trace("remove blob {}, because repository {} was removed", blob.getId(), repository.getName());
blobStore.remove(blob);
}
}
}

View File

@@ -0,0 +1,90 @@
package sonia.scm.web.lfs;
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
import org.eclipse.jgit.lfs.server.LargeFileRepository;
import org.eclipse.jgit.lfs.server.Response;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import java.io.IOException;
/**
* This LargeFileRepository is used for jGit-Servlet implementation. Under the jgit LFS Servlet hood, the
* SCM-Repository API is used to implement the Repository.
*
* @since 1.54
* Created by omilke on 03.05.2017.
*/
public class ScmBlobLfsRepository implements LargeFileRepository {
private final BlobStore blobStore;
/**
* This URI is used to determine the actual URI for Upload / Download. Must be full URI (or rewritable by reverse
* proxy).
*/
private final String baseUri;
/**
* Creates a {@link ScmBlobLfsRepository} for the provided repository.
*
* @param blobStore The SCM Blobstore used for this @{@link LargeFileRepository}.
* @param baseUri This URI is used to determine the actual URI for Upload / Download. Must be full URI (or
* rewritable by reverse proxy).
*/
public ScmBlobLfsRepository(BlobStore blobStore, String baseUri) {
this.blobStore = blobStore;
this.baseUri = baseUri;
}
@Override
public Response.Action getDownloadAction(AnyLongObjectId id) {
return getAction(id);
}
@Override
public Response.Action getUploadAction(AnyLongObjectId id, long size) {
return getAction(id);
}
@Override
public Response.Action getVerifyAction(AnyLongObjectId id) {
//validation is optional. We do not support it.
return null;
}
@Override
public long getSize(AnyLongObjectId id) throws IOException {
//this needs to be size of what is will be written into the response of the download. Clients are likely to
// verify it.
Blob blob = this.blobStore.get(id.getName());
if (blob == null) {
return -1;
} else {
return blob.getSize();
}
}
/**
* Constructs the Download / Upload actions to be supplied to the client.
*/
private Response.Action getAction(AnyLongObjectId id) {
//LFS protocol has to provide the information on where to put or get the actual content, i. e.
//the actual URI for up- and download.
Response.Action a = new Response.Action();
a.href = baseUri + id.getName();
return a;
}
}

View File

@@ -0,0 +1,76 @@
package sonia.scm.web.lfs.servlet;
import com.google.common.annotations.VisibleForTesting;
import org.eclipse.jgit.lfs.server.LargeFileRepository;
import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
import org.eclipse.jgit.lfs.server.fs.FileLfsServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Repository;
import sonia.scm.store.BlobStore;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.lfs.ScmBlobLfsRepository;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
* This factory class is a helper class to provide the {@link LfsProtocolServlet} and the {@link FileLfsServlet}
* belonging to a SCM Repository.
*
* @since 1.54
* Created by omilke on 11.05.2017.
*/
@Singleton
public class LfsServletFactory {
private static final Logger logger = LoggerFactory.getLogger(LfsServletFactory.class);
private final LfsBlobStoreFactory lfsBlobStoreFactory;
@Inject
public LfsServletFactory(LfsBlobStoreFactory lfsBlobStoreFactory) {
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
}
/**
* Builds the {@link LfsProtocolServlet} (jgit API) for a SCM Repository.
*
* @param repository The SCM Repository to build the servlet for.
* @param request The {@link HttpServletRequest} the used to access the SCM Repository.
* @return The {@link LfsProtocolServlet} to provide the LFS Batch API for a SCM Repository.
*/
public LfsProtocolServlet createProtocolServletFor(Repository repository, HttpServletRequest request) {
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
String baseUri = buildBaseUri(repository, request);
LargeFileRepository largeFileRepository = new ScmBlobLfsRepository(blobStore, baseUri);
return new ScmLfsProtocolServlet(largeFileRepository);
}
/**
* Builds the {@link FileLfsServlet} (jgit API) for a SCM Repository.
*
* @param repository The SCM Repository to build the servlet for.
* @param request The {@link HttpServletRequest} the used to access the SCM Repository.
* @return The {@link FileLfsServlet} to provide the LFS Upload / Download API for a SCM Repository.
*/
public HttpServlet createFileLfsServletFor(Repository repository, HttpServletRequest request) {
return new ScmFileTransferServlet(lfsBlobStoreFactory.getLfsBlobStore(repository));
}
/**
* Build the complete URI, under which the File Transfer API for this repository will be will be reachable.
*
* @param repository The repository to build the File Transfer URI for.
* @param request The request to construct the complete URI from.
*/
@VisibleForTesting
static String buildBaseUri(Repository repository, HttpServletRequest request) {
return String.format("%s/git/%s.git/info/lfs/objects/", HttpUtil.getCompleteUrl(request), repository.getName());
}
}

View File

@@ -0,0 +1,280 @@
package sonia.scm.web.lfs.servlet;
import com.google.common.annotations.VisibleForTesting;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.HttpStatus;
import org.eclipse.jgit.lfs.errors.CorruptLongObjectException;
import org.eclipse.jgit.lfs.errors.InvalidLongObjectIdException;
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
import org.eclipse.jgit.lfs.lib.Constants;
import org.eclipse.jgit.lfs.lib.LongObjectId;
import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
import org.eclipse.jgit.lfs.server.fs.FileLfsServlet;
import org.eclipse.jgit.lfs.server.internal.LfsServerText;
import org.eclipse.jgit.util.HttpSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import sonia.scm.util.IOUtil;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.MessageFormat;
/**
* This Servlet provides the upload and download of files via git-lfs.
* <p>
* This implementation is based on {@link FileLfsServlet} but adjusted to work with
* servlet-2.5 instead of servlet-3.1.
* <p>
*
* @see FileLfsServlet
* @since 1.54
* Created by omilke on 15.05.2017.
*/
public class ScmFileTransferServlet extends HttpServlet {
private static final Logger logger = LoggerFactory.getLogger(ScmFileTransferServlet.class);
private static final long serialVersionUID = 1L;
/**
* Gson is used because the implementation was based on the jgit implementation. However the {@link LfsProtocolServlet} (which we do use in
* {@link ScmLfsProtocolServlet}) also uses Gson, which currently ties us to Gson anyway.
*/
private static Gson gson = createGson();
private final BlobStore blobStore;
public ScmFileTransferServlet(BlobStore store) {
this.blobStore = store;
}
/**
* Extracts the part after the last slash from path.
*
* @return Returns {@code null} if the part after the last slash is itself {@code null} or if its length is not 64.
*/
@VisibleForTesting
static String objectIdFromPath(String info) {
int lastSlash = info.lastIndexOf('/');
String potentialObjectId = info.substring(lastSlash + 1);
if (potentialObjectId.length() != 64) {
return null;
} else {
return potentialObjectId;
}
}
/**
* Logs the message and provides it to the client.
*
* @param response The response
* @param status The HTTP Status Code to be provided to the client.
* @param message the message to used for server-side logging. It is also provided to the client.
*/
private static void sendErrorAndLog(HttpServletResponse response, int status, String message) throws IOException {
logger.warn("Error occurred during git-lfs file transfer: {}", message);
sendError(response, status, message);
}
/**
* Logs the exception and provides only the message of the exception to the client.
*
* @param response The response
* @param status The HTTP Status Code to be provided to the client.
* @param exception An exception to used for server-side logging.
*/
private static void sendErrorAndLog(HttpServletResponse response, int status, Exception exception) throws IOException {
logger.warn("Error occurred during git-lfs file transfer.", exception);
String message = exception.getMessage();
sendError(response, status, message);
}
private static void sendError(HttpServletResponse response, int status, String message) throws IOException {
try (PrintWriter writer = response.getWriter()) {
gson.toJson(new Error(message), writer);
response.setStatus(status);
writer.flush();
}
response.flushBuffer();
}
private static Gson createGson() {
GsonBuilder gb = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting().disableHtmlEscaping();
return gb.create();
}
/**
* Provides a blob to download.
* <p>
* Actual implementation is based on <code>org.eclipse.jgit.lfs.server.fs.ObjectDownloadListener</code> and adjusted
* to non-async as we're currently on servlet-2.5.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AnyLongObjectId objectId = getObjectToTransfer(request, response);
if (objectId == null) {
logInvalidObjectId(request.getRequestURI());
} else {
final String objectIdName = objectId.getName();
logger.trace("---- providing download for LFS-Oid: {}", objectIdName);
Blob savedBlob = blobStore.get(objectIdName);
if (isBlobPresent(savedBlob)) {
logger.trace("----- Object {}: providing {} bytes", objectIdName, savedBlob.getSize());
writeBlobIntoResponse(savedBlob, response);
} else {
sendErrorAndLog(response, HttpStatus.SC_NOT_FOUND, MessageFormat.format(LfsServerText.get().objectNotFound, objectIdName));
}
}
}
/**
* Receives a blob from an upload.
* <p>
* Actual implementation is based on <code>org.eclipse.jgit.lfs.server.fs.ObjectUploadListener</code> and adjusted
* to non-async as we're currently on servlet-2.5.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AnyLongObjectId objectId = getObjectToTransfer(request, response);
if (objectId == null) {
logInvalidObjectId(request.getRequestURI());
} else {
logger.trace("---- receiving upload for LFS-Oid: {}", objectId.getName());
readBlobFromResponse(request, response, objectId);
}
}
/**
* Extracts the {@link LongObjectId} from the request. Finishes the request, in case the {@link LongObjectId} cannot
* be extracted with an appropriate error.
*
* @throws IOException Thrown if the response could not be completed in an error case.
*/
private AnyLongObjectId getObjectToTransfer(HttpServletRequest request, HttpServletResponse response) throws IOException {
String path = request.getPathInfo();
String objectIdFromPath = objectIdFromPath(path);
if (objectIdFromPath == null) {
//ObjectId is not retrievable from URL
sendErrorAndLog(response, HttpStatus.SC_UNPROCESSABLE_ENTITY, MessageFormat.format(LfsServerText.get().invalidPathInfo, path));
return null;
} else {
try {
return LongObjectId.fromString(objectIdFromPath);
} catch (InvalidLongObjectIdException e) {
sendErrorAndLog(response, HttpStatus.SC_UNPROCESSABLE_ENTITY, e);
return null;
}
}
}
private void logInvalidObjectId(String requestURI) {
logger.warn("---- could not extract Oid from Request. Path seems to be invalid: {}", requestURI);
}
private boolean isBlobPresent(Blob savedBlob) {
return savedBlob != null && savedBlob.getSize() >= 0;
}
private void writeBlobIntoResponse(Blob savedBlob, HttpServletResponse response) throws IOException {
try (ServletOutputStream responseOutputStream = response.getOutputStream();
InputStream savedBlobInputStream = savedBlob.getInputStream()) {
response.addHeader(HttpSupport.HDR_CONTENT_LENGTH, String.valueOf(savedBlob.getSize()));
response.setContentType(Constants.HDR_APPLICATION_OCTET_STREAM);
IOUtil.copy(savedBlobInputStream, responseOutputStream);
} catch (IOException ex) {
sendErrorAndLog(response, HttpStatus.SC_INTERNAL_SERVER_ERROR, ex);
}
}
private void readBlobFromResponse(HttpServletRequest request, HttpServletResponse response, AnyLongObjectId objectId) throws IOException {
Blob blob = blobStore.create(objectId.getName());
try (OutputStream blobOutputStream = blob.getOutputStream();
ServletInputStream requestInputStream = request.getInputStream()) {
IOUtil.copy(requestInputStream, blobOutputStream);
blob.commit();
response.setContentType(Constants.CONTENT_TYPE_GIT_LFS_JSON);
response.setStatus(HttpServletResponse.SC_OK);
} catch (CorruptLongObjectException ex) {
sendErrorAndLog(response, HttpStatus.SC_BAD_REQUEST, ex);
}
}
/**
* Used for providing an error message.
*/
private static class Error {
String message;
Error(String m) {
this.message = m;
}
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.web.lfs.servlet;
import org.eclipse.jgit.lfs.errors.LfsException;
import org.eclipse.jgit.lfs.server.LargeFileRepository;
import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
/**
* Provides an implementation for the git-lfs Batch API.
*
* @since 1.54
* Created by omilke on 11.05.2017.
*/
public class ScmLfsProtocolServlet extends LfsProtocolServlet {
private final LargeFileRepository repository;
public ScmLfsProtocolServlet(LargeFileRepository largeFileRepository) {
this.repository = largeFileRepository;
}
@Override
protected LargeFileRepository getLargeFileRepository(LfsRequest request, String path) throws LfsException {
return repository;
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Unit tests for {@link GitRepositoryPathMatcher}.
*
* @author Sebastian Sdorra
* @since 1.54
*/
public class GitRepositoryPathMatcherTest {
private final GitRepositoryPathMatcher pathMatcher = new GitRepositoryPathMatcher();
@Test
public void testIsPathMatching() {
assertFalse(pathMatcher.isPathMatching(repository("my-repo"), "my-repoo"));
assertFalse(pathMatcher.isPathMatching(repository("my"), "my-repo"));
assertFalse(pathMatcher.isPathMatching(repository("my"), "my-repo/with/path"));
assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo"));
assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo.git"));
assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo/with/path"));
assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo.git/with/path"));
}
private Repository repository(String name) {
return new Repository(name, GitRepositoryHandler.TYPE_NAME, name);
}
}

View File

@@ -35,15 +35,10 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@@ -52,8 +47,9 @@ import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import static org.junit.Assert.*;
import sonia.scm.util.HttpUtil;
/**
* Unit tests for {@link GitUtil}.
@@ -125,9 +121,25 @@ public class GitUtilTest
return repo;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void testIsGitClient() {
HttpServletRequest request = mockRequestWithUserAgent("Git/2.9.3");
assertTrue(GitUtil.isGitClient(request));
request = mockRequestWithUserAgent("JGit/2.9.3");
assertTrue(GitUtil.isGitClient(request));
request = mockRequestWithUserAgent("Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) ...");
assertFalse(GitUtil.isGitClient(request));
}
private HttpServletRequest mockRequestWithUserAgent(String userAgent) {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getHeader(HttpUtil.HEADER_USERAGENT)).thenReturn(userAgent);
return request;
}
}

View File

@@ -191,7 +191,7 @@ public class GitRepositoryClientProvider extends RepositoryClientProvider
@Override
public File getWorkingCopy() {
return git.getRepository().getDirectory();
return git.getRepository().getWorkTree();
}
//~--- fields ---------------------------------------------------------------

View File

@@ -0,0 +1,135 @@
package sonia.scm.web;
import com.google.common.base.Charsets;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.servlet.ServletOutputStream;
import org.junit.Test;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.RepositoryProvider;
import sonia.scm.util.HttpUtil;
/**
* Unit tests for {@link GitPermissionFilter}.
*
* Created by omilke on 19.05.2017.
*/
@RunWith(MockitoJUnitRunner.class)
public class GitPermissionFilterTest {
@Mock
private RepositoryProvider repositoryProvider;
private final GitPermissionFilter permissionFilter = new GitPermissionFilter(
new ScmConfiguration(), repositoryProvider
);
@Mock
private HttpServletResponse response;
@Test
public void testIsWriteRequest() {
HttpServletRequest request = mockRequestWithMethodAndRequestURI("POST", "/scm/git/fanzy-project/git-receive-pack");
assertThat(permissionFilter.isWriteRequest(request), is(true));
request = mockRequestWithMethodAndRequestURI("GET", "/scm/git/fanzy-project/info/refs?service=git-receive-pack");
assertThat(permissionFilter.isWriteRequest(request), is(true));
request = mockRequestWithMethodAndRequestURI("GET", "/scm/git/fanzy-project/info/refs?service=some-other-service");
assertThat(permissionFilter.isWriteRequest(request), is(false));
request = mockRequestWithMethodAndRequestURI(
"PUT",
"/scm/git/git-lfs-demo.git/info/lfs/objects/8fcebeb5698230685f92028e560f8f1683ebc15ec82a620ffad5c12a3c19bdec"
);
assertThat(permissionFilter.isWriteRequest(request), is(true));
request = mockRequestWithMethodAndRequestURI(
"GET",
"/scm/git/git-lfs-demo.git/info/lfs/objects/8fcebeb5698230685f92028e560f8f1683ebc15ec82a620ffad5c12a3c19bdec"
);
assertThat(permissionFilter.isWriteRequest(request), is(false));
request = mockRequestWithMethodAndRequestURI("POST", "/scm/git/git-lfs-demo.git/info/lfs/objects/batch");
assertThat(permissionFilter.isWriteRequest(request), is(false));
}
private HttpServletRequest mockRequestWithMethodAndRequestURI(String method, String requestURI) {
HttpServletRequest mock = mock(HttpServletRequest.class);
when(mock.getMethod()).thenReturn(method);
when(mock.getRequestURI()).thenReturn(requestURI);
when(mock.getContextPath()).thenReturn("/scm");
return mock;
}
@Test
public void testSendNotEnoughPrivilegesErrorAsBrowser() throws IOException {
HttpServletRequest request = mockGitReceivePackServiceRequest();
permissionFilter.sendNotEnoughPrivilegesError(request, response);
verify(response).sendError(HttpServletResponse.SC_FORBIDDEN);
}
@Test
public void testSendNotEnoughPrivilegesErrorAsGitClient() throws IOException {
verifySendNotEnoughPrivilegesErrorAsGitClient("git/2.9.3");
}
@Test
public void testSendNotEnoughPrivilegesErrorAsJGitClient() throws IOException {
verifySendNotEnoughPrivilegesErrorAsGitClient("JGit/4.2");
}
private void verifySendNotEnoughPrivilegesErrorAsGitClient(String userAgent) throws IOException {
HttpServletRequest request = mockGitReceivePackServiceRequest();
when(request.getHeader(HttpUtil.HEADER_USERAGENT)).thenReturn(userAgent);
CapturingServletOutputStream stream = new CapturingServletOutputStream();
when(response.getOutputStream()).thenReturn(stream);
permissionFilter.sendNotEnoughPrivilegesError(request, response);
verify(response).setStatus(HttpServletResponse.SC_OK);
assertThat(stream.toString(), containsString("privileges"));
}
private HttpServletRequest mockGitReceivePackServiceRequest() {
HttpServletRequest request = mockRequestWithMethodAndRequestURI("GET", "/git/info/refs");
when(request.getParameter("service")).thenReturn("git-receive-pack");
return request;
}
private static class CapturingServletOutputStream extends ServletOutputStream {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@Override
public void write(int b) throws IOException {
baos.write(b);
}
@Override
public void close() throws IOException {
baos.close();
}
@Override
public String toString() {
return baos.toString();
}
}
}

View File

@@ -0,0 +1,109 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web;
import java.io.File;
import java.io.IOException;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;
/**
* Unit tests for {@link GitRepositoryResolver}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class GitRepositoryResolverTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File parentDirectory;
@Mock
private GitRepositoryHandler handler;
@InjectMocks
private GitRepositoryResolver resolver;
@Before
public void setUp() throws IOException {
parentDirectory = temporaryFolder.newFolder();
GitConfig config = new GitConfig();
config.setRepositoryDirectory(parentDirectory);
when(handler.getConfig()).thenReturn(config);
}
@Test
public void testFindRepositoryWithoutDotGit() {
createRepositories("a", "ab");
File directory = resolver.findRepository(parentDirectory, "a");
assertNotNull(directory);
assertEquals("a", directory.getName());
directory = resolver.findRepository(parentDirectory, "ab");
assertNotNull(directory);
assertEquals("ab", directory.getName());
}
@Test
public void testFindRepositoryWithDotGit() {
createRepositories("a", "ab");
File directory = resolver.findRepository(parentDirectory, "a.git");
assertNotNull(directory);
assertEquals("a", directory.getName());
directory = resolver.findRepository(parentDirectory, "ab.git");
assertNotNull(directory);
assertEquals("ab", directory.getName());
}
private void createRepositories(String... names) {
for (String name : names) {
assertTrue(new File(parentDirectory, name).mkdirs());
}
}
}

View File

@@ -33,51 +33,46 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import org.junit.Test;
import static org.junit.Assert.*;
//~--- JDK imports ------------------------------------------------------------
import java.util.Locale;
/**
*
* Unit tests for {@link GitUserAgentProvider}.
*
* @author Sebastian Sdorra <sebastian.sdorra@triology.de>
*/
public class GitUserAgentProviderTest
{
public class GitUserAgentProviderTest {
/**
* Method description
*
*/
private final GitUserAgentProvider provider = new GitUserAgentProvider();
@Test
public void testParseUserAgent()
{
public void testParseUserAgent() {
assertEquals(GitUserAgentProvider.GIT, parse("git/1.7.9.5"));
assertEquals(GitUserAgentProvider.JGIT, parse("jgit/4.5.2"));
assertEquals(GitUserAgentProvider.GIT_LFS, parse("git-lfs/2.0.1 (GitHub; windows amd64; go 1.8; git 678cdbd4)"));
assertEquals(GitUserAgentProvider.MSYSGIT, parse("git/1.8.3.msysgit.0"));
assertNull(parse("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"));
}
/**
* Method description
*
*
* @param v
*
* @return
*/
private UserAgent parse(String v)
{
return provider.parseUserAgent(
Strings.nullToEmpty(v).toLowerCase(Locale.ENGLISH));
@Test
public void testParseUserAgentCaseSensitive() {
assertEquals(GitUserAgentProvider.GIT, parse("Git/1.7.9.5"));
}
@Test
public void testParseUserAgentWithEmptyValue() {
assertNull(parse(null));
}
@Test
public void testParseUserAgentWithNullValue() {
assertNull(parse(null));
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final GitUserAgentProvider provider = new GitUserAgentProvider();
private UserAgent parse(String v) {
return provider.parseUserAgent(v);
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.web;
import org.junit.Test;
import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
/**
* Created by omilke on 11.05.2017.
*/
public class ScmGitServletTest {
@Test
public void isContentTypeMatches() throws Exception {
assertThat(ScmGitServlet.isLfsContentHeaderField("application/vnd.git-lfs+json", CONTENT_TYPE_GIT_LFS_JSON), is(true));
assertThat(ScmGitServlet.isLfsContentHeaderField("application/vnd.git-lfs+json;", CONTENT_TYPE_GIT_LFS_JSON), is(true));
assertThat(ScmGitServlet.isLfsContentHeaderField("application/vnd.git-lfs+json; charset=utf-8", CONTENT_TYPE_GIT_LFS_JSON), is(true));
assertThat(ScmGitServlet.isLfsContentHeaderField("application/vnd.git-lfs-json;", CONTENT_TYPE_GIT_LFS_JSON), is(false));
assertThat(ScmGitServlet.isLfsContentHeaderField("", CONTENT_TYPE_GIT_LFS_JSON), is(false));
assertThat(ScmGitServlet.isLfsContentHeaderField(null, CONTENT_TYPE_GIT_LFS_JSON), is(false));
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import static org.mockito.Matchers.matches;
import org.mockito.Mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.repository.Repository;
import sonia.scm.store.BlobStoreFactory;
/**
* Unit tests for {@link LfsBlobStoreFactory}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class LfsBlobStoreFactoryTest {
@Mock
private BlobStoreFactory blobStoreFactory;
@InjectMocks
private LfsBlobStoreFactory lfsBlobStoreFactory;
@Test
public void getBlobStore() throws Exception {
lfsBlobStoreFactory.getLfsBlobStore(new Repository("the-id", "GIT", "the-name"));
// just make sure the right parameter is passed, as properly validating the return value is nearly impossible with
// the return value (and should not be part of this test)
verify(blobStoreFactory).getBlobStore(matches("the-id-git-lfs"));
// make sure there have been no further usages of the factory
verifyNoMoreInteractions(blobStoreFactory);
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import com.google.common.collect.Lists;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.HandlerEventType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
/**
* Unit tests for {@link LfsStoreRemoveListener}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class LfsStoreRemoveListenerTest {
@Mock
private LfsBlobStoreFactory lfsBlobStoreFactory;
@Mock
private BlobStore blobStore;
@InjectMocks
private LfsStoreRemoveListener lfsStoreRemoveListener;
@Test
public void testHandleRepositoryEventWithNonDeleteEvents() {
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_CREATE));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.CREATE));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_MODIFY));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.MODIFY));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_DELETE));
verifyZeroInteractions(lfsBlobStoreFactory);
}
@Test
public void testHandleRepositoryEventWithNonGitRepositories() {
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "svn"));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "hg"));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "dummy"));
verifyZeroInteractions(lfsBlobStoreFactory);
}
@Test
public void testHandleRepositoryEvent() {
Repository heartOfGold = RepositoryTestData.createHeartOfGold("git");
when(lfsBlobStoreFactory.getLfsBlobStore(heartOfGold)).thenReturn(blobStore);
Blob blobA = mockBlob("a");
Blob blobB = mockBlob("b");
List<Blob> blobs = Lists.newArrayList(blobA, blobB);
when(blobStore.getAll()).thenReturn(blobs);
lfsStoreRemoveListener.handleRepositoryEvent(new RepositoryEvent(HandlerEventType.DELETE, heartOfGold));
verify(blobStore).getAll();
verify(blobStore).remove(blobA);
verify(blobStore).remove(blobB);
verifyNoMoreInteractions(blobStore);
}
private Blob mockBlob(String id) {
Blob blob = mock(Blob.class);
when(blob.getId()).thenReturn(id);
return blob;
}
private RepositoryEvent event(HandlerEventType eventType) {
return event(eventType, "git");
}
private RepositoryEvent event(HandlerEventType eventType, String repositoryType) {
return new RepositoryEvent(eventType, RepositoryTestData.create42Puzzle(repositoryType));
}
}

View File

@@ -0,0 +1,53 @@
package sonia.scm.web.lfs.servlet;
import org.junit.Test;
import sonia.scm.repository.Repository;
import javax.servlet.http.HttpServletRequest;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Created by omilke on 18.05.2017.
*/
public class LfsServletFactoryTest {
@Test
public void buildBaseUri() throws Exception {
String repositoryName = "git-lfs-demo";
String result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryName), RequestWithUri(repositoryName, true));
assertThat(result, is(equalTo("http://localhost:8081/scm/git/git-lfs-demo.git/info/lfs/objects/")));
//result will be with dot-gix suffix, ide
result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryName), RequestWithUri(repositoryName, false));
assertThat(result, is(equalTo("http://localhost:8081/scm/git/git-lfs-demo.git/info/lfs/objects/")));
}
private HttpServletRequest RequestWithUri(String repositoryName, boolean withDotGitSuffix) {
HttpServletRequest mockedRequest = mock(HttpServletRequest.class);
final String suffix;
if (withDotGitSuffix) {
suffix = ".git";
} else {
suffix = "";
}
//build from valid live request data
when(mockedRequest.getRequestURL()).thenReturn(
new StringBuffer(String.format("http://localhost:8081/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix)));
when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix));
when(mockedRequest.getContextPath()).thenReturn("/scm");
return mockedRequest;
}
}

View File

@@ -0,0 +1,42 @@
package sonia.scm.web.lfs.servlet;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.*;
/**
* Created by omilke on 16.05.2017.
*/
public class ScmFileTransferServletTest {
@Test
public void hasObjectId() throws Exception {
String SAMPLE_OBJECT_ID = "8fcebeb5698230685f92028e560f8f1683ebc15ec82a620ffad5c12a3c19bdec";
String path = "/git-lfs-demo.git/info/lfs/objects/" + SAMPLE_OBJECT_ID;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(equalTo(SAMPLE_OBJECT_ID)));
path = "/" + SAMPLE_OBJECT_ID;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(equalTo(SAMPLE_OBJECT_ID)));
path = SAMPLE_OBJECT_ID;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(equalTo(SAMPLE_OBJECT_ID)));
String nonObjectId = "this-ist-last-to-found";
path = "/git-lfs-demo.git/info/lfs/objects/" + nonObjectId;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(nullValue()));
nonObjectId = SAMPLE_OBJECT_ID.substring(1);
path = "/git-lfs-demo.git/info/lfs/objects/" + nonObjectId;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(nullValue()));
nonObjectId = SAMPLE_OBJECT_ID + "X";
path = "/git-lfs-demo.git/info/lfs/objects/" + nonObjectId;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(nullValue()));
}
}