replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / git4idea / src / git4idea / commands / GitImpl.java
1 /*
2  * Copyright 2000-2011 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 git4idea.commands;
17
18 import com.google.common.annotations.VisibleForTesting;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.util.Computable;
22 import com.intellij.openapi.util.Key;
23 import com.intellij.openapi.util.text.StringUtil;
24 import com.intellij.openapi.vcs.VcsException;
25 import com.intellij.openapi.vfs.VfsUtilCore;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.util.ObjectUtils;
28 import com.intellij.util.containers.ContainerUtil;
29 import com.intellij.vcsUtil.VcsFileUtil;
30 import git4idea.GitVcs;
31 import git4idea.branch.GitRebaseParams;
32 import git4idea.config.GitVersionSpecialty;
33 import git4idea.rebase.GitInteractiveRebaseEditorHandler;
34 import git4idea.rebase.GitRebaseEditorHandler;
35 import git4idea.rebase.GitRebaseEditorService;
36 import git4idea.rebase.GitRebaseResumeMode;
37 import git4idea.repo.GitRemote;
38 import git4idea.repo.GitRepository;
39 import git4idea.reset.GitResetMode;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import java.io.File;
44 import java.util.*;
45 import java.util.concurrent.atomic.AtomicBoolean;
46 import java.util.concurrent.atomic.AtomicInteger;
47 import java.util.concurrent.atomic.AtomicReference;
48
49 import static java.util.Collections.singleton;
50
51 /**
52  * Easy-to-use wrapper of common native Git commands.
53  * Most of them return result as {@link GitCommandResult}.
54  *
55  * @author Kirill Likhodedov
56  */
57 @SuppressWarnings("StringToUpperCaseOrToLowerCaseWithoutLocale")
58 public class GitImpl implements Git {
59
60   private static final Logger LOG = Logger.getInstance(Git.class);
61
62   public GitImpl() {
63   }
64
65   /**
66    * Calls 'git init' on the specified directory.
67    */
68   @NotNull
69   @Override
70   public GitCommandResult init(@NotNull Project project, @NotNull VirtualFile root, @NotNull GitLineHandlerListener... listeners) {
71     GitLineHandler h = new GitLineHandler(project, root, GitCommand.INIT);
72     for (GitLineHandlerListener listener : listeners) {
73       h.addLineListener(listener);
74     }
75     h.setSilent(false);
76     h.setStdoutSuppressed(false);
77     return run(h);
78   }
79
80   /**
81    * <p>Queries Git for the unversioned files in the given paths. </p>
82    * <p>Ignored files are left ignored, i. e. no information is returned about them (thus this method may also be used as a
83    *    ignored files checker.</p>
84    *
85    * @param files files that are to be checked for the unversioned files among them.
86    *              <b>Pass {@code null} to query the whole repository.</b>
87    * @return Unversioned not ignored files from the given scope.
88    */
89   @Override
90   @NotNull
91   public Set<VirtualFile> untrackedFiles(@NotNull Project project, @NotNull VirtualFile root,
92                                          @Nullable Collection<VirtualFile> files) throws VcsException {
93     final Set<VirtualFile> untrackedFiles = new HashSet<>();
94
95     if (files == null) {
96       untrackedFiles.addAll(untrackedFilesNoChunk(project, root, null));
97     }
98     else {
99       for (List<String> relativePaths : VcsFileUtil.chunkFiles(root, files)) {
100         untrackedFiles.addAll(untrackedFilesNoChunk(project, root, relativePaths));
101       }
102     }
103
104     return untrackedFiles;
105   }
106
107   // relativePaths are guaranteed to fit into command line length limitations.
108   @Override
109   @NotNull
110   public Collection<VirtualFile> untrackedFilesNoChunk(@NotNull Project project,
111                                                        @NotNull VirtualFile root,
112                                                        @Nullable List<String> relativePaths)
113     throws VcsException {
114     final Set<VirtualFile> untrackedFiles = new HashSet<>();
115     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LS_FILES);
116     h.setSilent(true);
117     h.addParameters("--exclude-standard", "--others", "-z");
118     h.endOptions();
119     if (relativePaths != null) {
120       h.addParameters(relativePaths);
121     }
122
123     final String output = h.run();
124     if (StringUtil.isEmptyOrSpaces(output)) {
125       return untrackedFiles;
126     }
127
128     for (String relPath : output.split("\u0000")) {
129       VirtualFile f = root.findFileByRelativePath(relPath);
130       if (f == null) {
131         // files was created on disk, but VirtualFile hasn't yet been created,
132         // when the GitChangeProvider has already been requested about changes.
133         LOG.info(String.format("VirtualFile for path [%s] is null", relPath));
134       } else {
135         untrackedFiles.add(f);
136       }
137     }
138
139     return untrackedFiles;
140   }
141
142   @Override
143   @NotNull
144   public GitCommandResult clone(@NotNull final Project project, @NotNull final File parentDirectory, @NotNull final String url,
145                                 @NotNull final String clonedDirectoryName, @NotNull final GitLineHandlerListener... listeners) {
146     return run(() -> {
147       GitLineHandler handler = new GitLineHandler(project, parentDirectory, GitCommand.CLONE);
148       handler.setStdoutSuppressed(false);
149       handler.setUrl(url);
150       handler.addParameters("--progress");
151       handler.addParameters(url);
152       handler.endOptions();
153       handler.addParameters(clonedDirectoryName);
154       addListeners(handler, listeners);
155       return handler;
156     });
157   }
158
159   @NotNull
160   @Override
161   public GitCommandResult config(@NotNull GitRepository repository, String... params) {
162     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CONFIG);
163     h.addParameters(params);
164     return run(h);
165   }
166
167   @NotNull
168   @Override
169   public GitCommandResult diff(@NotNull GitRepository repository, @NotNull List<String> parameters, @NotNull String range) {
170     final GitLineHandler diff = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.DIFF);
171     diff.addParameters(parameters);
172     diff.addParameters(range);
173     diff.setStdoutSuppressed(true);
174     diff.setStderrSuppressed(true);
175     diff.setSilent(true);
176     return run(diff);
177   }
178
179   @NotNull
180   @Override
181   public GitCommandResult checkAttr(@NotNull final GitRepository repository,
182                                     @NotNull final Collection<String> attributes,
183                                     @NotNull Collection<VirtualFile> files) {
184     List<List<String>> listOfPaths = VcsFileUtil.chunkFiles(repository.getRoot(), files);
185     return runAll(ContainerUtil.map(listOfPaths, relativePaths -> () -> {
186       final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECK_ATTR);
187       h.addParameters(new ArrayList<>(attributes));
188       h.endOptions();
189       h.addParameters(relativePaths);
190       return run(h);
191     }));
192   }
193
194   @NotNull
195   @Override
196   public GitCommandResult stashSave(@NotNull GitRepository repository, @NotNull String message) {
197     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.STASH);
198     h.addParameters("save");
199     h.addParameters(message);
200     return run(h);
201   }
202
203   @NotNull
204   @Override
205   public GitCommandResult stashPop(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) {
206     final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.STASH);
207     handler.addParameters("pop");
208     addListeners(handler, listeners);
209     return run(handler);
210   }
211
212   @Override
213   @NotNull
214   public GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge,
215                                 @Nullable List<String> additionalParams, @NotNull GitLineHandlerListener... listeners) {
216     final GitLineHandler mergeHandler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.MERGE);
217     mergeHandler.setSilent(false);
218     mergeHandler.addParameters(branchToMerge);
219     if (additionalParams != null) {
220       mergeHandler.addParameters(additionalParams);
221     }
222     for (GitLineHandlerListener listener : listeners) {
223       mergeHandler.addLineListener(listener);
224     }
225     return run(mergeHandler);
226   }
227
228
229   /**
230    * {@code git checkout &lt;reference&gt;} <br/>
231    * {@code git checkout -b &lt;newBranch&gt; &lt;reference&gt;}
232    */
233   @NotNull
234   @Override
235   public GitCommandResult checkout(@NotNull GitRepository repository,
236                                    @NotNull String reference,
237                                    @Nullable String newBranch,
238                                    boolean force,
239                                    boolean detach,
240                                    @NotNull GitLineHandlerListener... listeners) {
241     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT);
242     h.setSilent(false);
243     h.setStdoutSuppressed(false);
244     if (force) {
245       h.addParameters("--force");
246     }
247     if (newBranch == null) { // simply checkout
248       h.addParameters(detach ? reference + "^0" : reference); // we could use `--detach` here, but it is supported only since 1.7.5.
249     }
250     else { // checkout reference as new branch
251       h.addParameters("-b", newBranch, reference);
252     }
253     h.endOptions();
254     for (GitLineHandlerListener listener : listeners) {
255       h.addLineListener(listener);
256     }
257     return run(h);
258   }
259
260   /**
261    * {@code git checkout -b &lt;branchName&gt;}
262    */
263   @NotNull
264   @Override
265   public GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, @NotNull String branchName,
266                                                    @Nullable GitLineHandlerListener listener) {
267     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT.readLockingCommand());
268     h.setSilent(false);
269     h.setStdoutSuppressed(false);
270     h.addParameters("-b");
271     h.addParameters(branchName);
272     if (listener != null) {
273       h.addLineListener(listener);
274     }
275     return run(h);
276   }
277
278   @NotNull
279   @Override
280   public GitCommandResult createNewTag(@NotNull GitRepository repository, @NotNull String tagName,
281                                        @Nullable GitLineHandlerListener listener, @NotNull String reference) {
282     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.TAG);
283     h.setSilent(false);
284     h.addParameters(tagName);
285     if (!reference.isEmpty()) {
286       h.addParameters(reference);
287     }
288     if (listener != null) {
289       h.addLineListener(listener);
290     }
291     return run(h);
292   }
293
294   /**
295    * {@code git branch -d <reference>} or {@code git branch -D <reference>}
296    */
297   @NotNull
298   @Override
299   public GitCommandResult branchDelete(@NotNull GitRepository repository,
300                                               @NotNull String branchName,
301                                               boolean force,
302                                               @NotNull GitLineHandlerListener... listeners) {
303     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
304     h.setSilent(false);
305     h.setStdoutSuppressed(false);
306     h.addParameters(force ? "-D" : "-d");
307     h.addParameters(branchName);
308     for (GitLineHandlerListener listener : listeners) {
309       h.addLineListener(listener);
310     }
311     return run(h);
312   }
313
314   /**
315    * Get branches containing the commit.
316    * {@code git branch --contains <commit>}
317    */
318   @Override
319   @NotNull
320   public GitCommandResult branchContains(@NotNull GitRepository repository, @NotNull String commit) {
321     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
322     h.addParameters("--contains", commit);
323     return run(h);
324   }
325
326   @Override
327   @NotNull
328   public GitCommandResult branchCreate(@NotNull GitRepository repository, @NotNull String branchName, @NotNull String startPoint) {
329     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
330     h.setStdoutSuppressed(false);
331     h.addParameters(branchName);
332     h.addParameters(startPoint);
333     return run(h);
334   }
335
336   @NotNull
337   @Override
338   public GitCommandResult renameBranch(@NotNull GitRepository repository,
339                                        @NotNull String currentName,
340                                        @NotNull String newName,
341                                        @NotNull GitLineHandlerListener... listeners) {
342     GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
343     h.setSilent(false);
344     h.setStdoutSuppressed(false);
345     h.addParameters("-m", currentName, newName);
346     return run(h);
347   }
348
349   @Override
350   @NotNull
351   public GitCommandResult reset(@NotNull GitRepository repository, @NotNull GitResetMode mode, @NotNull String target,
352                                 @NotNull GitLineHandlerListener... listeners) {
353     return reset(repository, mode.getArgument(), target, listeners);
354   }
355
356   @Override
357   @NotNull
358   public GitCommandResult resetMerge(@NotNull GitRepository repository, @Nullable String revision) {
359     return reset(repository, "--merge", revision);
360   }
361
362   @NotNull
363   private static GitCommandResult reset(@NotNull GitRepository repository, @NotNull String argument, @Nullable String target,
364                                         @NotNull GitLineHandlerListener... listeners) {
365     final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.RESET);
366     handler.addParameters(argument);
367     if (target != null) {
368       handler.addParameters(target);
369     }
370     addListeners(handler, listeners);
371     return run(handler);
372   }
373
374   /**
375    * Returns the last (tip) commit on the given branch.<br/>
376    * {@code git rev-list -1 <branchName>}
377    */
378   @NotNull
379   @Override
380   public GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName) {
381     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REV_LIST);
382     h.addParameters("-1");
383     h.addParameters(branchName);
384     return run(h);
385   }
386
387   @Override
388   @NotNull
389   public GitCommandResult push(@NotNull GitRepository repository,
390                                @NotNull String remote,
391                                @Nullable String url,
392                                @NotNull String spec,
393                                boolean updateTracking,
394                                @NotNull GitLineHandlerListener... listeners) {
395     return doPush(repository, remote, singleton(url), spec, false, updateTracking, false, null, listeners);
396   }
397
398   @Override
399   @NotNull
400   public GitCommandResult push(@NotNull GitRepository repository,
401                                @NotNull GitRemote remote,
402                                @NotNull String spec,
403                                boolean force,
404                                boolean updateTracking,
405                                boolean skipHook,
406                                @Nullable String tagMode,
407                                GitLineHandlerListener... listeners) {
408     return doPush(repository, remote.getName(), remote.getPushUrls(), spec, force, updateTracking, skipHook, tagMode, listeners);
409   }
410
411   @NotNull
412   private GitCommandResult doPush(@NotNull final GitRepository repository,
413                                   @NotNull final String remoteName,
414                                   @NotNull final Collection<String> remoteUrls,
415                                   @NotNull final String spec,
416                                   final boolean force,
417                                   final boolean updateTracking,
418                                   final boolean skipHook,
419                                   @Nullable final String tagMode,
420                                   @NotNull final GitLineHandlerListener... listeners) {
421     return runCommand(() -> {
422       final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.PUSH);
423       h.setUrls(remoteUrls);
424       h.setSilent(false);
425       h.setStdoutSuppressed(false);
426       addListeners(h, listeners);
427       h.addProgressParameter();
428       h.addParameters("--porcelain");
429       h.addParameters(remoteName);
430       h.addParameters(spec);
431       if (updateTracking) {
432         h.addParameters("--set-upstream");
433       }
434       if (force) {
435         h.addParameters("--force");
436       }
437       if (tagMode != null) {
438         h.addParameters(tagMode);
439       }
440       if (skipHook) {
441         h.addParameters("--no-verify");
442       }
443       return h;
444     });
445   }
446
447   @NotNull
448   @Override
449   public GitCommandResult show(@NotNull GitRepository repository, @NotNull String... params) {
450     final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.SHOW);
451     handler.addParameters(params);
452     return run(handler);
453   }
454
455   @Override
456   @NotNull
457   public GitCommandResult cherryPick(@NotNull GitRepository repository, @NotNull String hash, boolean autoCommit,
458                                      @NotNull GitLineHandlerListener... listeners) {
459     final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHERRY_PICK);
460     handler.addParameters("-x");
461     if (!autoCommit) {
462       handler.addParameters("-n");
463     }
464     handler.addParameters(hash);
465     addListeners(handler, listeners);
466     handler.setSilent(false);
467     handler.setStdoutSuppressed(false);
468     return run(handler);
469   }
470
471   @NotNull
472   @Override
473   public GitCommandResult getUnmergedFiles(@NotNull GitRepository repository) {
474     GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.LS_FILES);
475     h.addParameters("--unmerged");
476     h.setSilent(true);
477     return run(h);
478   }
479
480   /**
481    * Fetch remote branch
482    * {@code git fetch <remote> <params>}
483    */
484   @Override
485   @NotNull
486   public GitCommandResult fetch(@NotNull final GitRepository repository,
487                                 @NotNull final GitRemote remote,
488                                 @NotNull final List<GitLineHandlerListener> listeners,
489                                 final String... params) {
490     return runCommand(() -> {
491       final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.FETCH);
492       h.setSilent(false);
493       h.setStdoutSuppressed(false);
494       h.setUrls(remote.getUrls());
495       h.addParameters(remote.getName());
496       h.addParameters(params);
497       h.addProgressParameter();
498       GitVcs vcs = GitVcs.getInstance(repository.getProject());
499       if (vcs != null && GitVersionSpecialty.SUPPORTS_FETCH_PRUNE.existsIn(vcs.getVersion())) {
500         h.addParameters("--prune");
501       }
502       addListeners(h, listeners);
503       return h;
504     });
505   }
506
507   @NotNull
508   @Override
509   public GitCommandResult addRemote(@NotNull GitRepository repository, @NotNull String name, @NotNull String url) {
510     GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REMOTE);
511     h.addParameters("add", name, url);
512     return run(h);
513   }
514
515   @NotNull
516   @Override
517   public GitCommandResult removeRemote(@NotNull GitRepository repository, @NotNull GitRemote remote) {
518     GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REMOTE);
519     h.addParameters("remove", remote.getName());
520     return run(h);
521   }
522
523   @NotNull
524   @Override
525   public GitCommandResult renameRemote(@NotNull GitRepository repository, @NotNull String oldName, @NotNull String newName) {
526     GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REMOTE);
527     h.addParameters("rename", oldName, newName);
528     return run(h);
529   }
530
531   @NotNull
532   @Override
533   public GitCommandResult setRemoteUrl(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String newUrl) {
534     GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REMOTE);
535     h.addParameters("set-url", remoteName, newUrl);
536     return run(h);
537   }
538
539   @NotNull
540   @Override
541   public GitCommandResult lsRemote(@NotNull final Project project,
542                                    @NotNull final File workingDir,
543                                    @NotNull final String url) {
544     return doLsRemote(project, workingDir, url, singleton(url));
545   }
546
547   @NotNull
548   @Override
549   public GitCommandResult lsRemote(@NotNull Project project,
550                                    @NotNull VirtualFile workingDir,
551                                    @NotNull GitRemote remote,
552                                    String... additionalParameters) {
553     return doLsRemote(project, VfsUtilCore.virtualToIoFile(workingDir), remote.getName(), remote.getUrls(), additionalParameters);
554   }
555
556   @NotNull
557   @Override
558   public GitCommandResult remotePrune(@NotNull final GitRepository repository, @NotNull final GitRemote remote) {
559     return run(() -> {
560       GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REMOTE.writeLockingCommand());
561       h.setStdoutSuppressed(false);
562       h.addParameters("prune");
563       h.addParameters(remote.getName());
564       h.setUrls(remote.getUrls());
565       return h;
566     });
567   }
568
569   @NotNull
570   @Override
571   public GitCommandResult rebase(@NotNull GitRepository repository,
572                                  @NotNull GitRebaseParams parameters,
573                                  @NotNull GitLineHandlerListener... listeners) {
574     Project project = repository.getProject();
575     VirtualFile root = repository.getRoot();
576     GitLineHandler handler = new GitLineHandler(project, root, GitCommand.REBASE);
577     handler.addParameters(parameters.asCommandLineArguments());
578     addListeners(handler, listeners);
579
580     if (parameters.isInteractive()) {
581       GitRebaseEditorHandler editorHandler = parameters.getEditorHandler();
582       if (editorHandler == null) {
583         editorHandler = createEditor(project, root, handler, true);
584       }
585       return runWithEditor(handler, editorHandler);
586     }
587     return run(handler);
588   }
589
590   @NotNull
591   @Override
592   public GitCommandResult rebaseAbort(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) {
593     GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REBASE);
594     handler.addParameters("--abort");
595     addListeners(handler, listeners);
596     return run(handler);
597   }
598
599   @NotNull
600   @Override
601   public GitCommandResult rebaseContinue(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) {
602     return rebaseResume(repository, GitRebaseResumeMode.CONTINUE, listeners);
603   }
604
605   @NotNull
606   @Override
607   public GitCommandResult rebaseSkip(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) {
608     return rebaseResume(repository, GitRebaseResumeMode.SKIP, listeners);
609   }
610
611   @NotNull
612   private GitCommandResult rebaseResume(@NotNull GitRepository repository,
613                                         @NotNull GitRebaseResumeMode rebaseMode,
614                                         @NotNull GitLineHandlerListener[] listeners) {
615     Project project = repository.getProject();
616     VirtualFile root = repository.getRoot();
617     GitLineHandler handler = new GitLineHandler(project, root, GitCommand.REBASE);
618     handler.addParameters(rebaseMode.asCommandLineArgument());
619     addListeners(handler, listeners);
620     return runWithEditor(handler, createEditor(project, root, handler, false));
621   }
622
623   @NotNull
624   private static GitCommandResult runWithEditor(@NotNull GitLineHandler handler, @NotNull GitRebaseEditorHandler editorHandler) {
625     GitRebaseEditorService service = GitRebaseEditorService.getInstance();
626     service.configureHandler(handler, editorHandler.getHandlerNo());
627     try {
628       GitCommandResult result = run(handler);
629       return editorHandler.wasEditorCancelled() ? toCancelledResult(result) : result;
630     }
631     finally {
632       service.unregisterHandler(editorHandler.getHandlerNo());
633     }
634   }
635
636   @NotNull
637   private static GitCommandResult toCancelledResult(@NotNull GitCommandResult result) {
638     int exitCode = result.getExitCode() == 0 ? 1 : result.getExitCode();
639     return new GitCommandResult(false, exitCode, result.getErrorOutput(), result.getOutput(), result.getException()) {
640       @Override
641       public boolean cancelled() {
642         return true;
643       }
644     };
645   }
646
647   @VisibleForTesting
648   @NotNull
649   protected GitInteractiveRebaseEditorHandler createEditor(@NotNull Project project,
650                                                            @NotNull VirtualFile root,
651                                                            @NotNull GitLineHandler handler,
652                                                            boolean commitListAware) {
653     GitInteractiveRebaseEditorHandler editor = new GitInteractiveRebaseEditorHandler(GitRebaseEditorService.getInstance(), project, root);
654     if (!commitListAware) {
655       editor.setRebaseEditorShown();
656     }
657     return editor;
658   }
659
660   @NotNull
661   @Override
662   public GitCommandResult revert(@NotNull GitRepository repository,
663                                  @NotNull String commit,
664                                  boolean autoCommit,
665                                  @NotNull GitLineHandlerListener... listeners) {
666     GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REVERT);
667     handler.addParameters(commit);
668     if (!autoCommit) {
669       handler.addParameters("--no-commit");
670     }
671     addListeners(handler, listeners);
672     return run(handler);
673   }
674
675   @NotNull
676   private static GitCommandResult doLsRemote(@NotNull final Project project,
677                                              @NotNull final File workingDir,
678                                              @NotNull final String remoteId,
679                                              @NotNull final Collection<String> authenticationUrls,
680                                              final String... additionalParameters) {
681     return run(() -> {
682       GitLineHandler h = new GitLineHandler(project, workingDir, GitCommand.LS_REMOTE);
683       h.addParameters(additionalParameters);
684       h.addParameters(remoteId);
685       h.setUrls(authenticationUrls);
686       return h;
687     });
688   }
689
690   private static void addListeners(@NotNull GitLineHandler handler, @NotNull GitLineHandlerListener... listeners) {
691     addListeners(handler, Arrays.asList(listeners));
692   }
693
694   private static void addListeners(@NotNull GitLineHandler handler, @NotNull List<GitLineHandlerListener> listeners) {
695     for (GitLineHandlerListener listener : listeners) {
696       handler.addLineListener(listener);
697     }
698   }
699
700   @NotNull
701   private static GitCommandResult run(@NotNull Computable<GitLineHandler> handlerConstructor) {
702     final List<String> errorOutput = new ArrayList<>();
703     final List<String> output = new ArrayList<>();
704     final AtomicInteger exitCode = new AtomicInteger();
705     final AtomicBoolean startFailed = new AtomicBoolean();
706     final AtomicReference<Throwable> exception = new AtomicReference<>();
707
708     int authAttempt = 0;
709     boolean authFailed;
710     boolean success;
711     do {
712       errorOutput.clear();
713       output.clear();
714       exitCode.set(0);
715       startFailed.set(false);
716       exception.set(null);
717
718       GitLineHandler handler = handlerConstructor.compute();
719       handler.addLineListener(new GitLineHandlerListener() {
720         @Override public void onLineAvailable(String line, Key outputType) {
721           if (looksLikeError(line)) {
722             synchronized (errorOutput) {
723               errorOutput.add(line);
724             }
725           } else {
726             synchronized (output) {
727               output.add(line);
728             }
729           }
730         }
731
732         @Override public void processTerminated(int code) {
733           exitCode.set(code);
734         }
735
736         @Override public void startFailed(Throwable t) {
737           startFailed.set(true);
738           errorOutput.add("Failed to start Git process");
739           exception.set(t);
740         }
741       });
742
743       handler.runInCurrentThread(null);
744       authFailed = handler.hasHttpAuthFailed();
745       success = !startFailed.get() && (handler.isIgnoredErrorCode(exitCode.get()) || exitCode.get() == 0);
746     }
747     while (authFailed && authAttempt++ < 2);
748     return new GitCommandResult(success, exitCode.get(), errorOutput, output, null);
749   }
750
751   /**
752    * Runs the given {@link GitLineHandler} in the current thread and returns the {@link GitCommandResult}.
753    */
754   @NotNull
755   private static GitCommandResult run(@NotNull GitLineHandler handler) {
756     return run(new Computable.PredefinedValueComputable<>(handler));
757   }
758
759   @Override
760   @NotNull
761   public GitCommandResult runCommand(@NotNull Computable<GitLineHandler> handlerConstructor) {
762     return run(handlerConstructor);
763   }
764
765   @NotNull
766   @Override
767   public GitCommandResult runCommand(@NotNull final GitLineHandler handler) {
768     return runCommand(() -> handler);
769   }
770
771   @NotNull
772   private static GitCommandResult runAll(@NotNull List<Computable<GitCommandResult>> commands) {
773     if (commands.isEmpty()) {
774       LOG.error("List of commands should not be empty", new Exception());
775       return GitCommandResult.error("Internal error");
776     }
777     GitCommandResult compoundResult = null;
778     for (Computable<GitCommandResult> command : commands) {
779       compoundResult = GitCommandResult.merge(compoundResult, command.compute());
780     }
781     return ObjectUtils.assertNotNull(compoundResult);
782   }
783
784   private static boolean looksLikeError(@NotNull final String text) {
785     return ContainerUtil.exists(ERROR_INDICATORS, indicator -> StringUtil.startsWithIgnoreCase(text.trim(), indicator));
786   }
787
788   // could be upper-cased, so should check case-insensitively
789   public static final String[] ERROR_INDICATORS = {
790     "error:", "remote: error", "fatal:",
791     "Cannot", "Could not", "Interactive rebase already started", "refusing to pull", "cannot rebase:", "conflict",
792     "unable"
793   };
794 }