Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ under the License.
<artifactId>commons-io</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.scm</groupId>
<artifactId>maven-scm-api</artifactId>
<version>2.1.0</version>
</dependency>

<!-- dependencies to annotations -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.site.AbstractSiteMojo;
import org.apache.maven.project.MavenProject;
import org.apache.maven.scm.provider.ScmUrlUtils;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
Expand Down Expand Up @@ -492,6 +493,66 @@ private static String getFullName(MavenProject project) {
+ project.getVersion() + ')';
}

/**
* Extracts the provider-specific URL from an SCM URL for comparison purposes.
* For non-SCM URLs, returns the original URL.
* For SCM URLs with SCP-like syntax (e.g., [email protected]:user/repo.git),
* converts them to a comparable format.
* For hierarchical SCM systems like SVN, normalizes the scheme to enable
* proper comparison of URLs that differ only in http vs https.
*
* @param url the URL to process
* @return the provider-specific URL for SCM URLs, or the original URL otherwise
*/
static String extractComparableUrl(String url) {
if (url != null && url.startsWith("scm:")) {
// Extract the SCM provider (e.g., "git", "svn")
String provider = ScmUrlUtils.getProvider(url);

// Extract the provider-specific part of the SCM URL
// For example: "scm:git:https://github.com/user/repo.git" -> "https://github.com/user/repo.git"
String providerSpecificPart = ScmUrlUtils.getProviderSpecificPart(url);
if (providerSpecificPart != null && !providerSpecificPart.isEmpty()) {
// Handle SCP-like Git syntax (e.g., [email protected]:user/repo.git or user@host:path)
// Convert it to a more standard format for comparison
// Note: This is a heuristic check - we look for the pattern of user@host:path
// where the colon comes after the @ symbol and is followed by a path
if (providerSpecificPart.contains("@")
&& !providerSpecificPart.startsWith("http://")
&& !providerSpecificPart.startsWith("https://")
&& !providerSpecificPart.startsWith("ssh://")) {
// Find the @ symbol and look for the first : after it that's not part of a URL scheme
int atIndex = providerSpecificPart.lastIndexOf('@');
int colonIndex = providerSpecificPart.indexOf(':', atIndex);

// Verify this looks like SCP syntax: user@host:path
// The colon should come after @ and before the end
if (atIndex >= 0 && colonIndex > atIndex + 1 && colonIndex < providerSpecificPart.length() - 1) {
String host = providerSpecificPart.substring(atIndex + 1, colonIndex);
String path = providerSpecificPart.substring(colonIndex + 1);
// Convert to a pseudo-URL format for comparison
// Note: IPv6 addresses in brackets are handled by this approach
// as the brackets will be preserved in the host part
return "ssh://" + host + "/" + path;
}
}

// For hierarchical VCS systems like SVN, normalize the scheme to allow
// comparison of URLs that differ only in http vs https
// SVN repositories can be accessed via both protocols and should be considered the same
if ("svn".equalsIgnoreCase(provider) && providerSpecificPart.startsWith("https://")) {
// Normalize https to http for SVN URLs to enable proper comparison
return "http" + providerSpecificPart.substring(5);
}

// Return the provider-specific part as-is for standard URLs or
// if SCP syntax conversion is not applicable
return providerSpecificPart;
}
}
return url;
}

