~ubuntu-branches/ubuntu/wily/phabricator/wily

« back to all changes in this revision

Viewing changes to src/workflow/ArcanistFeatureWorkflow.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-11-01 23:20:06 UTC
  • mto: This revision was merged to the branch mainline in revision 4.
  • Revision ID: package-import@ubuntu.com-20141101232006-mvlnp0cil67tsboe
Tags: upstream-0~git20141101/arcanist
Import upstream version 0~git20141101, component arcanist

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
4
 * Displays user's Git branches or Mercurial bookmarks.
 
5
 *
 
6
 * @concrete-extensible
 
7
 */
 
8
class ArcanistFeatureWorkflow extends ArcanistWorkflow {
 
9
 
 
10
  private $branches;
 
11
 
 
12
  public function getWorkflowName() {
 
13
    return 'feature';
 
14
  }
 
15
 
 
16
  public function getCommandSynopses() {
 
17
    return phutil_console_format(<<<EOTEXT
 
18
      **feature** [__options__]
 
19
      **feature** __name__ [__start__]
 
20
EOTEXT
 
21
      );
 
22
  }
 
23
 
 
24
  public function getCommandHelp() {
 
25
    return phutil_console_format(<<<EOTEXT
 
26
          Supports: git, hg
 
27
          A wrapper on 'git branch' or 'hg bookmark'.
 
28
 
 
29
          Without __name__, it lists the available branches and their revision
 
30
          status.
 
31
 
 
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.
 
37
EOTEXT
 
38
      );
 
39
  }
 
40
 
 
41
  public function requiresConduit() {
 
42
    return true;
 
43
  }
 
44
 
 
45
  public function requiresRepositoryAPI() {
 
46
    return true;
 
47
  }
 
48
 
 
49
  public function requiresAuthentication() {
 
50
    return !$this->getArgument('branch');
 
51
  }
 
52
 
 
53
  public function getArguments() {
 
54
    return array(
 
55
      'view-all' => array(
 
56
        'help' => 'Include closed and abandoned revisions.',
 
57
      ),
 
58
      'by-status' => array(
 
59
        'help' => 'Sort branches by status instead of time.',
 
60
      ),
 
61
      'output' => array(
 
62
        'param' => 'format',
 
63
        'support' => array(
 
64
          'json',
 
65
        ),
 
66
        'help' => "With 'json', show features in machine-readable JSON format.",
 
67
      ),
 
68
      '*' => 'branch',
 
69
    );
 
70
  }
 
71
 
 
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.');
 
78
    }
 
79
 
 
80
    $names = $this->getArgument('branch');
 
81
    if ($names) {
 
82
      if (count($names) > 2) {
 
83
        throw new ArcanistUsageException('Specify only one branch.');
 
84
      }
 
85
      return $this->checkoutBranch($names);
 
86
    }
 
87
 
 
88
    $branches = $repository_api->getAllBranches();
 
89
    if (!$branches) {
 
90
      throw new ArcanistUsageException('No branches in this working copy.');
 
91
    }
 
92
 
 
93
    $branches = $this->loadCommitInfo($branches);
 
94
    $revisions = $this->loadRevisions($branches);
 
95
    $this->printBranches($branches, $revisions);
 
96
 
 
97
    return 0;
 
98
  }
 
99
 
 
100
  private function checkoutBranch(array $names) {
 
101
    $api = $this->getRepositoryAPI();
 
102
 
 
103
    if ($api instanceof ArcanistMercurialAPI) {
 
104
      $command = 'update %s';
 
105
    } else {
 
106
      $command = 'checkout %s';
 
107
    }
 
108
 
 
109
    $err = 1;
 
110
 
 
111
    $name = $names[0];
 
112
    if (isset($names[1])) {
 
113
      $start = $names[1];
 
114
    } else {
 
115
      $start = $this->getConfigFromAnySource('arc.feature.start.default');
 
116
    }
 
117
 
 
118
    $branches = $api->getAllBranches();
 
119
    if (in_array($name, ipull($branches, 'name'))) {
 
120
      list($err, $stdout, $stderr) = $api->execManualLocal($command, $name);
 
121
    }
 
122
 
 
123
    if ($err) {
 
124
      $match = null;
 
125
      if (preg_match('/^D(\d+)$/', $name, $match)) {
 
126
        try {
 
127
          $diff = $this->getConduit()->callMethodSynchronous(
 
128
            'differential.getdiff',
 
129
            array(
 
130
              'revision_id' => $match[1],
 
131
            ));
 
132
 
 
133
          if ($diff['branch'] != '') {
 
134
            $name = $diff['branch'];
 
135
            list($err, $stdout, $stderr) = $api->execManualLocal(
 
136
              $command,
 
137
              $name);
 
138
          }
 
139
        } catch (ConduitClientException $ex) {}
 
140
      }
 
141
    }
 
142
 
 
143
    if ($err) {
 
144
      if ($api instanceof ArcanistMercurialAPI) {
 
145
        $rev = '';
 
146
        if ($start) {
 
147
          $rev = csprintf('-r %s', $start);
 
148
        }
 
149
 
 
150
        $exec = $api->execManualLocal('bookmark %C %s', $rev, $name);
 
151
 
 
152
        if (!$exec[0] && $start) {
 
153
          $api->execxLocal('update %s', $name);
 
154
        }
 
155
      } else {
 
156
        $startarg = $start ? csprintf('%s', $start) : '';
 
157
        $exec = $api->execManualLocal(
 
158
          'checkout --track -b %s %C',
 
159
          $name,
 
160
          $startarg);
 
161
      }
 
162
 
 
163
      list($err, $stdout, $stderr) = $exec;
 
164
    }
 
165
 
 
166
    echo $stdout;
 
167
    fprintf(STDERR, $stderr);
 
168
    return $err;
 
169
  }
 
