summaryrefslogtreecommitdiff
path: root/plugins/github/src/org/jetbrains/plugins/github/GithubCreatePullRequestWorker.java
blob: bf9803b6f46fd8400db614b1e054fa8015ecb55d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jetbrains.plugins.github;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.ThrowableConvertor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import com.intellij.util.containers.HashMap;
import git4idea.DialogManager;
import git4idea.GitCommit;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.history.GitHistoryUtils;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.ui.branch.GitCompareBranchesDialog;
import git4idea.update.GitFetchResult;
import git4idea.update.GitFetcher;
import git4idea.util.GitCommitCompareInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.github.api.*;
import org.jetbrains.plugins.github.ui.GithubSelectForkDialog;
import org.jetbrains.plugins.github.util.*;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author Aleksey Pivovarov
 */
public class GithubCreatePullRequestWorker {
  private static final Logger LOG = GithubUtil.LOG;
  private static final String CANNOT_CREATE_PULL_REQUEST = "Can't Create Pull Request";

  @NotNull private final Project myProject;
  @NotNull private final Git myGit;
  @NotNull private final GitRepository myGitRepository;
  @NotNull private final GithubFullPath myPath;
  @NotNull private final String myRemoteName;
  @NotNull private final String myRemoteUrl;
  @NotNull private final String myCurrentBranch;
  @NotNull private final GithubAuthDataHolder myAuthHolder;

  @NotNull private final Map<String, FutureTask<DiffInfo>> myDiffInfos;

  private volatile GithubFullPath myForkPath;
  private volatile String myTargetRemote;

  private GithubCreatePullRequestWorker(@NotNull Project project,
                                        @NotNull Git git,
                                        @NotNull GitRepository gitRepository,
                                        @NotNull GithubFullPath path,
                                        @NotNull String remoteName,
                                        @NotNull String remoteUrl,
                                        @NotNull String currentBranch,
                                        @NotNull GithubAuthDataHolder authHolder) {
    myProject = project;
    myGit = git;
    myGitRepository = gitRepository;
    myPath = path;
    myRemoteName = remoteName;
    myRemoteUrl = remoteUrl;
    myCurrentBranch = currentBranch;
    myAuthHolder = authHolder;

    myDiffInfos = new HashMap<String, FutureTask<DiffInfo>>();
  }

  @NotNull
  public Project getProject() {
    return myProject;
  }

  @NotNull
  public String getCurrentBranch() {
    return myCurrentBranch;
  }

  public boolean canShowDiff() {
    return myTargetRemote != null;
  }

  @Nullable
  public static GithubCreatePullRequestWorker createPullRequestWorker(@NotNull final Project project, @Nullable final VirtualFile file) {
    Git git = ServiceManager.getService(Git.class);

    GitRepository gitRepository = GithubUtil.getGitRepository(project, file);
    if (gitRepository == null) {
      GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find git repository");
      return null;
    }
    gitRepository.update();

    Pair<GitRemote, String> remote = GithubUtil.findGithubRemote(gitRepository);
    if (remote == null) {
      GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't find GitHub remote");
      return null;
    }
    String remoteName = remote.getFirst().getName();
    String remoteUrl = remote.getSecond();
    GithubFullPath path = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(remoteUrl);
    if (path == null) {
      GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Can't process remote: " + remoteUrl);
      return null;
    }

    GitLocalBranch currentBranch = gitRepository.getCurrentBranch();
    if (currentBranch == null) {
      GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "No current branch");
      return null;
    }

    GithubAuthDataHolder authHolder;
    try {
      authHolder = GithubUtil
        .computeValueInModal(project, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubAuthDataHolder, IOException>() {
          @NotNull
          @Override
          public GithubAuthDataHolder convert(ProgressIndicator indicator) throws IOException {
            return GithubUtil.getValidAuthDataHolderFromConfig(project, indicator);
          }
        });
    }
    catch (IOException e) {
      GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
      return null;
    }

