org.jetbrains.plugins.github.api.GithubApiUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.jetbrains.plugins.github.api.GithubApiUtil.java

Source

/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jetbrains.plugins.github.api;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HeaderElement;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException;
import org.jetbrains.plugins.github.exceptions.GithubJsonException;
import org.jetbrains.plugins.github.exceptions.GithubStatusCodeException;
import org.jetbrains.plugins.github.util.GithubAuthData;
import org.jetbrains.plugins.github.util.GithubSslSupport;
import org.jetbrains.plugins.github.util.GithubUrlUtil;
import org.jetbrains.plugins.github.util.GithubUtil;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ThrowableConvertor;
import com.intellij.util.net.HttpConfigurable;

/**
 * @author Kirill Likhodedov
 */
public class GithubApiUtil {

    public static final String DEFAULT_GITHUB_HOST = "github.com";

    private static final int CONNECTION_TIMEOUT = 5000;
    private static final String PER_PAGE = "per_page=100";
    private static final Logger LOG = GithubUtil.LOG;

    private static final Header ACCEPT_HTML_BODY_MARKUP = new Header("Accept",
            "application/vnd.github.v3.html+json");
    private static final Header ACCEPT_NEW_SEARCH_API = new Header("Accept", "application/vnd.github.preview");

    @NotNull
    private static final Gson gson = initGson();

    private static Gson initGson() {
        GsonBuilder builder = new GsonBuilder();
        builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
        return builder.create();
    }

    private enum HttpVerb {
        GET, POST, DELETE, HEAD
    }

    @Nullable
    private static JsonElement postRequest(@NotNull GithubAuthData auth, @NotNull String path,
            @Nullable String requestBody, @NotNull Header... headers) throws IOException {
        return request(auth, path, requestBody, Arrays.asList(headers), HttpVerb.POST).getJsonElement();
    }

    @Nullable
    private static JsonElement deleteRequest(@NotNull GithubAuthData auth, @NotNull String path,
            @NotNull Header... headers) throws IOException {
        return request(auth, path, null, Arrays.asList(headers), HttpVerb.DELETE).getJsonElement();
    }

    @Nullable
    private static JsonElement getRequest(@NotNull GithubAuthData auth, @NotNull String path,
            @NotNull Header... headers) throws IOException {
        return request(auth, path, null, Arrays.asList(headers), HttpVerb.GET).getJsonElement();
    }

    @NotNull
    private static ResponsePage request(@NotNull GithubAuthData auth, @NotNull String path,
            @Nullable String requestBody, @NotNull Collection<Header> headers, @NotNull HttpVerb verb)
            throws IOException {
        HttpMethod method = null;
        try {
            String uri = GithubUrlUtil.getApiUrl(auth.getHost()) + path;
            method = doREST(auth, uri, requestBody, headers, verb);

            checkStatusCode(method);

            InputStream resp = method.getResponseBodyAsStream();
            if (resp == null) {
                return new ResponsePage();
            }

            JsonElement ret = parseResponse(resp);
            if (ret.isJsonNull()) {
                return new ResponsePage();
            }

            Header header = method.getResponseHeader("Link");
            if (header != null) {
                String value = header.getValue();
                int end = value.indexOf(">; rel=\"next\"");
                int begin = value.lastIndexOf('<', end);
                if (begin >= 0 && end >= 0) {
                    String newPath = GithubUrlUtil.removeProtocolPrefix(value.substring(begin + 1, end));
                    int index = newPath.indexOf('/');

                    return new ResponsePage(ret, newPath.substring(index));
                }
            }

            return new ResponsePage(ret);
        } finally {
            if (method != null) {
                method.releaseConnection();
            }
        }
    }

    @NotNull
    private static HttpMethod doREST(@NotNull final GithubAuthData auth, @NotNull final String uri,
            @Nullable final String requestBody, @NotNull final Collection<Header> headers,
            @NotNull final HttpVerb verb) throws IOException {
        HttpClient client = getHttpClient(auth.getBasicAuth(), auth.isUseProxy());
        return GithubSslSupport.getInstance().executeSelfSignedCertificateAwareRequest(client, uri,
                new ThrowableConvertor<String, HttpMethod, IOException>() {
                    @Override
                    public HttpMethod convert(String uri) throws IOException {
                        HttpMethod method;
                        switch (verb) {
                        case POST:
                            method = new PostMethod(uri);
                            if (requestBody != null) {
                                ((PostMethod) method).setRequestEntity(
                                        new StringRequestEntity(requestBody, "application/json", "UTF-8"));
                            }
                            break;
                        case GET:
                            method = new GetMethod(uri);
                            break;
                        case DELETE:
                            method = new DeleteMethod(uri);
                            break;
                        case HEAD:
                            method = new HeadMethod(uri);
                            break;
                        default:
                            throw new IllegalStateException("Wrong HttpVerb: unknown method: " + verb.toString());
                        }
                        GithubAuthData.TokenAuth tokenAuth = auth.getTokenAuth();
                        if (tokenAuth != null) {
                            method.addRequestHeader("Authorization", "token " + tokenAuth.getToken());
                        }
                        for (Header header : headers) {
                            method.addRequestHeader(header);
                        }
                        return method;
                    }
                });
    }

