This is an automated email from the ASF dual-hosted git repository. royteeuwen pushed a commit to branch feature/SLING-13253-list in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git
commit b18e14b36ce68a7e48b96b10ae7589682a3efd38 Author: Roy Teeuwen <[email protected]> AuthorDate: Sat Jul 4 15:02:19 2026 +0200 SLING-13253 - release list: include open Nexus staging repositories Extends 'release list' to also show open Nexus staging repositories and adds the RepositoryService/StagingRepository support the post-vote release commands build on. Part of splitting PR #28 into per-command changes. --- .../sling/cli/impl/nexus/RepositoryService.java | 253 +++++++++++++++++++-- .../sling/cli/impl/nexus/StagingRepository.java | 14 ++ .../apache/sling/cli/impl/release/ListCommand.java | 7 +- .../org/apache/sling/cli/impl/nexus/MockNexus.java | 10 + .../nexus/RepositoryContentListingHandler.java | 58 +++++ .../cli/impl/nexus/RepositoryServiceTest.java | 138 ++++++++++- .../cli/impl/nexus/StagingRepositoryTest.java | 43 ++++ .../sling/cli/impl/release/ListCommandTest.java | 99 ++++++++ .../1.0.0/adapter-annotations-1.0.0.pom | 40 ++++ .../sling/adapter-annotations/1.0.0/listing.json | 14 ++ .../apache/sling/adapter-annotations/listing.json | 9 + .../orgapachesling-3/org/apache/sling/listing.json | 9 + src/test/resources/nexus/staging-repositories.json | 24 ++ 13 files changed, 692 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/apache/sling/cli/impl/nexus/RepositoryService.java b/src/main/java/org/apache/sling/cli/impl/nexus/RepositoryService.java index 31946aa..7f7d55a 100644 --- a/src/main/java/org/apache/sling/cli/impl/nexus/RepositoryService.java +++ b/src/main/java/org/apache/sling/cli/impl/nexus/RepositoryService.java @@ -31,6 +31,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -50,6 +52,9 @@ import org.apache.commons.io.IOUtils; import org.apache.http.HttpHeaders; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.sling.cli.impl.ComponentContextHelper; import org.apache.sling.cli.impl.http.HttpClientFactory; @@ -93,7 +98,6 @@ public class RepositoryService { return this.withStagingRepositories(reader -> { Gson gson = new Gson(); return gson.fromJson(reader, StagingRepositories.class).getData().stream() - .filter(r -> r.getType() == Status.closed) .filter(r -> r.getRepositoryId().startsWith(REPOSITORY_PREFIX)) .collect(Collectors.toList()); }); @@ -112,6 +116,65 @@ public class RepositoryService { }); } + public StagingRepository findAny(int stagingRepositoryId) throws IOException { + return this.withStagingRepositories(reader -> { + Gson gson = new Gson(); + return gson.fromJson(reader, StagingRepositories.class).getData().stream() + .filter(r -> r.getRepositoryId().startsWith(REPOSITORY_PREFIX)) + .filter(r -> r.getRepositoryId().endsWith("-" + stagingRepositoryId)) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException("No repository found with id " + stagingRepositoryId)); + }); + } + + public void close(StagingRepository repository) throws IOException { + executeBulkAction("close", repository.getRepositoryId(), Collections.emptyMap()); + } + + public void close(StagingRepository repository, String description) throws IOException { + executeBulkAction("close", repository.getRepositoryId(), Collections.singletonMap("description", description)); + } + + public void promote(StagingRepository repository) throws IOException { + // Nexus "Release": move the staged artifacts to the release repository (which syncs to Maven + // Central) and drop the staging repository afterwards. This matches the payload the Nexus UI + // sends. Note there is no targetRepositoryId — that field is for build-promotion profiles and + // is rejected with HTTP 400 by the bulk/promote endpoint. + executeBulkAction( + "promote", repository.getRepositoryId(), Collections.singletonMap("autoDropAfterRelease", true)); + } + + public void drop(StagingRepository repository) throws IOException { + executeBulkAction("delete", repository.getRepositoryId(), Collections.emptyMap()); + } + + private void executeBulkAction(String action, String repositoryId, Map<String, Object> extraData) + throws IOException { + try (CloseableHttpClient client = httpClientFactory.newClient()) { + HttpPost post = new HttpPost(nexusUrlPrefix + "/service/local/staging/bulk/" + action); + post.addHeader(HttpHeaders.ACCEPT, CONTENT_TYPE_JSON); + + Map<String, Object> data = new HashMap<>(); + data.put("stagedRepositoryIds", Collections.singletonList(repositoryId)); + data.put("description", ""); + data.putAll(extraData); + + JsonObject body = new JsonObject(); + body.add("data", new Gson().toJsonTree(data)); + + post.setEntity(new StringEntity(body.toString(), ContentType.APPLICATION_JSON)); + + try (CloseableHttpResponse response = client.execute(post)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 201) { + throw new IOException( + "Unexpected status " + statusCode + " for staging bulk/" + action + " on " + repositoryId); + } + } + } + } + private <T> T withStagingRepositories(Function<InputStreamReader, T> function) throws IOException { try (CloseableHttpClient client = httpClientFactory.newClient()) { HttpGet get = newGet("/service/local/staging/profile_repositories"); @@ -232,42 +295,186 @@ public class RepositoryService { } public Set<Release> getReleases(StagingRepository stagingRepository) throws IOException { - Set<Release> releases = new HashSet<>(); + List<PomCoordinates> poms = new ArrayList<>(); getArtifacts(stagingRepository).stream() .filter(artifact -> "pom".equals(artifact.getType())) .forEach(pom -> { try { - XPath xPath = xPathFactory.newXPath(); processArtifactStream(pom, stream -> { - try { - DocumentBuilder builder = builderFactory.newDocumentBuilder(); - Document xmlDocument = builder.parse(stream); - String name = (String) xPath.compile("/project/name/text()") - .evaluate(xmlDocument, XPathConstants.STRING); - String version = (String) xPath.compile("/project/version/text()") - .evaluate(xmlDocument, XPathConstants.STRING); - try { - releases.addAll(Release.fromString(name + " " + version)); - } catch (IllegalArgumentException e) { - LOGGER.error( - String.format( - "Unable to determine a valid release from '%s %s'", name, version), - e); - } - } catch (ParserConfigurationException - | SAXException - | XPathExpressionException - | IOException e) { - LOGGER.error(String.format("Unable to process artifact %s.", pom), e); + PomCoordinates coordinates = parsePom(stream, pom.toString()); + if (coordinates != null) { + poms.add(coordinates); } }); } catch (IOException e) { LOGGER.error(String.format("Unable to process artifact %s.", pom), e); } }); + return toReleases(poms); + } + + /** + * Determines the releases contained in a staging repository by browsing its content directly, + * rather than relying on the Nexus Lucene search index. This works for <em>open</em> staging + * repositories too, whereas {@link #getReleases(StagingRepository)} only sees repositories that + * have already been closed and indexed. + */ + public Set<Release> getReleasesFromContent(StagingRepository repository) throws IOException { + List<PomCoordinates> poms = new ArrayList<>(); + try (CloseableHttpClient client = httpClientFactory.newClient()) { + List<String> pomPaths = new ArrayList<>(); + collectPomPaths(client, repository.getRepositoryId(), "/org/apache/sling/", pomPaths); + for (String pomPath : pomPaths) { + HttpGet get = + newGet("/service/local/repositories/" + repository.getRepositoryId() + "/content" + pomPath); + try (CloseableHttpResponse response = client.execute(get)) { + if (response.getStatusLine().getStatusCode() != 200) { + continue; + } + try (InputStream stream = response.getEntity().getContent()) { + PomCoordinates coordinates = parsePom(stream, pomPath); + if (coordinates != null) { + poms.add(coordinates); + } + } + } + } + } + return toReleases(poms); + } + + private void collectPomPaths(CloseableHttpClient client, String repositoryId, String path, List<String> pomPaths) + throws IOException { + HttpGet get = newGet("/service/local/repositories/" + repositoryId + "/content" + path); + try (CloseableHttpResponse response = client.execute(get)) { + if (response.getStatusLine().getStatusCode() != 200) { + return; + } + try (InputStream content = response.getEntity().getContent(); + InputStreamReader reader = new InputStreamReader(content)) { + JsonArray data = new JsonParser() + .parse(reader) + .getAsJsonObject() + .get("data") + .getAsJsonArray(); + for (JsonElement element : data) { + JsonObject entry = element.getAsJsonObject(); + String relativePath = entry.get("relativePath").getAsString(); + if (entry.get("leaf").getAsBoolean()) { + if (entry.get("text").getAsString().endsWith(".pom")) { + pomPaths.add(relativePath); + } + } else { + collectPomPaths(client, repositoryId, relativePath, pomPaths); + } + } + } + } + } + + /** + * The Maven coordinates and name of a single staged POM, with versions resolved against the + * {@code <parent>} when a module inherits them. + */ + record PomCoordinates( + String name, String groupId, String artifactId, String version, String packaging, String parentKey) { + + /** {@code groupId:artifactId:version} identifying this artifact, or {@code null} if incomplete. */ + String ownKey() { + return coordinateKey(groupId, artifactId, version); + } + } + + private static String coordinateKey(String groupId, String artifactId, String version) { + if (groupId == null || groupId.isBlank() || artifactId == null || artifactId.isBlank()) { + return null; + } + return groupId + ":" + artifactId + ":" + version; + } + + private PomCoordinates parsePom(InputStream stream, String pomLabel) { + try { + XPath xPath = xPathFactory.newXPath(); + DocumentBuilder builder = builderFactory.newDocumentBuilder(); + Document doc = builder.parse(stream); + String name = xpathString(xPath, doc, "/project/name/text()"); + String artifactId = xpathString(xPath, doc, "/project/artifactId/text()"); + String groupId = xpathString(xPath, doc, "/project/groupId/text()"); + String version = xpathString(xPath, doc, "/project/version/text()"); + String packaging = xpathString(xPath, doc, "/project/packaging/text()"); + String parentGroupId = xpathString(xPath, doc, "/project/parent/groupId/text()"); + String parentArtifactId = xpathString(xPath, doc, "/project/parent/artifactId/text()"); + String parentVersion = xpathString(xPath, doc, "/project/parent/version/text()"); + // In a multi-module reactor a child module's POM frequently omits <groupId>/<version> and + // inherits them from its <parent>; fall back so such modules are not skipped. + if (groupId == null || groupId.isBlank()) { + groupId = parentGroupId; + } + if (version == null || version.isBlank()) { + version = parentVersion; + } + if (packaging == null || packaging.isBlank()) { + packaging = "jar"; + } + return new PomCoordinates( + name, + groupId, + artifactId, + version, + packaging, + coordinateKey(parentGroupId, parentArtifactId, parentVersion)); + } catch (ParserConfigurationException | SAXException | XPathExpressionException | IOException e) { + LOGGER.error(String.format("Unable to process pom %s.", pomLabel), e); + return null; + } + } + + private static String xpathString(XPath xPath, Document doc, String expression) throws XPathExpressionException { + return (String) xPath.compile(expression).evaluate(doc, XPathConstants.STRING); + } + + /** + * Reduces the staged POMs to the set of releases they represent. + * + * <p>When the staged POMs form a single multi-module reactor — i.e. there is exactly one staged + * {@code pom}-packaging aggregator that is the {@code <parent>} of other staged modules and is + * itself the top of the staged hierarchy — the release is that aggregator alone (the reactor is + * one logical release, tracked by one JIRA version). Otherwise (a single module, or several + * independent modules staged and voted together) every staged module becomes its own release. + */ + static Set<Release> toReleases(List<PomCoordinates> poms) { + Set<String> stagedKeys = + poms.stream().map(PomCoordinates::ownKey).filter(k -> k != null).collect(Collectors.toSet()); + List<PomCoordinates> aggregators = poms.stream() + .filter(p -> "pom".equals(p.packaging())) + // is the parent of at least one other staged module + .filter(p -> p.ownKey() != null + && poms.stream() + .anyMatch(other -> other != p && p.ownKey().equals(other.parentKey()))) + // and is the root of the staged hierarchy (its own parent is not itself staged, e.g. it + // is the shared org.apache.sling parent POM, which is not part of the release) + .filter(p -> p.parentKey() == null || !stagedKeys.contains(p.parentKey())) + .toList(); + if (aggregators.size() == 1) { + return buildReleases(aggregators.get(0)); + } + Set<Release> releases = new HashSet<>(); + for (PomCoordinates pom : poms) { + releases.addAll(buildReleases(pom)); + } return Set.copyOf(releases); } + private static Set<Release> buildReleases(PomCoordinates pom) { + try { + return new HashSet<>(Release.fromString(pom.name() + " " + pom.version())); + } catch (IllegalArgumentException e) { + LOGGER.error( + String.format("Unable to determine a valid release from '%s %s'", pom.name(), pom.version()), e); + return Set.of(); + } + } + private void downloadFileFromRepository( @NotNull StagingRepository repository, @NotNull CloseableHttpClient client, diff --git a/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java b/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java index af7d0c0..efcfbb1 100644 --- a/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java +++ b/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java @@ -33,6 +33,7 @@ public class StagingRepository { protected String repositoryId; protected String repositoryURI; protected Status type; + protected String userId; public String getDescription() { return description; @@ -42,6 +43,19 @@ public class StagingRepository { this.description = description; } + /** + * Returns the id of the committer who staged this repository, as reported by Nexus. + * + * @return the staging user id + */ + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + public String getRepositoryId() { return repositoryId; } diff --git a/src/main/java/org/apache/sling/cli/impl/release/ListCommand.java b/src/main/java/org/apache/sling/cli/impl/release/ListCommand.java index 43cc36a..c75ac0d 100644 --- a/src/main/java/org/apache/sling/cli/impl/release/ListCommand.java +++ b/src/main/java/org/apache/sling/cli/impl/release/ListCommand.java @@ -57,7 +57,12 @@ public class ListCommand implements Command { try { repositoryService .list() - .forEach(r -> logger.info("{}\t{}", r.getRepositoryId(), cleanupNewlines(r.getDescription()))); + .forEach(r -> logger.info( + "{}\t[{}]\t{}\t{}", + r.getRepositoryId(), + r.getType(), + r.getUserId(), + cleanupNewlines(r.getDescription()))); return CommandLine.ExitCode.OK; } catch (IOException e) { logger.warn("Failed executing command", e); diff --git a/src/test/java/org/apache/sling/cli/impl/nexus/MockNexus.java b/src/test/java/org/apache/sling/cli/impl/nexus/MockNexus.java index c4e76b5..8e84352 100644 --- a/src/test/java/org/apache/sling/cli/impl/nexus/MockNexus.java +++ b/src/test/java/org/apache/sling/cli/impl/nexus/MockNexus.java @@ -65,7 +65,17 @@ public class MockNexus extends ExternalResource { List<HttpExchangeHandler> handlers = new ArrayList<>(); handlers.add(new QueryLuceneIndexHandler()); handlers.add(new StagingRepositoriesHandler()); + handlers.add(new RepositoryContentListingHandler()); handlers.add(new RepositoryContentHandler()); + // staging bulk actions (close/promote/delete) are POSTs answered with HTTP 201 + handlers.add(ex -> { + if ("POST".equals(ex.getRequestMethod()) + && ex.getRequestURI().getPath().startsWith("/service/local/staging/bulk/")) { + ex.sendResponseHeaders(201, -1); + return true; + } + return false; + }); handlers.add(ex -> { ex.sendResponseHeaders(400, -1); return true; diff --git a/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryContentListingHandler.java b/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryContentListingHandler.java new file mode 100644 index 0000000..4401d81 --- /dev/null +++ b/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryContentListingHandler.java @@ -0,0 +1,58 @@ +/* + * 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.sling.cli.impl.nexus; + +import java.io.IOException; + +import com.sun.net.httpserver.HttpExchange; +import org.apache.sling.cli.impl.http.HttpExchangeHandler; + +/** + * Serves the Nexus repository content-browsing API used by + * {@code RepositoryService#getReleasesFromContent}. Requests to + * {@code /service/local/repositories/<id>/content/<path>} are mapped to classpath resources under + * {@code /nexus-content/<id>/<path>}; directory paths (ending in {@code /}) are served from a + * sibling {@code listing.json} fixture, raw files are served as-is. + */ +public class RepositoryContentListingHandler implements HttpExchangeHandler { + + private static final String PREFIX = "/service/local/repositories/"; + + @Override + public boolean tryHandle(HttpExchange ex) throws IOException { + String path = ex.getRequestURI().getPath(); + if (!"GET".equals(ex.getRequestMethod()) || !path.startsWith(PREFIX)) { + return false; + } + int contentIdx = path.indexOf("/content"); + if (contentIdx < 0) { + return false; + } + String repoAndAfter = path.substring(PREFIX.length(), contentIdx); // e.g. orgapachesling-3 + String relative = path.substring(contentIdx + "/content".length()); // e.g. /org/apache/sling/ + String resource; + if (relative.endsWith("/")) { + resource = "/nexus-content/" + repoAndAfter + relative + "listing.json"; + } else { + resource = "/nexus-content/" + repoAndAfter + relative; + } + serveFileFromClasspath(ex, resource); + return true; + } +} diff --git a/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryServiceTest.java b/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryServiceTest.java index 55e387b..97f4ce1 100644 --- a/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryServiceTest.java +++ b/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryServiceTest.java @@ -96,8 +96,10 @@ public class RepositoryServiceTest { @Test public void testRepositoryList() throws IOException { List<StagingRepository> stagingRepositories = repositoryService.list(); - assertEquals(2, stagingRepositories.size()); - Set<String> repositoriesIds = new HashSet<>(Set.of("orgapachesling-0", "orgapachesling-1")); + // Includes both closed repositories and the open (not yet closed) one, so that newly + // staged repositories show up before they have been closed for voting. + assertEquals(3, stagingRepositories.size()); + Set<String> repositoriesIds = new HashSet<>(Set.of("orgapachesling-0", "orgapachesling-1", "orgapachesling-2")); for (StagingRepository repository : stagingRepositories) { assertEquals( "http://localhost:" + nexus.getBoundPort() + "/content/repositories/" @@ -168,6 +170,138 @@ public class RepositoryServiceTest { assertEquals("Sling Adapter Annotations 1.0.0", release.getFullName()); } + @Test + public void testGetReleasesFromContent() throws IOException { + // browses the repository content tree directly (no Lucene index), recursing into directories + // and parsing every .pom leaf it finds + StagingRepository repository = new StagingRepository(); + repository.setRepositoryId("orgapachesling-3"); + Set<Release> releases = repositoryService.getReleasesFromContent(repository); + assertEquals(1, releases.size()); + assertEquals( + "Sling Adapter Annotations 1.0.0", releases.iterator().next().getFullName()); + } + + @Test + public void testFindAnyReturnsOpenRepository() throws IOException { + // findAny does not require the repository to be closed, so the open orgapachesling-2 resolves + StagingRepository repository = repositoryService.findAny(2); + assertNotNull(repository); + assertEquals("orgapachesling-2", repository.getRepositoryId()); + } + + @Test + public void testFindAnyUnknownThrows() { + try { + repositoryService.findAny(999); + fail("Expected an IllegalArgumentException for an unknown repository id."); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("999")); + } catch (IOException e) { + fail("Unexpected IOException."); + } + } + + @Test + public void testPromote() throws IOException { + // exercises the bulk-action POST path; MockNexus answers staging bulk actions with HTTP 201 + repositoryService.promote(getStagingRepository()); + } + + @Test + public void testCloseWithDescription() throws IOException { + repositoryService.close(getStagingRepository(), "voting"); + } + + @Test + public void testDrop() throws IOException { + repositoryService.drop(getStagingRepository()); + } + + @Test + public void testParsePomInheritsGroupIdAndVersionFromParent() throws Exception { + // a child module POM omitting <groupId>/<version>/<packaging> inherits them from <parent> + String pomXml = "<project>" + + "<name>Apache Sling Child</name>" + + "<artifactId>child</artifactId>" + + "<parent>" + + "<groupId>org.apache.sling</groupId>" + + "<artifactId>parent</artifactId>" + + "<version>3.4.5</version>" + + "</parent>" + + "</project>"; + RepositoryService.PomCoordinates coordinates = invokeParsePom(pomXml); + assertNotNull(coordinates); + assertEquals("org.apache.sling", coordinates.groupId()); + assertEquals("3.4.5", coordinates.version()); + assertEquals("jar", coordinates.packaging()); + } + + @Test + public void testParsePomInvalidXmlReturnsNull() throws Exception { + // malformed XML triggers the error branch which logs and returns null + assertEquals(null, invokeParsePom("this is not xml")); + } + + @Test + public void testToReleasesSkipsInvalidReleaseNames() { + // a POM whose name/version cannot be parsed into a Release yields an empty result rather than + // throwing, exercising the buildReleases error branch + Set<Release> releases = RepositoryService.toReleases( + List.of(pom("", "org.apache.sling", "org.apache.sling.broken", "", "jar", PARENT_POM))); + assertTrue(releases.isEmpty()); + } + + private RepositoryService.PomCoordinates invokeParsePom(String pomXml) throws Exception { + java.lang.reflect.Method method = + RepositoryService.class.getDeclaredMethod("parsePom", InputStream.class, String.class); + method.setAccessible(true); + try (InputStream stream = new java.io.ByteArrayInputStream(pomXml.getBytes(StandardCharsets.UTF_8))) { + return (RepositoryService.PomCoordinates) method.invoke(repositoryService, stream, "test.pom"); + } + } + + @Test + public void testToReleasesSingleModule() { + // a POM's <name> is the bare component name; the version comes from <version> + Set<Release> releases = RepositoryService.toReleases(List.of( + pom("Apache Sling Foo", "org.apache.sling", "org.apache.sling.foo", "1.2.0", "jar", PARENT_POM))); + assertEquals(Set.of("Apache Sling Foo 1.2.0"), fullNames(releases)); + } + + @Test + public void testToReleasesIndependentModulesAreAllReturned() { + // several unrelated modules staged + voted together: each keeps its own release/JIRA version + Set<Release> releases = RepositoryService.toReleases(List.of( + pom("Apache Sling Foo", "org.apache.sling", "org.apache.sling.foo", "1.2.0", "jar", PARENT_POM), + pom("Apache Sling Bar", "org.apache.sling", "org.apache.sling.bar", "2.0.0", "jar", PARENT_POM))); + assertEquals(Set.of("Apache Sling Foo 1.2.0", "Apache Sling Bar 2.0.0"), fullNames(releases)); + } + + @Test + public void testToReleasesReactorCollapsesToAggregator() { + // a real reactor: a pom-packaging aggregator that is the parent of the staged child modules. + // The whole reactor is one logical release -> only the aggregator's name is returned. + String reactorKey = "org.apache.sling:org.apache.sling.reactor:1.0.0"; + Set<Release> releases = RepositoryService.toReleases(List.of( + pom("Apache Sling Reactor", "org.apache.sling", "org.apache.sling.reactor", "1.0.0", "pom", PARENT_POM), + // children inherit groupId/version from the reactor parent + pom("Apache Sling Reactor Core", "org.apache.sling", "core", "1.0.0", "jar", reactorKey), + pom("Apache Sling Reactor API", "org.apache.sling", "api", "1.0.0", "jar", reactorKey))); + assertEquals(Set.of("Apache Sling Reactor 1.0.0"), fullNames(releases)); + } + + private static final String PARENT_POM = "org.apache.sling:sling:66"; + + private static RepositoryService.PomCoordinates pom( + String name, String groupId, String artifactId, String version, String packaging, String parentKey) { + return new RepositoryService.PomCoordinates(name, groupId, artifactId, version, packaging, parentKey); + } + + private static Set<String> fullNames(Set<Release> releases) { + return releases.stream().map(Release::getFullName).collect(java.util.stream.Collectors.toSet()); + } + private StagingRepository getStagingRepository() { StagingRepository stagingRepository = new StagingRepository(); stagingRepository.setRepositoryId("orgapachesling-0"); diff --git a/src/test/java/org/apache/sling/cli/impl/nexus/StagingRepositoryTest.java b/src/test/java/org/apache/sling/cli/impl/nexus/StagingRepositoryTest.java new file mode 100644 index 0000000..5b46ab2 --- /dev/null +++ b/src/test/java/org/apache/sling/cli/impl/nexus/StagingRepositoryTest.java @@ -0,0 +1,43 @@ +/* + * 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.sling.cli.impl.nexus; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class StagingRepositoryTest { + + @Test + public void testAccessors() { + StagingRepository repository = new StagingRepository(); + repository.setDescription("Apache Sling Foo 1.0.0"); + repository.setRepositoryId("orgapachesling-7"); + repository.setRepositoryURI("https://repository.apache.org/content/repositories/orgapachesling-7"); + repository.setUserId("johndoe"); + repository.setType(StagingRepository.Status.closed); + + assertEquals("Apache Sling Foo 1.0.0", repository.getDescription()); + assertEquals("orgapachesling-7", repository.getRepositoryId()); + assertEquals( + "https://repository.apache.org/content/repositories/orgapachesling-7", repository.getRepositoryURI()); + assertEquals("johndoe", repository.getUserId()); + assertEquals(StagingRepository.Status.closed, repository.getType()); + } +} diff --git a/src/test/java/org/apache/sling/cli/impl/release/ListCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/ListCommandTest.java new file mode 100644 index 0000000..24551d0 --- /dev/null +++ b/src/test/java/org/apache/sling/cli/impl/release/ListCommandTest.java @@ -0,0 +1,99 @@ +/* + * 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.sling.cli.impl.release; + +import java.util.Arrays; +import java.util.Collections; + +import org.apache.sling.cli.impl.Command; +import org.apache.sling.cli.impl.junit.LogCapture; +import org.apache.sling.cli.impl.nexus.RepositoryService; +import org.apache.sling.cli.impl.nexus.StagingRepository; +import org.apache.sling.testing.mock.osgi.junit.OsgiContext; +import org.junit.Rule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class ListCommandTest { + + @Rule + public OsgiContext osgiContext = new OsgiContext(); + + @Rule + public final LogCapture logCapture = new LogCapture(ListCommand.class); + + @Test + public void testList() throws Exception { + StagingRepository repo1 = mock(StagingRepository.class); + when(repo1.getRepositoryId()).thenReturn("orgapachesling-1"); + when(repo1.getUserId()).thenReturn("jagger"); + when(repo1.getDescription()).thenReturn("Apache Sling CLI Test 1.0.0"); + + StagingRepository repo2 = mock(StagingRepository.class); + when(repo2.getRepositoryId()).thenReturn("orgapachesling-2"); + when(repo2.getUserId()).thenReturn("richards"); + when(repo2.getDescription()).thenReturn("Apache Sling CLI Test 2.0.0"); + + RepositoryService repositoryService = mock(RepositoryService.class); + when(repositoryService.list()).thenReturn(Arrays.asList(repo1, repo2)); + osgiContext.registerService(repositoryService); + + Command list = createCommand(); + assertEquals(0, (int) list.call()); + + assertTrue(logCapture.containsMessage("orgapachesling-1")); + assertTrue(logCapture.containsMessage("orgapachesling-2")); + // the staging user is shown alongside the id/state/description + assertTrue(logCapture.containsMessage("jagger")); + assertTrue(logCapture.containsMessage("richards")); + assertTrue(logCapture.containsMessage("Apache Sling CLI Test 1.0.0")); + assertTrue(logCapture.containsMessage("Apache Sling CLI Test 2.0.0")); + } + + @Test + public void testListCollapsesNewlines() throws Exception { + StagingRepository repo = mock(StagingRepository.class); + when(repo.getRepositoryId()).thenReturn("orgapachesling-1"); + when(repo.getDescription()).thenReturn("line1\nline2"); + + RepositoryService repositoryService = mock(RepositoryService.class); + when(repositoryService.list()).thenReturn(Collections.singletonList(repo)); + osgiContext.registerService(repositoryService); + + Command list = createCommand(); + assertEquals(0, (int) list.call()); + + assertTrue(logCapture.containsMessage("line1 line2")); + } + + private Command createCommand() { + ListCommand listCommand = spy(new ListCommand()); + osgiContext.registerInjectActivateService(listCommand); + Command result = osgiContext.getService(Command.class); + assertTrue( + "Expected to retrieve the ListCommand from the mocked OSGi environment.", + result instanceof ListCommand); + return result; + } +} diff --git a/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/1.0.0/adapter-annotations-1.0.0.pom b/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/1.0.0/adapter-annotations-1.0.0.pom new file mode 100644 index 0000000..b9a3013 --- /dev/null +++ b/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/1.0.0/adapter-annotations-1.0.0.pom @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <parent> + <artifactId>sling</artifactId> + <groupId>org.apache.sling</groupId> + <version>12</version> + <relativePath>../../parent/pom.xml</relativePath> + </parent> + <groupId>org.apache.sling</groupId> + <artifactId>adapter-annotations</artifactId> + <version>1.0.0</version> + <name>Sling Adapter Annotations</name> + <description>Annotations used to generate Sling Adapter metadata</description> + + <scm> + <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/adapter-annotations-1.0.0</connection> + <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/adapter-annotations-1.0.0</developerConnection> + <url>http://svn.apache.org/viewvc/sling/tags/adapter-annotations-1.0.0</url> + </scm> +</project> \ No newline at end of file diff --git a/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/1.0.0/listing.json b/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/1.0.0/listing.json new file mode 100644 index 0000000..4a26249 --- /dev/null +++ b/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/1.0.0/listing.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "relativePath": "/org/apache/sling/adapter-annotations/1.0.0/adapter-annotations-1.0.0.pom", + "text": "adapter-annotations-1.0.0.pom", + "leaf": true + }, + { + "relativePath": "/org/apache/sling/adapter-annotations/1.0.0/adapter-annotations-1.0.0.jar", + "text": "adapter-annotations-1.0.0.jar", + "leaf": true + } + ] +} diff --git a/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/listing.json b/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/listing.json new file mode 100644 index 0000000..f925f34 --- /dev/null +++ b/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/adapter-annotations/listing.json @@ -0,0 +1,9 @@ +{ + "data": [ + { + "relativePath": "/org/apache/sling/adapter-annotations/1.0.0/", + "text": "1.0.0", + "leaf": false + } + ] +} diff --git a/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/listing.json b/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/listing.json new file mode 100644 index 0000000..71a203e --- /dev/null +++ b/src/test/resources/nexus-content/orgapachesling-3/org/apache/sling/listing.json @@ -0,0 +1,9 @@ +{ + "data": [ + { + "relativePath": "/org/apache/sling/adapter-annotations/", + "text": "adapter-annotations", + "leaf": false + } + ] +} diff --git a/src/test/resources/nexus/staging-repositories.json b/src/test/resources/nexus/staging-repositories.json index 1d418f7..3b644c0 100644 --- a/src/test/resources/nexus/staging-repositories.json +++ b/src/test/resources/nexus/staging-repositories.json @@ -47,6 +47,30 @@ "releaseRepositoryName": "Releases", "notifications" : 0, "transitioning" : false + }, + { + "profileId" : "6a443ac86e2212", + "profileName" : "org.apache.sling", + "profileType" : "repository", + "repositoryId" : "orgapachesling-2", + "type" : "open", + "policy" : "release", + "userId" : "radu", + "userAgent" : "Apache-Maven/3.6.1 (Java 1.8.0_222; Mac OS X 10.14.6)", + "ipAddress" : "127.0.0.1", + "repositoryURI" : "{nexusHost}/content/repositories/orgapachesling-2", + "created" : "2019-09-13T00:00:00.000Z", + "createdDate" : "Fri Sep 13 00:00:00 UTC 2019", + "createdTimestamp" : 1568332800000, + "updated" : "2019-09-13T00:00:00.000Z", + "updatedDate" : "Fri Sep 13 00:00:00 UTC 2019", + "updatedTimestamp" : 1568332800000, + "description" : "Implicitly created (auto staging).", + "provider" : "maven2", + "releaseRepositoryId" : "releases", + "releaseRepositoryName": "Releases", + "notifications" : 0, + "transitioning" : false } ] }
