From 56586a7dc28c2629da3c2ebeb7151a730485947f Mon Sep 17 00:00:00 2001 From: Jonathing Date: Sat, 2 Aug 2025 15:40:12 -0400 Subject: [PATCH] 0.6 - Dedicated Gradle plugin, Changelog from CLI, API restructuring (#2) --- .github/workflows/plugin-publish.yml | 30 ++ .github/workflows/publish.yml | 7 +- .gitversion | 3 + build.gradle | 3 + gradle-plugin/build.gradle | 168 +++++++++++ gradle-plugin/gradle.properties | 8 + gradle-plugin/settings.gradle | 45 +++ .../gradle/GitVersionExtension.java | 115 +++++++ .../gradle/GitVersionExtensionImpl.groovy | 86 ++++++ .../gradle/GitVersionExtensionInternal.java | 144 +++++++++ .../gitversion/gradle/GitVersionPlugin.java | 25 ++ .../gitversion/gradle/GitVersionProblems.java | 29 ++ .../gitversion/gradle/GitVersionTools.java | 20 ++ .../gradle/GitVersionValueSource.groovy | 91 ++++++ .../gitversion/gradle/Util.java | 75 +++++ .../gradle/changelog/ChangelogExtension.java | 48 +++ .../changelog/ChangelogExtensionImpl.groovy | 93 ++++++ .../changelog/ChangelogExtensionInternal.java | 19 ++ .../gradle/changelog/ChangelogPlugin.java | 21 ++ .../gradle/changelog/ChangelogProblems.java | 12 + .../gradle/changelog/ChangelogUtils.groovy | 138 +++++++++ .../gradle/changelog/CopyChangelog.java | 69 +++++ .../gradle/changelog/GenerateChangelog.java | 104 +++++++ .../gitversion/gradle/changelog/Util.java | 6 + .../gradle/changelog/package-info.java | 4 + .../gitversion/gradle/package-info.java | 4 + settings.gradle | 5 + src/main/java/module-info.java | 1 + .../minecraftforge/gitver/api/GitVersion.java | 282 +++--------------- .../gitver/api/GitVersionConfig.java | 47 +-- .../net/minecraftforge/gitver/cli/Main.java | 45 ++- .../gitver/internal/CommitCountProvider.java | 3 +- .../gitver/internal/GitUtils.java | 4 +- .../gitver/internal/GitVersionConfigImpl.java | 41 ++- .../internal/GitVersionConfigInternal.java | 25 ++ .../internal/GitVersionExceptionInternal.java | 2 +- .../gitver/internal/GitVersionImpl.java | 122 ++++---- .../gitver/internal/GitVersionInternal.java | 270 +++++++++++++++++ .../minecraftforge/gitver/internal/Util.java | 41 ++- 39 files changed, 1879 insertions(+), 376 deletions(-) create mode 100644 .github/workflows/plugin-publish.yml create mode 100644 .gitversion create mode 100644 gradle-plugin/build.gradle create mode 100644 gradle-plugin/gradle.properties create mode 100644 gradle-plugin/settings.gradle create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtension.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtensionImpl.groovy create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtensionInternal.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionPlugin.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionProblems.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionTools.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionValueSource.groovy create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/Util.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtension.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtensionImpl.groovy create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtensionInternal.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogPlugin.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogProblems.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogUtils.groovy create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/CopyChangelog.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/GenerateChangelog.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/Util.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/package-info.java create mode 100644 gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/package-info.java create mode 100644 src/main/java/net/minecraftforge/gitver/internal/GitVersionConfigInternal.java create mode 100644 src/main/java/net/minecraftforge/gitver/internal/GitVersionInternal.java diff --git a/.github/workflows/plugin-publish.yml b/.github/workflows/plugin-publish.yml new file mode 100644 index 0000000..aac6d54 --- /dev/null +++ b/.github/workflows/plugin-publish.yml @@ -0,0 +1,30 @@ +name: Publish + +on: + push: + branches: [ 'master' ] + paths: + - 'gradle-plugin/**' + - '!.github/workflows/**' + - '!README.md' + +permissions: + contents: read + +jobs: + build: + uses: MinecraftForge/SharedActions/.github/workflows/gradle.yml@v0 + with: + java: 17 + gradle_tasks: ':gradle-plugin:check :gradle-plugin:publish :gradle-plugin:publishPlugins' + artifact_name: 'gitversion-gradle' + project_path: 'gradle-plugin' + secrets: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + PROMOTE_ARTIFACT_WEBHOOK: ${{ secrets.PROMOTE_ARTIFACT_WEBHOOK }} + PROMOTE_ARTIFACT_USERNAME: ${{ secrets.PROMOTE_ARTIFACT_USERNAME }} + PROMOTE_ARTIFACT_PASSWORD: ${{ secrets.PROMOTE_ARTIFACT_PASSWORD }} + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c8651fe..0854ccf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,11 +2,10 @@ name: Publish on: push: - branches: [ "master" ] + branches: [ 'master' ] paths-ignore: - '.github/workflows/**' - 'README.md' - - 'settings.gradle' permissions: contents: read @@ -16,8 +15,8 @@ jobs: uses: MinecraftForge/SharedActions/.github/workflows/gradle.yml@v0 with: java: 17 - gradle_tasks: "check publish" - artifact_name: "gitversion" + gradle_tasks: 'check publish' + artifact_name: 'gitversion' secrets: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} PROMOTE_ARTIFACT_WEBHOOK: ${{ secrets.PROMOTE_ARTIFACT_WEBHOOK }} diff --git a/.gitversion b/.gitversion new file mode 100644 index 0000000..bec8727 --- /dev/null +++ b/.gitversion @@ -0,0 +1,3 @@ +[gradlePlugin] +path = "gradle-plugin" +tag = "gradle" diff --git a/build.gradle b/build.gradle index 26ecec5..a8f6041 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,9 @@ dependencies { // Config implementation libs.toml + // JSON Output + implementation libs.gson + // CLI shadowOnly libs.slf4j shadowOnly libs.jopt diff --git a/gradle-plugin/build.gradle b/gradle-plugin/build.gradle new file mode 100644 index 0000000..e3462b5 --- /dev/null +++ b/gradle-plugin/build.gradle @@ -0,0 +1,168 @@ +//import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.attributes.plugin.GradlePluginApiVersion + +plugins { + id 'java-gradle-plugin' + id 'groovy' + id 'idea' + id 'eclipse' + id 'maven-publish' + alias libs.plugins.licenser + alias libs.plugins.gradleutils + alias libs.plugins.javadoc.links + alias libs.plugins.plugin.publish + //alias libs.plugins.shadow +} + +final projectDisplayName = 'Git Version Gradle Plugin' +final projectArtifactId = base.archivesName = 'gitversion-gradle' +description = 'A gradle plugin for using Git Version to manage version numbers.' +group = 'net.minecraftforge' +version = gitversion.tagOffset + +println "Version: $version" + +java { + toolchain.languageVersion = JavaLanguageVersion.of(17) + withSourcesJar() + withJavadocJar() +} + +configurations { + named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME) { + // Fixes a conflict between Git Version's shadowed SLF4J from JGit and Gradle's own loggers + exclude group: 'org.slf4j', module: 'slf4j-api' + } + + // Applies the "Gradle Plugin API Version" attribute to configuration + // This was added in Gradle 7, gives consumers useful errors if they are on an old version + def applyGradleVersionAttribute = { Configuration configuration -> + configuration.attributes { + attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, objects.named(GradlePluginApiVersion, libs.versions.gradle.get())) + } + } + + named('apiElements', applyGradleVersionAttribute) + named('runtimeElements', applyGradleVersionAttribute) + //named('shadowRuntimeElements', applyGradleVersionAttribute) +} + +dependencies { + // Gradle API + compileOnly libs.gradle + compileOnly libs.nulls + + runtimeOnly libs.gradleutils.plugin + compileOnly libs.gradleutils.binary + + // Git Version + implementation libs.gitver + implementation libs.gson +} + +/* +// Removes local Gradle API from compileOnly. This is a workaround for bugged plugins. +// TODO [GradleUtils][GradleAPI] Remove this once they are fixed. +// Publish Plugin: https://github.com/gradle/plugin-portal-requests/issues/260 +// Shadow: https://github.com/GradleUp/shadow/pull/1422 +afterEvaluate { project -> + project.configurations.named(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME) { compileOnly -> + compileOnly.dependencies.remove project.dependencies.gradleApi() + } +} + */ + +license { + header = rootProject.file('../LICENSE-header.txt') + newLine = false + exclude '**/*.properties' +} + +tasks.named('jar', Jar) { + archiveClassifier = 'thin' +} + +/* +tasks.named('shadowJar', ShadowJar) { + enableAutoRelocation = true + archiveClassifier = null + relocationPrefix = 'net.minecraftforge.gitversion.gradle.shadow' +} + */ + +tasks.withType(GroovyCompile).configureEach { + groovyOptions.optimizationOptions.indy = true +} + +tasks.withType(Javadoc).configureEach { + javadocTool = javaToolchains.javadocToolFor { languageVersion = JavaLanguageVersion.of(24) } + + options { StandardJavadocDocletOptions options -> + options.windowTitle = projectDisplayName + project.version + options.tags 'apiNote:a:API Note:', 'implNote:a:Implementation Note:' + } +} + +changelog { + fromBase() + publishAll = false +} + +gradlePlugin { + website.set gitversion.url + vcsUrl.set gitversion.url + '.git' + + plugins { + register('gitversion') { + id = 'net.minecraftforge.gitversion' + implementationClass = 'net.minecraftforge.gitversion.gradle.GitVersionPlugin' + displayName = projectDisplayName + description = project.description + tags.set(['git', 'version']) + } + + register('changelog') { + id = 'net.minecraftforge.changelog' + implementationClass = 'net.minecraftforge.gitversion.gradle.ChangelogPlugin' + displayName = 'Git Changelog' + description = 'Creates a changelog text file based on git history using GitVersion.' + tags.set(['git', 'changelog']) + } + } +} + +/* +// Allows the thin jar to be published, but won't be considered as the java-runtime variant in the module +// This forces Gradle to use the fat jar when applying the plugin +(components.java as AdhocComponentWithVariants).withVariantsFromConfiguration(configurations.runtimeElements) { + skip() +} + */ + +publishing { + publications.register('pluginMaven', MavenPublication) { + artifactId = projectArtifactId + changelog.publish it + + pom { pom -> + name = projectDisplayName + description = project.description + + gradleutils.pom.setGitHubDetails pom + + licenses { + license gradleutils.pom.licenses.LGPLv2_1 + } + + developers { + developer gradleutils.pom.developers.Jonathing + } + } + } + + repositories { + maven gradleutils.publishingForgeMaven + } +} + +idea.module { downloadSources = downloadJavadoc = true } diff --git a/gradle-plugin/gradle.properties b/gradle-plugin/gradle.properties new file mode 100644 index 0000000..8d5be5b --- /dev/null +++ b/gradle-plugin/gradle.properties @@ -0,0 +1,8 @@ +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.configureondemand=true + +#org.gradle.configuration-cache=true +#org.gradle.configuration-cache.parallel=true + +systemProp.org.gradle.unsafe.suppress-gradle-api=true diff --git a/gradle-plugin/settings.gradle b/gradle-plugin/settings.gradle new file mode 100644 index 0000000..75495ce --- /dev/null +++ b/gradle-plugin/settings.gradle @@ -0,0 +1,45 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0' +} + +rootProject.name = 'gitversion-gradle' + +dependencyResolutionManagement { + repositories { + maven { url = 'https://maven.minecraftforge.net/' } + maven { url = 'https://maven.moddinglegacy.com' } // Gradle API + mavenCentral() + mavenLocal() + } + + versionCatalogs.register('libs') { + plugin 'licenser', 'net.minecraftforge.licenser' version '1.2.0' // https://plugins.gradle.org/plugin/net.minecraftforge.licenser + plugin 'gradleutils', 'net.minecraftforge.gradleutils' version '2.6.0' // https://plugins.gradle.org/plugin/net.minecraftforge.gradleutils + plugin 'javadoc-links', 'io.freefair.javadoc-links' version '8.14' // https://plugins.gradle.org/plugin/io.freefair.javadoc-links + plugin 'plugin-publish', 'com.gradle.plugin-publish' version '1.3.1' // https://plugins.gradle.org/plugin/com.gradle.plugin-publish + plugin 'shadow', 'com.gradleup.shadow' version '9.0.0-rc3' // https://plugins.gradle.org/plugin/com.gradleup.shadow + + //https://repos.moddinglegacy.com/modding-legacy/name/remal/gradle-api/gradle-api/9.0.0/gradle-api-9.0.0.pom + //https://repos.moddinglegacy.com/modding-legacy/#/name/remal/gradle-api/gradle-api/9.0.0/gradle-api-9.0.0.pom + // Gradle API + // TODO [ForgeGradle][FG7][Gradle Api] REMOVE once Gradle publish their own API artifacts + // Original: https://github.com/remal-gradle-api/packages/packages/760197 + // Mirror: https://maven.moddinglegacy.com/#browse/browse:maven-public:name%2Fremal%2Fgradle-api%2Fgradle-api%2F8.14.1 + version 'gradle', '9.0.0' + library 'gradle', 'name.remal.gradle-api', 'gradle-api' versionRef 'gradle' + library 'nulls', 'org.jetbrains', 'annotations' version '26.0.2' + + // GradleUtils + // https://plugins.gradle.org/plugin/net.minecraftforge.gradleutils + version 'gradleutils', '3.0.0-alpha.1' + library 'gradleutils-plugin', 'net.minecraftforge.gradleutils', 'net.minecraftforge.gradleutils.gradle.plugin' versionRef 'gradleutils' + library 'gradleutils-binary', 'net.minecraftforge', 'gradleutils' versionRef 'gradleutils' + bundle 'gradleutils', ['gradleutils-plugin', 'gradleutils-binary'] + + // Git Version + library 'gitver', 'net.minecraftforge', 'gitversion' version '0.6.0-alpha.1' + + // JSON Output + library 'gson', 'com.google.code.gson', 'gson' version '2.13.1' + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtension.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtension.java new file mode 100644 index 0000000..30a240c --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtension.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gitversion.gradle; + +import net.minecraftforge.gitver.api.GitVersionConfig; +import net.minecraftforge.gitver.api.GitVersionException; +import net.minecraftforge.gitver.internal.GitVersionImpl; +import net.minecraftforge.gitver.internal.GitVersionInternal; +import org.gradle.api.file.Directory; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.file.FileSystemLocationProperty; +import org.gradle.api.provider.Provider; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.File; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +// NOTE: See GitVersion +public sealed interface GitVersionExtension permits GitVersionExtensionInternal { + String NAME = "gitversion"; + + + /* VERSIONING */ + + String getTagOffset(); + + String getTagOffsetBranch(); + + String getTagOffsetBranch(String @UnknownNullability ... allowedBranches); + + String getTagOffsetBranch(@UnknownNullability Collection allowedBranches); + + String getMCTagOffsetBranch(@UnknownNullability String mcVersion); + + String getMCTagOffsetBranch(String mcVersion, String... allowedBranches); + + String getMCTagOffsetBranch(String mcVersion, Collection allowedBranches); + + + /* INFO */ + + Info getInfo(); + + @Nullable String getUrl(); + + @NotNullByDefault + sealed interface Info extends Serializable permits GitVersionExtensionInternal.Info { + String getTag(); + + String getOffset(); + + String getHash(); + + String getBranch(); + + String getBranch(boolean versionFriendly); + + String getCommit(); + + String getAbbreviatedId(); + } + + + /* FILTERING */ + + String getTagPrefix(); + + @Unmodifiable Collection getFilters(); + + + /* FILE SYSTEM */ + + Directory getGitDir(); + + Directory getRoot(); + + Directory getProject(); + + String getProjectPath(); + + default Provider getRelativePath(FileSystemLocationProperty file) { + return this.getRelativePath(file.getLocationOnly()); + } + + default Provider getRelativePath(Provider file) { + return file.map(FileSystemLocation::getAsFile).map(this::getRelativePath); + } + + default String getRelativePath(FileSystemLocation file) { + return this.getRelativePath(file.getAsFile()); + } + + String getRelativePath(File file); + + default Provider getRelativePath(boolean fromRoot, FileSystemLocationProperty file) { + return this.getRelativePath(fromRoot, file.getLocationOnly()); + } + + default Provider getRelativePath(boolean fromRoot, Provider file) { + return file.map(f -> this.getRelativePath(fromRoot, f.getAsFile())); + } + + default String getRelativePath(boolean fromRoot, FileSystemLocation file) { + return this.getRelativePath(fromRoot, file.getAsFile()); + } + + String getRelativePath(boolean fromRoot, File file); +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtensionImpl.groovy b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtensionImpl.groovy new file mode 100644 index 0000000..e0292aa --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtensionImpl.groovy @@ -0,0 +1,86 @@ +package net.minecraftforge.gitversion.gradle + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import net.minecraftforge.gradleutils.GenerateActionsWorkflow +import net.minecraftforge.gradleutils.PomUtils +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.publish.maven.MavenPom +import org.jetbrains.annotations.Nullable + +import javax.inject.Inject + +@CompileStatic +@PackageScope abstract class GitVersionExtensionImpl implements GitVersionExtensionInternal { + private final GitVersionProblems problems + private final Property gitversion + + protected abstract @Inject ObjectFactory getObjects() + protected abstract @Inject ProviderFactory getProviders() + + @Inject + GitVersionExtensionImpl(Project project) { + this.problems = this.objects.newInstance(GitVersionProblems) + this.gitversion = this.objects.property(Output).value(GitVersionValueSource.info(project, this.providers)).tap { disallowChanges(); finalizeValueOnRead() } + + project.plugins.withId('net.minecraftforge.gradleutils') { extendGradleUtils(project) } + project.afterEvaluate(this.&finish) + } + + @CompileDynamic + private void finish(Project project) { + project.tasks.withType(GenerateActionsWorkflow).configureEach { task -> + task.gitVersionPresent.set(true) + task.branch.convention(this.providers.provider { this.info.branch }) + task.localPath.convention(this.providers.provider { this.projectPath }) + task.paths.convention(this.providers.provider { this.gitversion.get().subprojectPaths().collect { "!${it}/**".toString() } }) + } + } + + @CompileDynamic + private void extendGradleUtils(Project project) { + final pomUtils = project.extensions.getByType(PomUtils) + pomUtils.metaClass.addRemoteDetails = { MavenPom pom -> + final String url + try { + //noinspection GroovyVariableNotAssigned + return pomUtils.addRemoteDetails(pom, Objects.requireNonNull(this.url)) + } catch (Exception e) { + throw this.problems.pomUtilsGitVersionNoUrl(e) + } + } + } + + @Lazy GitVersionExtension.Info info = { + this.gitversion.get().info() + }() + + @Lazy @Nullable String url = { + this.gitversion.get().url() + }() + + @Lazy String tagPrefix = { + this.gitversion.get().tagPrefix() + }() + + @Lazy Collection filters = { + this.gitversion.get().filters() + }() + + @Lazy Directory gitDir = { + this.objects.directoryProperty().fileValue(new File(this.gitversion.get().gitDirPath())).get() + }() + + @Lazy Directory root = { + this.objects.directoryProperty().fileValue(new File(this.gitversion.get().rootPath())).get() + }() + + @Lazy Directory project = { + this.objects.directoryProperty().fileValue(new File(this.gitversion.get().projectPath())).get() + }() +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtensionInternal.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtensionInternal.java new file mode 100644 index 0000000..8d02e26 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionExtensionInternal.java @@ -0,0 +1,144 @@ +package net.minecraftforge.gitversion.gradle; + +import org.gradle.api.reflect.HasPublicType; +import org.gradle.api.reflect.TypeOf; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.io.File; +import java.io.Serializable; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@NotNullByDefault +non-sealed interface GitVersionExtensionInternal extends GitVersionExtension, HasPublicType { + List DEFAULT_ALLOWED_BRANCHES = List.of("master", "main", "HEAD"); + + @Override + default TypeOf getPublicType() { + return TypeOf.typeOf(GitVersionExtension.class); + } + + + /* VERSIONING */ + + @Override + default String getTagOffset() { + var info = this.getInfo(); + return "%s.%s".formatted(info.getTag(), info.getOffset()); + } + + @Override + default String getTagOffsetBranch() { + return this.getTagOffsetBranch(DEFAULT_ALLOWED_BRANCHES); + } + + @Override + default String getTagOffsetBranch(String @UnknownNullability ... allowedBranches) { + return this.getTagOffsetBranch(Arrays.asList(allowedBranches != null ? allowedBranches : new String[0])); + } + + @Override + default String getTagOffsetBranch(@UnknownNullability Collection allowedBranches) { + allowedBranches = net.minecraftforge.gitver.internal.Util.ensure(allowedBranches); + var version = this.getTagOffset(); + if (allowedBranches.isEmpty()) return version; + + var branch = this.getInfo().getBranch(true); + return branch.isBlank() || allowedBranches.contains(branch) ? version : "%s-%s".formatted(version, branch); + } + + @Override + default String getMCTagOffsetBranch(@UnknownNullability String mcVersion) { + if (mcVersion == null || mcVersion.isBlank()) + return this.getTagOffsetBranch(); + + var allowedBranches = new ArrayList<>(DEFAULT_ALLOWED_BRANCHES); + allowedBranches.add(mcVersion); + allowedBranches.add(mcVersion + ".0"); + allowedBranches.add(mcVersion + ".x"); + allowedBranches.add(mcVersion.substring(0, mcVersion.lastIndexOf('.')) + ".x"); + + return this.getMCTagOffsetBranch(mcVersion, allowedBranches); + } + + @Override + default String getMCTagOffsetBranch(String mcVersion, String... allowedBranches) { + return this.getMCTagOffsetBranch(mcVersion, Arrays.asList(allowedBranches)); + } + + @Override + default String getMCTagOffsetBranch(String mcVersion, Collection allowedBranches) { + return "%s-%s".formatted(mcVersion, this.getTagOffsetBranch(allowedBranches)); + } + + + /* INFO */ + + @NotNullByDefault + record Info( + String getTag, + String getOffset, + String getHash, + String getBranch, + String getCommit, + String getAbbreviatedId + ) implements GitVersionExtension.Info { + @Override + public String getBranch(boolean versionFriendly) { + var branch = this.getBranch(); + if (!versionFriendly || branch.isBlank()) return branch; + + if (branch.startsWith("pulls/")) + branch = "pr" + branch.substring(branch.lastIndexOf('/') + 1); + return branch.replaceAll("[\\\\/]", "-"); + } + } + + + /* FILE SYSTEM */ + + private static String getRelativePath(File root, File file) { + if (root.equals(file)) return ""; + return getRelativePath(root.toPath(), file.toPath()); + } + + private static String getRelativePath(Path root, Path path) { + return root.relativize(path).toString().replace(root.getFileSystem().getSeparator(), "/"); + } + + @Override + default String getProjectPath() { + return getRelativePath(this.getRoot().getAsFile(), this.getProject().getAsFile()); + } + + @Override + default String getRelativePath(File file) { + return this.getRelativePath(false, file); + } + + @Override + default String getRelativePath(boolean fromRoot, File file) { + return getRelativePath(fromRoot ? this.getRoot().getAsFile() : this.getProject().getAsFile(), file); + } + + + /* SERIALIZATION */ + + record Output( + Info info, + @Nullable String url, + + @Nullable String gitDirPath, + @Nullable String rootPath, + @Nullable String projectPath, + + @Nullable String tagPrefix, + List filters, + List subprojectPaths + ) implements Serializable { } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionPlugin.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionPlugin.java new file mode 100644 index 0000000..9068997 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionPlugin.java @@ -0,0 +1,25 @@ +package net.minecraftforge.gitversion.gradle; + +import net.minecraftforge.gradleutils.shared.EnhancedPlugin; +import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; + +import javax.inject.Inject; + +abstract class GitVersionPlugin extends EnhancedPlugin { + static final String NAME = "gitversion"; + static final String DISPLAY_NAME = "Git Version"; + + static final Logger LOGGER = Logging.getLogger(GitVersionPlugin.class); + + @Inject + public GitVersionPlugin() { + super(NAME, DISPLAY_NAME); + } + + @Override + public void setup(Project project) { + project.getExtensions().create(GitVersionExtension.NAME, GitVersionExtensionImpl.class, project); + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionProblems.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionProblems.java new file mode 100644 index 0000000..bdcdbee --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionProblems.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gitversion.gradle; + +import net.minecraftforge.gradleutils.shared.EnhancedProblems; +import org.gradle.api.problems.Severity; + +import javax.inject.Inject; + +abstract class GitVersionProblems extends EnhancedProblems { + @Inject + public GitVersionProblems() { + super(GitVersionPlugin.NAME, GitVersionPlugin.DISPLAY_NAME); + } + + RuntimeException pomUtilsGitVersionNoUrl(Exception e) { + return this.getReporter().throwing(e, id("pomutils-missing-url", "Cannot add POM remote details without URL"), spec -> spec + .details(""" + Cannot add POM remote details using `gradleutils.pom.addRemoteDetails` without the URL. + The containing Git repository may not have a remote.""") + .severity(Severity.ERROR) + .stackLocation() + .solution("Check if the project's containing Git repository has a remote.") + .solution("Manually add the remote URL in `addRemoteDetails`.") + .solution(HELP_MESSAGE)); + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionTools.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionTools.java new file mode 100644 index 0000000..9be5159 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionTools.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gitversion.gradle; + +import net.minecraftforge.gradleutils.shared.Tool; + +/// The package-private constants used throughout ForgeGradle. +public final class GitVersionTools { + private static final String FORGE_MAVEN = "https://maven.minecraftforge.net/"; + + // Git Version + private static final String GITVERSION_NAME = "gitversion"; + private static final String GITVERSION_VERSION = "0.6.0"; + private static final String GITVERSION_URL = FORGE_MAVEN + "net/minecraftforge/gitversion/" + GITVERSION_VERSION + "/gitversion-" + GITVERSION_VERSION + "-fatjar.jar"; + private static final int GITVERSION_JAVA = 17; + private static final String GITVERSION_MAIN = "net.minecraftforge.gitver.cli.Main"; + public static final Tool GITVERSION = Tool.of(GITVERSION_NAME, GITVERSION_NAME, GITVERSION_URL, GITVERSION_JAVA, GITVERSION_MAIN); +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionValueSource.groovy b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionValueSource.groovy new file mode 100644 index 0000000..ee8f4e0 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/GitVersionValueSource.groovy @@ -0,0 +1,91 @@ +package net.minecraftforge.gitversion.gradle + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.jvm.toolchain.JavaToolchainService +import org.gradle.process.ExecOperations +import org.gradle.process.JavaExecSpec +import org.gradle.process.ProcessExecutionException + +import javax.inject.Inject +import java.nio.charset.StandardCharsets + +import static net.minecraftforge.gitversion.gradle.GitVersionPlugin.LOGGER + +@CompileStatic +@PackageScope abstract class GitVersionValueSource

implements ValueSource { + static interface Parameters extends ValueSourceParameters { + ConfigurableFileCollection getClasspath() + Property getJavaLauncher() + } + + @Inject GitVersionValueSource() {} + + protected abstract @Inject ExecOperations getExecOperations() + + static Provider info(Project project, ProviderFactory providers) { + providers.of(Info) { spec -> + spec.parameters { parameters -> + parameters.classpath.from(project.plugins.getPlugin(GitVersionPlugin).getTool(GitVersionTools.GITVERSION)) + parameters.javaLauncher.set(Util.launcherFor(project.extensions.getByType(JavaPluginExtension), project.extensions.getByType(JavaToolchainService), 17).map { it.executablePath.toString() }) + + parameters.projectPath.set(providers.provider { project.layout.projectDirectory.asFile.absolutePath }) + } + }.map { Util.fromJson(it, GitVersionExtensionInternal.Output) } + } + + @CompileStatic + @PackageScope static abstract class Info extends GitVersionValueSource { + static interface Parameters extends Parameters { + Property getProjectPath() + } + + @Inject + Info() {} + + @Override + String obtain() { + final parameters = this.parameters + final output = new ByteArrayOutputStream() + + Closure javaExecSpec = { JavaExecSpec exec -> + exec.classpath = parameters.classpath + exec.mainClass.set(GitVersionTools.GITVERSION.mainClass) + exec.executable = parameters.javaLauncher + + exec.standardOutput = output + exec.errorOutput = Util.toLog(LOGGER.&error) + + exec.args( + '--json', + '--project-dir', parameters.projectPath.get() + ) + } + + try { + this.execOperations.javaexec(javaExecSpec).rethrowFailure().assertNormalExitValue() + } catch (ProcessExecutionException e) { + output.reset() + + try { + this.execOperations.javaexec(javaExecSpec.andThen { JavaExecSpec exec -> + exec.args('--disable-strict') + }).rethrowFailure().assertNormalExitValue() + } catch (ProcessExecutionException suppressed) { + e.addSuppressed(suppressed) + throw e + } + } + + output.toString(StandardCharsets.UTF_8) + } + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/Util.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/Util.java new file mode 100644 index 0000000..bd610dd --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/Util.java @@ -0,0 +1,75 @@ +package net.minecraftforge.gitversion.gradle; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.ToNumberPolicy; +import com.google.gson.stream.JsonReader; +import groovy.lang.Closure; +import groovy.lang.DelegatesTo; +import groovy.transform.stc.ClosureParams; +import groovy.transform.stc.FirstParam; +import net.minecraftforge.gradleutils.shared.Closures; +import net.minecraftforge.gradleutils.shared.SharedUtil; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaLauncher; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.jvm.toolchain.JavaToolchainSpec; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Objects; +import java.util.concurrent.Callable; + +final class Util extends SharedUtil { + private static final Gson GSON = new GsonBuilder() + .setObjectToNumberStrategy(Util::readNumber) + .setPrettyPrinting() + .create(); + + private static Number readNumber(JsonReader in) throws IOException { + try { + return ToNumberPolicy.LONG_OR_DOUBLE.readNumber(in); + } catch (Throwable suppressed) { + try { + return ToNumberPolicy.BIG_DECIMAL.readNumber(in); + } catch (Throwable e) { + IOException throwing = new IOException("Failed to read number from " + in, e); + throwing.addSuppressed(suppressed); + throw throwing; + } + } + } + + public static T fromJson(File file, Class classOfT) throws JsonSyntaxException, JsonIOException { + try (FileInputStream stream = new FileInputStream(file)) { + return fromJson(stream, classOfT); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + + public static T fromJson(byte[] data, Class classOfT) throws JsonSyntaxException, JsonIOException { + return fromJson(new ByteArrayInputStream(data), classOfT); + } + + public static T fromJson(InputStream stream, Class classOfT) throws JsonSyntaxException, JsonIOException { + return GSON.fromJson(new InputStreamReader(stream), classOfT); + } + + public static T fromJson(String data, Class classOfT) throws JsonSyntaxException, JsonIOException { + return GSON.fromJson(data, classOfT); + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtension.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtension.java new file mode 100644 index 0000000..9cff176 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtension.java @@ -0,0 +1,48 @@ +package net.minecraftforge.gitversion.gradle.changelog; + +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.publish.maven.MavenPublication; + +/// Configuration for the Changelog plugin. +/// +/// This extension is added to the [project][org.gradle.api.Project] when the `net.minecraftforge.changelog` plugin is +/// applied. +/// ### Enabling Generation +/// To enable changelog generation for your project, a start marker for the changelog must be specified. This can be +/// done by calling [#from(String)] or one of its sister methods. +public sealed interface ChangelogExtension permits ChangelogExtensionInternal { + /// The name for this extension. + String NAME = "changelog"; + + /// Sets the changelog start marker to the last merge base commit. + /// + /// @see #from(String) + void fromBase(); + + /// Sets the changelog start marker to use when generating the changelog. This can be a tag name or a commit SHA. + /// + /// @param marker The start marker for the changelog + /// @apiNote To start from the last merge base commit, use [#fromBase()] + void from(String marker); + + /// Sets the changelog start marker to use when generating the changelog. This can be a tag name or a commit SHA. + /// + /// If `null`, the changelog will start from the last merge base commit. + /// + /// @param marker The start marker for the changelog + void from(Provider marker); + + /// Sets this project's changelog as an artifact for the given publication. + /// + /// @param publication The publication + void publish(MavenPublication publication); + + /// The property that sets if the changelog generation should be enabled for all maven publications in the project. + /// + /// It will also set up publishing for all subprojects as long as that subproject does not have another changelog + /// plugin overriding the propagation. + /// + /// @return The property for if the changelog generation is enabled for all maven publications + Property getPublishAll(); +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtensionImpl.groovy b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtensionImpl.groovy new file mode 100644 index 0000000..d0091d8 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtensionImpl.groovy @@ -0,0 +1,93 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gitversion.gradle.changelog + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.transform.PackageScopeTarget +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.TaskProvider + +import javax.inject.Inject + +/** The heart of the Changelog plugin. This extension is used to enable and partially configure the changelog generation task. */ +@CompileStatic +@PackageScope([PackageScopeTarget.CLASS, PackageScopeTarget.FIELDS]) +abstract class ChangelogExtensionImpl implements ChangelogExtensionInternal { + public static final String NAME = 'changelog' + + private final Project project + + private final Property publishingAll + private final Property isGenerating + + private @Lazy TaskProvider task = { + this.isGenerating.set(true) + Util.ensureAfterEvaluate(this.project, this.&finish) + + ChangelogUtils.setupChangelogTask(this.project) + }() + + protected abstract @Inject ObjectFactory getObjects() + + @Inject + ChangelogExtensionImpl(Project project) { + this.project = project + + this.publishingAll = this.objects.property(Boolean).convention(false) + this.isGenerating = this.objects.property(Boolean).convention(false) + } + + private void finish(Project project) { + if (this.publishAll) + ChangelogUtils.setupChangelogGenerationOnAllPublishTasks(project) + } + + @Override + void fromBase() { + this.task + } + + @Override + void from(String marker) { + this.task.configure { it.start.set(marker) } + } + + @Override + void from(Provider marker) { + this.task.configure { it.start.set(marker.map(Object.&toString)) } + } + + @Override + void publish(MavenPublication publication) { + ChangelogUtils.setupChangelogGenerationForPublishing(this.project, publication) + } + + @Override + Property getPublishAll() { + this.publishAll + } + + @Override + boolean isGenerating() { + this.isGenerating.get() + } + + @Override + Dependency asDependency() { + this.asDependency(this.project.dependencies) + } + + @Override + Dependency asDependency(DependencyHandler dependencies) { + dependencies.project('path': this.project.path, 'configuration': GenerateChangelog.NAME) + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtensionInternal.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtensionInternal.java new file mode 100644 index 0000000..c1a2f6d --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogExtensionInternal.java @@ -0,0 +1,19 @@ +package net.minecraftforge.gitversion.gradle.changelog; + +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.reflect.HasPublicType; +import org.gradle.api.reflect.TypeOf; + +non-sealed interface ChangelogExtensionInternal extends ChangelogExtension, HasPublicType { + @Override + default TypeOf getPublicType() { + return TypeOf.typeOf(ChangelogExtension.class); + } + + boolean isGenerating(); + + Dependency asDependency(); + + Dependency asDependency(DependencyHandler dependencies); +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogPlugin.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogPlugin.java new file mode 100644 index 0000000..456a0b1 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogPlugin.java @@ -0,0 +1,21 @@ +package net.minecraftforge.gitversion.gradle.changelog; + +import net.minecraftforge.gradleutils.shared.EnhancedPlugin; +import org.gradle.api.Project; + +import javax.inject.Inject; + +abstract class ChangelogPlugin extends EnhancedPlugin { + static final String NAME = "changelog"; + static final String DISPLAY_NAME = "Git Changelog"; + + @Inject + public ChangelogPlugin() { + super(NAME, DISPLAY_NAME); + } + + @Override + public void setup(Project project) { + project.getExtensions().create(ChangelogExtension.NAME, ChangelogExtensionImpl.class, project); + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogProblems.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogProblems.java new file mode 100644 index 0000000..8036be5 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogProblems.java @@ -0,0 +1,12 @@ +package net.minecraftforge.gitversion.gradle.changelog; + +import net.minecraftforge.gradleutils.shared.EnhancedProblems; + +import javax.inject.Inject; + +abstract class ChangelogProblems extends EnhancedProblems { + @Inject + public ChangelogProblems() { + super(ChangelogPlugin.NAME, ChangelogPlugin.DISPLAY_NAME); + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogUtils.groovy b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogUtils.groovy new file mode 100644 index 0000000..ef543d8 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/ChangelogUtils.groovy @@ -0,0 +1,138 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gitversion.gradle.changelog + + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import groovy.transform.PackageScopeTarget +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenArtifact +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.TaskProvider +import org.gradle.language.base.plugins.LifecycleBasePlugin + +/** Utility methods for configuring and working with the changelog tasks. */ +@CompileStatic +@PackageScope([PackageScopeTarget.CLASS, PackageScopeTarget.CONSTRUCTORS, PackageScopeTarget.METHODS]) +class ChangelogUtils { + /** + * Adds the createChangelog task to the target project. Also exposes it as a artifact of the 'createChangelog' + * configuration. + *

+ * This is the + * recommended way + * to share task outputs between multiple projects. + * + * @param project Project to add the task to + * @return The task responsible for generating the changelog + */ + static TaskProvider setupChangelogTask(Project project) { + project.tasks.register(GenerateChangelog.NAME, GenerateChangelog).tap { task -> + project.configurations.register(GenerateChangelog.NAME) { it.canBeResolved = false } + project.artifacts.add(GenerateChangelog.NAME, task) + project.tasks.named(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).configure { it.dependsOn task } + } + } + + /** + * Sets up the changelog generation on all maven publications in the project. + *

It also sets up publishing for all subprojects as long as that subproject does not have another changelog plugin + * overriding the propagation.

+ * + * @param project The project to add changelog generation publishing to + */ + static void setupChangelogGenerationOnAllPublishTasks(Project project) { + setupChangelogGenerationForAllPublications project + + project.subprojects { + Util.ensureAfterEvaluate(it) { subproject -> + // attempt to get the current subproject's changelog extension + var ext = subproject.extensions.findByType(ChangelogExtension) + + // find the changelog extension for the highest project that has it, if the subproject doesn't + for (var parent = project; ext == null && parent != null; parent = parent.parent == parent ? null : parent.parent) { + ext = parent.extensions.findByType(ChangelogExtension) + } + + // if the project with changelog is publishing all changelogs, set up changelogs for the subproject + if (ext != null && ext.publishAll) + setupChangelogGenerationForAllPublications subproject + } + } + } + + private static void setupChangelogGenerationForAllPublications(Project project) { + var ext = project.extensions.findByName(PublishingExtension.NAME) as PublishingExtension + if (ext === null) return + + // Get each extension and add the publishing task as a publishing artifact + ext.publications.withType(MavenPublication).configureEach { publication -> + setupChangelogGenerationForPublishing project, publication + } + } + + private static ChangelogExtensionInternal findParent(Project project) { + var ext = project.extensions.findByType(ChangelogExtension) as ChangelogExtensionInternal + if (ext?.generating) return ext + + var parent = project.parent == project ? null : project.parent + return parent == null ? null : findParent(parent) + } + + /** + * The recommended way to share task outputs across projects is to export them as dependencies + *

+ * So for any project that doesn't generate the changelog directly, we must create a + * {@linkplain CopyChangelog copy task} and new configuration + */ + private static TaskProvider findChangelogTask(Project project) { + // See if we've already made the task + if (project.tasks.names.contains(GenerateChangelog.NAME)) + return project.tasks.named(GenerateChangelog.NAME) + + if (project.tasks.names.contains(CopyChangelog.NAME)) + return project.tasks.named(CopyChangelog.NAME) + + // See if there is any parent with a changelog configured + var parent = findParent(project) + if (parent == null) return null + + project.tasks.register(CopyChangelog.NAME, CopyChangelog) { task -> + var dependency = parent.asDependency(project.dependencies) + var configuration = project.configurations.detachedConfiguration(dependency).tap { it.canBeConsumed = false } + task.inputFile.fileProvider project.providers.provider { configuration.singleFile } + } + } + + /** + * Sets up the changelog generation on the given maven publication. + * + * @param project The project in question + * @param publication The publication in question + */ + static void setupChangelogGenerationForPublishing(Project project, MavenPublication publication) { + Util.ensureAfterEvaluate(project) { p -> + setupChangelogGenerationForPublishingAfterEvaluation p, publication + } + } + + private static void setupChangelogGenerationForPublishingAfterEvaluation(Project project, MavenPublication publication) { + boolean existing = !publication.artifacts.findAll { MavenArtifact a -> a.classifier == 'changelog' && a.extension == 'txt' }.isEmpty() + if (existing) return + + // Grab the task + var task = findChangelogTask project + + // Add a new changelog artifact and publish it + publication.artifact(task.get().outputs.files.singleFile) { artifact -> + artifact.builtBy task + artifact.classifier = 'changelog' + artifact.extension = 'txt' + } + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/CopyChangelog.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/CopyChangelog.java new file mode 100644 index 0000000..7602ef7 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/CopyChangelog.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gitversion.gradle.changelog; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.reflect.HasPublicType; +import org.gradle.api.reflect.TypeOf; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +// This task class is internal. Do NOT attempt to use it directly. +// If you need the output, use `project.tasks.named('copyChangelog').outputs.files` instead +abstract class CopyChangelog extends DefaultTask implements HasPublicType { + static final String NAME = "copyChangelog"; + + protected abstract @Inject ProviderFactory getProviders(); + + @Inject + public CopyChangelog(ProjectLayout layout) { + this.setDescription("Copies a changelog file to this project's build directory."); + + this.getOutputFile().convention(layout.getBuildDirectory().file("changelog.txt")); + } + + @Override + public TypeOf getPublicType() { + return TypeOf.typeOf(DefaultTask.class); + } + + public abstract @OutputFile RegularFileProperty getOutputFile(); + + public abstract @InputFile RegularFileProperty getInputFile(); + + @TaskAction + public void exec() { + byte[] input; + try { + // ProviderFactory#fileContents so Gradle is aware of our usage of the input + input = this.getProviders().fileContents(this.getInputFile()).getAsBytes().get(); + } catch (IllegalStateException e) { + throw new RuntimeException(e); + } + + File output = this.getOutputFile().get().getAsFile(); + + if (!output.getParentFile().exists() && !output.getParentFile().mkdirs()) + throw new IllegalStateException(); + + try { + Files.write( + output.toPath(), + input + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/GenerateChangelog.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/GenerateChangelog.java new file mode 100644 index 0000000..03d2cc8 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/GenerateChangelog.java @@ -0,0 +1,104 @@ +package net.minecraftforge.gitversion.gradle.changelog; + +import net.minecraftforge.gitversion.gradle.GitVersionTools; +import net.minecraftforge.gradleutils.shared.ToolExecBase; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; + +import javax.inject.Inject; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/// Generates a changelog for the project based on the Git history using +/// Git Version. +public abstract class GenerateChangelog extends ToolExecBase { + /// The name for the task, used by the [extension][ChangelogExtension] when registering it. + public static final String NAME = "createChangelog"; + + /// Constructs a new task instance. + @Inject + public GenerateChangelog() { + super(ChangelogProblems.class, GitVersionTools.GITVERSION); + + this.setDescription("Generates a changelog for the project based on the Git history using Git Version."); + + //Setup defaults: Using merge-base based text changelog generation of the local project into build/changelog.txt + this.getOutputFile().convention(this.getProjectLayout().getBuildDirectory().file("changelog.txt").map(this.getProblems().ensureFileLocation())); + + this.getProjectPath().convention(this.getProviderFactory().provider(() -> this.getProjectLayout().getProjectDirectory().getAsFile().getAbsolutePath())); + this.getBuildMarkdown().convention(false); + } + + /// The output file for the changelog. + /// + /// @return A property for the output file + public abstract @OutputFile RegularFileProperty getOutputFile(); + + /// The absolute path to the current project. + /// + /// Used to configure Git Version without needing to specify the directory itself, since using the directory + /// itself can cause implicit dependencies on other tasks that use actually it. + /// + /// @return A property for the project path + protected abstract @Input Property getProjectPath(); + + /// The tag (or object ID) to start the changelog from. + /// + /// @return A property for the start tag + public abstract @Input @Optional Property getStart(); + + /// The project URL to use in the changelog. + /// + /// Git Version will automatically attempt to find a URL from the repository's remote details if left + /// unspecified. + /// + /// @return A property for the project URL + public abstract @Input @Optional Property getProjectUrl(); + + /** + * Whether to build the changelog in Markdown format. + * + * @return A property for Markdown formatting + */ + public abstract @Input Property getBuildMarkdown(); + + @Override + protected void addArguments() { + super.addArguments(); + + this.args( + "--changelog", + "--project-dir", this.getProjectPath().get() + ); + if (this.getStart().isPresent()) + this.args("--start", this.getStart().get()); + if (this.getProjectUrl().isPresent()) + this.args("--url", this.getProjectUrl().get()); + if (!this.getBuildMarkdown().getOrElse(false)) + this.args("--plain-text"); + } + + @Override + public void exec() { + var output = new ByteArrayOutputStream(); + this.setStandardOutput(output); + this.setErrorOutput(Util.toLog(this.getLogger()::error)); + + super.exec(); + + try { + Files.writeString( + this.getOutputFile().get().getAsFile().toPath(), + output.toString(), + StandardCharsets.UTF_8 + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/Util.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/Util.java new file mode 100644 index 0000000..ad8e882 --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/Util.java @@ -0,0 +1,6 @@ +package net.minecraftforge.gitversion.gradle.changelog; + +import net.minecraftforge.gradleutils.shared.SharedUtil; + +final class Util extends SharedUtil { +} diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/package-info.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/package-info.java new file mode 100644 index 0000000..c92923d --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/changelog/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.minecraftforge.gitversion.gradle.changelog; + +import org.jetbrains.annotations.NotNullByDefault; diff --git a/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/package-info.java b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/package-info.java new file mode 100644 index 0000000..11d11fb --- /dev/null +++ b/gradle-plugin/src/main/groovy/net/minecraftforge/gitversion/gradle/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.minecraftforge.gitversion.gradle; + +import org.jetbrains.annotations.NotNullByDefault; diff --git a/settings.gradle b/settings.gradle index 14fd8fd..6893f9c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,8 @@ plugins { rootProject.name = 'gitversion' +includeBuild 'gradle-plugin' + dependencyResolutionManagement { repositories { mavenCentral() @@ -22,6 +24,9 @@ dependencyResolutionManagement { // Config library 'toml', 'org.tomlj', 'tomlj' version '1.1.1' + // JSON Output + library 'gson', 'com.google.code.gson', 'gson' version '2.13.1' + // CLI library 'jopt', 'net.sf.jopt-simple', 'jopt-simple' version '6.0-alpha-3' library 'slf4j', 'org.slf4j', 'slf4j-simple' version '1.7.36' diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 5896ef9..756a417 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -10,4 +10,5 @@ requires static joptsimple; requires static org.jetbrains.annotations; + requires com.google.gson; } diff --git a/src/main/java/net/minecraftforge/gitver/api/GitVersion.java b/src/main/java/net/minecraftforge/gitver/api/GitVersion.java index 3d3c425..b5a6b74 100644 --- a/src/main/java/net/minecraftforge/gitver/api/GitVersion.java +++ b/src/main/java/net/minecraftforge/gitver/api/GitVersion.java @@ -4,32 +4,23 @@ */ package net.minecraftforge.gitver.api; -import net.minecraftforge.gitver.internal.GitUtils; import net.minecraftforge.gitver.internal.GitVersionImpl; -import net.minecraftforge.gitver.internal.Util; -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.util.FS; -import org.eclipse.jgit.util.StringUtils; -import org.eclipse.jgit.util.SystemReader; -import org.jetbrains.annotations.ApiStatus; +import net.minecraftforge.gitver.internal.GitVersionInternal; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; -import org.jetbrains.annotations.UnmodifiableView; +import org.jetbrains.annotations.Unmodifiable; import java.io.File; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.stream.Collectors; +import java.util.List; /** * The heart of the GitVersion library. Information about how GitVersion operates can be found on the * path page. */ -public sealed interface GitVersion extends AutoCloseable permits GitVersionImpl { +public sealed interface GitVersion extends AutoCloseable permits GitVersionInternal { /* BUILDER */ /** @@ -39,7 +30,7 @@ public sealed interface GitVersion extends AutoCloseable permits GitVersionImpl * @return A new builder */ static Builder builder() { - return new Builder(); + return GitVersionInternal.builder(); } /** @@ -50,25 +41,14 @@ static Builder builder() { * automatically from the project directory. If the git directory is not set, it will default to {@code .git} in the * root directory. */ - final class Builder { - private @Nullable File gitDir; - private @Nullable File root; - private @Nullable File project; - private @Nullable GitVersionConfig config; - private boolean strict = true; - - private Builder() { } - + sealed interface Builder permits GitVersionInternal.Builder { /** * Sets the git directory for the GitVersion instance. Ideally, this should be located in the root directory. * * @param gitDir The git directory * @return This builder */ - public Builder gitDir(File gitDir) { - this.gitDir = gitDir; - return this; - } + Builder gitDir(@UnknownNullability File gitDir); /** * Sets the root directory for the GitVersion instance. @@ -76,10 +56,7 @@ public Builder gitDir(File gitDir) { * @param root The root directory * @return This builder */ - public Builder root(@UnknownNullability File root) { - this.root = root != null ? root.getAbsoluteFile() : null; - return this; - } + Builder root(@UnknownNullability File root); /** * Sets the project directory for the GitVersion instance. @@ -87,32 +64,19 @@ public Builder root(@UnknownNullability File root) { * @param project The project directory * @return This builder */ - public Builder project(@UnknownNullability File project) { - this.project = project != null ? project.getAbsoluteFile() : null; - return this; - } + Builder project(@UnknownNullability File project); - public Builder config(@UnknownNullability File config) { - this.config = config != null ? GitVersionConfig.parse(config) : null; - return this; - } + Builder config(@UnknownNullability File config); /** * Sets the config to use for the GitVersion instance. * * @param config The config * @return This builder - * @see GitVersionConfig#EMPTY */ - public Builder config(GitVersionConfig config) { - this.config = config; - return this; - } + Builder config(GitVersionConfig config); - public Builder strict(boolean strict) { - this.strict = strict; - return this; - } + Builder strict(boolean strict); /** * Builds the GitVersion instance. @@ -121,31 +85,7 @@ public Builder strict(boolean strict) { * @throws IllegalArgumentException If neither the root nor project directory is set * @throws GitVersionException If an error occurs during construction */ - public GitVersion build() { - if (this.root == null && this.project == null) - throw new IllegalArgumentException("Either the root or project directory must be set"); - - if (this.root == null) - this.root = findGitRoot(this.project); - - if (this.project == null) - this.project = this.root; - - if (this.gitDir == null) - this.gitDir = new File(this.root, ".git"); - - if (this.config == null) - this.config = GitVersionConfig.parse(new File(this.root, ".gitversion")); - - try { - return new GitVersionImpl(this.gitDir, this.root, this.project, this.config, this.strict); - } catch (GitVersionException e) { - if (!this.strict) - return GitVersionImpl.emptyFor(this.project); - - throw e; - } - } + GitVersion build(); } @@ -160,20 +100,13 @@ public GitVersion build() { * * @return The calculated version */ - default String getTagOffset() { - var info = this.getInfo(); - return "%s.%s".formatted(info.getTag(), info.getOffset()); - } + String getTagOffset(); /** @see #getTagOffsetBranch(Collection) */ - default String getTagOffsetBranch() { - return this.getTagOffsetBranch(GitUtils.DEFAULT_ALLOWED_BRANCHES); - } + String getTagOffsetBranch(); /** @see #getTagOffsetBranch(Collection) */ - default String getTagOffsetBranch(@UnknownNullability String... allowedBranches) { - return this.getTagOffsetBranch(Arrays.asList(Util.ensure(allowedBranches))); - } + String getTagOffsetBranch(@UnknownNullability String... allowedBranches); /** * Calculates a version number using {@link #getTagOffset()}. If the current branch is not included in the defined @@ -188,33 +121,13 @@ default String getTagOffsetBranch(@UnknownNullability String... allowedBranches) * @return The calculated version * @see #getTagOffset() */ - default String getTagOffsetBranch(@UnknownNullability Collection allowedBranches) { - allowedBranches = Util.ensure(allowedBranches); - var version = this.getTagOffset(); - if (allowedBranches.isEmpty()) return version; - - var branch = this.getInfo().getBranch(true); - return StringUtils.isEmptyOrNull(branch) || allowedBranches.contains(branch) ? version : "%s-%s".formatted(version, branch); - } + String getTagOffsetBranch(@UnknownNullability Collection allowedBranches); /** @see #getMCTagOffsetBranch(String, Collection) */ - default String getMCTagOffsetBranch(@UnknownNullability String mcVersion) { - if (StringUtils.isEmptyOrNull(mcVersion)) - return this.getTagOffsetBranch(); - - var allowedBranches = new ArrayList<>(GitUtils.DEFAULT_ALLOWED_BRANCHES); - allowedBranches.add(mcVersion); - allowedBranches.add(mcVersion + ".0"); - allowedBranches.add(mcVersion + ".x"); - allowedBranches.add(mcVersion.substring(0, mcVersion.lastIndexOf('.')) + ".x"); - - return this.getMCTagOffsetBranch(mcVersion, allowedBranches); - } + String getMCTagOffsetBranch(@UnknownNullability String mcVersion); /** @see #getMCTagOffsetBranch(String, Collection) */ - default String getMCTagOffsetBranch(String mcVersion, String... allowedBranches) { - return this.getMCTagOffsetBranch(mcVersion, Arrays.asList(allowedBranches)); - } + String getMCTagOffsetBranch(String mcVersion, String... allowedBranches); /** * Calculates a version number using {@link #getTagOffsetBranch(String...)}, additionally prepending the given @@ -228,9 +141,7 @@ default String getMCTagOffsetBranch(String mcVersion, String... allowedBranches) * @param allowedBranches A list of allowed branches * @return The calculated version */ - default String getMCTagOffsetBranch(String mcVersion, Collection allowedBranches) { - return "%s-%s".formatted(mcVersion, this.getTagOffsetBranch(allowedBranches)); - } + String getMCTagOffsetBranch(String mcVersion, Collection allowedBranches); /* CHANGELOG */ @@ -243,7 +154,7 @@ default String getMCTagOffsetBranch(String mcVersion, Collection allowed * * @param start The tag or commit hash to start the changelog from, or {@code null} to start from the current * @param url The URL to the repository, or {@code null} to attempt to use the - * {@linkplain Info#getUrl() auto-calculated URL} (if available) + * {@linkplain #getUrl() auto-calculated URL} (if available) * @param plainText Whether to generate the changelog in plain text ({@code} false to use Markdown formatting) * @return The generated changelog * @throws GitVersionException If changelog fails to generate (in {@linkplain Builder#strict(boolean) strict mode}) @@ -278,7 +189,7 @@ default String getMCTagOffsetBranch(String mcVersion, Collection allowed * versioning methods in {@link GitVersion} do not suffice. */ @NotNullByDefault - sealed interface Info extends Serializable permits GitVersionImpl.Info { + sealed interface Info extends Serializable permits GitVersionInternal.Info { /** @return The current tag as described by the Git repository using the applied filters */ String getTag(); @@ -305,14 +216,7 @@ sealed interface Info extends Serializable permits GitVersionImpl.Info { * @param versionFriendly Whether to format the branch to make it version string friendly * @return The current branch */ - default String getBranch(boolean versionFriendly) { - var branch = this.getBranch(); - if (!versionFriendly || branch.isBlank()) return branch; - - if (branch.startsWith("pulls/")) - branch = "pr" + branch.substring(branch.lastIndexOf('/') + 1); - return branch.replaceAll("[\\\\/]", "-"); - } + String getBranch(boolean versionFriendly); /** * @return The long {@code HEAD} commit hash @@ -333,37 +237,8 @@ default String getBranch(boolean versionFriendly) { /** @return The tag prefix used when filtering tags */ String getTagPrefix(); - /** - * Sets the tag prefix to use when filtering tags. - * - * @param tagPrefix The tag prefix - * @deprecated This method only exists for backwards compatibility in GradleUtils 2.4. Using this is discouraged - * since it breaks the contract that the {@linkplain GitVersionConfig config} has the declarations of all the - * project's values, including the {@linkplain GitVersionConfig.Project#getTagPrefix() tag prefix} and any - * additional {@linkplain GitVersionConfig.Project#getFilters() filters}. - */ - @Deprecated - default void setTagPrefix(String tagPrefix) { } - /** @return The filters used when filtering tags (excluding the {@linkplain #getTagPrefix() tag prefix}) */ - @UnmodifiableView Collection getFilters(); - - /** @see #setFilters(String...) */ - default void setFilters(Collection filters) { - this.setFilters(filters.toArray(new String[0])); - } - - /** - * Sets the filters to use when filtering tags (excluding the {@linkplain #getTagPrefix() tag prefix}). - * - * @param filters The filters - * @deprecated This method only exists for backwards compatibility in GradleUtils 2.4. Using this is discouraged - * since it breaks the contract that the {@linkplain GitVersionConfig config} has the declarations of all the - * project's values, including the {@linkplain GitVersionConfig.Project#getTagPrefix() tag prefix} and any - * additional {@linkplain GitVersionConfig.Project#getFilters() filters}. - */ - @Deprecated - default void setFilters(String... filters) { } + @Unmodifiable Collection getFilters(); /* FILE SYSTEM */ @@ -386,13 +261,9 @@ default void setFilters(String... filters) { } * * @return The relative path string */ - default String getProjectPath() { - return GitUtils.getRelativePath(this.getRoot(), this.getProject()); - } + String getProjectPath(); - default String getRelativePath(File file) { - return this.getRelativePath(false, file); - } + String getRelativePath(File file); /** * Gets the relative path string of a given file. @@ -406,55 +277,13 @@ default String getRelativePath(File file) { * @param file The file to get the relative path to * @return The relative path string */ - default String getRelativePath(boolean fromRoot, File file) { - return GitUtils.getRelativePath(fromRoot ? this.getRoot() : this.getProject(), file); - } - - /** - * Attempts to find the git root from the given directory, using {@linkplain File#getAbsoluteFile() absolute files} - * to walk the filesystem. - * - * @param from The file to find the Git root from - * @return The Git root, or the given file if no Git root was found - */ - static File findGitRoot(File from) { - for (var dir = from.getAbsoluteFile(); dir != null; dir = dir.getParentFile()) - if (isGitRoot(dir)) return dir; - - return from; - } - - /** - * Checks if a given file is a Git root. - * - * @param dir The directory to check - * @return {@code true} if the directory is a Git root - */ - static boolean isGitRoot(File dir) { - return new File(dir, ".git").exists(); - } - - /** - * Attempts to get the relative path of the given file from the root of its Git repository. This is exposed - * primarily to allow Gradle plugins to get the relative path without needing to declare the path directory - * directly, as it can cause issues with task configuration. - * - * @param file The file to find the relative path to from the root of its Git repository - * @return The relative path, or an empty string if the file is not in (or itself is) a Git repository - * @see #getRelativePath(boolean, File) - * @deprecated Will be removed once GitVersion has its own Gradle plugin and extension, instead of being a part of - * GradleUtils - */ - @Deprecated(forRemoval = true) - static String findRelativePath(File file) { - return GitUtils.getRelativePath(findGitRoot(file), file); - } + String getRelativePath(boolean fromRoot, File file); /* SUBPROJECTS */ /** @return The declared subprojects of this path. */ - @UnmodifiableView Collection getSubprojects(); + @Unmodifiable Collection getSubprojects(); /** * Gets the relative subproject path strings from the declared subprojects. The path strings are relative from the @@ -485,13 +314,7 @@ default Collection getSubprojectPaths() { * @return The subproject paths * @see #getSubprojects() */ - default Collection getSubprojectPaths(boolean fromRoot) { - return this.getSubprojects() - .stream() - .map(dir -> GitUtils.getRelativePath(fromRoot ? this.getRoot() : this.getProject(), dir)) - .filter(s -> !s.isBlank()) - .collect(Collectors.toCollection(ArrayList::new)); - } + Collection getSubprojectPaths(boolean fromRoot); /* REPOSITORY */ @@ -501,41 +324,20 @@ default Collection getSubprojectPaths(boolean fromRoot) { void close(); - /* EXPERIMENTAL */ + /* SERIALIZATION */ - /** - * Prevents JGit's {@link SystemReader} from - * {@linkplain SystemReader#openSystemConfig(Config, FS) reading the system configuration file}. - *

This is a potentially very destructive action since it replaces the global system reader used - * for all JGit operations. It should not be used in production, with the exception of the Gradle environment with - * configuration cache. This is because reading the system configuration file requires executing the System's - * {@code git} command, which is not allowed when using Gradle configuration cache.

- *

A preferable alternative to using this method, if applicable, is to set the - * {@link org.eclipse.jgit.lib.Constants#GIT_CONFIG_NOSYSTEM_KEY "GIT_CONFIG_NOSYSTEM"} environment - * variable to {@code "true"}.

- * - * @apiNote Under no circumstances should this method be invoked in a non-Gradle production environment. If you do, - * it is at your own risk. - */ - @ApiStatus.Experimental - static void disableSystemConfig() { - SystemReader.setInstance(new SystemReader.Delegate(SystemReader.getInstance()) { - @Override - public FileBasedConfig openSystemConfig(Config parent, FS fs) { - return new FileBasedConfig(parent, null, fs) { - @Override - public void load() { } - - @Override - public boolean isOutdated() { - return false; - } - }; - } - }); - } + String toJson(); + + interface Output extends Serializable { + Info info(); + @Nullable String url(); + + @Nullable String gitDirPath(); + @Nullable String rootPath(); + @Nullable String projectPath(); - static void restoreSystemReader() { - SystemReader.setInstance(null); + @Nullable String tagPrefix(); + List filters(); + List subprojectPaths(); } } diff --git a/src/main/java/net/minecraftforge/gitver/api/GitVersionConfig.java b/src/main/java/net/minecraftforge/gitver/api/GitVersionConfig.java index 5f44bea..387619e 100644 --- a/src/main/java/net/minecraftforge/gitver/api/GitVersionConfig.java +++ b/src/main/java/net/minecraftforge/gitver/api/GitVersionConfig.java @@ -4,18 +4,14 @@ */ package net.minecraftforge.gitver.api; -import net.minecraftforge.gitver.internal.GitVersionConfigImpl; -import org.jetbrains.annotations.NotNullByDefault; +import net.minecraftforge.gitver.internal.GitVersionConfigInternal; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import org.tomlj.Toml; import java.io.File; -import java.io.IOException; import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.function.Function; /** * The configuration for GitVersion. This is used to determine the version of a project based on the declared @@ -23,7 +19,7 @@ * * @see Project */ -public sealed interface GitVersionConfig extends Function<@Nullable String, GitVersionConfig.@Nullable Project> permits GitVersionConfigImpl, GitVersionConfigImpl.Empty { +public sealed interface GitVersionConfig permits GitVersionConfigInternal { /** * Gets the project at the given path. * @@ -32,16 +28,8 @@ public sealed interface GitVersionConfig extends Function<@Nullable String, GitV */ @Nullable Project getProject(@Nullable String path); - /** @return The root project */ - @SuppressWarnings("DataFlowIssue") - private Project getRootProject() { - return this.getProject(""); - } - - /** @return All projects, including the {@linkplain #getRootProject() root project}. */ - default Collection getAllProjects() { - return Collections.singleton(this.getRootProject()); - } + /** @return All projects, including the root project. */ + Collection getAllProjects(); /** * Validates this configuration by ensuring that all declared subprojects exist from the given root. @@ -49,42 +37,27 @@ default Collection getAllProjects() { * @param root The root to validate from * @throws IllegalArgumentException If a subproject path does not exist */ - default void validate(@UnknownNullability File root) throws IllegalArgumentException { } + void validate(@UnknownNullability File root) throws IllegalArgumentException; /** @return Any errors made during the config TOML parsing */ - default List errors() { - return Collections.emptyList(); - } - - /** The default config, used if there is no config available. */ - GitVersionConfig EMPTY = GitVersionConfigImpl.EMPTY; + List errors(); /** * Attempts to parse the given config file into a {@link GitVersionConfig} using {@linkplain Toml TOMLJ}. If it - * cannot be parsed or read, the default {@linkplain #EMPTY empty config} is returned. Other errors will still throw - * an exception, however. + * cannot be parsed or read, an empty config is returned. Other errors will still throw an exception, however. * * @param config The config file - * @return The parsed config, or the default {@linkplain #EMPTY empty config} if the file does not exist + * @return The parsed config, or an empty config if the file does not exist */ static GitVersionConfig parse(@UnknownNullability File config) { - try { - return GitVersionConfigImpl.parse(config); - } catch (IOException e) { - return EMPTY; - } + return GitVersionConfigInternal.parse(config); } - sealed interface Project permits GitVersionConfigImpl.ProjectImpl { + sealed interface Project permits GitVersionConfigInternal.Project { String getPath(); String getTagPrefix(); String[] getFilters(); } - - @Override - default @Nullable Project apply(@Nullable String path) { - return this.getProject(path); - } } diff --git a/src/main/java/net/minecraftforge/gitver/cli/Main.java b/src/main/java/net/minecraftforge/gitver/cli/Main.java index c779ab9..f58d217 100644 --- a/src/main/java/net/minecraftforge/gitver/cli/Main.java +++ b/src/main/java/net/minecraftforge/gitver/cli/Main.java @@ -25,6 +25,7 @@ public static void main(String[] args) throws Exception { "Displays this help message and exits") .forHelp(); + // strict mode var disableStrict0 = parser.accepts("disable-strict", """ Disables strict mode, allowing GitVersion to continue even if an error occurs. @@ -55,6 +56,32 @@ The root directory to use (ideally containing the .git directory). """ The project directory to use. (default: .)""") .withOptionalArg().ofType(File.class).defaultsTo(new File(".")); + + var changelogO = parser.accepts("changelog", + """ + Use to generate a changelog for the repository."""); + + var changelogStartO = parser.accepts("start", + """ + The commit to start from when generating the changelog.""") + .availableIf(changelogO).withRequiredArg().ofType(String.class); + + var changelogUrlO = parser.accepts("url", + """ + The URL to use when generating the changelog. + If left unspecified, will attempt to automatically get the URL from the repository.""") + .availableIf(changelogO).withRequiredArg().ofType(String.class); + + var changelogPlainTextO = parser.accepts("plain-text", + """ + If the generated changelog should be in plain text instead of Markdown.""") + .availableIf(changelogO); + + var jsonO = parser.accepts("json", + """ + Use to output the Git Version info as JSON. + Used by the Git Version Gradle plugin.""") + .availableUnless(changelogO); //@formatter:on var options = parser.parse(args); @@ -65,10 +92,10 @@ The root directory to use (ideally containing the .git directory). } var strict = !options.has(disableStrict0); - var configFile = options.valueOf(configFile0); + var configFile = options.valueOfOptional(configFile0).orElse(null); var projectDir = options.valueOf(projectDir0); - var rootDir = options.valueOf(rootDir0); - var gitDir = options.valueOf(gitDir0); + var rootDir = options.valueOfOptional(rootDir0).orElse(null); + var gitDir = options.valueOfOptional(gitDir0).orElse(null); try (var version = GitVersion .builder() .gitDir(gitDir) @@ -78,7 +105,17 @@ The root directory to use (ideally containing the .git directory). .strict(strict) .build() ) { - System.out.print(version.getTagOffset()); + if (options.has(changelogO)) { + System.out.println(version.generateChangelog( + options.valueOfOptional(changelogStartO).orElse(null), + options.valueOfOptional(changelogUrlO).orElse(null), + options.has(changelogPlainTextO) + )); + } else if (options.has(jsonO)) { + System.out.println(version.toJson()); + } else { + System.out.print(version.getTagOffset()); + } } } diff --git a/src/main/java/net/minecraftforge/gitver/internal/CommitCountProvider.java b/src/main/java/net/minecraftforge/gitver/internal/CommitCountProvider.java index 4a58201..2dbf6dc 100644 --- a/src/main/java/net/minecraftforge/gitver/internal/CommitCountProvider.java +++ b/src/main/java/net/minecraftforge/gitver/internal/CommitCountProvider.java @@ -10,8 +10,7 @@ /** * A provider for the commit count of a given tag. This is done in GitVersion in * {@link GitVersionImpl#getSubprojectCommitCount(Git, String)} by using - * {@link GitUtils#countCommits(Git, String, Iterable, Iterable) GitUtils.countCommits(Git, String, Iterable, - * Iterable)}. + * {@link GitUtils#countCommits(Git, String, String, Iterable, Iterable)}. */ @SuppressWarnings("JavadocReference") @FunctionalInterface diff --git a/src/main/java/net/minecraftforge/gitver/internal/GitUtils.java b/src/main/java/net/minecraftforge/gitver/internal/GitUtils.java index c74dcaf..3009cd5 100644 --- a/src/main/java/net/minecraftforge/gitver/internal/GitUtils.java +++ b/src/main/java/net/minecraftforge/gitver/internal/GitUtils.java @@ -39,9 +39,7 @@ * This is used heavily by, and in conjunction with, GitVersion. It holds the majority of operations done on the Git * repository. */ -public interface GitUtils { - List DEFAULT_ALLOWED_BRANCHES = List.of("master", "main", "HEAD"); - +interface GitUtils { /** * Gets the relative path of a file from a root directory. Uses NIO's {@link Path} to guarantee cross-platform * compatibility and reproducible path strings. diff --git a/src/main/java/net/minecraftforge/gitver/internal/GitVersionConfigImpl.java b/src/main/java/net/minecraftforge/gitver/internal/GitVersionConfigImpl.java index 8c88854..def6747 100644 --- a/src/main/java/net/minecraftforge/gitver/internal/GitVersionConfigImpl.java +++ b/src/main/java/net/minecraftforge/gitver/internal/GitVersionConfigImpl.java @@ -23,18 +23,18 @@ @NotNullByDefault public record GitVersionConfigImpl( - Map projects, + Map projects, @Override List errors -) implements GitVersionConfig { - public static final GitVersionConfig EMPTY = new Empty(); +) implements GitVersionConfigInternal { + static final GitVersionConfig EMPTY = new Empty(); @Override - public @Nullable Project getProject(@Nullable String path) { + public @Nullable GitVersionConfig.Project getProject(@Nullable String path) { return this.projects.get(path); } @Override - public Collection getAllProjects() { + public Collection getAllProjects() { return this.projects.values(); } @@ -54,14 +54,14 @@ public static GitVersionConfig parse(@UnknownNullability File config) throws IOE if (keys.contains("")) throw new IllegalArgumentException("Config file cannot have a table with an empty string ([\"\"]), use [root] instead."); - var projects = new HashMap(); + var projects = new HashMap(keys.size() + 1); projects.put("", ProjectImpl.parse(toml)); for (var key : keys) { if ("root".equals(key) || !toml.isTable(key)) continue; //noinspection DataFlowIssue - checked by TomlTable#isTable var project = ProjectImpl.parse(key, toml.getTable(key)); - projects.put(project.getPath, project); + projects.put(project.getPath(), project); } return new GitVersionConfigImpl(projects, toml.errors()); @@ -71,10 +71,11 @@ public record ProjectImpl( @Override String getPath, @Override String getTagPrefix, @Override String[] getFilters - ) implements GitVersionConfig.Project { + ) implements GitVersionConfigInternal.Project { private static final ProjectImpl DEFAULT_ROOT = new ProjectImpl("", "", new String[0]); + private static final List DEFAULT_ROOT_LIST = List.of(DEFAULT_ROOT); - private static ProjectImpl parse(TomlParseResult toml) { + private static GitVersionConfig.Project parse(TomlParseResult toml) { var root = toml.getTableOrEmpty("root"); if (root.isEmpty()) return DEFAULT_ROOT; @@ -84,7 +85,7 @@ private static ProjectImpl parse(TomlParseResult toml) { return new ProjectImpl("", tagPrefix, filters); } - private static ProjectImpl parse(String key, TomlTable table) { + private static GitVersionConfig.Project parse(String key, TomlTable table) { var project = Objects.requireNonNullElse(table.getString("path"), key); var tagPrefix = table.contains("tag") ? table.getString("tag") : project.replace("/", "-"); var filters = table.getArrayOrEmpty("filters").toList().stream().map(String.class::cast).filter(s -> !s.isBlank()).toArray(String[]::new); @@ -92,11 +93,6 @@ private static ProjectImpl parse(String key, TomlTable table) { return new ProjectImpl(project, tagPrefix, filters); } - @Override - public String toString() { - return "GitVersionConfig.Project{path=%s, tagPrefix=%s, filters=[%s]}".formatted(this.getPath, this.getTagPrefix, String.join(", ", this.getFilters)); - } - @Override public boolean equals(Object o) { return this == o || o instanceof ProjectImpl p && Objects.equals(this.getPath, p.getPath); @@ -108,12 +104,25 @@ public int hashCode() { } } - public static final class Empty implements GitVersionConfig { + public static final class Empty implements GitVersionConfigInternal { private Empty() { } @Override public @Nullable Project getProject(@Nullable String path) { return path != null && path.isEmpty() ? ProjectImpl.DEFAULT_ROOT : null; } + + @Override + public Collection getAllProjects() { + return ProjectImpl.DEFAULT_ROOT_LIST; + } + + @Override + public void validate(@UnknownNullability File root) { } + + @Override + public List errors() { + return List.of(); + } } } diff --git a/src/main/java/net/minecraftforge/gitver/internal/GitVersionConfigInternal.java b/src/main/java/net/minecraftforge/gitver/internal/GitVersionConfigInternal.java new file mode 100644 index 0000000..d5f4691 --- /dev/null +++ b/src/main/java/net/minecraftforge/gitver/internal/GitVersionConfigInternal.java @@ -0,0 +1,25 @@ +package net.minecraftforge.gitver.internal; + +import net.minecraftforge.gitver.api.GitVersionConfig; +import org.jetbrains.annotations.UnknownNullability; + +import java.io.File; +import java.io.IOException; + +public non-sealed interface GitVersionConfigInternal extends GitVersionConfig { + static GitVersionConfig parse(@UnknownNullability File config) { + try { + return GitVersionConfigImpl.parse(config); + } catch (IOException e) { + return GitVersionConfigImpl.EMPTY; + } + } + + non-sealed interface Project extends GitVersionConfig.Project { + String getPath(); + + String getTagPrefix(); + + String[] getFilters(); + } +} diff --git a/src/main/java/net/minecraftforge/gitver/internal/GitVersionExceptionInternal.java b/src/main/java/net/minecraftforge/gitver/internal/GitVersionExceptionInternal.java index 86a9f8c..f890cc8 100644 --- a/src/main/java/net/minecraftforge/gitver/internal/GitVersionExceptionInternal.java +++ b/src/main/java/net/minecraftforge/gitver/internal/GitVersionExceptionInternal.java @@ -6,7 +6,7 @@ import net.minecraftforge.gitver.api.GitVersionException; -public final class GitVersionExceptionInternal extends GitVersionException { +public non-sealed class GitVersionExceptionInternal extends GitVersionException { GitVersionExceptionInternal(String message) { super(message); } diff --git a/src/main/java/net/minecraftforge/gitver/internal/GitVersionImpl.java b/src/main/java/net/minecraftforge/gitver/internal/GitVersionImpl.java index b7cf6b6..4d046f2 100644 --- a/src/main/java/net/minecraftforge/gitver/internal/GitVersionImpl.java +++ b/src/main/java/net/minecraftforge/gitver/internal/GitVersionImpl.java @@ -12,23 +12,23 @@ import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.util.StringUtils; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.UnmodifiableView; +import org.jetbrains.annotations.Unmodifiable; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; -public sealed class GitVersionImpl implements GitVersion permits GitVersionImpl.Empty { +public final class GitVersionImpl implements GitVersionInternal { // Git private final boolean strict; private Git git; @@ -43,13 +43,11 @@ public sealed class GitVersionImpl implements GitVersion permits GitVersionImpl. public final String localPath; // Config - private String tagPrefix; - private String[] filters; + // NOTE: These are not calculated lazily because they are used in both info gen and changelog gen + private final String tagPrefix; + private final List filters; private final List subprojects; - // Unmodifiable views - private final Lazy> filtersView = Lazy.of(() -> this.filters.length == 0 ? Collections.emptyList() : List.of(this.filters)); - public GitVersionImpl(File gitDir, File root, File project, GitVersionConfig config, boolean strict) { this.strict = strict; @@ -68,14 +66,12 @@ public GitVersionImpl(File gitDir, File root, File project, GitVersionConfig con throw new IllegalArgumentException("Invalid configuration", e); } - this.localPath = GitVersion.super.getProjectPath(); - var projectConfig = config.getProject(this.localPath); - if (projectConfig == null) - throw new IllegalArgumentException("Subproject '%s' is not configured in the git version config! An entry for it must exist.".formatted(this.localPath)); + this.localPath = GitVersionInternal.super.getProjectPath(); + var projectConfig = Objects.requireNonNull(config.getProject(this.localPath)); - this.tagPrefix = this.setTagPrefixInternal(projectConfig.getTagPrefix()); - this.filters = this.setFiltersInternal(projectConfig.getFilters()); - this.subprojects = this.setSubprojectsInternal(config.getAllProjects()); + this.tagPrefix = this.makeTagPrefix(projectConfig.getTagPrefix()); + this.filters = this.makeFilters(projectConfig.getFilters()); + this.subprojects = this.makeSubprojects(config.getAllProjects()); } @@ -192,7 +188,7 @@ public record Info( String getBranch, String getCommit, String getAbbreviatedId - ) implements GitVersion.Info { + ) implements GitVersionInternal.Info { private static final Info EMPTY = new Info("0.0", "0", "00000000", "master", "0000000000000000000000", "00000000"); } @@ -204,13 +200,7 @@ public String getTagPrefix() { return this.tagPrefix; } - @Override - public void setTagPrefix(String tagPrefix) { - this.tagPrefix = this.setTagPrefixInternal(tagPrefix); - this.info.reset(); - } - - private String setTagPrefixInternal(String tagPrefix) { + private String makeTagPrefix(String tagPrefix) { // String#isBlank in case a weird freak accident where the string has empty (space) characters if (StringUtils.isEmptyOrNull(tagPrefix) || tagPrefix.isBlank()) return ""; @@ -219,19 +209,17 @@ private String setTagPrefixInternal(String tagPrefix) { } @Override - public @UnmodifiableView Collection getFilters() { - return this.filtersView.get(); - } - - @Override - public void setFilters(String... filters) { - this.filters = this.setFiltersInternal(filters); - this.filtersView.reset(); - this.info.reset(); + public @Unmodifiable Collection getFilters() { + return this.filters; } - private String[] setFiltersInternal(String... filters) { - return Arrays.stream(filters).filter(s -> s.length() > (s.startsWith("!") ? 1 : 0)).toArray(String[]::new); + private @Unmodifiable List makeFilters(String... filters) { + var list = new ArrayList(filters.length); + for (var s : filters) { + if (s.length() > (s.startsWith("!") ? 1 : 0)) + list.add(s); + } + return Collections.unmodifiableList(list); } @@ -261,11 +249,11 @@ public String getProjectPath() { /* SUBPROJECTS */ @Override - public @UnmodifiableView Collection getSubprojects() { + public @Unmodifiable Collection getSubprojects() { return this.subprojects; } - private List setSubprojectsInternal(Collection projects) { + private @Unmodifiable List makeSubprojects(Collection projects) { var ret = new ArrayList(projects.size()); for (var project : projects) { var file = new File(this.root, project.getPath()).getAbsoluteFile(); @@ -291,6 +279,22 @@ private int getSubprojectCommitCount(Git git, String tag) { } } + private final Lazy> subprojectPathsFromRoot = Lazy.of(() -> this.makeSubprojectPaths(true)); + private final Lazy> subprojectPathsFromProject = Lazy.of(() -> this.makeSubprojectPaths(false)); + + private List makeSubprojectPaths(boolean fromRoot) { + return this.getSubprojects() + .stream() + .map(dir -> GitUtils.getRelativePath(fromRoot ? this.getRoot() : this.getProject(), dir)) + .filter(Predicate.not(String::isBlank)) + .toList(); + } + + @Override + public Collection getSubprojectPaths(boolean fromRoot) { + return fromRoot ? this.subprojectPathsFromRoot.get() : this.subprojectPathsFromProject.get(); + } + /* REPOSITORY */ @@ -319,40 +323,18 @@ public void close() { /* EMPTY */ - public static GitVersion empty() { - return new Empty(); - } - - public static GitVersion emptyFor(File project) { - return project == null ? empty() : new Empty() { - @Override - public File getProject() { - return project; - } - }; - } - - private GitVersionImpl() { - this.strict = false; - this.gitDir = null; - this.root = null; - this.project = null; - this.localPath = ""; - this.tagPrefix = ""; - this.filters = new String[0]; - this.subprojects = Collections.emptyList(); + public static GitVersion empty(@Nullable File project) { + return new Empty(project); } - static non-sealed class Empty extends GitVersionImpl { - private Empty() { } - + public record Empty(@Nullable File project) implements GitVersionInternal { @Override public String generateChangelog(@Nullable String start, @Nullable String url, boolean plainText) throws GitVersionException { throw new GitVersionExceptionInternal("Cannot generate a changelog without a repository"); } @Override - public GitVersion.Info getInfo() { + public Info getInfo() throws GitVersionException { return GitVersionImpl.Info.EMPTY; } @@ -367,7 +349,7 @@ public String getTagPrefix() { } @Override - public @UnmodifiableView Collection getFilters() { + public @Unmodifiable Collection getFilters() { throw new GitVersionExceptionInternal("Cannot get filters from an empty repository"); } @@ -383,17 +365,23 @@ public File getRoot() { @Override public File getProject() { + if (this.project != null) + return this.project; + throw new GitVersionExceptionInternal("Cannot get project directory without a project"); } @Override - public String getProjectPath() { - throw new GitVersionExceptionInternal("Cannot get project path from root with an empty repository"); + public @Unmodifiable Collection getSubprojects() { + throw new GitVersionExceptionInternal("Cannot get subprojects from an empty repository"); } @Override - public @UnmodifiableView Collection getSubprojects() { + public Collection getSubprojectPaths(boolean fromRoot) { throw new GitVersionExceptionInternal("Cannot get subprojects from an empty repository"); } + + @Override + public void close() { } } } diff --git a/src/main/java/net/minecraftforge/gitver/internal/GitVersionInternal.java b/src/main/java/net/minecraftforge/gitver/internal/GitVersionInternal.java new file mode 100644 index 0000000..bb49ff2 --- /dev/null +++ b/src/main/java/net/minecraftforge/gitver/internal/GitVersionInternal.java @@ -0,0 +1,270 @@ +package net.minecraftforge.gitver.internal; + +import net.minecraftforge.gitver.api.GitVersion; +import net.minecraftforge.gitver.api.GitVersionConfig; +import net.minecraftforge.gitver.api.GitVersionException; +import org.eclipse.jgit.util.StringUtils; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@NotNullByDefault +public non-sealed interface GitVersionInternal extends GitVersion { + List DEFAULT_ALLOWED_BRANCHES = List.of("master", "main", "HEAD"); + /* BUILDER */ + + static GitVersion.Builder builder() { + return new Builder(); + } + + final class Builder implements GitVersion.Builder { + private @Nullable File gitDir; + private @Nullable File root; + private @Nullable File project; + private @Nullable GitVersionConfig config; + private boolean strict = true; + + private Builder() { } + + @Override + public GitVersion.Builder gitDir(@UnknownNullability File gitDir) { + this.gitDir = gitDir; + return this; + } + + @Override + public GitVersion.Builder root(@UnknownNullability File root) { + this.root = root != null ? root.getAbsoluteFile() : null; + return this; + } + + @Override + public GitVersion.Builder project(@UnknownNullability File project) { + this.project = project != null ? project.getAbsoluteFile() : null; + return this; + } + + @Override + public GitVersion.Builder config(@UnknownNullability File config) { + this.config = config != null ? GitVersionConfig.parse(config) : null; + return this; + } + + @Override + public GitVersion.Builder config(GitVersionConfig config) { + this.config = config; + return this; + } + + @Override + public GitVersion.Builder strict(boolean strict) { + this.strict = strict; + return this; + } + + @Override + public GitVersion build() { + if (this.root == null && this.project == null) + throw new IllegalArgumentException("Either the root or project directory must be set"); + + if (this.root == null) + this.root = findGitRoot(this.project); + + if (this.project == null) + this.project = this.root; + + if (this.gitDir == null) + this.gitDir = new File(this.root, ".git"); + + if (this.config == null) + this.config = GitVersionConfig.parse(new File(this.root, ".gitversion")); + + try { + if (this.project.compareTo(this.root) > 0 && this.config.getProject(GitUtils.getRelativePath(this.root, this.project)) == null) { + for (var project = this.project.getParentFile(); project.compareTo(this.root) >= 0; project = project.getParentFile()) { + if (this.config.getProject(GitUtils.getRelativePath(this.root, project)) != null) { + this.project = project; + break; + } + } + } + + return new GitVersionImpl(this.gitDir, this.root, this.project, this.config, this.strict); + } catch (GitVersionException e) { + if (!this.strict) + return GitVersionImpl.empty(this.project); + + throw e; + } + } + } + + + /* VERSIONING */ + + @Override + default String getTagOffset() { + var info = this.getInfo(); + return "%s.%s".formatted(info.getTag(), info.getOffset()); + } + + @Override + default String getTagOffsetBranch() { + return this.getTagOffsetBranch(DEFAULT_ALLOWED_BRANCHES); + } + + @Override + default String getTagOffsetBranch(@UnknownNullability String... allowedBranches) { + return this.getTagOffsetBranch(Arrays.asList(Util.ensure(allowedBranches))); + } + + @Override + default String getTagOffsetBranch(@UnknownNullability Collection allowedBranches) { + allowedBranches = Util.ensure(allowedBranches); + var version = this.getTagOffset(); + if (allowedBranches.isEmpty()) return version; + + var branch = this.getInfo().getBranch(true); + return StringUtils.isEmptyOrNull(branch) || allowedBranches.contains(branch) ? version : "%s-%s".formatted(version, branch); + } + + @Override + default String getMCTagOffsetBranch(@UnknownNullability String mcVersion) { + if (StringUtils.isEmptyOrNull(mcVersion)) + return this.getTagOffsetBranch(); + + var allowedBranches = new ArrayList<>(DEFAULT_ALLOWED_BRANCHES); + allowedBranches.add(mcVersion); + allowedBranches.add(mcVersion + ".0"); + allowedBranches.add(mcVersion + ".x"); + allowedBranches.add(mcVersion.substring(0, mcVersion.lastIndexOf('.')) + ".x"); + + return this.getMCTagOffsetBranch(mcVersion, allowedBranches); + } + + @Override + default String getMCTagOffsetBranch(String mcVersion, String... allowedBranches) { + return this.getMCTagOffsetBranch(mcVersion, Arrays.asList(allowedBranches)); + } + + @Override + default String getMCTagOffsetBranch(String mcVersion, Collection allowedBranches) { + return "%s-%s".formatted(mcVersion, this.getTagOffsetBranch(allowedBranches)); + } + + + /* INFO */ + + /** + * Represents information about a git repository. This can be used to access other information when the standard + * versioning methods in {@link GitVersion} do not suffice. + */ + @NotNullByDefault + non-sealed interface Info extends GitVersion.Info { + @Override + default String getBranch(boolean versionFriendly) { + var branch = this.getBranch(); + if (!versionFriendly || branch.isBlank()) return branch; + + if (branch.startsWith("pulls/")) + branch = "pr" + branch.substring(branch.lastIndexOf('/') + 1); + return branch.replaceAll("[\\\\/]", "-"); + } + } + + + /* FILE SYSTEM */ + + @Override + default String getProjectPath() { + return GitUtils.getRelativePath(this.getRoot(), this.getProject()); + } + + @Override + default String getRelativePath(File file) { + return this.getRelativePath(false, file); + } + + @Override + default String getRelativePath(boolean fromRoot, File file) { + return GitUtils.getRelativePath(fromRoot ? this.getRoot() : this.getProject(), file); + } + + private static File findGitRoot(File from) { + for (var dir = from.getAbsoluteFile(); dir != null; dir = dir.getParentFile()) + if (isGitRoot(dir)) return dir; + + return from; + } + + private static boolean isGitRoot(File dir) { + return new File(dir, ".git").exists(); + } + + + /* SERIALIZATION */ + + default String toJson() { + return Util.toJson(new Output(this)); + } + + record Output( + GitVersion.Info info, + @Nullable String url, + + @Nullable String gitDirPath, + @Nullable String rootPath, + @Nullable String projectPath, + + @Nullable String tagPrefix, + List filters, + List subprojectPaths + ) implements GitVersion.Output { + public Output( + GitVersion.Info info, + @Nullable String url, + + @Nullable File gitDir, + @Nullable File root, + @Nullable File project, + + @Nullable String tagPrefix, + @Nullable Collection filters, + @Nullable Collection subprojectPaths + ) { + this( + info, + url, + + gitDir != null ? gitDir.getAbsolutePath() : null, + root != null ? root.getAbsolutePath() : null, + project != null ? project.getAbsolutePath() : null, + + tagPrefix, + filters != null ? List.copyOf(filters) : List.of(), + subprojectPaths != null ? List.copyOf(subprojectPaths) : List.of() + ); + } + + public Output(GitVersion gitVersion) { + this( + gitVersion.getInfo(), + gitVersion.getUrl(), + + Util.tryOrNull(gitVersion::getGitDir), + Util.tryOrNull(gitVersion::getRoot), + Util.tryOrNull(gitVersion::getProject), + + Util.tryOrNull(gitVersion::getTagPrefix), + Util.tryOrNull(gitVersion::getFilters), + Util.tryOrNull(gitVersion::getSubprojectPaths) + ); + } + } +} diff --git a/src/main/java/net/minecraftforge/gitver/internal/Util.java b/src/main/java/net/minecraftforge/gitver/internal/Util.java index 48ab6ed..e132e54 100644 --- a/src/main/java/net/minecraftforge/gitver/internal/Util.java +++ b/src/main/java/net/minecraftforge/gitver/internal/Util.java @@ -4,19 +4,25 @@ */ package net.minecraftforge.gitver.internal; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; +import com.google.gson.stream.JsonReader; import org.jetbrains.annotations.Nullable; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.Callable; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.function.UnaryOperator; -public interface Util { +public final class Util { /** * Hacky method to throw a checked exception without declaring it. * @@ -157,7 +163,38 @@ static String[] ensure(String[] array) { return array != null ? array : new String[0]; } - static Collection ensure(@Nullable Collection collection) { + public static Collection ensure(@Nullable Collection collection) { return collection != null ? collection : Collections.emptyList(); } + + public static @Nullable T tryOrNull(Callable<@Nullable T> callable) { + try { + return callable.call(); + } catch (Exception e) { + return null; + } + } + + private static final Gson GSON = new GsonBuilder() + .setObjectToNumberStrategy(Util::readNumber) + .setPrettyPrinting() + .create(); + + private static Number readNumber(JsonReader in) throws IOException { + try { + return ToNumberPolicy.LONG_OR_DOUBLE.readNumber(in); + } catch (Throwable suppressed) { + try { + return ToNumberPolicy.BIG_DECIMAL.readNumber(in); + } catch (Throwable e) { + IOException throwing = new IOException("Failed to read number from " + in, e); + throwing.addSuppressed(suppressed); + throw throwing; + } + } + } + + public static String toJson(Object src) { + return GSON.toJson(src); + } }