    @NotNull
    private static HttpClient getHttpClient(@Nullable GithubAuthData.BasicAuth basicAuth, boolean useProxy) {
        final HttpClient client = new HttpClient();
        HttpConnectionManagerParams params = client.getHttpConnectionManager().getParams();
        params.setConnectionTimeout(CONNECTION_TIMEOUT); //set connection timeout (how long it takes to connect to
        // remote host)
        params.setSoTimeout(CONNECTION_TIMEOUT); //set socket timeout (how long it takes to retrieve data from remote
        // host)

        client.getParams().setContentCharset("UTF-8");
        // Configure proxySettings if it is required
        final HttpConfigurable proxySettings = HttpConfigurable.getInstance();
        if (useProxy && proxySettings.USE_HTTP_PROXY && !StringUtil.isEmptyOrSpaces(proxySettings.PROXY_HOST)) {
            client.getHostConfiguration().setProxy(proxySettings.PROXY_HOST, proxySettings.PROXY_PORT);
            if (proxySettings.PROXY_AUTHENTICATION) {
                client.getState().setProxyCredentials(AuthScope.ANY, new UsernamePasswordCredentials(
                        proxySettings.getProxyLogin(), proxySettings.getPlainProxyPassword()));
            }
        }
        if (basicAuth != null) {
            client.getParams().setCredentialCharset("UTF-8");
            client.getParams().setAuthenticationPreemptive(true);
            client.getState().setCredentials(AuthScope.ANY,
                    new UsernamePasswordCredentials(basicAuth.getLogin(), basicAuth.getPassword()));
        }
        return client;
    }

    private static void checkStatusCode(@NotNull HttpMethod method) throws IOException {
        int code = method.getStatusCode();
        switch (code) {
        case HttpStatus.SC_OK:
        case HttpStatus.SC_CREATED:
        case HttpStatus.SC_ACCEPTED:
        case HttpStatus.SC_NO_CONTENT:
            return;
        case HttpStatus.SC_BAD_REQUEST:
        case HttpStatus.SC_UNAUTHORIZED:
        case HttpStatus.SC_PAYMENT_REQUIRED:
        case HttpStatus.SC_FORBIDDEN:
            throw new GithubAuthenticationException("Request response: " + getErrorMessage(method));
        default:
            throw new GithubStatusCodeException(code + ": " + getErrorMessage(method), code);
        }
    }

    @NotNull
    private static String getErrorMessage(@NotNull HttpMethod method) {
        try {
            InputStream resp = method.getResponseBodyAsStream();
            if (resp != null) {
                GithubErrorMessageRaw error = fromJson(parseResponse(resp), GithubErrorMessageRaw.class);
                return method.getStatusText() + " - " + error.getMessage();
            }
        } catch (IOException e) {
            LOG.info(e);
        }
        return method.getStatusText();
    }

    @NotNull
    private static JsonElement parseResponse(@NotNull InputStream githubResponse) throws IOException {
        Reader reader = new InputStreamReader(githubResponse);
        try {
            return new JsonParser().parse(reader);
        } catch (JsonSyntaxException jse) {
            throw new GithubJsonException("Couldn't parse GitHub response", jse);
        } finally {
            reader.close();
        }
    }

    private static class ResponsePage {
        @Nullable
        private final JsonElement response;
        @Nullable
        private final String nextPage;

        public ResponsePage() {
            this(null, null);
        }

        public ResponsePage(@Nullable JsonElement response) {
            this(response, null);
        }

        public ResponsePage(@Nullable JsonElement response, @Nullable String next) {
            this.response = response;
            this.nextPage = next;
        }

        @Nullable
        public JsonElement getJsonElement() {
            return response;
        }

        @Nullable
        public String getNextPage() {
            return nextPage;
        }
    }

    /*
    * Json API
    */

