diff --git a/src/repository/parser/ArcanistMercurialParser.php b/src/repository/parser/ArcanistMercurialParser.php index 8317cc7c..4eb10e2f 100644 --- a/src/repository/parser/ArcanistMercurialParser.php +++ b/src/repository/parser/ArcanistMercurialParser.php @@ -1,236 +1,240 @@ $flags, 'from' => null, ); $last_path = $path; } return $result; } /** * Parse the output of "hg status". This provides only basic information, you * can get more detailed information by invoking * @{method:parseMercurialStatusDetails}. * * @param string The stdout from running an "hg status" command. * @return dict Map of paths to ArcanistRepositoryAPI status flags. * @task parse */ public static function parseMercurialStatus($stdout) { $result = self::parseMercurialStatusDetails($stdout); return ipull($result, 'flags'); } /** * Parse the output of "hg log". This also parses "hg outgoing", "hg parents", * and other similar commands. This assumes "--style default". * * @param string The stdout from running an "hg log" command. * @return list List of dictionaries with commit information. * @task parse */ public static function parseMercurialLog($stdout) { $result = array(); $stdout = trim($stdout); if (!strlen($stdout)) { return $result; } $chunks = explode("\n\n", $stdout); foreach ($chunks as $chunk) { $commit = array(); $lines = explode("\n", $chunk); foreach ($lines as $line) { if (preg_match('/^(comparing with|searching for changes)/', $line)) { // These are sent to stdout when you run "hg outgoing" although the // format is otherwise identical to "hg log". continue; } if (preg_match('/^remote:/', $line)) { // This indicates remote error in "hg outgoing". continue; } list($name, $value) = explode(':', $line, 2); $value = trim($value); switch ($name) { case 'user': $commit['user'] = $value; break; case 'date': $commit['date'] = strtotime($value); break; case 'summary': $commit['summary'] = $value; break; case 'changeset': list($local, $rev) = explode(':', $value, 2); $commit['local'] = $local; $commit['rev'] = $rev; break; case 'parent': if (empty($commit['parents'])) { $commit['parents'] = array(); } list($local, $rev) = explode(':', $value, 2); $commit['parents'][] = array( 'local' => $local, 'rev' => $rev, ); break; case 'branch': $commit['branch'] = $value; break; case 'tag': $commit['tag'] = $value; break; case 'bookmark': $commit['bookmark'] = $value; break; + case 'obsolete': + // This is an extra field added by the "evolve" extension even + // if HGPLAIN=1 is set. See PHI502. + break; default: throw new Exception( pht("Unknown Mercurial log field '%s'!", $name)); } } $result[] = $commit; } return $result; } /** * Parse the output of "hg branches". * * @param string The stdout from running an "hg branches" command. * @return list A list of dictionaries with branch information. * @task parse */ public static function parseMercurialBranches($stdout) { $stdout = rtrim($stdout, "\n"); if (!strlen($stdout)) { // No branches; commonly, this occurs in a newly initialized repository. return array(); } $lines = explode("\n", $stdout); $branches = array(); foreach ($lines as $line) { $matches = null; // Output of "hg branches" normally looks like: // // default 15101:a21ccf4412d5 // // ...but may also have human-readable cues like: // // stable 15095:ec222a29bdf0 (inactive) // // See the unit tests for more examples. $regexp = '/^(\S+(?:\s+\S+)*)\s+(\d+):([a-f0-9]+)(\s+\\(inactive\\))?$/'; if (!preg_match($regexp, $line, $matches)) { throw new Exception( pht( "Failed to parse '%s' output: %s", 'hg branches', $line)); } $branches[$matches[1]] = array( 'local' => $matches[2], 'rev' => $matches[3], ); } return $branches; } } diff --git a/src/workflow/ArcanistFeatureWorkflow.php b/src/workflow/ArcanistFeatureWorkflow.php index 99a28200..a1cbe108 100644 --- a/src/workflow/ArcanistFeatureWorkflow.php +++ b/src/workflow/ArcanistFeatureWorkflow.php @@ -1,374 +1,374 @@ getArgument('branch'); } public function getArguments() { return array( 'view-all' => array( 'help' => pht('Include closed and abandoned revisions.'), ), 'by-status' => array( 'help' => pht('Sort branches by status instead of time.'), ), 'output' => array( 'param' => 'format', 'support' => array( 'json', ), 'help' => pht( "With '%s', show features in machine-readable JSON format.", 'json'), ), '*' => 'branch', ); } public function getSupportedRevisionControlSystems() { return array('git', 'hg'); } public function run() { $repository_api = $this->getRepositoryAPI(); $names = $this->getArgument('branch'); if ($names) { if (count($names) > 2) { throw new ArcanistUsageException(pht('Specify only one branch.')); } return $this->checkoutBranch($names); } $branches = $repository_api->getAllBranches(); if (!$branches) { throw new ArcanistUsageException( pht('No branches in this working copy.')); } $branches = $this->loadCommitInfo($branches); $revisions = $this->loadRevisions($branches); $this->printBranches($branches, $revisions); return 0; } private function checkoutBranch(array $names) { $api = $this->getRepositoryAPI(); if ($api instanceof ArcanistMercurialAPI) { $command = 'update %s'; } else { $command = 'checkout %s'; } $err = 1; $name = $names[0]; if (isset($names[1])) { $start = $names[1]; } else { $start = $this->getConfigFromAnySource('arc.feature.start.default'); } $branches = $api->getAllBranches(); if (in_array($name, ipull($branches, 'name'))) { list($err, $stdout, $stderr) = $api->execManualLocal($command, $name); } if ($err) { $match = null; if (preg_match('/^D(\d+)$/', $name, $match)) { try { $diff = $this->getConduit()->callMethodSynchronous( 'differential.querydiffs', array( 'revisionIDs' => array($match[1]), )); $diff = head($diff); if ($diff['branch'] != '') { $name = $diff['branch']; list($err, $stdout, $stderr) = $api->execManualLocal( $command, $name); } } catch (ConduitClientException $ex) {} } } if ($err) { if ($api instanceof ArcanistMercurialAPI) { $rev = ''; if ($start) { $rev = csprintf('-r %s', $start); } $exec = $api->execManualLocal('bookmark %C %s', $rev, $name); if (!$exec[0] && $start) { $api->execxLocal('update %s', $name); } } else { $startarg = $start ? csprintf('%s', $start) : ''; $exec = $api->execManualLocal( 'checkout --track -b %s %C', $name, $startarg); } list($err, $stdout, $stderr) = $exec; } echo $stdout; - fprintf(STDERR, $stderr); + fprintf(STDERR, '%s', $stderr); return $err; } private function loadCommitInfo(array $branches) { $repository_api = $this->getRepositoryAPI(); $branches = ipull($branches, null, 'name'); if ($repository_api instanceof ArcanistMercurialAPI) { $futures = array(); foreach ($branches as $branch) { $futures[$branch['name']] = $repository_api->execFutureLocal( 'log -l 1 --template %s -r %s', "{node}\1{date|hgdate}\1{p1node}\1{desc|firstline}\1{desc}", hgsprintf('%s', $branch['name'])); } $futures = id(new FutureIterator($futures)) ->limit(16); foreach ($futures as $name => $future) { list($info) = $future->resolvex(); $fields = explode("\1", trim($info), 5); list($hash, $epoch, $tree, $desc, $text) = $fields; $branches[$name] += array( 'hash' => $hash, 'desc' => $desc, 'tree' => $tree, 'epoch' => (int)$epoch, 'text' => $text, ); } } foreach ($branches as $name => $branch) { $text = $branch['text']; try { $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); $id = $message->getRevisionID(); $branch['revisionID'] = $id; } catch (ArcanistUsageException $ex) { // In case of invalid commit message which fails the parsing, // do nothing. $branch['revisionID'] = null; } $branches[$name] = $branch; } return $branches; } private function loadRevisions(array $branches) { $ids = array(); $hashes = array(); foreach ($branches as $branch) { if ($branch['revisionID']) { $ids[] = $branch['revisionID']; } $hashes[] = array('gtcm', $branch['hash']); $hashes[] = array('gttr', $branch['tree']); } $calls = array(); if ($ids) { $calls[] = $this->getConduit()->callMethod( 'differential.query', array( 'ids' => $ids, )); } if ($hashes) { $calls[] = $this->getConduit()->callMethod( 'differential.query', array( 'commitHashes' => $hashes, )); } $results = array(); foreach (new FutureIterator($calls) as $call) { $results[] = $call->resolve(); } return array_mergev($results); } private function printBranches(array $branches, array $revisions) { $revisions = ipull($revisions, null, 'id'); static $color_map = array( 'Closed' => 'cyan', 'Needs Review' => 'magenta', 'Needs Revision' => 'red', 'Accepted' => 'green', 'No Revision' => 'blue', 'Abandoned' => 'default', ); static $ssort_map = array( 'Closed' => 1, 'No Revision' => 2, 'Needs Review' => 3, 'Needs Revision' => 4, 'Accepted' => 5, ); $out = array(); foreach ($branches as $branch) { $revision = idx($revisions, idx($branch, 'revisionID')); // If we haven't identified a revision by ID, try to identify it by hash. if (!$revision) { foreach ($revisions as $rev) { $hashes = idx($rev, 'hashes', array()); foreach ($hashes as $hash) { if (($hash[0] == 'gtcm' && $hash[1] == $branch['hash']) || ($hash[0] == 'gttr' && $hash[1] == $branch['tree'])) { $revision = $rev; break; } } } } if ($revision) { $desc = 'D'.$revision['id'].': '.$revision['title']; $status = $revision['statusName']; } else { $desc = $branch['desc']; $status = pht('No Revision'); } if (!$this->getArgument('view-all') && !$branch['current']) { if ($status == 'Closed' || $status == 'Abandoned') { continue; } } $epoch = $branch['epoch']; $color = idx($color_map, $status, 'default'); $ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch); $out[] = array( 'name' => $branch['name'], 'current' => $branch['current'], 'status' => $status, 'desc' => $desc, 'revision' => $revision ? $revision['id'] : null, 'color' => $color, 'esort' => $epoch, 'epoch' => $epoch, 'ssort' => $ssort, ); } if (!$out) { // All of the revisions are closed or abandoned. return; } $len_name = max(array_map('strlen', ipull($out, 'name'))) + 2; $len_status = max(array_map('strlen', ipull($out, 'status'))) + 2; if ($this->getArgument('by-status')) { $out = isort($out, 'ssort'); } else { $out = isort($out, 'esort'); } if ($this->getArgument('output') == 'json') { foreach ($out as &$feature) { unset($feature['color'], $feature['ssort'], $feature['esort']); } echo json_encode(ipull($out, null, 'name'))."\n"; } else { $table = id(new PhutilConsoleTable()) ->setShowHeader(false) ->addColumn('current', array('title' => '')) ->addColumn('name', array('title' => pht('Name'))) ->addColumn('status', array('title' => pht('Status'))) ->addColumn('descr', array('title' => pht('Description'))); foreach ($out as $line) { $table->addRow(array( 'current' => $line['current'] ? '*' : '', 'name' => tsprintf('**%s**', $line['name']), 'status' => tsprintf( "%s", $line['status']), 'descr' => $line['desc'], )); } $table->draw(); } } }