diff --git a/README.md b/README.md index 697afad5..2cf3c714 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,78 @@ It also provides a Gradle plugin to easily get a Grgit instance for the build's - [Documentation Site](http://ajoberstar.org/grgit/index.html) - [Release Notes](https://github.com/ajoberstar/grgit/releases) +## Simple Usage in Gradle + +Apply the `org.ajoberstar.grgit` plugin in any project that needs to access a `Grgit` instance. + +NOTE: This plugin eagerly opens a Grgit instance, which may not be needed depending on the tasks you want to run. If this is not desired, see the next section. + +``` +plugins { + id 'org.ajoberstar.grgit' version '' +} + +// adds a grgit property to the project (will silently be null if there's no git repo) +tasks.register("describe") { + doFirst { + println grgit.describe() + } +} +``` + +## More Performant Usage in Gradle + +Apply the `org.ajoberstar.grgit-service` plugin instead of `org.ajoberstar.grgit` to avoid eagerly resolving the `Grgit` instance. This works best with custom tasks that accept a `Property`. + +This approach ensures you only open a `Grgit` instance when a task is run that uses it. + +``` +import org.ajoberstar.grgit.gradle.GrgitService + +plugins { + id 'org.ajoberstar.grgit-service' version '' +} + +tasks.register("describe", DescribeTask) { + service = grgitService.service +} + +class DescribeTask extends DefaultTask { + @Input + final Property service + + @Inject + DoStuffTask(ObjectFactory objectFactory) { + this.service = objectFactory.property(GrgitService.class); + } + + @TaskAction + void execute() { + println service.get().grgit.describe() + } +} +``` + +### Custom Gradle Plugins + +If you are writing a custom Gradle plugin, you'll want to use one or both of the following approaches: + +- If you need a `Grgit` instance representing the repository the project is in, use `org.ajoberstar.grgit-service` and use the `GrgitServiceExtension` to access the shared `GrgitService`. Wire this into any tasks or whatever needs to use the service via `Property` for full lazy evaluation benefits. +- If you need a `Grgit` instance that's separate from the project's repository, declare your own `GrgitService` naming it something _not_ prefixed with `grgit*`. + + ``` + Provider serviceProvider = project.getGradle().getSharedServices().registerIfAbsent("grgit", GrgitService.class, spec -> { + // use getCurrentDirectory() if you need to search upwards from the provided directory + spec.getParameters().getCurrentDirectory().set(project.getLayout().getProjectDirectory()); + // or use getDirectory() if you want to specify a specific directory and not search + spec.getParameters().getDirectory().set(project.getLayout().getBuildDirectory().dir("my-custom-repo")); + // generally, this should be false, unless you're using getDirectory() choose to have the repo initialized if the directory does not exist + spec.getParameters().getInitIfNotExists().set(false); + // I recommend setting this to 1 unless you know better, this will avoid multiple parallel tasks editing the repo at the same time + spec.getMaxParallelUsages().set(1); + }); + ``` + ## Questions, Bugs, and Features Please use the repo's [issues](https://github.com/ajoberstar/grgit/issues) diff --git a/grgit-gradle/build.gradle.kts b/grgit-gradle/build.gradle.kts index c199c781..46dc2fc4 100644 --- a/grgit-gradle/build.gradle.kts +++ b/grgit-gradle/build.gradle.kts @@ -72,6 +72,11 @@ pluginBundle { displayName = "The Groovy way to use Git" tags = listOf("git", "groovy") } + create("grgitServicePlugin") { + id = "org.ajoberstar.grgit-service" + displayName = "The Groovy way to use Git (BuildService edition)" + tags = listOf("git", "groovy") + } } mavenCoordinates { groupId = project.group as String diff --git a/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/ConfigCacheTest.groovy b/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/ConfigCacheTest.groovy deleted file mode 100644 index 0625f98c..00000000 --- a/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/ConfigCacheTest.groovy +++ /dev/null @@ -1,101 +0,0 @@ -package org.ajoberstar.grgit.gradle - -import org.ajoberstar.grgit.Grgit -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.TaskOutcome -import org.junit.Rule -import org.junit.rules.TemporaryFolder -import spock.lang.Specification - -class ConfigCacheTest extends Specification { - @Rule TemporaryFolder tempDir = new TemporaryFolder() - File projectDir - File buildFile - - def setup() { - projectDir = tempDir.newFolder('project') - buildFile = projectFile('build.gradle') - } - - def "grgit build service can be fetched from registered services"() { - given: - buildFile << """ - import org.ajoberstar.grgit.gradle.GrgitBuildService - - plugins { - id 'org.ajoberstar.grgit' - } - - task doStuff { - def injected = project.gradle.sharedServices.registrations.getByName("grgit").getService() - doLast { - assert injected.get().grgit == null - } - } - """ - - when: - runner() - .withArguments('--configuration-cache', 'doStuff') - .build() - - and: - def result = runner() - .withArguments('--configuration-cache', 'doStuff') - .build() - - then: - result.output.contains('Reusing configuration cache.') - } - - - def 'with repo, plugin opens the repo as grgit'() { - given: - Grgit git = Grgit.init(dir: projectDir) - projectFile('1.txt') << '1' - git.add(patterns: ['1.txt']) - git.commit(message: 'yay') - git.tag.add(name: '1.0.0') - - buildFile << '''\ -plugins { - id 'org.ajoberstar.grgit' -} - -task doStuff { - def injected = project.grgitExtension - doLast { - println injected.describe() - } -} -''' - when: - runner() - .withArguments('--configuration-cache', 'doStuff') - .build() - - and: - def result = runner() - .withArguments('--configuration-cache', 'doStuff') - .build() - then: - result.task(':doStuff').outcome == TaskOutcome.SUCCESS - result.output.contains('Reusing configuration cache.') - result.output.contains('1.0.0\n') - } - - private GradleRunner runner(String... args) { - return GradleRunner.create() - .withGradleVersion("6.6-milestone-3") - .withPluginClasspath() - .withProjectDir(projectDir) - .forwardOutput() - .withArguments((args + '--stacktrace') as String[]) - } - - private File projectFile(String path) { - File file = new File(projectDir, path) - file.parentFile.mkdirs() - return file - } -} diff --git a/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/BaseCompatTest.groovy b/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/GrgitPluginCompatTest.groovy similarity index 89% rename from grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/BaseCompatTest.groovy rename to grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/GrgitPluginCompatTest.groovy index 6a631751..82b14e59 100644 --- a/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/BaseCompatTest.groovy +++ b/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/GrgitPluginCompatTest.groovy @@ -1,7 +1,6 @@ package org.ajoberstar.grgit.gradle import spock.lang.Specification -import spock.lang.Unroll import org.ajoberstar.grgit.Grgit import org.gradle.testkit.runner.GradleRunner @@ -9,7 +8,7 @@ import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.TaskOutcome import spock.lang.TempDir -class BaseCompatTest extends Specification { +class GrgitPluginCompatTest extends Specification { @TempDir File tempDir File projectDir File buildFile @@ -17,7 +16,6 @@ class BaseCompatTest extends Specification { def setup() { projectDir = new File(tempDir, 'project') buildFile = projectFile('build.gradle') - } def 'with no repo, plugin sets grgit to null'() { @@ -34,7 +32,7 @@ task doStuff { } ''' when: - def result = build('doStuff') + def result = build('doStuff', '--configuration-cache') then: result.task(':doStuff').outcome == TaskOutcome.SUCCESS } @@ -59,7 +57,7 @@ task doStuff { } ''' when: - def result = build('doStuff', '--quiet') + def result = build('doStuff', '--quiet', '--configuration-cache') then: result.task(':doStuff').outcome == TaskOutcome.SUCCESS result.output.normalize() == '1.0.0\n' @@ -85,7 +83,7 @@ task doStuff { } ''' when: - def result = build('doStuff', '--info') + def result = build('doStuff', '--info', '--configuration-cache') then: result.task(':doStuff').outcome == TaskOutcome.SUCCESS result.output.contains('Closing Git repo') diff --git a/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/GrgitServicePluginCompatTest.groovy b/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/GrgitServicePluginCompatTest.groovy new file mode 100644 index 00000000..b776dc0c --- /dev/null +++ b/grgit-gradle/src/compatTest/groovy/org/ajoberstar/grgit/gradle/GrgitServicePluginCompatTest.groovy @@ -0,0 +1,109 @@ +package org.ajoberstar.grgit.gradle + +import spock.lang.Specification + +import org.ajoberstar.grgit.Grgit +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.TaskOutcome +import spock.lang.TempDir + +class GrgitServicePluginCompatTest extends Specification { + @TempDir File tempDir + File projectDir + File buildFile + + def setup() { + projectDir = new File(tempDir, 'project') + buildFile = projectFile('build.gradle') + buildFile << '''\ +import org.ajoberstar.grgit.gradle.GrgitService + +plugins { + id 'org.ajoberstar.grgit-service' +} + +tasks.register("doStuff", DoStuffTask.class) { + service = grgitService.service +} + +class DoStuffTask extends DefaultTask { + @Input + final Property service + + @Inject + DoStuffTask(ObjectFactory objectFactory) { + this.service = objectFactory.property(GrgitService.class); + } + + @TaskAction + void execute() { + println service.get().grgit.describe() + } +} +''' + } + + def 'with no repo, accessing service fails'() { + given: + // nothing + when: + def result = buildAndFail('doStuff', '--configuration-cache') + then: + result.task(':doStuff').outcome == TaskOutcome.FAILED + } + + def 'with repo, plugin opens the repo as grgit'() { + given: + Grgit git = Grgit.init(dir: projectDir) + projectFile('1.txt') << '1' + git.add(patterns: ['1.txt']) + git.commit(message: 'yay') + git.tag.add(name: '1.0.0') + when: + def result = build('doStuff', '--quiet', '--configuration-cache') + then: + result.task(':doStuff').outcome == TaskOutcome.SUCCESS + result.output.normalize() == '1.0.0\n' + } + + def 'with repo, plugin closes the repo after build is finished'() { + given: + Grgit git = Grgit.init(dir: projectDir) + projectFile('1.txt') << '1' + git.add(patterns: ['1.txt']) + git.commit(message: 'yay') + git.tag.add(name: '1.0.0') + when: + def result = build('doStuff', '--info', '--configuration-cache') + then: + result.task(':doStuff').outcome == TaskOutcome.SUCCESS + result.output.contains('Closing Git repo') + } + + private BuildResult build(String... args) { + return GradleRunner.create() + .withGradleVersion(System.properties['compat.gradle.version']) + .withPluginClasspath() + .withProjectDir(projectDir) + .forwardOutput() + .withArguments((args + '--stacktrace') as String[]) + .build() + } + + private BuildResult buildAndFail(String... args) { + return GradleRunner.create() + .withGradleVersion(System.properties['compat.gradle.version']) + .withPluginClasspath() + .withProjectDir(projectDir) + .forwardOutput() + .withArguments((args + '--stacktrace') as String[]) + .buildAndFail() + } + + private File projectFile(String path) { + File file = new File(projectDir, path) + file.parentFile.mkdirs() + return file + } +} diff --git a/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitBuildService.groovy b/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitBuildService.groovy deleted file mode 100644 index 0cfa6c60..00000000 --- a/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitBuildService.groovy +++ /dev/null @@ -1,41 +0,0 @@ -package org.ajoberstar.grgit.gradle - -import org.ajoberstar.grgit.Grgit -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.logging.Logger -import org.gradle.api.logging.Logging -import org.gradle.api.services.BuildService -import org.gradle.api.services.BuildServiceParameters - -abstract class GrgitBuildService implements BuildService, AutoCloseable { - - private static final Logger LOGGER = Logging.getLogger(GrgitBuildService.class); - - interface Params extends BuildServiceParameters { - DirectoryProperty getRootDirectory(); - } - - Grgit grgit; - - GrgitBuildService() { - try { - grgit = Grgit.open(currentDir: parameters.rootDirectory.get()) - } catch (Exception e) { - LOGGER.debug("Failed trying to find git repository for ${parameters.rootDirectory.get()}", e) - grgit = null - } - } - - @Delegate - public Grgit lookup() { - return grgit; - } - - @Override - public void close() throws Exception { - if (grgit != null) { - LOGGER.info("Closing Git repo: ${grgit.repository.rootDir}") - grgit.close() - } - } -} diff --git a/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitExtension.groovy b/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitExtension.groovy deleted file mode 100644 index c844b4e9..00000000 --- a/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitExtension.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package org.ajoberstar.grgit.gradle - -import org.ajoberstar.grgit.Grgit -import org.gradle.api.provider.Provider - -public class GrgitExtension { - public final Provider grgitBuildServiceProvider; - - public GrgitExtension(Provider grgitBuildServiceProvider) { - this.grgitBuildServiceProvider = grgitBuildServiceProvider - } - - @Delegate - public Grgit lookup() { - return grgitBuildServiceProvider.get().grgit; - } -} diff --git a/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitPlugin.groovy b/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitPlugin.groovy deleted file mode 100644 index 99acfd7e..00000000 --- a/grgit-gradle/src/main/groovy/org/ajoberstar/grgit/gradle/GrgitPlugin.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package org.ajoberstar.grgit.gradle - -import org.ajoberstar.grgit.Grgit -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.provider.Provider -import org.gradle.util.GradleVersion - -/** - * Plugin adding a {@code grgit} property to all projects - * that searches for a Git repo from the project's - * directory. - * @since 2.0.0 - */ -class GrgitPlugin implements Plugin { - @Override - void apply(Project project) { - if (GradleVersion.current() >= GradleVersion.version("6.1")) { - Provider provider = project.gradle.sharedServices.registerIfAbsent("grgit", GrgitBuildService, { spec -> - spec.parameters.rootDirectory = project.rootDir - }) - - if (provider.get().grgit != null) { - project.allprojects { - project.extensions.add(Grgit, 'grgit', provider.get().grgit) - project.extensions.create('grgitExtension', GrgitExtension, provider) - } - } else { - project.allprojects { - project.extensions.add(Grgit, 'grgit', null) - } - } - } else { - try { - Grgit grgit = Grgit.open(currentDir: project.rootDir) - - // Make sure Git repo is closed when the build is over. Ideally, this avoids issues with the daemon. - project.gradle.buildFinished { - project.logger.info "Closing Git repo: ${grgit.repository.rootDir}" - grgit.close() - } - - project.allprojects { Project prj -> - if (prj.extensions.hasProperty('grgit')) { - prj.logger.warn("Project ${prj.path} already has a grgit property. Remove org.ajoberstar.grgit from either ${prj.path} or ${project.path}.") - } - prj.extensions.add(Grgit, 'grgit', grgit) - } - } catch (Exception e) { - project.logger.debug("Failed trying to find git repository for ${project.path}", e) - project.ext.grgit = null - } - } - } -} diff --git a/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitPlugin.java b/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitPlugin.java new file mode 100644 index 00000000..bafe5b40 --- /dev/null +++ b/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitPlugin.java @@ -0,0 +1,27 @@ +package org.ajoberstar.grgit.gradle; + +import org.ajoberstar.grgit.Grgit; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.provider.Provider; + +/** + * Plugin adding a {@code grgit} property to all projects that searches for a Git repo from the + * project's directory. + * + * @since 2.0.0 + */ +public class GrgitPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getPluginManager().apply(GrgitServicePlugin.class); + var serviceExtension = project.getExtensions().getByType(GrgitServiceExtension.class); + try { + project.getLogger().info("The org.ajoberstar.grgit plugin eagerly opens a Grgit instance. Use org.ajoberstar.grgit-service for better performance."); + project.getExtensions().add(Grgit.class, "grgit", serviceExtension.getService().get().getGrgit()); + } catch (Exception e) { + project.getLogger().debug("Failed to open Grgit instance", e); + project.getExtensions().getExtraProperties().set("grgit", null); + } + } +} diff --git a/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitService.java b/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitService.java new file mode 100644 index 00000000..c10e07d9 --- /dev/null +++ b/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitService.java @@ -0,0 +1,58 @@ +package org.ajoberstar.grgit.gradle; + +import javax.inject.Inject; + +import org.ajoberstar.grgit.Grgit; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.provider.Property; +import org.gradle.api.services.BuildService; +import org.gradle.api.services.BuildServiceParameters; + +public abstract class GrgitService implements BuildService, AutoCloseable { + private static final Logger logger = Logging.getLogger(GrgitService.class); + + public interface Params extends BuildServiceParameters { + DirectoryProperty getCurrentDirectory(); + + DirectoryProperty getDirectory(); + + Property getInitIfNotExists(); + } + + private final Grgit grgit; + + @Inject + public GrgitService() { + if (getParameters().getCurrentDirectory().isPresent()) { + this.grgit = Grgit.open(op -> { + op.setCurrentDir(getParameters().getCurrentDirectory().get().getAsFile()); + }); + return; + } + + var dir = getParameters().getCurrentDirectory().get().getAsFile(); + if (dir.exists()) { + this.grgit = Grgit.open(op -> { + op.setDir(dir); + }); + } else if (getParameters().getInitIfNotExists().get()) { + this.grgit = Grgit.init(op -> { + op.setDir(dir); + }); + } else { + throw new IllegalStateException("No Git repo exists at " + dir + " and initIfNotExists is false. Cannot proceed."); + } + } + + public Grgit getGrgit() { + return grgit; + } + + @Override + public void close() throws Exception { + logger.info("Closing Git repo: {}", grgit.getRepository().getRootDir()); + grgit.close(); + } +} diff --git a/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitServiceExtension.java b/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitServiceExtension.java new file mode 100644 index 00000000..315bd89d --- /dev/null +++ b/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitServiceExtension.java @@ -0,0 +1,19 @@ +package org.ajoberstar.grgit.gradle; + +import javax.inject.Inject; + +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; + +public class GrgitServiceExtension { + private Property service; + + @Inject + public GrgitServiceExtension(ObjectFactory objectFactory) { + this.service = objectFactory.property(GrgitService.class); + } + + public Property getService() { + return service; + } +} diff --git a/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitServicePlugin.java b/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitServicePlugin.java new file mode 100644 index 00000000..ce4c8617 --- /dev/null +++ b/grgit-gradle/src/main/java/org/ajoberstar/grgit/gradle/GrgitServicePlugin.java @@ -0,0 +1,20 @@ +package org.ajoberstar.grgit.gradle; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.provider.Provider; + +public class GrgitServicePlugin implements Plugin { + @Override + public void apply(Project project) { + GrgitServiceExtension extension = project.getExtensions().create("grgitService", GrgitServiceExtension.class); + + Provider serviceProvider = project.getGradle().getSharedServices().registerIfAbsent("grgit", GrgitService.class, spec -> { + spec.getParameters().getCurrentDirectory().set(project.getLayout().getProjectDirectory()); + spec.getParameters().getInitIfNotExists().set(false); + spec.getMaxParallelUsages().set(1); + }); + + extension.getService().set(serviceProvider); + } +} diff --git a/grgit-gradle/src/main/resources/META-INF/gradle-plugins/org.ajoberstar.grgit-service.properties b/grgit-gradle/src/main/resources/META-INF/gradle-plugins/org.ajoberstar.grgit-service.properties new file mode 100644 index 00000000..2497a0ee --- /dev/null +++ b/grgit-gradle/src/main/resources/META-INF/gradle-plugins/org.ajoberstar.grgit-service.properties @@ -0,0 +1 @@ +implementation-class=org.ajoberstar.grgit.gradle.GrgitServicePlugin