From f28eaeca15e53046056368751c27834cf55d5848 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 23 Feb 2023 10:57:43 +0100 Subject: [PATCH] Fix repository resolution for ssh urls with context path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: René Pfeuffer --- gradle/changelog/ssh_repo_resolution.yaml | 2 + .../sonia/scm/config/ScmConfiguration.java | 17 ++++++ .../scm/config/ScmConfigurationTest.java | 9 +++ .../git/GitRepositoryContextResolver.java | 28 +++++++--- .../git/GitRepositoryContextResolverTest.java | 55 +++++++++++++++---- 5 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 gradle/changelog/ssh_repo_resolution.yaml diff --git a/gradle/changelog/ssh_repo_resolution.yaml b/gradle/changelog/ssh_repo_resolution.yaml new file mode 100644 index 0000000000..8b3596f108 --- /dev/null +++ b/gradle/changelog/ssh_repo_resolution.yaml @@ -0,0 +1,2 @@ +- type: fixed + description: Resolution of repositories from ssh urls with context paths diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 72a14521da..3508b5ad92 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -41,6 +41,7 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.io.File; +import java.net.URI; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -306,6 +307,22 @@ public class ScmConfiguration implements Configuration { return baseUrl; } + /** + * Returns the context path from the base url (see {@link #getBaseUrl()}) without starting or ending slashes. + * + * @since 2.42.0 + */ + public String getServerContextPath() { + String path = URI.create(getBaseUrl()).getPath(); + if (path.startsWith("/") && path.length() > 1) { + path = path.substring(1); + } + if (path.endsWith("/")) { + return path.substring(0, path.length() - 1); + } + return path; + } + /** * Returns the date format for the user interface. This format is a * JavaScript date format, from the library moment.js. diff --git a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationTest.java b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationTest.java index a1771b9c05..0121c874b0 100644 --- a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationTest.java +++ b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationTest.java @@ -25,6 +25,8 @@ package sonia.scm.config; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import static org.assertj.core.api.Assertions.assertThat; @@ -43,4 +45,11 @@ class ScmConfigurationTest { assertThat(scmConfiguration.isDefaultPluginAuthUrl()).isFalse(); } + @ParameterizedTest + @CsvSource({"https://hog.hitchiker.com/scm,scm", "https://hog.hitchiker.com/scm/,scm", "https://hog.hitchiker.com/,", "https://hog.hitchiker.com,"}) + void shouldReturnContextPath(String input, String expected) { + scmConfiguration.setBaseUrl(input); + + assertThat(scmConfiguration.getServerContextPath()).isEqualTo(expected == null ? "" : expected); + } } 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 index 97e113c363..e67e518bb0 100644 --- 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 @@ -21,10 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.protocolcommand.git; import com.google.common.base.Splitter; +import sonia.scm.ContextEntry; +import sonia.scm.NotFoundException; +import sonia.scm.config.ScmConfiguration; import sonia.scm.protocolcommand.RepositoryContext; import sonia.scm.protocolcommand.RepositoryContextResolver; import sonia.scm.repository.NamespaceAndName; @@ -35,34 +38,43 @@ import sonia.scm.repository.RepositoryManager; import javax.inject.Inject; import java.nio.file.Path; import java.util.Iterator; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; public class GitRepositoryContextResolver implements RepositoryContextResolver { - private RepositoryManager repositoryManager; - private RepositoryLocationResolver locationResolver; + private final RepositoryManager repositoryManager; + private final RepositoryLocationResolver locationResolver; + private final ScmConfiguration configuration; @Inject - public GitRepositoryContextResolver(RepositoryManager repositoryManager, RepositoryLocationResolver locationResolver) { + public GitRepositoryContextResolver(RepositoryManager repositoryManager, RepositoryLocationResolver locationResolver, ScmConfiguration configuration) { this.repositoryManager = repositoryManager; this.locationResolver = locationResolver; + this.configuration = configuration; } public RepositoryContext resolve(String[] args) { - NamespaceAndName namespaceAndName = extractNamespaceAndName(args); + NamespaceAndName namespaceAndName = extractNamespaceAndName(args).orElseThrow(() -> NotFoundException.notFound(ContextEntry.ContextBuilder.entity("path", String.join("/", args)))); Repository repository = repositoryManager.get(namespaceAndName); Path path = locationResolver.forClass(Path.class).getLocation(repository.getId()).resolve("data"); return new RepositoryContext(repository, path); } - private NamespaceAndName extractNamespaceAndName(String[] args) { + private Optional extractNamespaceAndName(String[] args) { String path = args[args.length - 1]; Iterator it = Splitter.on('/').omitEmptyStrings().split(path).iterator(); String type = it.next(); + if (type.equals(configuration.getServerContextPath())) { + type = it.next(); + } if ("repo".equals(type)) { String ns = it.next(); String name = it.next(); - return new NamespaceAndName(ns, name); + return of((new NamespaceAndName(ns, name))); } - return null; + return empty(); } } 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 index debc37ead3..e7e07e39ec 100644 --- 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 @@ -21,26 +21,31 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.protocolcommand.git; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.NotFoundException; +import sonia.scm.config.ScmConfiguration; 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.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -53,19 +58,47 @@ class GitRepositoryContextResolverTest { RepositoryManager repositoryManager; @Mock(answer = Answers.RETURNS_DEEP_STUBS) RepositoryLocationResolver locationResolver; + @Mock(answer = Answers.CALLS_REAL_METHODS) + ScmConfiguration scmConfiguration; @InjectMocks GitRepositoryContextResolver resolver; + @BeforeEach + void mockConfiguration() { + when(scmConfiguration.getBaseUrl()).thenReturn("https://hog.hitchhiker.com/scm"); + } + + @Nested + class WithRepository { + + @BeforeEach + void mockRepository(@TempDir Path repositoryPath) throws IOException { + when(scmConfiguration.getBaseUrl()).thenReturn("https://hog.hitchhiker.com/scm"); + when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(REPOSITORY); + when(locationResolver.forClass(any()).getLocation("id")).thenReturn(repositoryPath); + } + + @Test + void shouldResolveCorrectRepository(@TempDir Path 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")); + } + + @Test + void shouldResolveCorrectRepositoryWithContextPath(@TempDir Path repositoryPath) throws IOException { + RepositoryContext context = resolver.resolve(new String[]{"git", "scm/repo/space/X/something/else"}); + + assertThat(context.getRepository()).isSameAs(REPOSITORY); + assertThat(context.getDirectory()).isEqualTo(repositoryPath.resolve("data")); + } + } + @Test - void shouldResolveCorrectRepository() throws IOException { - when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(REPOSITORY); - Path repositoryPath = File.createTempFile("test", "scm").toPath(); - when(locationResolver.forClass(any()).getLocation("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")); + void shouldRejectWrongContextPath() { + assertThrows(NotFoundException.class, + () -> resolver.resolve(new String[]{"git", "vogons/repo/space/X/something/else"})); } }