@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
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}