diff --git a/src/workflow/ArcanistFeatureBaseWorkflow.php b/src/workflow/ArcanistFeatureBaseWorkflow.php index 94756c2a..b6224d34 100644 --- a/src/workflow/ArcanistFeatureBaseWorkflow.php +++ b/src/workflow/ArcanistFeatureBaseWorkflow.php @@ -1,265 +1,286 @@ newWorkflowArgument('view-all') ->setHelp(pht('Include closed and abandoned revisions.')), $this->newWorkflowArgument('by-status') ->setParameter('status') ->setHelp(pht('Sort branches by status instead of time.')), $this->newWorkflowArgument('output') ->setParameter('format') ->setHelp( pht( 'With "json", show features in machine-readable JSON format.')), $this->newWorkflowArgument('branch') ->setWildcard(true), ); } public function getWorkflowInformation() { return $this->newWorkflowInformation() ->setSynopsis(pht('Wrapper on "git branch" or "hg bookmark".')) ->addExample(pht('**%s** [__options__]', $this->getWorkflowName())) ->addExample(pht('**%s** __name__ [__start__]', $this->getWorkflowName())) ->setHelp( pht(<<getRepositoryAPI(); if (!$repository_api) { throw new PhutilArgumentUsageException( pht( 'This command must be run in a Git or Mercurial working copy.')); } $names = $this->getArgument('branch'); if ($names) { if (count($names) > 2) { throw new ArcanistUsageException(pht('Specify only one branch.')); } return $this->checkoutBranch($names); } // TODO: Everything in this whole workflow that says "branch" means // "bookmark" in Mercurial. $branches = $repository_api->getAllBranchRefs(); if (!$branches) { throw new ArcanistUsageException( pht('No branches in this working copy.')); } $states = array(); foreach ($branches as $branch_key => $branch) { $state_ref = id(new ArcanistWorkingCopyStateRef()) ->setCommitRef($branch->getCommitRef()); $states[] = array( 'branch' => $branch, 'state' => $state_ref, ); } $this->loadHardpoints( ipull($states, 'state'), ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); $this->printBranches($states); 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)) { $diff = $this->getConduitEngine()->resolveCall( 'differential.querydiffs', array( 'revisionIDs' => array($match[1]), )); $diff = head($diff); if ($diff['branch'] != '') { $name = $diff['branch']; list($err, $stdout, $stderr) = $api->execManualLocal( $command, $name); } } } 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, '%s', $stderr); return $err; } private function printBranches(array $states) { 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 ($states as $objects) { $state = $objects['state']; $branch = $objects['branch']; $revision = null; if ($state->hasAmbiguousRevisionRefs()) { $status = pht('Ambiguous Revision'); } else { $revision = $state->getRevisionRef(); if ($revision) { $status = $revision->getStatusDisplayName(); } else { $status = pht('No Revision'); } } if (!$this->getArgument('view-all') && !$branch->getIsCurrentBranch()) { if ($status == 'Closed' || $status == 'Abandoned') { continue; } } $commit = $branch->getCommitRef(); $epoch = $commit->getCommitEpoch(); $color = idx($color_map, $status, 'default'); - $ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch); + + $epoch_vector = id(new PhutilSortVector()) + ->addInt($epoch); + + $status_vector = id(new PhutilSortVector()) + ->addInt(idx($ssort_map, $status, 0)) + ->addInt($epoch); if ($revision) { $desc = $revision->getFullName(); } else { $desc = $commit->getSummary(); } $out[] = array( 'name' => $branch->getBranchName(), 'current' => $branch->getIsCurrentBranch(), 'status' => $status, 'desc' => $desc, 'revision' => $revision ? $revision->getID() : null, 'color' => $color, - 'esort' => $epoch, 'epoch' => $epoch, - 'ssort' => $ssort, + + 'esort' => $epoch_vector, + 'ssort' => $status_vector, ); } 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; + // Sort the list in natural order first. When we apply a stable sort to + // the list below, branches which were last updated at the same time will + // retain this ordering. This allows "feature1", "feature2", etc., to + // display in the correct order if they were touched at the same second, + // which is common when "arc land" performs a cascading rebase. + + $name_map = ipull($out, 'name'); + natcasesort($name_map); + $out = array_select_keys($out, array_keys($name_map)); + if ($this->getArgument('by-status')) { - $out = isort($out, 'ssort'); + $vectors = ipull($out, 'ssort'); } else { - $out = isort($out, 'esort'); + $vectors = ipull($out, 'esort'); } + + $vectors = msortv($vectors, 'getSelf'); + $out = array_select_keys($out, array_keys($vectors)); + 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(); } } }