@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
3final class DifferentialChangeset
4 extends DifferentialDAO
5 implements
6 PhabricatorPolicyInterface,
7 PhabricatorDestructibleInterface,
8 PhabricatorConduitResultInterface {
9
10 protected $diffID;
11 protected $oldFile;
12 protected $filename;
13 protected $awayPaths;
14 protected $changeType;
15 protected $fileType;
16 protected $metadata = array();
17 protected $oldProperties;
18 protected $newProperties;
19 protected $addLines;
20 protected $delLines;
21
22 private $unsavedHunks = array();
23 private $hunks = self::ATTACHABLE;
24 private $diff = self::ATTACHABLE;
25
26 private $authorityPackages;
27 private $changesetPackages;
28
29 private $newFileObject = self::ATTACHABLE;
30 private $oldFileObject = self::ATTACHABLE;
31
32 private $hasOldState;
33 private $hasNewState;
34 private $oldStateMetadata;
35 private $newStateMetadata;
36 private $oldFileType;
37 private $newFileType;
38
39 const TABLE_CACHE = 'differential_changeset_parse_cache';
40
41 const METADATA_TRUSTED_ATTRIBUTES = 'attributes.trusted';
42 const METADATA_UNTRUSTED_ATTRIBUTES = 'attributes.untrusted';
43 const METADATA_EFFECT_HASH = 'hash.effect';
44
45 const ATTRIBUTE_GENERATED = 'generated';
46
47 protected function getConfiguration() {
48 return array(
49 self::CONFIG_AUX_PHID => true,
50 self::CONFIG_SERIALIZATION => array(
51 'metadata' => self::SERIALIZATION_JSON,
52 'oldProperties' => self::SERIALIZATION_JSON,
53 'newProperties' => self::SERIALIZATION_JSON,
54 'awayPaths' => self::SERIALIZATION_JSON,
55 ),
56 self::CONFIG_COLUMN_SCHEMA => array(
57 'oldFile' => 'bytes?',
58 'filename' => 'bytes',
59 'changeType' => 'uint32',
60 'fileType' => 'uint32',
61 'addLines' => 'uint32',
62 'delLines' => 'uint32',
63
64 // T6203/NULLABILITY
65 // These should all be non-nullable, and store reasonable default
66 // JSON values if empty.
67 'awayPaths' => 'text?',
68 'metadata' => 'text?',
69 'oldProperties' => 'text?',
70 'newProperties' => 'text?',
71 ),
72 self::CONFIG_KEY_SCHEMA => array(
73 'diffID' => array(
74 'columns' => array('diffID'),
75 ),
76 ),
77 ) + parent::getConfiguration();
78 }
79
80 public function getPHIDType() {
81 return DifferentialChangesetPHIDType::TYPECONST;
82 }
83
84 public function getAffectedLineCount() {
85 return $this->getAddLines() + $this->getDelLines();
86 }
87
88 /**
89 * @param array<DifferentialHunk> $hunks
90 */
91 public function attachHunks(array $hunks) {
92 assert_instances_of($hunks, DifferentialHunk::class);
93 $this->hunks = $hunks;
94 return $this;
95 }
96
97 public function getHunks() {
98 return $this->assertAttached($this->hunks);
99 }
100
101 public function getDisplayFilename() {
102 $name = $this->getFilename();
103 if ($this->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
104 $name .= '/';
105 }
106 return $name;
107 }
108
109 public function getOwnersFilename() {
110 // TODO: For Subversion, we should adjust these paths to be relative to
111 // the repository root where possible.
112
113 $path = $this->getFilename();
114
115 if (!isset($path[0])) {
116 return '/';
117 }
118
119 if ($path[0] != '/') {
120 $path = '/'.$path;
121 }
122
123 return $path;
124 }
125
126 public function addUnsavedHunk(DifferentialHunk $hunk) {
127 if ($this->hunks === self::ATTACHABLE) {
128 $this->hunks = array();
129 }
130 $this->hunks[] = $hunk;
131 $this->unsavedHunks[] = $hunk;
132 return $this;
133 }
134
135 public function setAuthorityPackages(array $authority_packages) {
136 $this->authorityPackages = mpull($authority_packages, null, 'getPHID');
137 return $this;
138 }
139
140 public function getAuthorityPackages() {
141 return $this->authorityPackages;
142 }
143
144 public function setChangesetPackages($changeset_packages) {
145 $this->changesetPackages = mpull($changeset_packages, null, 'getPHID');
146 return $this;
147 }
148
149 public function getChangesetPackages() {
150 return $this->changesetPackages;
151 }
152
153 public function setHasOldState($has_old_state) {
154 $this->hasOldState = $has_old_state;
155 return $this;
156 }
157
158 public function setHasNewState($has_new_state) {
159 $this->hasNewState = $has_new_state;
160 return $this;
161 }
162
163 public function hasOldState() {
164 if ($this->hasOldState !== null) {
165 return $this->hasOldState;
166 }
167
168 $change_type = $this->getChangeType();
169 return !DifferentialChangeType::isCreateChangeType($change_type);
170 }
171
172 public function hasNewState() {
173 if ($this->hasNewState !== null) {
174 return $this->hasNewState;
175 }
176
177 $change_type = $this->getChangeType();
178 return !DifferentialChangeType::isDeleteChangeType($change_type);
179 }
180
181 public function save() {
182 $this->openTransaction();
183 $ret = parent::save();
184 foreach ($this->unsavedHunks as $hunk) {
185 $hunk->setChangesetID($this->getID());
186 $hunk->save();
187 }
188 $this->saveTransaction();
189 return $ret;
190 }
191
192 public function delete() {
193 $this->openTransaction();
194
195 $hunks = id(new DifferentialHunk())->loadAllWhere(
196 'changesetID = %d',
197 $this->getID());
198 foreach ($hunks as $hunk) {
199 $hunk->delete();
200 }
201
202 $this->unsavedHunks = array();
203
204 queryfx(
205 $this->establishConnection('w'),
206 'DELETE FROM %T WHERE id = %d',
207 self::TABLE_CACHE,
208 $this->getID());
209
210 $ret = parent::delete();
211 $this->saveTransaction();
212 return $ret;
213 }
214
215 /**
216 * Test if this changeset and some other changeset put the affected file in
217 * the same state.
218 *
219 * @param DifferentialChangeset $other Changeset to compare against.
220 * @return bool True if the two changesets have the same effect.
221 */
222 public function hasSameEffectAs(DifferentialChangeset $other) {
223 if ($this->getFilename() !== $other->getFilename()) {
224 return false;
225 }
226
227 $hash_key = self::METADATA_EFFECT_HASH;
228
229 $u_hash = $this->getChangesetMetadata($hash_key);
230 if ($u_hash === null) {
231 return false;
232 }
233
234 $v_hash = $other->getChangesetMetadata($hash_key);
235 if ($v_hash === null) {
236 return false;
237 }
238
239 if ($u_hash !== $v_hash) {
240 return false;
241 }
242
243 // Make sure the final states for the file properties (like the "+x"
244 // executable bit) match one another.
245 $u_props = $this->getNewProperties();
246 $v_props = $other->getNewProperties();
247 ksort($u_props);
248 ksort($v_props);
249
250 if ($u_props !== $v_props) {
251 return false;
252 }
253
254 return true;
255 }
256
257 public function getSortKey() {
258 $sort_key = $this->getFilename();
259 // Sort files with ".h" in them first, so headers (.h, .hpp) come before
260 // implementations (.c, .cpp, .cs).
261 $sort_key = str_replace('.h', '.!h', $sort_key);
262 return $sort_key;
263 }
264
265 public function makeNewFile() {
266 $file = mpull($this->getHunks(), 'makeNewFile');
267 return implode('', $file);
268 }
269
270 public function makeOldFile() {
271 $file = mpull($this->getHunks(), 'makeOldFile');
272 return implode('', $file);
273 }
274
275 public function makeChangesWithContext($num_lines = 3) {
276 $with_context = array();
277 foreach ($this->getHunks() as $hunk) {
278 $context = array();
279 $changes = explode("\n", $hunk->getChanges());
280 foreach ($changes as $l => $line) {
281 $type = substr($line, 0, 1);
282 if ($type == '+' || $type == '-') {
283 $context += array_fill($l - $num_lines, 2 * $num_lines + 1, true);
284 }
285 }
286 $with_context[] = array_intersect_key($changes, $context);
287 }
288 return array_mergev($with_context);
289 }
290
291 public function getAnchorName() {
292 return 'change-'.PhabricatorHash::digestForAnchor($this->getFilename());
293 }
294
295 public function getAbsoluteRepositoryPath(
296 ?PhabricatorRepository $repository = null,
297 ?DifferentialDiff $diff = null) {
298
299 $base = '/';
300 if ($diff && $diff->getSourceControlPath()) {
301 $base = id(new PhutilURI($diff->getSourceControlPath()))->getPath();
302 }
303
304 $path = $this->getFilename();
305 $path = rtrim($base, '/').'/'.ltrim($path, '/');
306
307 $svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
308 if ($repository && $repository->getVersionControlSystem() == $svn) {
309 $prefix = $repository->getDetail('remote-uri');
310 $prefix = id(new PhutilURI($prefix))->getPath();
311 if (!strncmp($path, $prefix, strlen($prefix))) {
312 $path = substr($path, strlen($prefix));
313 }
314 $path = '/'.ltrim($path, '/');
315 }
316
317 return $path;
318 }
319
320 public function attachDiff(DifferentialDiff $diff) {
321 $this->diff = $diff;
322 return $this;
323 }
324
325 public function getDiff() {
326 return $this->assertAttached($this->diff);
327 }
328
329 public function getOldStatePathVector() {
330 $path = $this->getOldFile();
331 if (!phutil_nonempty_string($path)) {
332 $path = $this->getFilename();
333 }
334
335 if (!phutil_nonempty_string($path)) {
336 return null;
337 }
338
339 $path = trim($path, '/');
340 return explode('/', $path);
341 }
342
343 public function getNewStatePathVector() {
344 if (!$this->hasNewState()) {
345 return null;
346 }
347
348 $path = $this->getFilename();
349 $path = trim($path, '/');
350 $path = explode('/', $path);
351
352 return $path;
353 }
354
355 public function newFileTreeIcon() {
356 $icon = $this->getPathIconIcon();
357 $color = $this->getPathIconColor();
358
359 return id(new PHUIIconView())
360 ->setIcon("{$icon} {$color}");
361 }
362
363 public function getIsOwnedChangeset() {
364 $authority_packages = $this->getAuthorityPackages();
365 $changeset_packages = $this->getChangesetPackages();
366
367 if (!$authority_packages || !$changeset_packages) {
368 return false;
369 }
370
371 return (bool)array_intersect_key($authority_packages, $changeset_packages);
372 }
373
374 public function getIsLowImportanceChangeset() {
375 if (!$this->hasNewState()) {
376 return true;
377 }
378
379 if ($this->isGeneratedChangeset()) {
380 return true;
381 }
382
383 return false;
384 }
385
386 public function getPathIconIcon() {
387 return idx($this->getPathIconDetails(), 'icon');
388 }
389
390 public function getPathIconColor() {
391 return idx($this->getPathIconDetails(), 'color');
392 }
393
394 private function getPathIconDetails() {
395 $change_icons = array(
396 DifferentialChangeType::TYPE_DELETE => array(
397 'icon' => 'fa-times',
398 'color' => 'delete-color',
399 ),
400 DifferentialChangeType::TYPE_ADD => array(
401 'icon' => 'fa-plus',
402 'color' => 'create-color',
403 ),
404 DifferentialChangeType::TYPE_MOVE_AWAY => array(
405 'icon' => 'fa-circle-o',
406 'color' => 'grey',
407 ),
408 DifferentialChangeType::TYPE_MULTICOPY => array(
409 'icon' => 'fa-circle-o',
410 'color' => 'grey',
411 ),
412 DifferentialChangeType::TYPE_MOVE_HERE => array(
413 'icon' => 'fa-plus-circle',
414 'color' => 'create-color',
415 ),
416 DifferentialChangeType::TYPE_COPY_HERE => array(
417 'icon' => 'fa-plus-circle',
418 'color' => 'create-color',
419 ),
420 );
421
422 $change_type = $this->getChangeType();
423 if (isset($change_icons[$change_type])) {
424 return $change_icons[$change_type];
425 }
426
427 if ($this->isGeneratedChangeset()) {
428 return array(
429 'icon' => 'fa-cogs',
430 'color' => 'grey',
431 );
432 }
433
434 $file_type = $this->getFileType();
435 $icon = DifferentialChangeType::getIconForFileType($file_type);
436
437 return array(
438 'icon' => $icon,
439 'color' => 'bluetext',
440 );
441 }
442
443 public function setChangesetMetadata($key, $value) {
444 if (!is_array($this->metadata)) {
445 $this->metadata = array();
446 }
447
448 $this->metadata[$key] = $value;
449
450 return $this;
451 }
452
453 public function getChangesetMetadata($key, $default = null) {
454 if (!is_array($this->metadata)) {
455 return $default;
456 }
457
458 return idx($this->metadata, $key, $default);
459 }
460
461 private function setInternalChangesetAttribute($trusted, $key, $value) {
462 if ($trusted) {
463 $meta_key = self::METADATA_TRUSTED_ATTRIBUTES;
464 } else {
465 $meta_key = self::METADATA_UNTRUSTED_ATTRIBUTES;
466 }
467
468 $attributes = $this->getChangesetMetadata($meta_key, array());
469 $attributes[$key] = $value;
470 $this->setChangesetMetadata($meta_key, $attributes);
471
472 return $this;
473 }
474
475 private function getInternalChangesetAttributes($trusted) {
476 if ($trusted) {
477 $meta_key = self::METADATA_TRUSTED_ATTRIBUTES;
478 } else {
479 $meta_key = self::METADATA_UNTRUSTED_ATTRIBUTES;
480 }
481
482 return $this->getChangesetMetadata($meta_key, array());
483 }
484
485 public function setTrustedChangesetAttribute($key, $value) {
486 return $this->setInternalChangesetAttribute(true, $key, $value);
487 }
488
489 public function getTrustedChangesetAttributes() {
490 return $this->getInternalChangesetAttributes(true);
491 }
492
493 public function getTrustedChangesetAttribute($key, $default = null) {
494 $map = $this->getTrustedChangesetAttributes();
495 return idx($map, $key, $default);
496 }
497
498 public function setUntrustedChangesetAttribute($key, $value) {
499 return $this->setInternalChangesetAttribute(false, $key, $value);
500 }
501
502 public function getUntrustedChangesetAttributes() {
503 return $this->getInternalChangesetAttributes(false);
504 }
505
506 public function getUntrustedChangesetAttribute($key, $default = null) {
507 $map = $this->getUntrustedChangesetAttributes();
508 return idx($map, $key, $default);
509 }
510
511 public function getChangesetAttributes() {
512 // Prefer trusted values over untrusted values when both exist.
513 return
514 $this->getTrustedChangesetAttributes() +
515 $this->getUntrustedChangesetAttributes();
516 }
517
518 public function getChangesetAttribute($key, $default = null) {
519 $map = $this->getChangesetAttributes();
520 return idx($map, $key, $default);
521 }
522
523 public function isGeneratedChangeset() {
524 return $this->getChangesetAttribute(self::ATTRIBUTE_GENERATED);
525 }
526
527 public function getNewFileObjectPHID() {
528 $metadata = $this->getMetadata();
529 return idx($metadata, 'new:binary-phid');
530 }
531
532 public function getOldFileObjectPHID() {
533 $metadata = $this->getMetadata();
534 return idx($metadata, 'old:binary-phid');
535 }
536
537 public function attachNewFileObject(PhabricatorFile $file) {
538 $this->newFileObject = $file;
539 return $this;
540 }
541
542 public function getNewFileObject() {
543 return $this->assertAttached($this->newFileObject);
544 }
545
546 public function attachOldFileObject(PhabricatorFile $file) {
547 $this->oldFileObject = $file;
548 return $this;
549 }
550
551 public function getOldFileObject() {
552 return $this->assertAttached($this->oldFileObject);
553 }
554
555 public function newComparisonChangeset(
556 ?DifferentialChangeset $against = null) {
557
558 $left = $this;
559 $right = $against;
560
561 $left_data = $left->makeNewFile();
562 $left_properties = $left->getNewProperties();
563 $left_metadata = $left->getNewStateMetadata();
564 $left_state = $left->hasNewState();
565 $shared_metadata = $left->getMetadata();
566 $left_type = $left->getNewFileType();
567 if ($right) {
568 $right_data = $right->makeNewFile();
569 $right_properties = $right->getNewProperties();
570 $right_metadata = $right->getNewStateMetadata();
571 $right_state = $right->hasNewState();
572 $shared_metadata = $right->getMetadata();
573 $right_type = $right->getNewFileType();
574
575 $file_name = $right->getFilename();
576 } else {
577 $right_data = $left->makeOldFile();
578 $right_properties = $left->getOldProperties();
579 $right_metadata = $left->getOldStateMetadata();
580 $right_state = $left->hasOldState();
581 $right_type = $left->getOldFileType();
582
583 $file_name = $left->getFilename();
584 }
585
586 $engine = new PhabricatorDifferenceEngine();
587
588 $synthetic = $engine->generateChangesetFromFileContent(
589 $left_data,
590 $right_data);
591
592 $comparison = id(new self())
593 ->makeEphemeral()
594 ->attachDiff($left->getDiff())
595 ->setOldFile($left->getFilename())
596 ->setFilename($file_name);
597
598 // TODO: Change type?
599 // TODO: Away paths?
600 // TODO: View state key?
601
602 $comparison->attachHunks($synthetic->getHunks());
603
604 $comparison->setOldProperties($left_properties);
605 $comparison->setNewProperties($right_properties);
606
607 $comparison
608 ->setOldStateMetadata($left_metadata)
609 ->setNewStateMetadata($right_metadata)
610 ->setHasOldState($left_state)
611 ->setHasNewState($right_state)
612 ->setOldFileType($left_type)
613 ->setNewFileType($right_type);
614
615 // NOTE: Some metadata is not stored statefully, like the "generated"
616 // flag. For now, use the rightmost "new state" metadata to fill in these
617 // values.
618
619 $metadata = $comparison->getMetadata();
620 $metadata = $metadata + $shared_metadata;
621 $comparison->setMetadata($metadata);
622
623 return $comparison;
624 }
625
626
627 public function setNewFileType($new_file_type) {
628 $this->newFileType = $new_file_type;
629 return $this;
630 }
631
632 public function getNewFileType() {
633 if ($this->newFileType !== null) {
634 return $this->newFileType;
635 }
636
637 return $this->getFiletype();
638 }
639
640 public function setOldFileType($old_file_type) {
641 $this->oldFileType = $old_file_type;
642 return $this;
643 }
644
645 public function getOldFileType() {
646 if ($this->oldFileType !== null) {
647 return $this->oldFileType;
648 }
649
650 return $this->getFileType();
651 }
652
653 public function hasSourceTextBody() {
654 $type_map = array(
655 DifferentialChangeType::FILE_TEXT => true,
656 DifferentialChangeType::FILE_SYMLINK => true,
657 );
658
659 $old_body = isset($type_map[$this->getOldFileType()]);
660 $new_body = isset($type_map[$this->getNewFileType()]);
661
662 return ($old_body || $new_body);
663 }
664
665 public function getNewStateMetadata() {
666 return $this->getMetadataWithPrefix('new:');
667 }
668
669 public function setNewStateMetadata(array $metadata) {
670 return $this->setMetadataWithPrefix($metadata, 'new:');
671 }
672
673 public function getOldStateMetadata() {
674 return $this->getMetadataWithPrefix('old:');
675 }
676
677 public function setOldStateMetadata(array $metadata) {
678 return $this->setMetadataWithPrefix($metadata, 'old:');
679 }
680
681 private function getMetadataWithPrefix($prefix) {
682 $length = strlen($prefix);
683
684 $result = array();
685 foreach ($this->getMetadata() as $key => $value) {
686 if (strncmp($key, $prefix, $length)) {
687 continue;
688 }
689
690 $key = substr($key, $length);
691 $result[$key] = $value;
692 }
693
694 return $result;
695 }
696
697 private function setMetadataWithPrefix(array $metadata, $prefix) {
698 foreach ($metadata as $key => $value) {
699 $key = $prefix.$key;
700 $this->metadata[$key] = $value;
701 }
702
703 return $this;
704 }
705
706
707/* -( PhabricatorPolicyInterface )----------------------------------------- */
708
709
710 public function getCapabilities() {
711 return array(
712 PhabricatorPolicyCapability::CAN_VIEW,
713 );
714 }
715
716 public function getPolicy($capability) {
717 return $this->getDiff()->getPolicy($capability);
718 }
719
720 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
721 return $this->getDiff()->hasAutomaticCapability($capability, $viewer);
722 }
723
724
725/* -( PhabricatorDestructibleInterface )----------------------------------- */
726
727
728 public function destroyObjectPermanently(
729 PhabricatorDestructionEngine $engine) {
730 $this->openTransaction();
731
732 $hunks = id(new DifferentialHunk())->loadAllWhere(
733 'changesetID = %d',
734 $this->getID());
735 foreach ($hunks as $hunk) {
736 $engine->destroyObject($hunk);
737 }
738
739 $this->delete();
740
741 $this->saveTransaction();
742 }
743
744/* -( PhabricatorConduitResultInterface )---------------------------------- */
745
746 public function getFieldSpecificationsForConduit() {
747 return array(
748 id(new PhabricatorConduitSearchFieldSpecification())
749 ->setKey('diffPHID')
750 ->setType('phid')
751 ->setDescription(pht('The diff the changeset is attached to.')),
752 );
753 }
754
755 public function getFieldValuesForConduit() {
756 $diff = $this->getDiff();
757
758 $repository = null;
759 if ($diff) {
760 $revision = $diff->getRevision();
761 if ($revision) {
762 $repository = $revision->getRepository();
763 }
764 }
765
766 $absolute_path = $this->getAbsoluteRepositoryPath($repository, $diff);
767 if (strlen($absolute_path)) {
768 $absolute_path = base64_encode($absolute_path);
769 } else {
770 $absolute_path = null;
771 }
772
773 $display_path = $this->getDisplayFilename();
774
775 return array(
776 'diffPHID' => $diff->getPHID(),
777 'path' => array(
778 'displayPath' => $display_path,
779 'absolutePath.base64' => $absolute_path,
780 ),
781 );
782 }
783
784 public function getConduitSearchAttachments() {
785 return array();
786 }
787
788
789}