    static <Raw extends DataConstructor, Result> Result createDataFromRaw(@NotNull Raw rawObject,
            @NotNull Class<Result> resultClass) throws GithubJsonException {
        try {
            return rawObject.create(resultClass);
        } catch (Exception e) {
            throw new GithubJsonException("Json parse error", e);
        }
    }

    public static class PagedRequest<T> {
        @Nullable
        private String myNextPage;
        @NotNull
        private final Collection<Header> myHeaders;
        @NotNull
        private final Class<T> myResult;
        @NotNull
        private final Class<? extends DataConstructor[]> myRawArray;

        @SuppressWarnings("NullableProblems")
        public PagedRequest(@NotNull String path, @NotNull Class<T> result,
                @NotNull Class<? extends DataConstructor[]> rawArray, @NotNull Header... headers) {
            myNextPage = path;
            myResult = result;
            myRawArray = rawArray;
            myHeaders = Arrays.asList(headers);
        }

        @NotNull
        public List<T> next(@NotNull GithubAuthData auth) throws IOException {
            if (myNextPage == null) {
                throw new NoSuchElementException();
            }

            String page = myNextPage;
            myNextPage = null;

            ResponsePage response = request(auth, page, null, myHeaders, HttpVerb.GET);

            if (response.getJsonElement() == null) {
                throw new HttpException("Empty response");
            }

            if (!response.getJsonElement().isJsonArray()) {
                throw new GithubJsonException("Wrong json type: expected JsonArray",
                        new Exception(response.getJsonElement().toString()));
            }

            myNextPage = response.getNextPage();

            List<T> result = new ArrayList<T>();
            for (DataConstructor raw : fromJson(response.getJsonElement().getAsJsonArray(), myRawArray)) {
                result.add(createDataFromRaw(raw, myResult));
            }
            return result;
        }

        public boolean hasNext() {
            return myNextPage != null;
        }

        @NotNull
        public List<T> getAll(@NotNull GithubAuthData auth) throws IOException {
            List<T> result = new ArrayList<T>();
            while (hasNext()) {
                result.addAll(next(auth));
            }
            return result;
        }
    }

    @NotNull
    private static <T> T fromJson(@Nullable JsonElement json, @NotNull Class<T> classT) throws IOException {
        if (json == null) {
            throw new GithubJsonException("Unexpected empty response");
        }

        T res;
        try {
            //cast as workaround for early java 1.6 bug
            //noinspection RedundantCast
            res = (T) gson.fromJson(json, classT);
        } catch (ClassCastException e) {
            throw new GithubJsonException("Parse exception while converting JSON to object " + classT.toString(),
                    e);
        } catch (JsonParseException e) {
            throw new GithubJsonException("Parse exception while converting JSON to object " + classT.toString(),
                    e);
        }
        if (res == null) {
            throw new GithubJsonException("Empty Json response");
        }
        return res;
    }

    /*
    * Github API
    */

    @NotNull
    public static Collection<String> getTokenScopes(@NotNull GithubAuthData auth) throws IOException {
        HttpMethod method = null;
        try {
            String uri = GithubUrlUtil.getApiUrl(auth.getHost()) + "/user";
            method = doREST(auth, uri, null, Collections.<Header>emptyList(), HttpVerb.HEAD);

            checkStatusCode(method);

            Header header = method.getResponseHeader("X-OAuth-Scopes");
            if (header == null) {
                throw new HttpException("No scopes header");
            }

            Collection<String> scopes = new ArrayList<String>();
            for (HeaderElement elem : header.getElements()) {
                scopes.add(elem.getName());
            }
            return scopes;
        } finally {
            if (method != null) {
                method.releaseConnection();
            }
        }
    }

    @NotNull
    public static String getScopedToken(@NotNull GithubAuthData auth, @NotNull Collection<String> scopes,
            @Nullable String note) throws IOException {
        String path = "/authorizations";

        GithubAuthorizationRequest request = new GithubAuthorizationRequest(new ArrayList<String>(scopes), note,
                null);
        GithubAuthorization response = createDataFromRaw(
                fromJson(postRequest(auth, path, gson.toJson(request)), GithubAuthorizationRaw.class),
                GithubAuthorization.class);

        return response.getToken();
    }

    @NotNull
    public static String getReadOnlyToken(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo,
            @Nullable String note) throws IOException {
        GithubRepo repository = getDetailedRepoInfo(auth, user, repo);

        List<String> scopes = repository.isPrivate() ? Collections.singletonList("repo")
                : Collections.<String>emptyList();

        return getScopedToken(auth, scopes, note);
    }

    @NotNull
    public static GithubUser getCurrentUser(@NotNull GithubAuthData auth) throws IOException {
        JsonElement result = getRequest(auth, "/user");
        return createDataFromRaw(fromJson(result, GithubUserRaw.class), GithubUser.class);
    }

