From 59a8e5c4fdaa161cfcba3af32207e456b34c378c Mon Sep 17 00:00:00 2001 From: John Burns Date: Fri, 6 Mar 2026 09:43:10 -0600 Subject: [PATCH] fix configuration cache compatibility in scm info plugin add archrules to track deprecation usage --- build.gradle.kts | 17 +- gradle.lockfile | 53 +++-- gradle/gradle-daemon-jvm.properties | 1 + settings.gradle | 7 + .../NebulaGradleInfoPluginArchRules.java | 49 +++++ .../info/scm/AbstractScmProvider.groovy | 92 ++++---- .../plugin/info/scm/GitScmProvider.groovy | 102 +++++---- .../info/scm/PerforceScmProvider.groovy | 169 +++++++-------- .../plugin/info/scm/ScmInfoPlugin.groovy | 32 +-- .../plugin/info/scm/ScmInfoProvider.java | 73 ++++++- .../plugin/info/scm/UnknownScmProvider.groovy | 31 +-- .../info/scm/CurrentBranchGitCommand.java | 18 ++ .../plugin/info/scm/FullChangeGitCommand.java | 14 ++ .../info/scm/GetRemoteOriginGitCommand.java | 17 ++ .../plugin/info/scm/GitCommandParameters.java | 10 + .../plugin/info/scm/GitReadCommand.java | 48 +++++ .../info/scm/GitReadOnlyCommandUtil.java | 63 ++++++ .../info/scm/ModuleSourceGitCommand.java | 19 ++ .../scm/PerforceScmProviderLocalSpec.groovy | 60 ------ .../info/scm/PerforceScmProviderSpec.groovy | 157 -------------- .../plugin/info/scm/ScmInfoPluginTest.groovy | 8 +- .../kotlin/nebula/plugin/info/TestUtil.kt | 13 ++ .../ci/ContinuousIntegrationInfoPluginTest.kt | 20 +- .../plugin/info/scm/GitScmProviderTest.kt | 49 +++++ .../info/scm/PerforceScmProviderTest.kt | 198 ++++++++++++++++++ 25 files changed, 856 insertions(+), 464 deletions(-) create mode 100644 gradle/gradle-daemon-jvm.properties create mode 100644 src/archRules/java/nebula/plugin/info/archrules/NebulaGradleInfoPluginArchRules.java create mode 100644 src/main/java/nebula/plugin/info/scm/CurrentBranchGitCommand.java create mode 100644 src/main/java/nebula/plugin/info/scm/FullChangeGitCommand.java create mode 100644 src/main/java/nebula/plugin/info/scm/GetRemoteOriginGitCommand.java create mode 100644 src/main/java/nebula/plugin/info/scm/GitCommandParameters.java create mode 100644 src/main/java/nebula/plugin/info/scm/GitReadCommand.java create mode 100644 src/main/java/nebula/plugin/info/scm/GitReadOnlyCommandUtil.java create mode 100644 src/main/java/nebula/plugin/info/scm/ModuleSourceGitCommand.java delete mode 100644 src/test/groovy/nebula/plugin/info/scm/PerforceScmProviderLocalSpec.groovy delete mode 100644 src/test/groovy/nebula/plugin/info/scm/PerforceScmProviderSpec.groovy create mode 100644 src/test/kotlin/nebula/plugin/info/TestUtil.kt create mode 100644 src/test/kotlin/nebula/plugin/info/scm/GitScmProviderTest.kt create mode 100644 src/test/kotlin/nebula/plugin/info/scm/PerforceScmProviderTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 9470ebd..27586ee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.plugin.compatibility.compatibility + /* * Copyright 2014-2019 Netflix, Inc. * @@ -15,8 +17,9 @@ */ plugins { - id("com.netflix.nebula.plugin-plugin") version ("25.+") + id("com.netflix.nebula.plugin-plugin") `kotlin-dsl` + id("com.netflix.nebula.archrules.library") } description = "Gradle plugin collect and provide information about the environment" @@ -41,6 +44,8 @@ dependencies { testImplementation("org.spockframework:spock-junit4:2.4-groovy-4.0") testImplementation("org.eclipse.jgit:org.eclipse.jgit:7.+") testImplementation(libs.assertj) + + archRulesImplementation("com.netflix.nebula:archrules-common:0.+") } testing { @@ -87,6 +92,11 @@ gradlePlugin { description = project.description implementationClass = "nebula.plugin.info.ci.ContinuousIntegrationInfoPlugin" tags.set(listOf("nebula", "info")) + compatibility { + features { + configurationCache = true + } + } } create("infoJAR") { id = "com.netflix.nebula.info-jar" @@ -129,6 +139,11 @@ gradlePlugin { description = project.description implementationClass = "nebula.plugin.info.scm.ScmInfoPlugin" tags.set(listOf("nebula", "info")) + compatibility { + features { + configurationCache = true + } + } } } } diff --git a/gradle.lockfile b/gradle.lockfile index 0de7b26..0846cbd 100644 --- a/gradle.lockfile +++ b/gradle.lockfile @@ -4,18 +4,21 @@ cglib:cglib-nodep:3.2.2=testRuntimeClasspath com.github.stefanbirkner:system-rules:1.19.0=testCompileClasspath,testRuntimeClasspath com.googlecode.javaewah:JavaEWAH:1.2.3=testCompileClasspath,testRuntimeClasspath -com.jcraft:jzlib:1.1.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.netflix.nebula:gradle-contacts-plugin:8.1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.netflix.nebula:nebula-gradle-interop:3.1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.netflix.nebula:nebula-test:11.11.3=testCompileClasspath,testRuntimeClasspath -com.perforce:p4java:2015.2.1365273=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.jcraft:jzlib:1.1.2=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.netflix.nebula:archrules-common:0.9.0=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +com.netflix.nebula:gradle-contacts-plugin:8.1.0=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.netflix.nebula:nebula-archrules-core:0.14.2=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +com.netflix.nebula:nebula-gradle-interop:3.1.0=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.netflix.nebula:nebula-test:11.12.0=testCompileClasspath,testRuntimeClasspath +com.perforce:p4java:2015.2.1365273=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.4.1=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath commons-codec:commons-codec:1.20.0=testCompileClasspath,testRuntimeClasspath io.leangen.geantyref:geantyref:1.3.16=testRuntimeClasspath junit:junit-dep:4.11=testCompileClasspath,testRuntimeClasspath junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.18.3=testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna-platform:5.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.16.0=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.16.0=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ajoberstar.grgit:grgit-core:4.1.1=testCompileClasspath,testRuntimeClasspath org.apache.groovy:groovy-bom:4.0.29=testCompileClasspath,testRuntimeClasspath org.apache.groovy:groovy:4.0.29=testCompileClasspath,testRuntimeClasspath @@ -26,21 +29,29 @@ org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath org.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath org.jetbrains.kotlin:kotlin-reflect:2.2.20=compileClasspath,embeddedKotlin,testCompileClasspath,testRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib:2.2.0=runtimeClasspath -org.jetbrains.kotlin:kotlin-stdlib:2.2.20=compileClasspath,embeddedKotlin,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:13.0=compileClasspath,embeddedKotlin,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jspecify:jspecify:1.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.14.1=testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter:5.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.14.1=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-launcher:1.14.1=testCompileClasspath,testRuntimeClasspath -org.junit:junit-bom:5.14.1=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.20=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,compileClasspath,embeddedKotlin,testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,compileClasspath,embeddedKotlin,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jspecify:jspecify:1.0.0=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.14.3=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.14.3=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.14.3=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.14.3=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.platform:junit-platform-commons:1.14.3=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.platform:junit-platform-engine:1.14.3=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=archRulesTestRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.14.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.12.2=archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit:junit-bom:5.14.3=testCompileClasspath,testRuntimeClasspath org.objenesis:objenesis:2.4=testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:2.0.17=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.spockframework:spock-bom:2.4-groovy-4.0=testCompileClasspath,testRuntimeClasspath org.spockframework:spock-core:2.4-groovy-4.0=testCompileClasspath,testRuntimeClasspath org.spockframework:spock-junit4:2.4-groovy-4.0=testCompileClasspath,testRuntimeClasspath -empty=annotationProcessor,testAnnotationProcessor +empty=annotationProcessor,archRulesAnnotationProcessor,archRulesTestAnnotationProcessor,testAnnotationProcessor diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..c18ca59 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1 @@ +toolchainVersion=21 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index c2f2455..4f6bbd7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,10 @@ +pluginManagement { + plugins { + id("com.netflix.nebula.plugin-plugin") version ("25.+") + id("com.netflix.nebula.archrules.library") version ("0.+") + } +} + plugins { id "com.gradle.develocity" version "4.2" } diff --git a/src/archRules/java/nebula/plugin/info/archrules/NebulaGradleInfoPluginArchRules.java b/src/archRules/java/nebula/plugin/info/archrules/NebulaGradleInfoPluginArchRules.java new file mode 100644 index 0000000..c14c0ee --- /dev/null +++ b/src/archRules/java/nebula/plugin/info/archrules/NebulaGradleInfoPluginArchRules.java @@ -0,0 +1,49 @@ +package nebula.plugin.info.archrules; + +import com.netflix.nebula.archrules.core.ArchRulesService; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.Priority; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import org.jspecify.annotations.NullMarked; + +import java.util.HashMap; +import java.util.Map; + +import static com.netflix.nebula.archrules.common.CanBeAnnotated.Predicates.deprecated; +import static com.netflix.nebula.archrules.common.CanBeAnnotated.Predicates.deprecatedForRemoval; +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.target; +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.targetOwner; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideOutsideOfPackages; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.is; + +@NullMarked +public class NebulaGradleInfoPluginArchRules implements ArchRulesService { + private static final String thisPackage = "nebula.plugin.info.."; + static final ArchRule DEPRECATED = ArchRuleDefinition.priority(Priority.MEDIUM) + .noClasses().that(resideOutsideOfPackages(thisPackage)) + .should().dependOnClassesThat(resideInAPackage(thisPackage) + .and(are(deprecated()))) + .orShould().accessTargetWhere(targetOwner(resideInAPackage(thisPackage)) + .and(target(is(deprecated())))) + .allowEmptyShould(true) + .because("deprecated APIs will be removed in a future version of nebula-gradle-info"); + + static final ArchRule DEPRECATED_FOR_REMOVAL = ArchRuleDefinition.priority(Priority.HIGH) + .noClasses().that(resideOutsideOfPackages(thisPackage)) + .should().dependOnClassesThat(resideInAPackage(thisPackage) + .and(are(deprecatedForRemoval()))) + .orShould().accessTargetWhere(targetOwner(resideInAPackage(thisPackage)) + .and(target(is(deprecatedForRemoval())))) + .allowEmptyShould(true) + .because("deprecated for removal APIs will be removed in the next major version of nebula-gradle-info"); + + @Override + public Map getRules() { + Map rules = new HashMap<>(); + rules.put("nebula-gradle-info deprecated", DEPRECATED); + rules.put("nebula-gradle-info deprecated for removal", DEPRECATED_FOR_REMOVAL); + return rules; + } +} diff --git a/src/main/groovy/nebula/plugin/info/scm/AbstractScmProvider.groovy b/src/main/groovy/nebula/plugin/info/scm/AbstractScmProvider.groovy index 0152b47..e517068 100644 --- a/src/main/groovy/nebula/plugin/info/scm/AbstractScmProvider.groovy +++ b/src/main/groovy/nebula/plugin/info/scm/AbstractScmProvider.groovy @@ -17,11 +17,21 @@ package nebula.plugin.info.scm import org.gradle.api.Project +import org.gradle.api.file.RegularFile import org.gradle.api.provider.ProviderFactory +import org.jspecify.annotations.NullMarked +import org.jspecify.annotations.Nullable - +@NullMarked abstract class AbstractScmProvider implements ScmInfoProvider { - abstract calculateModuleSource(File projectDir) + /** + * @deprecated Use {@link #source()} instead + */ + @Deprecated + @Nullable + String calculateModuleSource(File projectDir) { + source().getOrNull() + } private final ProviderFactory providerFactory @@ -33,53 +43,55 @@ abstract class AbstractScmProvider implements ScmInfoProvider { return this.providerFactory } - @Override - String calculateSource(Project project) { - return calculateModuleSource(project.projectDir) - } - - protected File findFile(File starting, String filename) { - // TODO Stop looking when we get to the home directory, to avoid paths which we know aren't a SCM root - if (!filename) { - return null + /** + * when we convert to kotlin, make sure to optimise with tailrec + */ + @Nullable + protected RegularFile findFile(Project starting, String filename) { + RegularFile file = starting.layout.projectDirectory.file(filename) + if (file.asFile.exists()) { + println("found" +filename + " at " + file.asFile.absolutePath) + return file } - - File dirToLookIn = starting - while(dirToLookIn) { - File p4configFile = new File(dirToLookIn, filename) - if (p4configFile.exists()) { - return p4configFile - } - dirToLookIn = dirToLookIn?.getParentFile() + if (starting.parent == null) { + return null } - return null + return findFile(starting.parent, filename) } - @Override - String calculateOrigin(Project project) { - return calculateModuleOrigin(project.projectDir) + /** + * @deprecated Use {@link #origin()} instead + */ + @Deprecated + @Nullable + String calculateModuleOrigin(File projectDir) { + return origin().getOrNull() } - abstract calculateModuleOrigin(File projectDir) - - @Override - String calculateChange(Project project) { - return calculateChange(project.projectDir) + /** + * @deprecated Use {@link #change()} instead + */ + @Nullable + @Deprecated + String calculateChange(File projectDir) { + change().getOrNull() } - abstract calculateChange(File projectDir) - - @Override - String calculateFullChange(Project project) { - return calculateFullChange(project.projectDir) + /** + * @deprecated Use {@link #fullChange()} instead + */ + @Nullable + @Deprecated + String calculateFullChange(File projectDir) { + return fullChange().getOrNull() } - abstract calculateFullChange(File projectDir) - - @Override - String calculateBranch(Project project) { - return calculateBranch(project.projectDir) + /** + * @deprecated Use {@link #branch()} instead + */ + @Nullable + @Deprecated + String calculateBranch(File projectDir) { + return branch().getOrNull() } - - abstract calculateBranch(File projectDir) } diff --git a/src/main/groovy/nebula/plugin/info/scm/GitScmProvider.groovy b/src/main/groovy/nebula/plugin/info/scm/GitScmProvider.groovy index 49852c2..18c7628 100644 --- a/src/main/groovy/nebula/plugin/info/scm/GitScmProvider.groovy +++ b/src/main/groovy/nebula/plugin/info/scm/GitScmProvider.groovy @@ -16,84 +16,78 @@ package nebula.plugin.info.scm -import groovy.transform.Memoized +import groovy.transform.CompileStatic import org.gradle.api.Project +import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory +import org.jspecify.annotations.NullMarked import org.jspecify.annotations.Nullable -import org.slf4j.Logger -import org.slf4j.LoggerFactory +@CompileStatic +@NullMarked class GitScmProvider extends AbstractScmProvider { - private Logger logger = LoggerFactory.getLogger(GitScmProvider - ) + private boolean supports + private final GitReadOnlyCommandUtil gitUtil + private final File projectDir - GitScmProvider(ProviderFactory providerFactory) { + GitScmProvider(Project project, ProviderFactory providerFactory) { super(providerFactory) + @Nullable + File gitDir = findFile(project, ".git")?.asFile + supports = gitDir != null && gitDir.exists() + if (supports) { + gitUtil = GitReadOnlyCommandUtil.create(gitDir.parentFile, providerFactory) + } else { + gitUtil = GitReadOnlyCommandUtil.create(project.rootProject.layout.projectDirectory.asFile, providerFactory) + } + projectDir = project.layout.projectDirectory.asFile } @Override - boolean supports(Project project) { - return findFile(project.projectDir, '.git') != null + boolean supports() { + return supports } - @Memoized @Override - String calculateModuleOrigin(File projectDir) { - String remoteOriginUrl = executeGitCommand(projectDir, "git", "config", "--get", "remote.origin.url") - if (remoteOriginUrl == null) { - return "LOCAL" - } - try { - URL url = remoteOriginUrl.toURL() - if (url.getUserInfo()) { - def user = url.getUserInfo().split(":").first() - url = new URL(url.protocol, user + "@" + url.host, url.port, url.file) - } - String urlAsExternalForm = url.toExternalForm() - return urlAsExternalForm.endsWith('.git') ? url.toExternalForm() : urlAsExternalForm + ".git" - } catch (MalformedURLException ignore) { - return remoteOriginUrl - } + Provider origin() { + return gitUtil.remoteOrigin() + .map { + try { + URL url = it.toURL() + if (url.getUserInfo()) { + def user = url.getUserInfo().split(":").first() + url = new URL(url.protocol, user + "@" + url.host, url.port, url.file) + } + String urlAsExternalForm = url.toExternalForm() + return urlAsExternalForm.endsWith('.git') ? url.toExternalForm() : urlAsExternalForm + ".git" + } catch (MalformedURLException ignore) { + return it + } + } + .orElse("LOCAL") } @Override - String calculateModuleSource(File projectDir) { - String gitWorkDir = executeGitCommand(projectDir, "git", "rev-parse", "--show-toplevel") - if (!gitWorkDir) { - return projectDir.absolutePath - } - return projectDir.absolutePath - new File(gitWorkDir).absolutePath + Provider source() { + File root = projectDir + return gitUtil.moduleSource() + .map { root.absolutePath - new File(it).absolutePath } + .orElse(projectDir.absolutePath) } @Override - String calculateChange(File projectDir) { - return calculateFullChange(projectDir)?.substring(0, 7) + Provider change() { + return fullChange().map { it.substring(0, 7) } } @Override - String calculateFullChange(File projectDir) { - String hash = providerFactory.environmentVariable('GIT_COMMIT').getOrElse(null) - if (!hash) { - hash = executeGitCommand(projectDir, "git", "rev-parse", "HEAD") - } - return hash + Provider fullChange() { + return providerFactory.environmentVariable('GIT_COMMIT') + .orElse(gitUtil.fullChange()) } @Override - String calculateBranch(File projectDir) { - return executeGitCommand(projectDir, "git", "rev-parse", "--abbrev-ref", "HEAD") - } - - @Nullable - private String executeGitCommand(File projectDir, Object... args) { - try { - return providerFactory.exec { - it.workingDir(projectDir) - it.commandLine(args) - }.standardOutput.asText.get().replaceAll("\n", "").trim() - } catch (Exception e) { - logger.error("Could not execute Git command: ${args.join(' ')}") - return null - } + Provider branch() { + return gitUtil.currentBranch() } } diff --git a/src/main/groovy/nebula/plugin/info/scm/PerforceScmProvider.groovy b/src/main/groovy/nebula/plugin/info/scm/PerforceScmProvider.groovy index 2d49019..25933a5 100644 --- a/src/main/groovy/nebula/plugin/info/scm/PerforceScmProvider.groovy +++ b/src/main/groovy/nebula/plugin/info/scm/PerforceScmProvider.groovy @@ -21,95 +21,87 @@ import com.perforce.p4java.server.IServer import com.perforce.p4java.server.ServerFactory import groovy.transform.PackageScope import org.gradle.api.Project +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory -import org.slf4j.Logger -import org.slf4j.LoggerFactory class PerforceScmProvider extends AbstractScmProvider { + private static final String DEFAULT_WORKSPACE = "." + private final File projectDir + private final Provider configFile - File p4configFile - - private Logger logger = LoggerFactory.getLogger(PerforceScmProvider) - - private static final String DEFAULT_WORKSPACE = '.' - - PerforceScmProvider(ProviderFactory providerFactory) { + PerforceScmProvider(Project project, ProviderFactory providerFactory) { super(providerFactory) + this.projectDir = project.layout.projectDirectory.asFile + configFile = providerFactory.environmentVariable("P4CONFIG").map { + findFile(project, it) + } } @Override - boolean supports(Project project) { + boolean supports() { // Pretty poor way to check, but Perforce leave no indication of where the current tree came from // Better to check git first, since it can make a more intelligent guess // TODO When we can make p4java optional, we'll add a classForName check here. try { boolean hasWorkspaceAndClient = providerFactory.environmentVariable('WORKSPACE').present && - providerFactory.environmentVariable('P4CLIENT').present - boolean hasP4ConfigFile = findFile(project.projectDir, providerFactory.environmentVariable('P4CONFIG').getOrElse(null)) + providerFactory.environmentVariable('P4CLIENT').present + boolean hasP4ConfigFile = configFile.map {it.asFile.exists()}.getOrElse(false) return hasWorkspaceAndClient || hasP4ConfigFile - } catch(Exception e) { + } catch (Exception e) { return false } } @Override - String calculateModuleSource(File projectDir) { - String workspacePath = providerFactory.environmentVariable('WORKSPACE').getOrElse(DEFAULT_WORKSPACE) - if (workspacePath == DEFAULT_WORKSPACE) { - logger.info("WORKSPACE environment variable is not set. Using ${DEFAULT_WORKSPACE}") - } - File workspace = new File(workspacePath) - return calculateModuleSource(workspace, projectDir) - } - - String calculateModuleSource(File workspace, File projectDir) { - // TODO Don't hardcode depot - String relativePath = projectDir.getAbsolutePath() - (workspace.getAbsolutePath() + '/') - return "//depot/${relativePath}" + Provider source() { + File root = projectDir + return providerFactory.environmentVariable("WORKSPACE") + .map { workspace -> root.relativePath(new File(workspace)) + "/" } + .map { "//depot/${it}" } + .orElse(DEFAULT_WORKSPACE) } @Override - String calculateModuleOrigin(File projectDir) { - Map defaults = perforceDefaults(projectDir) - return getUrl(defaults) + Provider origin() { + return getUrl() } @Override - String calculateChange(File projectDir) { - return providerFactory.environmentVariable('P4_CHANGELIST').getOrElse(null) + Provider change() { + return providerFactory.environmentVariable("P4_CHANGELIST") } @Override - def calculateFullChange(File projectDir) { - return calculateChange(projectDir) + Provider fullChange() { + return change() } @Override - String calculateBranch(File projectDir) { - return null // unsupported in perforce + Provider branch() { + return providerFactory.provider { null } } @PackageScope T withPerforce(File projectDir, Closure closure) { - Map defaults = perforceDefaults(projectDir) - String uri = getUrl(defaults) - IServer server = ServerFactory.getServer(uri, null); + String uri = getUrl() + IServer server = ServerFactory.getServer(uri, null) server.connect() - if (defaults.P4PASSWD) { - server.login(defaults.P4PASSWD) + if (getPassword().isPresent() && !getPassword().get().isBlank()) { + server.login(getPassword().get()) } IClient client - if (defaults.P4CLIENT) { - client = server.getClient(defaults.P4CLIENT) + if (getClient().isPresent()) { + client = server.getClient(getClient().get()) if (client != null) { - server.setCurrentClient(client); + server.setCurrentClient(client) } } T ret try { - if (closure.maximumNumberOfParameters==1) { + if (closure.maximumNumberOfParameters == 1) { ret = closure.call(server) } else { if (client == null) { @@ -118,59 +110,70 @@ class PerforceScmProvider extends AbstractScmProvider { ret = closure.call(server, client) } } finally { - if (server!=null) { + if (server != null) { server.disconnect() } } return ret } - @PackageScope - String getUrl(Map defaults) { - // TODO Support SSL - //return "p4java://${defaults.P4USER}${passAppend}@${defaults.P4PORT}" - return "p4java://${defaults.P4PORT}?userName=${defaults.P4USER}" + private Provider getPort() { + configFile.flatMap { + providerFactory.fileContents(it).asBytes.map { + Properties props = new Properties() + try (ByteArrayInputStream is = new ByteArrayInputStream(it)) { + props.load(is) + } + return props.get("P4PORT") + } + }.orElse(providerFactory.environmentVariable("P4PORT")) + .orElse('perforce:1666') } - @PackageScope - Map perforceDefaults(File projectDir) { - // Set some default values then look for overrides - Map defaults = [ - P4CLIENT: null, - P4USER: 'rolem', - P4PASSWD: '', - P4PORT: 'perforce:1666' - ] as Map - - // First look for P4CONFIG name - findP4Config(projectDir) // Might be noop - if (p4configFile) { - Properties props = new Properties() - p4configFile.withInputStream { inputStream -> - props.load(inputStream) + private Provider getUser() { + configFile.flatMap { + providerFactory.fileContents(it).asBytes.map { + Properties props = new Properties() + try (ByteArrayInputStream is = new ByteArrayInputStream(it)) { + props.load(is) + } + return props.get("P4USER") } - defaults = overrideFromMap(defaults, props as Map) - } - - // Second user environment variables - defaults = overrideFromMap(defaults, System.getenv()) + }.orElse(providerFactory.environmentVariable("P4USER")) + .orElse("rolem") + } - return defaults + private Provider getClient() { + configFile.flatMap { + providerFactory.fileContents(it).asBytes.map { + Properties props = new Properties() + try (ByteArrayInputStream is = new ByteArrayInputStream(it)) { + props.load(is) + } + return props.get("P4CLIENT") + } + }.orElse(providerFactory.environmentVariable("P4CLIENT")) + .orElse(null) } - @PackageScope - Map overrideFromMap(Map orig, Map override) { - Map dest = [:] - orig.keySet().each { String key -> - dest[key] = override.keySet().contains(key) ? override[key] : orig[key] - } - return dest + private Provider getPassword() { + configFile.flatMap { + providerFactory.fileContents(it).asBytes.map { bytes -> + Properties props = new Properties() + try (ByteArrayInputStream is = new ByteArrayInputStream(bytes)) { + props.load(is) + } + return props.get("P4PASSWD") + } + }.orElse(providerFactory.environmentVariable("P4PASSWD")) + .orElse("") } - @PackageScope - void findP4Config(File starting) { - if (p4configFile == null) { - p4configFile = findFile(starting, providerFactory.environmentVariable('P4CONFIG').getOrElse(null)) + private Provider getUrl() { + getPort().flatMap { port -> + getUser().map { user -> + return "p4java://${port}?userName=${user}" + } } } } diff --git a/src/main/groovy/nebula/plugin/info/scm/ScmInfoPlugin.groovy b/src/main/groovy/nebula/plugin/info/scm/ScmInfoPlugin.groovy index 83f8885..d87027a 100644 --- a/src/main/groovy/nebula/plugin/info/scm/ScmInfoPlugin.groovy +++ b/src/main/groovy/nebula/plugin/info/scm/ScmInfoPlugin.groovy @@ -44,9 +44,9 @@ class ScmInfoPlugin implements Plugin, InfoCollectorPlugin { @Override void apply(Project project) { configureProviders(project) - if(project.rootProject == project) { + if (project.rootProject == project) { configureWithScmProvider(project) - } else if(project.rootProject != project) { + } else if (project.rootProject != project) { handleSubProject(project) } } @@ -57,7 +57,11 @@ class ScmInfoPlugin implements Plugin, InfoCollectorPlugin { * @param project */ private void configureProviders(Project project) { - providers = [new GitScmProvider(providerFactory), new PerforceScmProvider(providerFactory), new UnknownScmProvider(providerFactory)] as List + providers = [ + new GitScmProvider(project, providerFactory), + new PerforceScmProvider(project, providerFactory), + new UnknownScmProvider(project, providerFactory) + ] as List selectedProvider = findProvider(project) } @@ -69,7 +73,7 @@ class ScmInfoPlugin implements Plugin, InfoCollectorPlugin { */ private void handleSubProject(Project project) { ScmInfoExtension scmInfoRootProjectExtension = project.rootProject.extensions.findByType(ScmInfoExtension) - if(!scmInfoRootProjectExtension) { + if (!scmInfoRootProjectExtension) { configureWithScmProvider(project) } else { configureWithoutScmProvider(project, scmInfoRootProjectExtension) @@ -83,7 +87,7 @@ class ScmInfoPlugin implements Plugin, InfoCollectorPlugin { private void configureWithScmProvider(Project project) { ScmInfoExtension extension = project.extensions.create('scminfo', ScmInfoExtension) project.logger.info("Project $project.name SCM information is being collected from provider ${selectedProvider.class.name}") - configureExtensionFromProvider(project, extension) + configureExtensionFromProvider(extension) configureInfoBrokerManifest(project, extension) } @@ -98,12 +102,12 @@ class ScmInfoPlugin implements Plugin, InfoCollectorPlugin { configureInfoBrokerManifest(project, extension) } - private void configureExtensionFromProvider(Project project, ScmInfoExtension extension) { - extension.origin.convention(providerFactory.provider { selectedProvider.calculateOrigin(project) }) - extension.source.convention(providerFactory.provider { selectedProvider.calculateSource(project)?.replace(File.separatorChar, '/' as char) }) - extension.change.convention(providerFactory.provider { selectedProvider.calculateChange(project) }) - extension.fullChange.convention(providerFactory.provider { selectedProvider.calculateFullChange(project) }) - extension.branch.convention(providerFactory.provider { selectedProvider.calculateBranch(project) }) + private void configureExtensionFromProvider(ScmInfoExtension extension) { + extension.origin.convention(selectedProvider.origin()) + extension.source.convention(selectedProvider.source().map { it.replace(File.separatorChar, '/' as char) }) + extension.change.convention(selectedProvider.change()) + extension.fullChange.convention(selectedProvider.fullChange()) + extension.branch.convention(selectedProvider.branch()) } private void configureExtensionFromRootProject(ScmInfoExtension extension, ScmInfoExtension scmInfoRootProjectExtension) { @@ -119,7 +123,7 @@ class ScmInfoPlugin implements Plugin, InfoCollectorPlugin { * @param project * @param scmInfoExtension */ - private void configureInfoBrokerManifest(Project project, ScmInfoExtension scmInfoExtension ) { + private void configureInfoBrokerManifest(Project project, ScmInfoExtension scmInfoExtension) { project.plugins.withType(InfoBrokerPlugin) { InfoBrokerPlugin manifestPlugin -> manifestPlugin.add(MODULE_SOURCE_PROPERTY) { scmInfoExtension.source.getOrNull() } manifestPlugin.add(MODULE_ORIGIN_PROPERTY) { scmInfoExtension.origin.getOrNull() } @@ -137,11 +141,11 @@ class ScmInfoPlugin implements Plugin, InfoCollectorPlugin { */ ScmInfoProvider findProvider(Project project) { ScmInfoExtension scmInfoRootProjectExtension = project.rootProject.extensions.findByType(ScmInfoExtension) - if(scmInfoRootProjectExtension) { + if (scmInfoRootProjectExtension) { return project.rootProject.plugins.findPlugin(ScmInfoPlugin).selectedProvider } - ScmInfoProvider provider = providers.find { it.supports(project) } + ScmInfoProvider provider = providers.find { it.supports() } if (provider) { return provider } else { diff --git a/src/main/groovy/nebula/plugin/info/scm/ScmInfoProvider.java b/src/main/groovy/nebula/plugin/info/scm/ScmInfoProvider.java index c85bd93..be71483 100644 --- a/src/main/groovy/nebula/plugin/info/scm/ScmInfoProvider.java +++ b/src/main/groovy/nebula/plugin/info/scm/ScmInfoProvider.java @@ -17,20 +17,81 @@ package nebula.plugin.info.scm; import org.gradle.api.Project; +import org.gradle.api.provider.Provider; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Could contribute to idea plugin, once we know the right SCM */ +@NullMarked public interface ScmInfoProvider { /** * Determine support. Attempt to not use a library, to reduce impact and side effect of calling + * * @param project project to validate support against * @return boolean of the provider's availibility to support the current environment. + * @deprecated use {@link #supports()} instead */ - boolean supports(Project project); - String calculateSource(Project project); - String calculateOrigin(Project project); - String calculateChange(Project project); - String calculateFullChange(Project project); - String calculateBranch(Project project); + @Deprecated + default boolean supports(Project project) { + return supports(); + } + + boolean supports(); + + /** + * @deprecated Use {@link #source()} instead + */ + @Deprecated + @Nullable + default String calculateSource(Project project) { + return source().getOrNull(); + } + + /** + * @deprecated Use {@link #origin()} instead + */ + @Deprecated + @Nullable + default String calculateOrigin(Project project) { + return origin().getOrNull(); + } + + /** + * @deprecated Use {@link #change()} instead + */ + @Deprecated + @Nullable + default String calculateChange(Project project) { + return change().getOrNull(); + } + + /** + * @deprecated Use {@link #fullChange()} instead + */ + @Deprecated + @Nullable + default String calculateFullChange(Project project) { + return fullChange().getOrNull(); + } + + /** + * @deprecated Use {@link #branch()} instead + */ + @Deprecated + @Nullable + default String calculateBranch(Project project) { + return branch().getOrNull(); + } + + Provider source(); + + Provider origin(); + + Provider change(); + + Provider fullChange(); + + Provider branch(); } diff --git a/src/main/groovy/nebula/plugin/info/scm/UnknownScmProvider.groovy b/src/main/groovy/nebula/plugin/info/scm/UnknownScmProvider.groovy index dfbb48e..7947533 100644 --- a/src/main/groovy/nebula/plugin/info/scm/UnknownScmProvider.groovy +++ b/src/main/groovy/nebula/plugin/info/scm/UnknownScmProvider.groovy @@ -16,44 +16,51 @@ package nebula.plugin.info.scm +import groovy.transform.CompileStatic import org.gradle.api.Project +import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory +import org.jspecify.annotations.NullMarked +@NullMarked +@CompileStatic class UnknownScmProvider extends AbstractScmProvider { public static final String LOCAL = 'LOCAL' + private final Project project - UnknownScmProvider(ProviderFactory providerFactory) { + UnknownScmProvider(Project project, ProviderFactory providerFactory) { super(providerFactory) + this.project = project } @Override - boolean supports(Project project) { + boolean supports() { return true } @Override - String calculateModuleOrigin(File projectDir) { - return LOCAL + Provider origin() { + return providerFactory.provider {LOCAL} } @Override - String calculateModuleSource(File projectDir) { - return projectDir.absolutePath + Provider source() { + return providerFactory.provider { project.layout.projectDirectory.asFile.absolutePath } } @Override - String calculateChange(File projectDir) { - return null + Provider change() { + return providerFactory.provider { null } } @Override - def calculateFullChange(File projectDir) { - return null + Provider fullChange() { + return providerFactory.provider { null } } @Override - String calculateBranch(File projectDir) { - return null + Provider branch() { + return providerFactory.provider { null } } } diff --git a/src/main/java/nebula/plugin/info/scm/CurrentBranchGitCommand.java b/src/main/java/nebula/plugin/info/scm/CurrentBranchGitCommand.java new file mode 100644 index 0000000..0bc3950 --- /dev/null +++ b/src/main/java/nebula/plugin/info/scm/CurrentBranchGitCommand.java @@ -0,0 +1,18 @@ +package nebula.plugin.info.scm; + +/** + * Returns current branch name + * ex. git rev-parse --abbrev-ref HEAD -> configuration-cache-support + */ +public abstract class CurrentBranchGitCommand extends GitReadCommand { + + @Override + public String obtain() { + try { + return executeGitCommand("rev-parse", "--abbrev-ref", "HEAD") + .replaceAll("\n", "").trim(); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/nebula/plugin/info/scm/FullChangeGitCommand.java b/src/main/java/nebula/plugin/info/scm/FullChangeGitCommand.java new file mode 100644 index 0000000..9b14470 --- /dev/null +++ b/src/main/java/nebula/plugin/info/scm/FullChangeGitCommand.java @@ -0,0 +1,14 @@ +package nebula.plugin.info.scm; + +public abstract class FullChangeGitCommand extends GitReadCommand { + + @Override + public String obtain() { + try { + return executeGitCommand("rev-parse", "HEAD") + .replaceAll("\n", "").trim(); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/nebula/plugin/info/scm/GetRemoteOriginGitCommand.java b/src/main/java/nebula/plugin/info/scm/GetRemoteOriginGitCommand.java new file mode 100644 index 0000000..0aea384 --- /dev/null +++ b/src/main/java/nebula/plugin/info/scm/GetRemoteOriginGitCommand.java @@ -0,0 +1,17 @@ +package nebula.plugin.info.scm; + +/** + * Returns remote origin url + * ex. git config --get remote.origin.url + */ +public abstract class GetRemoteOriginGitCommand extends GitReadCommand { + @Override + public String obtain() { + try { + return executeGitCommand("config", "--get", "remote.origin.url") + .replaceAll("\n", "").trim(); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/nebula/plugin/info/scm/GitCommandParameters.java b/src/main/java/nebula/plugin/info/scm/GitCommandParameters.java new file mode 100644 index 0000000..66d5d0d --- /dev/null +++ b/src/main/java/nebula/plugin/info/scm/GitCommandParameters.java @@ -0,0 +1,10 @@ +package nebula.plugin.info.scm; + +import org.gradle.api.provider.Property; +import org.gradle.api.provider.ValueSourceParameters; + +import java.io.File; + +interface GitCommandParameters extends ValueSourceParameters { + Property getRootDir(); +} diff --git a/src/main/java/nebula/plugin/info/scm/GitReadCommand.java b/src/main/java/nebula/plugin/info/scm/GitReadCommand.java new file mode 100644 index 0000000..df5db09 --- /dev/null +++ b/src/main/java/nebula/plugin/info/scm/GitReadCommand.java @@ -0,0 +1,48 @@ +package nebula.plugin.info.scm; + +import org.gradle.api.GradleException; +import org.gradle.api.provider.ValueSource; +import org.gradle.process.ExecOperations; + +import javax.inject.Inject; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * These read only git commands use ValueSource approach for configuration cache + * + * @see Gradle docs + */ +abstract class GitReadCommand implements ValueSource { + @Inject + public abstract ExecOperations getExecOperations(); + + /** + * Execute a git command with the given arguments + */ + String executeGitCommand(String... args) { + File rootDir = getParameters().getRootDir().get(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ByteArrayOutputStream error = new ByteArrayOutputStream(); + String gitDir = rootDir.toPath().toAbsolutePath().resolve(".git").toString(); + List commandLineArgs = new ArrayList<>( + Arrays.asList("git", + "--git-dir=" + gitDir, + "--work-tree=" + rootDir.getAbsolutePath())); + commandLineArgs.addAll(Arrays.asList(args)); + getExecOperations().exec(execSpec -> { + execSpec.setCommandLine(commandLineArgs); + execSpec.setStandardOutput(output); + execSpec.setErrorOutput(error); + }); + String errorMsg = new String(error.toByteArray(), Charset.defaultCharset()); + if (!errorMsg.isEmpty()) { + throw new GradleException(output + " " + errorMsg); + } + return new String(output.toByteArray(), Charset.defaultCharset()); + } +} \ No newline at end of file diff --git a/src/main/java/nebula/plugin/info/scm/GitReadOnlyCommandUtil.java b/src/main/java/nebula/plugin/info/scm/GitReadOnlyCommandUtil.java new file mode 100644 index 0000000..59399ed --- /dev/null +++ b/src/main/java/nebula/plugin/info/scm/GitReadOnlyCommandUtil.java @@ -0,0 +1,63 @@ +package nebula.plugin.info.scm; + +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.Serializable; + +/** + * Utility class for executing read-only Git operations using Gradle's Provider API. + * This class provides a safe, caching-friendly way to access Git repository information + * during build execution without modifying the repository state. + * + *

The utility supports configuration cache by using Gradle's Provider system + * for executing Git commands. All Git operations are delegated to specialized + * command classes that implement proper parameter passing and caching behavior.

+ */ +public class GitReadOnlyCommandUtil implements Serializable { + private final Provider currentBranchProvider; + private final Provider fullChangeProvider; + private final Provider remoteOriginProvider; + private final Provider moduleSourceProvider; + + public static GitReadOnlyCommandUtil create(File rootDir, ProviderFactory providerFactory) { + return new GitReadOnlyCommandUtil(rootDir, providerFactory); + } + + GitReadOnlyCommandUtil(File rootDir, ProviderFactory providerFactory) { + this.currentBranchProvider = providerFactory.of(CurrentBranchGitCommand.class, it -> + it.parameters(params -> params.getRootDir().set(rootDir))); + this.remoteOriginProvider = providerFactory.of(GetRemoteOriginGitCommand.class, it -> + it.parameters(params -> params.getRootDir().set(rootDir))); + this.fullChangeProvider = providerFactory.of(FullChangeGitCommand.class, it -> + it.parameters(params -> params.getRootDir().set(rootDir))); + this.moduleSourceProvider = providerFactory.of(ModuleSourceGitCommand.class, it -> + it.parameters(params -> params.getRootDir().set(rootDir))); + } + + /** + * Get the current branch of the git repository + */ + public Provider currentBranch() { + return currentBranchProvider.map(it -> it.replaceAll("\n", "").trim()); + } + + public Provider moduleSource() { + return moduleSourceProvider.map(it -> it.replaceAll("\n", "").trim()); + } + + public Provider fullChange() { + return fullChangeProvider.map(it -> it.replaceAll("\n", "").trim()); + } + + /** + * Get the remote origin of the git repository + */ + public Provider remoteOrigin() { + return remoteOriginProvider.map(it -> it.replaceAll("\n", "").trim()); + } + +} diff --git a/src/main/java/nebula/plugin/info/scm/ModuleSourceGitCommand.java b/src/main/java/nebula/plugin/info/scm/ModuleSourceGitCommand.java new file mode 100644 index 0000000..352826c --- /dev/null +++ b/src/main/java/nebula/plugin/info/scm/ModuleSourceGitCommand.java @@ -0,0 +1,19 @@ +package nebula.plugin.info.scm; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class ModuleSourceGitCommand extends GitReadCommand { + private static final Logger log = LoggerFactory.getLogger(ModuleSourceGitCommand.class); + + @Override + public String obtain() { + try { + return executeGitCommand("rev-parse", "--show-toplevel") + .replaceAll("\n", "").trim(); + } catch (Exception e) { + log.info(e.getMessage()); + return null; + } + } +} diff --git a/src/test/groovy/nebula/plugin/info/scm/PerforceScmProviderLocalSpec.groovy b/src/test/groovy/nebula/plugin/info/scm/PerforceScmProviderLocalSpec.groovy deleted file mode 100644 index 7ae269d..0000000 --- a/src/test/groovy/nebula/plugin/info/scm/PerforceScmProviderLocalSpec.groovy +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2014-2019 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package nebula.plugin.info.scm - -import com.perforce.p4java.client.IClient -import com.perforce.p4java.impl.generic.client.ClientView -import com.perforce.p4java.server.IServer -import nebula.test.ProjectSpec -import spock.lang.Ignore - -@Ignore -class PerforceScmProviderLocalSpec extends ProjectSpec { - def provider = new PerforceScmProvider() - - def 'connect to perforce'() { - when: - ClientView view = provider.withPerforce(projectDir) { IServer server -> - IClient client = server.getClient('jryan_uber'); - ClientView view = client.getClientView() - return view - } - - then: - view.size > 5 - } - - def 'calculate module status'() { - setup: - def config = new File(projectDir, 'p4config') - config.text = "P4CLIENT=jryan_uber\nP4USER=jryan" - provider.p4configFile = config - def workspace = new File('/Users/jryan/Workspaces/jryan_uber') - def fakeProjectDir = new File("/Users/jryan/Workspaces/jryan_uber/Tools/nebula-boot") - - when: - String mapped = provider.calculateModuleSource(workspace, fakeProjectDir) - - then: - mapped == '//depot/Tools/nebula-boot' - - when: - String origin = provider.calculateModuleOrigin(fakeProjectDir) - - then: - origin == 'p4java://perforce:1666?userName=jryan' - } -} diff --git a/src/test/groovy/nebula/plugin/info/scm/PerforceScmProviderSpec.groovy b/src/test/groovy/nebula/plugin/info/scm/PerforceScmProviderSpec.groovy deleted file mode 100644 index 6f88eae..0000000 --- a/src/test/groovy/nebula/plugin/info/scm/PerforceScmProviderSpec.groovy +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2014-2019 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package nebula.plugin.info.scm - -import org.gradle.api.provider.Provider -import org.gradle.api.provider.ProviderFactory -import org.junit.Rule -import org.junit.contrib.java.lang.system.EnvironmentVariables -import org.junit.rules.TemporaryFolder -import spock.lang.IgnoreIf -import spock.lang.Specification - -@IgnoreIf({ !jvm.isJava8() }) -class PerforceScmProviderSpec extends Specification { - @Rule - TemporaryFolder temp - - ProviderFactory providerFactoryMock = Mock(ProviderFactory) - Provider providerStringMock = Mock(Provider) - - def provider = new PerforceScmProvider(providerFactoryMock) - - @Rule - public final EnvironmentVariables environmentVariables = new EnvironmentVariables() - - def 'lookup settings'() { - setup: - def orig = [P4CONFIG: 'p4config', P4USER: 'jryan'] - def dest = [P4USER: 'aeinstein'] - - when: - def result = provider.overrideFromMap(orig, dest) - - then: - result.P4CONFIG == 'p4config' - result.P4USER == 'aeinstein' - } - - def 'find file in project direct'() { - setup: - def projectDir = temp.newFolder() - def deep = new File(projectDir, "level1/level2/level3") - deep.mkdirs() - def configName = 'p4config' - def config = new File(projectDir, configName) - config.text = "something" - - when: - def found = provider.findFile(deep, configName) - - then: - found.exists() - found == config - - when: - def notfound = provider.findFile(deep, "fake") - - then: - notfound == null - } - - def 'find perforce defaults'() { - setup: - def projectDir = temp.newFolder() - def config = new File(projectDir, 'p4config') - config.text = "P4CLIENT=jryan_uber\nP4USER=jryan" - provider.p4configFile = config - - when: - def foundMap = provider.perforceDefaults(projectDir) - - then: - foundMap.P4CLIENT == 'jryan_uber' - foundMap.P4USER == 'jryan' - foundMap.P4PORT == 'perforce:1666' - } - - def 'url looks right'() { - setup: - def defaults = [P4USER: 'user', P4PORT: 'port'] - - when: - def result = provider.getUrl(defaults) - - then: - result == 'p4java://port?userName=user' - } - - def 'calculate module status'() { - setup: - def workspace = new File('/Users/jryan/Workspaces/jryan_uber') - def fakeProjectDir = new File("/Users/jryan/Workspaces/jryan_uber/Tools/nebula-boot") - - environmentVariables.set("P4CONFIG", "test") - environmentVariables.set("WORKSPACE", workspace.path) - - - when: - String mapped = provider.calculateModuleSource(fakeProjectDir) - - then: - mapped == '//depot/Tools/nebula-boot' - - 2 * providerFactoryMock.environmentVariable('WORKSPACE') >> providerStringMock - 1 * providerStringMock.present >> true - 1 * providerStringMock.get() >> workspace.path - - when: - String origin = provider.calculateModuleOrigin(fakeProjectDir) - - then: - origin == 'p4java://perforce:1666?userName=rolem' - - 1 * providerFactoryMock.environmentVariable('P4CONFIG') >> providerStringMock - 1 * providerStringMock.get() >> fakeProjectDir.path - } - - def 'calculate module status - WORKSPACE is null'() { - setup: - environmentVariables.set("P4CONFIG", "test") - environmentVariables.set("WORKSPACE", null) - - def fakeProjectDir = new File("/Users/jryan/Workspaces/jryan_uber/Tools/nebula-boot") - - when: - String mapped = provider.calculateModuleSource(fakeProjectDir) - - then: - mapped == '//depot//Users/jryan/Workspaces/jryan_uber/Tools/nebula-boot' - - 1 * providerFactoryMock.environmentVariable('WORKSPACE') >> providerStringMock - 1 * providerStringMock.present >> false - 0 * providerStringMock.get() - - when: - String origin = provider.calculateModuleOrigin(fakeProjectDir) - - then: - origin == 'p4java://perforce:1666?userName=rolem' - - 1 * providerFactoryMock.environmentVariable('P4CONFIG') >> providerStringMock - 1 * providerStringMock.get() >> fakeProjectDir.path - } -} diff --git a/src/test/groovy/nebula/plugin/info/scm/ScmInfoPluginTest.groovy b/src/test/groovy/nebula/plugin/info/scm/ScmInfoPluginTest.groovy index 67e4584..6eb6ae4 100644 --- a/src/test/groovy/nebula/plugin/info/scm/ScmInfoPluginTest.groovy +++ b/src/test/groovy/nebula/plugin/info/scm/ScmInfoPluginTest.groovy @@ -24,6 +24,7 @@ class ScmInfoPluginTest { def runner = testProject(projectDir) { properties { buildCache(true) + configurationCache(true) } rootProject { plugins { @@ -38,7 +39,7 @@ afterEvaluate { } } - def result = runner.run("build") + def result = runner.run("build", "--stacktrace") assertThat(result.output) .contains("scminfo.origin = file:" + remoteGitDir.absolutePath + "/.git") .contains("scminfo.source = ") @@ -51,6 +52,7 @@ afterEvaluate { def runner = testProject(projectDir) { properties { buildCache(true) + configurationCache(true) } rootProject { plugins { @@ -74,6 +76,7 @@ afterEvaluate { def runner = testProject(projectDir) { properties { buildCache(true) + configurationCache(true) } rootProject { plugins { @@ -87,7 +90,7 @@ afterEvaluate { } } - def result = runner.run("build") + def result = runner.run("build", "--stacktrace") assertThat(result.output).contains("scminfo.origin = LOCAL") } @@ -97,6 +100,7 @@ afterEvaluate { def runner = testProject(projectDir) { properties { buildCache(true) + configurationCache(true) } rootProject { } diff --git a/src/test/kotlin/nebula/plugin/info/TestUtil.kt b/src/test/kotlin/nebula/plugin/info/TestUtil.kt new file mode 100644 index 0000000..36ccac1 --- /dev/null +++ b/src/test/kotlin/nebula/plugin/info/TestUtil.kt @@ -0,0 +1,13 @@ +package nebula.plugin.info + +import java.io.File +import java.util.jar.Attributes +import java.util.jar.JarFile + +fun readJarAttributes(projectDir: File, moduleName: String, version: String): Attributes { + return JarFile(projectDir.resolve("build/libs/${moduleName}-${version}.jar")).manifest.mainAttributes +} + +fun Attributes.getKey(key: String): Any? { + return get(Attributes.Name(key)) +} diff --git a/src/test/kotlin/nebula/plugin/info/ci/ContinuousIntegrationInfoPluginTest.kt b/src/test/kotlin/nebula/plugin/info/ci/ContinuousIntegrationInfoPluginTest.kt index 3beb7e3..d46bebf 100644 --- a/src/test/kotlin/nebula/plugin/info/ci/ContinuousIntegrationInfoPluginTest.kt +++ b/src/test/kotlin/nebula/plugin/info/ci/ContinuousIntegrationInfoPluginTest.kt @@ -1,12 +1,12 @@ package nebula.plugin.info.ci +import nebula.plugin.info.getKey +import nebula.plugin.info.readJarAttributes import nebula.test.dsl.* import nebula.test.dsl.TestKitAssertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File -import java.util.jar.Attributes -import java.util.jar.JarFile internal class ContinuousIntegrationInfoPluginTest { @@ -53,7 +53,7 @@ internal class ContinuousIntegrationInfoPluginTest { .hasNoDeprecationWarnings() .hasNoMutableStateWarnings() - val attributes = readJarAttributes() + val attributes = readJarAttributes(projectDir, "module", "1.0") assertThat(attributes.getKey("Build-Job")).isEqualTo("my-action") assertThat(attributes.getKey("Build-Number")).isEqualTo("10") assertThat(attributes.getKey("Build-Id")).isEqualTo("1") @@ -83,7 +83,7 @@ internal class ContinuousIntegrationInfoPluginTest { .hasNoDeprecationWarnings() .hasNoMutableStateWarnings() - val attributes = readJarAttributes() + val attributes = readJarAttributes(projectDir, "module", "1.0") assertThat(attributes.getKey("Build-Job")).isEqualTo("org/my-repo") assertThat(attributes.getKey("Build-Number")).isEqualTo("10") assertThat(attributes.getKey("Build-Id")).isEqualTo("1") @@ -111,7 +111,7 @@ internal class ContinuousIntegrationInfoPluginTest { .hasNoDeprecationWarnings() .hasNoMutableStateWarnings() - val attributes = readJarAttributes() + val attributes = readJarAttributes(projectDir, "module", "1.0") assertThat(attributes.getKey("Build-Job")).isEqualTo("org/my-repo") assertThat(attributes.getKey("Build-Number")).isEqualTo("1") assertThat(attributes.getKey("Build-Id")).isEqualTo("1") @@ -136,19 +136,11 @@ internal class ContinuousIntegrationInfoPluginTest { .hasNoDeprecationWarnings() .hasNoMutableStateWarnings() - val attributes = readJarAttributes() + val attributes = readJarAttributes(projectDir, "module", "1.0") assertThat(attributes.getKey("Build-Job")).isEqualTo("LOCAL") assertThat(attributes.getKey("Build-Number")).isEqualTo("LOCAL") assertThat(attributes.getKey("Build-Id")).isEqualTo("LOCAL") assertThat(attributes.getKey("Build-Url")).isEqualTo("${HostnameValueSource.hostname()}/LOCAL") assertThat(attributes.getKey("Build-Host")).isEqualTo(HostnameValueSource.hostname()) } - - fun Attributes.getKey(key: String): Any? { - return get(Attributes.Name(key)) - } - - fun readJarAttributes(): Attributes { - return JarFile(projectDir.resolve("build/libs/module-1.0.jar")).manifest.mainAttributes - } } diff --git a/src/test/kotlin/nebula/plugin/info/scm/GitScmProviderTest.kt b/src/test/kotlin/nebula/plugin/info/scm/GitScmProviderTest.kt new file mode 100644 index 0000000..deaa3c5 --- /dev/null +++ b/src/test/kotlin/nebula/plugin/info/scm/GitScmProviderTest.kt @@ -0,0 +1,49 @@ +package nebula.plugin.info.scm + +import nebula.plugin.info.getKey +import nebula.plugin.info.readJarAttributes +import nebula.plugin.info.testutil.TestHelpers.withRemoteGit +import nebula.test.dsl.* +import nebula.test.dsl.TestKitAssertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +internal class GitScmProviderTest { + @TempDir + lateinit var projectDir: File + + @TempDir + lateinit var remoteGitDir: File + + @Test + fun `test git with remote`() { + withRemoteGit(remoteGitDir, projectDir) { remote, working -> + val runner = testProject(projectDir) { + properties { + buildCache(true) + configurationCache(true) + } + settings { + name("module") + } + rootProject { + plugins { + id("java-library") + id("com.netflix.nebula.info-broker") + id("com.netflix.nebula.info-scm") + id("com.netflix.nebula.info-jar") + } + } + } + val result = runner.run("jar", "-Pversion=1.0", "--stacktrace") + + assertThat(result) + .hasNoDeprecationWarnings() + .hasNoMutableStateWarnings() + + val attributes = readJarAttributes(projectDir, "module", "1.0") + assertThat(attributes.getKey("Module-Origin")).isEqualTo("file:" + remoteGitDir.absolutePath + "/.git") + } + } +} diff --git a/src/test/kotlin/nebula/plugin/info/scm/PerforceScmProviderTest.kt b/src/test/kotlin/nebula/plugin/info/scm/PerforceScmProviderTest.kt new file mode 100644 index 0000000..71e1149 --- /dev/null +++ b/src/test/kotlin/nebula/plugin/info/scm/PerforceScmProviderTest.kt @@ -0,0 +1,198 @@ +package nebula.plugin.info.scm + +import nebula.plugin.info.getKey +import nebula.plugin.info.readJarAttributes +import nebula.test.dsl.* +import nebula.test.dsl.TestKitAssertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +internal class PerforceScmProviderTest { + + @TempDir + lateinit var projectDir: File + + private fun TestProjectBuilder.exampleProject() { + properties { + buildCache(true) + configurationCache(true) + } + settings { + name("module") + } + } + + @Test + fun `test perforce file`() { + val runner = testProject(projectDir) { + exampleProject() + rootProject { + plugins { + id("java-library") + id("com.netflix.nebula.info-broker") + id("com.netflix.nebula.info-scm") + id("com.netflix.nebula.info-jar") + } + } + } + projectDir.resolve("p4config").apply { + createNewFile() + writeText( + """ +P4CLIENT=jryan_uber +P4USER=jryan +""" + ) + } + val result = runner.run("jar", "-Pversion=1.0", "--stacktrace") { + withEnvironment( + mapOf( + "P4CONFIG" to "p4config", + "P4_CHANGELIST" to "change" + ) + ) + forwardOutput() + } + + assertThat(result) + .hasNoDeprecationWarnings() + .hasNoMutableStateWarnings() + + val attributes = readJarAttributes(projectDir, "module", "1.0") + assertThat(attributes.getKey("Module-Source")).isEqualTo(".") + assertThat(attributes.getKey("Module-Origin")).isEqualTo("p4java://perforce:1666?userName=jryan") + assertThat(attributes.getKey("Change")).isEqualTo("change") + assertThat(attributes.getKey("Full-Change")).isEqualTo("change") + assertThat(attributes.getKey("Branch")).isNull() + } + + @Test + fun `test perforce file from subproject`() { + val runner = testProject(projectDir) { + exampleProject() + subProject("sub") { + plugins { + id("java-library") + id("com.netflix.nebula.info-broker") + id("com.netflix.nebula.info-scm") + id("com.netflix.nebula.info-jar") + } + } + } + projectDir.resolve("p4config").apply { + createNewFile() + writeText( + """ +P4CLIENT=jryan_uber +P4USER=jryan +""" + ) + } + val result = runner.run("jar", "-Pversion=1.0", "--stacktrace") { + withEnvironment( + mapOf( + "P4CONFIG" to "p4config", + "P4_CHANGELIST" to "change" + ) + ) + forwardOutput() + } + + assertThat(result) + .hasNoDeprecationWarnings() + .hasNoMutableStateWarnings() + + val attributes = readJarAttributes(projectDir.resolve("sub"), "sub", "1.0") + assertThat(attributes.getKey("Module-Source")).isEqualTo(".") + assertThat(attributes.getKey("Module-Origin")).isEqualTo("p4java://perforce:1666?userName=jryan") + assertThat(attributes.getKey("Change")).isEqualTo("change") + assertThat(attributes.getKey("Full-Change")).isEqualTo("change") + assertThat(attributes.getKey("Branch")).isNull() + } + + @Test + fun `test perforce env`() { + val runner = testProject(projectDir) { + exampleProject() + rootProject { + plugins { + id("java-library") + id("com.netflix.nebula.info-broker") + id("com.netflix.nebula.info-scm") + id("com.netflix.nebula.info-jar") + } + } + } + + val result = runner.run("jar", "-Pversion=1.0", "--stacktrace") { + withEnvironment( + mapOf( + "P4CLIENT" to "client", + "P4_CHANGELIST" to "change", + "P4USER" to "jryan", + "P4PORT" to "perforce:8888", + "WORKSPACE" to this@PerforceScmProviderTest.projectDir.resolve("workspace") + .apply { mkdirs() } + .absolutePath + ) + ) + } + + assertThat(result) + .hasNoDeprecationWarnings() + .hasNoMutableStateWarnings() + + val attributes = readJarAttributes(projectDir, "module", "1.0") + assertThat(attributes.getKey("Module-Source")).isEqualTo("//depot/workspace/") + assertThat(attributes.getKey("Module-Origin")).isEqualTo("p4java://perforce:8888?userName=jryan") + assertThat(attributes.getKey("Change")).isEqualTo("change") + assertThat(attributes.getKey("Full-Change")).isEqualTo("change") + assertThat(attributes.getKey("Branch")).isNull() + } + + @Test + fun `test perforce file custom source`() { + val runner = testProject(projectDir) { + exampleProject() + rootProject { + plugins { + id("java-library") + id("com.netflix.nebula.info-broker") + id("com.netflix.nebula.info-scm") + id("com.netflix.nebula.info-jar") + } + } + } + projectDir.resolve("p4config").apply { + createNewFile() + writeText( + """ +P4CLIENT=jryan_uber +P4USER=jryan +""" + ) + } + val result = runner.run("jar", "-Pversion=1.0", "--stacktrace") { + withEnvironment( + mapOf( + "P4CONFIG" to "p4config", + "WORKSPACE" to this@PerforceScmProviderTest.projectDir.resolve("workspace") + .apply { mkdirs() } + .absolutePath + ) + ) + } + + assertThat(result) + .hasNoDeprecationWarnings() + .hasNoMutableStateWarnings() + + val attributes = readJarAttributes(projectDir, "module", "1.0") + assertThat(attributes.getKey("Module-Source")).isEqualTo("//depot/workspace/") + assertThat(attributes.getKey("Module-Origin")).isEqualTo("p4java://perforce:1666?userName=jryan") + assertThat(attributes.getKey("Change")).isNull() + assertThat(attributes.getKey("Full-Change")).isNull() + assertThat(attributes.getKey("Branch")).isNull() + } +}