From b4c3b0682dd0b4fed4f6ec783c4cd2ef833d760e Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Sat, 26 Oct 2024 14:58:19 +0200 Subject: [PATCH] fix server side --- ...assImportIntegratorIntegratorProvider.java | 6 - .../www1/hephaestus/config/GitHubConfig.java | 1 - .../GitHubAuthorAssociationConverter.java | 2 +- .../gitprovider/issue/dto/IssueInfoDTO.java | 7 +- .../issuecomment/dto/IssueCommentInfoDTO.java | 7 +- .../label/dto/LabelDTOConverter.java | 16 ++ .../milestone/dto/MilestoneInfoDTO.java | 10 +- .../pullrequest/PullRequestRepository.java | 27 +-- .../dto/PullRequestBaseInfoDTO.java | 18 ++ .../dto/PullRequestDTOConverter.java | 68 ++++++++ .../pullrequest/dto/PullRequestInfoDTO.java | 12 +- .../pullrequestreview/PullRequestReview.java | 1 + .../PullRequestReviewRepository.java | 38 +++- .../dto/PullRequestReviewDTOConverter.java | 32 ++++ .../dto/PullRequestReviewInfoDTO.java | 23 +++ ...ubPullRequestReviewCommentSyncService.java | 2 +- .../repository/RepositoryRepository.java | 10 +- .../dto/RepositoryDTOConverter.java | 18 ++ .../repository/dto/RepositoryInfoDTO.java | 5 +- .../hephaestus/gitprovider/user/User.java | 2 + .../gitprovider/user/UserRepository.java | 18 +- .../gitprovider/user/UserService.java | 158 ++++++++--------- .../user/dto/UserDTOConverter.java | 18 ++ .../gitprovider/user/dto/UserInfoDTO.java | 4 +- .../user/github/GitHubUserConverter.java | 3 +- .../leaderboard/LeaderboardController.java | 4 +- .../leaderboard/LeaderboardEntry.java | 28 --- .../leaderboard/LeaderboardService.java | 162 ++++++++++-------- .../leaderboard/dto/LeaderboardEntryDTO.java | 16 ++ .../syncing/GitHubDataSyncService.java | 2 - .../syncing/NatsConsumerService.java | 5 +- .../src/main/resources/application-prod.yml | 1 + .../src/main/resources/application.yml | 1 + 33 files changed, 458 insertions(+), 267 deletions(-) create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/label/dto/LabelDTOConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestBaseInfoDTO.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestDTOConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/dto/PullRequestReviewDTOConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/dto/PullRequestReviewInfoDTO.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/dto/RepositoryDTOConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/dto/UserDTOConverter.java delete mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardEntry.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/dto/LeaderboardEntryDTO.java diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java index f47e69f2..b5627d0d 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java @@ -14,7 +14,6 @@ import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.dto.PullRequestReviewInfoDTO; import de.tum.in.www1.hephaestus.gitprovider.repository.dto.RepositoryInfoDTO; import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserInfoDTO; -import de.tum.in.www1.hephaestus.leaderboard.dto.LeaderboardEntryDTO; import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; public class ClassImportIntegratorIntegratorProvider implements IntegratorProvider { @@ -30,14 +29,9 @@ public List getIntegrators() { classes.add(MilestoneInfoDTO.class); classes.add(PullRequestInfoDTO.class); classes.add(IssueCommentInfoDTO.class); - classes.add(PullRequestReviewInfoDTO.class); - classes.add(PullRequestReviewInfoDTO.PullRequest.class); - classes.add(RepositoryInfoDTO.class); - classes.add(LeaderboardEntryDTO.class); - return List.of(new ClassImportIntegrator(classes)); } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/GitHubConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/GitHubConfig.java index c46fde7d..8ee52166 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/GitHubConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/GitHubConfig.java @@ -10,7 +10,6 @@ import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHubBuilder; -import org.kohsuke.github.connector.GitHubConnector; @Configuration public class GitHubConfig { diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubAuthorAssociationConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubAuthorAssociationConverter.java index f9cb9650..1b94800a 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubAuthorAssociationConverter.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubAuthorAssociationConverter.java @@ -4,10 +4,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import de.tum.in.www1.hephaestus.gitprovider.common.AuthorAssociation; -import io.micrometer.common.lang.NonNull; @Component public class GitHubAuthorAssociationConverter implements Converter { diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issue/dto/IssueInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issue/dto/IssueInfoDTO.java index 39ab2f0f..2a6820db 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issue/dto/IssueInfoDTO.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issue/dto/IssueInfoDTO.java @@ -1,5 +1,6 @@ package de.tum.in.www1.hephaestus.gitprovider.issue.dto; +import java.time.OffsetDateTime; import java.util.List; import org.springframework.lang.NonNull; @@ -17,9 +18,9 @@ public record IssueInfoDTO( UserInfoDTO author, List labels, List assignees, - @NonNull String repositoryNameWithOwner, + String repositoryNameWithOwner, @NonNull String htmlUrl, - @NonNull String createdAt, - @NonNull String updatedAt + OffsetDateTime createdAt, + OffsetDateTime updatedAt ) { } \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issuecomment/dto/IssueCommentInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issuecomment/dto/IssueCommentInfoDTO.java index c2e2effb..6248b634 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issuecomment/dto/IssueCommentInfoDTO.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issuecomment/dto/IssueCommentInfoDTO.java @@ -1,5 +1,7 @@ package de.tum.in.www1.hephaestus.gitprovider.issuecomment.dto; +import java.time.OffsetDateTime; + import org.springframework.lang.NonNull; import com.fasterxml.jackson.annotation.JsonInclude; @@ -14,7 +16,6 @@ public record IssueCommentInfoDTO( UserInfoDTO author, IssueInfoDTO issue, @NonNull String htmlUrl, - @NonNull String createdAt, - @NonNull String updatedAt) { - + OffsetDateTime createdAt, + OffsetDateTime updatedAt) { } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/label/dto/LabelDTOConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/label/dto/LabelDTOConverter.java new file mode 100644 index 00000000..77766737 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/label/dto/LabelDTOConverter.java @@ -0,0 +1,16 @@ +package de.tum.in.www1.hephaestus.gitprovider.label.dto; + +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.gitprovider.label.Label; + +@Component +public class LabelDTOConverter { + + public LabelInfoDTO convertToDTO(Label label) { + return new LabelInfoDTO( + label.getId(), + label.getName(), + label.getColor()); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/milestone/dto/MilestoneInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/milestone/dto/MilestoneInfoDTO.java index 2a92b697..f56aaff6 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/milestone/dto/MilestoneInfoDTO.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/milestone/dto/MilestoneInfoDTO.java @@ -1,5 +1,7 @@ package de.tum.in.www1.hephaestus.gitprovider.milestone.dto; +import java.time.OffsetDateTime; + import org.springframework.lang.NonNull; import de.tum.in.www1.hephaestus.gitprovider.milestone.Milestone.State; @@ -10,10 +12,10 @@ public record MilestoneInfoDTO( @NonNull State state, @NonNull String title, String description, - String closedAt, - String dueOn, + OffsetDateTime closedAt, + OffsetDateTime dueOn, @NonNull String htmlUrl, - @NonNull String createdAt, - @NonNull String updatedAt + OffsetDateTime createdAt, + OffsetDateTime updatedAt ) { } \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/PullRequestRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/PullRequestRepository.java index e082d22d..c8859afc 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/PullRequestRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/PullRequestRepository.java @@ -10,8 +10,6 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import de.tum.in.www1.hephaestus.gitprovider.pullrequest.dto.PullRequestInfoDTO; - @Repository public interface PullRequestRepository extends JpaRepository { @@ -23,29 +21,18 @@ SELECT MIN(p.createdAt) WHERE p.author.login = :authorLogin """) Optional firstContributionByAuthorLogin(@Param("authorLogin") String authorLogin); - + @Query(""" - SELECT new PullRequestInfoDTO( - p.id, - p.number, - p.title, - p.state, - p.commentCount, - new UserInfoDTO(p.author.id, p.author.login, p.author.avatarUrl, p.author.name, p.author.htmlUrl, p.author.createdAt, p.author.updatedAt), - (SELECT new LabelInfoDTO(l.id, l.name, l.color) FROM Label l WHERE l MEMBER OF p.labels ORDER BY l.name), - (SELECT new UserInfoDTO(u.id, u.login, u.avatarUrl, u.name, u.htmlUrl, u.createdAt, u.updatedAt) FROM User u WHERE u MEMBER OF p.assignees ORDER BY u.login), - p.repository.nameWithOwner, - p.additions, - p.deletions, - p.mergedAt, - p.htmlUrl, - p.createdAt, - p.updatedAt) + SELECT p FROM PullRequest p + JOIN FETCH p.labels + JOIN FETCH p.author + JOIN FETCH p.assignees + JOIN FETCH p.repository WHERE (p.author.login = :assigneeLogin OR :assigneeLogin IN (SELECT u.login FROM p.assignees u)) AND p.state IN :states ORDER BY p.createdAt DESC """) - List findAssignedByLoginAndStates( + List findAssignedByLoginAndStates( @Param("assigneeLogin") String assigneeLogin, @Param("states") Set states); } \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestBaseInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestBaseInfoDTO.java new file mode 100644 index 00000000..fe7dd15a --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestBaseInfoDTO.java @@ -0,0 +1,18 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequest.dto; + +import org.springframework.lang.NonNull; +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.hephaestus.gitprovider.issue.Issue.State; +import de.tum.in.www1.hephaestus.gitprovider.repository.dto.RepositoryInfoDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PullRequestBaseInfoDTO( + @NonNull Long id, + @NonNull Integer number, + @NonNull String title, + @NonNull State state, + RepositoryInfoDTO repository, + @NonNull String htmlUrl) { + +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestDTOConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestDTOConverter.java new file mode 100644 index 00000000..5d3ee3ba --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestDTOConverter.java @@ -0,0 +1,68 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequest.dto; + +import java.util.Comparator; + +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.gitprovider.label.dto.LabelDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.label.dto.LabelInfoDTO; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; +import de.tum.in.www1.hephaestus.gitprovider.repository.dto.RepositoryDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserInfoDTO; + +@Component +public class PullRequestDTOConverter { + + private final LabelDTOConverter labelDTOConverter; + private final UserDTOConverter userDTOConverter; + private final RepositoryDTOConverter repositoryDTOConverter; + + public PullRequestDTOConverter( + LabelDTOConverter labelDTOConverter, + UserDTOConverter userDTOConverter, + RepositoryDTOConverter repositoryDTOConverter) { + this.labelDTOConverter = labelDTOConverter; + this.userDTOConverter = userDTOConverter; + this.repositoryDTOConverter = repositoryDTOConverter; + } + + public PullRequestInfoDTO convertToDTO(PullRequest pullRequest) { + return new PullRequestInfoDTO( + pullRequest.getId(), + pullRequest.getNumber(), + pullRequest.getTitle(), + pullRequest.getState(), + pullRequest.getCommentsCount(), + userDTOConverter.convertToDTO(pullRequest.getAuthor()), + pullRequest.getLabels() + .stream() + .map(labelDTOConverter::convertToDTO) + .sorted(Comparator.comparing(LabelInfoDTO::name)) + .toList(), + pullRequest.getAssignees() + .stream() + .map(userDTOConverter::convertToDTO) + .sorted(Comparator.comparing(UserInfoDTO::login)) + .toList(), + repositoryDTOConverter.convertToDTO(pullRequest.getRepository()), + pullRequest.getAdditions(), + pullRequest.getDeletions(), + pullRequest.getMergedAt(), + pullRequest.getClosedAt(), + pullRequest.getHtmlUrl(), + pullRequest.getCreatedAt(), + pullRequest.getUpdatedAt()); + } + + public PullRequestBaseInfoDTO convertToBaseDTO(PullRequest pullRequest) { + return new PullRequestBaseInfoDTO( + pullRequest.getId(), + pullRequest.getNumber(), + pullRequest.getTitle(), + pullRequest.getState(), + repositoryDTOConverter.convertToDTO(pullRequest.getRepository()), + pullRequest.getHtmlUrl()); + } + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestInfoDTO.java index c2c788bf..6e71589b 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestInfoDTO.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/dto/PullRequestInfoDTO.java @@ -1,12 +1,14 @@ package de.tum.in.www1.hephaestus.gitprovider.pullrequest.dto; import java.util.List; +import java.time.OffsetDateTime; import org.springframework.lang.NonNull; import com.fasterxml.jackson.annotation.JsonInclude; import de.tum.in.www1.hephaestus.gitprovider.issue.Issue.State; import de.tum.in.www1.hephaestus.gitprovider.label.dto.LabelInfoDTO; +import de.tum.in.www1.hephaestus.gitprovider.repository.dto.RepositoryInfoDTO; import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserInfoDTO; @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -19,11 +21,13 @@ public record PullRequestInfoDTO( UserInfoDTO author, List labels, List assignees, - @NonNull String repositoryNameWithOwner, + RepositoryInfoDTO repository, @NonNull Integer additions, @NonNull Integer deletions, - @NonNull String mergedAt, + OffsetDateTime mergedAt, + OffsetDateTime closedAt, @NonNull String htmlUrl, - @NonNull String createdAt, - @NonNull String updatedAt) { + OffsetDateTime createdAt, + OffsetDateTime updatedAt) { + } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/PullRequestReview.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/PullRequestReview.java index 8676a681..d306fb8c 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/PullRequestReview.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/PullRequestReview.java @@ -41,6 +41,7 @@ public class PullRequestReview { private String body; // We handle dismissed in a separate field to keep the original state + @NonNull @Enumerated(EnumType.STRING) private PullRequestReview.State state; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/PullRequestReviewRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/PullRequestReviewRepository.java index 6893fffd..e8ff4ebe 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/PullRequestReviewRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/PullRequestReviewRepository.java @@ -1,9 +1,12 @@ package de.tum.in.www1.hephaestus.gitprovider.pullrequestreview; +import java.time.OffsetDateTime; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -11,13 +14,36 @@ public interface PullRequestReviewRepository extends JpaRepository findByAuthor_Login(String authorLogin); - Optional findByPullRequest(PullRequestReview pullRequest); + @Query(""" + SELECT prr + FROM PullRequestReview prr + JOIN FETCH prr.author + JOIN FETCH prr.pullRequest + JOIN FETCH prr.pullRequest.repository + JOIN FETCH prr.comments + WHERE prr.author.login = :authorLogin AND prr.submittedAt >= :activitySince + ORDER BY prr.submittedAt DESC + """) + List findAllByAuthorLoginSince( + @Param("authorLogin") String authorLogin, + @Param("activitySince") OffsetDateTime activitySince); @Query(""" - SELECT pr - FROM PullRequestReview pr - JOIN FETCH pr.comments - WHERE pr.id = :reviewId + SELECT prr + FROM PullRequestReview prr + JOIN FETCH prr.author + JOIN FETCH prr.pullRequest + JOIN FETCH prr.pullRequest.repository + JOIN FETCH prr.comments + WHERE + prr.submittedAt BETWEEN :after AND :before + AND (:repository IS NULL OR prr.pullRequest.repository.nameWithOwner = :repository) + AND prr.author.type = 'USER' + ORDER BY prr.submittedAt DESC """) - Optional findByIdWithEagerComments(Long reviewId); + List findAllInTimeframe( + @Param("after") OffsetDateTime after, + @Param("before") OffsetDateTime before, + @Param("repository") Optional repository); + } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/dto/PullRequestReviewDTOConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/dto/PullRequestReviewDTOConverter.java new file mode 100644 index 00000000..0aad592c --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/dto/PullRequestReviewDTOConverter.java @@ -0,0 +1,32 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.dto; + +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.dto.PullRequestDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReview; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserDTOConverter; + +@Component +public class PullRequestReviewDTOConverter { + + private final UserDTOConverter userDTOConverter; + private final PullRequestDTOConverter pullRequestDTOConverter; + + public PullRequestReviewDTOConverter(UserDTOConverter userDTOConverter, + PullRequestDTOConverter pullRequestDTOConverter) { + this.userDTOConverter = userDTOConverter; + this.pullRequestDTOConverter = pullRequestDTOConverter; + } + + public PullRequestReviewInfoDTO convertToDTO(PullRequestReview pullRequestReview) { + return new PullRequestReviewInfoDTO( + pullRequestReview.getId(), + pullRequestReview.isDismissed(), + pullRequestReview.getState(), + pullRequestReview.getComments().size(), + userDTOConverter.convertToDTO(pullRequestReview.getAuthor()), + pullRequestDTOConverter.convertToBaseDTO(pullRequestReview.getPullRequest()), + pullRequestReview.getHtmlUrl(), + pullRequestReview.getSubmittedAt()); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/dto/PullRequestReviewInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/dto/PullRequestReviewInfoDTO.java new file mode 100644 index 00000000..4da8195a --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/dto/PullRequestReviewInfoDTO.java @@ -0,0 +1,23 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.dto; + +import java.time.OffsetDateTime; + +import org.springframework.lang.NonNull; +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.dto.PullRequestBaseInfoDTO; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReview; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserInfoDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PullRequestReviewInfoDTO( + @NonNull Long id, + @NonNull Boolean isDismissed, + @NonNull PullRequestReview.State state, + @NonNull Integer codeComments, + UserInfoDTO author, + PullRequestBaseInfoDTO pullRequest, + @NonNull String htmlUrl, + OffsetDateTime submittedAt) { + // We do not have createdAt and updatedAt for reviews +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentSyncService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentSyncService.java index 987ec6f7..7010d043 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentSyncService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentSyncService.java @@ -67,7 +67,7 @@ public void syncReviewCommentsOfAllPullRequests(List pullRequests */ public void syncReviewCommentsOfPullRequest(GHPullRequest pullRequest) { try { - pullRequest.listReviewComments().forEach(this::processPullRequestReviewComment); + pullRequest.listReviewComments().withPageSize(100).forEach(this::processPullRequestReviewComment); } catch (IOException e) { logger.error("Failed to fetch review comments for pull request {}: {}", pullRequest.getId(), e.getMessage()); diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/RepositoryRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/RepositoryRepository.java index 5c6cba80..69c7ae4b 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/RepositoryRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/RepositoryRepository.java @@ -1,7 +1,10 @@ package de.tum.in.www1.hephaestus.gitprovider.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; @org.springframework.stereotype.Repository public interface RepositoryRepository @@ -12,8 +15,9 @@ public interface RepositoryRepository @Query(""" SELECT r FROM Repository r - JOIN FETCH r.issues i - WHERE r.nameWithOwner = :nameWithOwner AND TYPE(i) = PullRequest + JOIN PullRequest pr ON r.id = pr.repository.id + WHERE pr.author.login = :contributorLogin + ORDER BY r.name ASC """) - Repository findByNameWithOwnerWithEagerPullRequests(String nameWithOwner); + List findContributedByLogin(@Param("contributorLogin") String contributorLogin); } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/dto/RepositoryDTOConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/dto/RepositoryDTOConverter.java new file mode 100644 index 00000000..7bd99837 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/dto/RepositoryDTOConverter.java @@ -0,0 +1,18 @@ +package de.tum.in.www1.hephaestus.gitprovider.repository.dto; + +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.gitprovider.repository.Repository; + +@Component +public class RepositoryDTOConverter { + + public RepositoryInfoDTO convertToDTO(Repository repository) { + return new RepositoryInfoDTO( + repository.getId(), + repository.getName(), + repository.getNameWithOwner(), + repository.getDescription(), + repository.getHtmlUrl()); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/dto/RepositoryInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/dto/RepositoryInfoDTO.java index 4c5ed48e..d609944f 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/dto/RepositoryInfoDTO.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/dto/RepositoryInfoDTO.java @@ -5,10 +5,9 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record RepositoryInfoDTO( + @NonNull Long id, @NonNull String name, @NonNull String nameWithOwner, String description, - @NonNull String htmlUrl, - @NonNull String createdAt, - @NonNull String updatedAt) { + @NonNull String htmlUrl) { } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java index 6e5485ef..e3155308 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java @@ -41,6 +41,8 @@ public class User extends BaseGitServiceEntity { // AKA bio private String description; + @NonNull + // Equals login if not fetched / existing private String name; private String company; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserRepository.java index db0dbc1b..51e87c8e 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserRepository.java @@ -1,31 +1,17 @@ package de.tum.in.www1.hephaestus.gitprovider.user; -import java.time.OffsetDateTime; -import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserInfoDTO; - public interface UserRepository extends JpaRepository { - @Query(""" - SELECT new UserInfoDTO(u.id, u.login, u.avatarUrl, u.name, u.htmlUrl, u.createdAt, u.updatedAt) - FROM User u - WHERE u.login = :login - """) - Optional findByLogin(@Param("login") String login); - @Query(""" SELECT u FROM User u - JOIN FETCH u.reviews re - WHERE re.createdAt BETWEEN :after AND :before - AND (:repository IS NULL OR re.pullRequest.repository.nameWithOwner = :repository) + WHERE u.login = :login """) - List findAllInTimeframe(@Param("after") OffsetDateTime after, @Param("before") OffsetDateTime before, - @Param("repository") Optional repository); + Optional findByLogin(@Param("login") String login); } \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserService.java index 80bb1aee..0128a8b8 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserService.java @@ -1,116 +1,94 @@ package de.tum.in.www1.hephaestus.gitprovider.user; -import java.time.OffsetDateTime; import java.util.Comparator; -import java.util.HashSet; -import java.util.LinkedHashSet; +import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntity; -import de.tum.in.www1.hephaestus.codereview.pullrequest.IssueState; -import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest; -import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestDTO; -import de.tum.in.www1.hephaestus.codereview.pullrequest.review.PullRequestReview; -import de.tum.in.www1.hephaestus.codereview.pullrequest.review.PullRequestReviewDTO; -import de.tum.in.www1.hephaestus.codereview.repository.RepositoryDTO; +import de.tum.in.www1.hephaestus.gitprovider.issue.Issue; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequestRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.dto.PullRequestDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.dto.PullRequestInfoDTO; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReviewRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.dto.PullRequestReviewDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.dto.PullRequestReviewInfoDTO; +import de.tum.in.www1.hephaestus.gitprovider.repository.RepositoryRepository; +import de.tum.in.www1.hephaestus.gitprovider.repository.dto.RepositoryDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.repository.dto.RepositoryInfoDTO; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserInfoDTO; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserProfileDTO; +import jakarta.transaction.Transactional; @Service public class UserService { private static final Logger logger = LoggerFactory.getLogger(UserService.class); private final UserRepository userRepository; - - public UserService(UserRepository actorRepository) { - this.userRepository = actorRepository; - } - - public Optional getUser(String login) { - logger.info("Getting user with login: " + login); - return userRepository.findUser(login); + private final RepositoryRepository repositoryRepository; + private final PullRequestRepository pullRequestRepository; + private final PullRequestReviewRepository pullRequestReviewRepository; + private final UserDTOConverter userDTOConverter; + private final RepositoryDTOConverter repositoryDTOConverter; + private final PullRequestDTOConverter pullRequestDTOConverter; + private final PullRequestReviewDTOConverter pullRequestReviewDTOConverter; + + public UserService( + UserRepository userRepository, + RepositoryRepository repositoryRepository, + PullRequestRepository pullRequestRepository, + PullRequestReviewRepository pullRequestReviewRepository, + UserDTOConverter userDTOConverter, + RepositoryDTOConverter repositoryDTOConverter, + PullRequestDTOConverter pullRequestDTOConverter, + PullRequestReviewDTOConverter pullRequestReviewDTOConverter) { + this.userRepository = userRepository; + this.repositoryRepository = repositoryRepository; + this.pullRequestRepository = pullRequestRepository; + this.pullRequestReviewRepository = pullRequestReviewRepository; + this.userDTOConverter = userDTOConverter; + this.repositoryDTOConverter = repositoryDTOConverter; + this.pullRequestDTOConverter = pullRequestDTOConverter; + this.pullRequestReviewDTOConverter = pullRequestReviewDTOConverter; } - public Optional getUserDTO(String login) { - logger.info("Getting userDTO with login: " + login); - return userRepository.findByLogin(login); - } - - public List getAllUsers() { - logger.info("Getting all users"); - return userRepository.findAll().stream().toList(); - } - - public List getAllUsersInTimeframe(OffsetDateTime after, OffsetDateTime before, Optional repository) { - logger.info("Getting all users in timeframe between " + after + " and " + before + " for repository: " - + repository.orElse("all")); - return userRepository.findAllInTimeframe(after, before, repository); - } + @Transactional + public Optional getUserProfile(String login) { + logger.info("Getting user profile with login: " + login); - public Optional getUserProfileDTO(String login) { - logger.info("Getting userProfileDTO with login: " + login); - Optional optionalUser = userRepository.findUser(login); + Optional optionalUser = userRepository.findByLogin(login).map(userDTOConverter::convertToDTO); if (optionalUser.isEmpty()) { return Optional.empty(); } - User user = optionalUser.get(); - - OffsetDateTime firstContribution = user.getPullRequests().stream().map(pr -> pr.getCreatedAt()) - .min(OffsetDateTime::compareTo).orElse(null); - Set repositories = mapToDTO(user.getPullRequests(), pr -> true, - pr -> pr.getRepository().getNameWithOwner(), - (r1, r2) -> r1.compareTo(r2)); - Set pullRequests = getPullRequestDTOs(user.getPullRequests()); - Set activity = getPullRequestReviewDTOs(user.getReviews()); - - return Optional.of(new UserProfileDTO(user.getId(), user.getLogin(), user.getAvatarUrl(), firstContribution, - repositories, activity, pullRequests)); - } - - private Set getPullRequestDTOs(Set pullRequests) { - return mapToDTO(pullRequests, - isRecentlyPredicate().and(pr -> ((PullRequest) pr).getState().equals(IssueState.OPEN)), - pr -> new PullRequestDTO( - pr.getId(), pr.getTitle(), pr.getNumber(), pr.getUrl(), pr.getState(), pr.getAdditions(), - pr.getDeletions(), - pr.getCreatedAt(), pr.getUpdatedAt(), null, - pr.getPullRequestLabels(), - new RepositoryDTO(pr.getRepository().getName(), - pr.getRepository().getNameWithOwner(), null, - pr.getRepository().getUrl())), - (pr1, pr2) -> pr1.createdAt().compareTo(pr2.createdAt())); - } - - private Set getPullRequestReviewDTOs(Set reviews) { - return mapToDTO(reviews, isRecentlyPredicate(), re -> { - PullRequest pr = re.getPullRequest(); - return new PullRequestReviewDTO(re.getId(), - re.getCreatedAt(), re.getUpdatedAt(), re.getSubmittedAt(), re.getState(), re.getUrl(), - new PullRequestDTO(pr.getId(), pr.getTitle(), pr.getNumber(), pr.getUrl(), pr.getState(), - pr.getAdditions(), pr.getDeletions(), pr.getCreatedAt(), pr.getUpdatedAt(), null, - new HashSet<>(), - new RepositoryDTO(pr.getRepository().getName(), - pr.getRepository().getNameWithOwner(), null, - pr.getRepository().getUrl()))); - }, (prr1, prr2) -> prr2.submittedAt().compareTo(prr1.submittedAt())); - } - - private Set mapToDTO(Set entities, Predicate predicate, - Function mapper, - Comparator comparator) { - return entities.stream().filter(predicate).map(mapper).sorted(comparator) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - // Predicate filtering createdAt for within past 7 days - private Predicate isRecentlyPredicate() { - return entity -> entity.getCreatedAt().isAfter(OffsetDateTime.now().minusDays(7)); + UserInfoDTO user = optionalUser.get(); + OffsetDateTime firstContribution = pullRequestRepository.firstContributionByAuthorLogin(login).orElse(null); + List openPullRequests = pullRequestRepository.findAssignedByLoginAndStates(login, Set.of(Issue.State.OPEN)) + .stream() + .map(pullRequestDTOConverter::convertToDTO) + .toList(); + List contributedRepositories = repositoryRepository.findContributedByLogin(login) + .stream() + .map(repositoryDTOConverter::convertToDTO) + .sorted(Comparator.comparing(RepositoryInfoDTO::name)) + .toList(); + List reviewActivity = pullRequestReviewRepository.findAllByAuthorLoginSince(login, OffsetDateTime.now().minusDays(7)) + .stream() + .map(pullRequestReviewDTOConverter::convertToDTO) + .sorted(Comparator.comparing(PullRequestReviewInfoDTO::submittedAt)) + .toList(); + + return Optional.of(new UserProfileDTO( + user, + firstContribution, + contributedRepositories, + reviewActivity, + openPullRequests + )); } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/dto/UserDTOConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/dto/UserDTOConverter.java new file mode 100644 index 00000000..63e57f91 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/dto/UserDTOConverter.java @@ -0,0 +1,18 @@ +package de.tum.in.www1.hephaestus.gitprovider.user.dto; + +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.gitprovider.user.User; + +@Component +public class UserDTOConverter { + + public UserInfoDTO convertToDTO(User user) { + return new UserInfoDTO( + user.getId(), + user.getLogin(), + user.getAvatarUrl(), + user.getName(), + user.getHtmlUrl()); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/dto/UserInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/dto/UserInfoDTO.java index 88dae77a..d627241f 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/dto/UserInfoDTO.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/dto/UserInfoDTO.java @@ -9,7 +9,5 @@ public record UserInfoDTO( @NonNull String login, @NonNull String avatarUrl, @NonNull String name, - @NonNull String htmlUrl, - @NonNull String createdAt, - @NonNull String updatedAt) { + @NonNull String htmlUrl) { } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/github/GitHubUserConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/github/GitHubUserConverter.java index 0fef38f0..f1aaa078 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/github/GitHubUserConverter.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/github/GitHubUserConverter.java @@ -29,9 +29,10 @@ public User update(@NonNull GHUser source, @NonNull User user) { user.setDescription(source.getBio()); user.setHtmlUrl(source.getHtmlUrl().toString()); try { - user.setName(source.getName()); + user.setName(source.getName() != null ? source.getName() : source.getLogin()); } catch (IOException e) { logger.error("Failed to convert user name field for source {}: {}", source.getId(), e.getMessage()); + user.setName(source.getLogin()); } try { user.setCompany(source.getCompany()); diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardController.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardController.java index 5ced3e6a..f193a2f6 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardController.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardController.java @@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import de.tum.in.www1.hephaestus.leaderboard.dto.LeaderboardEntryDTO; + @RestController @RequestMapping("/leaderboard") public class LeaderboardController { @@ -22,7 +24,7 @@ public LeaderboardController(LeaderboardService leaderboardService) { } @GetMapping - public ResponseEntity> getLeaderboard( + public ResponseEntity> getLeaderboard( @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional after, @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Optional before, @RequestParam Optional repository) { diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardEntry.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardEntry.java deleted file mode 100644 index 2d20c508..00000000 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardEntry.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.tum.in.www1.hephaestus.leaderboard; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReviewDTO; -import de.tum.in.www1.hephaestus.gitprovider.user.User; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -@JsonInclude(JsonInclude.Include.NON_EMPTY) -@AllArgsConstructor -@Getter -@Setter -@ToString -public class LeaderboardEntry { - private String githubName; - private String avatarUrl; - private String name; - private User.Type type; - private int score; - private int rank; - private int numberOfReviewedPRs; - private PullRequestReviewDTO[] changesRequested; - private PullRequestReviewDTO[] approvals; - private PullRequestReviewDTO[] comments; -} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardService.java index 7aada2e9..fb13197e 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardService.java @@ -1,17 +1,14 @@ package de.tum.in.www1.hephaestus.leaderboard; +import java.util.stream.IntStream; +import java.util.Map; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; -import java.util.ArrayList; import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -20,24 +17,33 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; -import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReviewDTO; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReview; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReviewRepository; import de.tum.in.www1.hephaestus.gitprovider.user.User; -import de.tum.in.www1.hephaestus.gitprovider.user.UserService; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserDTOConverter; +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserInfoDTO; +import de.tum.in.www1.hephaestus.leaderboard.dto.LeaderboardEntryDTO; +import jakarta.transaction.Transactional; @Service public class LeaderboardService { private static final Logger logger = LoggerFactory.getLogger(LeaderboardService.class); - private final UserService userService; + private final PullRequestReviewRepository pullRequestReviewRepository; + private final UserDTOConverter userDTOConverter; @Value("${monitoring.timeframe}") private int timeframe; - public LeaderboardService(UserService userService) { - this.userService = userService; + public LeaderboardService( + PullRequestReviewRepository pullRequestReviewRepository, + UserDTOConverter userDTOConverter) { + this.pullRequestReviewRepository = pullRequestReviewRepository; + this.userDTOConverter = userDTOConverter; } - public List createLeaderboard(Optional after, Optional before, + @Transactional + public List createLeaderboard(Optional after, Optional before, Optional repository) { logger.info("Creating leaderboard dataset"); @@ -45,76 +51,92 @@ public List createLeaderboard(Optional after, Optio : LocalDate.now().minusDays(timeframe).atStartOfDay(); Optional beforeCutOff = before.map(date -> date.plusDays(1).atStartOfDay()); - List users = userService.getAllUsersInTimeframe(afterCutOff.atOffset(ZoneOffset.UTC), - beforeCutOff.map(b -> b.atOffset(ZoneOffset.UTC)).orElse(OffsetDateTime.now()), repository); - - logger.info("Found " + users.size() + " users for the leaderboard"); - - List leaderboard = users.stream().map(user -> { - // ignore non-users, e.g. bots - if (user.getType() != User.Type.USER) { - return null; - } - AtomicInteger score = new AtomicInteger(0); - Set reviewedPRs = new HashSet<>(); - Set changesRequestedSet = new HashSet<>(); - Set approvedSet = new HashSet<>(); - Set commentSet = new HashSet<>(); - - user.getReviews().stream() - .forEach(review -> { - if (review.getPullRequest().getAuthor().getLogin().equals(user.getLogin())) { - return; - } - - PullRequestReviewDTO reviewDTO = new PullRequestReviewDTO(review.getId(), review.getSubmittedAt(), review.getState()); - - reviewedPRs.add(review.getPullRequest().getNumber()); - switch (review.getState()) { - case CHANGES_REQUESTED: - changesRequestedSet.add(reviewDTO); - break; - case APPROVED: - approvedSet.add(reviewDTO); - break; - case COMMENTED: - commentSet.add(reviewDTO); - break; - default: - // ignore other states and don't add to score - return; - } - score.addAndGet(calculateScore(review.getPullRequest())); - }); - return new LeaderboardEntry(user.getLogin(), user.getAvatarUrl(), user.getName(), user.getType(), - score.get(), - 0, // preliminary rank - reviewedPRs.size(), - changesRequestedSet.toArray(new PullRequestReviewDTO[changesRequestedSet.size()]), - approvedSet.toArray(new PullRequestReviewDTO[approvedSet.size()]), - commentSet.toArray(new PullRequestReviewDTO[commentSet.size()])); - }).filter(Objects::nonNull).collect(Collectors.toCollection(ArrayList::new)); - - // update ranks by score - leaderboard.sort(Comparator.comparingInt(LeaderboardEntry::getScore).reversed()); - AtomicInteger rank = new AtomicInteger(1); - leaderboard.stream().forEach(entry -> { - entry.setRank(rank.get()); - rank.incrementAndGet(); - }); + var afterOffset = afterCutOff.atOffset(ZoneOffset.UTC); + var beforeOffset = beforeCutOff.map(b -> b.atOffset(ZoneOffset.UTC)).orElse(OffsetDateTime.now()); + List reviews = pullRequestReviewRepository.findAllInTimeframe(afterOffset, beforeOffset, repository); + + Map usersById = reviews.stream().map(PullRequestReview::getAuthor).collect(Collectors.toMap(User::getId, user -> user, (u1, u2) -> u1)); + Map> reviewsByUserId = reviews.stream().collect(Collectors.groupingBy(review -> review.getAuthor().getId())); + Map scoresByUserId = reviewsByUserId.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> calculateTotalScore(entry.getValue()))); + + // Ranking (sorted by score descending) + List rankingByUserId = scoresByUserId.entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + + List leaderboard = IntStream.range(0, rankingByUserId.size()) + .mapToObj(index -> { + int rank = index + 1; + Long userId = rankingByUserId.get(index); + int score = scoresByUserId.get(userId); + UserInfoDTO user = userDTOConverter.convertToDTO(usersById.get(userId)); + List userReviews = reviewsByUserId.get(userId); + int numberOfReviewedPRs = userReviews.stream().map(review -> review.getPullRequest().getId()).collect(Collectors.toSet()).size(); + int numberOfApprovals = (int) userReviews.stream().filter(review -> review.getState() == PullRequestReview.State.APPROVED).count(); + int numberOfChangeRequests = (int) userReviews.stream().filter(review -> review.getState() == PullRequestReview.State.CHANGES_REQUESTED).count(); + int numberOfComments = (int) userReviews.stream().filter(review -> review.getState() == PullRequestReview.State.COMMENTED).count(); + int numberOfUnknowns = userReviews.size() - numberOfApprovals - numberOfChangeRequests - numberOfComments; + int numberOfCodeComments = userReviews.stream().map(review -> review.getComments().size()).reduce(0, Integer::sum); + + return new LeaderboardEntryDTO(rank, score, user, numberOfReviewedPRs, numberOfApprovals, numberOfChangeRequests, numberOfComments, numberOfUnknowns, numberOfCodeComments); + }) + .toList(); return leaderboard; } + private int calculateTotalScore(List reviews) { + // Could contain multiple reviews for the same pull request + Map> reviewsByPullRequestId = reviews.stream() + .collect(Collectors.groupingBy(review -> review.getPullRequest().getId())); + + double totalScore = reviewsByPullRequestId + .values() + .stream() + .map(pullRequestReviews -> { + // All reviews are for the same pull request + int complexityScore = calculateComplexityScore(pullRequestReviews.get(0).getPullRequest()); + + int approvalReviews = (int) pullRequestReviews.stream() + .filter(review -> review.getState() == PullRequestReview.State.APPROVED) + .count(); + int changesRequestedReviews = (int) pullRequestReviews.stream() + .filter(review -> review.getState() == PullRequestReview.State.CHANGES_REQUESTED) + .count(); + int commentReviews = (int) pullRequestReviews.stream() + .filter(review -> review.getState() == PullRequestReview.State.COMMENTED) + .count(); + int unknownReviews = pullRequestReviews.size() - approvalReviews - changesRequestedReviews + - commentReviews; + + int codeComments = pullRequestReviews.stream() + .map(review -> review.getComments().size()) + .reduce(0, Integer::sum); + + double interactionScore = (approvalReviews * 1.5 + + changesRequestedReviews * 2.0 + + (commentReviews + unknownReviews) + + codeComments * 0.5); + + double complexityBonus = 1 + (complexityScore - 1) / 32.0; + + return 5 * interactionScore * complexityBonus; + }) + .reduce(0.0, Double::sum); + return (int) Math.ceil(totalScore); + } + /** - * Calculates the score for a given pull request. + * Calculates the complexity score for a given pull request. * Possible values: 1, 3, 7, 17, 33. * Taken from the original leaderboard implementation script. * * @param pullRequest * @return score */ - private int calculateScore(PullRequest pullRequest) { + private int calculateComplexityScore(PullRequest pullRequest) { Double complexityScore = ((pullRequest.getChangedFiles() * 3) + (pullRequest.getCommits() * 0.5) + pullRequest.getAdditions() + pullRequest.getDeletions()) / 10; if (complexityScore < 10) { diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/dto/LeaderboardEntryDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/dto/LeaderboardEntryDTO.java new file mode 100644 index 00000000..65092943 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/dto/LeaderboardEntryDTO.java @@ -0,0 +1,16 @@ +package de.tum.in.www1.hephaestus.leaderboard.dto; + +import de.tum.in.www1.hephaestus.gitprovider.user.dto.UserInfoDTO; +import io.micrometer.common.lang.NonNull; + +public record LeaderboardEntryDTO( + @NonNull Integer rank, + @NonNull Integer score, + @NonNull UserInfoDTO user, + @NonNull Integer numberOfReviewedPRs, + @NonNull Integer numberOfApprovals, + @NonNull Integer numberOfChangeRequests, + @NonNull Integer numberOfComments, + @NonNull Integer numberOfUnknowns, + @NonNull Integer numberOfCodeComments) { +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/syncing/GitHubDataSyncService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/syncing/GitHubDataSyncService.java index 97fd2423..a276f28e 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/syncing/GitHubDataSyncService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/syncing/GitHubDataSyncService.java @@ -2,8 +2,6 @@ import java.util.Date; import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/syncing/NatsConsumerService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/syncing/NatsConsumerService.java index 8d0cc3d2..f05704c7 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/syncing/NatsConsumerService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/syncing/NatsConsumerService.java @@ -37,6 +37,9 @@ public class NatsConsumerService { @Value("${nats.enabled}") private boolean isNatsEnabled; + @Value("${nats.timeframe}") + private int timeframe; + @Value("${nats.server}") private String natsServer; @@ -101,7 +104,7 @@ private void setupConsumer(Connection connection) throws IOException, Interrupte ConsumerConfiguration.Builder consumerConfigBuilder = ConsumerConfiguration.builder() .filterSubjects(getSubjects()) .deliverPolicy(DeliverPolicy.ByStartTime) - .startTime(ZonedDateTime.now().minusDays(30)); + .startTime(ZonedDateTime.now().minusDays(timeframe)); if (durableConsumerName != null && !durableConsumerName.isEmpty()) { consumerConfigBuilder.durable(durableConsumerName); diff --git a/server/application-server/src/main/resources/application-prod.yml b/server/application-server/src/main/resources/application-prod.yml index d1943ab2..22613429 100644 --- a/server/application-server/src/main/resources/application-prod.yml +++ b/server/application-server/src/main/resources/application-prod.yml @@ -16,6 +16,7 @@ spring: nats: enabled: ${NATS_ENABLED:false} + timeframe: ${MONITORING_TIMEFRAME:7} durableConsumerName: ${NATS_DURABLE_CONSUMER_NAME} server: ${NATS_SERVER} diff --git a/server/application-server/src/main/resources/application.yml b/server/application-server/src/main/resources/application.yml index 6258e83f..6258297f 100644 --- a/server/application-server/src/main/resources/application.yml +++ b/server/application-server/src/main/resources/application.yml @@ -33,6 +33,7 @@ springdoc: nats: enabled: false + timeframe: 7 durableConsumerName: "" server: ""