Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / differential / storage / DifferentialChangeset.php
blob374a3f3238e474a06bde3a9506ed0e1668601e3d
1 <?php
3 final class DifferentialChangeset
4 extends DifferentialDAO
5 implements
6 PhabricatorPolicyInterface,
7 PhabricatorDestructibleInterface,
8 PhabricatorConduitResultInterface {
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;
22 private $unsavedHunks = array();
23 private $hunks = self::ATTACHABLE;
24 private $diff = self::ATTACHABLE;
26 private $authorityPackages;
27 private $changesetPackages;
29 private $newFileObject = self::ATTACHABLE;
30 private $oldFileObject = self::ATTACHABLE;
32 private $hasOldState;
33 private $hasNewState;
34 private $oldStateMetadata;
35 private $newStateMetadata;
36 private $oldFileType;
37 private $newFileType;
39 const TABLE_CACHE = 'differential_changeset_parse_cache';
41 const METADATA_TRUSTED_ATTRIBUTES = 'attributes.trusted';
42 const METADATA_UNTRUSTED_ATTRIBUTES = 'attributes.untrusted';
43 const METADATA_EFFECT_HASH = 'hash.effect';
45 const ATTRIBUTE_GENERATED = 'generated';
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,
56 self::CONFIG_COLUMN_SCHEMA => array(
57 'oldFile' => 'bytes?',
58 'filename' => 'bytes',
59 'changeType' => 'uint32',
60 'fileType' => 'uint32',
61 'addLines' => 'uint32',
62 'delLines' => 'uint32',
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?',
72 self::CONFIG_KEY_SCHEMA => array(
73 'diffID' => array(
74 'columns' => array('diffID'),
77 ) + parent::getConfiguration();
80 public function getPHIDType() {
81 return DifferentialChangesetPHIDType::TYPECONST;
84 public function getAffectedLineCount() {
85 return $this->getAddLines() + $this->getDelLines();
88 public function attachHunks(array $hunks) {
89 assert_instances_of($hunks, 'DifferentialHunk');
90 $this->hunks = $hunks;
91 return $this;
94 public function getHunks() {
95 return $this->assertAttached($this->hunks);
98 public function getDisplayFilename() {
99 $name = $this->getFilename();
100 if ($this->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
101 $name .= '/';
103 return $name;
106 public function getOwnersFilename() {
107 // TODO: For Subversion, we should adjust these paths to be relative to
108 // the repository root where possible.
110 $path = $this->getFilename();
112 if (!isset($path[0])) {
113 return '/';
116 if ($path[0] != '/') {
117 $path = '/'.$path;
120 return $path;
123 public function addUnsavedHunk(DifferentialHunk $hunk) {
124 if ($this->hunks === self::ATTACHABLE) {
125 $this->hunks = array();
127 $this->hunks[] = $hunk;
128 $this->unsavedHunks[] = $hunk;
129 return $this;
132 public function setAuthorityPackages(array $authority_packages) {
133 $this->authorityPackages = mpull($authority_packages, null, 'getPHID');
134 return $this;
137 public function getAuthorityPackages() {
138 return $this->authorityPackages;
141 public function setChangesetPackages($changeset_packages) {
142 $this->changesetPackages = mpull($changeset_packages, null, 'getPHID');
143 return $this;
146 public function getChangesetPackages() {
147 return $this->changesetPackages;
150 public function setHasOldState($has_old_state) {
151 $this->hasOldState = $has_old_state;
152 return $this;
155 public function setHasNewState($has_new_state) {
156 $this->hasNewState = $has_new_state;
157 return $this;
160 public function hasOldState() {
161 if ($this->hasOldState !== null) {
162 return $this->hasOldState;
165 $change_type = $this->getChangeType();
166 return !DifferentialChangeType::isCreateChangeType($change_type);
169 public function hasNewState() {
170 if ($this->hasNewState !== null) {
171 return $this->hasNewState;
174 $change_type = $this->getChangeType();
175 return !DifferentialChangeType::isDeleteChangeType($change_type);
178 public function save() {
179 $this->openTransaction();
180 $ret = parent::save();
181 foreach ($this->unsavedHunks as $hunk) {
182 $hunk->setChangesetID($this->getID());
183 $hunk->save();
185 $this->saveTransaction();
186 return $ret;
189 public function delete() {
190 $this->openTransaction();
192 $hunks = id(new DifferentialHunk())->loadAllWhere(
193 'changesetID = %d',
194 $this->getID());
195 foreach ($hunks as $hunk) {
196 $hunk->delete();
199 $this->unsavedHunks = array();
201 queryfx(
202 $this->establishConnection('w'),
203 'DELETE FROM %T WHERE id = %d',
204 self::TABLE_CACHE,
205 $this->getID());
207 $ret = parent::delete();
208 $this->saveTransaction();
209 return $ret;
213 * Test if this changeset and some other changeset put the affected file in
214 * the same state.
216 * @param DifferentialChangeset Changeset to compare against.
217 * @return bool True if the two changesets have the same effect.
219 public function hasSameEffectAs(DifferentialChangeset $other) {
220 if ($this->getFilename() !== $other->getFilename()) {
221 return false;
224 $hash_key = self::METADATA_EFFECT_HASH;
226 $u_hash = $this->getChangesetMetadata($hash_key);
227 if ($u_hash === null) {
228 return false;
231 $v_hash = $other->getChangesetMetadata($hash_key);
232 if ($v_hash === null) {
233 return false;
236 if ($u_hash !== $v_hash) {
237 return false;
240 // Make sure the final states for the file properties (like the "+x"
241 // executable bit) match one another.
242 $u_props = $this->getNewProperties();
243 $v_props = $other->getNewProperties();
244 ksort($u_props);
245 ksort($v_props);
247 if ($u_props !== $v_props) {
248 return false;
251 return true;
254 public function getSortKey() {
255 $sort_key = $this->getFilename();
256 // Sort files with ".h" in them first, so headers (.h, .hpp) come before
257 // implementations (.c, .cpp, .cs).
258 $sort_key = str_replace('.h', '.!h', $sort_key);
259 return $sort_key;
262 public function makeNewFile() {
263 $file = mpull($this->getHunks(), 'makeNewFile');
264 return implode('', $file);
267 public function makeOldFile() {
268 $file = mpull($this->getHunks(), 'makeOldFile');
269 return implode('', $file);
272 public function makeChangesWithContext($num_lines = 3) {
273 $with_context = array();
274 foreach ($this->getHunks() as $hunk) {
275 $context = array();
276 $changes = explode("\n", $hunk->getChanges());
277 foreach ($changes as $l => $line) {
278 $type = substr($line, 0, 1);
279 if ($type == '+' || $type == '-') {
280 $context += array_fill($l - $num_lines, 2 * $num_lines + 1, true);
283 $with_context[] = array_intersect_key($changes, $context);
285 return array_mergev($with_context);
288 public function getAnchorName() {
289 return 'change-'.PhabricatorHash::digestForAnchor($this->getFilename());
292 public function getAbsoluteRepositoryPath(
293 PhabricatorRepository $repository = null,
294 DifferentialDiff $diff = null) {
296 $base = '/';
297 if ($diff && $diff->getSourceControlPath()) {
298 $base = id(new PhutilURI($diff->getSourceControlPath()))->getPath();
301 $path = $this->getFilename();
302 $path = rtrim($base, '/').'/'.ltrim($path, '/');
304 $svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
305 if ($repository && $repository->getVersionControlSystem() == $svn) {
306 $prefix = $repository->getDetail('remote-uri');
307 $prefix = id(new PhutilURI($prefix))->getPath();
308 if (!strncmp($path, $prefix, strlen($prefix))) {
309 $path = substr($path, strlen($prefix));
311 $path = '/'.ltrim($path, '/');
314 return $path;
317 public function attachDiff(DifferentialDiff $diff) {
318 $this->diff = $diff;
319 return $this;
322 public function getDiff() {
323 return $this->assertAttached($this->diff);
326 public function getOldStatePathVector() {
327 $path = $this->getOldFile();
328 if ($path === null || !strlen($path)) {
329 $path = $this->getFilename();
332 $path = trim($path, '/');
333 $path = explode('/', $path);
335 return $path;
338 public function getNewStatePathVector() {
339 if (!$this->hasNewState()) {
340 return null;
343 $path = $this->getFilename();
344 $path = trim($path, '/');
345 $path = explode('/', $path);
347 return $path;
350 public function newFileTreeIcon() {
351 $icon = $this->getPathIconIcon();
352 $color = $this->getPathIconColor();
354 return id(new PHUIIconView())
355 ->setIcon("{$icon} {$color}");
358 public function getIsOwnedChangeset() {
359 $authority_packages = $this->getAuthorityPackages();
360 $changeset_packages = $this->getChangesetPackages();
362 if (!$authority_packages || !$changeset_packages) {
363 return false;
366 return (bool)array_intersect_key($authority_packages, $changeset_packages);
369 public function getIsLowImportanceChangeset() {
370 if (!$this->hasNewState()) {
371 return true;
374 if ($this->isGeneratedChangeset()) {
375 return true;
378 return false;
381 public function getPathIconIcon() {
382 return idx($this->getPathIconDetails(), 'icon');
385 public function getPathIconColor() {
386 return idx($this->getPathIconDetails(), 'color');
389 private function getPathIconDetails() {
390 $change_icons = array(
391 DifferentialChangeType::TYPE_DELETE => array(
392 'icon' => 'fa-times',
393 'color' => 'delete-color',
395 DifferentialChangeType::TYPE_ADD => array(
396 'icon' => 'fa-plus',
397 'color' => 'create-color',
399 DifferentialChangeType::TYPE_MOVE_AWAY => array(
400 'icon' => 'fa-circle-o',
401 'color' => 'grey',
403 DifferentialChangeType::TYPE_MULTICOPY => array(
404 'icon' => 'fa-circle-o',
405 'color' => 'grey',
407 DifferentialChangeType::TYPE_MOVE_HERE => array(
408 'icon' => 'fa-plus-circle',
409 'color' => 'create-color',
411 DifferentialChangeType::TYPE_COPY_HERE => array(
412 'icon' => 'fa-plus-circle',
413 'color' => 'create-color',
417 $change_type = $this->getChangeType();
418 if (isset($change_icons[$change_type])) {
419 return $change_icons[$change_type];
422 if ($this->isGeneratedChangeset()) {
423 return array(
424 'icon' => 'fa-cogs',
425 'color' => 'grey',
429 $file_type = $this->getFileType();
430 $icon = DifferentialChangeType::getIconForFileType($file_type);
432 return array(
433 'icon' => $icon,
434 'color' => 'bluetext',
438 public function setChangesetMetadata($key, $value) {
439 if (!is_array($this->metadata)) {
440 $this->metadata = array();
443 $this->metadata[$key] = $value;
445 return $this;
448 public function getChangesetMetadata($key, $default = null) {
449 if (!is_array($this->metadata)) {
450 return $default;
453 return idx($this->metadata, $key, $default);
456 private function setInternalChangesetAttribute($trusted, $key, $value) {
457 if ($trusted) {
458 $meta_key = self::METADATA_TRUSTED_ATTRIBUTES;
459 } else {
460 $meta_key = self::METADATA_UNTRUSTED_ATTRIBUTES;
463 $attributes = $this->getChangesetMetadata($meta_key, array());
464 $attributes[$key] = $value;
465 $this->setChangesetMetadata($meta_key, $attributes);
467 return $this;
470 private function getInternalChangesetAttributes($trusted) {
471 if ($trusted) {
472 $meta_key = self::METADATA_TRUSTED_ATTRIBUTES;
473 } else {
474 $meta_key = self::METADATA_UNTRUSTED_ATTRIBUTES;
477 return $this->getChangesetMetadata($meta_key, array());
480 public function setTrustedChangesetAttribute($key, $value) {
481 return $this->setInternalChangesetAttribute(true, $key, $value);
484 public function getTrustedChangesetAttributes() {
485 return $this->getInternalChangesetAttributes(true);
488 public function getTrustedChangesetAttribute($key, $default = null) {
489 $map = $this->getTrustedChangesetAttributes();
490 return idx($map, $key, $default);
493 public function setUntrustedChangesetAttribute($key, $value) {
494 return $this->setInternalChangesetAttribute(false, $key, $value);
497 public function getUntrustedChangesetAttributes() {
498 return $this->getInternalChangesetAttributes(false);
501 public function getUntrustedChangesetAttribute($key, $default = null) {
502 $map = $this->getUntrustedChangesetAttributes();
503 return idx($map, $key, $default);
506 public function getChangesetAttributes() {
507 // Prefer trusted values over untrusted values when both exist.
508 return
509 $this->getTrustedChangesetAttributes() +
510 $this->getUntrustedChangesetAttributes();
513 public function getChangesetAttribute($key, $default = null) {
514 $map = $this->getChangesetAttributes();
515 return idx($map, $key, $default);
518 public function isGeneratedChangeset() {
519 return $this->getChangesetAttribute(self::ATTRIBUTE_GENERATED);
522 public function getNewFileObjectPHID() {
523 $metadata = $this->getMetadata();
524 return idx($metadata, 'new:binary-phid');
527 public function getOldFileObjectPHID() {
528 $metadata = $this->getMetadata();
529 return idx($metadata, 'old:binary-phid');
532 public function attachNewFileObject(PhabricatorFile $file) {
533 $this->newFileObject = $file;
534 return $this;
537 public function getNewFileObject() {
538 return $this->assertAttached($this->newFileObject);
541 public function attachOldFileObject(PhabricatorFile $file) {
542 $this->oldFileObject = $file;
543 return $this;
546 public function getOldFileObject() {
547 return $this->assertAttached($this->oldFileObject);
550 public function newComparisonChangeset(
551 DifferentialChangeset $against = null) {
553 $left = $this;
554 $right = $against;
556 $left_data = $left->makeNewFile();
557 $left_properties = $left->getNewProperties();
558 $left_metadata = $left->getNewStateMetadata();
559 $left_state = $left->hasNewState();
560 $shared_metadata = $left->getMetadata();
561 $left_type = $left->getNewFileType();
562 if ($right) {
563 $right_data = $right->makeNewFile();
564 $right_properties = $right->getNewProperties();
565 $right_metadata = $right->getNewStateMetadata();
566 $right_state = $right->hasNewState();
567 $shared_metadata = $right->getMetadata();
568 $right_type = $right->getNewFileType();
570 $file_name = $right->getFilename();
571 } else {
572 $right_data = $left->makeOldFile();
573 $right_properties = $left->getOldProperties();
574 $right_metadata = $left->getOldStateMetadata();
575 $right_state = $left->hasOldState();
576 $right_type = $left->getOldFileType();
578 $file_name = $left->getFilename();
581 $engine = new PhabricatorDifferenceEngine();
583 $synthetic = $engine->generateChangesetFromFileContent(
584 $left_data,
585 $right_data);
587 $comparison = id(new self())
588 ->makeEphemeral(true)
589 ->attachDiff($left->getDiff())
590 ->setOldFile($left->getFilename())
591 ->setFilename($file_name);
593 // TODO: Change type?
594 // TODO: Away paths?
595 // TODO: View state key?
597 $comparison->attachHunks($synthetic->getHunks());
599 $comparison->setOldProperties($left_properties);
600 $comparison->setNewProperties($right_properties);
602 $comparison
603 ->setOldStateMetadata($left_metadata)
604 ->setNewStateMetadata($right_metadata)
605 ->setHasOldState($left_state)
606 ->setHasNewState($right_state)
607 ->setOldFileType($left_type)
608 ->setNewFileType($right_type);
610 // NOTE: Some metadata is not stored statefully, like the "generated"
611 // flag. For now, use the rightmost "new state" metadata to fill in these
612 // values.
614 $metadata = $comparison->getMetadata();
615 $metadata = $metadata + $shared_metadata;
616 $comparison->setMetadata($metadata);
618 return $comparison;
622 public function setNewFileType($new_file_type) {
623 $this->newFileType = $new_file_type;
624 return $this;
627 public function getNewFileType() {
628 if ($this->newFileType !== null) {
629 return $this->newFileType;
632 return $this->getFiletype();
635 public function setOldFileType($old_file_type) {
636 $this->oldFileType = $old_file_type;
637 return $this;
640 public function getOldFileType() {
641 if ($this->oldFileType !== null) {
642 return $this->oldFileType;
645 return $this->getFileType();
648 public function hasSourceTextBody() {
649 $type_map = array(
650 DifferentialChangeType::FILE_TEXT => true,
651 DifferentialChangeType::FILE_SYMLINK => true,
654 $old_body = isset($type_map[$this->getOldFileType()]);
655 $new_body = isset($type_map[$this->getNewFileType()]);
657 return ($old_body || $new_body);
660 public function getNewStateMetadata() {
661 return $this->getMetadataWithPrefix('new:');
664 public function setNewStateMetadata(array $metadata) {
665 return $this->setMetadataWithPrefix($metadata, 'new:');
668 public function getOldStateMetadata() {
669 return $this->getMetadataWithPrefix('old:');
672 public function setOldStateMetadata(array $metadata) {
673 return $this->setMetadataWithPrefix($metadata, 'old:');
676 private function getMetadataWithPrefix($prefix) {
677 $length = strlen($prefix);
679 $result = array();
680 foreach ($this->getMetadata() as $key => $value) {
681 if (strncmp($key, $prefix, $length)) {
682 continue;
685 $key = substr($key, $length);
686 $result[$key] = $value;
689 return $result;
692 private function setMetadataWithPrefix(array $metadata, $prefix) {
693 foreach ($metadata as $key => $value) {
694 $key = $prefix.$key;
695 $this->metadata[$key] = $value;
698 return $this;
702 /* -( PhabricatorPolicyInterface )----------------------------------------- */
705 public function getCapabilities() {
706 return array(
707 PhabricatorPolicyCapability::CAN_VIEW,
711 public function getPolicy($capability) {
712 return $this->getDiff()->getPolicy($capability);
715 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
716 return $this->getDiff()->hasAutomaticCapability($capability, $viewer);
720 /* -( PhabricatorDestructibleInterface )----------------------------------- */
723 public function destroyObjectPermanently(
724 PhabricatorDestructionEngine $engine) {
725 $this->openTransaction();
727 $hunks = id(new DifferentialHunk())->loadAllWhere(
728 'changesetID = %d',
729 $this->getID());
730 foreach ($hunks as $hunk) {
731 $engine->destroyObject($hunk);
734 $this->delete();
736 $this->saveTransaction();
739 /* -( PhabricatorConduitResultInterface )---------------------------------- */
741 public function getFieldSpecificationsForConduit() {
742 return array(
743 id(new PhabricatorConduitSearchFieldSpecification())
744 ->setKey('diffPHID')
745 ->setType('phid')
746 ->setDescription(pht('The diff the changeset is attached to.')),
750 public function getFieldValuesForConduit() {
751 $diff = $this->getDiff();
753 $repository = null;
754 if ($diff) {
755 $revision = $diff->getRevision();
756 if ($revision) {
757 $repository = $revision->getRepository();
761 $absolute_path = $this->getAbsoluteRepositoryPath($repository, $diff);
762 if (strlen($absolute_path)) {
763 $absolute_path = base64_encode($absolute_path);
764 } else {
765 $absolute_path = null;
768 $display_path = $this->getDisplayFilename();
770 return array(
771 'diffPHID' => $diff->getPHID(),
772 'path' => array(
773 'displayPath' => $display_path,
774 'absolutePath.base64' => $absolute_path,
779 public function getConduitSearchAttachments() {
780 return array();