Github: allow to change issue state
[idea/community.git] / plugins / github / src / org / jetbrains / plugins / github / tasks / GithubRepository.java
1 package org.jetbrains.plugins.github.tasks;
2
3 import com.intellij.openapi.diagnostic.Logger;
4 import com.intellij.openapi.progress.ProgressIndicator;
5 import com.intellij.openapi.util.Comparing;
6 import com.intellij.openapi.util.PasswordUtil;
7 import com.intellij.openapi.util.text.StringUtil;
8 import com.intellij.tasks.*;
9 import com.intellij.tasks.impl.BaseRepository;
10 import com.intellij.tasks.impl.BaseRepositoryImpl;
11 import com.intellij.util.Function;
12 import com.intellij.util.containers.ContainerUtil;
13 import com.intellij.util.xmlb.annotations.Tag;
14 import com.intellij.util.xmlb.annotations.Transient;
15 import icons.TasksIcons;
16 import org.jetbrains.annotations.NotNull;
17 import org.jetbrains.annotations.Nullable;
18 import org.jetbrains.plugins.github.api.GithubApiUtil;
19 import org.jetbrains.plugins.github.api.GithubConnection;
20 import org.jetbrains.plugins.github.api.GithubIssue;
21 import org.jetbrains.plugins.github.api.GithubIssueComment;
22 import org.jetbrains.plugins.github.exceptions.*;
23 import org.jetbrains.plugins.github.util.GithubAuthData;
24 import org.jetbrains.plugins.github.util.GithubUtil;
25
26 import javax.swing.*;
27 import java.util.Date;
28 import java.util.List;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31
32 /**
33  * @author Dennis.Ushakov
34  */
35 @Tag("GitHub")
36 public class GithubRepository extends BaseRepositoryImpl {
37   private static final Logger LOG = GithubUtil.LOG;
38
39   private Pattern myPattern = Pattern.compile("($^)");
40   @NotNull private String myRepoAuthor = "";
41   @NotNull private String myRepoName = "";
42   @NotNull private String myUser = "";
43   @NotNull private String myToken = "";
44
45   @SuppressWarnings({"UnusedDeclaration"})
46   public GithubRepository() {
47   }
48
49   public GithubRepository(GithubRepository other) {
50     super(other);
51     setRepoName(other.myRepoName);
52     setRepoAuthor(other.myRepoAuthor);
53     setToken(other.myToken);
54   }
55
56   public GithubRepository(GithubRepositoryType type) {
57     super(type);
58     setUrl(GithubApiUtil.DEFAULT_GITHUB_HOST);
59   }
60
61   @NotNull
62   @Override
63   public CancellableConnection createCancellableConnection() {
64     return new CancellableConnection() {
65       private final GithubConnection myConnection = new GithubConnection(getAuthData(), false);
66
67       @Override
68       protected void doTest() throws Exception {
69         try {
70           GithubApiUtil.getIssuesQueried(myConnection, getRepoAuthor(), getRepoName(), "", false);
71         }
72         catch (GithubOperationCanceledException ignore) {
73         }
74       }
75
76       @Override
77       public void cancel() {
78         myConnection.abort();
79       }
80     };
81   }
82
83   @Override
84   public boolean isConfigured() {
85     return super.isConfigured() &&
86            !StringUtil.isEmptyOrSpaces(getRepoAuthor()) &&
87            !StringUtil.isEmptyOrSpaces(getRepoName()) &&
88            !StringUtil.isEmptyOrSpaces(getToken());
89   }
90
91   @Override
92   public String getPresentableName() {
93     final String name = super.getPresentableName();
94     return name +
95            (!StringUtil.isEmpty(getRepoAuthor()) ? "/" + getRepoAuthor() : "") +
96            (!StringUtil.isEmpty(getRepoName()) ? "/" + getRepoName() : "");
97   }
98
99   @Override
100   public Task[] getIssues(@Nullable String query, int offset, int limit, boolean withClosed) throws Exception {
101     try {
102       return getIssues(query, offset + limit, withClosed);
103     }
104     catch (GithubRateLimitExceededException e) {
105       return new Task[0];
106     }
107     catch (GithubAuthenticationException e) {
108       throw new Exception(e.getMessage(), e); // Wrap to show error message
109     }
110     catch (GithubStatusCodeException e) {
111       throw new Exception(e.getMessage(), e);
112     }
113     catch (GithubJsonException e) {
114       throw new Exception("Bad response format", e);
115     }
116   }
117
118   @Override
119   public Task[] getIssues(@Nullable String query, int offset, int limit, boolean withClosed, @NotNull ProgressIndicator cancelled)
120     throws Exception {
121     return getIssues(query, offset, limit, withClosed);
122   }
123
124   @NotNull
125   private Task[] getIssues(@Nullable String query, int max, boolean withClosed) throws Exception {
126     GithubConnection connection = getConnection();
127
128     try {
129       List<GithubIssue> issues;
130       if (StringUtil.isEmptyOrSpaces(query)) {
131         if (StringUtil.isEmptyOrSpaces(myUser)) {
132           myUser = GithubApiUtil.getCurrentUser(connection).getLogin();
133         }
134         issues = GithubApiUtil.getIssuesAssigned(connection, getRepoAuthor(), getRepoName(), myUser, max, withClosed);
135       }
136       else {
137         issues = GithubApiUtil.getIssuesQueried(connection, getRepoAuthor(), getRepoName(), query, withClosed);
138       }
139
140       return ContainerUtil.map2Array(issues, Task.class, new Function<GithubIssue, Task>() {
141         @Override
142         public Task fun(GithubIssue issue) {
143           return createTask(issue);
144         }
145       });
146     }
147     finally {
148       connection.close();
149     }
150   }
151
152   @NotNull
153   private Task createTask(final GithubIssue issue) {
154     return new Task() {
155       @NotNull String myRepoName = getRepoName();
156
157       @Override
158       public boolean isIssue() {
159         return true;
160       }
161
162       @Override
163       public String getIssueUrl() {
164         return issue.getHtmlUrl();
165       }
166
167       @NotNull
168       @Override
169       public String getId() {
170         return myRepoName + "-" + issue.getNumber();
171       }
172
173       @NotNull
174       @Override
175       public String getSummary() {
176         return issue.getTitle();
177       }
178
179       public String getDescription() {
180         return issue.getBody();
181       }
182
183       @NotNull
184       @Override
185       public Comment[] getComments() {
186         try {
187           return fetchComments(issue.getNumber());
188         }
189         catch (Exception e) {
190           LOG.warn("Error fetching comments for " + issue.getNumber(), e);
191           return Comment.EMPTY_ARRAY;
192         }
193       }
194
195       @NotNull
196       @Override
197       public Icon getIcon() {
198         return TasksIcons.Github;
199       }
200
201       @NotNull
202       @Override
203       public TaskType getType() {
204         return TaskType.BUG;
205       }
206
207       @Override
208       public Date getUpdated() {
209         return issue.getUpdatedAt();
210       }
211
212       @Override
213       public Date getCreated() {
214         return issue.getCreatedAt();
215       }
216
217       @Override
218       public boolean isClosed() {
219         return !"open".equals(issue.getState());
220       }
221
222       @Override
223       public TaskRepository getRepository() {
224         return GithubRepository.this;
225       }
226
227       @Override
228       public String getPresentableName() {
229         return getId() + ": " + getSummary();
230       }
231     };
232   }
233
234   private Comment[] fetchComments(final long id) throws Exception {
235     GithubConnection connection = getConnection();
236     try {
237       List<GithubIssueComment> result = GithubApiUtil.getIssueComments(connection, getRepoAuthor(), getRepoName(), id);
238
239       return ContainerUtil.map2Array(result, Comment.class, new Function<GithubIssueComment, Comment>() {
240         @Override
241         public Comment fun(GithubIssueComment comment) {
242           return new GithubComment(comment.getCreatedAt(), comment.getUser().getLogin(), comment.getBodyHtml(),
243                                    comment.getUser().getGravatarId(),
244                                    comment.getUser().getHtmlUrl());
245         }
246       });
247     }
248     finally {
249       connection.close();
250     }
251   }
252
253   @Nullable
254   public String extractId(@NotNull String taskName) {
255     Matcher matcher = myPattern.matcher(taskName);
256     return matcher.find() ? matcher.group(1) : null;
257   }
258
259   @Nullable
260   @Override
261   public Task findTask(@NotNull String id) throws Exception {
262     GithubConnection connection = getConnection();
263     try {
264       return createTask(GithubApiUtil.getIssue(connection, getRepoAuthor(), getRepoName(), id));
265     }
266     finally {
267       connection.close();
268     }
269   }
270
271   @Override
272   public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception {
273     GithubConnection connection = getConnection();
274     try {
275       boolean isOpen;
276       switch (state) {
277         case OPEN:
278           isOpen = true;
279           break;
280         case RESOLVED:
281           isOpen = false;
282           break;
283         default:
284           throw new IllegalStateException("Unknown state: " + state);
285       }
286       GithubApiUtil.setIssueState(connection, getRepoAuthor(), getRepoName(), task.getNumber(), isOpen);
287     }
288     finally {
289       connection.close();
290     }
291   }
292
293   @NotNull
294   @Override
295   public BaseRepository clone() {
296     return new GithubRepository(this);
297   }
298
299   @NotNull
300   public String getRepoName() {
301     return myRepoName;
302   }
303
304   public void setRepoName(@NotNull String repoName) {
305     myRepoName = repoName;
306     myPattern = Pattern.compile("(" + StringUtil.escapeToRegexp(repoName) + "\\-\\d+):\\s+");
307   }
308
309   @NotNull
310   public String getRepoAuthor() {
311     return myRepoAuthor;
312   }
313
314   public void setRepoAuthor(@NotNull String repoAuthor) {
315     myRepoAuthor = repoAuthor;
316   }
317
318   @NotNull
319   public String getUser() {
320     return myUser;
321   }
322
323   public void setUser(@NotNull String user) {
324     myUser = user;
325   }
326
327   @Transient
328   @NotNull
329   public String getToken() {
330     return myToken;
331   }
332
333   public void setToken(@NotNull String token) {
334     myToken = token;
335     setUser("");
336   }
337
338   @Tag("token")
339   public String getEncodedToken() {
340     return PasswordUtil.encodePassword(getToken());
341   }
342
343   public void setEncodedToken(String password) {
344     try {
345       setToken(PasswordUtil.decodePassword(password));
346     }
347     catch (NumberFormatException e) {
348       LOG.warn("Can't decode token", e);
349     }
350   }
351
352   private GithubAuthData getAuthData() {
353     return GithubAuthData.createTokenAuth(getUrl(), getToken(), isUseProxy());
354   }
355
356   private GithubConnection getConnection() {
357     return new GithubConnection(getAuthData(), true);
358   }
359
360   @Override
361   public boolean equals(Object o) {
362     if (!super.equals(o)) return false;
363     if (!(o instanceof GithubRepository)) return false;
364
365     GithubRepository that = (GithubRepository)o;
366     if (!Comparing.equal(getRepoAuthor(), that.getRepoAuthor())) return false;
367     if (!Comparing.equal(getRepoName(), that.getRepoName())) return false;
368     if (!Comparing.equal(getToken(), that.getToken())) return false;
369
370     return true;
371   }
372
373   @Override
374   protected int getFeatures() {
375     return super.getFeatures() | STATE_UPDATING;
376   }
377 }