    return new GithubCreatePullRequestWorker(project, git, gitRepository, path, remoteName, remoteUrl, currentBranch.getName(), authHolder);
  }

  @Nullable
  public GithubTargetInfo setTarget(@NotNull final GithubFullPath forkPath) {
    try {
      GithubInfo info =
        GithubUtil.computeValueInModal(myProject, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubInfo, IOException>() {
          @NotNull
          @Override
          public GithubInfo convert(ProgressIndicator indicator) throws IOException {
            // configure remote
            GitRemote targetRemote = GithubUtil.findGithubRemote(myGitRepository, forkPath);
            String targetRemoteName = targetRemote == null ? null : targetRemote.getName();
            if (targetRemoteName == null) {
              final Ref<Integer> responseRef = new Ref<Integer>();
              ApplicationManager.getApplication().invokeAndWait(new Runnable() {
                @Override
                public void run() {
                  responseRef.set(GithubNotifications
                                    .showYesNoDialog(myProject, "Can't Find Remote", "Configure remote for '" + forkPath.getUser() + "'?"));
                }
              }, indicator.getModalityState());
              if (responseRef.get() == Messages.YES) {
                targetRemoteName = configureRemote(myProject, myGitRepository, forkPath);
              }
            }

            // load available branches
            List<String> branches = ContainerUtil.map(GithubUtil.runTask(myProject, myAuthHolder, indicator,
                                                                         new ThrowableConvertor<GithubAuthData, List<GithubBranch>, IOException>() {
                                                                           @Override
                                                                           public List<GithubBranch> convert(@NotNull GithubAuthData auth)
                                                                             throws IOException {
                                                                             return GithubApiUtil.getRepoBranches(auth, forkPath.getUser(),
                                                                                                                  forkPath.getRepository());
                                                                           }
                                                                         }
            ), new Function<GithubBranch, String>() {
              @Override
              public String fun(GithubBranch githubBranch) {
                return githubBranch.getName();
              }
            });

            // fetch
            if (targetRemoteName != null) {
              GitFetchResult result = new GitFetcher(myProject, indicator, false).fetch(myGitRepository.getRoot(), targetRemoteName, null);
              if (!result.isSuccess()) {
                GitFetcher.displayFetchResult(myProject, result, null, result.getErrors());
                targetRemoteName = null;
              }
            }

            return new GithubInfo(branches, targetRemoteName);
          }
        });

      myForkPath = forkPath;
      myTargetRemote = info.getTargetRemote();

      myDiffInfos.clear();
      if (canShowDiff()) {
        for (final String branch : info.getBranches()) {
          myDiffInfos.put(branch, new FutureTask<DiffInfo>(new Callable<DiffInfo>() {
            @Nullable
            @Override
            public DiffInfo call() throws Exception {
              return loadDiffInfo(myProject, myGitRepository, myCurrentBranch, myTargetRemote + "/" + branch);
            }
          }));
        }
      }

      return new GithubTargetInfo(info.getBranches());
    }
    catch (IOException e) {
      GithubNotifications.showErrorDialog(myProject, CANNOT_CREATE_PULL_REQUEST, e);
      return null;
    }
  }

  public void showDiffDialog(@NotNull String branch) {
    if (canShowDiff()) {
      DiffInfo info = getDiffInfoWithModal(branch);
      if (info == null) {
        GithubNotifications.showErrorDialog(myProject, "Can't Show Diff", "Can't get diff info");
        return;
      }

      GitCompareBranchesDialog dialog =
        new GitCompareBranchesDialog(myProject, info.getTo(), info.getFrom(), info.getInfo(), myGitRepository);
      dialog.show();
    }
  }

  @Nullable
  public GithubFullPath showTargetDialog() {
    return showTargetDialog(false);
  }

  @Nullable
  public GithubFullPath showTargetDialog(boolean firstTime) {
    final GithubInfo2 info = getAvailableForksInModal(myProject, myGitRepository, myAuthHolder, myPath);
    if (info == null) {
      return null;
    }

    if (firstTime) {
      if (info.getForks().size() == 1) {
        return info.getForks().iterator().next();
      }
      if (info.getForks().size() == 2) {
        Iterator<GithubFullPath> it = info.getForks().iterator();
        GithubFullPath path1 = it.next();
        GithubFullPath path2 = it.next();

        if (myPath.equals(path1)) {
          return path2;
        }
        if (myPath.equals(path2)) {
          return path1;
        }
      }
    }

    Convertor<String, GithubFullPath> getForkPath = new Convertor<String, GithubFullPath>() {
      @Nullable
      @Override
      public GithubFullPath convert(@NotNull final String user) {
        return GithubUtil.computeValueInModal(myProject, "Access to GitHub", new Convertor<ProgressIndicator, GithubFullPath>() {
          @Nullable
          @Override
          public GithubFullPath convert(ProgressIndicator indicator) {
            return findRepositoryByUser(myProject, myAuthHolder, indicator, user, info.getForks(), info.getSource());
          }
        });
      }
    };
    GithubSelectForkDialog dialog = new GithubSelectForkDialog(myProject, info.getForks(), getForkPath);
    DialogManager.show(dialog);
    if (!dialog.isOK()) {
      return null;
    }
    return dialog.getPath();
  }

  public boolean checkAction(@NotNull String targetBranch) {
    DiffInfo info = getDiffInfoWithModal(targetBranch);
    if (info == null) {
      return true;
    }

    String localBranchName = "'" + getCurrentBranch() + "'";
    String targetBranchName = "'" + myTargetRemote + ":" + targetBranch + "'";
    if (info.getInfo().getBranchToHeadCommits(myGitRepository).isEmpty()) {
      GithubNotifications
        .showWarningDialog(myProject, CANNOT_CREATE_PULL_REQUEST,
                           "Can't create empty pull request: the branch " + localBranchName +
                           " is fully merged to the branch " + targetBranchName
        );
      return false;
    }
    if (!info.getInfo().getHeadToBranchCommits(myGitRepository).isEmpty()) {
      return GithubNotifications
               .showYesNoDialog(myProject, "Do you want to proceed anyway?",
                                "The branch " + targetBranchName + " is not fully merged to the branch " + localBranchName) == Messages.YES;
    }

    return true;
  }

  public void performAction(@NotNull final String title, @NotNull final String description, @NotNull final String targetBranch) {
    @NotNull final Project project = myProject;

    new Task.Backgroundable(myProject, "Creating pull request...") {
      @Override
      public void run(@NotNull ProgressIndicator indicator) {
        LOG.info("Pushing current branch");
        indicator.setText("Pushing current branch...");
        GitCommandResult result = myGit.push(myGitRepository, myRemoteName, myRemoteUrl, myCurrentBranch, true);
        if (!result.success()) {
          GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, "Push failed:<br/>" + result.getErrorOutputAsHtmlString());
          return;
        }

        String headBranch = myPath.getUser() + ":" + myCurrentBranch;

        LOG.info("Creating pull request");
        indicator.setText("Creating pull request...");
        GithubPullRequest request =
          createPullRequest(project, myAuthHolder, indicator, myForkPath, title, description, headBranch, targetBranch);
        if (request == null) {
          return;
        }

        GithubNotifications
          .showInfoURL(project, "Successfully created pull request", "Pull request #" + request.getNumber(), request.getHtmlUrl());
      }
    }.queue();
  }

  @Nullable
  private static String configureRemote(@NotNull Project project, @NotNull GitRepository gitRepository, @NotNull GithubFullPath forkPath) {
    String url = GithubUrlUtil.getCloneUrl(forkPath);

    if (GithubUtil.addGithubRemote(project, gitRepository, forkPath.getUser(), url)) {
      return forkPath.getUser();
    }
    else {
      return null;
    }
  }

  @Nullable
  private static GithubPullRequest createPullRequest(@NotNull Project project,
                                                     @NotNull GithubAuthDataHolder authHolder,
                                                     @NotNull ProgressIndicator indicator,
                                                     @NotNull final GithubFullPath targetRepo,
                                                     @NotNull final String title,
                                                     @NotNull final String description,
                                                     @NotNull final String head,
                                                     @NotNull final String base) {
    try {
      return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubAuthData, GithubPullRequest, IOException>() {
        @NotNull
        @Override
        public GithubPullRequest convert(@NotNull GithubAuthData auth) throws IOException {
          return GithubApiUtil.createPullRequest(auth, targetRepo.getUser(), targetRepo.getRepository(), title, description, head, base);
        }
      });
    }
    catch (IOException e) {
      GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
      return null;
    }
  }

  @Nullable
  private DiffInfo getDiffInfo(@NotNull String branch) {
    try {
      FutureTask<DiffInfo> future = myDiffInfos.get(branch);
      if (future == null) {
        return null;
      }
      future.run();
      return future.get();
    }
    catch (InterruptedException e) {
      LOG.error(e);
      return null;
    }
    catch (ExecutionException e) {
      LOG.error(e);
      return null;
    }
  }

  @Nullable
  private DiffInfo getDiffInfoWithModal(@NotNull final String branch) {
    return GithubUtil.computeValueInModal(myProject, "Collecting diff data...", new Convertor<ProgressIndicator, DiffInfo>() {
      @Override
      @Nullable
      public DiffInfo convert(ProgressIndicator indicator) {
        return getDiffInfo(branch);
      }
    });
  }

  public void getDiffDescriptionInPooledThread(@NotNull final String branch, @NotNull final Consumer<DiffDescription> after) {
    ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
      @Override
      public void run() {
        after.consume(getDefaultDescriptionMessage(branch, getDiffInfo(branch), myGitRepository));
      }
    });
  }

  @Nullable
  private static DiffInfo loadDiffInfo(@NotNull final Project project,
                                       @NotNull final GitRepository repository,
                                       @NotNull final String currentBranch,
                                       @NotNull final String targetBranch) {
    try {
      List<GitCommit> commits1 = GitHistoryUtils.history(project, repository.getRoot(), ".." + targetBranch);
      List<GitCommit> commits2 = GitHistoryUtils.history(project, repository.getRoot(), targetBranch + "..");
      Collection<Change> diff = GitChangeUtils.getDiff(repository.getProject(), repository.getRoot(), targetBranch, currentBranch, null);
      GitCommitCompareInfo info = new GitCommitCompareInfo(GitCommitCompareInfo.InfoType.BRANCH_TO_HEAD);
      info.put(repository, diff);
      info.put(repository, Couple.of(commits1, commits2));
      return new DiffInfo(info, currentBranch, targetBranch);
    }
    catch (VcsException e) {
      LOG.info(e);
      return null;
    }
  }

  @NotNull
  private static DiffDescription getDefaultDescriptionMessage(@NotNull String branch,
                                                              @Nullable DiffInfo info,
                                                              @NotNull GitRepository gitRepository) {
    if (info == null) {
      return new DiffDescription(branch, null, null);
    }

    if (info.getInfo().getBranchToHeadCommits(gitRepository).size() != 1) {
      return new DiffDescription(branch, info.getFrom(), null);
    }

    GitCommit commit = info.getInfo().getBranchToHeadCommits(gitRepository).get(0);
    return new DiffDescription(branch, commit.getSubject(), commit.getFullMessage());
  }

  @Nullable
  private static GithubInfo2 getAvailableForksInModal(@NotNull final Project project,
                                                      @NotNull final GitRepository gitRepository,
                                                      @NotNull final GithubAuthDataHolder authHolder,
                                                      @NotNull final GithubFullPath path) {
    try {
      return GithubUtil
        .computeValueInModal(project, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubInfo2, IOException>() {
          @NotNull
          @Override
          public GithubInfo2 convert(ProgressIndicator indicator) throws IOException {
            final Set<GithubFullPath> forks = new HashSet<GithubFullPath>();

            // GitHub
            GithubRepoDetailed repo =
              GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubAuthData, GithubRepoDetailed, IOException>() {
                @NotNull
                @Override
                public GithubRepoDetailed convert(@NotNull GithubAuthData auth) throws IOException {
                  return GithubApiUtil.getDetailedRepoInfo(auth, path.getUser(), path.getRepository());
                }
              });

            forks.add(path);
            if (repo.getParent() != null) {
              forks.add(repo.getParent().getFullPath());
            }
            if (repo.getSource() != null) {
              forks.add(repo.getSource().getFullPath());
            }

            // Git
            forks.addAll(getAvailableForksFromGit(gitRepository));

            GithubRepo forkTreeRoot = repo.getSource() == null ? repo : repo.getSource();
            return new GithubInfo2(forks, forkTreeRoot);
          }
        });
    }
    catch (IOException e) {
      GithubNotifications.showErrorDialog(project, CANNOT_CREATE_PULL_REQUEST, e);
      return null;
    }
  }

  @NotNull
  private static List<GithubFullPath> getAvailableForksFromGit(@NotNull GitRepository gitRepository) {
    List<GithubFullPath> forks = new ArrayList<GithubFullPath>();
    for (GitRemoteBranch remoteBranch : gitRepository.getBranches().getRemoteBranches()) {
      for (String url : remoteBranch.getRemote().getUrls()) {
        if (GithubUrlUtil.isGithubUrl(url)) {
          GithubFullPath path = GithubUrlUtil.getUserAndRepositoryFromRemoteUrl(url);
          if (path != null) {
            forks.add(path);
            break;
          }
        }
      }
    }
    return forks;
  }

  @Nullable
  private static GithubFullPath findRepositoryByUser(@NotNull Project project,
                                                     @NotNull GithubAuthDataHolder authHolder,
                                                     @NotNull ProgressIndicator indicator,
                                                     @NotNull final String user,
                                                     @NotNull Set<GithubFullPath> forks,
                                                     @NotNull final GithubRepo source) {
    for (GithubFullPath path : forks) {
      if (StringUtil.equalsIgnoreCase(user, path.getUser())) {
        return path;
      }
    }

    try {
      return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubAuthData, GithubFullPath, IOException>() {
        @Nullable
        @Override
        public GithubFullPath convert(@NotNull GithubAuthData auth) throws IOException {
          try {
            GithubRepoDetailed target = GithubApiUtil.getDetailedRepoInfo(auth, user, source.getName());
            if (target.getSource() != null && StringUtil.equals(target.getSource().getUserName(), source.getUserName())) {
              return target.getFullPath();
            }
          }
          catch (IOException ignore) {
            // such repo may not exist
          }

          GithubRepo fork = GithubApiUtil.findForkByUser(auth, source.getUserName(), source.getName(), user);
          if (fork != null) {
            return fork.getFullPath();
          }

          return null;
        }
      });
    }
    catch (IOException e) {
      GithubNotifications.showError(project, CANNOT_CREATE_PULL_REQUEST, e);
    }

    return null;
  }

  private static class GithubInfo {
    @NotNull private final List<String> myBranches;
    @Nullable private final String myTargetRemote;

    private GithubInfo(@NotNull List<String> repo, @Nullable String targetRemote) {
      myBranches = repo;
      myTargetRemote = targetRemote;
    }

    @NotNull
    public List<String> getBranches() {
      return myBranches;
    }

    @Nullable
    public String getTargetRemote() {
      return myTargetRemote;
    }
  }

  private static class GithubInfo2 {
    @NotNull private final Set<GithubFullPath> myForks;
    @NotNull private final GithubRepo mySource;

    private GithubInfo2(@NotNull Set<GithubFullPath> forks, @NotNull GithubRepo source) {
      myForks = forks;
      mySource = source;
    }

    @NotNull
    public Set<GithubFullPath> getForks() {
      return myForks;
    }

    @NotNull
    public GithubRepo getSource() {
      return mySource;
    }
  }

  public static class GithubTargetInfo {
    @NotNull private final List<String> myBranches;

    private GithubTargetInfo(@NotNull List<String> branches) {
      myBranches = branches;
    }

    @NotNull
    public List<String> getBranches() {
      return myBranches;
    }
  }

  private static class DiffInfo {
    @NotNull private final GitCommitCompareInfo myInfo;
    @NotNull private final String myFrom;
    @NotNull private final String myTo;

    private DiffInfo(@NotNull GitCommitCompareInfo info, @NotNull String from, @NotNull String to) {
      myInfo = info;
      myFrom = from; // HEAD
      myTo = to;     // BASE
    }

    @NotNull
    public GitCommitCompareInfo getInfo() {
      return myInfo;
    }

    @NotNull
    public String getFrom() {
      return myFrom;
    }

    @NotNull
    public String getTo() {
      return myTo;
    }
  }

  public static class DiffDescription {
    @NotNull private final String myBranch;
    @Nullable private final String myTitle;
    @Nullable private final String myDescription;

    public DiffDescription(@NotNull String branch, @Nullable String title, @Nullable String description) {
      myBranch = branch;
      myTitle = title;
      myDescription = description;
    }

    @NotNull
    public String getBranch() {
      return myBranch;
    }

    @Nullable
    public String getTitle() {
      return myTitle;
    }

    @Nullable
    public String getDescription() {
      return myDescription;
    }
  }
}