arcanistConfiguration = $arcanist_configuration; return $this; } public function getArcanistConfiguration() { return $this->arcanistConfiguration; } public function getCommandHelp() { return get_class($this).": Undocumented"; } public function requiresWorkingCopy() { return false; } public function requiresConduit() { return false; } public function requiresAuthentication() { return false; } public function requiresRepositoryAPI() { return false; } public function setCommand($command) { $this->command = $command; return $this; } public function getCommand() { return $this->command; } public function setUserName($user_name) { $this->userName = $user_name; return $this; } public function getUserName() { return $this->userName; } public function getArguments() { return array(); } private function setParentWorkflow($parent_workflow) { $this->parentWorkflow = $parent_workflow; return $this; } protected function getParentWorkflow() { return $this->parentWorkflow; } public function buildChildWorkflow($command, array $argv) { $arc_config = $this->getArcanistConfiguration(); $workflow = $arc_config->buildWorkflow($command); $workflow->setParentWorkflow($this); $workflow->setCommand($command); if ($this->repositoryAPI) { $workflow->setRepositoryAPI($this->repositoryAPI); } if ($this->userGUID) { $workflow->setUserGUID($this->getUserGUID()); $workflow->setUserName($this->getUserName()); } if ($this->conduit) { $workflow->setConduit($this->conduit); } if ($this->workingCopy) { $workflow->setWorkingCopy($this->workingCopy); } $workflow->setArcanistConfiguration($arc_config); $workflow->parseArguments(array_values($argv)); return $workflow; } public function getArgument($key, $default = null) { $args = $this->arguments; if (!array_key_exists($key, $args)) { return $default; } return $args[$key]; } final public function getCompleteArgumentSpecification() { $spec = $this->getArguments(); $arc_config = $this->getArcanistConfiguration(); $command = $this->getCommand(); $spec += $arc_config->getCustomArgumentsForCommand($command); return $spec; } public function parseArguments(array $args) { $spec = $this->getCompleteArgumentSpecification(); $dict = array(); $more_key = null; if (!empty($spec['*'])) { $more_key = $spec['*']; unset($spec['*']); $dict[$more_key] = array(); } $short_to_long_map = array(); foreach ($spec as $long => $options) { if (!empty($options['short'])) { $short_to_long_map[$options['short']] = $long; } } $more = array(); for ($ii = 0; $ii < count($args); $ii++) { $arg = $args[$ii]; $arg_name = null; $arg_key = null; if ($arg == '--') { $more = array_merge( $more, array_slice($args, $ii + 1)); break; } else if (!strncmp($arg, '--', 2)) { $arg_key = substr($arg, 2); if (!array_key_exists($arg_key, $spec)) { throw new ArcanistUsageException( "Unknown argument '{$arg_key}'. Try 'arc help'."); } } else if (!strncmp($arg, '-', 1)) { $arg_key = substr($arg, 1); if (empty($short_to_long_map[$arg_key])) { throw new ArcanistUsageException( "Unknown argument '{$arg_key}'. Try 'arc help'."); } $arg_key = $short_to_long_map[$arg_key]; } else { $more[] = $arg; continue; } $options = $spec[$arg_key]; if (empty($options['param'])) { $dict[$arg_key] = true; } else { if ($ii == count($args) - 1) { throw new ArcanistUsageException( "Option '{$arg}' requires a parameter."); } $dict[$arg_key] = $args[$ii + 1]; $ii++; } } if ($more) { if ($more_key) { $dict[$more_key] = $more; } else { $example = reset($more); throw new ArcanistUsageException( "Unrecognized argument '{$example}'. Try 'arc help'."); } } foreach ($dict as $key => $value) { if (empty($spec[$key]['conflicts'])) { continue; } foreach ($spec[$key]['conflicts'] as $conflict => $more) { if (isset($dict[$conflict])) { if ($more) { $more = ': '.$more; } else { $more = '.'; } // TODO: We'll always display these as long-form, when the user might // have typed them as short form. throw new ArcanistUsageException( "Arguments '--{$key}' and '--{$conflict}' are mutually exclusive". $more); } } } $this->arguments = $dict; $this->didParseArguments(); return $this; } protected function didParseArguments() { // Override this to customize workflow argument behavior. } public function getWorkingCopy() { if (!$this->workingCopy) { $workflow = get_class($this); throw new Exception( "This workflow ('{$workflow}') requires a working copy, override ". "requiresWorkingCopy() to return true."); } return $this->workingCopy; } public function setWorkingCopy( ArcanistWorkingCopyIdentity $working_copy) { $this->workingCopy = $working_copy; return $this; } public function getConduit() { if (!$this->conduit) { $workflow = get_class($this); throw new Exception( "This workflow ('{$workflow}') requires a Conduit, override ". "requiresConduit() to return true."); } return $this->conduit; } public function setConduit(ConduitClient $conduit) { $this->conduit = $conduit; return $this; } public function getUserGUID() { if (!$this->userGUID) { $workflow = get_class($this); throw new Exception( "This workflow ('{$workflow}') requires authentication, override ". "requiresAuthentication() to return true."); } return $this->userGUID; } public function setUserGUID($guid) { $this->userGUID = $guid; return $this; } public function setRepositoryAPI($api) { $this->repositoryAPI = $api; return $this; } public function getRepositoryAPI() { if (!$this->repositoryAPI) { $workflow = get_class($this); throw new Exception( "This workflow ('{$workflow}') requires a Repository API, override ". "requiresRepositoryAPI() to return true."); } return $this->repositoryAPI; } protected function shouldRequireCleanUntrackedFiles() { return empty($this->arguments['allow-untracked']); } protected function requireCleanWorkingCopy() { $api = $this->getRepositoryAPI(); $untracked = $api->getUntrackedChanges(); if ($this->shouldRequireCleanUntrackedFiles()) { if (!empty($untracked)) { throw new ArcanistUsageException( "You have untracked files in this working copy:\n". " ".implode("\n ", $untracked)."\n\n". "Add or delete them before proceeding, or include them in your ". "ignore rules. To bypass this check, use --allow-untracked."); } } if ($api->getMergeConflicts()) { throw new ArcanistUsageException( "You have merge conflicts in this working copy. Resolve merge ". "conflicts before proceeding."); } if ($api->getUnstagedChanges()) { throw new ArcanistUsageException( "You have unstaged changes in this branch. Stage and commit (or ". "revert) them before proceeding."); } if ($api->getUncommittedChanges()) { throw new ArcanistUsageException( "You have uncommitted changes in this branch. Commit (or revert) them ". "before proceeding."); } } protected function chooseRevision( array $revision_data, $revision_id, $prompt = null) { $revisions = array(); foreach ($revision_data as $data) { $ref = ArcanistDifferentialRevisionRef::newFromDictionary($data); $revisions[$ref->getID()] = $ref; } if ($revision_id) { $revision_id = $this->normalizeRevisionID($revision_id); if (empty($revisions[$revision_id])) { throw new ArcanistChooseInvalidRevisionException(); } return $revisions[$revision_id]; } if (!count($revisions)) { throw new ArcanistChooseNoRevisionsException(); } $repository_api = $this->getRepositoryAPI(); $candidates = array(); $cur_path = $repository_api->getPath(); foreach ($revisions as $revision) { $source_path = $revision->getSourcePath(); if ($source_path == $cur_path) { $candidates[] = $revision; } } if (count($candidates) == 1) { $candidate = reset($candidates); $revision_id = $candidate->getID(); } if ($revision_id) { return $revisions[$revision_id]; } $revision_indexes = array_keys($revisions); echo "\n"; $ii = 1; foreach ($revisions as $revision) { echo ' ['.$ii++.'] D'.$revision->getID().' '.$revision->getName()."\n"; } while (true) { $id = phutil_console_prompt($prompt); $id = trim(strtoupper($id), 'D'); if (isset($revisions[$id])) { return $revisions[$id]; } if (isset($revision_indexes[$id - 1])) { return $revisions[$revision_indexes[$id - 1]]; } } } protected function loadDiffBundleFromConduit( ConduitClient $conduit, $diff_id) { return $this->loadBundleFromConduit( $conduit, array( 'diff_id' => $diff_id, )); } protected function loadRevisionBundleFromConduit( ConduitClient $conduit, $revision_id) { return $this->loadBundleFromConduit( $conduit, array( 'revision_id' => $revision_id, )); } private function loadBundleFromConduit( ConduitClient $conduit, $params) { $future = $conduit->callMethod('differential.getdiff', $params); $diff = $future->resolve(); $changes = array(); foreach ($diff['changes'] as $changedict) { $changes[] = ArcanistDiffChange::newFromDictionary($changedict); } $bundle = ArcanistBundle::newFromChanges($changes); return $bundle; } protected function getChangedLines($path, $mode) { if (is_dir($path)) { return array(); } $change = $this->getChange($path); $lines = $change->getChangedLines($mode); return array_keys($lines); } private function getChange($path) { $repository_api = $this->getRepositoryAPI(); if ($repository_api instanceof ArcanistSubversionAPI) { if (empty($this->changeCache[$path])) { $diff = $repository_api->getRawDiffText($path); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($diff); if (count($changes) != 1) { throw new Exception("Expected exactly one change."); } $this->changeCache[$path] = reset($changes); } } else { if (empty($this->changeCache)) { $diff = $repository_api->getFullGitDiff(); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($diff); foreach ($changes as $change) { $this->changeCache[$change->getCurrentPath()] = $change; } } } if (empty($this->changeCache[$path])) { if ($repository_api instanceof ArcanistGitAPI) { // This can legitimately occur under git if you make a change, "git // commit" it, and then revert the change in the working copy and run // "arc lint". $change = new ArcanistDiffChange(); $change->setCurrentPath($path); return $change; } else { throw new Exception( "Trying to get change for unchanged path '{$path}'!"); } } return $this->changeCache[$path]; } final public function willRunWorkflow() { $spec = $this->getCompleteArgumentSpecification(); foreach ($this->arguments as $arg => $value) { if (empty($spec[$arg])) { continue; } $options = $spec[$arg]; if (!empty($options['supports'])) { $system_name = $this->getRepositoryAPI()->getSourceControlSystemName(); if (!in_array($system_name, $options['supports'])) { $extended_info = null; if (!empty($options['nosupport'][$system_name])) { $extended_info = ' '.$options['nosupport'][$system_name]; } throw new ArcanistUsageException( "Option '--{$arg}' is not supported under {$system_name}.". $extended_info); } } } } protected function parseGitRelativeCommit(ArcanistGitAPI $api, array $argv) { if (count($argv) == 0) { return; } if (count($argv) != 1) { throw new ArcanistUsageException( "Specify exactly one commit."); } $base = reset($argv); if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) { $merge_base = $base; } else { list($err, $merge_base) = exec_manual( '(cd %s; git merge-base %s HEAD)', $api->getPath(), $base); if ($err) { throw new ArcanistUsageException( "Unable to parse git commit name '{$base}'."); } } $api->setRelativeCommit(trim($merge_base)); } protected function normalizeRevisionID($revision_id) { return ltrim(strtoupper($revision_id), 'D'); } protected function shouldShellComplete() { return true; } protected function getShellCompletions(array $argv) { return array(); } protected function getSupportedRevisionControlSystems() { return array('any'); } }