From 4cc13505aa101c95de3a266546311b7cb5809a90 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 26 Jan 2025 09:39:03 +0100 Subject: [PATCH 1/7] :bug: filter non semantic version tags --- .../sam42r/semver/SemanticReleaseMojo.java | 2 +- .../github/sam42r/semver/model/Version.java | 28 +++++++++++++------ .../sam42r/semver/model/VersionTest.java | 17 +++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/semver-maven-plugin/src/main/java/io/github/sam42r/semver/SemanticReleaseMojo.java b/semver-maven-plugin/src/main/java/io/github/sam42r/semver/SemanticReleaseMojo.java index b86aad7..cdaf6b6 100644 --- a/semver-maven-plugin/src/main/java/io/github/sam42r/semver/SemanticReleaseMojo.java +++ b/semver-maven-plugin/src/main/java/io/github/sam42r/semver/SemanticReleaseMojo.java @@ -219,7 +219,7 @@ private LatestReleaseInfo getLatestRelease(SCMProvider scmProvider) throws MojoE var tags = scmProvider.readTags(); var commits = scmProvider.readCommits(null); - var latestTagOpt = tags.max(new TagVersionComparator(tagFormat)); + var latestTagOpt = tags.filter(t -> Version.matchesPattern(t.getName(), tagFormat)).max(new TagVersionComparator(tagFormat)); var latestCommitOpt = latestTagOpt.map(Tag::getCommitId) .or(() -> commits.min(Comparator.comparing(Commit::getTimestamp)).map(Commit::getId)); diff --git a/semver-maven-plugin/src/main/java/io/github/sam42r/semver/model/Version.java b/semver-maven-plugin/src/main/java/io/github/sam42r/semver/model/Version.java index 3e8e1f0..6ca6a63 100644 --- a/semver-maven-plugin/src/main/java/io/github/sam42r/semver/model/Version.java +++ b/semver-maven-plugin/src/main/java/io/github/sam42r/semver/model/Version.java @@ -4,12 +4,14 @@ import org.apache.commons.text.StringSubstitutor; import java.util.Map; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author Sam42R */ @Getter +@EqualsAndHashCode @NoArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE) public final class Version { @@ -42,6 +44,18 @@ public static Version of(@NonNull String version) { } public static Version of(@NonNull String version, @NonNull String tagFormat) { + var matcher = getMatcher(version, tagFormat); + if (matcher.find()) { + return new Version( + Integer.parseInt(matcher.group(Type.MAJOR.name())), + Integer.parseInt(matcher.group(Type.MINOR.name())), + Integer.parseInt(matcher.group(Type.PATCH.name())), + tagFormat); + } + throw new IllegalArgumentException("Could not create version for '%s' with tag format '%s'".formatted(version, tagFormat)); + } + + private static Matcher getMatcher(String version, String tagFormat) { if (!tagFormat.contains(VERSION_PLACEHOLDER)) { throw new IllegalArgumentException( "Given tag format '%s' does not contain required version placeholder '%s'".formatted( @@ -55,15 +69,7 @@ public static Version of(@NonNull String version, @NonNull String tagFormat) { PLACEHOLDER_SUFFIX); var pattern = Pattern.compile(regex); - var matcher = pattern.matcher(version); - if (matcher.find()) { - return new Version( - Integer.parseInt(matcher.group(Type.MAJOR.name())), - Integer.parseInt(matcher.group(Type.MINOR.name())), - Integer.parseInt(matcher.group(Type.PATCH.name())), - tagFormat); - } - throw new IllegalArgumentException("Could not create version for '%s' with tag format '%s'".formatted(version, tagFormat)); + return pattern.matcher(version); } public static Version of(@NonNull int major, int minor, int patch) { @@ -74,6 +80,10 @@ public static Version of(@NonNull int major, int minor, int patch, String tagFor return new Version(major, minor, patch, tagFormat); } + public static boolean matchesPattern(String version, String tagFormat) { + return getMatcher(version, tagFormat).find(); + } + public void increment(@NonNull Type type) { if (Type.MAJOR.equals(type)) { this.major++; diff --git a/semver-maven-plugin/src/test/java/io/github/sam42r/semver/model/VersionTest.java b/semver-maven-plugin/src/test/java/io/github/sam42r/semver/model/VersionTest.java index 4173e58..958c35c 100644 --- a/semver-maven-plugin/src/test/java/io/github/sam42r/semver/model/VersionTest.java +++ b/semver-maven-plugin/src/test/java/io/github/sam42r/semver/model/VersionTest.java @@ -2,6 +2,8 @@ import org.junit.jupiter.api.Test; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -52,4 +54,19 @@ void shouldIncrement() { assertThat(actual).hasToString("2.0.0"); assertThat(actual.toTag()).isEqualTo("v2.0.0"); } + + @Test + void shouldFilterInvalidVersions() { + var tags = List.of("v0.0.1", "r-0-0-1", "test", "v0.0.2"); + + var actual = tags.stream() + .filter(v -> Version.matchesPattern(v, Version.TAG_FORMAT_DEFAULT)) + .map(Version::of) + .toList(); + + assertThat(actual).containsExactlyInAnyOrder( + Version.of(0,0,1), + Version.of(0,0,2) + ); + } } From 76d66f1dee5a124b7c68a9e797cab1800cd57b90 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 26 Jan 2025 09:42:23 +0100 Subject: [PATCH 2/7] :bug: trim commit messages and dates --- .../sam42r/semver/scm/AbstractScmProvider.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java b/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java index 951d562..c27a53d 100644 --- a/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java +++ b/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java @@ -20,6 +20,9 @@ import org.apache.maven.scm.repository.ScmRepositoryException; import java.nio.file.Path; +import java.util.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; @@ -69,19 +72,25 @@ protected AbstractScmProvider( var changeLogScmRequest = new ChangeLogScmRequest(repository, new ScmFileSet(path.toFile())); if (fromCommitId != null) { changeLogScmRequest.setStartRevision(new ScmRevision(fromCommitId)); + } else { + changeLogScmRequest.setStartDate(Date.from(Instant.EPOCH)); } if (toCommitId != null) { changeLogScmRequest.setEndRevision(new ScmRevision(toCommitId)); } var changeLogScmResult = scmManager.changeLog(changeLogScmRequest); + if (!changeLogScmResult.isSuccess()) { + throw new SCMException(changeLogScmResult.getProviderMessage(), new IllegalStateException(changeLogScmResult.getCommandOutput())); + } return changeLogScmResult.getChangeLog().getChangeSets().stream() + .filter(v -> v.getComment() != null) .map(v -> Commit.builder() .id(v.getRevision()) - .timestamp(v.getDate().toInstant()) + .timestamp(v.getDate().toInstant().truncatedTo(ChronoUnit.SECONDS)) .author(v.getAuthor()) - .message(v.getComment()) + .message(v.getComment().trim()) .build()); } catch (ScmException e) { throw new SCMException(e); From ccd78f1beab2a8f94ba347f0095ddf275117f89c Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 26 Jan 2025 09:44:17 +0100 Subject: [PATCH 3/7] :bug: include file path to tag detection (for scm without explicit tag support, e.g svn) --- .../semver/scm/AbstractScmProvider.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java b/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java index c27a53d..3f6b6df 100644 --- a/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java +++ b/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java @@ -35,6 +35,8 @@ abstract class AbstractScmProvider implements SCMProvider { private static final Predicate hasTag = changeSet -> changeSet.getTags() != null && !changeSet.getTags().isEmpty(); + private static final Predicate hasTagPath = changeSet -> changeSet.getFiles() != null && + !changeSet.getFiles().isEmpty() && changeSet.getFiles().get(0).getName().contains("/tags/"); private final Path path; private final String username; @@ -103,12 +105,17 @@ protected AbstractScmProvider( var repository = getScmRepository(); var changeLogScmRequest = new ChangeLogScmRequest(repository, new ScmFileSet(path.toFile())); + changeLogScmRequest.setStartDate(Date.from(Instant.EPOCH)); + var changeLogScmResult = scmManager.changeLog(changeLogScmRequest); + if (!changeLogScmResult.isSuccess()) { + throw new SCMException(changeLogScmResult.getProviderMessage(), new IllegalStateException(changeLogScmResult.getCommandOutput())); + } return changeLogScmResult.getChangeLog().getChangeSets().stream() - .filter(hasTag) + .filter(hasTag.or(hasTagPath)) .map(v -> Tag.builder() - .name(v.getTags().get(0)) + .name(getTag(v)) .commitId(v.getRevision()) .build()); } catch (ScmException e) { @@ -116,6 +123,16 @@ protected AbstractScmProvider( } } + private String getTag(@NonNull ChangeSet changeSet) { + if (hasTag.test(changeSet)) { + return changeSet.getTags().get(0); + } else if (hasTagPath.test(changeSet)) { + var filename = changeSet.getFiles().get(0).getName(); + return filename.substring(filename.lastIndexOf("/") + 1); + } + return null; + } + @Override public void addFile(@NonNull Path file) throws SCMException { try { From cd2174090c173bfcbc1ee08a5b40fc47166e5f41 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 26 Jan 2025 19:36:38 +0100 Subject: [PATCH 4/7] :recycle: replace assert with detailed exception --- .../semver/scm/AbstractScmProvider.java | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java b/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java index 3f6b6df..6fffb84 100644 --- a/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java +++ b/semver-scm-api/src/main/java/io/github/sam42r/semver/scm/AbstractScmProvider.java @@ -7,10 +7,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; -import org.apache.maven.scm.ChangeSet; -import org.apache.maven.scm.ScmException; -import org.apache.maven.scm.ScmFileSet; -import org.apache.maven.scm.ScmRevision; +import org.apache.maven.scm.*; import org.apache.maven.scm.command.changelog.ChangeLogScmRequest; import org.apache.maven.scm.manager.BasicScmManager; import org.apache.maven.scm.manager.NoSuchScmProviderException; @@ -20,9 +17,10 @@ import org.apache.maven.scm.repository.ScmRepositoryException; import java.nio.file.Path; -import java.util.Date; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Comparator; +import java.util.Date; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; @@ -62,6 +60,10 @@ protected AbstractScmProvider( this.scmManager.setScmProvider(providerType, provider); } + protected Path getFileBase() { + return getPath(); + } + @Override public @NonNull Stream readCommits(String fromCommitId) throws SCMException { return readCommits(fromCommitId, null); @@ -82,9 +84,7 @@ protected AbstractScmProvider( } var changeLogScmResult = scmManager.changeLog(changeLogScmRequest); - if (!changeLogScmResult.isSuccess()) { - throw new SCMException(changeLogScmResult.getProviderMessage(), new IllegalStateException(changeLogScmResult.getCommandOutput())); - } + check(changeLogScmResult); return changeLogScmResult.getChangeLog().getChangeSets().stream() .filter(v -> v.getComment() != null) @@ -108,9 +108,7 @@ protected AbstractScmProvider( changeLogScmRequest.setStartDate(Date.from(Instant.EPOCH)); var changeLogScmResult = scmManager.changeLog(changeLogScmRequest); - if (!changeLogScmResult.isSuccess()) { - throw new SCMException(changeLogScmResult.getProviderMessage(), new IllegalStateException(changeLogScmResult.getCommandOutput())); - } + check(changeLogScmResult); return changeLogScmResult.getChangeLog().getChangeSets().stream() .filter(hasTag.or(hasTagPath)) @@ -138,8 +136,8 @@ public void addFile(@NonNull Path file) throws SCMException { try { var repository = getScmRepository(); - var addScmResult = scmManager.add(repository, new ScmFileSet(path.toFile(), file.toFile())); - assert addScmResult.isSuccess(); + var addScmResult = scmManager.add(repository, new ScmFileSet(getFileBase().toFile(), getFileBase().relativize(file).toFile())); + check(addScmResult); } catch (ScmException e) { throw new SCMException(e); } @@ -151,9 +149,9 @@ public void addFile(@NonNull Path file) throws SCMException { var repository = getScmRepository(); var checkInScmResult = scmManager.checkIn(repository, new ScmFileSet(path.toFile()), message); - var scmRevision = checkInScmResult.getScmRevision(); + check(checkInScmResult); - return readCommits(scmRevision, scmRevision).findFirst().orElseThrow(); + return readCommits(null).max(Comparator.comparing(Commit::getTimestamp)).orElseThrow(); } catch (ScmException e) { throw new SCMException(e); } @@ -164,8 +162,8 @@ public void addFile(@NonNull Path file) throws SCMException { try { var repository = getScmRepository(); - var tagScmResult = scmManager.tag(repository, new ScmFileSet(path.toFile()), name); - assert tagScmResult.isSuccess(); + var tagScmResult = scmManager.tag(repository, new ScmFileSet(getFileBase().toFile()), name); + check(tagScmResult); return readTags().filter(v -> name.equals(v.getName())).findFirst().orElseThrow(); } catch (ScmException e) { @@ -189,5 +187,11 @@ protected ScmRepository getScmRepository() throws SCMException { } } + private void check(@NonNull ScmResult scmResult) throws SCMException { + if (!scmResult.isSuccess()) { + throw new SCMException(scmResult.getProviderMessage(), new IllegalStateException(scmResult.getCommandOutput())); + } + } + protected abstract Optional getRemoteUrl() throws SCMException; } From 1be2936937ccd485d01c2cab0f5e0eaa63636465 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 2 Feb 2025 18:53:01 +0100 Subject: [PATCH 5/7] :technologist: add abstract test and scm status assert --- semver-scm-api/pom.xml | 17 ++++++++ .../semver/scm/AbstractScmProviderTest.java | 33 ++++++++++++++ .../semver/scm/StatusScmResultAssert.java | 43 +++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 semver-scm-api/src/test/java/io/github/sam42r/semver/scm/AbstractScmProviderTest.java create mode 100644 semver-scm-api/src/test/java/io/github/sam42r/semver/scm/StatusScmResultAssert.java diff --git a/semver-scm-api/pom.xml b/semver-scm-api/pom.xml index 9b3e4b5..dc55bd4 100644 --- a/semver-scm-api/pom.xml +++ b/semver-scm-api/pom.xml @@ -42,4 +42,21 @@ test + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + test-jar + + + + + + diff --git a/semver-scm-api/src/test/java/io/github/sam42r/semver/scm/AbstractScmProviderTest.java b/semver-scm-api/src/test/java/io/github/sam42r/semver/scm/AbstractScmProviderTest.java new file mode 100644 index 0000000..2e25543 --- /dev/null +++ b/semver-scm-api/src/test/java/io/github/sam42r/semver/scm/AbstractScmProviderTest.java @@ -0,0 +1,33 @@ +package io.github.sam42r.semver.scm; + +import io.github.sam42r.semver.scm.AbstractScmProvider; +import io.github.sam42r.semver.scm.SCMException; +import io.github.sam42r.semver.scm.SCMProvider; +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.command.status.StatusScmResult; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; + +public abstract class AbstractScmProviderTest { + + @TempDir + protected Path tempDirectory; + protected SCMProvider uut; + + protected StatusScmResult status() throws SCMException { + if (uut instanceof AbstractScmProvider abstractScmProvider) { + var path = abstractScmProvider.getPath(); + var scmManager = abstractScmProvider.getScmManager(); + var scmRepository = abstractScmProvider.getScmRepository(); + + try { + return scmManager.status(scmRepository, new ScmFileSet(path.toFile())); + } catch (ScmException e) { + throw new SCMException(e); + } + } + throw new IllegalArgumentException("Invalid SCMProvider '%s'".formatted(uut.getClass())); + } +} diff --git a/semver-scm-api/src/test/java/io/github/sam42r/semver/scm/StatusScmResultAssert.java b/semver-scm-api/src/test/java/io/github/sam42r/semver/scm/StatusScmResultAssert.java new file mode 100644 index 0000000..bbc1e92 --- /dev/null +++ b/semver-scm-api/src/test/java/io/github/sam42r/semver/scm/StatusScmResultAssert.java @@ -0,0 +1,43 @@ +package io.github.sam42r.semver.scm; + +import org.apache.maven.scm.ScmFile; +import org.apache.maven.scm.command.status.StatusScmResult; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; + +public class StatusScmResultAssert extends AbstractAssert { + + protected StatusScmResultAssert(StatusScmResult statusScmResult) { + super(statusScmResult, StatusScmResultAssert.class); + } + + public static StatusScmResultAssert assertThat(StatusScmResult statusScmResult) { + return new StatusScmResultAssert(statusScmResult); + } + + public StatusScmResultAssert isSuccess() { + if (!actual.isSuccess()) { + failWithMessage("Expected result to be successful"); + } + return this; + } + + public StatusScmResultAssert hasChangedFiles() { + if (actual.getChangedFiles().isEmpty()) { + failWithMessage("Expected changed files to be not empty"); + } + return this; + } + + public StatusScmResultAssert hasNoChangedFiles() { + if (!actual.getChangedFiles().isEmpty()) { + failWithMessage("Expected changed files to be empty"); + } + return this; + } + + public StatusScmResultAssert containsChangedFilesExactlyInAnyOrder(ScmFile ... changedFiles) { + Assertions.assertThat(actual.getChangedFiles()).containsExactlyInAnyOrder(changedFiles); + return this; + } +} From 87a3f8657896978ffe3d688ef087e3724cef7d5c Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 23 Feb 2025 18:02:07 +0100 Subject: [PATCH 6/7] :memo: add common scm api information --- semver-scm-api/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 semver-scm-api/README.md diff --git a/semver-scm-api/README.md b/semver-scm-api/README.md new file mode 100644 index 0000000..1cba498 --- /dev/null +++ b/semver-scm-api/README.md @@ -0,0 +1,11 @@ +# SCM provider distribution + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'pie1': 'lightsteelblue', 'pie2': 'goldenrod', 'pie3': 'ghostwhite'}}}%% +pie title "SCM provider ditribution" + "git": 70 + "svn": 15 + "others": 15 +``` + +source: [worldmetrics.org](https://worldmetrics.org/version-control-systems-industry-statistics/) \ No newline at end of file From fa3d664d20136d051a2f1030f0bde46db543926c Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 11 Aug 2024 19:45:01 +0200 Subject: [PATCH 7/7] :sparkles: (scm) add subversion provider --- docs/external-resources.md | 1 + semver-scm-subversion/README.md | 58 ++++++ semver-scm-subversion/pom.xml | 48 +++++ .../github/sam42r/semver/scm/SvnProvider.java | 87 +++++---- .../sam42r/semver/scm/SvnProviderFactory.java | 20 +++ ...ithub.sam42r.semver.scm.SCMProviderFactory | 2 +- .../sam42r/semver/scm/SvnProviderTest.java | 165 ++++++++++++++++++ .../src/test/resources/junit.dmp | 97 ++++++++++ 8 files changed, 439 insertions(+), 39 deletions(-) create mode 100644 semver-scm-subversion/README.md create mode 100644 semver-scm-subversion/src/main/java/io/github/sam42r/semver/scm/SvnProviderFactory.java create mode 100644 semver-scm-subversion/src/test/java/io/github/sam42r/semver/scm/SvnProviderTest.java create mode 100644 semver-scm-subversion/src/test/resources/junit.dmp diff --git a/docs/external-resources.md b/docs/external-resources.md index 1880b24..1844884 100644 --- a/docs/external-resources.md +++ b/docs/external-resources.md @@ -18,6 +18,7 @@ ## Third Party - [Mustache Templates](https://github.com/mustache/mustache.github.com) - [JGit Authentication](https://www.codeaffine.com/2014/12/09/jgit-authentication/) +- [SVNKit](https://svnkit.com/documentation.html) ## Others - [Git documentation](https://git-scm.com/doc) diff --git a/semver-scm-subversion/README.md b/semver-scm-subversion/README.md new file mode 100644 index 0000000..f5b69a2 --- /dev/null +++ b/semver-scm-subversion/README.md @@ -0,0 +1,58 @@ +# SCM Subversion Provider + +TODO general notes and limitations + +## Public SVN repositories +> Info: Seems that working copies checked out with HTTPS causing HTTP-400 + +### Apache-Software-Foundation (ASF) +The [Apache-Software-Foundation](https://www.apache.org/) serves [SVN](https://svn.apache.org/) repositories +for all their [projects](https://www.apache.org/index.html#projects-list). + +```bash +$ svn checkout https://svn.apache.org/repos/asf/avro avro-trunk +``` + +### Source-Forge (SF) +[SourceForge](https://sourceforge.net/) serves [SVN](https://sourceforge.net/p/forge/documentation/SVN%20Overview/) repositories +for a lot of open source projects. + +HTTPS +```bash +$ svn checkout https://svn.code.sf.net/p/keepass/code/trunk keepass-code +``` +SVN +```bash +svn checkout svn://svn.code.sf.net/p/keepass/code/trunk keepass-code +``` + +## Local SVN repositories + +[SVN Chaet Sheet](https://www.perforce.com/blog/vcs/svn-commands-cheat-sheet) + +### Create local repository +```bash +$ svnadmin create sample +``` + +### Create working copy +```bash +$ svn checkout file:///$PWD/sample working +``` + +### Create test repository +```bash +$ svnadmin create junit +$ svn checkout file:///$PWD/junit junit-working +$ cd junit-working +$ mkdir trunk +$ mkdir tags +$ mkdir branches +$ echo "# JUnit" > trunk/README.md +$ svn add branches tags trunk +$ svn commit -m "chore: initial project setup" +$ svn copy trunk/ tags/v0.0.1 +$ svn commit -m "release: v0.0.1" +$ cd .. +$ svnadmin dump junit > junit.dmp +``` \ No newline at end of file diff --git a/semver-scm-subversion/pom.xml b/semver-scm-subversion/pom.xml index a4e0ea4..3de7c21 100644 --- a/semver-scm-subversion/pom.xml +++ b/semver-scm-subversion/pom.xml @@ -29,10 +29,58 @@ lombok provided + + + com.google.code.maven-scm-provider-svnjava + maven-scm-provider-svnjava + 2.3.0 + + org.apache.commons commons-lang3 + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + io.github.sam42r + semver-scm-api + ${project.version} + tests + test + + + + + + org.tmatesoft.svnkit + svnkit + 1.10.11 + + + + org.apache.maven.scm + maven-scm-api + 2.1.0 + + + + org.apache.maven.scm + maven-scm-privider-svn-commons + 2.1.0 + + + + diff --git a/semver-scm-subversion/src/main/java/io/github/sam42r/semver/scm/SvnProvider.java b/semver-scm-subversion/src/main/java/io/github/sam42r/semver/scm/SvnProvider.java index 2786c1c..f71edda 100644 --- a/semver-scm-subversion/src/main/java/io/github/sam42r/semver/scm/SvnProvider.java +++ b/semver-scm-subversion/src/main/java/io/github/sam42r/semver/scm/SvnProvider.java @@ -1,66 +1,77 @@ package io.github.sam42r.semver.scm; -import io.github.sam42r.semver.scm.model.Commit; -import io.github.sam42r.semver.scm.model.Remote; -import io.github.sam42r.semver.scm.model.Tag; import lombok.NonNull; -import org.apache.commons.lang3.NotImplementedException; +import lombok.extern.slf4j.Slf4j; +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFile; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.ScmFileStatus; +import org.apache.maven.scm.provider.svn.svnjava.SvnJavaScmProvider; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.SVNRevision; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.stream.Stream; +import java.util.Optional; /** - * {@link SCMProvider} for subversion. - * TODO implement + * {@link SCMProvider} for subversion.
+ * Subversion's Recommended Repository Layout + * is required (trunk, branches, tags). * * @author Sam42R */ -public class SvnProvider implements SCMProviderFactory, SCMProvider { +@Slf4j +public class SvnProvider extends AbstractScmProvider { - private static final String PROVIDER_NAME = "Subversion"; - - @Override - public @NonNull String getProviderName() { - return PROVIDER_NAME; - } - - @Override - public @NonNull SvnProvider getInstance(@NonNull Path path, String username, String password) { - throw new NotImplementedException(); - } - - @Override - public @NonNull Stream readCommits(String fromCommitId) throws SCMException { - throw new SCMException(new NotImplementedException()); + public SvnProvider(Path path, String username, String password) { + super("javasvn", new SvnJavaScmProvider(), path, username, password); } @Override - public @NonNull Stream readTags() throws SCMException { - throw new SCMException(new NotImplementedException()); + protected Path getFileBase() { + return getPath().resolve("trunk"); } @Override public void addFile(@NonNull Path file) throws SCMException { - throw new SCMException(new NotImplementedException()); - } - - @Override - public @NonNull Commit commit(@NonNull String message) throws SCMException { - throw new SCMException(new NotImplementedException()); + try { + var statusResult = getScmManager().status(getScmRepository(), new ScmFileSet(getPath().toFile(), getPath().relativize(file).toFile())); + var scmFileStatus = statusResult.getChangedFiles().stream() + .filter(v -> file.endsWith(Path.of(v.getPath()))) + .map(ScmFile::getStatus) + .findAny() + .orElseThrow(); + if (ScmFileStatus.UNKNOWN.equals(scmFileStatus)) { + super.addFile(file); + } + } catch (ScmException e) { + throw new SCMException(e); + } } @Override - public @NonNull Tag tag(@NonNull String name) throws SCMException { - throw new SCMException(new NotImplementedException()); + protected Optional getRemoteUrl() throws SCMException { + if (isWorkingCopy()) { + try { + var info = SVNClientManager.newInstance().getWCClient().doInfo(getPath().toFile(), SVNRevision.HEAD); + return Optional.ofNullable(info.getRepositoryRootURL().toDecodedString()); + } catch (SVNException e) { + throw new SCMException(e); + } + } else { + return Optional.of("file://%s".formatted(getPath().toUri().getPath())); + } } @Override - public String push(boolean force) throws SCMException { - throw new SCMException(new NotImplementedException()); + public @NonNull String push(boolean force) throws SCMException { + // SVN does NOT support push on demand (no distributed version control system) + return "changes already pushed to repository"; } - @Override - public @NonNull Remote getRemote() throws SCMException { - throw new SCMException(new NotImplementedException()); + private boolean isWorkingCopy() { + return Files.exists(getPath().resolve(".svn")); } } diff --git a/semver-scm-subversion/src/main/java/io/github/sam42r/semver/scm/SvnProviderFactory.java b/semver-scm-subversion/src/main/java/io/github/sam42r/semver/scm/SvnProviderFactory.java new file mode 100644 index 0000000..f1fae3c --- /dev/null +++ b/semver-scm-subversion/src/main/java/io/github/sam42r/semver/scm/SvnProviderFactory.java @@ -0,0 +1,20 @@ +package io.github.sam42r.semver.scm; + +import lombok.NonNull; + +import java.nio.file.Path; + +public class SvnProviderFactory implements SCMProviderFactory { + + private static final String PROVIDER_NAME = "Subversion"; + + @Override + public @NonNull String getProviderName() { + return PROVIDER_NAME; + } + + @Override + public @NonNull SvnProvider getInstance(@NonNull Path path, String username, String password) { + return new SvnProvider(path, username, password); + } +} diff --git a/semver-scm-subversion/src/main/resources/META-INF/services/io.github.sam42r.semver.scm.SCMProviderFactory b/semver-scm-subversion/src/main/resources/META-INF/services/io.github.sam42r.semver.scm.SCMProviderFactory index 9606ef9..ca61c70 100644 --- a/semver-scm-subversion/src/main/resources/META-INF/services/io.github.sam42r.semver.scm.SCMProviderFactory +++ b/semver-scm-subversion/src/main/resources/META-INF/services/io.github.sam42r.semver.scm.SCMProviderFactory @@ -1 +1 @@ -io.github.sam42r.semver.scm.SvnProvider +io.github.sam42r.semver.scm.SvnProviderFactory diff --git a/semver-scm-subversion/src/test/java/io/github/sam42r/semver/scm/SvnProviderTest.java b/semver-scm-subversion/src/test/java/io/github/sam42r/semver/scm/SvnProviderTest.java new file mode 100644 index 0000000..bb317e8 --- /dev/null +++ b/semver-scm-subversion/src/test/java/io/github/sam42r/semver/scm/SvnProviderTest.java @@ -0,0 +1,165 @@ +package io.github.sam42r.semver.scm; + +import io.github.sam42r.semver.scm.model.Commit; +import io.github.sam42r.semver.scm.model.Remote; +import io.github.sam42r.semver.scm.model.Tag; +import org.apache.maven.scm.ScmFile; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.ScmFileStatus; +import org.apache.maven.scm.manager.BasicScmManager; +import org.apache.maven.scm.provider.svn.svnjava.SvnJavaScmProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.tmatesoft.svn.core.wc.SVNClientManager; + +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Instant; + +import static io.github.sam42r.semver.scm.StatusScmResultAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +class SvnProviderTest extends AbstractScmProviderTest { + + @BeforeEach + void setup() throws Exception { + var repository = tempDirectory.resolve("repository"); + var workingCopy = tempDirectory.resolve("working-copy"); + initializeLocal(repository, workingCopy); + + uut = new SvnProviderFactory().getInstance(workingCopy, null, null); + } + + @Test + void shouldReadCommits() throws Exception { + var actual = uut.readCommits(null); + + assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields("timestamp").containsExactly( + Commit.builder() + .id("1") + .timestamp(Instant.EPOCH) // ignored + .author("r_ric") + .message("chore: initial project setup") + .build(), + Commit.builder() + .id("2") + .timestamp(Instant.EPOCH) + .author("r_ric") + .message("release: v0.0.1") + .build() + ); + } + + @Test + void shouldReadTags() throws Exception { + var actual = uut.readTags(); + + assertThat(actual).containsExactly( + Tag.builder() + .name("v0.0.1") + .commitId("2") + .build() + ); + } + + @Test + void shouldAddAndCommitFiles() throws Exception { + // update file + var readme = ((AbstractScmProvider) uut).getFileBase().resolve("README.md"); + Files.writeString(readme, "JUnit test", StandardOpenOption.APPEND); + // create file (e.g. Changelog.md) + var changelog = ((AbstractScmProvider) uut).getFileBase().resolve("Changelog.md"); + Files.writeString(changelog, "# Changelog", StandardOpenOption.CREATE_NEW); + + uut.addFile(readme); + uut.addFile(changelog); + + assertThat(status()) + .isSuccess() + .containsChangedFilesExactlyInAnyOrder( + new ScmFile("trunk/Changelog.md", ScmFileStatus.ADDED), + new ScmFile("trunk/README.md", ScmFileStatus.MODIFIED) + ); + + var actual = uut.commit("test: commit files to repository"); + assertThat(actual.getId()).isNotNull(); + assertThat(actual.getMessage()).isEqualTo("test: commit files to repository"); + + assertThat(status()) + .isSuccess() + .hasNoChangedFiles(); + } + + @Test + void shouldTag() throws SCMException { + var actual = uut.readTags(); + assertThat(actual).hasSize(1); + + uut.tag("v0.0.2"); + + actual = uut.readTags(); + + assertThat(actual) + .hasSize(2) + .contains(Tag.builder() + .name("v0.0.2") + .commitId("3") + .build() + ); + } + + @Test + void shouldGetRemote() throws Exception { + var workingCopy = tempDirectory.resolve("working-copy-remote"); + checkoutWorkingCopyRemote(URI.create("https://svn.code.sf.net/p/keepass/code/trunk"), workingCopy); + + uut = new SvnProviderFactory().getInstance(workingCopy, null, null); + + var actual = uut.getRemote(); + + assertThat(actual).isEqualTo( + Remote.builder() + .url("https://svn.code.sf.net/p/keepass/code") + .scheme("https") + .host("svn.code.sf.net") + .group("p/keepass") + .project("code") + .build() + ); + } + + private void initializeLocal(Path repository, Path workingCopy) throws Exception { + initializeRepositoryLocal(repository); + checkoutWorkingCopyLocal(repository, workingCopy); + } + + private void initializeRepositoryLocal(Path repository) throws Exception { + var svnAdminClient = SVNClientManager.newInstance().getAdminClient(); + try (var inputStream = SvnProviderTest.class.getClassLoader().getResourceAsStream("junit.dmp")) { + svnAdminClient.doCreateRepository(repository.toFile(), null, true, true); + svnAdminClient.doLoad(repository.toFile(), inputStream); + } + } + + private void checkoutWorkingCopyLocal(Path repository, Path workingCopy) throws Exception { + var scmManager = new BasicScmManager(); + scmManager.setScmProvider("javasvn", new SvnJavaScmProvider()); + + var scmRepository = scmManager.makeScmRepository("scm:javasvn:%s".formatted(repository.toUri())); + + var checkOutResult = scmManager.checkOut(scmRepository, new ScmFileSet(workingCopy.toFile())); + assertThat(checkOutResult.isSuccess()).isTrue(); + } + + private void checkoutWorkingCopyRemote(URI repository, Path workingCopy) throws Exception { + var scmManager = new BasicScmManager(); + scmManager.setScmProvider("javasvn", new SvnJavaScmProvider()); + + var scmRepository = scmManager.makeScmRepository("scm:javasvn:%s".formatted(repository)); + + var checkOutResult = scmManager.checkOut(scmRepository, new ScmFileSet(workingCopy.toFile())); + assertThat(checkOutResult.isSuccess()).isTrue(); + } +} diff --git a/semver-scm-subversion/src/test/resources/junit.dmp b/semver-scm-subversion/src/test/resources/junit.dmp new file mode 100644 index 0000000..aa69766 --- /dev/null +++ b/semver-scm-subversion/src/test/resources/junit.dmp @@ -0,0 +1,97 @@ +SVN-fs-dump-format-version: 2 + +UUID: 2ff3aad0-4ff7-d14d-a396-6c0b991586e8 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2025-01-26T09:00:46.354900Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 128 +Content-length: 128 + +K 10 +svn:author +V 5 +r_ric +K 8 +svn:date +V 27 +2025-01-26T09:04:49.946444Z +K 7 +svn:log +V 28 +chore: initial project setup +PROPS-END + +Node-path: branches +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: tags +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk/README.md +Node-kind: file +Node-action: add +Text-content-md5: 818a729d088a42248bfd42bf240afd54 +Text-content-sha1: 6a4e24dc1e2e832583b4d70a751caa73978ca611 +Prop-content-length: 10 +Text-content-length: 8 +Content-length: 18 + +PROPS-END +# JUnit + + +Revision-number: 2 +Prop-content-length: 115 +Content-length: 115 + +K 10 +svn:author +V 5 +r_ric +K 8 +svn:date +V 27 +2025-01-26T09:06:28.901810Z +K 7 +svn:log +V 15 +release: v0.0.1 +PROPS-END + +Node-path: tags/v0.0.1 +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 1 +Node-copyfrom-path: trunk + +