diff --git a/docs/contributor/erd/schema.mmd b/docs/contributor/erd/schema.mmd index 2a078a86f..c311bc2c9 100644 --- a/docs/contributor/erd/schema.mmd +++ b/docs/contributor/erd/schema.mmd @@ -203,6 +203,18 @@ erDiagram BIGINT author_id FK BIGINT pull_request_id FK BIGINT review_id FK + BIGINT thread_id FK "NOT NULL" + BIGINT in_reply_to_id FK + } + + PullRequestReviewThread { + BIGINT id PK + TIMESTAMPTZ created_at + TIMESTAMPTZ updated_at + VARCHAR(20) state "NOT NULL" + TIMESTAMPTZ resolved_at + BIGINT pull_request_id FK "NOT NULL" + BIGINT root_comment_id FK,UK } PullRequestBadPractice { @@ -320,6 +332,7 @@ erDiagram %% One-to-One relationships ChatMessage ||--|| ChatMessagePart : has ChatMessage ||--|| ChatThread : references + PullRequestReviewComment ||--|| PullRequestReviewThread : reviewed Organization ||--|| Workspace : has %% One-to-Many relationships @@ -341,8 +354,11 @@ erDiagram User ||--o{ PullRequestReview : reviewed Issue ||--o{ PullRequestReview : reviewed User ||--o{ PullRequestReviewComment : commented_on + PullRequestReviewComment ||--o{ PullRequestReviewComment : commented_on Issue ||--o{ PullRequestReviewComment : commented_on PullRequestReview ||--o{ PullRequestReviewComment : commented_on + PullRequestReviewThread ||--o{ PullRequestReviewComment : commented_on + Issue ||--o{ PullRequestReviewThread : reviewed BadPracticeDetection ||--o{ PullRequestBadPractice : has Issue ||--o{ PullRequestBadPractice : references Organization ||--o{ Repository : has diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/PullRequest.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/PullRequest.java index e2a682dc5..0d25f07e8 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/PullRequest.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequest/PullRequest.java @@ -3,6 +3,7 @@ import de.tum.in.www1.hephaestus.gitprovider.issue.Issue; import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReview; import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.PullRequestReviewComment; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThread; import de.tum.in.www1.hephaestus.gitprovider.user.User; import jakarta.persistence.*; import java.time.Instant; @@ -63,6 +64,10 @@ public class PullRequest extends Issue { @ToString.Exclude private Set reviewComments = new HashSet<>(); + @OneToMany(mappedBy = "pullRequest", cascade = CascadeType.REMOVE, orphanRemoval = true) + @ToString.Exclude + private Set reviewThreads = new HashSet<>(); + @Lob private String badPracticeSummary; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/PullRequestReviewComment.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/PullRequestReviewComment.java index 600a4ae8d..c0bdc4d8c 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/PullRequestReviewComment.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/PullRequestReviewComment.java @@ -4,6 +4,7 @@ import de.tum.in.www1.hephaestus.gitprovider.common.BaseGitServiceEntity; import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReview; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThread; import de.tum.in.www1.hephaestus.gitprovider.user.User; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -11,7 +12,10 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.Lob; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import java.util.HashSet; +import java.util.Set; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -96,6 +100,20 @@ public class PullRequestReviewComment extends BaseGitServiceEntity { @ToString.Exclude private PullRequest pullRequest; + @ManyToOne + @JoinColumn(name = "thread_id", nullable = false) + @ToString.Exclude + private PullRequestReviewThread thread; + + @ManyToOne + @JoinColumn(name = "in_reply_to_id") + @ToString.Exclude + private PullRequestReviewComment inReplyTo; + + @OneToMany(mappedBy = "inReplyTo") + @ToString.Exclude + private Set replies = new HashSet<>(); + public enum Side { LEFT, RIGHT, diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/PullRequestReviewCommentRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/PullRequestReviewCommentRepository.java index 0e3b12e34..eae6aa924 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/PullRequestReviewCommentRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/PullRequestReviewCommentRepository.java @@ -2,4 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; -public interface PullRequestReviewCommentRepository extends JpaRepository {} +public interface PullRequestReviewCommentRepository extends JpaRepository { + boolean existsByThreadId(Long threadId); + + long countByThreadId(Long threadId); +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentConverter.java index ea6f81af9..f6b605c43 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentConverter.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentConverter.java @@ -40,17 +40,28 @@ public PullRequestReviewComment update( comment.setBody(source.getBody()); comment.setHtmlUrl(source.getHtmlUrl().toString()); comment.setAuthorAssociation(authorAssociationConverter.convert(source.getAuthorAssociation())); - comment.setStartLine(source.getPosition()); - comment.setOriginalStartLine(source.getOriginalPosition()); - comment.setLine(source.getPosition()); - comment.setOriginalLine(source.getOriginalPosition()); - comment.setStartSide(convertSide(source.getStartSide())); + comment.setStartLine(nullIfZero(source.getStartLine())); + comment.setOriginalStartLine(nullIfZero(source.getOriginalStartLine())); + comment.setLine(source.getLine()); + comment.setOriginalLine(source.getOriginalLine()); + comment.setStartSide(convertNullableSide(source.getStartSide())); comment.setSide(convertSide(source.getSide())); comment.setPosition(source.getPosition()); comment.setOriginalPosition(source.getOriginalPosition()); return comment; } + private Integer nullIfZero(int value) { + return value == 0 ? null : value; + } + + private PullRequestReviewComment.Side convertNullableSide(Side side) { + if (side == null) { + return null; + } + return convertSide(side); + } + private PullRequestReviewComment.Side convertSide(Side side) { switch (side) { case LEFT: diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentMessageHandler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentMessageHandler.java index 5d3c07939..d26ba5c3d 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentMessageHandler.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentMessageHandler.java @@ -21,7 +21,7 @@ public class GitHubPullRequestReviewCommentMessageHandler private final GitHubPullRequestSyncService pullRequestSyncService; private final GitHubRepositorySyncService repositorySyncService; - private GitHubPullRequestReviewCommentMessageHandler( + public GitHubPullRequestReviewCommentMessageHandler( PullRequestReviewCommentRepository pullRequestReviewCommentRepository, GitHubPullRequestReviewCommentSyncService pullRequestReviewCommentSyncService, GitHubPullRequestSyncService pullRequestSyncService, @@ -51,9 +51,9 @@ protected void handleEvent(GHEventPayload.PullRequestReviewComment eventPayload) pullRequestSyncService.processPullRequest(pullRequest); if (action.equals("deleted")) { - pullRequestReviewCommentRepository.deleteById(comment.getId()); + pullRequestReviewCommentSyncService.deletePullRequestReviewComment(comment.getId()); } else { - pullRequestReviewCommentSyncService.processPullRequestReviewComment(comment); + pullRequestReviewCommentSyncService.processPullRequestReviewComment(comment, pullRequest); } } 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 30010a08b..1cb18a9b1 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 @@ -5,11 +5,14 @@ import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReviewRepository; import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.PullRequestReviewComment; import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.PullRequestReviewCommentRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThread; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThreadRepository; import de.tum.in.www1.hephaestus.gitprovider.user.UserRepository; import de.tum.in.www1.hephaestus.gitprovider.user.github.GitHubUserConverter; import jakarta.transaction.Transactional; import java.io.IOException; import java.util.List; +import java.util.Optional; import org.kohsuke.github.GHPullRequest; import org.kohsuke.github.GHPullRequestReviewComment; import org.kohsuke.github.GHUser; @@ -25,6 +28,7 @@ public class GitHubPullRequestReviewCommentSyncService { private final PullRequestReviewCommentRepository pullRequestReviewCommentRepository; private final PullRequestReviewRepository pullRequestReviewRepository; private final PullRequestRepository pullRequestRepository; + private final PullRequestReviewThreadRepository pullRequestReviewThreadRepository; private final UserRepository userRepository; private final GitHubPullRequestReviewCommentConverter pullRequestReviewCommentConverter; private final GitHubPullRequestConverter pullRequestConverter; @@ -34,6 +38,7 @@ public GitHubPullRequestReviewCommentSyncService( PullRequestReviewCommentRepository pullRequestReviewCommentRepository, PullRequestReviewRepository pullRequestReviewRepository, PullRequestRepository pullRequestRepository, + PullRequestReviewThreadRepository pullRequestReviewThreadRepository, UserRepository userRepository, GitHubPullRequestReviewCommentConverter pullRequestReviewCommentConverter, GitHubPullRequestConverter pullRequestConverter, @@ -42,6 +47,7 @@ public GitHubPullRequestReviewCommentSyncService( this.pullRequestReviewCommentRepository = pullRequestReviewCommentRepository; this.pullRequestReviewRepository = pullRequestReviewRepository; this.pullRequestRepository = pullRequestRepository; + this.pullRequestReviewThreadRepository = pullRequestReviewThreadRepository; this.userRepository = userRepository; this.pullRequestReviewCommentConverter = pullRequestReviewCommentConverter; this.pullRequestConverter = pullRequestConverter; @@ -64,7 +70,10 @@ public void syncReviewCommentsOfAllPullRequests(List pullRequests * @param pullRequest the GitHub pull request to sync review comments for */ public void syncReviewCommentsOfPullRequest(GHPullRequest pullRequest) { - pullRequest.listReviewComments().withPageSize(100).forEach(this::processPullRequestReviewComment); + pullRequest + .listReviewComments() + .withPageSize(100) + .forEach(comment -> processPullRequestReviewComment(comment, pullRequest)); } /** @@ -82,71 +91,209 @@ public void syncReviewCommentsOfPullRequest(GHPullRequest pullRequest) { public PullRequestReviewComment processPullRequestReviewComment( GHPullRequestReviewComment ghPullRequestReviewComment ) { - var result = pullRequestReviewCommentRepository - .findById(ghPullRequestReviewComment.getId()) - .map(pullRequestReviewComment -> { - try { - if ( - pullRequestReviewComment.getUpdatedAt() == null || - pullRequestReviewComment.getUpdatedAt().isBefore(ghPullRequestReviewComment.getUpdatedAt()) - ) { - return pullRequestReviewCommentConverter.update( - ghPullRequestReviewComment, - pullRequestReviewComment - ); - } - return pullRequestReviewComment; - } catch (IOException e) { - logger.error( - "Failed to update pull request review comment {}: {}", - ghPullRequestReviewComment.getId(), - e.getMessage() - ); - return null; - } - }) - .orElseGet(() -> pullRequestReviewCommentConverter.convert(ghPullRequestReviewComment)); + return processPullRequestReviewComment(ghPullRequestReviewComment, null); + } + + @Transactional + public PullRequestReviewComment processPullRequestReviewComment( + GHPullRequestReviewComment ghPullRequestReviewComment, + GHPullRequest providedPullRequest + ) { + var existing = pullRequestReviewCommentRepository.findById(ghPullRequestReviewComment.getId()).orElse(null); + var result = existing != null + ? updateIfNewer(ghPullRequestReviewComment, existing) + : pullRequestReviewCommentConverter.convert(ghPullRequestReviewComment); if (result == null) { return null; } - // Link pull request - var pullRequest = ghPullRequestReviewComment.getParent(); + var pullRequest = resolvePullRequest(ghPullRequestReviewComment, providedPullRequest); + if (pullRequest == null) { + logger.warn( + "Unable to determine pull request for review comment {}. Skipping.", + ghPullRequestReviewComment.getId() + ); + return null; + } var resultPullRequest = pullRequestRepository .findById(pullRequest.getId()) .orElseGet(() -> pullRequestRepository.save(pullRequestConverter.convert(pullRequest))); result.setPullRequest(resultPullRequest); - // Link review - var review = pullRequestReviewRepository.findById(ghPullRequestReviewComment.getPullRequestReviewId()); - if (review.isPresent()) { - result.setReview(review.get()); - } else { - // If review is not found, we cannot link the review comment and would need to - // fetch the associated review - logger.error( + pullRequestReviewRepository + .findById(ghPullRequestReviewComment.getPullRequestReviewId()) + .ifPresentOrElse(result::setReview, () -> logger.error( "Failed to link review for pull request review comment {}: {}", ghPullRequestReviewComment.getId(), "Review not found" - ); + )); + + attachAuthor(ghPullRequestReviewComment, result); + + attachInReplyTo(ghPullRequestReviewComment, result); + + PullRequestReviewThread thread = ensureThread(ghPullRequestReviewComment, resultPullRequest); + result.setThread(thread); + + var persisted = pullRequestReviewCommentRepository.save(result); + + updateThreadMetadata(thread, persisted, ghPullRequestReviewComment); + + return persisted; + } + + private PullRequestReviewComment updateIfNewer( + GHPullRequestReviewComment source, + PullRequestReviewComment target + ) { + try { + if (target.getUpdatedAt() == null || target.getUpdatedAt().isBefore(source.getUpdatedAt())) { + return pullRequestReviewCommentConverter.update(source, target); + } + return target; + } catch (IOException e) { + logger.error("Failed to update pull request review comment {}: {}", source.getId(), e.getMessage()); + return null; } + } - // Link author + private void attachAuthor( + GHPullRequestReviewComment ghPullRequestReviewComment, + PullRequestReviewComment comment + ) { try { GHUser user = ghPullRequestReviewComment.getUser(); + if (user == null) { + comment.setAuthor(null); + return; + } var resultAuthor = userRepository .findById(user.getId()) .orElseGet(() -> userRepository.save(userConverter.convert(user))); - result.setAuthor(resultAuthor); - } catch (IOException e) { + comment.setAuthor(resultAuthor); + } catch (IOException | NullPointerException e) { logger.error( "Failed to link author for pull request review comment {}: {}", ghPullRequestReviewComment.getId(), e.getMessage() ); } + } + + private void attachInReplyTo( + GHPullRequestReviewComment ghPullRequestReviewComment, + PullRequestReviewComment comment + ) { + long inReplyToId = ghPullRequestReviewComment.getInReplyToId(); + if (inReplyToId <= 0L) { + comment.setInReplyTo(null); + return; + } + + Optional parent = pullRequestReviewCommentRepository.findById(inReplyToId); + if (parent.isPresent()) { + comment.setInReplyTo(parent.get()); + } else { + logger.warn( + "Parent review comment {} not found for reply {}", + inReplyToId, + ghPullRequestReviewComment.getId() + ); + comment.setInReplyTo(null); + } + } + + private PullRequestReviewThread ensureThread( + GHPullRequestReviewComment ghPullRequestReviewComment, + de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest pullRequest + ) { + long rootCommentId = extractRootCommentId(ghPullRequestReviewComment); + + PullRequestReviewThread thread = pullRequestReviewThreadRepository + .findById(rootCommentId) + .orElseGet(() -> { + PullRequestReviewThread newThread = new PullRequestReviewThread(); + newThread.setId(rootCommentId); + newThread.setState(PullRequestReviewThread.State.UNRESOLVED); + newThread.setPullRequest(pullRequest); + return pullRequestReviewThreadRepository.save(newThread); + }); + + thread.setPullRequest(pullRequest); + return thread; + } + + private void updateThreadMetadata( + PullRequestReviewThread thread, + PullRequestReviewComment comment, + GHPullRequestReviewComment ghPullRequestReviewComment + ) { + if (!thread.getComments().contains(comment)) { + thread.getComments().add(comment); + } + + if (ghPullRequestReviewComment.getInReplyToId() <= 0L) { + thread.setRootComment(comment); + } + + if ( + thread.getCreatedAt() == null || + (comment.getCreatedAt() != null && comment.getCreatedAt().isBefore(thread.getCreatedAt())) + ) { + thread.setCreatedAt(comment.getCreatedAt()); + } + + var updatedTimestamp = comment.getUpdatedAt() != null ? comment.getUpdatedAt() : comment.getCreatedAt(); + if (updatedTimestamp != null) { + thread.setUpdatedAt(updatedTimestamp); + } + + pullRequestReviewThreadRepository.save(thread); + } + + @Transactional + public void deletePullRequestReviewComment(long commentId) { + pullRequestReviewCommentRepository + .findById(commentId) + .ifPresent(comment -> { + var thread = comment.getThread(); + boolean isRootComment = thread != null && thread.getRootComment() != null && thread.getRootComment().getId().equals(commentId); + + if (isRootComment) { + pullRequestReviewThreadRepository.delete(thread); + } else { + pullRequestReviewCommentRepository.delete(comment); + if (thread != null && pullRequestReviewCommentRepository.countByThreadId(thread.getId()) == 0) { + pullRequestReviewThreadRepository.delete(thread); + } + } + }); + } + + private GHPullRequest resolvePullRequest( + GHPullRequestReviewComment ghPullRequestReviewComment, + GHPullRequest providedPullRequest + ) { + if (providedPullRequest != null) { + return providedPullRequest; + } + + var parent = ghPullRequestReviewComment.getParent(); + if (parent == null) { + logger.warn( + "GitHub did not include pull request details for review comment {}", + ghPullRequestReviewComment.getId() + ); + } + return parent; + } - return pullRequestReviewCommentRepository.save(result); + private long extractRootCommentId(GHPullRequestReviewComment ghPullRequestReviewComment) { + long inReplyToId = ghPullRequestReviewComment.getInReplyToId(); + if (inReplyToId <= 0L) { + return ghPullRequestReviewComment.getId(); + } + return inReplyToId; } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/PullRequestReviewThread.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/PullRequestReviewThread.java new file mode 100644 index 000000000..b1c3d513f --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/PullRequestReviewThread.java @@ -0,0 +1,57 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread; + +import de.tum.in.www1.hephaestus.gitprovider.common.BaseGitServiceEntity; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.PullRequestReviewComment; +import de.tum.in.www1.hephaestus.gitprovider.user.User; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Table(name = "pull_request_review_thread") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class PullRequestReviewThread extends BaseGitServiceEntity { + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private State state = State.UNRESOLVED; + + private Instant resolvedAt; + + @OneToOne + @JoinColumn(name = "root_comment_id") + @ToString.Exclude + private PullRequestReviewComment rootComment; + + @ManyToOne(optional = false) + @JoinColumn(name = "pull_request_id") + @ToString.Exclude + private PullRequest pullRequest; + + @OneToMany(mappedBy = "thread", cascade = CascadeType.ALL, orphanRemoval = true) + @ToString.Exclude + private Set comments = new HashSet<>(); + + public enum State { + RESOLVED, + UNRESOLVED, + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/PullRequestReviewThreadRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/PullRequestReviewThreadRepository.java new file mode 100644 index 000000000..75d9660a4 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/PullRequestReviewThreadRepository.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PullRequestReviewThreadRepository extends JpaRepository {} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadMessageHandler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadMessageHandler.java new file mode 100644 index 000000000..500778d48 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadMessageHandler.java @@ -0,0 +1,56 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.github; + +import de.tum.in.www1.hephaestus.gitprovider.common.github.GitHubMessageHandler; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.github.GitHubPullRequestSyncService; +import de.tum.in.www1.hephaestus.gitprovider.repository.github.GitHubRepositorySyncService; +import org.kohsuke.github.GHEvent; +import org.kohsuke.github.GHEventPayloadPullRequestReviewThread; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class GitHubPullRequestReviewThreadMessageHandler + extends GitHubMessageHandler { + + private static final Logger logger = LoggerFactory.getLogger(GitHubPullRequestReviewThreadMessageHandler.class); + + private final GitHubPullRequestReviewThreadSyncService threadSyncService; + private final GitHubPullRequestSyncService pullRequestSyncService; + private final GitHubRepositorySyncService repositorySyncService; + + public GitHubPullRequestReviewThreadMessageHandler( + GitHubPullRequestReviewThreadSyncService threadSyncService, + GitHubPullRequestSyncService pullRequestSyncService, + GitHubRepositorySyncService repositorySyncService + ) { + super(GHEventPayloadPullRequestReviewThread.class); + this.threadSyncService = threadSyncService; + this.pullRequestSyncService = pullRequestSyncService; + this.repositorySyncService = repositorySyncService; + } + + @Override + protected void handleEvent(GHEventPayloadPullRequestReviewThread eventPayload) { + var action = eventPayload.getAction(); + var pullRequest = eventPayload.getPullRequest(); + var repository = eventPayload.getRepository(); + + logger.info( + "Received pull request review thread event for repository: {}, pull request: {}, action: {}", + repository.getFullName(), + pullRequest.getNumber(), + action + ); + + repositorySyncService.processRepository(repository); + pullRequestSyncService.processPullRequest(pullRequest); + + threadSyncService.processThreadEvent(eventPayload); + } + + @Override + protected GHEvent getHandlerEvent() { + return GHEvent.PULL_REQUEST_REVIEW_THREAD; + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadSyncService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadSyncService.java new file mode 100644 index 000000000..491b9c881 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadSyncService.java @@ -0,0 +1,112 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.github; + +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequestRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.github.GitHubPullRequestConverter; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.PullRequestReviewComment; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.github.GitHubPullRequestReviewCommentSyncService; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThread; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThreadRepository; +import de.tum.in.www1.hephaestus.gitprovider.user.UserRepository; +import de.tum.in.www1.hephaestus.gitprovider.user.github.GitHubUserConverter; +import jakarta.transaction.Transactional; +import java.io.IOException; +import java.time.Instant; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import org.kohsuke.github.GHEventPayloadPullRequestReviewThread; +import org.kohsuke.github.GHPullRequestReviewComment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class GitHubPullRequestReviewThreadSyncService { + + private static final Logger logger = LoggerFactory.getLogger(GitHubPullRequestReviewThreadSyncService.class); + + private final GitHubPullRequestReviewCommentSyncService commentSyncService; + private final PullRequestReviewThreadRepository pullRequestReviewThreadRepository; + private final PullRequestRepository pullRequestRepository; + private final GitHubPullRequestConverter pullRequestConverter; + private final UserRepository userRepository; + private final GitHubUserConverter userConverter; + + public GitHubPullRequestReviewThreadSyncService( + GitHubPullRequestReviewCommentSyncService commentSyncService, + PullRequestReviewThreadRepository pullRequestReviewThreadRepository, + PullRequestRepository pullRequestRepository, + GitHubPullRequestConverter pullRequestConverter, + UserRepository userRepository, + GitHubUserConverter userConverter + ) { + this.commentSyncService = commentSyncService; + this.pullRequestReviewThreadRepository = pullRequestReviewThreadRepository; + this.pullRequestRepository = pullRequestRepository; + this.pullRequestConverter = pullRequestConverter; + this.userRepository = userRepository; + this.userConverter = userConverter; + } + + @Transactional + public PullRequestReviewThread processThreadEvent(GHEventPayloadPullRequestReviewThread payload) { + var threadPayload = payload.getThread(); + if (threadPayload == null || threadPayload.getComments() == null || threadPayload.getComments().isEmpty()) { + logger.warn("Received pull request review thread event without comments"); + return null; + } + + var ghPullRequest = payload.getPullRequest(); + var pullRequest = pullRequestRepository + .findById(ghPullRequest.getId()) + .orElseGet(() -> pullRequestRepository.save(pullRequestConverter.convert(ghPullRequest))); + + PullRequestReviewThread thread = null; + var sortedComments = threadPayload + .getComments() + .stream() + .sorted((left, right) -> Long.compare(left.getInReplyToId(), right.getInReplyToId())) + .toList(); + + for (GHPullRequestReviewComment comment : sortedComments) { + PullRequestReviewComment persisted = commentSyncService.processPullRequestReviewComment(comment, ghPullRequest); + if (persisted != null) { + thread = persisted.getThread(); + } + } + + if (thread == null) { + logger.warn("Unable to resolve thread from comments, skipping state update"); + return null; + } + + thread.setPullRequest(pullRequest); + thread.setUpdatedAt(determineTimestamp(threadPayload.getComments())); + + if ("resolved".equalsIgnoreCase(payload.getAction())) { + thread.setState(PullRequestReviewThread.State.RESOLVED); + thread.setResolvedAt(thread.getUpdatedAt()); + } else if ("unresolved".equalsIgnoreCase(payload.getAction())) { + thread.setState(PullRequestReviewThread.State.UNRESOLVED); + thread.setResolvedAt(null); + } + + return pullRequestReviewThreadRepository.save(thread); + } + + private Instant determineTimestamp(List comments) { + return comments + .stream() + .map(comment -> { + try { + return Optional.ofNullable(comment.getUpdatedAt()).orElse(comment.getCreatedAt()); + } catch (IOException e) { + logger.warn("Failed to read timestamps for comment {}: {}", comment.getId(), e.getMessage()); + return (Instant) null; + } + }) + .filter(java.util.Objects::nonNull) + .max(Comparator.naturalOrder()) + .orElse(Instant.now()); + } +} diff --git a/server/application-server/src/main/java/org/kohsuke/github/GHEventPayloadPullRequestReviewThread.java b/server/application-server/src/main/java/org/kohsuke/github/GHEventPayloadPullRequestReviewThread.java new file mode 100644 index 000000000..f7120ae06 --- /dev/null +++ b/server/application-server/src/main/java/org/kohsuke/github/GHEventPayloadPullRequestReviewThread.java @@ -0,0 +1,52 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +/** + * Lightweight representation of the {@code pull_request_review_thread} webhook payload. + * + *

+ * GitHub ships the payload with an array of comments that belong to the thread. We reuse + * {@link GHPullRequestReviewComment} here so the rest of the application can keep relying on the + * official API model when converting the payload. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GHEventPayloadPullRequestReviewThread extends GHEventPayload { + + private GHPullRequest pullRequest; + + private GHRepository repository; + + private Thread thread; + + public GHPullRequest getPullRequest() { + return pullRequest; + } + + public GHRepository getRepository() { + return repository; + } + + public Thread getThread() { + return thread; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Thread { + + @JsonProperty("node_id") + private String nodeId; + + private List comments; + + public String getNodeId() { + return nodeId; + } + + public List getComments() { + return comments; + } + } +} diff --git a/server/application-server/src/main/resources/db/changelog/1761774703038_changelog.xml b/server/application-server/src/main/resources/db/changelog/1761774703038_changelog.xml new file mode 100644 index 000000000..38ab53d4d --- /dev/null +++ b/server/application-server/src/main/resources/db/changelog/1761774703038_changelog.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a thread for each existing comment, treating each as its own thread + + INSERT INTO pull_request_review_thread (id, created_at, updated_at, state, pull_request_id, root_comment_id) + SELECT + c.id as id, + c.created_at, + c.updated_at, + 'UNRESOLVED' as state, + c.pull_request_id, + c.id as root_comment_id + FROM pull_request_review_comment c + WHERE c.thread_id IS NULL; + + + + + Link existing comments to their newly created threads + + UPDATE pull_request_review_comment c + SET thread_id = c.id + WHERE c.thread_id IS NULL; + + + + + Add NOT NULL constraint to thread_id after migrating existing data + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/application-server/src/main/resources/db/master.xml b/server/application-server/src/main/resources/db/master.xml index 7ce3ae2dc..5df024ab3 100644 --- a/server/application-server/src/main/resources/db/master.xml +++ b/server/application-server/src/main/resources/db/master.xml @@ -24,4 +24,5 @@ + diff --git a/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/common/GitHubPayloadExtension.java b/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/common/GitHubPayloadExtension.java index 03546aac7..80c792c5b 100644 --- a/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/common/GitHubPayloadExtension.java +++ b/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/common/GitHubPayloadExtension.java @@ -1,8 +1,9 @@ package de.tum.in.www1.hephaestus.gitprovider.common; +import java.io.IOException; +import java.io.InputStream; import java.io.StringReader; -import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; @@ -35,8 +36,16 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } } - private T loadPayload(String fileName, Class payloadType) throws Exception { - String jsonPayload = Files.readString(Paths.get("src/test/resources/github/" + fileName)); - return GitHub.offline().parseEventPayload(new StringReader(jsonPayload), payloadType); + private T loadPayload(String fileName, Class payloadType) throws IOException { + String resourcePath = "github/" + fileName; + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath)) { + if (inputStream == null) { + throw new IOException("GitHub payload not found on classpath: " + resourcePath); + } + String jsonPayload = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + try (StringReader reader = new StringReader(jsonPayload)) { + return GitHub.offline().parseEventPayload(reader, payloadType); + } + } } } diff --git a/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/github/GitHubPullRequestReviewMessageHandlerIntegrationTest.java b/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/github/GitHubPullRequestReviewMessageHandlerIntegrationTest.java new file mode 100644 index 000000000..543b0b2e8 --- /dev/null +++ b/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreview/github/GitHubPullRequestReviewMessageHandlerIntegrationTest.java @@ -0,0 +1,108 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.github; + +import static org.assertj.core.api.Assertions.assertThat; + +import de.tum.in.www1.hephaestus.gitprovider.common.GitHubPayload; +import de.tum.in.www1.hephaestus.gitprovider.common.GitHubPayloadExtension; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequestRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.github.GitHubPullRequestConverter; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReview; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreview.PullRequestReviewRepository; +import de.tum.in.www1.hephaestus.testconfig.BaseIntegrationTest; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHEventPayload; +import org.springframework.beans.factory.annotation.Autowired; + +@DisplayName("GitHub Pull Request Review Message Handler") +@ExtendWith(GitHubPayloadExtension.class) +class GitHubPullRequestReviewMessageHandlerIntegrationTest extends BaseIntegrationTest { + + @Autowired + private GitHubPullRequestReviewMessageHandler handler; + + @Autowired + private PullRequestReviewRepository reviewRepository; + + @Autowired + private PullRequestRepository pullRequestRepository; + + @Autowired + private GitHubPullRequestConverter pullRequestConverter; + + @BeforeEach + void setUp() { + databaseTestUtils.cleanDatabase(); + } + + @Test + @DisplayName("should persist submitted reviews") + void submittedEventPersistsReview( + @GitHubPayload("pull_request_review.submitted") GHEventPayload.PullRequestReview payload + ) throws Exception { + // Act + handler.handleEvent(payload); + + // Assert + var review = reviewRepository.findById(payload.getReview().getId()); + assertThat(review) + .isPresent() + .get() + .satisfies(saved -> { + assertThat(saved.getState()).isEqualTo(PullRequestReview.State.APPROVED); + assertThat(saved.isDismissed()).isFalse(); + assertThat(saved.getHtmlUrl()).isEqualTo(payload.getReview().getHtmlUrl().toString()); + assertThat(saved.getSubmittedAt()).isEqualTo(payload.getReview().getSubmittedAt()); + }); + } + + @Test + @DisplayName("should retain review state when dismissed") + void dismissedEventMarksReview( + @GitHubPayload("pull_request_review.dismissed") GHEventPayload.PullRequestReview dismissed + ) throws Exception { + // Arrange + var pullRequest = dismissed.getPullRequest(); + var pullRequestEntity = pullRequestRepository + .findById(pullRequest.getId()) + .orElseGet(() -> pullRequestRepository.save(pullRequestConverter.convert(pullRequest))); + + PullRequestReview existingReview = new PullRequestReview(); + existingReview.setId(dismissed.getReview().getId()); + existingReview.setState(PullRequestReview.State.APPROVED); + existingReview.setDismissed(false); + existingReview.setHtmlUrl(dismissed.getReview().getHtmlUrl().toString()); + existingReview.setSubmittedAt(Instant.now()); + existingReview.setPullRequest(pullRequestEntity); + reviewRepository.save(existingReview); + + // Act + handler.handleEvent(dismissed); + + // Assert + var review = reviewRepository.findById(dismissed.getReview().getId()).orElseThrow(); + assertThat(review.getState()).isEqualTo(PullRequestReview.State.APPROVED); + assertThat(review.isDismissed()).isTrue(); + } + + @Test + @DisplayName("should update existing reviews on edit events") + void editedEventUpdatesReview( + @GitHubPayload("pull_request_review.submitted") GHEventPayload.PullRequestReview submitted, + @GitHubPayload("pull_request_review.edited") GHEventPayload.PullRequestReview edited + ) throws Exception { + // Arrange + handler.handleEvent(submitted); + + // Act + handler.handleEvent(edited); + + // Assert + var review = reviewRepository.findById(submitted.getReview().getId()).orElseThrow(); + assertThat(review.getBody()).isEqualTo(edited.getReview().getBody()); + assertThat(review.getSubmittedAt()).isEqualTo(edited.getReview().getSubmittedAt()); + } +} diff --git a/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentMessageHandlerIntegrationTest.java b/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentMessageHandlerIntegrationTest.java new file mode 100644 index 000000000..a1d93066a --- /dev/null +++ b/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewcomment/github/GitHubPullRequestReviewCommentMessageHandlerIntegrationTest.java @@ -0,0 +1,205 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.github; + +import static org.assertj.core.api.Assertions.assertThat; + +import de.tum.in.www1.hephaestus.gitprovider.common.GitHubPayload; +import de.tum.in.www1.hephaestus.gitprovider.common.GitHubPayloadExtension; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequestRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.github.GitHubPullRequestConverter; +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.pullrequestreview.github.GitHubPullRequestReviewSyncService; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.PullRequestReviewComment; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewcomment.PullRequestReviewCommentRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThreadRepository; +import de.tum.in.www1.hephaestus.testconfig.BaseIntegrationTest; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHEventPayload; +import org.springframework.beans.factory.annotation.Autowired; + +@DisplayName("GitHub Pull Request Review Comment Message Handler") +@ExtendWith(GitHubPayloadExtension.class) +class GitHubPullRequestReviewCommentMessageHandlerIntegrationTest extends BaseIntegrationTest { + + @Autowired + private GitHubPullRequestReviewCommentMessageHandler handler; + + @Autowired + private GitHubPullRequestReviewSyncService reviewSyncService; + + @Autowired + private PullRequestReviewCommentRepository commentRepository; + + @Autowired + private PullRequestReviewThreadRepository threadRepository; + + @Autowired + private PullRequestReviewRepository reviewRepository; + + @Autowired + private PullRequestRepository pullRequestRepository; + + @Autowired + private GitHubPullRequestConverter pullRequestConverter; + + @BeforeEach + void setUp() { + databaseTestUtils.cleanDatabase(); + } + + @Test + @DisplayName("should persist newly created review comments and threads") + void createdEventPersistsCommentAndThread( + @GitHubPayload("pull_request_review_comment.created") GHEventPayload.PullRequestReviewComment commentPayload, + @GitHubPayload("pull_request_review.submitted") GHEventPayload.PullRequestReview reviewPayload + ) throws Exception { + // Arrange + reviewSyncService.processPullRequestReview(reviewPayload.getReview()); + assertThat(commentRepository.findById(commentPayload.getComment().getId())).isEmpty(); + + // Act + handler.handleEvent(commentPayload); + + // Assert + var savedComment = commentRepository.findById(commentPayload.getComment().getId()); + assertThat(savedComment) + .isPresent() + .get() + .satisfies(comment -> { + assertThat(comment.getBody()).isEqualTo(commentPayload.getComment().getBody()); + assertThat(comment.getHtmlUrl()).isEqualTo(commentPayload.getComment().getHtmlUrl().toString()); + assertThat(comment.getLine()).isEqualTo(commentPayload.getComment().getLine()); + assertThat(comment.getSide().name()).isEqualTo(commentPayload.getComment().getSide().name()); + assertThat(comment.getThread()).isNotNull(); + assertThat(comment.getThread().getId()).isEqualTo(comment.getId()); + assertThat(comment.getThread().getState()).isEqualTo(de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThread.State.UNRESOLVED); + }); + + assertThat(threadRepository.findById(commentPayload.getComment().getId())).isPresent(); + } + + @Test + @DisplayName("should update existing comments on edit events") + void editedEventUpdatesComment( + @GitHubPayload("pull_request_review_comment.created") GHEventPayload.PullRequestReviewComment createdPayload, + @GitHubPayload("pull_request_review_comment.edited") GHEventPayload.PullRequestReviewComment editedPayload, + @GitHubPayload("pull_request_review.submitted") GHEventPayload.PullRequestReview reviewPayload + ) throws Exception { + // Arrange + reviewSyncService.processPullRequestReview(reviewPayload.getReview()); + handler.handleEvent(createdPayload); + var original = commentRepository.findById(createdPayload.getComment().getId()).orElseThrow(); + assertThat(original.getBody()).isEqualTo(createdPayload.getComment().getBody()); + + // Act + handler.handleEvent(editedPayload); + + // Assert + var updated = commentRepository.findById(editedPayload.getComment().getId()).orElseThrow(); + assertThat(updated.getBody()).isEqualTo(editedPayload.getComment().getBody()); + assertThat(updated.getUpdatedAt()).isAfterOrEqualTo(original.getUpdatedAt()); + } + + @Test + @DisplayName("should remove comments and associated threads on delete events") + void deletedEventRemovesCommentAndThread( + @GitHubPayload("pull_request_review_comment.created") GHEventPayload.PullRequestReviewComment createdPayload, + @GitHubPayload("pull_request_review_comment.deleted") GHEventPayload.PullRequestReviewComment deletedPayload, + @GitHubPayload("pull_request_review.submitted") GHEventPayload.PullRequestReview reviewPayload + ) throws Exception { + // Arrange + reviewSyncService.processPullRequestReview(reviewPayload.getReview()); + handler.handleEvent(createdPayload); + assertThat(commentRepository.findById(createdPayload.getComment().getId())).isPresent(); + assertThat(threadRepository.findById(createdPayload.getComment().getId())).isPresent(); + + // Act + handler.handleEvent(deletedPayload); + + // Assert + assertThat(commentRepository.findById(createdPayload.getComment().getId())).isEmpty(); + assertThat(threadRepository.findById(createdPayload.getComment().getId())).isEmpty(); + } + + @Test + @DisplayName("should persist replies and link them to the root thread") + void createdReplyLinksToThread( + @GitHubPayload("pull_request_review_comment.created.thread-1") GHEventPayload.PullRequestReviewComment rootPayload, + @GitHubPayload("pull_request_review_comment.created.thread-2") GHEventPayload.PullRequestReviewComment replyPayload + ) throws Exception { + // Arrange root review + ensureReviewExists(rootPayload); + + handler.handleEvent(rootPayload); + var rootComment = commentRepository.findById(rootPayload.getComment().getId()).orElseThrow(); + assertThat(rootComment.getThread()).isNotNull(); + + // Act + ensureReviewExists(replyPayload); + handler.handleEvent(replyPayload); + + // Assert + var replyComment = commentRepository.findById(replyPayload.getComment().getId()).orElseThrow(); + assertThat(replyComment.getInReplyTo()).isNotNull(); + assertThat(replyComment.getInReplyTo().getId()).isEqualTo(rootPayload.getComment().getId()); + assertThat(replyComment.getThread()).isNotNull(); + var threadId = rootPayload.getComment().getId(); + assertThat(replyComment.getThread().getId()).isEqualTo(threadId); + assertThat(commentRepository.countByThreadId(threadId)).isEqualTo(2); + } + + @Test + @DisplayName("should retain threads when a reply is deleted") + void deletedReplyKeepsThread( + @GitHubPayload("pull_request_review_comment.created.thread-1") GHEventPayload.PullRequestReviewComment rootPayload, + @GitHubPayload("pull_request_review_comment.created.thread-2") GHEventPayload.PullRequestReviewComment replyPayload, + @GitHubPayload("pull_request_review_comment.deleted.thread-2") GHEventPayload.PullRequestReviewComment deletedReplyPayload + ) throws Exception { + // Arrange + ensureReviewExists(rootPayload); + handler.handleEvent(rootPayload); + ensureReviewExists(replyPayload); + handler.handleEvent(replyPayload); + var threadId = rootPayload.getComment().getId(); + assertThat(threadRepository.findById(threadId)).isPresent(); + assertThat(commentRepository.findById(replyPayload.getComment().getId())).isPresent(); + + // Act + handler.handleEvent(deletedReplyPayload); + + // Assert + assertThat(commentRepository.findById(replyPayload.getComment().getId())).isEmpty(); + var thread = threadRepository.findById(threadId).orElseThrow(); + assertThat(commentRepository.countByThreadId(threadId)).isEqualTo(1); + assertThat(thread.getRootComment()).isNotNull(); + assertThat(thread.getRootComment().getId()).isEqualTo(threadId); + } + + private void ensureReviewExists(GHEventPayload.PullRequestReviewComment payload) throws Exception { + var reviewId = payload.getComment().getPullRequestReviewId(); + if (reviewId == null) { + return; + } + + if (reviewRepository.findById(reviewId).isPresent()) { + return; + } + + var pullRequest = payload.getPullRequest(); + var pullRequestEntity = pullRequestRepository + .findById(pullRequest.getId()) + .orElseGet(() -> pullRequestRepository.save(pullRequestConverter.convert(pullRequest))); + + PullRequestReview review = new PullRequestReview(); + review.setId(reviewId); + review.setState(PullRequestReview.State.COMMENTED); + review.setHtmlUrl(payload.getComment().getHtmlUrl().toString()); + review.setSubmittedAt(Instant.now()); + review.setPullRequest(pullRequestEntity); + reviewRepository.save(review); + } +} diff --git a/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadMessageHandlerIntegrationTest.java b/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadMessageHandlerIntegrationTest.java new file mode 100644 index 000000000..83c8e236c --- /dev/null +++ b/server/application-server/src/test/java/de/tum/in/www1/hephaestus/gitprovider/pullrequestreviewthread/github/GitHubPullRequestReviewThreadMessageHandlerIntegrationTest.java @@ -0,0 +1,106 @@ +package de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.github; + +import static org.assertj.core.api.Assertions.assertThat; + +import de.tum.in.www1.hephaestus.gitprovider.common.GitHubPayload; +import de.tum.in.www1.hephaestus.gitprovider.common.GitHubPayloadExtension; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequestRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.github.GitHubPullRequestConverter; +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.pullrequestreviewcomment.github.GitHubPullRequestReviewCommentSyncService; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThreadRepository; +import de.tum.in.www1.hephaestus.gitprovider.pullrequestreviewthread.PullRequestReviewThread; +import de.tum.in.www1.hephaestus.testconfig.BaseIntegrationTest; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHEventPayload; +import org.kohsuke.github.GHEventPayloadPullRequestReviewThread; +import org.springframework.beans.factory.annotation.Autowired; + +@DisplayName("GitHub Pull Request Review Thread Message Handler") +@ExtendWith(GitHubPayloadExtension.class) +class GitHubPullRequestReviewThreadMessageHandlerIntegrationTest extends BaseIntegrationTest { + + @Autowired + private GitHubPullRequestReviewThreadMessageHandler handler; + + @Autowired + private GitHubPullRequestReviewCommentSyncService commentSyncService; + + @Autowired + private PullRequestReviewThreadRepository threadRepository; + + @Autowired + private PullRequestReviewRepository reviewRepository; + + @Autowired + private PullRequestRepository pullRequestRepository; + + @Autowired + private GitHubPullRequestConverter pullRequestConverter; + + @BeforeEach + void setUp() { + databaseTestUtils.cleanDatabase(); + } + + @Test + @DisplayName("should update thread state on resolve and unresolve events") + void threadEventsUpdateState( + @GitHubPayload("pull_request_review_comment.created.thread-1") GHEventPayload.PullRequestReviewComment rootComment, + @GitHubPayload("pull_request_review_comment.created.thread-2") GHEventPayload.PullRequestReviewComment replyComment, + @GitHubPayload("pull_request_review_thread.resolved") GHEventPayloadPullRequestReviewThread resolvedPayload, + @GitHubPayload("pull_request_review_thread.unresolved") GHEventPayloadPullRequestReviewThread unresolvedPayload + ) throws Exception { + // Arrange - ensure prerequisite review data and comments are stored + ensureReviewExists(rootComment); + commentSyncService.processPullRequestReviewComment(rootComment.getComment(), rootComment.getPullRequest()); + ensureReviewExists(replyComment); + commentSyncService.processPullRequestReviewComment(replyComment.getComment(), replyComment.getPullRequest()); + + var threadId = rootComment.getComment().getId(); + var initialThread = threadRepository.findById(threadId).orElseThrow(); + assertThat(initialThread.getState()).isEqualTo(PullRequestReviewThread.State.UNRESOLVED); + + // Act - resolve event + handler.handleEvent(resolvedPayload); + + // Assert resolved state + var resolvedThread = threadRepository.findById(threadId).orElseThrow(); + assertThat(resolvedThread.getState()).isEqualTo(PullRequestReviewThread.State.RESOLVED); + assertThat(resolvedThread.getResolvedAt()).isNotNull(); + + // Act - unresolved event + handler.handleEvent(unresolvedPayload); + + // Assert unresolved state + var unresolvedThread = threadRepository.findById(threadId).orElseThrow(); + assertThat(unresolvedThread.getState()).isEqualTo(PullRequestReviewThread.State.UNRESOLVED); + assertThat(unresolvedThread.getResolvedAt()).isNull(); + } + + private void ensureReviewExists(GHEventPayload.PullRequestReviewComment payload) { + var reviewId = payload.getComment().getPullRequestReviewId(); + if (reviewId == null || reviewRepository.findById(reviewId).isPresent()) { + return; + } + + var review = new PullRequestReview(); + review.setId(reviewId); + review.setState(PullRequestReview.State.COMMENTED); + review.setHtmlUrl(payload.getComment().getHtmlUrl().toString()); + review.setSubmittedAt(Instant.now()); + var pullRequest = payload.getPullRequest(); + if (pullRequest != null) { + var pullRequestEntity = pullRequestRepository + .findById(pullRequest.getId()) + .orElseGet(() -> pullRequestRepository.save(pullRequestConverter.convert(pullRequest))); + review.setPullRequest(pullRequestEntity); + } + reviewRepository.save(review); + } +} diff --git a/server/application-server/src/test/resources/github/pull_request_review_comment.deleted.thread-2.json b/server/application-server/src/test/resources/github/pull_request_review_comment.deleted.thread-2.json new file mode 100644 index 000000000..07e1aebba --- /dev/null +++ b/server/application-server/src/test/resources/github/pull_request_review_comment.deleted.thread-2.json @@ -0,0 +1,571 @@ +{ + "action": "deleted", + "comment": { + "url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/comments/2471132755", + "pull_request_review_id": 3390927684, + "id": 2471132755, + "node_id": "PRRC_kwDOO4CKW86TSn5T", + "diff_hunk": "@@ -1,2 +1,4 @@\n # TestRepository\n This is a test repository\n+\n+Hello world!", + "path": "README.md", + "commit_id": "c1ba4e4e037b7774e30355003415c6a07330726a", + "original_commit_id": "c1ba4e4e037b7774e30355003415c6a07330726a", + "user": { + "login": "FelixTJDietrich", + "id": 5898705, + "node_id": "MDQ6VXNlcjU4OTg3MDU=", + "avatar_url": "https://avatars.githubusercontent.com/u/5898705?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/FelixTJDietrich", + "html_url": "https://github.com/FelixTJDietrich", + "followers_url": "https://api.github.com/users/FelixTJDietrich/followers", + "following_url": "https://api.github.com/users/FelixTJDietrich/following{/other_user}", + "gists_url": "https://api.github.com/users/FelixTJDietrich/gists{/gist_id}", + "starred_url": "https://api.github.com/users/FelixTJDietrich/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/FelixTJDietrich/subscriptions", + "organizations_url": "https://api.github.com/users/FelixTJDietrich/orgs", + "repos_url": "https://api.github.com/users/FelixTJDietrich/repos", + "events_url": "https://api.github.com/users/FelixTJDietrich/events{/privacy}", + "received_events_url": "https://api.github.com/users/FelixTJDietrich/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "body": "Nice comment!", + "created_at": "2025-10-28T21:40:57Z", + "updated_at": "2025-10-28T21:40:57Z", + "html_url": "https://github.com/HephaestusTest/TestRepository/pull/1#discussion_r2471132755", + "pull_request_url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/1", + "_links": { + "self": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/comments/2471132755" + }, + "html": { + "href": "https://github.com/HephaestusTest/TestRepository/pull/1#discussion_r2471132755" + }, + "pull_request": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/1" + } + }, + "reactions": { + "url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/comments/2471132755/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "start_line": null, + "original_start_line": null, + "start_side": null, + "line": 4, + "original_line": 4, + "side": "RIGHT", + "in_reply_to_id": 2471131704, + "author_association": "CONTRIBUTOR", + "original_position": 4, + "position": 4, + "subject_type": "line" + }, + "pull_request": { + "url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/1", + "id": 2956850041, + "node_id": "PR_kwDOO4CKW86wPfN5", + "html_url": "https://github.com/HephaestusTest/TestRepository/pull/1", + "diff_url": "https://github.com/HephaestusTest/TestRepository/pull/1.diff", + "patch_url": "https://github.com/HephaestusTest/TestRepository/pull/1.patch", + "issue_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/1", + "number": 1, + "state": "open", + "locked": false, + "title": "Add greeting message to README", + "user": { + "login": "FelixTJDietrich", + "id": 5898705, + "node_id": "MDQ6VXNlcjU4OTg3MDU=", + "avatar_url": "https://avatars.githubusercontent.com/u/5898705?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/FelixTJDietrich", + "html_url": "https://github.com/FelixTJDietrich", + "followers_url": "https://api.github.com/users/FelixTJDietrich/followers", + "following_url": "https://api.github.com/users/FelixTJDietrich/following{/other_user}", + "gists_url": "https://api.github.com/users/FelixTJDietrich/gists{/gist_id}", + "starred_url": "https://api.github.com/users/FelixTJDietrich/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/FelixTJDietrich/subscriptions", + "organizations_url": "https://api.github.com/users/FelixTJDietrich/orgs", + "repos_url": "https://api.github.com/users/FelixTJDietrich/repos", + "events_url": "https://api.github.com/users/FelixTJDietrich/events{/privacy}", + "received_events_url": "https://api.github.com/users/FelixTJDietrich/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "body": "This pull request makes a minor update to the `README.md` file by adding a greeting message.", + "created_at": "2025-10-28T21:34:33Z", + "updated_at": "2025-10-28T21:40:57Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": "4582cb8614de1f965f1a5abd40cc128fe05f53af", + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/1/commits", + "review_comments_url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/1/comments", + "review_comment_url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/1/comments", + "statuses_url": "https://api.github.com/repos/HephaestusTest/TestRepository/statuses/c1ba4e4e037b7774e30355003415c6a07330726a", + "head": { + "label": "HephaestusTest:FelixTJDietrich-patch-1", + "ref": "FelixTJDietrich-patch-1", + "sha": "c1ba4e4e037b7774e30355003415c6a07330726a", + "user": { + "login": "HephaestusTest", + "id": 215361191, + "node_id": "O_kgDODNYmpw", + "avatar_url": "https://avatars.githubusercontent.com/u/215361191?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/HephaestusTest", + "html_url": "https://github.com/HephaestusTest", + "followers_url": "https://api.github.com/users/HephaestusTest/followers", + "following_url": "https://api.github.com/users/HephaestusTest/following{/other_user}", + "gists_url": "https://api.github.com/users/HephaestusTest/gists{/gist_id}", + "starred_url": "https://api.github.com/users/HephaestusTest/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/HephaestusTest/subscriptions", + "organizations_url": "https://api.github.com/users/HephaestusTest/orgs", + "repos_url": "https://api.github.com/users/HephaestusTest/repos", + "events_url": "https://api.github.com/users/HephaestusTest/events{/privacy}", + "received_events_url": "https://api.github.com/users/HephaestusTest/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 998279771, + "node_id": "R_kgDOO4CKWw", + "name": "TestRepository", + "full_name": "HephaestusTest/TestRepository", + "private": false, + "owner": { + "login": "HephaestusTest", + "id": 215361191, + "node_id": "O_kgDODNYmpw", + "avatar_url": "https://avatars.githubusercontent.com/u/215361191?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/HephaestusTest", + "html_url": "https://github.com/HephaestusTest", + "followers_url": "https://api.github.com/users/HephaestusTest/followers", + "following_url": "https://api.github.com/users/HephaestusTest/following{/other_user}", + "gists_url": "https://api.github.com/users/HephaestusTest/gists{/gist_id}", + "starred_url": "https://api.github.com/users/HephaestusTest/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/HephaestusTest/subscriptions", + "organizations_url": "https://api.github.com/users/HephaestusTest/orgs", + "repos_url": "https://api.github.com/users/HephaestusTest/repos", + "events_url": "https://api.github.com/users/HephaestusTest/events{/privacy}", + "received_events_url": "https://api.github.com/users/HephaestusTest/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/HephaestusTest/TestRepository", + "description": "This is a test repository", + "fork": false, + "url": "https://api.github.com/repos/HephaestusTest/TestRepository", + "forks_url": "https://api.github.com/repos/HephaestusTest/TestRepository/forks", + "keys_url": "https://api.github.com/repos/HephaestusTest/TestRepository/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/HephaestusTest/TestRepository/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/HephaestusTest/TestRepository/teams", + "hooks_url": "https://api.github.com/repos/HephaestusTest/TestRepository/hooks", + "issue_events_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/events{/number}", + "events_url": "https://api.github.com/repos/HephaestusTest/TestRepository/events", + "assignees_url": "https://api.github.com/repos/HephaestusTest/TestRepository/assignees{/user}", + "branches_url": "https://api.github.com/repos/HephaestusTest/TestRepository/branches{/branch}", + "tags_url": "https://api.github.com/repos/HephaestusTest/TestRepository/tags", + "blobs_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/HephaestusTest/TestRepository/statuses/{sha}", + "languages_url": "https://api.github.com/repos/HephaestusTest/TestRepository/languages", + "stargazers_url": "https://api.github.com/repos/HephaestusTest/TestRepository/stargazers", + "contributors_url": "https://api.github.com/repos/HephaestusTest/TestRepository/contributors", + "subscribers_url": "https://api.github.com/repos/HephaestusTest/TestRepository/subscribers", + "subscription_url": "https://api.github.com/repos/HephaestusTest/TestRepository/subscription", + "commits_url": "https://api.github.com/repos/HephaestusTest/TestRepository/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/HephaestusTest/TestRepository/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/HephaestusTest/TestRepository/contents/{+path}", + "compare_url": "https://api.github.com/repos/HephaestusTest/TestRepository/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/HephaestusTest/TestRepository/merges", + "archive_url": "https://api.github.com/repos/HephaestusTest/TestRepository/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/HephaestusTest/TestRepository/downloads", + "issues_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues{/number}", + "pulls_url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls{/number}", + "milestones_url": "https://api.github.com/repos/HephaestusTest/TestRepository/milestones{/number}", + "notifications_url": "https://api.github.com/repos/HephaestusTest/TestRepository/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/HephaestusTest/TestRepository/labels{/name}", + "releases_url": "https://api.github.com/repos/HephaestusTest/TestRepository/releases{/id}", + "deployments_url": "https://api.github.com/repos/HephaestusTest/TestRepository/deployments", + "created_at": "2025-06-08T09:09:18Z", + "updated_at": "2025-07-29T11:38:08Z", + "pushed_at": "2025-10-28T21:34:16Z", + "git_url": "git://github.com/HephaestusTest/TestRepository.git", + "ssh_url": "git@github.com:HephaestusTest/TestRepository.git", + "clone_url": "https://github.com/HephaestusTest/TestRepository.git", + "svn_url": "https://github.com/HephaestusTest/TestRepository", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 1, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 1, + "watchers": 0, + "default_branch": "main", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": false, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE" + } + }, + "base": { + "label": "HephaestusTest:main", + "ref": "main", + "sha": "a76020c0f7e2afac4475770bb83cf4fe06ab5da1", + "user": { + "login": "HephaestusTest", + "id": 215361191, + "node_id": "O_kgDODNYmpw", + "avatar_url": "https://avatars.githubusercontent.com/u/215361191?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/HephaestusTest", + "html_url": "https://github.com/HephaestusTest", + "followers_url": "https://api.github.com/users/HephaestusTest/followers", + "following_url": "https://api.github.com/users/HephaestusTest/following{/other_user}", + "gists_url": "https://api.github.com/users/HephaestusTest/gists{/gist_id}", + "starred_url": "https://api.github.com/users/HephaestusTest/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/HephaestusTest/subscriptions", + "organizations_url": "https://api.github.com/users/HephaestusTest/orgs", + "repos_url": "https://api.github.com/users/HephaestusTest/repos", + "events_url": "https://api.github.com/users/HephaestusTest/events{/privacy}", + "received_events_url": "https://api.github.com/users/HephaestusTest/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 998279771, + "node_id": "R_kgDOO4CKWw", + "name": "TestRepository", + "full_name": "HephaestusTest/TestRepository", + "private": false, + "owner": { + "login": "HephaestusTest", + "id": 215361191, + "node_id": "O_kgDODNYmpw", + "avatar_url": "https://avatars.githubusercontent.com/u/215361191?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/HephaestusTest", + "html_url": "https://github.com/HephaestusTest", + "followers_url": "https://api.github.com/users/HephaestusTest/followers", + "following_url": "https://api.github.com/users/HephaestusTest/following{/other_user}", + "gists_url": "https://api.github.com/users/HephaestusTest/gists{/gist_id}", + "starred_url": "https://api.github.com/users/HephaestusTest/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/HephaestusTest/subscriptions", + "organizations_url": "https://api.github.com/users/HephaestusTest/orgs", + "repos_url": "https://api.github.com/users/HephaestusTest/repos", + "events_url": "https://api.github.com/users/HephaestusTest/events{/privacy}", + "received_events_url": "https://api.github.com/users/HephaestusTest/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/HephaestusTest/TestRepository", + "description": "This is a test repository", + "fork": false, + "url": "https://api.github.com/repos/HephaestusTest/TestRepository", + "forks_url": "https://api.github.com/repos/HephaestusTest/TestRepository/forks", + "keys_url": "https://api.github.com/repos/HephaestusTest/TestRepository/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/HephaestusTest/TestRepository/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/HephaestusTest/TestRepository/teams", + "hooks_url": "https://api.github.com/repos/HephaestusTest/TestRepository/hooks", + "issue_events_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/events{/number}", + "events_url": "https://api.github.com/repos/HephaestusTest/TestRepository/events", + "assignees_url": "https://api.github.com/repos/HephaestusTest/TestRepository/assignees{/user}", + "branches_url": "https://api.github.com/repos/HephaestusTest/TestRepository/branches{/branch}", + "tags_url": "https://api.github.com/repos/HephaestusTest/TestRepository/tags", + "blobs_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/HephaestusTest/TestRepository/statuses/{sha}", + "languages_url": "https://api.github.com/repos/HephaestusTest/TestRepository/languages", + "stargazers_url": "https://api.github.com/repos/HephaestusTest/TestRepository/stargazers", + "contributors_url": "https://api.github.com/repos/HephaestusTest/TestRepository/contributors", + "subscribers_url": "https://api.github.com/repos/HephaestusTest/TestRepository/subscribers", + "subscription_url": "https://api.github.com/repos/HephaestusTest/TestRepository/subscription", + "commits_url": "https://api.github.com/repos/HephaestusTest/TestRepository/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/HephaestusTest/TestRepository/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/HephaestusTest/TestRepository/contents/{+path}", + "compare_url": "https://api.github.com/repos/HephaestusTest/TestRepository/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/HephaestusTest/TestRepository/merges", + "archive_url": "https://api.github.com/repos/HephaestusTest/TestRepository/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/HephaestusTest/TestRepository/downloads", + "issues_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues{/number}", + "pulls_url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls{/number}", + "milestones_url": "https://api.github.com/repos/HephaestusTest/TestRepository/milestones{/number}", + "notifications_url": "https://api.github.com/repos/HephaestusTest/TestRepository/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/HephaestusTest/TestRepository/labels{/name}", + "releases_url": "https://api.github.com/repos/HephaestusTest/TestRepository/releases{/id}", + "deployments_url": "https://api.github.com/repos/HephaestusTest/TestRepository/deployments", + "created_at": "2025-06-08T09:09:18Z", + "updated_at": "2025-07-29T11:38:08Z", + "pushed_at": "2025-10-28T21:34:16Z", + "git_url": "git://github.com/HephaestusTest/TestRepository.git", + "ssh_url": "git@github.com:HephaestusTest/TestRepository.git", + "clone_url": "https://github.com/HephaestusTest/TestRepository.git", + "svn_url": "https://github.com/HephaestusTest/TestRepository", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 1, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 1, + "watchers": 0, + "default_branch": "main", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": false, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/1" + }, + "html": { + "href": "https://github.com/HephaestusTest/TestRepository/pull/1" + }, + "issue": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/1" + }, + "comments": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/1/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/1/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls/1/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/HephaestusTest/TestRepository/statuses/c1ba4e4e037b7774e30355003415c6a07330726a" + } + }, + "author_association": "CONTRIBUTOR", + "auto_merge": null, + "active_lock_reason": null + }, + "repository": { + "id": 998279771, + "node_id": "R_kgDOO4CKWw", + "name": "TestRepository", + "full_name": "HephaestusTest/TestRepository", + "private": false, + "owner": { + "login": "HephaestusTest", + "id": 215361191, + "node_id": "O_kgDODNYmpw", + "avatar_url": "https://avatars.githubusercontent.com/u/215361191?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/HephaestusTest", + "html_url": "https://github.com/HephaestusTest", + "followers_url": "https://api.github.com/users/HephaestusTest/followers", + "following_url": "https://api.github.com/users/HephaestusTest/following{/other_user}", + "gists_url": "https://api.github.com/users/HephaestusTest/gists{/gist_id}", + "starred_url": "https://api.github.com/users/HephaestusTest/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/HephaestusTest/subscriptions", + "organizations_url": "https://api.github.com/users/HephaestusTest/orgs", + "repos_url": "https://api.github.com/users/HephaestusTest/repos", + "events_url": "https://api.github.com/users/HephaestusTest/events{/privacy}", + "received_events_url": "https://api.github.com/users/HephaestusTest/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/HephaestusTest/TestRepository", + "description": "This is a test repository", + "fork": false, + "url": "https://api.github.com/repos/HephaestusTest/TestRepository", + "forks_url": "https://api.github.com/repos/HephaestusTest/TestRepository/forks", + "keys_url": "https://api.github.com/repos/HephaestusTest/TestRepository/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/HephaestusTest/TestRepository/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/HephaestusTest/TestRepository/teams", + "hooks_url": "https://api.github.com/repos/HephaestusTest/TestRepository/hooks", + "issue_events_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/events{/number}", + "events_url": "https://api.github.com/repos/HephaestusTest/TestRepository/events", + "assignees_url": "https://api.github.com/repos/HephaestusTest/TestRepository/assignees{/user}", + "branches_url": "https://api.github.com/repos/HephaestusTest/TestRepository/branches{/branch}", + "tags_url": "https://api.github.com/repos/HephaestusTest/TestRepository/tags", + "blobs_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/HephaestusTest/TestRepository/statuses/{sha}", + "languages_url": "https://api.github.com/repos/HephaestusTest/TestRepository/languages", + "stargazers_url": "https://api.github.com/repos/HephaestusTest/TestRepository/stargazers", + "contributors_url": "https://api.github.com/repos/HephaestusTest/TestRepository/contributors", + "subscribers_url": "https://api.github.com/repos/HephaestusTest/TestRepository/subscribers", + "subscription_url": "https://api.github.com/repos/HephaestusTest/TestRepository/subscription", + "commits_url": "https://api.github.com/repos/HephaestusTest/TestRepository/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/HephaestusTest/TestRepository/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/HephaestusTest/TestRepository/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/HephaestusTest/TestRepository/contents/{+path}", + "compare_url": "https://api.github.com/repos/HephaestusTest/TestRepository/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/HephaestusTest/TestRepository/merges", + "archive_url": "https://api.github.com/repos/HephaestusTest/TestRepository/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/HephaestusTest/TestRepository/downloads", + "issues_url": "https://api.github.com/repos/HephaestusTest/TestRepository/issues{/number}", + "pulls_url": "https://api.github.com/repos/HephaestusTest/TestRepository/pulls{/number}", + "milestones_url": "https://api.github.com/repos/HephaestusTest/TestRepository/milestones{/number}", + "notifications_url": "https://api.github.com/repos/HephaestusTest/TestRepository/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/HephaestusTest/TestRepository/labels{/name}", + "releases_url": "https://api.github.com/repos/HephaestusTest/TestRepository/releases{/id}", + "deployments_url": "https://api.github.com/repos/HephaestusTest/TestRepository/deployments", + "created_at": "2025-06-08T09:09:18Z", + "updated_at": "2025-07-29T11:38:08Z", + "pushed_at": "2025-10-28T21:34:16Z", + "git_url": "git://github.com/HephaestusTest/TestRepository.git", + "ssh_url": "git@github.com:HephaestusTest/TestRepository.git", + "clone_url": "https://github.com/HephaestusTest/TestRepository.git", + "svn_url": "https://github.com/HephaestusTest/TestRepository", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 1, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 1, + "watchers": 0, + "default_branch": "main", + "custom_properties": {} + }, + "organization": { + "login": "HephaestusTest", + "id": 215361191, + "node_id": "O_kgDODNYmpw", + "url": "https://api.github.com/orgs/HephaestusTest", + "repos_url": "https://api.github.com/orgs/HephaestusTest/repos", + "events_url": "https://api.github.com/orgs/HephaestusTest/events", + "hooks_url": "https://api.github.com/orgs/HephaestusTest/hooks", + "issues_url": "https://api.github.com/orgs/HephaestusTest/issues", + "members_url": "https://api.github.com/orgs/HephaestusTest/members{/member}", + "public_members_url": "https://api.github.com/orgs/HephaestusTest/public_members{/member}", + "avatar_url": "https://avatars.githubusercontent.com/u/215361191?v=4", + "description": null + }, + "sender": { + "login": "FelixTJDietrich", + "id": 5898705, + "node_id": "MDQ6VXNlcjU4OTg3MDU=", + "avatar_url": "https://avatars.githubusercontent.com/u/5898705?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/FelixTJDietrich", + "html_url": "https://github.com/FelixTJDietrich", + "followers_url": "https://api.github.com/users/FelixTJDietrich/followers", + "following_url": "https://api.github.com/users/FelixTJDietrich/following{/other_user}", + "gists_url": "https://api.github.com/users/FelixTJDietrich/gists{/gist_id}", + "starred_url": "https://api.github.com/users/FelixTJDietrich/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/FelixTJDietrich/subscriptions", + "organizations_url": "https://api.github.com/users/FelixTJDietrich/orgs", + "repos_url": "https://api.github.com/users/FelixTJDietrich/repos", + "events_url": "https://api.github.com/users/FelixTJDietrich/events{/privacy}", + "received_events_url": "https://api.github.com/users/FelixTJDietrich/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "installation": { + "id": 85578532, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uODU1Nzg1MzI=" + } +} diff --git a/server/intelligence-service/app/db/models_gen.py b/server/intelligence-service/app/db/models_gen.py index 15802ca4a..472376f52 100644 --- a/server/intelligence-service/app/db/models_gen.py +++ b/server/intelligence-service/app/db/models_gen.py @@ -180,6 +180,133 @@ class OrganizationMembership(Base): role: Mapped[Optional[str]] = mapped_column(String(255)) +class PullRequestReviewComment(Base): + __tablename__ = "pull_request_review_comment" + __table_args__ = ( + ForeignKeyConstraint( + ["author_id"], ["user.id"], name="fktl08ieowbl171xem2bciho7kw" + ), + ForeignKeyConstraint( + ["in_reply_to_id"], + ["pull_request_review_comment.id"], + ondelete="SET NULL", + name="fk_pr_review_comment_reply", + ), + ForeignKeyConstraint( + ["pull_request_id"], ["issue.id"], name="fkohqvdiswptbm0h8cniq7r1tgq" + ), + ForeignKeyConstraint( + ["review_id"], + ["pull_request_review.id"], + name="fkbx1g5jpdegymhyv9pbk2jdgfw", + ), + ForeignKeyConstraint( + ["thread_id"], + ["pull_request_review_thread.id"], + ondelete="CASCADE", + name="fk_pr_review_comment_thread", + ), + PrimaryKeyConstraint("id", name="pull_request_review_comment_pkey"), + Index("idx_pull_request_review_comment_thread", "thread_id"), + ) + id: Mapped[int] = mapped_column(BigInteger, primary_key=True) + line: Mapped[int] = mapped_column(Integer) + original_line: Mapped[int] = mapped_column(Integer) + original_position: Mapped[int] = mapped_column(Integer) + position: Mapped[int] = mapped_column(Integer) + thread_id: Mapped[int] = mapped_column(BigInteger) + created_at: Mapped[Optional[datetime.datetime]] = mapped_column(DateTime(True)) + updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(DateTime(True)) + author_association: Mapped[Optional[str]] = mapped_column(String(255)) + body: Mapped[Optional[Any]] = mapped_column(OID) + commit_id: Mapped[Optional[str]] = mapped_column(String(255)) + diff_hunk: Mapped[Optional[Any]] = mapped_column(OID) + html_url: Mapped[Optional[str]] = mapped_column(String(255)) + original_commit_id: Mapped[Optional[str]] = mapped_column(String(255)) + original_start_line: Mapped[Optional[int]] = mapped_column(Integer) + path: Mapped[Optional[str]] = mapped_column(String(255)) + side: Mapped[Optional[str]] = mapped_column(String(255)) + start_line: Mapped[Optional[int]] = mapped_column(Integer) + start_side: Mapped[Optional[str]] = mapped_column(String(255)) + author_id: Mapped[Optional[int]] = mapped_column(BigInteger) + pull_request_id: Mapped[Optional[int]] = mapped_column(BigInteger) + review_id: Mapped[Optional[int]] = mapped_column(BigInteger) + in_reply_to_id: Mapped[Optional[int]] = mapped_column(BigInteger) + author: Mapped[Optional["User"]] = relationship( + "User", back_populates="pull_request_review_comment" + ) + in_reply_to: Mapped[Optional["PullRequestReviewComment"]] = relationship( + "PullRequestReviewComment", + remote_side=[id], + back_populates="in_reply_to_reverse", + ) + in_reply_to_reverse: Mapped[List["PullRequestReviewComment"]] = relationship( + "PullRequestReviewComment", + remote_side=[in_reply_to_id], + back_populates="in_reply_to", + ) + pull_request: Mapped[Optional["Issue"]] = relationship( + "Issue", back_populates="pull_request_review_comment" + ) + review: Mapped[Optional["PullRequestReview"]] = relationship( + "PullRequestReview", back_populates="pull_request_review_comment" + ) + thread: Mapped["PullRequestReviewThread"] = relationship( + "PullRequestReviewThread", + foreign_keys=[thread_id], + back_populates="pull_request_review_comment", + ) + pull_request_review_thread: Mapped[Optional["PullRequestReviewThread"]] = ( + relationship( + "PullRequestReviewThread", + uselist=False, + foreign_keys="[PullRequestReviewThread.root_comment_id]", + back_populates="root_comment", + ) + ) + + +class PullRequestReviewThread(Base): + __tablename__ = "pull_request_review_thread" + __table_args__ = ( + ForeignKeyConstraint( + ["pull_request_id"], ["issue.id"], name="fk_pr_review_thread_pull_request" + ), + ForeignKeyConstraint( + ["root_comment_id"], + ["pull_request_review_comment.id"], + name="fk_pr_review_thread_root_comment", + ), + PrimaryKeyConstraint("id", name="pull_request_review_thread_pkey"), + UniqueConstraint("root_comment_id", name="uq_pr_review_thread_root_comment"), + Index("idx_pull_request_review_thread_pull_request", "pull_request_id"), + ) + id: Mapped[int] = mapped_column(BigInteger, primary_key=True) + state: Mapped[str] = mapped_column( + String(20), server_default=text("'UNRESOLVED'::character varying") + ) + pull_request_id: Mapped[int] = mapped_column(BigInteger) + created_at: Mapped[Optional[datetime.datetime]] = mapped_column(DateTime(True)) + updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(DateTime(True)) + resolved_at: Mapped[Optional[datetime.datetime]] = mapped_column(DateTime(True)) + root_comment_id: Mapped[Optional[int]] = mapped_column(BigInteger) + pull_request_review_comment: Mapped[List["PullRequestReviewComment"]] = ( + relationship( + "PullRequestReviewComment", + foreign_keys="[PullRequestReviewComment.thread_id]", + back_populates="thread", + ) + ) + pull_request: Mapped["Issue"] = relationship( + "Issue", back_populates="pull_request_review_thread" + ) + root_comment: Mapped[Optional["PullRequestReviewComment"]] = relationship( + "PullRequestReviewComment", + foreign_keys=[root_comment_id], + back_populates="pull_request_review_thread", + ) + + class Team(Base): __tablename__ = "team" __table_args__ = (PrimaryKeyConstraint("id", name="teamPK"),) @@ -243,6 +370,9 @@ class User(Base): chat_thread: Mapped[List["ChatThread"]] = relationship( "ChatThread", back_populates="user" ) + pull_request_review_comment: Mapped[List["PullRequestReviewComment"]] = ( + relationship("PullRequestReviewComment", back_populates="author") + ) document: Mapped[List["Document"]] = relationship("Document", back_populates="user") team_membership: Mapped[List["TeamMembership"]] = relationship( "TeamMembership", back_populates="user" @@ -268,9 +398,6 @@ class User(Base): pull_request_review: Mapped[List["PullRequestReview"]] = relationship( "PullRequestReview", back_populates="author" ) - pull_request_review_comment: Mapped[List["PullRequestReviewComment"]] = ( - relationship("PullRequestReviewComment", back_populates="author") - ) class ChatMessagePart(Base): @@ -584,6 +711,12 @@ class Issue(Base): last_detection_time: Mapped[Optional[datetime.datetime]] = mapped_column( TIMESTAMP(True, 6) ) + pull_request_review_comment: Mapped[List["PullRequestReviewComment"]] = ( + relationship("PullRequestReviewComment", back_populates="pull_request") + ) + pull_request_review_thread: Mapped[List["PullRequestReviewThread"]] = relationship( + "PullRequestReviewThread", back_populates="pull_request" + ) author: Mapped[Optional["User"]] = relationship( "User", foreign_keys=[author_id], back_populates="issue" ) @@ -616,9 +749,6 @@ class Issue(Base): pull_request_review: Mapped[List["PullRequestReview"]] = relationship( "PullRequestReview", back_populates="pull_request" ) - pull_request_review_comment: Mapped[List["PullRequestReviewComment"]] = ( - relationship("PullRequestReviewComment", back_populates="pull_request") - ) pullrequestbadpractice: Mapped[List["Pullrequestbadpractice"]] = relationship( "Pullrequestbadpractice", back_populates="pullrequest" ) @@ -757,62 +887,14 @@ class PullRequestReview(Base): submitted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DateTime(True)) author_id: Mapped[Optional[int]] = mapped_column(BigInteger) pull_request_id: Mapped[Optional[int]] = mapped_column(BigInteger) - author: Mapped[Optional["User"]] = relationship( - "User", back_populates="pull_request_review" - ) - pull_request: Mapped[Optional["Issue"]] = relationship( - "Issue", back_populates="pull_request_review" - ) pull_request_review_comment: Mapped[List["PullRequestReviewComment"]] = ( relationship("PullRequestReviewComment", back_populates="review") ) - - -class PullRequestReviewComment(Base): - __tablename__ = "pull_request_review_comment" - __table_args__ = ( - ForeignKeyConstraint( - ["author_id"], ["user.id"], name="fktl08ieowbl171xem2bciho7kw" - ), - ForeignKeyConstraint( - ["pull_request_id"], ["issue.id"], name="fkohqvdiswptbm0h8cniq7r1tgq" - ), - ForeignKeyConstraint( - ["review_id"], - ["pull_request_review.id"], - name="fkbx1g5jpdegymhyv9pbk2jdgfw", - ), - PrimaryKeyConstraint("id", name="pull_request_review_comment_pkey"), - ) - id: Mapped[int] = mapped_column(BigInteger, primary_key=True) - line: Mapped[int] = mapped_column(Integer) - original_line: Mapped[int] = mapped_column(Integer) - original_position: Mapped[int] = mapped_column(Integer) - position: Mapped[int] = mapped_column(Integer) - created_at: Mapped[Optional[datetime.datetime]] = mapped_column(DateTime(True)) - updated_at: Mapped[Optional[datetime.datetime]] = mapped_column(DateTime(True)) - author_association: Mapped[Optional[str]] = mapped_column(String(255)) - body: Mapped[Optional[Any]] = mapped_column(OID) - commit_id: Mapped[Optional[str]] = mapped_column(String(255)) - diff_hunk: Mapped[Optional[Any]] = mapped_column(OID) - html_url: Mapped[Optional[str]] = mapped_column(String(255)) - original_commit_id: Mapped[Optional[str]] = mapped_column(String(255)) - original_start_line: Mapped[Optional[int]] = mapped_column(Integer) - path: Mapped[Optional[str]] = mapped_column(String(255)) - side: Mapped[Optional[str]] = mapped_column(String(255)) - start_line: Mapped[Optional[int]] = mapped_column(Integer) - start_side: Mapped[Optional[str]] = mapped_column(String(255)) - author_id: Mapped[Optional[int]] = mapped_column(BigInteger) - pull_request_id: Mapped[Optional[int]] = mapped_column(BigInteger) - review_id: Mapped[Optional[int]] = mapped_column(BigInteger) author: Mapped[Optional["User"]] = relationship( - "User", back_populates="pull_request_review_comment" + "User", back_populates="pull_request_review" ) pull_request: Mapped[Optional["Issue"]] = relationship( - "Issue", back_populates="pull_request_review_comment" - ) - review: Mapped[Optional["PullRequestReview"]] = relationship( - "PullRequestReview", back_populates="pull_request_review_comment" + "Issue", back_populates="pull_request_review" )