Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
package org.eclipse.egit.pullrequest.internal.github;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

import java.util.Arrays;
import java.util.List;

import org.junit.Test;
Expand All @@ -36,6 +40,49 @@ public void testTokenOnlyClientConstruction() {
assertThat(new GitHubClient("test-token"), notNullValue()); //$NON-NLS-1$
}

@Test
public void testAccessibleSearchCoversOwnedOrgsAndForeignRepos() {
List<String> queries = GitHubSearchQueries
.accessiblePullRequestQueries("alice", //$NON-NLS-1$
Arrays.asList("acme", "friends"), //$NON-NLS-1$ //$NON-NLS-2$
Arrays.asList("alice/notes", "acme/app", //$NON-NLS-1$ //$NON-NLS-2$
"bob/tool"), //$NON-NLS-1$
null, null);

assertThat(queries, contains("is:pr user:alice is:open", //$NON-NLS-1$
"is:pr org:acme is:open", //$NON-NLS-1$
"is:pr org:friends is:open", //$NON-NLS-1$
"is:pr repo:bob/tool is:open")); //$NON-NLS-1$
}

@Test
public void testAccessibleSearchAppliesAuthorFilterOnlyWhenSet() {
List<String> allAuthors = GitHubSearchQueries
.accessiblePullRequestQueries("alice", null, null, //$NON-NLS-1$
"OPEN", null); //$NON-NLS-1$
List<String> oneAuthor = GitHubSearchQueries
.accessiblePullRequestQueries("alice", null, null, //$NON-NLS-1$
"OPEN", "carol"); //$NON-NLS-1$ //$NON-NLS-2$

assertThat(allAuthors, contains("is:pr user:alice is:open")); //$NON-NLS-1$
assertThat(allAuthors.get(0), not(containsString("author:"))); //$NON-NLS-1$
assertThat(oneAuthor,
contains("is:pr user:alice author:carol is:open")); //$NON-NLS-1$
}

@Test
public void testAccessibleSearchMapsMergedAndAllStates() {
assertThat(GitHubSearchQueries.pullRequestQuery("user:alice", //$NON-NLS-1$
"MERGED", null), //$NON-NLS-1$
equalTo("is:pr user:alice is:merged")); //$NON-NLS-1$
assertThat(GitHubSearchQueries.pullRequestQuery("org:acme", //$NON-NLS-1$
"ALL", null), //$NON-NLS-1$
equalTo("is:pr org:acme")); //$NON-NLS-1$
assertThat(GitHubSearchQueries.pullRequestQuery("repo:bob/tool", //$NON-NLS-1$
"DECLINED", null), //$NON-NLS-1$
equalTo("is:pr repo:bob/tool is:closed is:unmerged")); //$NON-NLS-1$
}