    @NotNull
    public static GithubUserDetailed getCurrentUserDetailed(@NotNull GithubAuthData auth) throws IOException {
        JsonElement result = getRequest(auth, "/user");
        return createDataFromRaw(fromJson(result, GithubUserRaw.class), GithubUserDetailed.class);
    }

    @NotNull
    public static List<GithubRepo> getUserRepos(@NotNull GithubAuthData auth) throws IOException {
        String path = "/user/repos?" + PER_PAGE;

        PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class,
                GithubRepoRaw[].class);

        return request.getAll(auth);
    }

    @NotNull
    public static List<GithubRepo> getUserRepos(@NotNull GithubAuthData auth, @NotNull String user)
            throws IOException {
        String path = "/users/" + user + "/repos?" + PER_PAGE;

        PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class,
                GithubRepoRaw[].class);

        return request.getAll(auth);
    }

    @NotNull
    public static List<GithubRepo> getAvailableRepos(@NotNull GithubAuthData auth) throws IOException {
        List<GithubRepo> repos = new ArrayList<GithubRepo>();

        repos.addAll(getUserRepos(auth));

        String path = "/user/orgs?" + PER_PAGE;
        PagedRequest<GithubOrg> request = new PagedRequest<GithubOrg>(path, GithubOrg.class, GithubOrgRaw[].class);

        for (GithubOrg org : request.getAll(auth)) {
            String pathOrg = "/orgs/" + org.getLogin() + "/repos?type=member&" + PER_PAGE;
            PagedRequest<GithubRepo> requestOrg = new PagedRequest<GithubRepo>(pathOrg, GithubRepo.class,
                    GithubRepoRaw[].class);
            repos.addAll(requestOrg.getAll(auth));
        }

        String pathWatched = "/user/subscriptions?" + PER_PAGE;
        PagedRequest<GithubRepo> requestWatched = new PagedRequest<GithubRepo>(pathWatched, GithubRepo.class,
                GithubRepoRaw[].class);
        repos.addAll(requestWatched.getAll(auth));

        return repos;
    }

    @NotNull
    public static GithubRepoDetailed getDetailedRepoInfo(@NotNull GithubAuthData auth, @NotNull String owner,
            @NotNull String name) throws IOException {
        final String request = "/repos/" + owner + "/" + name;

        JsonElement jsonObject = getRequest(auth, request);

        return createDataFromRaw(fromJson(jsonObject, GithubRepoRaw.class), GithubRepoDetailed.class);
    }

    public static void deleteGithubRepository(@NotNull GithubAuthData auth, @NotNull String username,
            @NotNull String repo) throws IOException {
        String path = "/repos/" + username + "/" + repo;
        deleteRequest(auth, path);
    }

    public static void deleteGist(@NotNull GithubAuthData auth, @NotNull String id) throws IOException {
        String path = "/gists/" + id;
        deleteRequest(auth, path);
    }

    @NotNull
    public static GithubGist getGist(@NotNull GithubAuthData auth, @NotNull String id) throws IOException {
        String path = "/gists/" + id;
        JsonElement result = getRequest(auth, path);

        return createDataFromRaw(fromJson(result, GithubGistRaw.class), GithubGist.class);
    }

    @NotNull
    public static GithubGist createGist(@NotNull GithubAuthData auth,
            @NotNull List<GithubGist.FileContent> contents, @NotNull String description, boolean isPrivate)
            throws IOException {
        String request = gson.toJson(new GithubGistRequest(contents, description, !isPrivate));
        return createDataFromRaw(fromJson(postRequest(auth, "/gists", request), GithubGistRaw.class),
                GithubGist.class);
    }

    @NotNull
    public static GithubPullRequest createPullRequest(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, @NotNull String title, @NotNull String description, @NotNull String from,
            @NotNull String onto) throws IOException {
        String request = gson.toJson(new GithubPullRequestRequest(title, description, from, onto));
        return createDataFromRaw(fromJson(postRequest(auth, "/repos/" + user + "/" + repo + "/pulls", request),
                GithubPullRequestRaw.class), GithubPullRequest.class);
    }

    @NotNull
    public static GithubRepo createRepo(@NotNull GithubAuthData auth, @NotNull String name,
            @NotNull String description, boolean isPublic) throws IOException {
        String path = "/user/repos";

        GithubRepoRequest request = new GithubRepoRequest(name, description, isPublic);

        return createDataFromRaw(fromJson(postRequest(auth, path, gson.toJson(request)), GithubRepoRaw.class),
                GithubRepo.class);
    }

    @NotNull
    public static List<GithubIssue> getIssuesAssigned(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, @Nullable String assigned) throws IOException {
        String path;
        if (StringUtil.isEmptyOrSpaces(assigned)) {
            path = "/repos/" + user + "/" + repo + "/issues?" + PER_PAGE;
        } else {
            path = "/repos/" + user + "/" + repo + "/issues?assignee=" + assigned + "&" + PER_PAGE;
        }

        PagedRequest<GithubIssue> request = new PagedRequest<GithubIssue>(path, GithubIssue.class,
                GithubIssueRaw[].class);

        return request.getAll(auth);
    }

    @NotNull
    public static List<GithubIssue> getIssuesQueried(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, @Nullable String query) throws IOException {
        query = URLEncoder.encode("@" + user + "/" + repo + " " + query, "UTF-8");
        String path = "/search/issues?q=" + query;

        //TODO: remove header after end of preview period. ~ october 2013
        //TODO: Use bodyHtml for issues - preview does not support this feature
        JsonElement result = getRequest(auth, path, ACCEPT_NEW_SEARCH_API);

        return createDataFromRaw(fromJson(result, GithubIssuesSearchResultRaw.class),
                GithubIssuesSearchResult.class).getIssues();
    }

    @NotNull
    public static GithubIssue getIssue(@NotNull GithubAuthData auth, @NotNull String user, @NotNull String repo,
            @NotNull String id) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/issues/" + id;

        JsonElement result = getRequest(auth, path);

        return createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class);
    }

    @NotNull
    public static List<GithubIssueComment> getIssueComments(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, long id) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/issues/" + id + "/comments?" + PER_PAGE;

        PagedRequest<GithubIssueComment> request = new PagedRequest<GithubIssueComment>(path,
                GithubIssueComment.class, GithubIssueCommentRaw[].class, ACCEPT_HTML_BODY_MARKUP);

        return request.getAll(auth);
    }

    @NotNull
    public static GithubCommitDetailed getCommit(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, @NotNull String sha) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/commits/" + sha;

        JsonElement result = getRequest(auth, path);
        return createDataFromRaw(fromJson(result, GithubCommitRaw.class), GithubCommitDetailed.class);
    }

    @NotNull
    public static GithubPullRequest getPullRequest(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, int id) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/pulls/" + id;
        return createDataFromRaw(
                fromJson(getRequest(auth, path, ACCEPT_HTML_BODY_MARKUP), GithubPullRequestRaw.class),
                GithubPullRequest.class);
    }

    @NotNull
    public static List<GithubPullRequest> getPullRequests(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE;

        PagedRequest<GithubPullRequest> request = new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class,
                GithubPullRequestRaw[].class, ACCEPT_HTML_BODY_MARKUP);

        return request.getAll(auth);
    }

    @NotNull
    public static PagedRequest<GithubPullRequest> getPullRequests(@NotNull String user, @NotNull String repo) {
        String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE;

        return new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class, GithubPullRequestRaw[].class,
                ACCEPT_HTML_BODY_MARKUP);
    }

    @NotNull
    public static List<GithubCommit> getPullRequestCommits(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, long id) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/commits?" + PER_PAGE;

        PagedRequest<GithubCommit> request = new PagedRequest<GithubCommit>(path, GithubCommit.class,
                GithubCommitRaw[].class);

        return request.getAll(auth);
    }

    @NotNull
    public static List<GithubFile> getPullRequestFiles(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, long id) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/files?" + PER_PAGE;

        PagedRequest<GithubFile> request = new PagedRequest<GithubFile>(path, GithubFile.class,
                GithubFileRaw[].class);

        return request.getAll(auth);
    }

    @NotNull
    public static List<GithubBranch> getRepoBranches(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/branches?" + PER_PAGE;

        PagedRequest<GithubBranch> request = new PagedRequest<GithubBranch>(path, GithubBranch.class,
                GithubBranchRaw[].class);

        return request.getAll(auth);
    }

    @Nullable
    public static GithubRepo findForkByUser(@NotNull GithubAuthData auth, @NotNull String user,
            @NotNull String repo, @NotNull String forkUser) throws IOException {
        String path = "/repos/" + user + "/" + repo + "/forks?" + PER_PAGE;

        PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class,
                GithubRepoRaw[].class);

        while (request.hasNext()) {
            for (GithubRepo fork : request.next(auth)) {
                if (StringUtil.equalsIgnoreCase(fork.getUserName(), forkUser)) {
                    return fork;
                }
            }
        }

        return null;
    }
}