private @NotNull GitRebaseStatus rebaseSingleRoot(@NotNull GitRepository repository,
                                                    @Nullable GitRebaseResumeMode customMode,
                                                    @NotNull Map<GitRepository, GitSuccessfulRebase> alreadyRebased,
                                                    @NotNull ProgressIndicator indicator) {
    VirtualFile root = repository.getRoot();
    String repoName = getShortRepositoryName(repository);
    LOG.info("Rebasing root " + repoName + ", mode: " + notNull(customMode, "standard"));

    boolean retryWhenDirty = false;

    int commitsToRebase = 0;
    try {
      GitRebaseParams params = myRebaseSpec.getParams();
      if (params != null) {
        String upstream = params.getUpstream();
        String branch = params.getBranch();
        commitsToRebase = GitRebaseUtils.getNumberOfCommitsToRebase(repository, upstream, branch);
      }
    }
    catch (VcsException e) {
      LOG.warn("Couldn't get the number of commits to rebase", e);
    }
    GitRebaseProgressListener progressListener = new GitRebaseProgressListener(commitsToRebase, indicator);

    while (true) {
      GitRebaseProblemDetector rebaseDetector = new GitRebaseProblemDetector();
      GitUntrackedFilesOverwrittenByOperationDetector untrackedDetector = new GitUntrackedFilesOverwrittenByOperationDetector(root);
      GitRebaseCommandResult rebaseCommandResult = callRebase(repository, customMode, rebaseDetector, untrackedDetector, progressListener);
      GitCommandResult result = rebaseCommandResult.getCommandResult();

      boolean somethingRebased = customMode != null || progressListener.currentCommit > 1;

      if (rebaseCommandResult.wasCancelledInCommitList()) {
        return GitRebaseStatus.notStarted();
      }
      else if (rebaseCommandResult.wasCancelledInCommitMessage()) {
        showStoppedForEditingMessage();
        return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED);
      }
      else if (result.success()) {
        if (rebaseDetector.hasStoppedForEditing()) {
          showStoppedForEditingMessage();
          return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED);
        }
        LOG.debug("Successfully rebased " + repoName);
        return new GitSuccessfulRebase();
      }
      else if (rebaseDetector.isDirtyTree() && customMode == null && !retryWhenDirty) {
        // if the initial dirty tree check doesn't find all local changes, we are still ready to stash-on-demand,
        // but only once per repository (if the error happens again, that means that the previous stash attempt failed for some reason),
        // and not in the case of --continue (where all local changes are expected to be committed) or --skip.
        LOG.debug("Dirty tree detected in " + repoName);
        String saveError = saveLocalChanges(singleton(repository.getRoot()));
        if (saveError == null) {
          retryWhenDirty = true; // try same repository again
        }
        else {
          LOG.warn(String.format(
            "Couldn't %s root %s: %s",
            mySaver.getSaveMethod() == GitSaveChangesPolicy.SHELVE ? "shelve" : "stash",
            repository.getRoot(),
            saveError
          ));
          showFatalError(saveError, repository, somethingRebased, alreadyRebased.keySet());
          GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR;
          return new GitRebaseStatus(type);
        }
      }
      else if (untrackedDetector.wasMessageDetected()) {
        LOG.info("Untracked files detected in " + repoName);
        showUntrackedFilesError(untrackedDetector.getRelativeFilePaths(), repository, somethingRebased, alreadyRebased.keySet());
        GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR;
        return new GitRebaseStatus(type);
      }
      else if (rebaseDetector.isNoChangeError()) {
        LOG.info("'No changes' situation detected in " + repoName);
        customMode = GitRebaseResumeMode.SKIP;
      }
      else if (rebaseDetector.isMergeConflict()) {
        LOG.info("Merge conflict in " + repoName);
        ResolveConflictResult resolveResult = showConflictResolver(repository, false);
        if (resolveResult == ResolveConflictResult.ALL_RESOLVED) {
          customMode = GitRebaseResumeMode.CONTINUE;
        }
        else if (resolveResult == ResolveConflictResult.NOTHING_TO_MERGE) {
          // the output is the same for the cases:
          // (1) "unresolved conflicts"
          // (2) "manual editing of a file not followed by `git add`
          // => we check if there are any unresolved conflicts, and if not, then it is the case #2 which we are not handling
          LOG.info("Unmerged changes while rebasing root " + repoName + ": " + result.getErrorOutputAsJoinedString());
          showFatalError(result.getErrorOutputAsHtmlString(), repository, somethingRebased, alreadyRebased.keySet());
          GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR;
          return new GitRebaseStatus(type);
        }
        else {
          notifyNotAllConflictsResolved(repository);
          return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED);
        }
      }
      else {
        String error = getErrorMessage(rebaseCommandResult, result, repoName);
        showFatalError(error, repository, somethingRebased, alreadyRebased.keySet());
        GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR;
        return new GitRebaseStatus(type);
      }
    }
  }
private @NotNull GitRebaseStatus rebaseSingleRoot(@NotNull GitRepository repository,
                                                    @Nullable GitRebaseResumeMode customMode,
                                                    @NotNull Map<GitRepository, GitSuccessfulRebase> alreadyRebased,
                                                    @NotNull ProgressIndicator indicator)