/**
* Extract the distributionManagement site of the top level parent of the given MavenProject.
* This climbs up the project hierarchy and returns the site of the last project
Expand Down Expand Up @@ -523,8 +584,12 @@ protected MavenProject getTopLevelProject(MavenProject project) throws MojoExecu
}

// MSITE-600
URIPathDescriptor siteURI = new URIPathDescriptor(URIEncoder.encodeURI(site.getUrl()), "");
URIPathDescriptor oldSiteURI = new URIPathDescriptor(URIEncoder.encodeURI(oldSite.getUrl()), "");
// MSITE-1033: For SCM URLs, extract the provider-specific part for comparison
String siteUrlToCompare = extractComparableUrl(site.getUrl());
String oldSiteUrlToCompare = extractComparableUrl(oldSite.getUrl());

URIPathDescriptor siteURI = new URIPathDescriptor(URIEncoder.encodeURI(siteUrlToCompare), "");
URIPathDescriptor oldSiteURI = new URIPathDescriptor(URIEncoder.encodeURI(oldSiteUrlToCompare), "");

if (!siteURI.sameSite(oldSiteURI.getBaseURI())) {
return oldProject;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugins.site.deploy;

import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Site;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

/**
* Tests for AbstractDeployMojo.
*/
public class AbstractDeployMojoTest {

/**
* Test that getTopLevelProject correctly handles SCM URLs with different repositories.
* This is the test case for MSITE-1033.
*/
@Test
public void testGetTopLevelProjectWithDifferentScmUrls() throws Exception {
// Create a mock deploy mojo
TestDeployMojo mojo = new TestDeployMojo();

// Create child project with SCM URL
MavenProject childProject =
createProjectWithSite("child", "scm:git:[email protected]:codehaus-plexus/plexus-sec-dispatcher.git/");

// Create parent project with different SCM URL
MavenProject parentProject =
createProjectWithSite("parent", "scm:git:https://github.com/codehaus-plexus/plexus-pom.git/");

// Set up the parent-child relationship
childProject.setParent(parentProject);

// Call getTopLevelProject - it should return childProject, not parentProject
// because the SCM URLs point to different repositories
MavenProject topProject = mojo.getTopLevelProject(childProject);

// The top project should be the child project itself since the parent has a different site
assertEquals("Top project should be child project due to different SCM URLs", childProject, topProject);
}

/**
* Test that getTopLevelProject correctly handles SCM URLs with the same repository.
*/
@Test
public void testGetTopLevelProjectWithSameScmUrls() throws Exception {
// Create a mock deploy mojo
TestDeployMojo mojo = new TestDeployMojo();

// Create child project with SCM URL
MavenProject childProject =
createProjectWithSite("child", "scm:git:https://github.com/codehaus-plexus/plexus-pom.git/child");

// Create parent project with same base SCM URL
MavenProject parentProject =
createProjectWithSite("parent", "scm:git:https://github.com/codehaus-plexus/plexus-pom.git/");

// Set up the parent-child relationship
childProject.setParent(parentProject);

// Call getTopLevelProject - it should return parentProject
// because the SCM URLs point to the same repository
MavenProject topProject = mojo.getTopLevelProject(childProject);

// The top project should be the parent project since they share the same site
assertEquals("Top project should be parent project due to same SCM base URL", parentProject, topProject);
}

/**
* Test that getTopLevelProject correctly handles non-SCM URLs.
*/
@Test
public void testGetTopLevelProjectWithNonScmUrls() throws Exception {
// Create a mock deploy mojo
TestDeployMojo mojo = new TestDeployMojo();

// Create child project with regular URL
MavenProject childProject = createProjectWithSite("child", "https://example.com/site/child");

// Create parent project with same base URL
MavenProject parentProject = createProjectWithSite("parent", "https://example.com/site/");

// Set up the parent-child relationship
childProject.setParent(parentProject);

// Call getTopLevelProject - it should return parentProject
MavenProject topProject = mojo.getTopLevelProject(childProject);

// The top project should be the parent project since they share the same site
assertEquals("Top project should be parent project for regular URLs", parentProject, topProject);
}

/**
* Test that getTopLevelProject correctly handles SCM URLs with standard https format.
*/
@Test
public void testGetTopLevelProjectWithHttpsScmUrls() throws Exception {
// Create a mock deploy mojo
TestDeployMojo mojo = new TestDeployMojo();

// Create child project with https SCM URL
MavenProject childProject = createProjectWithSite("child", "scm:git:https://github.com/user/repo1.git/");

// Create parent project with different https SCM URL (different domain)
MavenProject parentProject = createProjectWithSite("parent", "scm:git:https://gitlab.com/user/repo2.git/");

// Set up the parent-child relationship
childProject.setParent(parentProject);

// Call getTopLevelProject - it should return childProject
// because the SCM URLs point to different repositories
MavenProject topProject = mojo.getTopLevelProject(childProject);

// The top project should be the child project itself since the parent has a different site
assertEquals("Top project should be child project due to different https SCM URLs", childProject, topProject);
}

/**
* Test that extractComparableUrl properly handles SVN URLs with different schemes but same host.
* For SVN (hierarchical VCS), URLs with different schemes (http vs https) should be normalized
* to the same scheme to allow URIPathDescriptor.sameSite() to recognize them as the same site.
* Note: The paths may differ (one being a subpath of another), but as long as scheme, host, and port
* are the same, URIPathDescriptor.sameSite() will correctly identify them as the same site.
*/
@Test
public void testExtractComparableUrlForSvnUrls() throws Exception {
// Extract and normalize both URLs
String url1 = AbstractDeployMojo.extractComparableUrl("scm:svn:http://svn.apache.org/repos/asf/maven/website");
String url2 = AbstractDeployMojo.extractComparableUrl(
"scm:svn:https://svn.apache.org/repos/asf/maven/website/components");

// Both should be normalized to http scheme
assertEquals("http://svn.apache.org/repos/asf/maven/website", url1);
assertEquals("http://svn.apache.org/repos/asf/maven/website/components", url2);

// Now verify they would be considered the same site by creating projects with these URLs
TestDeployMojo mojo = new TestDeployMojo();
MavenProject childProject =
createProjectWithSite("child", "scm:svn:https://svn.apache.org/repos/asf/maven/website/components");
MavenProject parentProject =
createProjectWithSite("parent", "scm:svn:http://svn.apache.org/repos/asf/maven/website");
childProject.setParent(parentProject);

// The parent should be returned as the top project since they share the same SVN site
MavenProject topProject = mojo.getTopLevelProject(childProject);
assertEquals(
"Top project should be parent due to normalized SVN URLs pointing to same site",
parentProject,
topProject);
}

private MavenProject createProjectWithSite(String artifactId, String siteUrl) {
MavenProject project = new MavenProject();
project.setGroupId("org.apache.maven.test");
project.setArtifactId(artifactId);
project.setVersion("1.0");
project.setName(artifactId);

DistributionManagement distributionManagement = new DistributionManagement();
Site site = new Site();
site.setId("test-site");
site.setUrl(siteUrl);
distributionManagement.setSite(site);
project.setDistributionManagement(distributionManagement);

return project;
}

/**
* Test implementation of AbstractDeployMojo for testing.
*/
private static class TestDeployMojo extends AbstractDeployMojo {
@Override
protected boolean isDeploy() {
return true;
}

@Override
protected String determineTopDistributionManagementSiteUrl() throws MojoExecutionException {
return "test";
}

@Override
protected Site determineDeploySite() throws MojoExecutionException {
Site site = new Site();
site.setId("test");
site.setUrl("test");
return site;
}
}
}