diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/CommandContext.java b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandContext.java new file mode 100644 index 0000000000..44a1cce95a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandContext.java @@ -0,0 +1,20 @@ +package sonia.scm.protocolcommand; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.InputStream; +import java.io.OutputStream; + +@Getter +@AllArgsConstructor +public class CommandContext { + + private String command; + private String[] args; + + private InputStream inputStream; + private OutputStream outputStream; + private OutputStream errorStream; + +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreter.java b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreter.java new file mode 100644 index 0000000000..24bf7d30fa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreter.java @@ -0,0 +1,10 @@ +package sonia.scm.protocolcommand; + +public interface CommandInterpreter { + + String[] getParsedArgs(); + + ScmCommandProtocol getProtocolHandler(); + + RepositoryContextResolver getRepositoryContextResolver(); +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreterFactory.java b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreterFactory.java new file mode 100644 index 0000000000..9d6bfa1d7f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/CommandInterpreterFactory.java @@ -0,0 +1,10 @@ +package sonia.scm.protocolcommand; + +import sonia.scm.plugin.ExtensionPoint; + +import java.util.Optional; + +@ExtensionPoint +public interface CommandInterpreterFactory { + Optional canHandle(String command); +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContext.java b/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContext.java new file mode 100644 index 0000000000..a64d5a6047 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContext.java @@ -0,0 +1,23 @@ +package sonia.scm.protocolcommand; + +import sonia.scm.repository.Repository; + +import java.nio.file.Path; + +public class RepositoryContext { + private Repository repository; + private Path directory; + + public RepositoryContext(Repository repository, Path directory) { + this.repository = repository; + this.directory = directory; + } + + public Repository getRepository() { + return repository; + } + + public Path getDirectory() { + return directory; + } +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContextResolver.java b/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContextResolver.java new file mode 100644 index 0000000000..2c48ece185 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/RepositoryContextResolver.java @@ -0,0 +1,8 @@ +package sonia.scm.protocolcommand; + +@FunctionalInterface +public interface RepositoryContextResolver { + + RepositoryContext resolve(String[] args); + +} diff --git a/scm-core/src/main/java/sonia/scm/protocolcommand/ScmCommandProtocol.java b/scm-core/src/main/java/sonia/scm/protocolcommand/ScmCommandProtocol.java new file mode 100644 index 0000000000..e7dbf65cad --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/protocolcommand/ScmCommandProtocol.java @@ -0,0 +1,9 @@ +package sonia.scm.protocolcommand; + +import java.io.IOException; + +public interface ScmCommandProtocol { + + void handle(CommandContext context, RepositoryContext repositoryContext) throws IOException; + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 18613c1a12..665f63487d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -307,8 +307,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per this.permissions.add(newPermission); } - public void removePermission(RepositoryPermission permission) { - this.permissions.remove(permission); + public boolean removePermission(RepositoryPermission permission) { + return this.permissions.remove(permission); } public void setPublicReadable(boolean publicReadable) { diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java index 54163e0393..4a458a6e6a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -45,6 +45,7 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -55,6 +56,8 @@ import static java.util.Collections.unmodifiableSet; /** * Permissions controls the access to {@link Repository}. + * This object should be immutable, but could not be due to mapstruct. Do not modify instances of this because this + * would change the hash code and therefor make it undeletable in a repository. * * @author Sebastian Sdorra */ @@ -64,22 +67,26 @@ public class RepositoryPermission implements PermissionObject, Serializable { private static final long serialVersionUID = -2915175031430884040L; + public static final String REPOSITORY_MODIFIED_EXCEPTION_TEXT = "repository permission must not be modified"; - private boolean groupPermission = false; + private Boolean groupPermission; private String name; @XmlElement(name = "verb") private Set verbs; /** - * Constructs a new {@link RepositoryPermission}. - * This constructor is used by JAXB and mapstruct. + * This constructor exists for mapstruct and JAXB, only -- do not use this in "normal" code. + * + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} instead. */ + @Deprecated public RepositoryPermission() {} public RepositoryPermission(String name, Collection verbs, boolean groupPermission) { this.name = name; - this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); + this.verbs = new LinkedHashSet<>(verbs); this.groupPermission = groupPermission; } @@ -163,7 +170,7 @@ public class RepositoryPermission implements PermissionObject, Serializable */ public Collection getVerbs() { - return verbs == null? emptyList(): verbs; + return verbs == null ? emptyList() : Collections.unmodifiableSet(verbs); } /** @@ -181,35 +188,50 @@ public class RepositoryPermission implements PermissionObject, Serializable //~--- set methods ---------------------------------------------------------- /** - * Sets true if the permission is a group permission. + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. * - * - * @param groupPermission true if the permission is a group permission + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} instead. */ + @Deprecated public void setGroupPermission(boolean groupPermission) { + if (this.groupPermission != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } this.groupPermission = groupPermission; } /** - * The name of the user or group. + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. * - * - * @param name name of the user or group + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} instead. */ + @Deprecated public void setName(String name) { + if (this.name != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } this.name = name; } /** - * Sets the verb of the permission. + * Use this for creation only. This will throw an {@link IllegalStateException} when modified. + * @throws IllegalStateException when modified after the value has been set once. * - * - * @param verbs verbs of the permission + * @deprecated Do not use this for "normal" code. + * Use {@link RepositoryPermission#RepositoryPermission(String, Collection, boolean)} instead. */ + @Deprecated public void setVerbs(Collection verbs) { + if (this.verbs != null) { + throw new IllegalStateException(REPOSITORY_MODIFIED_EXCEPTION_TEXT); + } this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs)); } } diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java index d65358a66e..c7f05c5f3c 100644 --- a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java @@ -50,8 +50,7 @@ class RepositoryPermissionTest { @Test void shouldBeEqualWithRedundantVerbs() { RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one", "two"), false); - RepositoryPermission permission2 = new RepositoryPermission("name1", asList("one", "two"), false); - permission2.setVerbs(asList("one", "two", "two")); + RepositoryPermission permission2 = new RepositoryPermission("name1", asList("one", "two", "two"), false); assertThat(permission1).isEqualTo(permission2); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java new file mode 100644 index 0000000000..4fc3a5415c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/BaseReceivePackFactory.java @@ -0,0 +1,42 @@ +package sonia.scm.protocolcommand.git; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.spi.HookEventFacade; +import sonia.scm.web.CollectingPackParserListener; +import sonia.scm.web.GitReceiveHook; + +public abstract class BaseReceivePackFactory implements ReceivePackFactory { + + private final GitRepositoryHandler handler; + private final GitReceiveHook hook; + + protected BaseReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { + this.handler = handler; + this.hook = new GitReceiveHook(hookEventFacade, handler); + } + + @Override + public final ReceivePack create(T connection, Repository repository) throws ServiceNotAuthorizedException, ServiceNotEnabledException { + ReceivePack receivePack = createBasicReceivePack(connection, repository); + receivePack.setAllowNonFastForwards(isNonFastForwardAllowed()); + + receivePack.setPreReceiveHook(hook); + receivePack.setPostReceiveHook(hook); + // apply collecting listener, to be able to check which commits are new + CollectingPackParserListener.set(receivePack); + + return receivePack; + } + + protected abstract ReceivePack createBasicReceivePack(T request, Repository repository) + throws ServiceNotEnabledException, ServiceNotAuthorizedException; + + private boolean isNonFastForwardAllowed() { + return ! handler.getConfig().isNonFastForwardDisallowed(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreter.java new file mode 100644 index 0000000000..d4e6f8edbb --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreter.java @@ -0,0 +1,32 @@ +package sonia.scm.protocolcommand.git; + +import sonia.scm.protocolcommand.CommandInterpreter; +import sonia.scm.protocolcommand.RepositoryContextResolver; +import sonia.scm.protocolcommand.ScmCommandProtocol; + +class GitCommandInterpreter implements CommandInterpreter { + private final GitRepositoryContextResolver gitRepositoryContextResolver; + private final GitCommandProtocol gitCommandProtocol; + private final String[] args; + + GitCommandInterpreter(GitRepositoryContextResolver gitRepositoryContextResolver, GitCommandProtocol gitCommandProtocol, String[] args) { + this.gitRepositoryContextResolver = gitRepositoryContextResolver; + this.gitCommandProtocol = gitCommandProtocol; + this.args = args; + } + + @Override + public String[] getParsedArgs() { + return args; + } + + @Override + public ScmCommandProtocol getProtocolHandler() { + return gitCommandProtocol; + } + + @Override + public RepositoryContextResolver getRepositoryContextResolver() { + return gitRepositoryContextResolver; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreterFactory.java new file mode 100644 index 0000000000..bd38dc01db --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandInterpreterFactory.java @@ -0,0 +1,37 @@ +package sonia.scm.protocolcommand.git; + +import sonia.scm.plugin.Extension; +import sonia.scm.protocolcommand.CommandInterpreter; +import sonia.scm.protocolcommand.CommandInterpreterFactory; + +import javax.inject.Inject; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +@Extension +public class GitCommandInterpreterFactory implements CommandInterpreterFactory { + private final GitCommandProtocol gitCommandProtocol; + private final GitRepositoryContextResolver gitRepositoryContextResolver; + + @Inject + public GitCommandInterpreterFactory(GitCommandProtocol gitCommandProtocol, GitRepositoryContextResolver gitRepositoryContextResolver) { + this.gitCommandProtocol = gitCommandProtocol; + this.gitRepositoryContextResolver = gitRepositoryContextResolver; + } + + @Override + public Optional canHandle(String command) { + try { + String[] args = GitCommandParser.parse(command); + if (args[0].startsWith("git")) { + return of(new GitCommandInterpreter(gitRepositoryContextResolver, gitCommandProtocol, args)); + } else { + return empty(); + } + } catch (IllegalArgumentException e) { + return empty(); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandParser.java new file mode 100644 index 0000000000..624b951d42 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandParser.java @@ -0,0 +1,88 @@ +package sonia.scm.protocolcommand.git; + +import java.util.ArrayList; +import java.util.List; + +class GitCommandParser { + + private GitCommandParser() { + } + + static String[] parse(String command) { + List strs = parseDelimitedString(command, " ", true); + String[] args = strs.toArray(new String[strs.size()]); + for (int i = 0; i < args.length; i++) { + String argVal = args[i]; + if (argVal.startsWith("'") && argVal.endsWith("'")) { + args[i] = argVal.substring(1, argVal.length() - 1); + argVal = args[i]; + } + if (argVal.startsWith("\"") && argVal.endsWith("\"")) { + args[i] = argVal.substring(1, argVal.length() - 1); + } + } + + if (args.length != 2) { + throw new IllegalArgumentException("Invalid git command line (no arguments): " + command); + } + return args; + } + + private static List parseDelimitedString(String value, String delim, boolean trim) { + if (value == null) { + value = ""; + } + + List list = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + int expecting = 7; + boolean isEscaped = false; + + for(int i = 0; i < value.length(); ++i) { + char c = value.charAt(i); + boolean isDelimiter = delim.indexOf(c) >= 0; + if (!isEscaped && c == '\\') { + isEscaped = true; + } else { + if (isEscaped) { + sb.append(c); + } else if (isDelimiter && (expecting & 2) != 0) { + if (trim) { + String str = sb.toString(); + list.add(str.trim()); + } else { + list.add(sb.toString()); + } + + sb.delete(0, sb.length()); + expecting = 7; + } else if (c == '"' && (expecting & 4) != 0) { + sb.append(c); + expecting = 9; + } else if (c == '"' && (expecting & 8) != 0) { + sb.append(c); + expecting = 7; + } else { + if ((expecting & 1) == 0) { + throw new IllegalArgumentException("Invalid delimited string: " + value); + } + + sb.append(c); + } + + isEscaped = false; + } + } + + if (sb.length() > 0) { + if (trim) { + String str = sb.toString(); + list.add(str.trim()); + } else { + list.add(sb.toString()); + } + } + + return list; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandProtocol.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandProtocol.java new file mode 100644 index 0000000000..b11ea80cfe --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitCommandProtocol.java @@ -0,0 +1,74 @@ +package sonia.scm.protocolcommand.git; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.UploadPack; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.plugin.Extension; +import sonia.scm.protocolcommand.CommandContext; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.protocolcommand.ScmCommandProtocol; +import sonia.scm.repository.RepositoryPermissions; + +import javax.inject.Inject; +import java.io.IOException; + +@Extension +public class GitCommandProtocol implements ScmCommandProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(GitCommandProtocol.class); + + private ScmUploadPackFactory uploadPackFactory; + private ScmReceivePackFactory receivePackFactory; + + @Inject + public GitCommandProtocol(ScmUploadPackFactory uploadPackFactory, ScmReceivePackFactory receivePackFactory) { + this.uploadPackFactory = uploadPackFactory; + this.receivePackFactory = receivePackFactory; + } + + @Override + public void handle(CommandContext commandContext, RepositoryContext repositoryContext) throws IOException { + String subCommand = commandContext.getArgs()[0]; + + if (RemoteConfig.DEFAULT_UPLOAD_PACK.equals(subCommand)) { + LOG.trace("got upload pack"); + upload(commandContext, repositoryContext); + } else if (RemoteConfig.DEFAULT_RECEIVE_PACK.equals(subCommand)) { + LOG.trace("got receive pack"); + receive(commandContext, repositoryContext); + } else { + throw new IllegalArgumentException("Unknown git command: " + commandContext.getCommand()); + } + } + + private void receive(CommandContext commandContext, RepositoryContext repositoryContext) throws IOException { + RepositoryPermissions.push(repositoryContext.getRepository()).check(); + try (Repository repository = open(repositoryContext)) { + ReceivePack receivePack = receivePackFactory.create(repositoryContext, repository); + receivePack.receive(commandContext.getInputStream(), commandContext.getOutputStream(), commandContext.getErrorStream()); + } catch (ServiceNotEnabledException | ServiceNotAuthorizedException e) { + throw new IOException("error creating receive pack for ssh", e); + } + } + + private void upload(CommandContext commandContext, RepositoryContext repositoryContext) throws IOException { + RepositoryPermissions.pull(repositoryContext.getRepository()).check(); + try (Repository repository = open(repositoryContext)) { + UploadPack uploadPack = uploadPackFactory.create(repositoryContext, repository); + uploadPack.upload(commandContext.getInputStream(), commandContext.getOutputStream(), commandContext.getErrorStream()); + } + } + + private Repository open(RepositoryContext repositoryContext) throws IOException { + RepositoryCache.FileKey key = RepositoryCache.FileKey.lenient(repositoryContext.getDirectory().toFile(), FS.DETECTED); + return key.open(true); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java new file mode 100644 index 0000000000..8acfc68dce --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolver.java @@ -0,0 +1,44 @@ +package sonia.scm.protocolcommand.git; + +import com.google.common.base.Splitter; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.protocolcommand.RepositoryContextResolver; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Inject; +import java.nio.file.Path; +import java.util.Iterator; + +public class GitRepositoryContextResolver implements RepositoryContextResolver { + + private RepositoryManager repositoryManager; + private RepositoryLocationResolver locationResolver; + + @Inject + public GitRepositoryContextResolver(RepositoryManager repositoryManager, RepositoryLocationResolver locationResolver) { + this.repositoryManager = repositoryManager; + this.locationResolver = locationResolver; + } + + public RepositoryContext resolve(String[] args) { + NamespaceAndName namespaceAndName = extractNamespaceAndName(args); + Repository repository = repositoryManager.get(namespaceAndName); + Path path = locationResolver.getPath(repository.getId()).resolve("data"); + return new RepositoryContext(repository, path); + } + + private NamespaceAndName extractNamespaceAndName(String[] args) { + String path = args[args.length - 1]; + Iterator it = Splitter.on('/').omitEmptyStrings().split(path).iterator(); + String type = it.next(); + if ("repo".equals(type)) { + String ns = it.next(); + String name = it.next(); + return new NamespaceAndName(ns, name); + } + return null; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java new file mode 100644 index 0000000000..105a04ccdb --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmReceivePackFactory.java @@ -0,0 +1,21 @@ +package sonia.scm.protocolcommand.git; + +import com.google.inject.Inject; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceivePack; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.spi.HookEventFacade; + +public class ScmReceivePackFactory extends BaseReceivePackFactory { + + @Inject + public ScmReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { + super(handler, hookEventFacade); + } + + @Override + protected ReceivePack createBasicReceivePack(RepositoryContext repositoryContext, Repository repository) { + return new ReceivePack(repository); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmUploadPackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmUploadPackFactory.java new file mode 100644 index 0000000000..962437a59b --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/protocolcommand/git/ScmUploadPackFactory.java @@ -0,0 +1,13 @@ +package sonia.scm.protocolcommand.git; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.UploadPack; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import sonia.scm.protocolcommand.RepositoryContext; + +public class ScmUploadPackFactory implements UploadPackFactory { + @Override + public UploadPack create(RepositoryContext repositoryContext, Repository repository) { + return new UploadPack(repository); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java index 5cb8007986..b59fa7526b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java @@ -35,7 +35,6 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory; import org.eclipse.jgit.lib.Repository; @@ -43,6 +42,7 @@ import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import sonia.scm.protocolcommand.git.BaseReceivePackFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.spi.HookEventFacade; @@ -56,42 +56,20 @@ import javax.servlet.http.HttpServletRequest; * * @author Sebastian Sdorra */ -public class GitReceivePackFactory implements ReceivePackFactory +public class GitReceivePackFactory extends BaseReceivePackFactory { - private final GitRepositoryHandler handler; - - private ReceivePackFactory wrapped; - - private final GitReceiveHook hook; + private ReceivePackFactory wrapped; @Inject public GitReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) { - this.handler = handler; - this.hook = new GitReceiveHook(hookEventFacade, handler); - this.wrapped = new DefaultReceivePackFactory(); + super(handler, hookEventFacade); + this.wrapped = new DefaultReceivePackFactory(); } @Override - public ReceivePack create(HttpServletRequest request, Repository repository) + protected ReceivePack createBasicReceivePack(HttpServletRequest request, Repository repository) throws ServiceNotEnabledException, ServiceNotAuthorizedException { - ReceivePack receivePack = wrapped.create(request, repository); - receivePack.setAllowNonFastForwards(isNonFastForwardAllowed()); - - receivePack.setPreReceiveHook(hook); - receivePack.setPostReceiveHook(hook); - // apply collecting listener, to be able to check which commits are new - CollectingPackParserListener.set(receivePack); - - return receivePack; - } - - private boolean isNonFastForwardAllowed() { - return ! handler.getConfig().isNonFastForwardDisallowed(); - } - - @VisibleForTesting - void setWrapped(ReceivePackFactory wrapped) { - this.wrapped = wrapped; + return wrapped.create(request, repository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java similarity index 80% rename from scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java rename to scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java index 4ed9d5a46a..fa1a97ec3b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/BaseReceivePackFactoryTest.java @@ -29,13 +29,15 @@ * */ -package sonia.scm.web; +package sonia.scm.protocolcommand.git; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -45,21 +47,20 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.web.CollectingPackParserListener; +import sonia.scm.web.GitReceiveHook; -import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.*; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -/** - * Unit tests for {@link GitReceivePackFactory}. - */ @RunWith(MockitoJUnitRunner.class) -public class GitReceivePackFactoryTest { +public class BaseReceivePackFactoryTest { @Mock private GitRepositoryHandler handler; @@ -67,12 +68,11 @@ public class GitReceivePackFactoryTest { private GitConfig config; @Mock - private ReceivePackFactory wrappedReceivePackFactory; + private ReceivePackFactory wrappedReceivePackFactory; - private GitReceivePackFactory factory; + private BaseReceivePackFactory factory; - @Mock - private HttpServletRequest request; + private Object request = new Object(); private Repository repository; @@ -89,8 +89,12 @@ public class GitReceivePackFactoryTest { ReceivePack receivePack = new ReceivePack(repository); when(wrappedReceivePackFactory.create(request, repository)).thenReturn(receivePack); - factory = new GitReceivePackFactory(handler, null); - factory.setWrapped(wrappedReceivePackFactory); + factory = new BaseReceivePackFactory(handler, null) { + @Override + protected ReceivePack createBasicReceivePack(Object request, Repository repository) throws ServiceNotEnabledException, ServiceNotAuthorizedException { + return wrappedReceivePackFactory.create(request, repository); + } + }; } private Repository createRepositoryForTesting() throws GitAPIException, IOException { @@ -105,6 +109,7 @@ public class GitReceivePackFactoryTest { assertThat(receivePack.getPreReceiveHook(), instanceOf(GitReceiveHook.class)); assertThat(receivePack.getPostReceiveHook(), instanceOf(GitReceiveHook.class)); assertTrue(receivePack.isAllowNonFastForwards()); + verify(wrappedReceivePackFactory).create(request, repository); } @Test diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java new file mode 100644 index 0000000000..6ac4cdb54b --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/protocolcommand/git/GitRepositoryContextResolverTest.java @@ -0,0 +1,45 @@ +package sonia.scm.protocolcommand.git; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.protocolcommand.RepositoryContext; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryManager; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GitRepositoryContextResolverTest { + + private static final Repository REPOSITORY = new Repository("id", "git", "space", "X"); + + @Mock + RepositoryManager repositoryManager; + @Mock + RepositoryLocationResolver locationResolver; + + @InjectMocks + GitRepositoryContextResolver resolver; + + @Test + void shouldResolveCorrectRepository() throws IOException { + when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(REPOSITORY); + Path repositoryPath = File.createTempFile("test", "scm").toPath(); + when(locationResolver.getPath("id")).thenReturn(repositoryPath); + + RepositoryContext context = resolver.resolve(new String[] {"git", "repo/space/X/something/else"}); + + assertThat(context.getRepository()).isSameAs(REPOSITORY); + assertThat(context.getDirectory()).isEqualTo(repositoryPath.resolve("data")); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java index da26ebaf20..70c34fa122 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java @@ -27,12 +27,16 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + // keep this so that it will not be garbage collected (Transport keeps this in a week reference) + private ScmTransportProtocol proto; + @Before public void bindScmProtocol() { HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory); GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); - Transport.register(new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler))); + proto = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)); + Transport.register(proto); } @Test diff --git a/scm-ui-components/packages/ui-components/src/buttons/SubmitButton.js b/scm-ui-components/packages/ui-components/src/buttons/SubmitButton.js index 0f03d850b2..cca5b9eb8a 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/SubmitButton.js +++ b/scm-ui-components/packages/ui-components/src/buttons/SubmitButton.js @@ -2,9 +2,17 @@ import React from "react"; import Button, { type ButtonProps } from "./Button"; -class SubmitButton extends React.Component { +type SubmitButtonProps = ButtonProps & { + scrollToTop: boolean +} + +class SubmitButton extends React.Component { + static defaultProps = { + scrollToTop: true + }; + render() { - const { action } = this.props; + const { action, scrollToTop } = this.props; return (