170
 
 
171
  private function loadCommitInfo(array $branches) {
 
172
    $repository_api = $this->getRepositoryAPI();
 
173
 
 
174
    $futures = array();
 
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']));
 
181
      } else {
 
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',
 
188
          $branch['name']);
 
189
      }
 
190
    }
 
191
 
 
192
    $branches = ipull($branches, null, 'name');
 
193
 
 
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);
 
197
 
 
198
      $branch = $branches[$name] + array(
 
199
        'hash' => $hash,
 
200
        'desc' => $desc,
 
201
        'tree' => $tree,
 
202
        'epoch' => (int)$epoch,
 
203
      );
 
204
 
 
205
      try {
 
206
        $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
 
207
        $id = $message->getRevisionID();
 
208
 
 
209
        $branch['revisionID'] = $id;
 
210
      } catch (ArcanistUsageException $ex) {
 
211
        // In case of invalid commit message which fails the parsing,
 
212
        // do nothing.
 
213
        $branch['revisionID'] = null;
 
214
      }
 
215
 
 
216
      $branches[$name] = $branch;
 
217
    }
 
218
 
 
219
    return $branches;
 
220
  }
 
221
 
 
222
  private function loadRevisions(array $branches) {
 
223
    $ids = array();
 
224
    $hashes = array();
 
225
 
 
226
    foreach ($branches as $branch) {
 
227
      if ($branch['revisionID']) {
 
228
        $ids[] = $branch['revisionID'];
 
229
      }
 
230
      $hashes[] = array('gtcm', $branch['hash']);
 
231
      $hashes[] = array('gttr', $branch['tree']);
 
232
    }
 
233
 
 
234
    $calls = array();
 
235
 
 
236
    if ($ids) {
 
237
      $calls[] = $this->getConduit()->callMethod(
 
238
        'differential.query',
 
239
        array(
 
240
          'ids' => $ids,
 
241
        ));
 
242
    }
 
243
 
 
244
    if ($hashes) {
 
245
      $calls[] = $this->getConduit()->callMethod(
 
246
        'differential.query',
 
247
        array(
 
248
          'commitHashes' => $hashes,
 
249
        ));
 
250
    }
 
251
 
 
252
    $results = array();
 
253
    foreach (Futures($calls) as $call) {
 
254
      $results[] = $call->resolve();
 
255
    }
 
256
 
 
257
    return array_mergev($results);
 
258
  }
 
259
 
 
260
  private function printBranches(array $branches, array $revisions) {
 
261
    $revisions = ipull($revisions, null, 'id');
 
262
 
 
263
    static $color_map = array(
 
264
      'Closed'          => 'cyan',
 
265
      'Needs Review'    => 'magenta',
 
266
      'Needs Revision'  => 'red',
 
267
      'Accepted'        => 'green',
 
268
      'No Revision'     => 'blue',
 
269
      'Abandoned'       => 'default',
 
270
    );
 
271
 
 
272
    static $ssort_map = array(
 
273
      'Closed'          => 1,
 
274
      'No Revision'     => 2,
 
275
      'Needs Review'    => 3,
 
276
      'Needs Revision'  => 4,
 
277
      'Accepted'        => 5,
 
278
    );
 
279
 
 
280
    $out = array();
 
281
    foreach ($branches as $branch) {
 
282
      $revision = idx($revisions, idx($branch, 'revisionID'));
 
283
 
 
284
      // If we haven't identified a revision by ID, try to identify it by hash.
 
285
      if (!$revision) {
 
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'])) {
 
291
              $revision = $rev;
 
292
              break;
 
293
            }
 
294
          }
 
295
        }
 
296
      }
 
297
 
 
298
      if ($revision) {
 
299
        $desc = 'D'.$revision['id'].': '.$revision['title'];
 
300
        $status = $revision['statusName'];
 
301
      } else {
 
302
        $desc = $branch['desc'];
 
303
        $status = 'No Revision';
 
304
      }
 
305
 
 
306
      if (!$this->getArgument('view-all') && !$branch['current']) {
 
307
        if ($status == 'Closed' || $status == 'Abandoned') {
 
308
          continue;
 
309
        }
 
310
      }
 
311
 
 
312
      $epoch = $branch['epoch'];
 
313
 
 
314
      $color = idx($color_map, $status, 'default');
 
315
      $ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch);
 
316
 
 
317
      $out[] = array(
 
318
        'name'      => $branch['name'],
 
319
        'current'   => $branch['current'],
 
320
        'status'    => $status,
 
321
        'desc'      => $desc,
 
322
        'revision'  => $revision ? $revision['id'] : null,
 
323
        'color'     => $color,
 
324
        'esort'     => $epoch,
 
325
        'epoch'     => $epoch,
 
326
        'ssort'     => $ssort,
 
327
      );
 
328
    }
 
329
 
 
330
    $len_name = max(array_map('strlen', ipull($out, 'name'))) + 2;
 
331
    $len_status = max(array_map('strlen', ipull($out, 'status'))) + 2;
 
332
 
 
333
    if ($this->getArgument('by-status')) {
 
334
      $out = isort($out, 'ssort');
 
335
    } else {
 
336
      $out = isort($out, 'esort');
 
337
    }
 
338
    if ($this->getArgument('output') == 'json') {
 
339
      foreach ($out as &$feature) {
 
340
        unset($feature['color'], $feature['ssort'], $feature['esort']);
 
341
      }
 
342
      echo json_encode(ipull($out, null, 'name'))."\n";
 
343
    } else {
 
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'));
 
350
 
 
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'],
 
358
        ));
 
359
      }
 
360
 
 
361
      $table->draw();
 
362
    }
 
363
  }
 
364
 
 
365
}