1<?php 2 3final class ArcanistBaseCommitParser extends Phobject { 4 5 private $api; 6 private $try; 7 private $verbose = false; 8 9 public function __construct(ArcanistRepositoryAPI $api) { 10 $this->api = $api; 11 } 12 13 private function tokenizeBaseCommitSpecification($raw_spec) { 14 if (!$raw_spec) { 15 return array(); 16 } 17 18 $spec = preg_split('/\s*,\s*/', $raw_spec); 19 $spec = array_filter($spec); 20 21 foreach ($spec as $rule) { 22 if (strpos($rule, ':') === false) { 23 throw new ArcanistUsageException( 24 pht( 25 "Rule '%s' is invalid, it must have a type and name like '%s'.", 26 $rule, 27 'arc:upstream')); 28 } 29 } 30 31 return $spec; 32 } 33 34 private function log($message) { 35 if ($this->verbose) { 36 fwrite(STDERR, $message."\n"); 37 } 38 } 39 40 public function resolveBaseCommit(array $specs) { 41 $specs += array( 42 'runtime' => '', 43 'local' => '', 44 'project' => '', 45 'user' => '', 46 'system' => '', 47 ); 48 49 foreach ($specs as $source => $spec) { 50 $specs[$source] = self::tokenizeBaseCommitSpecification($spec); 51 } 52 53 $this->try = array( 54 'runtime', 55 'local', 56 'project', 57 'user', 58 'system', 59 ); 60 61 while ($this->try) { 62 $source = head($this->try); 63 64 if (!idx($specs, $source)) { 65 $this->log(pht("No rules left from source '%s'.", $source)); 66 array_shift($this->try); 67 continue; 68 } 69 70 $this->log(pht("Trying rules from source '%s'.", $source)); 71 72 $rules = &$specs[$source]; 73 while ($rule = array_shift($rules)) { 74 $this->log(pht("Trying rule '%s'.", $rule)); 75 76 $commit = $this->resolveRule($rule, $source); 77 78 if ($commit === false) { 79 // If a rule returns false, it means to go to the next ruleset. 80 break; 81 } else if ($commit !== null) { 82 $this->log(pht( 83 "Resolved commit '%s' from rule '%s'.", 84 $commit, 85 $rule)); 86 return $commit; 87 } 88 } 89 } 90 91 return null; 92 } 93 94 /** 95 * Handle resolving individual rules. 96 */ 97 private function resolveRule($rule, $source) { 98 // NOTE: Returning `null` from this method means "no match". 99 // Returning `false` from this method means "stop current ruleset". 100 101 list($type, $name) = explode(':', $rule, 2); 102 switch ($type) { 103 case 'literal': 104 return $name; 105 case 'git': 106 case 'hg': 107 return $this->api->resolveBaseCommitRule($rule, $source); 108 case 'arc': 109 return $this->resolveArcRule($rule, $name, $source); 110 default: 111 throw new ArcanistUsageException( 112 pht( 113 "Base commit rule '%s' (from source '%s') ". 114 "is not a recognized rule.", 115 $rule, 116 $source)); 117 } 118 } 119 120 121 /** 122 * Handle resolving "arc:*" rules. 123 */ 124 private function resolveArcRule($rule, $name, $source) { 125 $name = $this->updateLegacyRuleName($name); 126 127 switch ($name) { 128 case 'verbose': 129 $this->verbose = true; 130 $this->log(pht('Enabled verbose mode.')); 131 break; 132 case 'prompt': 133 $reason = pht('it is what you typed when prompted.'); 134 $this->api->setBaseCommitExplanation($reason); 135 $result = phutil_console_prompt(pht('Against which commit?')); 136 if (!strlen($result)) { 137 // Allow the user to continue to the next rule by entering no 138 // text. 139 return null; 140 } 141 return $result; 142 case 'local': 143 case 'user': 144 case 'project': 145 case 'runtime': 146 case 'system': 147 // Push the other source on top of the list. 148 array_unshift($this->try, $name); 149 $this->log(pht("Switching to source '%s'.", $name)); 150 return false; 151 case 'yield': 152 // Cycle this source to the end of the list. 153 $this->try[] = array_shift($this->try); 154 $this->log(pht("Yielding processing of rules from '%s'.", $source)); 155 return false; 156 case 'halt': 157 // Dump the whole stack. 158 $this->try = array(); 159 $this->log(pht('Halting all rule processing.')); 160 return false; 161 case 'skip': 162 return null; 163 case 'empty': 164 case 'upstream': 165 case 'outgoing': 166 case 'bookmark': 167 case 'amended': 168 case 'this': 169 return $this->api->resolveBaseCommitRule($rule, $source); 170 default: 171 $matches = null; 172 if (preg_match('/^exec\((.*)\)$/', $name, $matches)) { 173 $root = $this->api->getWorkingCopyIdentity()->getProjectRoot(); 174 $future = new ExecFuture('%C', $matches[1]); 175 $future->setCWD($root); 176 list($err, $stdout) = $future->resolve(); 177 if (!$err) { 178 return trim($stdout); 179 } else { 180 return null; 181 } 182 } else if (preg_match('/^nodiff\((.*)\)$/', $name, $matches)) { 183 return $this->api->resolveBaseCommitRule($rule, $source); 184 } 185 186 throw new ArcanistUsageException( 187 pht( 188 "Base commit rule '%s' (from source '%s') ". 189 "is not a recognized rule.", 190 $rule, 191 $source)); 192 } 193 } 194 195 private function updateLegacyRuleName($name) { 196 $updated = array( 197 'global' => 'user', 198 'args' => 'runtime', 199 ); 200 $new_name = idx($updated, $name); 201 if ($new_name) { 202 $this->log(pht("Translating legacy name '%s' to '%s'", $name, $new_name)); 203 return $new_name; 204 } 205 return $name; 206 } 207 208} 209