From f1983f3b51849ad707d5c35ad12b3b9719df91e7 Mon Sep 17 00:00:00 2001 From: Sorena Sarabadani Date: Mon, 26 Jan 2026 23:28:28 +0100 Subject: [PATCH] fix: GHPullRequestReviewComment.getLine() returning -1 --- .../org/kohsuke/github/GHPullRequest.java | 10 +++ .../kohsuke/github/GHPullRequestReview.java | 12 ++++ .../github/GHPullRequestReviewComment.java | 68 +++++++++++++++--- .../org/kohsuke/github/GHPullRequestTest.java | 9 +++ .../pullRequestReviews/__files/12-user.json | 36 ++++++++++ .../13-r_h_g_pulls_comments_1641771497.json | 69 +++++++++++++++++++ .../pullRequestReviews/mappings/12-user.json | 48 +++++++++++++ .../13-r_h_g_pulls_comments_1641771497.json | 48 +++++++++++++ 8 files changed, 292 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/__files/12-user.json create mode 100644 src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/__files/13-r_h_g_pulls_comments_1641771497.json create mode 100644 src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/mappings/12-user.json create mode 100644 src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/mappings/13-r_h_g_pulls_comments_1641771497.json diff --git a/src/main/java/org/kohsuke/github/GHPullRequest.java b/src/main/java/org/kohsuke/github/GHPullRequest.java index 6062102c59..3b6353a8be 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequest.java +++ b/src/main/java/org/kohsuke/github/GHPullRequest.java @@ -520,7 +520,17 @@ public PagedIterable listFiles() { /** * Obtains all the review comments associated with this pull request. * + *

+ * This method uses the pull request comments endpoint which returns complete comment data including + * {@link GHPullRequestReviewComment#getLine() line}, {@link GHPullRequestReviewComment#getOriginalLine() + * originalLine}, {@link GHPullRequestReviewComment#getSide() side}, and other position-related fields. + * + *

+ * If you need line number information, prefer this method over {@link GHPullRequestReview#listReviewComments()}, + * which uses a different API endpoint that does not return line-related fields. + * * @return the paged iterable + * @see GHPullRequestReview#listReviewComments() */ public PagedIterable listReviewComments() { return root().createRequest() diff --git a/src/main/java/org/kohsuke/github/GHPullRequestReview.java b/src/main/java/org/kohsuke/github/GHPullRequestReview.java index 10eb93ba4e..4601b6745c 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequestReview.java +++ b/src/main/java/org/kohsuke/github/GHPullRequestReview.java @@ -174,7 +174,19 @@ public GHUser getUser() throws IOException { /** * Obtains all the review comments associated with this pull request review. * + *

+ * Note: The GitHub API endpoint used by this method does not return line-related fields such as + * {@link GHPullRequestReviewComment#getLine() line}, {@link GHPullRequestReviewComment#getOriginalLine() + * originalLine}, {@link GHPullRequestReviewComment#getSide() side}, + * {@link GHPullRequestReviewComment#getStartLine() startLine}, etc. These fields will return their default values + * (-1 or UNKNOWN). + * + *

+ * If you need line number information, use {@link GHPullRequest#listReviewComments()} instead and filter by + * {@link GHPullRequestReviewComment#getPullRequestReviewId()} if needed. + * * @return the paged iterable + * @see GHPullRequest#listReviewComments() */ public PagedIterable listReviewComments() { return owner.root() diff --git a/src/main/java/org/kohsuke/github/GHPullRequestReviewComment.java b/src/main/java/org/kohsuke/github/GHPullRequestReviewComment.java index a21378ef86..d5ea0613d0 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequestReviewComment.java +++ b/src/main/java/org/kohsuke/github/GHPullRequestReviewComment.java @@ -40,7 +40,7 @@ * @see GHPullRequest#createReviewComment(String, String, String, int) GHPullRequest#createReviewComment(String, String, * String, int) */ -public class GHPullRequestReviewComment extends GHObject implements Reactable { +public class GHPullRequestReviewComment extends GHObject implements Reactable, Refreshable { /** * The side of the diff to which the comment applies @@ -218,7 +218,13 @@ public long getInReplyToId() { /** * Gets The line of the blob to which the comment applies. The last line of the range for a multi-line comment. * - * @return the line to which the comment applies + *

+ * Note: This field is only populated when comments are retrieved via + * {@link GHPullRequest#listReviewComments()}. When retrieved via {@link GHPullRequestReview#listReviewComments()}, + * this method returns -1 due to limitations in the GitHub API. + * + * @return the line to which the comment applies, or -1 if not available + * @see GHPullRequest#listReviewComments() */ public int getLine() { return line; @@ -236,7 +242,13 @@ public String getOriginalCommitId() { /** * Gets The line of the blob to which the comment applies. The last line of the range for a multi-line comment. * - * @return the line to which the comment applies + *

+ * Note: This field is only populated when comments are retrieved via + * {@link GHPullRequest#listReviewComments()}. When retrieved via {@link GHPullRequestReview#listReviewComments()}, + * this method returns -1 due to limitations in the GitHub API. + * + * @return the line to which the comment applies, or -1 if not available + * @see GHPullRequest#listReviewComments() */ public int getOriginalLine() { return originalLine; @@ -254,7 +266,13 @@ public int getOriginalPosition() { /** * Gets The first line of the range for a multi-line comment. * - * @return the original start line + *

+ * Note: This field is only populated when comments are retrieved via + * {@link GHPullRequest#listReviewComments()}. When retrieved via {@link GHPullRequestReview#listReviewComments()}, + * this method returns -1 due to limitations in the GitHub API. + * + * @return the original start line, or -1 if not available or not a multi-line comment + * @see GHPullRequest#listReviewComments() */ public int getOriginalStartLine() { return originalStartLine != null ? originalStartLine : -1; @@ -318,9 +336,15 @@ public GHPullRequestReviewCommentReactions getReactions() { /** * Gets The side of the diff to which the comment applies. The side of the last line of the range for a multi-line - * comment + * comment. + * + *

+ * Note: This field is only populated when comments are retrieved via + * {@link GHPullRequest#listReviewComments()}. When retrieved via {@link GHPullRequestReview#listReviewComments()}, + * this method returns {@link Side#UNKNOWN} due to limitations in the GitHub API. * - * @return {@link Side} the side if the diff to which the comment applies + * @return {@link Side} the side of the diff to which the comment applies, or {@link Side#UNKNOWN} if not available + * @see GHPullRequest#listReviewComments() */ public Side getSide() { return Side.from(side); @@ -329,7 +353,13 @@ public Side getSide() { /** * Gets The first line of the range for a multi-line comment. * - * @return the start line + *

+ * Note: This field is only populated when comments are retrieved via + * {@link GHPullRequest#listReviewComments()}. When retrieved via {@link GHPullRequestReview#listReviewComments()}, + * this method returns -1 due to limitations in the GitHub API. + * + * @return the start line, or -1 if not available or not a multi-line comment + * @see GHPullRequest#listReviewComments() */ public int getStartLine() { return startLine != null ? startLine : -1; @@ -338,7 +368,13 @@ public int getStartLine() { /** * Gets The side of the first line of the range for a multi-line comment. * - * @return {@link Side} the side of the first line + *

+ * Note: This field is only populated when comments are retrieved via + * {@link GHPullRequest#listReviewComments()}. When retrieved via {@link GHPullRequestReview#listReviewComments()}, + * this method returns {@link Side#UNKNOWN} due to limitations in the GitHub API. + * + * @return {@link Side} the side of the first line, or {@link Side#UNKNOWN} if not available + * @see GHPullRequest#listReviewComments() */ public Side getStartSide() { return Side.from(startSide); @@ -367,6 +403,22 @@ public PagedIterable listReactions() { .toIterable(GHReaction[].class, item -> owner.root()); } + /** + * Refreshes this comment by fetching the full data from the API. + * + *

+ * This is useful when the comment was obtained via {@link GHPullRequestReview#listReviewComments()}, which uses a + * GitHub API endpoint that does not return line-related fields. After calling this method, fields like + * {@link #getLine()}, {@link #getOriginalLine()}, {@link #getSide()}, etc. will return their actual values. + * + * @throws IOException + * if an I/O error occurs + * @see GHPullRequest#listReviewComments() + */ + public void refresh() throws IOException { + owner.root().createRequest().withUrlPath(getApiRoute()).fetchInto(this).wrapUp(owner); + } + /** * Create a new comment that replies to this comment. * diff --git a/src/test/java/org/kohsuke/github/GHPullRequestTest.java b/src/test/java/org/kohsuke/github/GHPullRequestTest.java index 8fec076e08..4fee2e48a3 100644 --- a/src/test/java/org/kohsuke/github/GHPullRequestTest.java +++ b/src/test/java/org/kohsuke/github/GHPullRequestTest.java @@ -701,6 +701,15 @@ public void pullRequestReviews() throws Exception { assertThat(comments.size(), equalTo(3)); GHPullRequestReviewComment comment = comments.get(0); assertThat(comment.getBody(), equalTo("Some niggle")); + + // Verify that line is not available when fetched via review.listReviewComments() + // due to GitHub API limitation (the review comments endpoint doesn't return line field) + assertThat(comment.getLine(), equalTo(-1)); + + // After refresh(), line information should be available + comment.refresh(); + assertThat(comment.getLine(), equalTo(1)); + comment = comments.get(1); assertThat(comment.getBody(), equalTo("A single line comment")); assertThat(comment.getPosition(), equalTo(4)); diff --git a/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/__files/12-user.json b/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/__files/12-user.json new file mode 100644 index 0000000000..fbc5eae788 --- /dev/null +++ b/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/__files/12-user.json @@ -0,0 +1,36 @@ +{ + "login": "Anonycoders", + "id": 40047636, + "node_id": "MDQ6VXNlcjQwMDQ3NjM2", + "avatar_url": "https://avatars.githubusercontent.com/u/40047636?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Anonycoders", + "html_url": "https://github.com/Anonycoders", + "followers_url": "https://api.github.com/users/Anonycoders/followers", + "following_url": "https://api.github.com/users/Anonycoders/following{/other_user}", + "gists_url": "https://api.github.com/users/Anonycoders/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Anonycoders/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Anonycoders/subscriptions", + "organizations_url": "https://api.github.com/users/Anonycoders/orgs", + "repos_url": "https://api.github.com/users/Anonycoders/repos", + "events_url": "https://api.github.com/users/Anonycoders/events{/privacy}", + "received_events_url": "https://api.github.com/users/Anonycoders/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false, + "name": "Sorena Sarabadani", + "company": "@Adevinta", + "blog": "", + "location": "Berlin, Germany", + "email": "sorena.sarabadani@gmail.com", + "hireable": null, + "bio": "Ex-Shopifyer - Adevinta/Kleinanzeigen", + "twitter_username": "sorena_s", + "notification_email": "sorena.sarabadani@gmail.com", + "public_repos": 12, + "public_gists": 0, + "followers": 38, + "following": 4, + "created_at": "2018-06-08T02:07:15Z", + "updated_at": "2026-01-24T22:07:12Z" +} \ No newline at end of file diff --git a/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/__files/13-r_h_g_pulls_comments_1641771497.json b/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/__files/13-r_h_g_pulls_comments_1641771497.json new file mode 100644 index 0000000000..a51974394d --- /dev/null +++ b/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/__files/13-r_h_g_pulls_comments_1641771497.json @@ -0,0 +1,69 @@ +{ + "url": "https://api.github.com/repos/hub4j-test-org/github-api/pulls/comments/1641771497", + "pull_request_review_id": 2121304234, + "id": 1641771497, + "node_id": "PRRC_kwDODFTdCc5h23Hp", + "diff_hunk": "@@ -1,3 +1,4 @@\n-Java API for GitHub", + "path": "README.md", + "commit_id": "07374fe73aff1c2024a8d4114b32406c7a8e89b7", + "original_commit_id": "07374fe73aff1c2024a8d4114b32406c7a8e89b7", + "user": { + "login": "maximevw", + "id": 48218208, + "node_id": "MDQ6VXNlcjQ4MjE4MjA4", + "avatar_url": "https://avatars.githubusercontent.com/u/48218208?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/maximevw", + "html_url": "https://github.com/maximevw", + "followers_url": "https://api.github.com/users/maximevw/followers", + "following_url": "https://api.github.com/users/maximevw/following{/other_user}", + "gists_url": "https://api.github.com/users/maximevw/gists{/gist_id}", + "starred_url": "https://api.github.com/users/maximevw/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/maximevw/subscriptions", + "organizations_url": "https://api.github.com/users/maximevw/orgs", + "repos_url": "https://api.github.com/users/maximevw/repos", + "events_url": "https://api.github.com/users/maximevw/events{/privacy}", + "received_events_url": "https://api.github.com/users/maximevw/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "body": "Some niggle", + "created_at": "2024-06-16T09:55:53Z", + "updated_at": "2024-06-16T09:55:55Z", + "html_url": "https://github.com/hub4j-test-org/github-api/pull/482#discussion_r1641771497", + "pull_request_url": "https://api.github.com/repos/hub4j-test-org/github-api/pulls/482", + "_links": { + "self": { + "href": "https://api.github.com/repos/hub4j-test-org/github-api/pulls/comments/1641771497" + }, + "html": { + "href": "https://github.com/hub4j-test-org/github-api/pull/482#discussion_r1641771497" + }, + "pull_request": { + "href": "https://api.github.com/repos/hub4j-test-org/github-api/pulls/482" + } + }, + "reactions": { + "url": "https://api.github.com/repos/hub4j-test-org/github-api/pulls/comments/1641771497/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": 1, + "original_line": 1, + "side": "LEFT", + "author_association": "MEMBER", + "original_position": 1, + "position": 1, + "subject_type": "line" +} \ No newline at end of file diff --git a/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/mappings/12-user.json b/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/mappings/12-user.json new file mode 100644 index 0000000000..40afaf8cbf --- /dev/null +++ b/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/mappings/12-user.json @@ -0,0 +1,48 @@ +{ + "id": "249d36bd-a54d-4106-a62f-f86c4f834d34", + "name": "user", + "request": { + "url": "/user", + "method": "GET", + "headers": { + "Accept": { + "equalTo": "application/vnd.github+json" + } + } + }, + "response": { + "status": 200, + "bodyFileName": "12-user.json", + "headers": { + "Date": "Mon, 26 Jan 2026 22:10:52 GMT", + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "private, max-age=60, s-maxage=60", + "Vary": "Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With", + "ETag": "W/\"15d7e1ad92a3639b979fc55254902e63ee0bfa5c8f6766990bf989044d491ce1\"", + "Last-Modified": "Sat, 24 Jan 2026 22:07:12 GMT", + "X-OAuth-Scopes": "repo", + "X-Accepted-OAuth-Scopes": "", + "github-authentication-token-expiration": "2026-02-19 19:55:13 UTC", + "X-GitHub-Media-Type": "github.v3; format=json", + "x-github-api-version-selected": "2022-11-28", + "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", + "Access-Control-Allow-Origin": "*", + "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", + "X-Frame-Options": "deny", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "0", + "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin", + "Content-Security-Policy": "default-src 'none'", + "Server": "github.com", + "X-RateLimit-Limit": "5000", + "X-RateLimit-Remaining": "4963", + "X-RateLimit-Reset": "1769467964", + "X-RateLimit-Used": "37", + "X-RateLimit-Resource": "core", + "X-GitHub-Request-Id": "D98D:30C18B:44C18DC:3CA8C0F:6977E66C" + } + }, + "uuid": "249d36bd-a54d-4106-a62f-f86c4f834d34", + "persistent": true, + "insertionIndex": 12 +} \ No newline at end of file diff --git a/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/mappings/13-r_h_g_pulls_comments_1641771497.json b/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/mappings/13-r_h_g_pulls_comments_1641771497.json new file mode 100644 index 0000000000..6f44c6308a --- /dev/null +++ b/src/test/resources/org/kohsuke/github/GHPullRequestTest/wiremock/pullRequestReviews/mappings/13-r_h_g_pulls_comments_1641771497.json @@ -0,0 +1,48 @@ +{ + "id": "78aa6b0d-5dc4-4b90-8b1a-3864ad247264", + "name": "repos_hub4j-test-org_github-api_pulls_comments_1641771497", + "request": { + "url": "/repos/hub4j-test-org/github-api/pulls/comments/1641771497", + "method": "GET", + "headers": { + "Accept": { + "equalTo": "application/vnd.github+json" + } + } + }, + "response": { + "status": 200, + "bodyFileName": "13-r_h_g_pulls_comments_1641771497.json", + "headers": { + "Date": "Mon, 26 Jan 2026 22:10:53 GMT", + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "private, max-age=60, s-maxage=60", + "Vary": "Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With", + "ETag": "W/\"089067a7341750dff9af7d18f68314ec595952d4062b8605e17e39a85285a435\"", + "Last-Modified": "Fri, 23 Jan 2026 05:51:42 GMT", + "X-OAuth-Scopes": "repo", + "X-Accepted-OAuth-Scopes": "", + "github-authentication-token-expiration": "2026-02-19 19:55:13 UTC", + "X-GitHub-Media-Type": "github.v3; format=json", + "x-github-api-version-selected": "2022-11-28", + "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", + "Access-Control-Allow-Origin": "*", + "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", + "X-Frame-Options": "deny", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "0", + "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin", + "Content-Security-Policy": "default-src 'none'", + "Server": "github.com", + "X-RateLimit-Limit": "5000", + "X-RateLimit-Remaining": "4958", + "X-RateLimit-Reset": "1769467964", + "X-RateLimit-Used": "42", + "X-RateLimit-Resource": "core", + "X-GitHub-Request-Id": "D98F:31BF39:4380FA4:3B615A1:6977E66D" + } + }, + "uuid": "78aa6b0d-5dc4-4b90-8b1a-3864ad247264", + "persistent": true, + "insertionIndex": 13 +} \ No newline at end of file