dd51519db9f5ea5fbb1ea4eaaf2a41a5d4f856b0
[idea/community.git] / plugins / github / src / org / jetbrains / plugins / github / api / GithubApiUtil.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.plugins.github.api;
17
18 import com.google.gson.*;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.util.Condition;
21 import com.intellij.openapi.util.text.StringUtil;
22 import com.intellij.openapi.vfs.CharsetToolkit;
23 import com.intellij.util.containers.ContainerUtil;
24 import org.apache.http.Header;
25 import org.apache.http.HeaderElement;
26 import org.apache.http.message.BasicHeader;
27 import org.jetbrains.annotations.NotNull;
28 import org.jetbrains.annotations.Nullable;
29 import org.jetbrains.plugins.github.api.GithubConnection.PagedRequest;
30 import org.jetbrains.plugins.github.exceptions.GithubConfusingException;
31 import org.jetbrains.plugins.github.exceptions.GithubJsonException;
32 import org.jetbrains.plugins.github.exceptions.GithubStatusCodeException;
33 import org.jetbrains.plugins.github.util.GithubUtil;
34
35 import java.io.IOException;
36 import java.net.URLEncoder;
37 import java.util.*;
38
39 public class GithubApiUtil {
40   private static final Logger LOG = GithubUtil.LOG;
41
42   public static final String DEFAULT_GITHUB_HOST = "github.com";
43
44   private static final String PER_PAGE = "per_page=100";
45
46   private static final Header ACCEPT_V3_JSON_HTML_MARKUP = new BasicHeader("Accept", "application/vnd.github.v3.html+json");
47   private static final Header ACCEPT_V3_JSON = new BasicHeader("Accept", "application/vnd.github.v3+json");
48
49   @NotNull private static final Gson gson = initGson();
50
51   private static Gson initGson() {
52     GsonBuilder builder = new GsonBuilder();
53     builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
54     builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
55     return builder.create();
56   }
57
58   @NotNull
59   public static <T> T fromJson(@Nullable JsonElement json, @NotNull Class<T> classT) throws IOException {
60     if (json == null) {
61       throw new GithubJsonException("Unexpected empty response");
62     }
63
64     T res;
65     try {
66       //cast as workaround for early java 1.6 bug
67       //noinspection RedundantCast
68       res = (T)gson.fromJson(json, classT);
69     }
70     catch (ClassCastException e) {
71       throw new GithubJsonException("Parse exception while converting JSON to object " + classT.toString(), e);
72     }
73     catch (JsonParseException e) {
74       throw new GithubJsonException("Parse exception while converting JSON to object " + classT.toString(), e);
75     }
76     if (res == null) {
77       throw new GithubJsonException("Empty Json response");
78     }
79     return res;
80   }
81
82   @NotNull
83   public static <Raw extends DataConstructor, Result> Result createDataFromRaw(@NotNull Raw rawObject, @NotNull Class<Result> resultClass)
84     throws GithubJsonException {
85     try {
86       return rawObject.create(resultClass);
87     }
88     catch (Exception e) {
89       throw new GithubJsonException("Json parse error", e);
90     }
91   }
92
93   /*
94    * Operations
95    */
96
97   public static void askForTwoFactorCodeSMS(@NotNull GithubConnection connection) {
98     try {
99       connection.postRequest("/authorizations", null, ACCEPT_V3_JSON);
100     }
101     catch (IOException e) {
102       LOG.info(e);
103     }
104   }
105
106   @NotNull
107   public static Collection<String> getTokenScopes(@NotNull GithubConnection connection) throws IOException {
108     Header[] headers = connection.headRequest("/user", ACCEPT_V3_JSON);
109
110     Header scopesHeader = null;
111     for (Header header : headers) {
112       if (header.getName().equals("X-OAuth-Scopes")) {
113         scopesHeader = header;
114         break;
115       }
116     }
117     if (scopesHeader == null) {
118       throw new GithubConfusingException("No scopes header");
119     }
120
121     Collection<String> scopes = new ArrayList<String>();
122     for (HeaderElement elem : scopesHeader.getElements()) {
123       scopes.add(elem.getName());
124     }
125     return scopes;
126   }
127
128   @NotNull
129   public static String getScopedToken(@NotNull GithubConnection connection, @NotNull Collection<String> scopes, @NotNull String note)
130     throws IOException {
131     try {
132       return getNewScopedToken(connection, scopes, note).getToken();
133     }
134     catch (GithubStatusCodeException e) {
135       if (e.getError() != null && e.getError().containsErrorCode("already_exists")) {
136         // with new API we can't reuse old token, so let's just create new one
137         // we need to change note as well, because it should be unique
138
139         List<GithubAuthorization> tokens = getAllTokens(connection);
140
141         for (int i = 1; i < 100; i++) {
142           final String newNote = note + "_" + i;
143           if (ContainerUtil.find(tokens, new Condition<GithubAuthorization>() {
144             @Override
145             public boolean value(GithubAuthorization authorization) {
146               return newNote.equals(authorization.getNote());
147             }
148           }) == null) {
149             return getNewScopedToken(connection, scopes, newNote).getToken();
150           }
151         }
152       }
153       throw e;
154     }
155   }
156
157   @NotNull
158   private static GithubAuthorization updateTokenScopes(@NotNull GithubConnection connection,
159                                                        @NotNull GithubAuthorization token,
160                                                        @NotNull Collection<String> scopes) throws IOException {
161     try {
162       String path = "/authorizations/" + token.getId();
163
164       GithubAuthorizationUpdateRequest request = new GithubAuthorizationUpdateRequest(new ArrayList<String>(scopes));
165
166       return createDataFromRaw(fromJson(connection.patchRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubAuthorizationRaw.class),
167                                GithubAuthorization.class);
168     }
169     catch (GithubConfusingException e) {
170       e.setDetails("Can't update token: scopes - " + scopes);
171       throw e;
172     }
173   }
174
175   @NotNull
176   private static GithubAuthorization getNewScopedToken(@NotNull GithubConnection connection,
177                                                        @NotNull Collection<String> scopes,
178                                                        @NotNull String note)
179     throws IOException {
180     try {
181       String path = "/authorizations";
182
183       GithubAuthorizationCreateRequest request = new GithubAuthorizationCreateRequest(new ArrayList<String>(scopes), note, null);
184
185       return createDataFromRaw(fromJson(connection.postRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubAuthorizationRaw.class),
186                                GithubAuthorization.class);
187     }
188     catch (GithubConfusingException e) {
189       e.setDetails("Can't create token: scopes - " + scopes + " - note " + note);
190       throw e;
191     }
192   }
193
194   @NotNull
195   private static List<GithubAuthorization> getAllTokens(@NotNull GithubConnection connection) throws IOException {
196     try {
197       String path = "/authorizations";
198
199       PagedRequest<GithubAuthorization> request =
200         new PagedRequest<GithubAuthorization>(path, GithubAuthorization.class, GithubAuthorizationRaw[].class, ACCEPT_V3_JSON);
201
202       return request.getAll(connection);
203     }
204     catch (GithubConfusingException e) {
205       e.setDetails("Can't get available tokens");
206       throw e;
207     }
208   }
209
210   @NotNull
211   public static String getMasterToken(@NotNull GithubConnection connection, @NotNull String note) throws IOException {
212     // "repo" - read/write access to public/private repositories
213     // "gist" - create/delete gists
214     List<String> scopes = Arrays.asList("repo", "gist");
215
216     return getScopedToken(connection, scopes, note);
217   }
218
219   @NotNull
220   public static String getTasksToken(@NotNull GithubConnection connection,
221                                      @NotNull String user,
222                                      @NotNull String repo,
223                                      @NotNull String note)
224     throws IOException {
225     GithubRepo repository = getDetailedRepoInfo(connection, user, repo);
226
227     List<String> scopes = repository.isPrivate() ? Collections.singletonList("repo") : Collections.singletonList("public_repo");
228
229     return getScopedToken(connection, scopes, note);
230   }
231
232   @NotNull
233   public static GithubUser getCurrentUser(@NotNull GithubConnection connection) throws IOException {
234     try {
235       JsonElement result = connection.getRequest("/user", ACCEPT_V3_JSON);
236       return createDataFromRaw(fromJson(result, GithubUserRaw.class), GithubUser.class);
237     }
238     catch (GithubConfusingException e) {
239       e.setDetails("Can't get user info");
240       throw e;
241     }
242   }
243
244   @NotNull
245   public static GithubUserDetailed getCurrentUserDetailed(@NotNull GithubConnection connection) throws IOException {
246     try {
247       JsonElement result = connection.getRequest("/user", ACCEPT_V3_JSON);
248       return createDataFromRaw(fromJson(result, GithubUserRaw.class), GithubUserDetailed.class);
249     }
250     catch (GithubConfusingException e) {
251       e.setDetails("Can't get user info");
252       throw e;
253     }
254   }
255
256   @NotNull
257   public static List<GithubRepo> getUserRepos(@NotNull GithubConnection connection) throws IOException {
258     try {
259       String path = "/user/repos?" + PER_PAGE;
260
261       PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
262
263       return request.getAll(connection);
264     }
265     catch (GithubConfusingException e) {
266       e.setDetails("Can't get user repositories");
267       throw e;
268     }
269   }
270
271   @NotNull
272   public static List<GithubRepo> getUserRepos(@NotNull GithubConnection connection, @NotNull String user) throws IOException {
273     try {
274       String path = "/users/" + user + "/repos?" + PER_PAGE;
275
276       PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
277
278       return request.getAll(connection);
279     }
280     catch (GithubConfusingException e) {
281       e.setDetails("Can't get user repositories: " + user);
282       throw e;
283     }
284   }
285
286   @NotNull
287   public static List<GithubRepo> getAvailableRepos(@NotNull GithubConnection connection) throws IOException {
288     try {
289       List<GithubRepo> repos = new ArrayList<GithubRepo>();
290
291       repos.addAll(getUserRepos(connection));
292
293       // We already can return something useful from getUserRepos, so let's ignore errors.
294       // One of this may not exist in GitHub enterprise
295       try {
296         repos.addAll(getMembershipRepos(connection));
297       }
298       catch (GithubStatusCodeException ignore) {
299       }
300       try {
301         repos.addAll(getWatchedRepos(connection));
302       }
303       catch (GithubStatusCodeException ignore) {
304       }
305
306       return repos;
307     }
308     catch (GithubConfusingException e) {
309       e.setDetails("Can't get available repositories");
310       throw e;
311     }
312   }
313
314   @NotNull
315   public static List<GithubRepoOrg> getMembershipRepos(@NotNull GithubConnection connection) throws IOException {
316     String orgsPath = "/user/orgs?" + PER_PAGE;
317     PagedRequest<GithubOrg> orgsRequest = new PagedRequest<GithubOrg>(orgsPath, GithubOrg.class, GithubOrgRaw[].class);
318
319     List<GithubRepoOrg> repos = new ArrayList<GithubRepoOrg>();
320     for (GithubOrg org : orgsRequest.getAll(connection)) {
321       String path = "/orgs/" + org.getLogin() + "/repos?type=member&" + PER_PAGE;
322       PagedRequest<GithubRepoOrg> request =
323         new PagedRequest<GithubRepoOrg>(path, GithubRepoOrg.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
324       repos.addAll(request.getAll(connection));
325     }
326
327     return repos;
328   }
329
330   @NotNull
331   public static List<GithubRepo> getWatchedRepos(@NotNull GithubConnection connection) throws IOException {
332     String pathWatched = "/user/subscriptions?" + PER_PAGE;
333     PagedRequest<GithubRepo> requestWatched =
334       new PagedRequest<GithubRepo>(pathWatched, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
335     return requestWatched.getAll(connection);
336   }
337
338   @NotNull
339   public static GithubRepoDetailed getDetailedRepoInfo(@NotNull GithubConnection connection, @NotNull String owner, @NotNull String name)
340     throws IOException {
341     try {
342       final String request = "/repos/" + owner + "/" + name;
343
344       JsonElement jsonObject = connection.getRequest(request, ACCEPT_V3_JSON);
345
346       return createDataFromRaw(fromJson(jsonObject, GithubRepoRaw.class), GithubRepoDetailed.class);
347     }
348     catch (GithubConfusingException e) {
349       e.setDetails("Can't get repository info: " + owner + "/" + name);
350       throw e;
351     }
352   }
353
354   public static void deleteGithubRepository(@NotNull GithubConnection connection, @NotNull String username, @NotNull String repo)
355     throws IOException {
356     try {
357       String path = "/repos/" + username + "/" + repo;
358       connection.deleteRequest(path);
359     }
360     catch (GithubConfusingException e) {
361       e.setDetails("Can't delete repository: " + username + "/" + repo);
362       throw e;
363     }
364   }
365
366   public static void deleteGist(@NotNull GithubConnection connection, @NotNull String id) throws IOException {
367     try {
368       String path = "/gists/" + id;
369       connection.deleteRequest(path);
370     }
371     catch (GithubConfusingException e) {
372       e.setDetails("Can't delete gist: id - " + id);
373       throw e;
374     }
375   }
376
377   @NotNull
378   public static GithubGist getGist(@NotNull GithubConnection connection, @NotNull String id) throws IOException {
379     try {
380       String path = "/gists/" + id;
381       JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
382
383       return createDataFromRaw(fromJson(result, GithubGistRaw.class), GithubGist.class);
384     }
385     catch (GithubConfusingException e) {
386       e.setDetails("Can't get gist info: id " + id);
387       throw e;
388     }
389   }
390
391   @NotNull
392   public static GithubGist createGist(@NotNull GithubConnection connection,
393                                       @NotNull List<GithubGist.FileContent> contents,
394                                       @NotNull String description,
395                                       boolean isPrivate) throws IOException {
396     try {
397       String request = gson.toJson(new GithubGistRequest(contents, description, !isPrivate));
398       return createDataFromRaw(fromJson(connection.postRequest("/gists", request, ACCEPT_V3_JSON), GithubGistRaw.class), GithubGist.class);
399     }
400     catch (GithubConfusingException e) {
401       e.setDetails("Can't create gist");
402       throw e;
403     }
404   }
405
406   @NotNull
407   public static List<GithubRepo> getForks(@NotNull GithubConnection connection, @NotNull String owner, @NotNull String name)
408     throws IOException {
409     String path = "/repos/" + owner + "/" + name + "/forks?" + PER_PAGE;
410     PagedRequest<GithubRepo> requestWatched =
411       new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
412     return requestWatched.getAll(connection);
413   }
414
415   @NotNull
416   public static GithubPullRequest createPullRequest(@NotNull GithubConnection connection,
417                                                     @NotNull String user,
418                                                     @NotNull String repo,
419                                                     @NotNull String title,
420                                                     @NotNull String description,
421                                                     @NotNull String head,
422                                                     @NotNull String base) throws IOException {
423     try {
424       String request = gson.toJson(new GithubPullRequestRequest(title, description, head, base));
425       return createDataFromRaw(
426         fromJson(connection.postRequest("/repos/" + user + "/" + repo + "/pulls", request, ACCEPT_V3_JSON), GithubPullRequestRaw.class),
427         GithubPullRequest.class);
428     }
429     catch (GithubConfusingException e) {
430       e.setDetails("Can't create pull request");
431       throw e;
432     }
433   }
434
435   @NotNull
436   public static GithubRepo createRepo(@NotNull GithubConnection connection,
437                                       @NotNull String name,
438                                       @NotNull String description,
439                                       boolean isPrivate)
440     throws IOException {
441     try {
442       String path = "/user/repos";
443
444       GithubRepoRequest request = new GithubRepoRequest(name, description, isPrivate);
445
446       return createDataFromRaw(fromJson(connection.postRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubRepoRaw.class),
447                                GithubRepo.class);
448     }
449     catch (GithubConfusingException e) {
450       e.setDetails("Can't create repository: " + name);
451       throw e;
452     }
453   }
454
455   /*
456    * Open issues only
457    */
458   @NotNull
459   public static List<GithubIssue> getIssuesAssigned(@NotNull GithubConnection connection,
460                                                     @NotNull String user,
461                                                     @NotNull String repo,
462                                                     @Nullable String assigned,
463                                                     int max,
464                                                     boolean withClosed) throws IOException {
465     try {
466       String state = "state=" + (withClosed ? "all" : "open");
467       String path;
468       if (StringUtil.isEmptyOrSpaces(assigned)) {
469         path = "/repos/" + user + "/" + repo + "/issues?" + PER_PAGE + "&" + state;
470       }
471       else {
472         path = "/repos/" + user + "/" + repo + "/issues?assignee=" + assigned + "&" + PER_PAGE + "&" + state;
473       }
474
475       PagedRequest<GithubIssue> request = new PagedRequest<GithubIssue>(path, GithubIssue.class, GithubIssueRaw[].class, ACCEPT_V3_JSON);
476
477       List<GithubIssue> result = new ArrayList<GithubIssue>();
478       while (request.hasNext() && max > result.size()) {
479         result.addAll(request.next(connection));
480       }
481       return result;
482     }
483     catch (GithubConfusingException e) {
484       e.setDetails("Can't get assigned issues: " + user + "/" + repo + " - " + assigned);
485       throw e;
486     }
487   }
488
489   @NotNull
490   /*
491    * All issues - open and closed
492    */
493   public static List<GithubIssue> getIssuesQueried(@NotNull GithubConnection connection,
494                                                    @NotNull String user,
495                                                    @NotNull String repo,
496                                                    @Nullable String query,
497                                                    boolean withClosed) throws IOException {
498     try {
499       String state = withClosed ? "" : " state:open";
500       query = URLEncoder.encode("repo:" + user + "/" + repo + " " + query + state, CharsetToolkit.UTF8);
501       String path = "/search/issues?q=" + query;
502
503       //TODO: Use bodyHtml for issues - GitHub does not support this feature for SearchApi yet
504       JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
505
506       return createDataFromRaw(fromJson(result, GithubIssuesSearchResultRaw.class), GithubIssuesSearchResult.class).getIssues();
507     }
508     catch (GithubConfusingException e) {
509       e.setDetails("Can't get queried issues: " + user + "/" + repo + " - " + query);
510       throw e;
511     }
512   }
513
514   @NotNull
515   public static GithubIssue getIssue(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @NotNull String id)
516     throws IOException {
517     try {
518       String path = "/repos/" + user + "/" + repo + "/issues/" + id;
519
520       JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
521
522       return createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class);
523     }
524     catch (GithubConfusingException e) {
525       e.setDetails("Can't get issue info: " + user + "/" + repo + " - " + id);
526       throw e;
527     }
528   }
529
530   @NotNull
531   public static List<GithubIssueComment> getIssueComments(@NotNull GithubConnection connection,
532                                                           @NotNull String user,
533                                                           @NotNull String repo,
534                                                           long id)
535     throws IOException {
536     try {
537       String path = "/repos/" + user + "/" + repo + "/issues/" + id + "/comments?" + PER_PAGE;
538
539       PagedRequest<GithubIssueComment> request =
540         new PagedRequest<GithubIssueComment>(path, GithubIssueComment.class, GithubIssueCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
541
542       return request.getAll(connection);
543     }
544     catch (GithubConfusingException e) {
545       e.setDetails("Can't get issue comments: " + user + "/" + repo + " - " + id);
546       throw e;
547     }
548   }
549
550   public static void setIssueState(@NotNull GithubConnection connection,
551                                    @NotNull String user,
552                                    @NotNull String repo,
553                                    @NotNull String id,
554                                    boolean open)
555     throws IOException {
556     try {
557       String path = "/repos/" + user + "/" + repo + "/issues/" + id;
558
559       GithubChangeIssueStateRequest request = new GithubChangeIssueStateRequest(open ? "open" : "closed");
560
561       JsonElement result = connection.patchRequest(path, gson.toJson(request), ACCEPT_V3_JSON);
562
563       createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class);
564     }
565     catch (GithubConfusingException e) {
566       e.setDetails("Can't set issue state: " + user + "/" + repo + " - " + id + "@" + (open ? "open" : "closed"));
567       throw e;
568     }
569   }
570
571
572   @NotNull
573   public static GithubCommitDetailed getCommit(@NotNull GithubConnection connection,
574                                                @NotNull String user,
575                                                @NotNull String repo,
576                                                @NotNull String sha) throws IOException {
577     try {
578       String path = "/repos/" + user + "/" + repo + "/commits/" + sha;
579
580       JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
581       return createDataFromRaw(fromJson(result, GithubCommitRaw.class), GithubCommitDetailed.class);
582     }
583     catch (GithubConfusingException e) {
584       e.setDetails("Can't get commit info: " + user + "/" + repo + " - " + sha);
585       throw e;
586     }
587   }
588
589   @NotNull
590   public static List<GithubCommitComment> getCommitComments(@NotNull GithubConnection connection,
591                                                             @NotNull String user,
592                                                             @NotNull String repo,
593                                                             @NotNull String sha) throws IOException {
594     try {
595       String path = "/repos/" + user + "/" + repo + "/commits/" + sha + "/comments";
596
597       PagedRequest<GithubCommitComment> request =
598         new PagedRequest<GithubCommitComment>(path, GithubCommitComment.class, GithubCommitCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
599
600       return request.getAll(connection);
601     }
602     catch (GithubConfusingException e) {
603       e.setDetails("Can't get commit comments: " + user + "/" + repo + " - " + sha);
604       throw e;
605     }
606   }
607
608   @NotNull
609   public static List<GithubCommitComment> getPullRequestComments(@NotNull GithubConnection connection,
610                                                                  @NotNull String user,
611                                                                  @NotNull String repo,
612                                                                  long id) throws IOException {
613     try {
614       String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/comments";
615
616       PagedRequest<GithubCommitComment> request =
617         new PagedRequest<GithubCommitComment>(path, GithubCommitComment.class, GithubCommitCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
618
619       return request.getAll(connection);
620     }
621     catch (GithubConfusingException e) {
622       e.setDetails("Can't get pull request comments: " + user + "/" + repo + " - " + id);
623       throw e;
624     }
625   }
626
627   @NotNull
628   public static GithubPullRequest getPullRequest(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, int id)
629     throws IOException {
630     try {
631       String path = "/repos/" + user + "/" + repo + "/pulls/" + id;
632       return createDataFromRaw(fromJson(connection.getRequest(path, ACCEPT_V3_JSON_HTML_MARKUP), GithubPullRequestRaw.class),
633                                GithubPullRequest.class);
634     }
635     catch (GithubConfusingException e) {
636       e.setDetails("Can't get pull request info: " + user + "/" + repo + " - " + id);
637       throw e;
638     }
639   }
640
641   @NotNull
642   public static List<GithubPullRequest> getPullRequests(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo)
643     throws IOException {
644     try {
645       String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE;
646
647       PagedRequest<GithubPullRequest> request =
648         new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class, GithubPullRequestRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
649
650       return request.getAll(connection);
651     }
652     catch (GithubConfusingException e) {
653       e.setDetails("Can't get pull requests" + user + "/" + repo);
654       throw e;
655     }
656   }
657
658   @NotNull
659   public static PagedRequest<GithubPullRequest> getPullRequests(@NotNull String user, @NotNull String repo) {
660     String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE;
661
662     return new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class, GithubPullRequestRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
663   }
664
665   @NotNull
666   public static List<GithubCommit> getPullRequestCommits(@NotNull GithubConnection connection,
667                                                          @NotNull String user,
668                                                          @NotNull String repo,
669                                                          long id)
670     throws IOException {
671     try {
672       String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/commits?" + PER_PAGE;
673
674       PagedRequest<GithubCommit> request =
675         new PagedRequest<GithubCommit>(path, GithubCommit.class, GithubCommitRaw[].class, ACCEPT_V3_JSON);
676
677       return request.getAll(connection);
678     }
679     catch (GithubConfusingException e) {
680       e.setDetails("Can't get pull request commits: " + user + "/" + repo + " - " + id);
681       throw e;
682     }
683   }
684
685   @NotNull
686   public static List<GithubFile> getPullRequestFiles(@NotNull GithubConnection connection,
687                                                      @NotNull String user,
688                                                      @NotNull String repo,
689                                                      long id)
690     throws IOException {
691     try {
692       String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/files?" + PER_PAGE;
693
694       PagedRequest<GithubFile> request = new PagedRequest<GithubFile>(path, GithubFile.class, GithubFileRaw[].class, ACCEPT_V3_JSON);
695
696       return request.getAll(connection);
697     }
698     catch (GithubConfusingException e) {
699       e.setDetails("Can't get pull request files: " + user + "/" + repo + " - " + id);
700       throw e;
701     }
702   }
703
704   @NotNull
705   public static List<GithubBranch> getRepoBranches(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo)
706     throws IOException {
707     try {
708       String path = "/repos/" + user + "/" + repo + "/branches?" + PER_PAGE;
709
710       PagedRequest<GithubBranch> request =
711         new PagedRequest<GithubBranch>(path, GithubBranch.class, GithubBranchRaw[].class, ACCEPT_V3_JSON);
712
713       return request.getAll(connection);
714     }
715     catch (GithubConfusingException e) {
716       e.setDetails("Can't get repository branches: " + user + "/" + repo);
717       throw e;
718     }
719   }
720
721   @Nullable
722   public static GithubRepo findForkByUser(@NotNull GithubConnection connection,
723                                           @NotNull String user,
724                                           @NotNull String repo,
725                                           @NotNull String forkUser) throws IOException {
726     try {
727       String path = "/repos/" + user + "/" + repo + "/forks?" + PER_PAGE;
728
729       PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
730
731       while (request.hasNext()) {
732         for (GithubRepo fork : request.next(connection)) {
733           if (StringUtil.equalsIgnoreCase(fork.getUserName(), forkUser)) {
734             return fork;
735           }
736         }
737       }
738
739       return null;
740     }
741     catch (GithubConfusingException e) {
742       e.setDetails("Can't find fork by user: " + user + "/" + repo + " - " + forkUser);
743       throw e;
744     }
745   }
746 }