2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org.jetbrains.plugins.github.api;
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;
35 import java.io.IOException;
36 import java.net.URLEncoder;
39 public class GithubApiUtil {
40 private static final Logger LOG = GithubUtil.LOG;
42 public static final String DEFAULT_GITHUB_HOST = "github.com";
44 private static final String PER_PAGE = "per_page=100";
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");
49 @NotNull private static final Gson gson = initGson();
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();
59 public static <T> T fromJson(@Nullable JsonElement json, @NotNull Class<T> classT) throws IOException {
61 throw new GithubJsonException("Unexpected empty response");
66 //cast as workaround for early java 1.6 bug
67 //noinspection RedundantCast
68 res = (T)gson.fromJson(json, classT);
70 catch (ClassCastException e) {
71 throw new GithubJsonException("Parse exception while converting JSON to object " + classT.toString(), e);
73 catch (JsonParseException e) {
74 throw new GithubJsonException("Parse exception while converting JSON to object " + classT.toString(), e);
77 throw new GithubJsonException("Empty Json response");
83 public static <Raw extends DataConstructor, Result> Result createDataFromRaw(@NotNull Raw rawObject, @NotNull Class<Result> resultClass)
84 throws GithubJsonException {
86 return rawObject.create(resultClass);
89 throw new GithubJsonException("Json parse error", e);
97 public static void askForTwoFactorCodeSMS(@NotNull GithubConnection connection) {
99 connection.postRequest("/authorizations", null, ACCEPT_V3_JSON);
101 catch (IOException e) {
107 public static Collection<String> getTokenScopes(@NotNull GithubConnection connection) throws IOException {
108 Header[] headers = connection.headRequest("/user", ACCEPT_V3_JSON);
110 Header scopesHeader = null;
111 for (Header header : headers) {
112 if (header.getName().equals("X-OAuth-Scopes")) {
113 scopesHeader = header;
117 if (scopesHeader == null) {
118 throw new GithubConfusingException("No scopes header");
121 Collection<String> scopes = new ArrayList<String>();
122 for (HeaderElement elem : scopesHeader.getElements()) {
123 scopes.add(elem.getName());
129 public static String getScopedToken(@NotNull GithubConnection connection, @NotNull Collection<String> scopes, @NotNull String note)
132 return getNewScopedToken(connection, scopes, note).getToken();
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
139 List<GithubAuthorization> tokens = getAllTokens(connection);
141 for (int i = 1; i < 100; i++) {
142 final String newNote = note + "_" + i;
143 if (ContainerUtil.find(tokens, new Condition<GithubAuthorization>() {
145 public boolean value(GithubAuthorization authorization) {
146 return newNote.equals(authorization.getNote());
149 return getNewScopedToken(connection, scopes, newNote).getToken();
158 private static GithubAuthorization updateTokenScopes(@NotNull GithubConnection connection,
159 @NotNull GithubAuthorization token,
160 @NotNull Collection<String> scopes) throws IOException {
162 String path = "/authorizations/" + token.getId();
164 GithubAuthorizationUpdateRequest request = new GithubAuthorizationUpdateRequest(new ArrayList<String>(scopes));
166 return createDataFromRaw(fromJson(connection.patchRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubAuthorizationRaw.class),
167 GithubAuthorization.class);
169 catch (GithubConfusingException e) {
170 e.setDetails("Can't update token: scopes - " + scopes);
176 private static GithubAuthorization getNewScopedToken(@NotNull GithubConnection connection,
177 @NotNull Collection<String> scopes,
178 @NotNull String note)
181 String path = "/authorizations";
183 GithubAuthorizationCreateRequest request = new GithubAuthorizationCreateRequest(new ArrayList<String>(scopes), note, null);
185 return createDataFromRaw(fromJson(connection.postRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubAuthorizationRaw.class),
186 GithubAuthorization.class);
188 catch (GithubConfusingException e) {
189 e.setDetails("Can't create token: scopes - " + scopes + " - note " + note);
195 private static List<GithubAuthorization> getAllTokens(@NotNull GithubConnection connection) throws IOException {
197 String path = "/authorizations";
199 PagedRequest<GithubAuthorization> request =
200 new PagedRequest<GithubAuthorization>(path, GithubAuthorization.class, GithubAuthorizationRaw[].class, ACCEPT_V3_JSON);
202 return request.getAll(connection);
204 catch (GithubConfusingException e) {
205 e.setDetails("Can't get available tokens");
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");
216 return getScopedToken(connection, scopes, note);
220 public static String getTasksToken(@NotNull GithubConnection connection,
221 @NotNull String user,
222 @NotNull String repo,
223 @NotNull String note)
225 GithubRepo repository = getDetailedRepoInfo(connection, user, repo);
227 List<String> scopes = repository.isPrivate() ? Collections.singletonList("repo") : Collections.singletonList("public_repo");
229 return getScopedToken(connection, scopes, note);
233 public static GithubUser getCurrentUser(@NotNull GithubConnection connection) throws IOException {
235 JsonElement result = connection.getRequest("/user", ACCEPT_V3_JSON);
236 return createDataFromRaw(fromJson(result, GithubUserRaw.class), GithubUser.class);
238 catch (GithubConfusingException e) {
239 e.setDetails("Can't get user info");
245 public static GithubUserDetailed getCurrentUserDetailed(@NotNull GithubConnection connection) throws IOException {
247 JsonElement result = connection.getRequest("/user", ACCEPT_V3_JSON);
248 return createDataFromRaw(fromJson(result, GithubUserRaw.class), GithubUserDetailed.class);
250 catch (GithubConfusingException e) {
251 e.setDetails("Can't get user info");
257 public static List<GithubRepo> getUserRepos(@NotNull GithubConnection connection) throws IOException {
259 String path = "/user/repos?" + PER_PAGE;
261 PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
263 return request.getAll(connection);
265 catch (GithubConfusingException e) {
266 e.setDetails("Can't get user repositories");
272 public static List<GithubRepo> getUserRepos(@NotNull GithubConnection connection, @NotNull String user) throws IOException {
274 String path = "/users/" + user + "/repos?" + PER_PAGE;
276 PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
278 return request.getAll(connection);
280 catch (GithubConfusingException e) {
281 e.setDetails("Can't get user repositories: " + user);
287 public static List<GithubRepo> getAvailableRepos(@NotNull GithubConnection connection) throws IOException {
289 List<GithubRepo> repos = new ArrayList<GithubRepo>();
291 repos.addAll(getUserRepos(connection));
293 // We already can return something useful from getUserRepos, so let's ignore errors.
294 // One of this may not exist in GitHub enterprise
296 repos.addAll(getMembershipRepos(connection));
298 catch (GithubStatusCodeException ignore) {
301 repos.addAll(getWatchedRepos(connection));
303 catch (GithubStatusCodeException ignore) {
308 catch (GithubConfusingException e) {
309 e.setDetails("Can't get available repositories");
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);
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));
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);
339 public static GithubRepoDetailed getDetailedRepoInfo(@NotNull GithubConnection connection, @NotNull String owner, @NotNull String name)
342 final String request = "/repos/" + owner + "/" + name;
344 JsonElement jsonObject = connection.getRequest(request, ACCEPT_V3_JSON);
346 return createDataFromRaw(fromJson(jsonObject, GithubRepoRaw.class), GithubRepoDetailed.class);
348 catch (GithubConfusingException e) {
349 e.setDetails("Can't get repository info: " + owner + "/" + name);
354 public static void deleteGithubRepository(@NotNull GithubConnection connection, @NotNull String username, @NotNull String repo)
357 String path = "/repos/" + username + "/" + repo;
358 connection.deleteRequest(path);
360 catch (GithubConfusingException e) {
361 e.setDetails("Can't delete repository: " + username + "/" + repo);
366 public static void deleteGist(@NotNull GithubConnection connection, @NotNull String id) throws IOException {
368 String path = "/gists/" + id;
369 connection.deleteRequest(path);
371 catch (GithubConfusingException e) {
372 e.setDetails("Can't delete gist: id - " + id);
378 public static GithubGist getGist(@NotNull GithubConnection connection, @NotNull String id) throws IOException {
380 String path = "/gists/" + id;
381 JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
383 return createDataFromRaw(fromJson(result, GithubGistRaw.class), GithubGist.class);
385 catch (GithubConfusingException e) {
386 e.setDetails("Can't get gist info: id " + id);
392 public static GithubGist createGist(@NotNull GithubConnection connection,
393 @NotNull List<GithubGist.FileContent> contents,
394 @NotNull String description,
395 boolean isPrivate) throws IOException {
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);
400 catch (GithubConfusingException e) {
401 e.setDetails("Can't create gist");
407 public static List<GithubRepo> getForks(@NotNull GithubConnection connection, @NotNull String owner, @NotNull String name)
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);
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 {
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);
429 catch (GithubConfusingException e) {
430 e.setDetails("Can't create pull request");
436 public static GithubRepo createRepo(@NotNull GithubConnection connection,
437 @NotNull String name,
438 @NotNull String description,
442 String path = "/user/repos";
444 GithubRepoRequest request = new GithubRepoRequest(name, description, isPrivate);
446 return createDataFromRaw(fromJson(connection.postRequest(path, gson.toJson(request), ACCEPT_V3_JSON), GithubRepoRaw.class),
449 catch (GithubConfusingException e) {
450 e.setDetails("Can't create repository: " + name);
459 public static List<GithubIssue> getIssuesAssigned(@NotNull GithubConnection connection,
460 @NotNull String user,
461 @NotNull String repo,
462 @Nullable String assigned,
464 boolean withClosed) throws IOException {
466 String state = "state=" + (withClosed ? "all" : "open");
468 if (StringUtil.isEmptyOrSpaces(assigned)) {
469 path = "/repos/" + user + "/" + repo + "/issues?" + PER_PAGE + "&" + state;
472 path = "/repos/" + user + "/" + repo + "/issues?assignee=" + assigned + "&" + PER_PAGE + "&" + state;
475 PagedRequest<GithubIssue> request = new PagedRequest<GithubIssue>(path, GithubIssue.class, GithubIssueRaw[].class, ACCEPT_V3_JSON);
477 List<GithubIssue> result = new ArrayList<GithubIssue>();
478 while (request.hasNext() && max > result.size()) {
479 result.addAll(request.next(connection));
483 catch (GithubConfusingException e) {
484 e.setDetails("Can't get assigned issues: " + user + "/" + repo + " - " + assigned);
491 * All issues - open and closed
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 {
499 String state = withClosed ? "" : " state:open";
500 query = URLEncoder.encode("repo:" + user + "/" + repo + " " + query + state, CharsetToolkit.UTF8);
501 String path = "/search/issues?q=" + query;
503 //TODO: Use bodyHtml for issues - GitHub does not support this feature for SearchApi yet
504 JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
506 return createDataFromRaw(fromJson(result, GithubIssuesSearchResultRaw.class), GithubIssuesSearchResult.class).getIssues();
508 catch (GithubConfusingException e) {
509 e.setDetails("Can't get queried issues: " + user + "/" + repo + " - " + query);
515 public static GithubIssue getIssue(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, @NotNull String id)
518 String path = "/repos/" + user + "/" + repo + "/issues/" + id;
520 JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
522 return createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class);
524 catch (GithubConfusingException e) {
525 e.setDetails("Can't get issue info: " + user + "/" + repo + " - " + id);
531 public static List<GithubIssueComment> getIssueComments(@NotNull GithubConnection connection,
532 @NotNull String user,
533 @NotNull String repo,
537 String path = "/repos/" + user + "/" + repo + "/issues/" + id + "/comments?" + PER_PAGE;
539 PagedRequest<GithubIssueComment> request =
540 new PagedRequest<GithubIssueComment>(path, GithubIssueComment.class, GithubIssueCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
542 return request.getAll(connection);
544 catch (GithubConfusingException e) {
545 e.setDetails("Can't get issue comments: " + user + "/" + repo + " - " + id);
550 public static void setIssueState(@NotNull GithubConnection connection,
551 @NotNull String user,
552 @NotNull String repo,
557 String path = "/repos/" + user + "/" + repo + "/issues/" + id;
559 GithubChangeIssueStateRequest request = new GithubChangeIssueStateRequest(open ? "open" : "closed");
561 JsonElement result = connection.patchRequest(path, gson.toJson(request), ACCEPT_V3_JSON);
563 createDataFromRaw(fromJson(result, GithubIssueRaw.class), GithubIssue.class);
565 catch (GithubConfusingException e) {
566 e.setDetails("Can't set issue state: " + user + "/" + repo + " - " + id + "@" + (open ? "open" : "closed"));
573 public static GithubCommitDetailed getCommit(@NotNull GithubConnection connection,
574 @NotNull String user,
575 @NotNull String repo,
576 @NotNull String sha) throws IOException {
578 String path = "/repos/" + user + "/" + repo + "/commits/" + sha;
580 JsonElement result = connection.getRequest(path, ACCEPT_V3_JSON);
581 return createDataFromRaw(fromJson(result, GithubCommitRaw.class), GithubCommitDetailed.class);
583 catch (GithubConfusingException e) {
584 e.setDetails("Can't get commit info: " + user + "/" + repo + " - " + sha);
590 public static List<GithubCommitComment> getCommitComments(@NotNull GithubConnection connection,
591 @NotNull String user,
592 @NotNull String repo,
593 @NotNull String sha) throws IOException {
595 String path = "/repos/" + user + "/" + repo + "/commits/" + sha + "/comments";
597 PagedRequest<GithubCommitComment> request =
598 new PagedRequest<GithubCommitComment>(path, GithubCommitComment.class, GithubCommitCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
600 return request.getAll(connection);
602 catch (GithubConfusingException e) {
603 e.setDetails("Can't get commit comments: " + user + "/" + repo + " - " + sha);
609 public static List<GithubCommitComment> getPullRequestComments(@NotNull GithubConnection connection,
610 @NotNull String user,
611 @NotNull String repo,
612 long id) throws IOException {
614 String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/comments";
616 PagedRequest<GithubCommitComment> request =
617 new PagedRequest<GithubCommitComment>(path, GithubCommitComment.class, GithubCommitCommentRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
619 return request.getAll(connection);
621 catch (GithubConfusingException e) {
622 e.setDetails("Can't get pull request comments: " + user + "/" + repo + " - " + id);
628 public static GithubPullRequest getPullRequest(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo, int id)
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);
635 catch (GithubConfusingException e) {
636 e.setDetails("Can't get pull request info: " + user + "/" + repo + " - " + id);
642 public static List<GithubPullRequest> getPullRequests(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo)
645 String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE;
647 PagedRequest<GithubPullRequest> request =
648 new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class, GithubPullRequestRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
650 return request.getAll(connection);
652 catch (GithubConfusingException e) {
653 e.setDetails("Can't get pull requests" + user + "/" + repo);
659 public static PagedRequest<GithubPullRequest> getPullRequests(@NotNull String user, @NotNull String repo) {
660 String path = "/repos/" + user + "/" + repo + "/pulls?" + PER_PAGE;
662 return new PagedRequest<GithubPullRequest>(path, GithubPullRequest.class, GithubPullRequestRaw[].class, ACCEPT_V3_JSON_HTML_MARKUP);
666 public static List<GithubCommit> getPullRequestCommits(@NotNull GithubConnection connection,
667 @NotNull String user,
668 @NotNull String repo,
672 String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/commits?" + PER_PAGE;
674 PagedRequest<GithubCommit> request =
675 new PagedRequest<GithubCommit>(path, GithubCommit.class, GithubCommitRaw[].class, ACCEPT_V3_JSON);
677 return request.getAll(connection);
679 catch (GithubConfusingException e) {
680 e.setDetails("Can't get pull request commits: " + user + "/" + repo + " - " + id);
686 public static List<GithubFile> getPullRequestFiles(@NotNull GithubConnection connection,
687 @NotNull String user,
688 @NotNull String repo,
692 String path = "/repos/" + user + "/" + repo + "/pulls/" + id + "/files?" + PER_PAGE;
694 PagedRequest<GithubFile> request = new PagedRequest<GithubFile>(path, GithubFile.class, GithubFileRaw[].class, ACCEPT_V3_JSON);
696 return request.getAll(connection);
698 catch (GithubConfusingException e) {
699 e.setDetails("Can't get pull request files: " + user + "/" + repo + " - " + id);
705 public static List<GithubBranch> getRepoBranches(@NotNull GithubConnection connection, @NotNull String user, @NotNull String repo)
708 String path = "/repos/" + user + "/" + repo + "/branches?" + PER_PAGE;
710 PagedRequest<GithubBranch> request =
711 new PagedRequest<GithubBranch>(path, GithubBranch.class, GithubBranchRaw[].class, ACCEPT_V3_JSON);
713 return request.getAll(connection);
715 catch (GithubConfusingException e) {
716 e.setDetails("Can't get repository branches: " + user + "/" + repo);
722 public static GithubRepo findForkByUser(@NotNull GithubConnection connection,
723 @NotNull String user,
724 @NotNull String repo,
725 @NotNull String forkUser) throws IOException {
727 String path = "/repos/" + user + "/" + repo + "/forks?" + PER_PAGE;
729 PagedRequest<GithubRepo> request = new PagedRequest<GithubRepo>(path, GithubRepo.class, GithubRepoRaw[].class, ACCEPT_V3_JSON);
731 while (request.hasNext()) {
732 for (GithubRepo fork : request.next(connection)) {
733 if (StringUtil.equalsIgnoreCase(fork.getUserName(), forkUser)) {
741 catch (GithubConfusingException e) {
742 e.setDetails("Can't find fork by user: " + user + "/" + repo + " - " + forkUser);