From 80da9de21ca8b5b6316038e737c8d0eeb9f8d26d Mon Sep 17 00:00:00 2001 From: Andrew Oberstar Date: Fri, 11 Feb 2022 19:20:03 -0600 Subject: [PATCH] Supports multiple git publications Previously we only supported one git publication per project. Now you can declare multiple of them if you need to push to multiple branches or multiple repositories. Fixes #86 --- README.md | 25 +++ .../publish/MultiPublicationCompatTest.groovy | 144 ++++++++++++++++++ .../gradle/git/publish/GitPublication.java | 82 ++++++++++ .../git/publish/GitPublishExtension.java | 50 +++--- .../gradle/git/publish/GitPublishPlugin.java | 108 +++++++------ 5 files changed, 335 insertions(+), 74 deletions(-) create mode 100644 src/compatTest/groovy/org/ajoberstar/gradle/git/publish/MultiPublicationCompatTest.groovy create mode 100644 src/main/java/org/ajoberstar/gradle/git/publish/GitPublication.java diff --git a/README.md b/README.md index 30881b1..e76d20c 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,27 @@ gitPublish { } ``` +As of v4.1.0, you can now configure multiple publications (e.g. to target different repositories or branches). + +```groovy +gitPublish { + commitMessage = 'My favorite commit message' // configures the main publication + + publications { + // main + main { + branch = 'great-branch' // alternatively can configure at the top-level of the gitPublish block + // ... any other config from the gitPublish block ... + } + + other { + branch = 'some-branch' // may need branch.set('some-branch') + // ... any other config from the gitPublish block ... + } + } +} +``` + ### Tasks and Execution Generally, you'll just run `gitPublishPush`, but there is a series of four tasks that happen in order. @@ -101,6 +122,10 @@ Generally, you'll just run `gitPublishPush`, but there is a series of four tasks - `gitPublishCommit` - Commits all changes to the working repo. - `gitPublishPush` - If changes were committed, pushed them to the `repoUri`. +Each publication gets its own set of tasks, with a general `gitPublishPushAll` if you want to push all publications to their respective repos/branches. + +As is common in Gradle, the `main` publication is not indicated in task names (e.g. for `main` `gitPublishCommit` and for `other` `gitPublishOtherCommit`). + ### Avoiding Extra Copy If you are generating a large site, you may want to directly generate it into the working repo to save an extra copy step. You can do this with task dependencies and referring to the `repoDir`. diff --git a/src/compatTest/groovy/org/ajoberstar/gradle/git/publish/MultiPublicationCompatTest.groovy b/src/compatTest/groovy/org/ajoberstar/gradle/git/publish/MultiPublicationCompatTest.groovy new file mode 100644 index 0000000..297e1d5 --- /dev/null +++ b/src/compatTest/groovy/org/ajoberstar/gradle/git/publish/MultiPublicationCompatTest.groovy @@ -0,0 +1,144 @@ +package org.ajoberstar.gradle.git.publish + +import org.ajoberstar.grgit.Grgit +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.gradle.testkit.runner.UnexpectedBuildFailure +import spock.lang.IgnoreIf +import spock.lang.Specification +import spock.lang.TempDir + +class MultiPublicationCompatTest extends Specification { + @TempDir File tempDir + File projectDir + File buildFile + Grgit remote1 + Grgit remote2 + + def setup() { + projectDir = new File(tempDir, 'project') + buildFile = projectFile('build.gradle') + + def remoteDir = new File(tempDir, 'remote') + remote1 = Grgit.init(dir: remoteDir) + + remote1File('master.txt') << 'contents here' + remote1.add(patterns: ['.']) + remote1.commit(message: 'first commit') + + // handle different init branches to keep existing tests the same + if (remote1.branch.current().name != 'master') { + remote1.checkout(branch: 'master', createBranch: true) + } + + remote1.checkout(branch: 'gh-pages', orphan: true) + remote1.remove(patterns: ['master.txt']) + remote1File('index.md') << '# This Page is Awesome!' + remote1File('1.0.0/index.md') << '# Version 1.0.0 is the Best!' + remote1.add(patterns: ['.']) + remote1.commit(message: 'first pages commit') + + remote1.checkout(branch: 'master') + + def remote2Dir = new File(tempDir, 'remote2') + remote2 = Grgit.init(dir: remote2Dir) + + remote2File('master.txt') << 'contents here' + remote2.add(patterns: ['.']) + remote2.commit(message: 'first commit') + + // handle different init branches to keep existing tests the same + if (remote2.branch.current().name != 'master') { + remote2.checkout(branch: 'master', createBranch: true) + } + + remote2.checkout(branch: 'gh-pages', orphan: true) + remote2.remove(patterns: ['master.txt']) + remote2File('index.md') << '# This Page is Awesomest!' + remote2File('1.0.0/index.md') << '# Version 1.0.0 is the Best!' + remote2.add(patterns: ['.']) + remote2.commit(message: 'first pages commit') + + remote2.checkout(branch: 'master') + } + + def 'publish multiple publications'() { + given: + projectFile('src/content.txt') << 'published content here' + projectFile('src2/content.txt') << 'second published content here' + + buildFile << """ +plugins { + id 'org.ajoberstar.git-publish' +} + +gitPublish { + // can configure main at top-level + repoUri = '${remote1.repository.rootDir.toURI()}' + contents.from 'src' + + publications { + // can configure main under publication + main { + branch.set('my-pages') + } + + second { + repoUri.set('${remote2.repository.rootDir.toURI()}') + branch.set('gh-pages') + contents.from 'src2' + } + } +} +""" + when: + def result = build() + and: + remote1.checkout(branch: 'my-pages') + remote2.checkout(branch: 'gh-pages') + then: + result.task(':gitPublishPush').outcome == TaskOutcome.SUCCESS + remote1.log().size() == 1 + remote1File('content.txt').text == 'published content here' + and: + result.task(':gitPublishSecondPush').outcome == TaskOutcome.SUCCESS + remote2.log().size() == 2 + remote2File('content.txt').text == 'second published content here' + } + + private BuildResult build(String... args = ['gitPublishPushAll', '--stacktrace', '--info', '--configuration-cache']) { + return runner(args).build() + } + + private BuildResult buildAndFail(String... args = ['gitPublishPushAll', '--stacktrace', '--info', '--configuration-cache']) { + return runner(args).buildAndFail() + } + + private GradleRunner runner(String... args) { + return GradleRunner.create() + .withGradleVersion(System.properties['compat.gradle.version']) + .withPluginClasspath() + .withProjectDir(projectDir) + .forwardOutput() + .withArguments(args) + } + + private File remote1File(String path) { + File file = new File(remote1.repository.rootDir, path) + file.parentFile.mkdirs() + return file + } + + private File remote2File(String path) { + File file = new File(remote2.repository.rootDir, path) + file.parentFile.mkdirs() + return file + } + + private File projectFile(String path) { + File file = new File(projectDir, path) + file.parentFile.mkdirs() + return file + } +} diff --git a/src/main/java/org/ajoberstar/gradle/git/publish/GitPublication.java b/src/main/java/org/ajoberstar/gradle/git/publish/GitPublication.java new file mode 100644 index 0000000..d994705 --- /dev/null +++ b/src/main/java/org/ajoberstar/gradle/git/publish/GitPublication.java @@ -0,0 +1,82 @@ +package org.ajoberstar.gradle.git.publish; + +import org.gradle.api.Action; +import org.gradle.api.Named; +import org.gradle.api.Project; +import org.gradle.api.file.CopySpec; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.util.PatternFilterable; +import org.gradle.api.tasks.util.PatternSet; + +public class GitPublication implements Named { + private final String name; + private final DirectoryProperty repoDir; + private final Property repoUri; + private final Property referenceRepoUri; + private final Property branch; + private final Property commitMessage; + private final Property sign; + private final CopySpec contents; + private final PatternFilterable preserve; + + public GitPublication(String name, Project project, ObjectFactory objectFactory) { + this.name = name; + this.repoDir = objectFactory.directoryProperty(); + this.repoUri = objectFactory.property(String.class); + this.referenceRepoUri = objectFactory.property(String.class); + this.branch = objectFactory.property(String.class); + this.commitMessage = objectFactory.property(String.class); + this.sign = objectFactory.property(Boolean.class); + + this.contents = project.copySpec(); + this.preserve = new PatternSet(); + this.preserve.include(".git/**/*"); + } + + @Override + public String getName() { + return name; + } + + public DirectoryProperty getRepoDir() { + return repoDir; + } + + public Property getRepoUri() { + return repoUri; + } + + public Property getReferenceRepoUri() { + return referenceRepoUri; + } + + public Property getBranch() { + return branch; + } + + public Property getCommitMessage() { + return commitMessage; + } + + public Property getSign() { + return sign; + } + + public CopySpec getContents() { + return contents; + } + + public void contents(Action action) { + action.execute(contents); + } + + public PatternFilterable getPreserve() { + return preserve; + } + + public void preserve(Action action) { + action.execute(preserve); + } +} diff --git a/src/main/java/org/ajoberstar/gradle/git/publish/GitPublishExtension.java b/src/main/java/org/ajoberstar/gradle/git/publish/GitPublishExtension.java index 5be0fb3..6434840 100644 --- a/src/main/java/org/ajoberstar/gradle/git/publish/GitPublishExtension.java +++ b/src/main/java/org/ajoberstar/gradle/git/publish/GitPublishExtension.java @@ -3,75 +3,67 @@ import javax.inject.Inject; import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.Project; import org.gradle.api.file.CopySpec; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; import org.gradle.api.tasks.util.PatternFilterable; -import org.gradle.api.tasks.util.PatternSet; public class GitPublishExtension { - private final DirectoryProperty repoDir; - private final Property repoUri; - private final Property referenceRepoUri; - private final Property branch; - private final Property commitMessage; - private final Property sign; - private final CopySpec contents; - private final PatternFilterable preserve; + private final NamedDomainObjectContainer publications; @Inject public GitPublishExtension(Project project, ObjectFactory objectFactory) { - this.repoDir = objectFactory.directoryProperty(); - this.repoUri = objectFactory.property(String.class); - this.referenceRepoUri = objectFactory.property(String.class); - this.branch = objectFactory.property(String.class); - this.commitMessage = objectFactory.property(String.class); - this.sign = objectFactory.property(Boolean.class); - - this.contents = project.copySpec(); - this.preserve = new PatternSet(); - this.preserve.include(".git/**/*"); + this.publications = objectFactory.domainObjectContainer(GitPublication.class, name -> new GitPublication(name, project, objectFactory)); + } + + public NamedDomainObjectContainer getPublications() { + return publications; + } + + public void publications(Action> action) { + action.execute(publications); } public DirectoryProperty getRepoDir() { - return repoDir; + return publications.getByName("main").getRepoDir(); } public Property getRepoUri() { - return repoUri; + return publications.getByName("main").getRepoUri(); } public Property getReferenceRepoUri() { - return referenceRepoUri; + return publications.getByName("main").getReferenceRepoUri(); } public Property getBranch() { - return branch; + return publications.getByName("main").getBranch(); } public Property getCommitMessage() { - return commitMessage; + return publications.getByName("main").getCommitMessage(); } public Property getSign() { - return sign; + return publications.getByName("main").getSign(); } public CopySpec getContents() { - return contents; + return publications.getByName("main").getContents(); } public void contents(Action action) { - action.execute(contents); + publications.getByName("main").contents(action); } public PatternFilterable getPreserve() { - return preserve; + return publications.getByName("main").getPreserve(); } public void preserve(Action action) { - action.execute(preserve); + publications.getByName("main").preserve(action); } } diff --git a/src/main/java/org/ajoberstar/gradle/git/publish/GitPublishPlugin.java b/src/main/java/org/ajoberstar/gradle/git/publish/GitPublishPlugin.java index 9ee1973..32b0748 100644 --- a/src/main/java/org/ajoberstar/gradle/git/publish/GitPublishPlugin.java +++ b/src/main/java/org/ajoberstar/gradle/git/publish/GitPublishPlugin.java @@ -13,85 +13,94 @@ import org.gradle.api.tasks.TaskProvider; public class GitPublishPlugin implements Plugin { - static final String RESET_TASK = "gitPublishReset"; - static final String COPY_TASK = "gitPublishCopy"; - static final String COMMIT_TASK = "gitPublishCommit"; - static final String PUSH_TASK = "gitPublishPush"; - @Override public void apply(Project project) { var extension = project.getExtensions().create("gitPublish", GitPublishExtension.class, project); - configureExtensionDefaults(project, extension); - var grgitService = project.getGradle().getSharedServices().registerIfAbsent("git-publish-grgit", GrgitService.class, spec -> { - spec.parameters(parameters -> { - parameters.getDirectory().set(extension.getRepoDir()); - parameters.getInitIfNotExists().set(true); + // create the default + extension.getPublications().create("main"); + + // configure defaults and tasks for each publication + extension.getPublications().configureEach(publication -> { + configurePublicationDefaults(project, publication); + + var grgitService = project.getGradle().getSharedServices().registerIfAbsent(String.format("git-publish-%s-grgit", publication.getName()), GrgitService.class, spec -> { + spec.parameters(parameters -> { + parameters.getDirectory().set(publication.getRepoDir()); + parameters.getInitIfNotExists().set(true); + }); + spec.getMaxParallelUsages().set(1); }); - spec.getMaxParallelUsages().set(1); - }); - var reset = createResetTask(project, extension, grgitService); - var copy = createCopyTask(project, extension); - var commit = createCommitTask(project, extension, grgitService); - var push = createPushTask(project, extension, grgitService); + var reset = createResetTask(project, publication, grgitService); + var copy = createCopyTask(project, publication); + var commit = createCommitTask(project, publication, grgitService); + var push = createPushTask(project, publication, grgitService); + + push.configure(t -> t.dependsOn(commit)); + commit.configure(t -> t.dependsOn(copy)); + copy.configure(t -> t.dependsOn(reset)); + }); - push.configure(t -> t.dependsOn(commit)); - commit.configure(t -> t.dependsOn(copy)); - copy.configure(t -> t.dependsOn(reset)); + // add helper task to push all publications + project.getTasks().register("gitPublishPushAll", task -> { + task.setGroup("publishing"); + task.setDescription("Pushes all publications to git"); + task.dependsOn(project.getTasks().withType(GitPublishPush.class)); + }); } - private void configureExtensionDefaults(Project project, GitPublishExtension extension) { - extension.getCommitMessage().set("Generated by gradle-git-publish."); + private void configurePublicationDefaults(Project project, GitPublication publication) { + publication.getCommitMessage().set("Generated by gradle-git-publish."); // if using the grgit-service plugin, default to the repo's origin project.getPluginManager().withPlugin("org.ajoberstar.grgit.service", plugin -> { var grgitExt = project.getExtensions().getByType(GrgitServiceExtension.class); // TODO should this be based on tracking branch instead of assuming origin? - extension.getRepoUri().set(grgitExt.getService().map(service -> getOriginUri(service.getGrgit()))); - extension.getReferenceRepoUri().set(grgitExt.getService().map(service -> service.getGrgit().getRepository().getRootDir().toURI().toString())); + publication.getRepoUri().set(grgitExt.getService().map(service -> getOriginUri(service.getGrgit()))); + publication.getReferenceRepoUri().set(grgitExt.getService().map(service -> service.getGrgit().getRepository().getRootDir().toURI().toString())); }); - extension.getRepoDir().set(project.getLayout().getBuildDirectory().dir("gitPublish")); + publication.getRepoDir().set(project.getLayout().getBuildDirectory().dir("gitPublish/" + publication.getName())); } - private TaskProvider createResetTask(Project project, GitPublishExtension extension, Provider grgitService) { - return project.getTasks().register(RESET_TASK, GitPublishReset.class, task -> { + private TaskProvider createResetTask(Project project, GitPublication publication, Provider grgitService) { + return project.getTasks().register(getTaskName(publication, "Reset"), GitPublishReset.class, task -> { task.setGroup("publishing"); - task.setDescription("Prepares a git repo for new content to be generated."); + task.setDescription("Prepares a git repo for " + publication.getName() + " publication content to be generated."); task.getGrgitService().set(grgitService); - task.getRepoUri().set(extension.getRepoUri()); - task.getReferenceRepoUri().set(extension.getReferenceRepoUri()); - task.getBranch().set(extension.getBranch()); - task.setPreserve(extension.getPreserve()); + task.getRepoUri().set(publication.getRepoUri()); + task.getReferenceRepoUri().set(publication.getReferenceRepoUri()); + task.getBranch().set(publication.getBranch()); + task.setPreserve(publication.getPreserve()); }); } - private TaskProvider createCopyTask(Project project, GitPublishExtension extension) { - return project.getTasks().register(COPY_TASK, Copy.class, task -> { + private TaskProvider createCopyTask(Project project, GitPublication publication) { + return project.getTasks().register(getTaskName(publication, "Copy"), Copy.class, task -> { task.setGroup("publishing"); - task.setDescription("Copy contents to be published to git."); - task.with(extension.getContents()); - task.into(extension.getRepoDir()); + task.setDescription("Copy " + publication.getName() + " publication contents to be published to git."); + task.with(publication.getContents()); + task.into(publication.getRepoDir()); }); } - private TaskProvider createCommitTask(Project project, GitPublishExtension extension, Provider grgitService) { - return project.getTasks().register(COMMIT_TASK, GitPublishCommit.class, task -> { + private TaskProvider createCommitTask(Project project, GitPublication publication, Provider grgitService) { + return project.getTasks().register(getTaskName(publication, "Commit"), GitPublishCommit.class, task -> { task.setGroup("publishing"); - task.setDescription("Commits changes to be published to git."); + task.setDescription("Commits " + publication.getName() + " publication changes to be published to git."); task.getGrgitService().set(grgitService); - task.getMessage().set(extension.getCommitMessage()); - task.getSign().set(extension.getSign()); + task.getMessage().set(publication.getCommitMessage()); + task.getSign().set(publication.getSign()); }); } - private TaskProvider createPushTask(Project project, GitPublishExtension extension, Provider grgitService) { - return project.getTasks().register(PUSH_TASK, GitPublishPush.class, task -> { + private TaskProvider createPushTask(Project project, GitPublication publication, Provider grgitService) { + return project.getTasks().register(getTaskName(publication, "Push"), GitPublishPush.class, task -> { task.setGroup("publishing"); - task.setDescription("Pushes changes to git."); + task.setDescription("Pushes " + publication.getName() + " publication changes to git."); task.getGrgitService().set(grgitService); - task.getBranch().set(extension.getBranch()); + task.getBranch().set(publication.getBranch()); }); } @@ -102,4 +111,13 @@ private String getOriginUri(Grgit grgit) { .findAny() .orElse(null); } + + private String getTaskName(GitPublication publication, String task) { + if ("main".equals(publication.getName())) { + return "gitPublish" + task; + } else { + var capitalizedName = publication.getName().substring(0, 1).toUpperCase() + publication.getName().substring(1); + return "gitPublish" + capitalizedName + task; + } + } }