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); return $err; } private function loadCommitInfo(array $branches) { $repository_api = $this->getRepositoryAPI(); $futures = array(); foreach ($branches as $branch) { if ($repository_api instanceof ArcanistMercurialAPI) { $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'])); } else { // NOTE: "-s" is an option deep in git's diff argument parser that // doesn't seem to have much documentation and has no long form. It // suppresses any diff output. $futures[$branch['name']] = $repository_api->execFutureLocal( 'show -s --format=%C %s --', '%H%x01%ct%x01%T%x01%s%x01%s%n%n%b', $branch['name']); } } $branches = ipull($branches, null, 'name'); $futures = id(new FutureIterator($futures)) ->limit(16); foreach ($futures as $name => $future) { list($info) = $future->resolvex(); list($hash, $epoch, $tree, $desc, $text) = explode("\1", trim($info), 5); $branch = $branches[$name] + array( 'hash' => $hash, 'desc' => $desc, 'tree' => $tree, 'epoch' => (int)$epoch, ); 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, ); } $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' => 'Name')) ->addColumn('status', array('title' => 'Status')) ->addColumn('descr', array('title' => 'Description')); foreach ($out as $line) { $table->addRow(array( 'current' => $line['current'] ? '*' : '', 'name' => phutil_console_format('**%s**', $line['name']), 'status' => phutil_console_format( "%s", $line['status']), 'descr' => $line['desc'], )); } $table->draw(); } } }