diff --git a/src/main/java/org/openrewrite/jenkins/github/AddTeamToCodeowners.java b/src/main/java/org/openrewrite/jenkins/github/AddTeamToCodeowners.java new file mode 100644 index 0000000..bf4a123 --- /dev/null +++ b/src/main/java/org/openrewrite/jenkins/github/AddTeamToCodeowners.java @@ -0,0 +1,167 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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 org.openrewrite.jenkins.github; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.maven.MavenIsoVisitor; +import org.openrewrite.text.PlainText; +import org.openrewrite.text.PlainTextParser; +import org.openrewrite.text.PlainTextVisitor; +import org.openrewrite.xml.XPathMatcher; +import org.openrewrite.xml.tree.Xml; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; + +@Value +@EqualsAndHashCode(callSuper = true) +public class AddTeamToCodeowners extends ScanningRecipe { + private static final String FILE_PATH = ".github/CODEOWNERS"; + + @Override + public String getDisplayName() { + return "Add plugin developer team to CODEOWNERS"; + } + + @Override + public String getDescription() { + return "Adds the `{artifactId}-plugin-developers` team to all files in `.github/CODEOWNERS` if absent."; + } + + @Override + public Scanned getInitialValue(ExecutionContext ctx) { + return new Scanned(new ArtifactIdTeamNameGenerator()); + } + + @Override + public TreeVisitor getScanner(Scanned acc) { + return new TreeVisitor() { + @Override + public Tree visit(@Nullable Tree tree, ExecutionContext executionContext, Cursor parent) { + SourceFile sourceFile = (SourceFile) requireNonNull(tree); + Path path = sourceFile.getSourcePath(); + String fileName = path.getFileName().toString(); + if ("CODEOWNERS".equals(fileName)) { + acc.foundFile = true; + } else if (acc.artifactId == null && "pom.xml".equals(fileName)) { + Xml.Document pom = (Xml.Document) sourceFile; + ArtifactIdExtractor extractor = new ArtifactIdExtractor(); + extractor.visit(pom, executionContext); + acc.artifactId = extractor.artifactId; + } + return sourceFile; + } + }; + } + + @Override + public Collection generate(Scanned acc, ExecutionContext ctx) { + if (acc.foundFile) { + return Collections.emptyList(); + } + PlainTextParser parser = new PlainTextParser(); + return parser.parse("* " + acc.teamName()) + .map(brandNewFile -> (PlainText) brandNewFile.withSourcePath(Paths.get(FILE_PATH))) + .collect(Collectors.toList()); + } + + @Override + public TreeVisitor getVisitor(Scanned acc) { + return new PlainTextVisitor() { + @Override + public PlainText visitText(PlainText text, ExecutionContext executionContext) { + if (acc.presentIn(text.getText())) { + return text; + } + List lines = new LinkedList<>(); + List after = new LinkedList<>(); + Scanner scanner = new Scanner(text.getText()); + int atPos = 0; + boolean lastComment = true; + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (atPos == 0 && line.contains("@")) { + atPos = line.indexOf("@"); + } + if (lastComment && line.startsWith("#")) { + lines.add(line); + } else { + lastComment = false; + after.add(line); + } + } + int spaces = Math.max(1, atPos - 1); + lines.add("*" + StringUtils.repeat(" ", spaces) + acc.teamName()); + lines.addAll(after); + return text.withText(String.join("\n", lines)); + } + }; + } + + @Data + public static class Scanned { + private final TeamNameGenerator generator; + String artifactId; + boolean foundFile; + + public Scanned(TeamNameGenerator generator) { + this.generator = generator; + } + + boolean presentIn(String text) { + Pattern p = Pattern.compile("^\\*\\s+" + teamName() + "$"); + try (Scanner s = new Scanner(text)) { + while (s.hasNextLine()) { + String line = s.nextLine(); + Matcher matcher = p.matcher(line); + if (matcher.matches()) { + return true; + } + } + return false; + } + } + + String teamName() { + return generator.generate(new TeamNameInput(artifactId)); + } + } + + private static class ArtifactIdExtractor extends MavenIsoVisitor { + private String artifactId = ""; + private static final XPathMatcher PROJECT_ARTIFACTID_MATCHER = new XPathMatcher("/project/artifactId"); + + @Override + public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext executionContext) { + Xml.Tag t = super.visitTag(tag, executionContext); + if (PROJECT_ARTIFACTID_MATCHER.matches(getCursor())) { + artifactId = t.getValue().orElseThrow(() -> new IllegalStateException("Expected to find an artifact id")); + } + return t; + } + } +} diff --git a/src/main/java/org/openrewrite/jenkins/github/ArtifactIdTeamNameGenerator.java b/src/main/java/org/openrewrite/jenkins/github/ArtifactIdTeamNameGenerator.java new file mode 100644 index 0000000..fb646bb --- /dev/null +++ b/src/main/java/org/openrewrite/jenkins/github/ArtifactIdTeamNameGenerator.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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 org.openrewrite.jenkins.github; + +import java.util.Locale; + +class ArtifactIdTeamNameGenerator implements TeamNameGenerator { + + @Override + public String generate(TeamNameInput input) { + String artifactId = input.getArtifactId(); + String withoutParent = artifactId; + if (artifactId.endsWith("-parent")) { + withoutParent = artifactId.substring(0, artifactId.lastIndexOf('-')); + } + return ("@jenkinsci/" + (withoutParent + "-plugin-developers")).toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/org/openrewrite/jenkins/github/TeamNameGenerator.java b/src/main/java/org/openrewrite/jenkins/github/TeamNameGenerator.java new file mode 100644 index 0000000..d3406f4 --- /dev/null +++ b/src/main/java/org/openrewrite/jenkins/github/TeamNameGenerator.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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 org.openrewrite.jenkins.github; + +public interface TeamNameGenerator { + String generate(T input); +} diff --git a/src/main/java/org/openrewrite/jenkins/github/TeamNameInput.java b/src/main/java/org/openrewrite/jenkins/github/TeamNameInput.java new file mode 100644 index 0000000..bb9e717 --- /dev/null +++ b/src/main/java/org/openrewrite/jenkins/github/TeamNameInput.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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 org.openrewrite.jenkins.github; + +import lombok.Value; + +@Value +public class TeamNameInput { + String artifactId; +} diff --git a/src/main/java/org/openrewrite/jenkins/github/package-info.java b/src/main/java/org/openrewrite/jenkins/github/package-info.java new file mode 100644 index 0000000..d11b9a3 --- /dev/null +++ b/src/main/java/org/openrewrite/jenkins/github/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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. + */ +@NonNullApi +package org.openrewrite.jenkins.github; + +import org.openrewrite.internal.lang.NonNullApi; \ No newline at end of file diff --git a/src/test/java/org/openrewrite/jenkins/github/AddTeamToCodeownersTest.java b/src/test/java/org/openrewrite/jenkins/github/AddTeamToCodeownersTest.java new file mode 100644 index 0000000..1240bda --- /dev/null +++ b/src/test/java/org/openrewrite/jenkins/github/AddTeamToCodeownersTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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 org.openrewrite.jenkins.github; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.mavenProject; +import static org.openrewrite.maven.Assertions.pomXml; +import static org.openrewrite.test.SourceSpecs.text; + +class AddTeamToCodeownersTest implements RewriteTest { + + @Language("xml") + // language=xml + private static final String POM = """ + + + org.jenkins-ci.plugins + plugin + 4.72 + + sample + 0.1 + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + """.stripIndent(); + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AddTeamToCodeowners()); + } + + @Test + @DocumentExample + void shouldAddFileIfMissing() { + rewriteRun( + pomXml(POM), + text(null, + """ + * @jenkinsci/sample-plugin-developers + """.stripIndent(), + s -> s.path(".github/CODEOWNERS") + ) + ); + } + + @Test + void shouldAddLineIfTeamNotDefinedForAll() { + rewriteRun( + pomXml(POM), + text( + """ + # This is a comment. + * @global-owner1 @global-owner2 + *.js @js-owner #This is an inline comment. + /build/logs/ @doctocat + """.stripIndent(), + """ + # This is a comment. + * @jenkinsci/sample-plugin-developers + * @global-owner1 @global-owner2 + *.js @js-owner #This is an inline comment. + /build/logs/ @doctocat + """.stripIndent(), + s -> s.path(".github/CODEOWNERS") + ) + ); + } + + @Test + void shouldHandleMultiModule() { + rewriteRun( + mavenProject("sample-parent", + pomXml(""" + + org.example + sample-parent + 0.1 + pom + + plugin + different-plugin + + + """.stripIndent()), + mavenProject("plugin", + pomXml(""" + + + org.jenkins-ci.plugins + plugin + 4.72 + + my-plugin + 0.1 + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + """.stripIndent())), + mavenProject("different-plugin", + pomXml(""" + + + org.jenkins-ci.plugins + plugin + 4.72 + + different-plugin + 0.1 + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + """.stripIndent()))), + text( + null, + """ + * @jenkinsci/sample-plugin-developers + """.stripIndent(), + s -> s.path(".github/CODEOWNERS") + )); + } + + @Test + void shouldNoOpIfTeamAlreadyDefinedForAll() { + rewriteRun( + pomXml(POM), + text( + "* @jenkinsci/sample-plugin-developers", + s -> s.path(".github/CODEOWNERS") + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/jenkins/github/ArtifactIdTeamNameGeneratorTest.java b/src/test/java/org/openrewrite/jenkins/github/ArtifactIdTeamNameGeneratorTest.java new file mode 100644 index 0000000..de3815d --- /dev/null +++ b/src/test/java/org/openrewrite/jenkins/github/ArtifactIdTeamNameGeneratorTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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 org.openrewrite.jenkins.github; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class ArtifactIdTeamNameGeneratorTest { + private final ArtifactIdTeamNameGenerator generator = new ArtifactIdTeamNameGenerator(); + + @ParameterizedTest + @CsvSource({ + "commons-text-api,@jenkinsci/commons-text-api-plugin-developers", + "stashNotifier,@jenkinsci/stashnotifier-plugin-developers", + "aws-java-sdk-parent,@jenkinsci/aws-java-sdk-plugin-developers", + "warnings-ng-parent,@jenkinsci/warnings-ng-plugin-developers", + }) + void shouldGenerateExpectedTeamName(String artifactId, String expected) { + String actual = generator.generate(new TeamNameInput(artifactId)); + assertThat(actual).isEqualTo(expected); + } +}