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