1<?php
2
3/**
4 * Covers your professional reputation by blaming changes to locate reviewers.
5 */
6final class ArcanistCoverWorkflow extends ArcanistWorkflow {
7
8  public function getWorkflowName() {
9    return 'cover';
10  }
11
12  public function getCommandSynopses() {
13    return phutil_console_format(<<<EOTEXT
14      **cover** [--rev __revision__] [__path__ ...]
15EOTEXT
16      );
17  }
18
19  public function getCommandHelp() {
20    return phutil_console_format(<<<EOTEXT
21          Supports: svn, git, hg
22          Cover your... professional reputation. Show blame for the lines you
23          changed in your working copy (svn) or since some commit (hg, git).
24          This will take a minute because blame takes a minute, especially under
25          SVN.
26EOTEXT
27      );
28  }
29
30  public function getArguments() {
31    return array(
32      'rev' => array(
33        'param'     => 'revision',
34        'help'      => pht('Cover changes since a specific revision.'),
35        'supports'  => array(
36          'git',
37          'hg',
38        ),
39        'nosupport' => array(
40          'svn' => pht('cover does not currently support %s in svn.', '--rev'),
41        ),
42      ),
43      '*' => 'paths',
44    );
45  }
46
47  public function requiresWorkingCopy() {
48    return true;
49  }
50
51  public function requiresConduit() {
52    return false;
53  }
54
55  public function requiresAuthentication() {
56    return false;
57  }
58
59  public function requiresRepositoryAPI() {
60    return true;
61  }
62
63  public function run() {
64    $repository_api = $this->getRepositoryAPI();
65
66    $in_paths = $this->getArgument('paths');
67    $in_rev = $this->getArgument('rev');
68
69    if ($in_rev) {
70      $this->parseBaseCommitArgument(array($in_rev));
71    }
72
73    $paths = $this->selectPathsForWorkflow(
74      $in_paths,
75      $in_rev,
76      ArcanistRepositoryAPI::FLAG_UNTRACKED |
77      ArcanistRepositoryAPI::FLAG_ADDED);
78
79    if (!$paths) {
80      throw new ArcanistNoEffectException(
81        pht("You're covered, you didn't change anything."));
82    }
83
84    $covers = array();
85    foreach ($paths as $path) {
86      if (is_dir($repository_api->getPath($path))) {
87        continue;
88      }
89
90      $lines = $this->getChangedLines($path, 'cover');
91      if (!$lines) {
92        continue;
93      }
94
95      $blame = $repository_api->getBlame($path);
96      foreach ($lines as $line) {
97        list($author, $revision) = idx($blame, $line, array(null, null));
98        if (!$author) {
99          continue;
100        }
101        if (!isset($covers[$author])) {
102          $covers[$author] = array();
103        }
104        if (!isset($covers[$author][$path])) {
105          $covers[$author][$path] = array(
106            'lines'     => array(),
107            'revisions' => array(),
108          );
109        }
110        $covers[$author][$path]['lines'][] = $line;
111        $covers[$author][$path]['revisions'][] = $revision;
112      }
113    }
114
115    if (count($covers)) {
116      foreach ($covers as $author => $files) {
117        echo phutil_console_format(
118          "**%s**\n",
119          $author);
120        foreach ($files as $file => $info) {
121          $line_noun = pht(
122            '%s line(s)',
123            phutil_count($info['lines']));
124          $lines = $this->readableSequenceFromLineNumbers($info['lines']);
125          echo "  {$file}: {$line_noun} {$lines}\n";
126        }
127      }
128    } else {
129      echo pht(
130        "You're covered, your changes didn't touch anyone else's code.\n");
131    }
132
133    return 0;
134  }
135
136  private function readableSequenceFromLineNumbers(array $array) {
137    $sequence = array();
138    $last = null;
139    $seq  = null;
140    $array = array_unique(array_map('intval', $array));
141    sort($array);
142    foreach ($array as $element) {
143      if ($seq !== null && $element == ($seq + 1)) {
144        $seq++;
145        continue;
146      }
147
148      if ($seq === null) {
149        $last = $element;
150        $seq  = $element;
151        continue;
152      }
153
154      if ($seq > $last) {
155        $sequence[] = $last.'-'.$seq;
156      } else {
157        $sequence[] = $last;
158      }
159
160      $last = $element;
161      $seq  = $element;
162    }
163    if ($last !== null && $seq > $last) {
164      $sequence[] = $last.'-'.$seq;
165    } else if ($last !== null) {
166      $sequence[] = $element;
167    }
168
169    return implode(', ', $sequence);
170  }
171
172}
173