final public function requireCleanWorkingCopy()

in src/workflow/ArcanistWorkflow.php [845:1080]


  final public function requireCleanWorkingCopy() {
    $api = $this->getRepositoryAPI();

    $must_commit = array();

    $working_copy_desc = phutil_console_format(
      "  %s: __%s__\n\n",
      pht('Working copy'),
      $api->getPath());

    // NOTE: this is a subversion-only concept.
    $incomplete = $api->getIncompleteChanges();
    if ($incomplete) {
      throw new ArcanistUsageException(
        sprintf(
          "%s\n\n%s  %s\n    %s\n\n%s",
          pht(
            "You have incompletely checked out directories in this working ".
            "copy. Fix them before proceeding.'"),
          $working_copy_desc,
          pht('Incomplete directories in working copy:'),
          implode("\n    ", $incomplete),
          pht(
            "You can fix these paths by running '%s' on them.",
            'svn update')));
    }

    $conflicts = $api->getMergeConflicts();
    if ($conflicts) {
      throw new ArcanistUsageException(
        sprintf(
          "%s\n\n%s  %s\n    %s",
          pht(
            'You have merge conflicts in this working copy. Resolve merge '.
            'conflicts before proceeding.'),
          $working_copy_desc,
          pht('Conflicts in working copy:'),
          implode("\n    ", $conflicts)));
    }

    $missing = $api->getMissingChanges();
    if ($missing) {
      throw new ArcanistUsageException(
        sprintf(
          "%s\n\n%s  %s\n    %s\n",
          pht(
            'You have missing files in this working copy. Revert or formally '.
            'remove them (with `%s`) before proceeding.',
            'svn rm'),
          $working_copy_desc,
          pht('Missing files in working copy:'),
          implode("\n    ", $missing)));
    }

    $externals = $api->getDirtyExternalChanges();

    // TODO: This state can exist in Subversion, but it is currently handled
    // elsewhere. It should probably be handled here, eventually.
    if ($api instanceof ArcanistSubversionAPI) {
      $externals = array();
    }

    if ($externals) {
      $message = pht(
        '%s submodule(s) have uncommitted or untracked changes:',
        new PhutilNumber(count($externals)));

      $prompt = pht(
        'Ignore the changes to these %s submodule(s) and continue?',
        new PhutilNumber(count($externals)));

      $list = id(new PhutilConsoleList())
        ->setWrap(false)
        ->addItems($externals);

      id(new PhutilConsoleBlock())
        ->addParagraph($message)
        ->addList($list)
        ->draw();

      $ok = phutil_console_confirm($prompt, $default_no = false);
      if (!$ok) {
        throw new ArcanistUserAbortException();
      }
    }

    $uncommitted = $api->getUncommittedChanges();
    $unstaged = $api->getUnstagedChanges();

    // We already dealt with externals.
    $unstaged = array_diff($unstaged, $externals);

    // We only want files which are purely uncommitted.
    $uncommitted = array_diff($uncommitted, $unstaged);
    $uncommitted = array_diff($uncommitted, $externals);

    $untracked = $api->getUntrackedChanges();
    if (!$this->shouldRequireCleanUntrackedFiles()) {
      $untracked = array();
    }

    if ($untracked) {
      echo sprintf(
        "%s\n\n%s",
        pht('You have untracked files in this working copy.'),
        $working_copy_desc);

      if ($api instanceof ArcanistGitAPI) {
        $hint = pht(
          '(To ignore these %s change(s), add them to "%s".)',
          phutil_count($untracked),
          '.git/info/exclude');
      } else if ($api instanceof ArcanistSubversionAPI) {
        $hint = pht(
          '(To ignore these %s change(s), add them to "%s".)',
          phutil_count($untracked),
          'svn:ignore');
      } else if ($api instanceof ArcanistMercurialAPI) {
        $hint = pht(
          '(To ignore these %s change(s), add them to "%s".)',
          phutil_count($untracked),
          '.hgignore');
      }

      $untracked_list = "    ".implode("\n    ", $untracked);
      echo sprintf(
        "  %s\n  %s\n%s",
        pht('Untracked changes in working copy:'),
        $hint,
        $untracked_list);

      $prompt = pht(
        'Ignore these %s untracked file(s) and continue?',
        phutil_count($untracked));

      if (!phutil_console_confirm($prompt)) {
        throw new ArcanistUserAbortException();
      }
    }


    $should_commit = false;
    if ($unstaged || $uncommitted) {

      // NOTE: We're running this because it builds a cache and can take a
      // perceptible amount of time to arrive at an answer, but we don't want
      // to pause in the middle of printing the output below.
      $this->getShouldAmend();

      echo sprintf(
        "%s\n\n%s",
        pht('You have uncommitted changes in this working copy.'),
        $working_copy_desc);

      $lists = array();

      if ($unstaged) {
        $unstaged_list = "    ".implode("\n    ", $unstaged);
        $lists[] = sprintf(
          "  %s\n%s",
          pht('Unstaged changes in working copy:'),
          $unstaged_list);
      }

      if ($uncommitted) {
        $uncommitted_list = "    ".implode("\n    ", $uncommitted);
        $lists[] = sprintf(
          "%s\n%s",
          pht('Uncommitted changes in working copy:'),
          $uncommitted_list);
      }

      echo implode("\n\n", $lists)."\n";

      $all_uncommitted = array_merge($unstaged, $uncommitted);
      if ($this->askForAdd($all_uncommitted)) {
        if ($unstaged) {
          $api->addToCommit($unstaged);
        }
        $should_commit = true;
      } else {
        $permit_autostash = $this->getConfigFromAnySource('arc.autostash');
        if ($permit_autostash && $api->canStashChanges()) {
           echo pht(
            'Stashing uncommitted changes. (You can restore them with `%s`).',
            'git stash pop')."\n";
          $api->stashChanges();
          $this->stashed = true;
        } else {
          throw new ArcanistUsageException(
            pht(
              'You can not continue with uncommitted changes. '.
              'Commit or discard them before proceeding.'));
        }
      }
    }

    if ($should_commit) {
      if ($this->getShouldAmend()) {
        $commit = head($api->getLocalCommitInformation());
        $api->amendCommit($commit['message']);
      } else if ($api->supportsLocalCommits()) {
        $template = sprintf(
          "\n\n# %s\n#\n# %s\n#\n",
          pht('Enter a commit message.'),
          pht('Changes:'));

        $paths = array_merge($uncommitted, $unstaged);
        $paths = array_unique($paths);
        sort($paths);

        foreach ($paths as $path) {
          $template .= "#     ".$path."\n";
        }

        $commit_message = $this->newInteractiveEditor($template)
          ->setName(pht('commit-message'))
          ->editInteractively();

        if ($commit_message === $template) {
          throw new ArcanistUsageException(
            pht('You must provide a commit message.'));
        }

        $commit_message = ArcanistCommentRemover::removeComments(
          $commit_message);

        if (!strlen($commit_message)) {
          throw new ArcanistUsageException(
            pht('You must provide a nonempty commit message.'));
        }

        $api->doCommit($commit_message);
      }
    }
  }