From a5f1772b35c29e98e2d07ba6ab54ed37384b280e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Schn=C3=A9ider?= Date: Wed, 1 Nov 2023 16:48:40 -0400 Subject: [PATCH] Info on committer activity (#3654) --- .../org/openrewrite/marker/GitProvenance.java | 59 ++++++++-- .../marker/ci/CircleCiBuildEnvironment.java | 4 +- .../marker/ci/CustomBuildEnvironment.java | 4 +- .../marker/ci/DroneBuildEnvironment.java | 4 +- .../ci/GithubActionsBuildEnvironment.java | 4 +- .../marker/ci/GitlabBuildEnvironment.java | 4 +- .../openrewrite/search/FindCommitters.java | 102 ++++++++++++++++++ .../org/openrewrite/table/CommitsByDay.java | 39 +++++++ .../openrewrite/table/DistinctCommitters.java | 39 +++++++ .../openrewrite/FindGitProvenanceTest.java | 6 +- .../openrewrite/marker/GitProvenanceTest.java | 8 +- .../search/FindCommittersTest.java | 56 ++++++++++ 12 files changed, 310 insertions(+), 19 deletions(-) create mode 100644 rewrite-core/src/main/java/org/openrewrite/search/FindCommitters.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/table/CommitsByDay.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/table/DistinctCommitters.java create mode 100644 rewrite-core/src/test/java/org/openrewrite/search/FindCommittersTest.java diff --git a/rewrite-core/src/main/java/org/openrewrite/marker/GitProvenance.java b/rewrite-core/src/main/java/org/openrewrite/marker/GitProvenance.java index 0031960f114..c0d49881101 100644 --- a/rewrite-core/src/main/java/org/openrewrite/marker/GitProvenance.java +++ b/rewrite-core/src/main/java/org/openrewrite/marker/GitProvenance.java @@ -21,9 +21,11 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.*; +import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.openrewrite.Incubating; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.marker.ci.BuildEnvironment; import org.openrewrite.marker.ci.IncompleteGitConfigException; @@ -35,10 +37,11 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.util.*; +import static java.util.Collections.emptyList; import static org.openrewrite.Tree.randomId; @Value @@ -52,6 +55,7 @@ public class GitProvenance implements Marker { @Nullable String branch; + @Nullable String change; @Nullable @@ -60,6 +64,10 @@ public class GitProvenance implements Marker { @Nullable EOL eol; + @Nullable + @Incubating(since = "8.9.0") + List committers; + /** * Extract the organization name, including sub-organizations for git hosting services which support such a concept, * from the origin URL. Needs to be supplied with the @@ -140,8 +148,8 @@ public static GitProvenance fromProjectDirectory(Path projectDir) { * determined from a {@link BuildEnvironment} marker if possible. * @return A marker containing git provenance information. */ - public static @Nullable GitProvenance fromProjectDirectory(Path projectDir, @Nullable BuildEnvironment environment) { - + @Nullable + public static GitProvenance fromProjectDirectory(Path projectDir, @Nullable BuildEnvironment environment) { if (environment != null) { if (environment instanceof JenkinsBuildEnvironment) { JenkinsBuildEnvironment jenkinsBuildEnvironment = (JenkinsBuildEnvironment) environment; @@ -182,6 +190,7 @@ private static void printRequireGitDirOrWorkTreeException(Exception e) { } } + @Nullable private static GitProvenance fromGitConfig(Path projectDir) { String branch = null; try (Repository repository = new RepositoryBuilder().findGitDir(projectDir.toFile()).build()) { @@ -198,17 +207,18 @@ private static GitProvenance fromGitConfig(Path projectDir) { } } - private static GitProvenance fromGitConfig(Repository repository, @Nullable String branch, String changeset) { + private static GitProvenance fromGitConfig(Repository repository, @Nullable String branch, @Nullable String changeset) { if (branch == null) { branch = resolveBranchFromGitConfig(repository); } return new GitProvenance(randomId(), getOrigin(repository), branch, changeset, - getAutocrlf(repository), getEOF(repository)); + getAutocrlf(repository), getEOF(repository), + getCommitters(repository)); } @Nullable static String resolveBranchFromGitConfig(Repository repository) { - String branch = null; + String branch; try { try (Git git = Git.open(repository.getDirectory())) { ObjectId commit = repository.resolve(Constants.HEAD); @@ -256,7 +266,7 @@ private static String localBranchName(Repository repository, @Nullable String re List remotes = git.remoteList().call(); for (RemoteConfig remote : remotes) { if (remoteBranch.startsWith(remote.getName()) && - (branch == null || branch.length() > remoteBranch.length() - remote.getName().length() - 1)) { + (branch == null || branch.length() > remoteBranch.length() - remote.getName().length() - 1)) { branch = remoteBranch.substring(remote.getName().length() + 1); // +1 for the forward slash } } @@ -308,6 +318,29 @@ private static EOL getEOF(Repository repository) { } } + private static List getCommitters(Repository repository) { + try (Git git = Git.open(repository.getDirectory())) { + ObjectId head = repository.readOrigHead(); + if(head == null) { + return emptyList(); + } + + Map committers = new TreeMap<>(); + for (RevCommit commit : git.log().add(head).call()) { + PersonIdent who = commit.getAuthorIdent(); + Committer committer = committers.computeIfAbsent(who.getEmailAddress(), + email -> new Committer(who.getName(), email, new TreeMap<>())); + committer.getCommitsByDay().compute(ZonedDateTime + .ofInstant(who.getWhen().toInstant(), who.getTimeZone().toZoneId()) + .toLocalDate(), + (day, count) -> count == null ? 1 : count + 1); + } + return new ArrayList<>(committers.values()); + } catch (IOException | GitAPIException e) { + return emptyList(); + } + } + @Nullable private static String getChangeset(Repository repository) throws IOException { ObjectId head = repository.resolve(Constants.HEAD); @@ -340,4 +373,12 @@ public enum EOL { LF, Native } + + @Value + @With + public static class Committer { + String name; + String email; + NavigableMap commitsByDay; + } } diff --git a/rewrite-core/src/main/java/org/openrewrite/marker/ci/CircleCiBuildEnvironment.java b/rewrite-core/src/main/java/org/openrewrite/marker/ci/CircleCiBuildEnvironment.java index 45befa6dc2b..3261e3c5c4f 100644 --- a/rewrite-core/src/main/java/org/openrewrite/marker/ci/CircleCiBuildEnvironment.java +++ b/rewrite-core/src/main/java/org/openrewrite/marker/ci/CircleCiBuildEnvironment.java @@ -21,9 +21,11 @@ import org.openrewrite.internal.StringUtils; import org.openrewrite.marker.GitProvenance; +import java.util.Collections; import java.util.UUID; import java.util.function.UnaryOperator; +import static java.util.Collections.emptyList; import static org.openrewrite.Tree.randomId; import static org.openrewrite.marker.OperatingSystemProvenance.hostname; @@ -67,6 +69,6 @@ public GitProvenance buildGitProvenance() throws IncompleteGitConfigException { throw new IncompleteGitConfigException(); } return new GitProvenance(UUID.randomUUID(), repositoryURL, StringUtils.isBlank(branch)? tag : branch, - sha1, null, null); + sha1, null, null, emptyList()); } } diff --git a/rewrite-core/src/main/java/org/openrewrite/marker/ci/CustomBuildEnvironment.java b/rewrite-core/src/main/java/org/openrewrite/marker/ci/CustomBuildEnvironment.java index a251f2649d2..b5ee098ff11 100644 --- a/rewrite-core/src/main/java/org/openrewrite/marker/ci/CustomBuildEnvironment.java +++ b/rewrite-core/src/main/java/org/openrewrite/marker/ci/CustomBuildEnvironment.java @@ -24,6 +24,7 @@ import java.util.UUID; import java.util.function.UnaryOperator; +import static java.util.Collections.emptyList; import static org.openrewrite.Tree.randomId; @Value @@ -50,7 +51,8 @@ public GitProvenance buildGitProvenance() throws IncompleteGitConfigException { || StringUtils.isBlank(sha)) { throw new IncompleteGitConfigException(); } else { - return new GitProvenance(UUID.randomUUID(), cloneURL, ref, sha, null, null); + return new GitProvenance(UUID.randomUUID(), cloneURL, ref, sha, + null, null, emptyList()); } } diff --git a/rewrite-core/src/main/java/org/openrewrite/marker/ci/DroneBuildEnvironment.java b/rewrite-core/src/main/java/org/openrewrite/marker/ci/DroneBuildEnvironment.java index 63a4730dcf5..62df3e4dcf8 100644 --- a/rewrite-core/src/main/java/org/openrewrite/marker/ci/DroneBuildEnvironment.java +++ b/rewrite-core/src/main/java/org/openrewrite/marker/ci/DroneBuildEnvironment.java @@ -24,6 +24,7 @@ import java.util.UUID; import java.util.function.UnaryOperator; +import static java.util.Collections.emptyList; import static org.openrewrite.Tree.randomId; import static org.openrewrite.marker.OperatingSystemProvenance.hostname; @@ -67,7 +68,8 @@ public GitProvenance buildGitProvenance() throws IncompleteGitConfigException { throw new IncompleteGitConfigException(); } return new GitProvenance(UUID.randomUUID(), remoteURL, - StringUtils.isBlank(branch)? tag: branch, commitSha, null, null); + StringUtils.isBlank(branch)? tag: branch, commitSha, + null, null, emptyList()); } public String getBuildUrl() { diff --git a/rewrite-core/src/main/java/org/openrewrite/marker/ci/GithubActionsBuildEnvironment.java b/rewrite-core/src/main/java/org/openrewrite/marker/ci/GithubActionsBuildEnvironment.java index 8600379ef87..60b15ef104c 100644 --- a/rewrite-core/src/main/java/org/openrewrite/marker/ci/GithubActionsBuildEnvironment.java +++ b/rewrite-core/src/main/java/org/openrewrite/marker/ci/GithubActionsBuildEnvironment.java @@ -24,6 +24,7 @@ import java.util.UUID; import java.util.function.UnaryOperator; +import static java.util.Collections.emptyList; import static org.openrewrite.Tree.randomId; @Value @@ -75,7 +76,6 @@ public GitProvenance buildGitProvenance() throws IncompleteGitConfigException { } return new GitProvenance(UUID.randomUUID(), host + "/" + getRepository() - + ".git", gitRef, getSha(), null, null); + + ".git", gitRef, getSha(), null, null, emptyList()); } - } diff --git a/rewrite-core/src/main/java/org/openrewrite/marker/ci/GitlabBuildEnvironment.java b/rewrite-core/src/main/java/org/openrewrite/marker/ci/GitlabBuildEnvironment.java index 4ebe96ffbcb..fa5ed975817 100644 --- a/rewrite-core/src/main/java/org/openrewrite/marker/ci/GitlabBuildEnvironment.java +++ b/rewrite-core/src/main/java/org/openrewrite/marker/ci/GitlabBuildEnvironment.java @@ -24,6 +24,7 @@ import java.util.UUID; import java.util.function.UnaryOperator; +import static java.util.Collections.emptyList; import static org.openrewrite.Tree.randomId; @Value @@ -60,6 +61,7 @@ public GitProvenance buildGitProvenance() throws IncompleteGitConfigException { || StringUtils.isBlank(ciCommitSha)) { throw new IncompleteGitConfigException(); } - return new GitProvenance(UUID.randomUUID(), ciRepositoryUrl, ciCommitRefName, ciCommitSha, null, null); + return new GitProvenance(UUID.randomUUID(), ciRepositoryUrl, ciCommitRefName, ciCommitSha, + null, null, emptyList()); } } diff --git a/rewrite-core/src/main/java/org/openrewrite/search/FindCommitters.java b/rewrite-core/src/main/java/org/openrewrite/search/FindCommitters.java new file mode 100644 index 00000000000..cf9f08a4956 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/search/FindCommitters.java @@ -0,0 +1,102 @@ +/* + * 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.search; + +import org.openrewrite.*; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.GitProvenance; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.table.CommitsByDay; +import org.openrewrite.table.DistinctCommitters; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +public class FindCommitters extends ScanningRecipe> { + private transient final DistinctCommitters committers = new DistinctCommitters(this); + private transient final CommitsByDay commitsByDay = new CommitsByDay(this); + + @Override + public String getDisplayName() { + return "Find committers on repositories"; + } + + @Override + public String getDescription() { + return "List the committers on a repository."; + } + + @Override + public Map getInitialValue(ExecutionContext ctx) { + return new TreeMap<>(); + } + + @Override + public TreeVisitor getScanner(Map acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (tree instanceof SourceFile) { + SourceFile sourceFile = (SourceFile) tree; + sourceFile.getMarkers().findFirst(GitProvenance.class).ifPresent(provenance -> { + if (provenance.getCommitters() != null) { + for (GitProvenance.Committer committer : provenance.getCommitters()) { + acc.put(committer.getEmail(), committer); + } + } + }); + } + return tree; + } + }; + } + + @Override + public Collection generate(Map acc, ExecutionContext ctx) { + for (GitProvenance.Committer committer : acc.values()) { + committers.insertRow(ctx, new DistinctCommitters.Row( + committer.getName(), + committer.getEmail(), + committer.getCommitsByDay().lastKey(), + committer.getCommitsByDay().values().stream().mapToInt(Integer::intValue).sum() + )); + + committer.getCommitsByDay().forEach((day, commits) -> commitsByDay.insertRow(ctx, new CommitsByDay.Row( + committer.getName(), + committer.getEmail(), + day, + commits + ))); + } + return Collections.emptyList(); + } + + @Override + public TreeVisitor getVisitor(Map acc) { + return new TreeVisitor() { + @Override + @Nullable + public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (tree instanceof SourceFile) { + return SearchResult.found(tree, String.join("\n", acc.keySet())); + } + return tree; + } + }; + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/table/CommitsByDay.java b/rewrite-core/src/main/java/org/openrewrite/table/CommitsByDay.java new file mode 100644 index 00000000000..d05b06f8d44 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/table/CommitsByDay.java @@ -0,0 +1,39 @@ +/* + * 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.table; + +import lombok.Value; +import org.openrewrite.DataTable; +import org.openrewrite.Recipe; + +import java.time.LocalDate; + +public class CommitsByDay extends DataTable { + + public CommitsByDay(Recipe recipe) { + super(recipe, + "Commits by day", + "The commit activity by day by committer."); + } + + @Value + public static class Row { + String name; + String email; + LocalDate day; + int commits; + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/table/DistinctCommitters.java b/rewrite-core/src/main/java/org/openrewrite/table/DistinctCommitters.java new file mode 100644 index 00000000000..08dffaededa --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/table/DistinctCommitters.java @@ -0,0 +1,39 @@ +/* + * 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.table; + +import lombok.Value; +import org.openrewrite.DataTable; +import org.openrewrite.Recipe; + +import java.time.LocalDate; + +public class DistinctCommitters extends DataTable { + + public DistinctCommitters(Recipe recipe) { + super(recipe, + "Repository committers", + "The distinct set of committers per repository."); + } + + @Value + public static class Row { + String name; + String email; + LocalDate lastCommit; + int commits; + } +} diff --git a/rewrite-core/src/test/java/org/openrewrite/FindGitProvenanceTest.java b/rewrite-core/src/test/java/org/openrewrite/FindGitProvenanceTest.java index 07362368ec1..bac7647aa6e 100644 --- a/rewrite-core/src/test/java/org/openrewrite/FindGitProvenanceTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/FindGitProvenanceTest.java @@ -21,6 +21,9 @@ import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import java.util.Collections; + +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.marker.GitProvenance.AutoCRLF.False; import static org.openrewrite.marker.GitProvenance.EOL.Native; @@ -45,7 +48,8 @@ void showGitProvenance() { }), text( "Hello, World!", - spec -> spec.markers(new GitProvenance(Tree.randomId(), "https://github.com/openrewrite/rewrite", "main", "1234567", False, Native)) + spec -> spec.markers(new GitProvenance(Tree.randomId(), "https://github.com/openrewrite/rewrite", + "main", "1234567", False, Native, emptyList())) ) ); } diff --git a/rewrite-core/src/test/java/org/openrewrite/marker/GitProvenanceTest.java b/rewrite-core/src/test/java/org/openrewrite/marker/GitProvenanceTest.java index d14f7eda6ab..2f97f79af70 100644 --- a/rewrite-core/src/test/java/org/openrewrite/marker/GitProvenanceTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/marker/GitProvenanceTest.java @@ -41,6 +41,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_BRANCH_SECTION; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -65,14 +66,14 @@ private static Stream remotes() { @ParameterizedTest @MethodSource("remotes") void getOrganizationName(String remote) { - assertThat(new GitProvenance(randomId(), remote, "main", "123", null, null).getOrganizationName()) + assertThat(new GitProvenance(randomId(), remote, "main", "123", null, null, emptyList()).getOrganizationName()) .isEqualTo("openrewrite"); } @ParameterizedTest @MethodSource("remotes") void getRepositoryName(String remote) { - assertThat(new GitProvenance(randomId(), remote, "main", "123", null, null).getRepositoryName()) + assertThat(new GitProvenance(randomId(), remote, "main", "123", null, null, emptyList()).getRepositoryName()) .isEqualTo("rewrite"); } @@ -192,7 +193,8 @@ void multiplePathSegments(String baseUrl) { "master", "1234567890abcdef1234567890abcdef12345678", null, - null); + null, + emptyList()); assertThat(provenance.getOrganizationName(baseUrl)).isEqualTo("group/subgroup1/subgroup2"); assertThat(provenance.getRepositoryName()).isEqualTo("repo"); diff --git a/rewrite-core/src/test/java/org/openrewrite/search/FindCommittersTest.java b/rewrite-core/src/test/java/org/openrewrite/search/FindCommittersTest.java new file mode 100644 index 00000000000..2239b5fe85d --- /dev/null +++ b/rewrite-core/src/test/java/org/openrewrite/search/FindCommittersTest.java @@ -0,0 +1,56 @@ +/* + * 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.search; + +import org.junit.jupiter.api.Test; +import org.openrewrite.marker.GitProvenance; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import java.time.LocalDate; +import java.util.List; +import java.util.TreeMap; + +import static org.openrewrite.Tree.randomId; +import static org.openrewrite.test.SourceSpecs.text; + +public class FindCommittersTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new FindCommitters()); + } + + @Test + void findCommitters() { + GitProvenance git = new GitProvenance( + randomId(), "github.com", "main", "123", null, null, + List.of(new GitProvenance.Committer("Jon", "jkschneider@gmail.com", + new TreeMap<>() {{ + put(LocalDate.now().minusDays(5), 5); + put(LocalDate.now(), 5); + }})) + ); + + rewriteRun( + text( + "hi", + "~~(jkschneider@gmail.com)~~>hi", + spec -> spec.mapBeforeRecipe(pt -> pt.withMarkers(pt.getMarkers().add(git))) + ) + ); + } +}