Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions src/main/java/org/kohsuke/github/GHCommitPointer.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@
*/
public class GHCommitPointer {

private String ref, sha, label;
/** The pull request this pointer belongs to, used when {@link #repo} is unavailable. */
private transient GHPullRequest pullRequest;

private String ref, sha, label;
private GHRepository repo;

private GHUser user;
/**
* Create default GHCommitPointer instance
Expand All @@ -48,12 +51,23 @@ public GHCommitPointer() {
/**
* Obtains the commit that this pointer is referring to.
*
* If the repository is unavailable (for example, the head repository of a pull request opened from a fork that has
* since been deleted), the commit is looked up in the repository of the pull request instead. GitHub keeps pull
* request commits in the base repository, so they remain reachable there.
*
* @return the commit
* @throws IOException
* the io exception
*/
public GHCommit getCommit() throws IOException {
return getRepository().getCommit(getSha());
GHRepository repository = getRepository();
if (repository == null && pullRequest != null) {
repository = pullRequest.getRepository();
}
if (repository == null) {
throw new IOException("Repository information is unavailable for commit " + getSha());
}
return repository.getCommit(getSha());
}

/**
Expand All @@ -77,7 +91,10 @@ public String getRef() {
/**
* The repository that contains the commit.
*
* @return the repository
* This value is {@code null} when GitHub does not report a repository, for example when the head repository of a
* pull request has been deleted. {@link #getCommit()} still works in that case.
*
* @return the repository, or {@code null} when it is unavailable
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Expected behavior")
public GHRepository getRepository() {
Expand Down Expand Up @@ -105,4 +122,15 @@ public GHUser getUser() {
return user;
}

/**
* Binds this pointer to the pull request it belongs to.
*
* @param pullRequest
* the pull request
* @return this pointer
*/
GHCommitPointer wrapUp(GHPullRequest pullRequest) {
this.pullRequest = pullRequest;
return this;
}
}
4 changes: 2 additions & 2 deletions src/main/java/org/kohsuke/github/GHPullRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ public AutoMerge getAutoMerge() {
* @return the base
*/
public GHCommitPointer getBase() {
return base;
return base == null ? null : base.wrapUp(this);
}

/**
Expand Down Expand Up @@ -340,7 +340,7 @@ public URL getDiffUrl() {
* @return the head
*/
public GHCommitPointer getHead() {
return head;
return head == null ? null : head.wrapUp(this);
}

/**
Expand Down
89 changes: 89 additions & 0 deletions src/test/java/org/kohsuke/github/GHCommitPointerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.kohsuke.github;

import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.Test;

import java.io.IOException;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThrows;

// TODO: Auto-generated Javadoc
/**
* The Class GHCommitPointerTest.
*/
public class GHCommitPointerTest extends AbstractGitHubWireMockTest {

/**
* Create default GHCommitPointerTest instance
*/
public GHCommitPointerTest() {
}

/**
* The head of checkstyle/checkstyle#1 comes from a fork that no longer exists, so GitHub reports its repository as
* {@code null}. The head commit is still reachable through the base repository.
*
* @throws Exception
* the exception
*/
@Test
public void getCommitFromDeletedHeadRepository() throws Exception {
GHPullRequest pullRequest = gitHub.getRepository("checkstyle/checkstyle").getPullRequest(1);
GHCommitPointer head = pullRequest.getHead();
assertThat(head.getRepository(), nullValue());
assertThat(head.getSha(), equalTo("4889a777da54c6848b92da273a28223cfaf8dfe9"));

GHCommit commit = head.getCommit();
assertThat(commit, notNullValue());
assertThat(commit.getSHA1(), equalTo("4889a777da54c6848b92da273a28223cfaf8dfe9"));
assertThat(commit.getOwner().getFullName(), equalTo("checkstyle/checkstyle"));

// Base still has its repository, so getCommit() must use that repository instead of the pull-request fallback.
// The recorded commit body is only for the head SHA; serve the same shape for the base SHA.
GHCommitPointer base = pullRequest.getBase();
assertThat(base, notNullValue());
assertThat(base.getRepository(), notNullValue());
assertThat(base.getRepository().getFullName(), equalTo("checkstyle/checkstyle"));
String baseSha = base.getSha();
mockGitHub.apiServer()
.stubFor(WireMock.get(WireMock.urlPathEqualTo("/repos/checkstyle/checkstyle/commits/" + baseSha))
.withHeader("Accept", WireMock.equalTo("application/vnd.github+json"))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json; charset=utf-8")
.withBody("{\"sha\":\"" + baseSha + "\"}")));

GHCommit baseCommit = base.getCommit();
assertThat(baseCommit, notNullValue());
assertThat(baseCommit.getSHA1(), equalTo(baseSha));
assertThat(baseCommit.getOwner().getFullName(), equalTo("checkstyle/checkstyle"));
}

/**
* A pointer that has neither a repository nor a pull request cannot resolve its commit.
*
* @throws Exception
* the exception
*/
@Test
public void getCommitWithoutRepositoryOrPullRequest() throws Exception {
snapshotNotAllowed();
GHCommitPointer pointer = new GHCommitPointer();
IOException failure = assertThrows(IOException.class, pointer::getCommit);
assertThat(failure.getMessage(), equalTo("Repository information is unavailable for commit null"));
}

/**
* {@link GHPullRequest#getHead()} and {@link GHPullRequest#getBase()} return null when the pointer was not set.
*/
@Test
public void getHeadAndBaseWhenUnset() {
snapshotNotAllowed();
GHPullRequest pullRequest = new GHPullRequest();
assertThat(pullRequest.getHead(), nullValue());
assertThat(pullRequest.getBase(), nullValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"login": "mvanhorn",
"id": 455140,
"node_id": "MDQ6VXNlcjQ1NTE0MA==",
"avatar_url": "https://avatars.githubusercontent.com/u/455140?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/mvanhorn",
"html_url": "https://github.com/mvanhorn",
"followers_url": "https://api.github.com/users/mvanhorn/followers",
"following_url": "https://api.github.com/users/mvanhorn/following{/other_user}",
"gists_url": "https://api.github.com/users/mvanhorn/gists{/gist_id}",
"starred_url": "https://api.github.com/users/mvanhorn/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/mvanhorn/subscriptions",
"organizations_url": "https://api.github.com/users/mvanhorn/orgs",
"repos_url": "https://api.github.com/users/mvanhorn/repos",
"events_url": "https://api.github.com/users/mvanhorn/events{/privacy}",
"received_events_url": "https://api.github.com/users/mvanhorn/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false,
"name": "Matt Van Horn",
"company": null,
"blog": "",
"location": null,
"email": null,
"hireable": null,
"bio": "Co-founded June (\"self-driving oven\" acquired by @webergrills) & the co that became @Lyft. Building again, more soon. OS: @slashlast30days 47k★ @ppressdev 5.4k★",
"twitter_username": "mvanhorn",
"notification_email": null,
"public_repos": 2860,
"public_gists": 1,
"followers": 5590,
"following": 7,
"created_at": "2010-10-26T19:09:59Z",
"updated_at": "2026-10-22T06:09:50Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
{
"id": 12499251,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjQ5OTI1MQ==",
"name": "checkstyle",
"full_name": "checkstyle/checkstyle",
"private": false,
"owner": {
"login": "checkstyle",
"id": 5179750,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjUxNzk3NTA=",
"avatar_url": "https://avatars.githubusercontent.com/u/5179750?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/checkstyle",
"html_url": "https://github.com/checkstyle",
"followers_url": "https://api.github.com/users/checkstyle/followers",
"following_url": "https://api.github.com/users/checkstyle/following{/other_user}",
"gists_url": "https://api.github.com/users/checkstyle/gists{/gist_id}",
"starred_url": "https://api.github.com/users/checkstyle/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/checkstyle/subscriptions",
"organizations_url": "https://api.github.com/users/checkstyle/orgs",
"repos_url": "https://api.github.com/users/checkstyle/repos",
"events_url": "https://api.github.com/users/checkstyle/events{/privacy}",
"received_events_url": "https://api.github.com/users/checkstyle/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"html_url": "https://github.com/checkstyle/checkstyle",
"description": "Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard. By default it supports the Google Java Style Guide and Sun Code Conventions, but is highly configurable. It can be invoked with an ANT task and a command line program.",
"fork": false,
"url": "https://api.github.com/repos/checkstyle/checkstyle",
"forks_url": "https://api.github.com/repos/checkstyle/checkstyle/forks",
"keys_url": "https://api.github.com/repos/checkstyle/checkstyle/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/checkstyle/checkstyle/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/checkstyle/checkstyle/teams",
"hooks_url": "https://api.github.com/repos/checkstyle/checkstyle/hooks",
"issue_events_url": "https://api.github.com/repos/checkstyle/checkstyle/issues/events{/number}",
"events_url": "https://api.github.com/repos/checkstyle/checkstyle/events",
"assignees_url": "https://api.github.com/repos/checkstyle/checkstyle/assignees{/user}",
"branches_url": "https://api.github.com/repos/checkstyle/checkstyle/branches{/branch}",
"tags_url": "https://api.github.com/repos/checkstyle/checkstyle/tags",
"blobs_url": "https://api.github.com/repos/checkstyle/checkstyle/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/checkstyle/checkstyle/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/checkstyle/checkstyle/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/checkstyle/checkstyle/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/checkstyle/checkstyle/statuses/{sha}",
"languages_url": "https://api.github.com/repos/checkstyle/checkstyle/languages",
"stargazers_url": "https://api.github.com/repos/checkstyle/checkstyle/stargazers",
"contributors_url": "https://api.github.com/repos/checkstyle/checkstyle/contributors",
"subscribers_url": "https://api.github.com/repos/checkstyle/checkstyle/subscribers",
"subscription_url": "https://api.github.com/repos/checkstyle/checkstyle/subscription",
"commits_url": "https://api.github.com/repos/checkstyle/checkstyle/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/checkstyle/checkstyle/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/checkstyle/checkstyle/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/checkstyle/checkstyle/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/checkstyle/checkstyle/contents/{+path}",
"compare_url": "https://api.github.com/repos/checkstyle/checkstyle/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/checkstyle/checkstyle/merges",
"archive_url": "https://api.github.com/repos/checkstyle/checkstyle/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/checkstyle/checkstyle/downloads",
"issues_url": "https://api.github.com/repos/checkstyle/checkstyle/issues{/number}",
"pulls_url": "https://api.github.com/repos/checkstyle/checkstyle/pulls{/number}",
"milestones_url": "https://api.github.com/repos/checkstyle/checkstyle/milestones{/number}",
"notifications_url": "https://api.github.com/repos/checkstyle/checkstyle/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/checkstyle/checkstyle/labels{/name}",
"releases_url": "https://api.github.com/repos/checkstyle/checkstyle/releases{/id}",
"deployments_url": "https://api.github.com/repos/checkstyle/checkstyle/deployments",
"created_at": "2013-08-31T02:05:05Z",
"updated_at": "2026-10-23T12:15:07Z",
"pushed_at": "2026-10-23T13:53:31Z",
"git_url": "git://github.com/checkstyle/checkstyle.git",
"ssh_url": "git@github.com:checkstyle/checkstyle.git",
"clone_url": "https://github.com/checkstyle/checkstyle.git",
"svn_url": "https://github.com/checkstyle/checkstyle",
"homepage": "https://checkstyle.org",
"size": 321106,
"stargazers_count": 9616,
"watchers_count": 9616,
"language": "Java",
"has_issues": true,
"has_projects": true,
"has_downloads": false,
"has_wiki": true,
"has_pages": false,
"has_discussions": true,
"forks_count": 4300,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 821,
"license": {
"key": "lgpl-2.1",
"name": "GNU Lesser General Public License v2.1",
"spdx_id": "LGPL-2.1",
"url": "https://api.github.com/licenses/lgpl-2.1",
"node_id": "MDc6TGljZW5zZTEx"
},
"allow_forking": true,
"is_template": false,
"web_commit_signoff_required": false,
"has_pull_requests": true,
"pull_request_creation_policy": "all",
"topics": [
"code-quality",
"command-line-tool",
"hacktoberfest",
"java",
"static-analysis",
"static-code-analysis"
],
"visibility": "public",
"forks": 4300,
"open_issues": 821,
"watchers": 9616,
"default_branch": "master",
"permissions": {
"admin": false,
"maintain": false,
"push": false,
"triage": false,
"pull": true
},
"temp_clone_token": "",
"custom_properties": {},
"organization": {
"login": "checkstyle",
"id": 5179750,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjUxNzk3NTA=",
"avatar_url": "https://avatars.githubusercontent.com/u/5179750?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/checkstyle",
"html_url": "https://github.com/checkstyle",
"followers_url": "https://api.github.com/users/checkstyle/followers",
"following_url": "https://api.github.com/users/checkstyle/following{/other_user}",
"gists_url": "https://api.github.com/users/checkstyle/gists{/gist_id}",
"starred_url": "https://api.github.com/users/checkstyle/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/checkstyle/subscriptions",
"organizations_url": "https://api.github.com/users/checkstyle/orgs",
"repos_url": "https://api.github.com/users/checkstyle/repos",
"events_url": "https://api.github.com/users/checkstyle/events{/privacy}",
"received_events_url": "https://api.github.com/users/checkstyle/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"network_count": 4300,
"subscribers_count": 223
}
Loading
Loading