3 final class DifferentialChangeset
4 extends DifferentialDAO
6 PhabricatorPolicyInterface
,
7 PhabricatorDestructibleInterface
,
8 PhabricatorConduitResultInterface
{
14 protected $changeType;
16 protected $metadata = array();
17 protected $oldProperties;
18 protected $newProperties;
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
;
34 private $oldStateMetadata;
35 private $newStateMetadata;
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() {
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',
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(
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;
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
) {
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])) {
116 if ($path[0] != '/') {
123 public function addUnsavedHunk(DifferentialHunk
$hunk) {
124 if ($this->hunks
=== self
::ATTACHABLE
) {
125 $this->hunks
= array();
127 $this->hunks
[] = $hunk;
128 $this->unsavedHunks
[] = $hunk;
132 public function setAuthorityPackages(array $authority_packages) {
133 $this->authorityPackages
= mpull($authority_packages, null, 'getPHID');
137 public function getAuthorityPackages() {
138 return $this->authorityPackages
;
141 public function setChangesetPackages($changeset_packages) {
142 $this->changesetPackages
= mpull($changeset_packages, null, 'getPHID');
146 public function getChangesetPackages() {
147 return $this->changesetPackages
;
150 public function setHasOldState($has_old_state) {
151 $this->hasOldState
= $has_old_state;
155 public function setHasNewState($has_new_state) {
156 $this->hasNewState
= $has_new_state;
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());
185 $this->saveTransaction();
189 public function delete() {
190 $this->openTransaction();
192 $hunks = id(new DifferentialHunk())->loadAllWhere(
195 foreach ($hunks as $hunk) {
199 $this->unsavedHunks
= array();
202 $this->establishConnection('w'),
203 'DELETE FROM %T WHERE id = %d',
207 $ret = parent
::delete();
208 $this->saveTransaction();
213 * Test if this changeset and some other changeset put the affected file in
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()) {
224 $hash_key = self
::METADATA_EFFECT_HASH
;
226 $u_hash = $this->getChangesetMetadata($hash_key);
227 if ($u_hash === null) {
231 $v_hash = $other->getChangesetMetadata($hash_key);
232 if ($v_hash === null) {
236 if ($u_hash !== $v_hash) {
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();
247 if ($u_props !== $v_props) {
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);
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) {
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) {
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, '/');
317 public function attachDiff(DifferentialDiff
$diff) {
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);
338 public function getNewStatePathVector() {
339 if (!$this->hasNewState()) {
343 $path = $this->getFilename();
344 $path = trim($path, '/');
345 $path = explode('/', $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) {
366 return (bool)array_intersect_key($authority_packages, $changeset_packages);
369 public function getIsLowImportanceChangeset() {
370 if (!$this->hasNewState()) {
374 if ($this->isGeneratedChangeset()) {
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(
397 'color' => 'create-color',
399 DifferentialChangeType
::TYPE_MOVE_AWAY
=> array(
400 'icon' => 'fa-circle-o',
403 DifferentialChangeType
::TYPE_MULTICOPY
=> array(
404 'icon' => 'fa-circle-o',
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()) {
429 $file_type = $this->getFileType();
430 $icon = DifferentialChangeType
::getIconForFileType($file_type);
434 'color' => 'bluetext',
438 public function setChangesetMetadata($key, $value) {
439 if (!is_array($this->metadata
)) {
440 $this->metadata
= array();
443 $this->metadata
[$key] = $value;
448 public function getChangesetMetadata($key, $default = null) {
449 if (!is_array($this->metadata
)) {
453 return idx($this->metadata
, $key, $default);
456 private function setInternalChangesetAttribute($trusted, $key, $value) {
458 $meta_key = self
::METADATA_TRUSTED_ATTRIBUTES
;
460 $meta_key = self
::METADATA_UNTRUSTED_ATTRIBUTES
;
463 $attributes = $this->getChangesetMetadata($meta_key, array());
464 $attributes[$key] = $value;
465 $this->setChangesetMetadata($meta_key, $attributes);
470 private function getInternalChangesetAttributes($trusted) {
472 $meta_key = self
::METADATA_TRUSTED_ATTRIBUTES
;
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.
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;
537 public function getNewFileObject() {
538 return $this->assertAttached($this->newFileObject
);
541 public function attachOldFileObject(PhabricatorFile
$file) {
542 $this->oldFileObject
= $file;
546 public function getOldFileObject() {
547 return $this->assertAttached($this->oldFileObject
);
550 public function newComparisonChangeset(
551 DifferentialChangeset
$against = null) {
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();
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();
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(
587 $comparison = id(new self())
588 ->makeEphemeral(true)
589 ->attachDiff($left->getDiff())
590 ->setOldFile($left->getFilename())
591 ->setFilename($file_name);
593 // TODO: Change type?
595 // TODO: View state key?
597 $comparison->attachHunks($synthetic->getHunks());
599 $comparison->setOldProperties($left_properties);
600 $comparison->setNewProperties($right_properties);
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
614 $metadata = $comparison->getMetadata();
615 $metadata = $metadata +
$shared_metadata;
616 $comparison->setMetadata($metadata);
622 public function setNewFileType($new_file_type) {
623 $this->newFileType
= $new_file_type;
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;
640 public function getOldFileType() {
641 if ($this->oldFileType
!== null) {
642 return $this->oldFileType
;
645 return $this->getFileType();
648 public function hasSourceTextBody() {
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);
680 foreach ($this->getMetadata() as $key => $value) {
681 if (strncmp($key, $prefix, $length)) {
685 $key = substr($key, $length);
686 $result[$key] = $value;
692 private function setMetadataWithPrefix(array $metadata, $prefix) {
693 foreach ($metadata as $key => $value) {
695 $this->metadata
[$key] = $value;
702 /* -( PhabricatorPolicyInterface )----------------------------------------- */
705 public function getCapabilities() {
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(
730 foreach ($hunks as $hunk) {
731 $engine->destroyObject($hunk);
736 $this->saveTransaction();
739 /* -( PhabricatorConduitResultInterface )---------------------------------- */
741 public function getFieldSpecificationsForConduit() {
743 id(new PhabricatorConduitSearchFieldSpecification())
746 ->setDescription(pht('The diff the changeset is attached to.')),
750 public function getFieldValuesForConduit() {
751 $diff = $this->getDiff();
755 $revision = $diff->getRevision();
757 $repository = $revision->getRepository();
761 $absolute_path = $this->getAbsoluteRepositoryPath($repository, $diff);
762 if (strlen($absolute_path)) {
763 $absolute_path = base64_encode($absolute_path);
765 $absolute_path = null;
768 $display_path = $this->getDisplayFilename();
771 'diffPHID' => $diff->getPHID(),
773 'displayPath' => $display_path,
774 'absolutePath.base64' => $absolute_path,
779 public function getConduitSearchAttachments() {