@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 368 lines 11 kB view raw
1<?php 2 3abstract class PhabricatorSQLPatchList extends Phobject { 4 5 abstract public function getNamespace(); 6 abstract public function getPatches(); 7 8 /** 9 * Examine a directory for `.php` and `.sql` files and build patch 10 * specifications for them. 11 */ 12 protected function buildPatchesFromDirectory($directory) { 13 $patch_list = Filesystem::listDirectory( 14 $directory, 15 $include_hidden = false); 16 17 sort($patch_list); 18 $patches = array(); 19 20 foreach ($patch_list as $patch) { 21 $matches = null; 22 if (!preg_match('/\.(sql|php|db)$/', $patch, $matches)) { 23 throw new Exception( 24 pht( 25 'Unknown patch "%s" in "%s", '. 26 'expected ".php" or ".sql" or ".db" suffix.', 27 $patch, 28 $directory)); 29 } 30 31 $patch_type = $matches[1]; 32 $patch_full_path = rtrim($directory, '/').'/'.$patch; 33 34 $attributes = array(); 35 switch ($patch_type) { 36 case 'php': 37 $attributes = $this->getPHPPatchAttributes( 38 $patch, 39 $patch_full_path); 40 $patch_name = $patch_full_path; 41 break; 42 43 case 'db': 44 $attributes = $this->getDBPatchAttributes( 45 $patch, 46 $patch_full_path); 47 $patch_name = $attributes['name']; 48 break; 49 50 default: 51 $patch_name = $patch_full_path; 52 break; 53 } 54 55 56 $patches[$patch] = array( 57 'type' => $patch_type, 58 'name' => $patch_name, 59 ) + $attributes; 60 } 61 62 return $patches; 63 } 64 65 final public static function buildAllPatches() { 66 $patch_lists = id(new PhutilClassMapQuery()) 67 ->setAncestorClass(self::class) 68 ->setUniqueMethod('getNamespace') 69 ->execute(); 70 71 $specs = array(); 72 $seen_namespaces = array(); 73 74 $phases = PhabricatorStoragePatch::getPhaseList(); 75 $phases = array_fuse($phases); 76 77 $default_phase = PhabricatorStoragePatch::getDefaultPhase(); 78 79 foreach ($patch_lists as $patch_list) { 80 $last_keys = array_fill_keys( 81 array_keys($phases), 82 null); 83 84 foreach ($patch_list->getPatches() as $key => $patch) { 85 if (!is_array($patch)) { 86 throw new Exception( 87 pht( 88 "%s '%s' has a patch '%s' which is not an array.", 89 self::class, 90 get_class($patch_list), 91 $key)); 92 } 93 94 $valid = array( 95 'type' => true, 96 'name' => true, 97 'after' => true, 98 'legacy' => true, 99 'dead' => true, 100 'phase' => true, 101 ); 102 103 foreach ($patch as $pkey => $pval) { 104 if (empty($valid[$pkey])) { 105 throw new Exception( 106 pht( 107 "%s '%s' has a patch, '%s', with an unknown property, '%s'.". 108 "Patches must have only valid keys: %s.", 109 self::class, 110 get_class($patch_list), 111 $key, 112 $pkey, 113 implode(', ', array_keys($valid)))); 114 } 115 } 116 117 if (is_numeric($key)) { 118 throw new Exception( 119 pht( 120 "%s '%s' has a patch with a numeric key, '%s'. ". 121 "Patches must use string keys.", 122 self::class, 123 get_class($patch_list), 124 $key)); 125 } 126 127 if (strpos($key, ':') !== false) { 128 throw new Exception( 129 pht( 130 "%s '%s' has a patch with a colon in the key name, '%s'. ". 131 "Patch keys may not contain colons.", 132 self::class, 133 get_class($patch_list), 134 $key)); 135 } 136 137 $namespace = $patch_list->getNamespace(); 138 $full_key = "{$namespace}:{$key}"; 139 140 if (isset($specs[$full_key])) { 141 throw new Exception( 142 pht( 143 "%s '%s' has a patch '%s' which duplicates an ". 144 "existing patch key.", 145 self::class, 146 get_class($patch_list), 147 $key)); 148 } 149 150 $patch['key'] = $key; 151 $patch['fullKey'] = $full_key; 152 $patch['dead'] = (bool)idx($patch, 'dead', false); 153 154 if (isset($patch['legacy'])) { 155 if ($namespace != 'phabricator') { 156 throw new Exception( 157 pht( 158 "Only patches in the '%s' namespace may contain '%s' keys.", 159 'phabricator', 160 'legacy')); 161 } 162 } else { 163 $patch['legacy'] = false; 164 } 165 166 if (!array_key_exists('phase', $patch)) { 167 $patch['phase'] = $default_phase; 168 } 169 170 $patch_phase = $patch['phase']; 171 172 if (!isset($phases[$patch_phase])) { 173 throw new Exception( 174 pht( 175 'Storage patch "%s" specifies it should apply in phase "%s", '. 176 'but this phase is unrecognized. Valid phases are: %s.', 177 $full_key, 178 $patch_phase, 179 implode(', ', array_keys($phases)))); 180 } 181 182 $last_key = $last_keys[$patch_phase]; 183 184 if (!array_key_exists('after', $patch)) { 185 if ($last_key === null && $patch_phase === $default_phase) { 186 throw new Exception( 187 pht( 188 "Patch '%s' is missing key 'after', and is the first patch ". 189 "in the patch list '%s', so its application order can not be ". 190 "determined implicitly. The first patch in a patch list must ". 191 "list the patch or patches it depends on explicitly.", 192 $full_key, 193 get_class($patch_list))); 194 } else { 195 if ($last_key === null) { 196 $patch['after'] = array(); 197 } else { 198 $patch['after'] = array($last_key); 199 } 200 } 201 } 202 $last_keys[$patch_phase] = $full_key; 203 204 foreach ($patch['after'] as $after_key => $after) { 205 if (strpos($after, ':') === false) { 206 $patch['after'][$after_key] = $namespace.':'.$after; 207 } 208 } 209 210 $type = idx($patch, 'type'); 211 if (!$type) { 212 throw new Exception( 213 pht( 214 "Patch '%s' is missing key '%s'. Every patch must have a type.", 215 "{$namespace}:{$key}", 216 'type')); 217 } 218 219 switch ($type) { 220 case 'db': 221 case 'sql': 222 case 'php': 223 break; 224 default: 225 throw new Exception( 226 pht( 227 "Patch '%s' has unknown patch type '%s'.", 228 "{$namespace}:{$key}", 229 $type)); 230 } 231 232 $specs[$full_key] = $patch; 233 } 234 } 235 236 foreach ($specs as $key => $patch) { 237 foreach ($patch['after'] as $after) { 238 if (empty($specs[$after])) { 239 throw new Exception( 240 pht( 241 "Patch '%s' references nonexistent dependency, '%s'. ". 242 "Patches may only depend on patches which actually exist.", 243 $key, 244 $after)); 245 } 246 247 $patch_phase = $patch['phase']; 248 $after_phase = $specs[$after]['phase']; 249 250 if ($patch_phase !== $after_phase) { 251 throw new Exception( 252 pht( 253 'Storage patch "%s" executes in phase "%s", but depends on '. 254 'patch "%s" which is in a different phase ("%s"). Patches '. 255 'may not have dependencies across phases.', 256 $key, 257 $patch_phase, 258 $after, 259 $after_phase)); 260 } 261 } 262 } 263 264 $patches = array(); 265 foreach ($specs as $full_key => $spec) { 266 $patches[$full_key] = new PhabricatorStoragePatch($spec); 267 } 268 269 // TODO: Detect cycles? 270 271 $patches = msortv($patches, 'newSortVector'); 272 273 return $patches; 274 } 275 276 private function getPHPPatchAttributes($patch_name, $full_path) { 277 $data = Filesystem::readFile($full_path); 278 279 $phase_list = PhabricatorStoragePatch::getPhaseList(); 280 $phase_map = array_fuse($phase_list); 281 282 $attributes = array(); 283 284 $lines = phutil_split_lines($data, false); 285 foreach ($lines as $line) { 286 // Skip over the "PHP" line. 287 if (preg_match('(^<\?)', $line)) { 288 continue; 289 } 290 291 // Skip over blank lines. 292 if (!strlen(trim($line))) { 293 continue; 294 } 295 296 // If this is a "//" comment... 297 if (preg_match('(^\s*//)', $line)) { 298 $matches = null; 299 if (preg_match('(^\s*//\s*@(\S+)(?:\s+(.*))?\z)', $line, $matches)) { 300 $attr_key = $matches[1]; 301 $attr_value = trim(idx($matches, 2)); 302 303 switch ($attr_key) { 304 case 'phase': 305 $phase_name = $attr_value; 306 307 if (!strlen($phase_name)) { 308 throw new Exception( 309 pht( 310 'Storage patch "%s" specifies a "@phase" attribute with '. 311 'no phase value. Phase attributes must specify a value, '. 312 'like "@phase default".', 313 $patch_name)); 314 } 315 316 if (!isset($phase_map[$phase_name])) { 317 throw new Exception( 318 pht( 319 'Storage patch "%s" specifies a "@phase" value ("%s"), '. 320 'but this is not a recognized phase. Valid phases '. 321 'are: %s.', 322 $patch_name, 323 $phase_name, 324 implode(', ', $phase_list))); 325 } 326 327 if (isset($attributes['phase'])) { 328 throw new Exception( 329 pht( 330 'Storage patch "%s" specifies a "@phase" value ("%s"), '. 331 'but it already has a specified phase ("%s"). Patches '. 332 'may not specify multiple phases.', 333 $patch_name, 334 $phase_name, 335 $attributes['phase'])); 336 } 337 338 $attributes[$attr_key] = $phase_name; 339 break; 340 default: 341 throw new Exception( 342 pht( 343 'Storage patch "%s" specifies attribute "%s", but this '. 344 'attribute is unknown.', 345 $patch_name, 346 $attr_key)); 347 } 348 } 349 continue; 350 } 351 352 // If this is anything else, we're all done. Attributes must be marked 353 // in the header of the file. 354 break; 355 } 356 357 358 return $attributes; 359 } 360 361 private function getDBPatchAttributes($patch_name, $full_path) { 362 $content = Filesystem::readFile($full_path); 363 // Should be a valid JSON 364 $data = phutil_json_decode($content); 365 366 return array_select_keys($data, array('name', 'after', 'dead')); 367 } 368}