@Override
  protected void save(@NotNull Collection<VirtualFile> rootsToSave) throws VcsException {
    LOG.info("saving " + rootsToSave);

    for (VirtualFile root : rootsToSave) {
      final String message = GitHandlerUtil.formatOperationName("Stashing changes from", root);
      LOG.info(message);
      final String oldProgressTitle = myProgressIndicator.getText();
      myProgressIndicator.setText(message);
      GitRepository repository = myRepositoryManager.getRepositoryForRoot(root);
      if (repository == null) {
        LOG.error("Repository is null for root " + root);
      } else {
        GitCommandResult result = myGit.stashSave(repository, myStashMessage);
        if (result.success() && somethingWasStashed(result)) {
          myStashedRoots.add(root);
        } else {
          String error =
              "stash " + repository.getRoot() + ": " + result.getErrorOutputAsJoinedString();
          if (!result.success()) {
            throw new VcsException(error);
          } else {
            LOG.warn(error);
          }
        }
      }
      myProgressIndicator.setText(oldProgressTitle);
    }
  }
  private boolean doDeleteRemote(
      @NotNull String branchName, @NotNull Collection<GitRepository> repositories) {
    Couple<String> pair = splitNameOfRemoteBranch(branchName);
    String remoteName = pair.getFirst();
    String branch = pair.getSecond();

    GitCompoundResult result = new GitCompoundResult(myProject);
    for (GitRepository repository : repositories) {
      GitCommandResult res;
      GitRemote remote = getRemoteByName(repository, remoteName);
      if (remote == null) {
        String error = "Couldn't find remote by name: " + remoteName;
        LOG.error(error);
        res = GitCommandResult.error(error);
      } else {
        res = pushDeletion(repository, remote, branch);
        if (!res.success() && isAlreadyDeletedError(res.getErrorOutputAsJoinedString())) {
          res = myGit.remotePrune(repository, remote);
        }
      }
      result.append(repository, res);
      repository.update();
    }
    if (!result.totalSuccess()) {
      VcsNotifier.getInstance(myProject)
          .notifyError(
              "Failed to delete remote branch " + branchName,
              result.getErrorOutputWithReposIndication());
    }
    return result.totalSuccess();
  }
  /**
   * Returns absolute paths which have changed remotely comparing to the current branch, i.e.
   * performs <code>git diff --name-only master..origin/master</code>
   *
   * <p>Paths are absolute, Git-formatted (i.e. with forward slashes).
   */
  @NotNull
  public static Collection<String> getPathsDiffBetweenRefs(
      @NotNull Git git,
      @NotNull GitRepository repository,
      @NotNull String beforeRef,
      @NotNull String afterRef)
      throws VcsException {
    List<String> parameters = Arrays.asList("--name-only", "--pretty=format:");
    String range = beforeRef + ".." + afterRef;
    GitCommandResult result = git.diff(repository, parameters, range);
    if (!result.success()) {
      LOG.info(
          String.format(
              "Couldn't get diff in range [%s] for repository [%s]",
              range, repository.toLogString()));
      return Collections.emptyList();
    }

    final Collection<String> remoteChanges = new HashSet<String>();
    for (StringScanner s = new StringScanner(result.getOutputAsJoinedString()); s.hasMoreData(); ) {
      final String relative = s.line();
      if (StringUtil.isEmptyOrSpaces(relative)) {
        continue;
      }
      final String path = repository.getRoot().getPath() + "/" + unescapePath(relative);
      remoteChanges.add(path);
    }
    return remoteChanges;
  }
  /**
   * Returns true if the root was loaded with conflict. False is returned in all other cases: in the
   * case of success and in case of some other error.
   */
  private boolean loadRoot(final VirtualFile root) {
    LOG.info("loadRoot " + root);
    myProgressIndicator.setText(GitHandlerUtil.formatOperationName("Unstashing changes to", root));

    GitRepository repository = myRepositoryManager.getRepositoryForRoot(root);
    if (repository == null) {
      LOG.error("Repository is null for root " + root);
      return false;
    }

    GitSimpleEventDetector conflictDetector =
        new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT_ON_UNSTASH);
    GitCommandResult result = myGit.stashPop(repository, conflictDetector);
    VfsUtil.markDirtyAndRefresh(false, true, false, root);
    if (result.success()) {
      return false;
    } else if (conflictDetector.hasHappened()) {
      return true;
    } else {
      LOG.info("unstash failed " + result.getErrorOutputAsJoinedString());
      GitUIUtil.notifyImportantError(
          myProject, "Couldn't unstash", "<br/>" + result.getErrorOutputAsHtmlString());
      return false;
    }
  }
  private static boolean pushCurrentBranch(
      @NotNull Project project,
      @NotNull GitRepository repository,
      @NotNull String remoteName,
      @NotNull String remoteUrl,
      @NotNull String name,
      @NotNull String url) {
    Git git = ServiceManager.getService(Git.class);

    GitLocalBranch currentBranch = repository.getCurrentBranch();
    if (currentBranch == null) {
      GithubNotifications.showErrorURL(
          project,
          "Can't finish GitHub sharing process",
          "Successfully created project ",
          "'" + name + "'",
          " on GitHub, but initial push failed: no current branch",
          url);
      return false;
    }
    GitCommandResult result =
        git.push(repository, remoteName, remoteUrl, currentBranch.getName(), true);
    if (!result.success()) {
      GithubNotifications.showErrorURL(
          project,
          "Can't finish GitHub sharing process",
          "Successfully created project ",
          "'" + name + "'",
          " on GitHub, but initial push failed:<br/>" + result.getErrorOutputAsHtmlString(),
          url);
      return false;
    }
    return true;
  }
  /**
   * Tries to execute {@code git merge --ff-only}.
   *
   * @return true, if everything is successful; false for any error (to let a usual "fair" update
   *     deal with it).
   */
  public boolean fastForwardMerge() {
    LOG.info("Trying fast-forward merge for " + myRoot);
    GitRepository repository = GitUtil.getRepositoryManager(myProject).getRepositoryForRoot(myRoot);
    if (repository == null) {
      LOG.error("Repository is null for " + myRoot);
      return false;
    }
    try {
      markStart(myRoot);
    } catch (VcsException e) {
      LOG.info("Couldn't mark start for repository " + myRoot, e);
      return false;
    }

    GitCommandResult result =
        myGit.merge(repository, getRemoteBranchToMerge(), Collections.singletonList("--ff-only"));

    try {
      markEnd(myRoot);
    } catch (VcsException e) {
      // this is not critical, and update has already happened,
      // so we just notify the user about problems with collecting the updated changes.
      LOG.info("Couldn't mark end for repository " + myRoot, e);
      Notificator.getInstance(myProject)
          .notifyWeakWarning(
              "Couldn't collect the updated files info",
              String.format(
                  "Update of %s was successful, but we couldn't collect the updated changes because of an error",
                  myRoot),
              null);
    }
    return result.success();
  }
 private boolean wasFileTouched(@NotNull GitRepository repository, @NotNull GitFileRevision rev)
     throws VcsException {
   GitCommandResult result = myGit.show(repository, rev.getHash());
   if (result.success()) {
     return isFilePresentInOutput(repository, rev.getPath(), result.getOutput());
   }
   throw new VcsException(result.getErrorOutputAsJoinedString());
 }
 protected static void checkGitResult(GitCommandResult commandResult)
     throws ServerRuntimeException {
   if (!commandResult.success()) {
     Throwable exception = commandResult.getException();
     if (exception != null) {
       LOG.info(exception);
       throw new ServerRuntimeException(exception);
     } else {
       throw new ServerRuntimeException(commandResult.getErrorOutputAsJoinedString());
     }
   }
 }
  private ListenableFuture<Pair<String, GitCommandResult>> doPushCommits(
      @NotNull final GitRepository gitRepository,
      @NotNull final GitLocalBranch localBranch,
      @NotNull final GitRemote gitRemote,
      @NotNull final ProgressIndicator indicator) {
    // just set the result without going off to another thread, we should already be in a background
    // task
    SettableFuture<Pair<String, GitCommandResult>> pushResult =
        SettableFuture.<Pair<String, GitCommandResult>>create();

    indicator.setText(TfPluginBundle.message(TfPluginBundle.KEY_CREATE_PR_PUSH_TITLE));
    final Git git = ServiceManager.getService(Git.class);

    final GitRemoteBranch trackingBranch = localBranch.findTrackedBranch(gitRepository);

    final String createdBranchNameOnServer;
    final StringBuilder pushSpec = new StringBuilder(localBranch.getName());
    if (trackingBranch != null && trackingBranch.getRemote().equals(gitRemote)) {
      // if the tracking branch is on the same remote, we should update that
      pushSpec.append(":").append(trackingBranch.getNameForRemoteOperations());
      createdBranchNameOnServer = trackingBranch.getNameForRemoteOperations();
    } else {
      createdBranchNameOnServer = localBranch.getName();
    }

    final String fetchUrl = getFetchUrl(gitRemote);
    final String pushSpecStr = pushSpec.toString();
    final String gitRemoteName = gitRemote.getName();
    logger.debug("Pushing {} to {}: {}", pushSpecStr, gitRemoteName, fetchUrl);
    final GitCommandResult result =
        git.push(gitRepository, gitRemoteName, fetchUrl, pushSpecStr, true);

    if (result.success()) {
      pushResult.set(Pair.create(createdBranchNameOnServer, result));
    } else {
      final String errMsg = result.getErrorOutputAsJoinedString();
      pushResult.setException(new GitExecutionException(errMsg, null));
    }

    return pushResult;
  }
  /**
   * Parse changes from lines
   *
   * @param root the git root
   * @return a set of unmerged files
   * @throws com.intellij.openapi.vcs.VcsException if the input format does not matches expected
   *     format
   */
  private List<VirtualFile> unmergedFiles(final VirtualFile root) throws VcsException {
    GitRepository repository = myRepositoryManager.getRepositoryForRoot(root);
    if (repository == null) {
      LOG.error("Repository not found for root " + root);
      return Collections.emptyList();
    }

    GitCommandResult result = myGit.getUnmergedFiles(repository);
    if (!result.success()) {
      throw new VcsException(result.getErrorOutputAsJoinedString());
    }

    String output = StringUtil.join(result.getOutput(), "\n");
    HashSet<String> unmergedPaths = ContainerUtil.newHashSet();
    for (StringScanner s = new StringScanner(output); s.hasMoreData(); ) {
      if (s.isEol()) {
        s.nextLine();
        continue;
      }
      s.boundedToken('\t');
      String relative = s.line();
      unmergedPaths.add(GitUtil.unescapePath(relative));
    }

    if (unmergedPaths.size() == 0) {
      return Collections.emptyList();
    } else {
      List<File> files =
          ContainerUtil.map(
              unmergedPaths,
              new Function<String, File>() {
                @Override
                public File fun(String path) {
                  return new File(root.getPath(), path);
                }
              });
      return sortVirtualFilesByPresentation(findVirtualFilesWithRefresh(files));
    }
  }
 private static boolean somethingWasStashed(@NotNull GitCommandResult result) {
   return !StringUtil.containsIgnoreCase(
           result.getErrorOutputAsJoinedString(), NO_LOCAL_CHANGES_TO_SAVE)
       && !StringUtil.containsIgnoreCase(
           result.getOutputAsJoinedString(), NO_LOCAL_CHANGES_TO_SAVE);
 }