4
* Displays user's Git branches or Mercurial bookmarks.
8
class ArcanistFeatureWorkflow extends ArcanistWorkflow {
12
public function getWorkflowName() {
16
public function getCommandSynopses() {
17
return phutil_console_format(<<<EOTEXT
18
**feature** [__options__]
19
**feature** __name__ [__start__]
24
public function getCommandHelp() {
25
return phutil_console_format(<<<EOTEXT
27
A wrapper on 'git branch' or 'hg bookmark'.
29
Without __name__, it lists the available branches and their revision
32
With __name__, it creates or checks out a branch. If the branch
33
__name__ doesn't exist and is in format D123 then the branch of
34
revision D123 is checked out. Use __start__ to specify where the new
35
branch will start. Use 'arc.feature.start.default' to set the default
36
feature start location.
41
public function requiresConduit() {
45
public function requiresRepositoryAPI() {
49
public function requiresAuthentication() {
50
return !$this->getArgument('branch');
53
public function getArguments() {
56
'help' => 'Include closed and abandoned revisions.',
59
'help' => 'Sort branches by status instead of time.',
66
'help' => "With 'json', show features in machine-readable JSON format.",
72
public function run() {
73
$repository_api = $this->getRepositoryAPI();
74
if (!($repository_api instanceof ArcanistGitAPI) &&
75
!($repository_api instanceof ArcanistMercurialAPI)) {
76
throw new ArcanistUsageException(
77
'arc feature is only supported under Git and Mercurial.');
80
$names = $this->getArgument('branch');
82
if (count($names) > 2) {
83
throw new ArcanistUsageException('Specify only one branch.');
85
return $this->checkoutBranch($names);
88
$branches = $repository_api->getAllBranches();
90
throw new ArcanistUsageException('No branches in this working copy.');
93
$branches = $this->loadCommitInfo($branches);
94
$revisions = $this->loadRevisions($branches);
95
$this->printBranches($branches, $revisions);
100
private function checkoutBranch(array $names) {
101
$api = $this->getRepositoryAPI();
103
if ($api instanceof ArcanistMercurialAPI) {
104
$command = 'update %s';
106
$command = 'checkout %s';
112
if (isset($names[1])) {
115
$start = $this->getConfigFromAnySource('arc.feature.start.default');
118
$branches = $api->getAllBranches();
119
if (in_array($name, ipull($branches, 'name'))) {
120
list($err, $stdout, $stderr) = $api->execManualLocal($command, $name);
125
if (preg_match('/^D(\d+)$/', $name, $match)) {
127
$diff = $this->getConduit()->callMethodSynchronous(
128
'differential.getdiff',
130
'revision_id' => $match[1],
133
if ($diff['branch'] != '') {
134
$name = $diff['branch'];
135
list($err, $stdout, $stderr) = $api->execManualLocal(
139
} catch (ConduitClientException $ex) {}
144
if ($api instanceof ArcanistMercurialAPI) {
147
$rev = csprintf('-r %s', $start);
150
$exec = $api->execManualLocal('bookmark %C %s', $rev, $name);
152
if (!$exec[0] && $start) {
153
$api->execxLocal('update %s', $name);
156
$startarg = $start ? csprintf('%s', $start) : '';
157
$exec = $api->execManualLocal(
158
'checkout --track -b %s %C',
163
list($err, $stdout, $stderr) = $exec;
167
fprintf(STDERR, $stderr);
171
private function loadCommitInfo(array $branches) {
172
$repository_api = $this->getRepositoryAPI();
175
foreach ($branches as $branch) {
176
if ($repository_api instanceof ArcanistMercurialAPI) {
177
$futures[$branch['name']] = $repository_api->execFutureLocal(
178
'log -l 1 --template %s -r %s',
179
"{node}\1{date|hgdate}\1{p1node}\1{desc|firstline}\1{desc}",
180
hgsprintf('%s', $branch['name']));
182
// NOTE: "-s" is an option deep in git's diff argument parser that
183
// doesn't seem to have much documentation and has no long form. It
184
// suppresses any diff output.
185
$futures[$branch['name']] = $repository_api->execFutureLocal(
186
'show -s --format=%C %s --',
187
'%H%x01%ct%x01%T%x01%s%x01%s%n%n%b',
192
$branches = ipull($branches, null, 'name');
194
foreach (Futures($futures)->limit(16) as $name => $future) {
195
list($info) = $future->resolvex();
196
list($hash, $epoch, $tree, $desc, $text) = explode("\1", trim($info), 5);
198
$branch = $branches[$name] + array(
202
'epoch' => (int)$epoch,
206
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
207
$id = $message->getRevisionID();
209
$branch['revisionID'] = $id;
210
} catch (ArcanistUsageException $ex) {
211
// In case of invalid commit message which fails the parsing,
213
$branch['revisionID'] = null;
216
$branches[$name] = $branch;
222
private function loadRevisions(array $branches) {
226
foreach ($branches as $branch) {
227
if ($branch['revisionID']) {
228
$ids[] = $branch['revisionID'];
230
$hashes[] = array('gtcm', $branch['hash']);
231
$hashes[] = array('gttr', $branch['tree']);
237
$calls[] = $this->getConduit()->callMethod(
238
'differential.query',
245
$calls[] = $this->getConduit()->callMethod(
246
'differential.query',
248
'commitHashes' => $hashes,
253
foreach (Futures($calls) as $call) {
254
$results[] = $call->resolve();
257
return array_mergev($results);
260
private function printBranches(array $branches, array $revisions) {
261
$revisions = ipull($revisions, null, 'id');
263
static $color_map = array(
265
'Needs Review' => 'magenta',
266
'Needs Revision' => 'red',
267
'Accepted' => 'green',
268
'No Revision' => 'blue',
269
'Abandoned' => 'default',
272
static $ssort_map = array(
276
'Needs Revision' => 4,
281
foreach ($branches as $branch) {
282
$revision = idx($revisions, idx($branch, 'revisionID'));
284
// If we haven't identified a revision by ID, try to identify it by hash.
286
foreach ($revisions as $rev) {
287
$hashes = idx($rev, 'hashes', array());
288
foreach ($hashes as $hash) {
289
if (($hash[0] == 'gtcm' && $hash[1] == $branch['hash']) ||
290
($hash[0] == 'gttr' && $hash[1] == $branch['tree'])) {
299
$desc = 'D'.$revision['id'].': '.$revision['title'];
300
$status = $revision['statusName'];
302
$desc = $branch['desc'];
303
$status = 'No Revision';
306
if (!$this->getArgument('view-all') && !$branch['current']) {
307
if ($status == 'Closed' || $status == 'Abandoned') {
312
$epoch = $branch['epoch'];
314
$color = idx($color_map, $status, 'default');
315
$ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch);
318
'name' => $branch['name'],
319
'current' => $branch['current'],
322
'revision' => $revision ? $revision['id'] : null,
330
$len_name = max(array_map('strlen', ipull($out, 'name'))) + 2;
331
$len_status = max(array_map('strlen', ipull($out, 'status'))) + 2;
333
if ($this->getArgument('by-status')) {
334
$out = isort($out, 'ssort');
336
$out = isort($out, 'esort');
338
if ($this->getArgument('output') == 'json') {
339
foreach ($out as &$feature) {
340
unset($feature['color'], $feature['ssort'], $feature['esort']);
342
echo json_encode(ipull($out, null, 'name'))."\n";
344
$table = id(new PhutilConsoleTable())
345
->setShowHeader(false)
346
->addColumn('current', array('title' => ''))
347
->addColumn('name', array('title' => 'Name'))
348
->addColumn('status', array('title' => 'Status'))
349
->addColumn('descr', array('title' => 'Description'));
351
foreach ($out as $line) {
352
$table->addRow(array(
353
'current' => $line['current'] ? '*' : '',
354
'name' => phutil_console_format('**%s**', $line['name']),
355
'status' => phutil_console_format(
356
"<fg:{$line['color']}>%s</fg>", $line['status']),
357
'descr' => $line['desc'],