IDEA-131201 github tasks: allow to specify, whether to load all issues or assigned...
[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 assignedUser,
497                                                    @Nullable String query,
498                                                    boolean withClosed) throws IOException {
499     try {
500       String state = withClosed ? "" : " state:open";
501       String assignee = StringUtil.isEmptyOrSpaces(assignedUser) ? "" : " assignee:" + assignedUser;
502       query = URLEncoder.encode("repo:" + user + "/" + repo + state + assignee + " " + query, CharsetToolkit.UTF8);
503       String path = "/search/issues?q=" + query;
504
505       //TODO: Use bodyHtml for issues - GitHub does not support this feature for SearchApi yet
506       JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
507
508       return createDataFromRaw(fromJson(result, GithubIssuesSearchResultRaw.class), GithubIssuesSearchResult.class).getIssues();
509     }
510     catch (GithubConfusingException e) {
511       e.setDetails("Can't get queried issues: " + user + "/" + repo + " - " + query);
512       throw e;
513     }
514   }
515
516   @NotNull
517   public static GithubIssue getIssue(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @NotNull String id)
518     throws IOException {
519     try {
520       String path = "/repos/" + user + "/" + repo + "/issues/" + id;
521
522       JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
523
524       return createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class);
525     }
526     catch (GithubConfusingException e) {
527       e.setDetails("Can't get issue info: " + user + "/" + repo + " - " + id);
528       throw e;
529     }
530   }
531
532   @NotNull
533   public static List<GithubIssueComment> getIssueComments(@NotNull GithubConnection connection,
534                                                           @NotNull String user,
535                                                           @NotNull String repo,
536                                                           long id)
537     throws IOException {
538     try {
539       String path = "/repos/" + user + "/" + repo + "/issues/" + id + "/comments?" + PER_PAGE;
540
541       PagedRequest<GithubIssueComment> request =
542         new PagedRequest<GithubIssueComment>(path, GithubIssueComment.class, GithubIssueCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
543
544       return request.getAll(connection);
545     }
546     catch (GithubConfusingException e) {
547       e.setDetails("Can't get issue comments: " + user + "/" + repo + " - " + id);
548       throw e;
549     }
550   }
551
552   public static void setIssueState(@NotNull GithubConnection connection,
553                                    @NotNull String user,
554                                    @NotNull String repo,
555                                    @NotNull String id,
556                                    boolean open)
557     throws IOException {
558     try {
559       String path = "/repos/" + user + "/" + repo + "/issues/" + id;
560
561       GithubChangeIssueStateRequest request = new GithubChangeIssueStateRequest(open ? "open" : "closed");
562
563       JsonElement result = connection.patchRequest(path, gson.toJson(request), ACCEPT_V3_JSON);
564
565       createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class);
566     }
567     catch (GithubConfusingException e) {
568       e.setDetails("Can't set issue state: " + user + "/" + repo + " - " + id + "@" + (open ? "open" : "closed"));
569       throw e;
570     }
571   }
572
573
574   @NotNull
575   public static GithubCommitDetailed getCommit(@NotNull GithubConnection connection,
576                                                @NotNull String user,
577                                                @NotNull String repo,
578                                                @NotNull String sha) throws IOException {
579     try {
580       String path = "/repos/" + user + "/" + repo + "/commits/" + sha;
581
582       JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
583       return createDataFromRaw(fromJson(result, GithubCommitRaw.class), GithubCommitDetailed.class);
584     }
585     catch (GithubConfusingException e) {
586       e.setDetails("Can't get commit info: " + user + "/" + repo + " - " + sha);
587       throw e;
588     }
589   }
590
591   @NotNull
592   public static List<GithubCommitComment> getCommitComments(@NotNull GithubConnection connection,
593                                                             @NotNull String user,
594                                                             @NotNull String repo,
595                                                             @NotNull String sha) throws IOException {
596     try {
597       String path = "/repos/" + user + "/" + repo + "/commits/" + sha + "/comments";
598
599       PagedRequest<GithubCommitComment> request =
600         new PagedRequest<GithubCommitComment>(path, GithubCommitComment.class, GithubCommitCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
601
602       return request.getAll(connection);
603     }
604     catch (GithubConfusingException e) {
605       e.setDetails("Can't get commit comments: " + user + "/" + repo + " - " + sha);
606       throw e;
607     }
608   }
609
610   @NotNull
611   public static List<GithubCommitComment> getPullRequestComments(@NotNull GithubConnection connection,
612                                                                  @NotNull String user,
613                                                                  @NotNull String repo,
614                                                                  long id) throws IOException {
615     try {
616       String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/comments";
617
618       PagedRequest<GithubCommitComment> request =
619         new PagedRequest<GithubCommitComment>(path, GithubCommitComment.class, GithubCommitCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
620
621       return request.getAll(connection);
622     }
623     catch (GithubConfusingException e) {
624       e.setDetails("Can't get pull request comments: " + user + "/" + repo + " - " + id);
625       throw e;
626     }
627   }
628
629   @NotNull
630   public static GithubPullRequest getPullRequest(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, int id)
631     throws IOException {
632     try {
633       String path = "/repos/" + user + "/" + repo + "/pulls/" + id;
634       return createDataFromRaw(fromJson(connection.getRequest(path, ACCEPT_V3_JSON_HTML_MARKUP), GithubPullRequestRaw.class),
635                                GithubPullRequest.class);
636     }
637     catch (GithubConfusingException e) {
638       e.setDetails("Can't get pull request info: " + user + "/" + repo + " - " + id);
639       throw e;
640     }
641   }
642
643   @NotNull
644   public static List<GithubPullRequest> getPullRequests(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo)
645     throws IOException {
646     try {
647       String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE;
648
649       PagedRequest<GithubPullRequest> request =
650         new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class, GithubPullRequestRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
651
652       return request.getAll(connection);
653     }
654     catch (GithubConfusingException e) {
655       e.setDetails("Can't get pull requests" + user + "/" + repo);
656       throw e;
657     }
658   }
659
660   @NotNull
661   public static PagedRequest<GithubPullRequest> getPullRequests(@NotNull String user, @NotNull String repo) {
662     String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE;
663
664     return new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class, GithubPullRequestRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
665   }
666
667   @NotNull
668   public static List<GithubCommit> getPullRequestCommits(@NotNull GithubConnection connection,
669                                                          @NotNull String user,
670                                                          @NotNull String repo,
671                                                          long id)
672     throws IOException {
673     try {
674       String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/commits?" + PER_PAGE;
675
676       PagedRequest<GithubCommit> request =
677         new PagedRequest<GithubCommit>(path, GithubCommit.class, GithubCommitRaw[].class, ACCEPT_V3_JSON);
678
679       return request.getAll(connection);
680     }
681     catch (GithubConfusingException e) {
682       e.setDetails("Can't get pull request commits: " + user + "/" + repo + " - " + id);
683       throw e;
684     }
685   }
686
687   @NotNull
688   public static List<GithubFile> getPullRequestFiles(@NotNull GithubConnection connection,
689                                                      @NotNull String user,
690                                                      @NotNull String repo,
691                                                      long id)
692     throws IOException {
693     try {
694       String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/files?" + PER_PAGE;
695
696       PagedRequest<GithubFile> request = new PagedRequest<GithubFile>(path, GithubFile.class, GithubFileRaw[].class, ACCEPT_V3_JSON);
697
698       return request.getAll(connection);
699     }
700     catch (GithubConfusingException e) {
701       e.setDetails("Can't get pull request files: " + user + "/" + repo + " - " + id);
702       throw e;
703     }
704   }
705
706   @NotNull
707   public static List<GithubBranch> getRepoBranches(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo)
708     throws IOException {
709     try {
710       String path = "/repos/" + user + "/" + repo + "/branches?" + PER_PAGE;
711
712       PagedRequest<GithubBranch> request =
713         new PagedRequest<GithubBranch>(path, GithubBranch.class, GithubBranchRaw[].class, ACCEPT_V3_JSON);
714
715       return request.getAll(connection);
716     }
717     catch (GithubConfusingException e) {
718       e.setDetails("Can't get repository branches: " + user + "/" + repo);
719       throw e;
720     }
721   }
722
723   @Nullable
724   public static GithubRepo findForkByUser(@NotNull GithubConnection connection,
725                                           @NotNull String user,
726                                           @NotNull String repo,
727                                           @NotNull String forkUser) throws IOException {
728     try {
729       String path = "/repos/" + user + "/" + repo + "/forks?" + PER_PAGE;
730
731       PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
732
733       while (request.hasNext()) {
734         for (GithubRepo fork : request.next(connection)) {
735           if (StringUtil.equalsIgnoreCase(fork.getUserName(), forkUser)) {
736             return fork;
737           }
738         }
739       }
740
741       return null;
742     }
743     catch (GithubConfusingException e) {
744       e.setDetails("Can't find fork by user: " + user + "/" + repo + " - " + forkUser);
745       throw e;
746     }
747   }
748 }