@Test
public void testParsePullRequestPathsFromSearch() {
String json = "{\"total_count\":2,\"items\":[" //$NON-NLS-1$
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@
*/
public class GitHubJsonParserTest {

@Test
public void testParseOrganizationLogins() {
String json = "[{\"login\":\"acme\",\"id\":1}," //$NON-NLS-1$
+ "{\"login\":\"friends\",\"id\":2}]"; //$NON-NLS-1$

assertThat(GitHubJsonParser.parseLogins(json), hasSize(2));
assertThat(GitHubJsonParser.parseLogins(json).get(0),
equalTo("acme")); //$NON-NLS-1$
assertThat(GitHubJsonParser.parseLogins(json).get(1),
equalTo("friends")); //$NON-NLS-1$
}

@Test
public void testParseRepositoryFullNamesIgnoresNestedOwner() {
String json = "[{\"full_name\":\"bob/tool\",\"owner\":{\"login\":\"bob\"}}," //$NON-NLS-1$
+ "{\"full_name\":\"acme/app\",\"owner\":{\"login\":\"acme\"}}]"; //$NON-NLS-1$

List<String> names = GitHubJsonParser.parseRepositoryFullNames(json);

assertThat(names, hasSize(2));
assertThat(names.get(0), equalTo("bob/tool")); //$NON-NLS-1$
assertThat(names.get(1), equalTo("acme/app")); //$NON-NLS-1$
}

@Test
public void testParseInlineComment() {
String json = "{\"id\":123,\"body\":\"Fix this issue\",\"created_at\":\"2026-01-15T10:00:00Z\"," //$NON-NLS-1$
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ default ConnectionDiagnostics diagnoseConnection() {
int count = getPullRequests(null, null, null, 1, 0).size();
diagnostics.add("Read pull requests", //$NON-NLS-1$
ConnectionDiagnostics.Outcome.OK,
"Repository returned " + count + " pull request(s)"); //$NON-NLS-1$ //$NON-NLS-2$
"Returned " + count + " pull request(s)"); //$NON-NLS-1$ //$NON-NLS-2$
} catch (IOException | RuntimeException e) {
diagnostics.add("Read pull requests", //$NON-NLS-1$
ConnectionDiagnostics.Outcome.FAILED, describe(e));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -64,8 +66,8 @@ public GitHubClient(@NonNull String owner, @NonNull String repo,
}

/**
* Creates a token-scoped GitHub client that lists pull requests authored by
* the authenticated user across all repositories visible to the token.
* Creates a token-scoped GitHub client that lists pull requests from every
* repository the authenticated user owns or belongs to.
*
* @param token
* the GitHub access token
Expand Down Expand Up @@ -195,26 +197,38 @@ public void setActivePullRequest(PullRequest pullRequest) {

private List<PullRequest> getUserPullRequests(String state,
String authorUsername, int limit, int start) throws IOException {
String author = authorUsername == null || authorUsername.isBlank()
? "@me" : authorUsername; //$NON-NLS-1$
StringBuilder query = new StringBuilder("is:pr author:") //$NON-NLS-1$
.append(author);
if ("MERGED".equalsIgnoreCase(state)) { //$NON-NLS-1$
query.append(" is:merged"); //$NON-NLS-1$
} else if ("DECLINED".equalsIgnoreCase(state)) { //$NON-NLS-1$
query.append(" is:closed is:unmerged"); //$NON-NLS-1$
} else if (state == null || !"ALL".equalsIgnoreCase(state)) { //$NON-NLS-1$
query.append(" is:open"); //$NON-NLS-1$
List<String> queries = GitHubSearchQueries
.accessiblePullRequestQueries(getCurrentUser(),
listOrganizationLogins(),
listCollaboratorRepositories(), state,
authorUsername);

int needed = Math.max(1, start + Math.max(1, limit));
int pageSize = Math.min(100, needed);
LinkedHashSet<String> pullRequestPaths = new LinkedHashSet<>();
for (String query : queries) {
int collected = 0;
int page = 1;
while (collected < needed) {
String path = "/search/issues?q=" //$NON-NLS-1$
+ URLEncoder.encode(query, StandardCharsets.UTF_8)
+ "&per_page=" + pageSize //$NON-NLS-1$
+ "&page=" + page //$NON-NLS-1$
+ "&sort=updated&order=desc"; //$NON-NLS-1$
List<String> pagePaths = GitHubJsonParser
.parseSearchPullRequestPaths(doGet(path));
if (pagePaths.isEmpty()) {
break;
}
pullRequestPaths.addAll(pagePaths);
collected += pagePaths.size();
if (pagePaths.size() < pageSize) {
break;
}
page++;
}
}

int pageSize = Math.max(1, Math.min(limit, 100));
int page = start / pageSize + 1;
String path = "/search/issues?q=" //$NON-NLS-1$
+ URLEncoder.encode(query.toString(), StandardCharsets.UTF_8)
+ "&per_page=" + pageSize + "&page=" + page; //$NON-NLS-1$ //$NON-NLS-2$
String searchResult = doGet(path);
List<String> pullRequestPaths = GitHubJsonParser
.parseSearchPullRequestPaths(searchResult);
List<PullRequest> result = new ArrayList<>();
for (String pullRequestPath : pullRequestPaths) {
PullRequest pullRequest = GitHubJsonParser
Expand All @@ -223,7 +237,28 @@ private List<PullRequest> getUserPullRequests(String state,
result.add(pullRequest);
}
}
return result;
result.sort(Comparator.comparing(PullRequest::getUpdatedDate,
Comparator.nullsLast(Comparator.reverseOrder())));
int from = Math.min(Math.max(0, start), result.size());
int to = Math.min(from + Math.max(0, limit), result.size());
return new ArrayList<>(result.subList(from, to));
}

private List<String> listOrganizationLogins() throws IOException {
List<String> logins = new ArrayList<>();
for (String page : doGetAllPages("/user/orgs?per_page=100")) { //$NON-NLS-1$
logins.addAll(GitHubJsonParser.parseLogins(page));
}
return logins;
}

private List<String> listCollaboratorRepositories() throws IOException {
List<String> fullNames = new ArrayList<>();
for (String page : doGetAllPages(
"/user/repos?affiliation=collaborator&per_page=100")) { //$NON-NLS-1$
fullNames.addAll(GitHubJsonParser.parseRepositoryFullNames(page));
}
return fullNames;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,88 @@ static List<String> parseSearchPullRequestPaths(String json) {
return result;
}

/**
* Parses {@code login} values from a GitHub user or organization array.
*
* @param json
* JSON array from {@code /user/orgs}
* @return organization or user logins
*/
static List<String> parseLogins(String json) {
List<String> result = new ArrayList<>();
for (String object : splitTopLevelObjects(json)) {
String login = extractString(object, "login"); //$NON-NLS-1$
if (login != null && !login.isBlank()) {
result.add(login);
}
}
return result;
}

/**
* Parses {@code owner/name} values from a GitHub repository array.
*
* @param json
* JSON array from {@code /user/repos}
* @return repository full names
*/
static List<String> parseRepositoryFullNames(String json) {
List<String> result = new ArrayList<>();
for (String object : splitTopLevelObjects(json)) {
String fullName = extractString(object, "full_name"); //$NON-NLS-1$
if (fullName != null && !fullName.isBlank()) {
result.add(fullName);
}
}
return result;
}

private static List<String> splitTopLevelObjects(String json) {
List<String> result = new ArrayList<>();
if (json == null || json.isBlank()) {
return result;
}
String trimmed = json.trim();
if (!trimmed.startsWith("[")) { //$NON-NLS-1$
return result;
}
int depth = 0;
int start = -1;
boolean inString = false;
boolean escaped = false;
for (int i = 0; i < trimmed.length(); i++) {
char c = trimmed.charAt(i);
if (escaped) {
escaped = false;
continue;
}
if (c == '\\') {
escaped = true;
continue;
}
if (c == '"') {
inString = !inString;
continue;
}
if (inString) {
continue;
}
if (c == '{') {
if (depth == 0) {
start = i;
}
depth++;
} else if (c == '}') {
depth--;
if (depth == 0 && start >= 0) {
result.add(trimmed.substring(start, i + 1));
start = -1;
}
}
}
return result;
}

/**
* Parses a list of pull requests from GitHub API JSON
*
Expand Down
Loading