Generate file attachment transactions for explicit Remarkup attachments on common...
[phabricator.git] / src / applications / differential / storage / DifferentialDiff.php
blobdfa6d1f791d9ac4c331b7a936bea28f0d80dce8f
1 <?php
3 final class DifferentialDiff
4 extends DifferentialDAO
5 implements
6 PhabricatorPolicyInterface,
7 PhabricatorExtendedPolicyInterface,
8 HarbormasterBuildableInterface,
9 HarbormasterCircleCIBuildableInterface,
10 HarbormasterBuildkiteBuildableInterface,
11 PhabricatorApplicationTransactionInterface,
12 PhabricatorDestructibleInterface,
13 PhabricatorConduitResultInterface {
15 protected $revisionID;
16 protected $authorPHID;
17 protected $repositoryPHID;
18 protected $commitPHID;
20 protected $sourceMachine;
21 protected $sourcePath;
23 protected $sourceControlSystem;
24 protected $sourceControlBaseRevision;
25 protected $sourceControlPath;
27 protected $lintStatus;
28 protected $unitStatus;
30 protected $lineCount;
32 protected $branch;
33 protected $bookmark;
35 protected $creationMethod;
36 protected $repositoryUUID;
38 protected $description;
40 protected $viewPolicy;
42 private $unsavedChangesets = array();
43 private $changesets = self::ATTACHABLE;
44 private $revision = self::ATTACHABLE;
45 private $properties = self::ATTACHABLE;
46 private $buildable = self::ATTACHABLE;
48 private $unitMessages = self::ATTACHABLE;
50 protected function getConfiguration() {
51 return array(
52 self::CONFIG_AUX_PHID => true,
53 self::CONFIG_COLUMN_SCHEMA => array(
54 'revisionID' => 'id?',
55 'authorPHID' => 'phid?',
56 'repositoryPHID' => 'phid?',
57 'sourceMachine' => 'text255?',
58 'sourcePath' => 'text255?',
59 'sourceControlSystem' => 'text64?',
60 'sourceControlBaseRevision' => 'text255?',
61 'sourceControlPath' => 'text255?',
62 'lintStatus' => 'uint32',
63 'unitStatus' => 'uint32',
64 'lineCount' => 'uint32',
65 'branch' => 'text255?',
66 'bookmark' => 'text255?',
67 'repositoryUUID' => 'text64?',
68 'commitPHID' => 'phid?',
70 // T6203/NULLABILITY
71 // These should be non-null; all diffs should have a creation method
72 // and the description should just be empty.
73 'creationMethod' => 'text255?',
74 'description' => 'text255?',
76 self::CONFIG_KEY_SCHEMA => array(
77 'revisionID' => array(
78 'columns' => array('revisionID'),
80 'key_commit' => array(
81 'columns' => array('commitPHID'),
84 ) + parent::getConfiguration();
87 public function generatePHID() {
88 return PhabricatorPHID::generateNewPHID(
89 DifferentialDiffPHIDType::TYPECONST);
92 public function addUnsavedChangeset(DifferentialChangeset $changeset) {
93 if ($this->changesets === null) {
94 $this->changesets = array();
96 $this->unsavedChangesets[] = $changeset;
97 $this->changesets[] = $changeset;
98 return $this;
101 public function attachChangesets(array $changesets) {
102 assert_instances_of($changesets, 'DifferentialChangeset');
103 $this->changesets = $changesets;
104 return $this;
107 public function getChangesets() {
108 return $this->assertAttached($this->changesets);
111 public function loadChangesets() {
112 if (!$this->getID()) {
113 return array();
115 $changesets = id(new DifferentialChangeset())->loadAllWhere(
116 'diffID = %d',
117 $this->getID());
119 foreach ($changesets as $changeset) {
120 $changeset->attachDiff($this);
123 return $changesets;
126 public function save() {
127 $this->openTransaction();
128 $ret = parent::save();
129 foreach ($this->unsavedChangesets as $changeset) {
130 $changeset->setDiffID($this->getID());
131 $changeset->save();
133 $this->saveTransaction();
134 return $ret;
137 public static function initializeNewDiff(PhabricatorUser $actor) {
138 $app = id(new PhabricatorApplicationQuery())
139 ->setViewer($actor)
140 ->withClasses(array('PhabricatorDifferentialApplication'))
141 ->executeOne();
142 $view_policy = $app->getPolicy(
143 DifferentialDefaultViewCapability::CAPABILITY);
145 $diff = id(new DifferentialDiff())
146 ->setViewPolicy($view_policy);
148 return $diff;
151 public static function newFromRawChanges(
152 PhabricatorUser $actor,
153 array $changes) {
155 assert_instances_of($changes, 'ArcanistDiffChange');
157 $diff = self::initializeNewDiff($actor);
158 return self::buildChangesetsFromRawChanges($diff, $changes);
161 public static function newEphemeralFromRawChanges(array $changes) {
162 assert_instances_of($changes, 'ArcanistDiffChange');
164 $diff = id(new DifferentialDiff())->makeEphemeral();
165 return self::buildChangesetsFromRawChanges($diff, $changes);
168 private static function buildChangesetsFromRawChanges(
169 DifferentialDiff $diff,
170 array $changes) {
172 // There may not be any changes; initialize the changesets list so that
173 // we don't throw later when accessing it.
174 $diff->attachChangesets(array());
176 $lines = 0;
177 foreach ($changes as $change) {
178 if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
179 // If a user pastes a diff into Differential which includes a commit
180 // message (e.g., they ran `git show` to generate it), discard that
181 // change when constructing a DifferentialDiff.
182 continue;
185 $changeset = new DifferentialChangeset();
186 $add_lines = 0;
187 $del_lines = 0;
188 $first_line = PHP_INT_MAX;
189 $hunks = $change->getHunks();
190 if ($hunks) {
191 foreach ($hunks as $hunk) {
192 $dhunk = new DifferentialHunk();
193 $dhunk->setOldOffset($hunk->getOldOffset());
194 $dhunk->setOldLen($hunk->getOldLength());
195 $dhunk->setNewOffset($hunk->getNewOffset());
196 $dhunk->setNewLen($hunk->getNewLength());
197 $dhunk->setChanges($hunk->getCorpus());
198 $changeset->addUnsavedHunk($dhunk);
199 $add_lines += $hunk->getAddLines();
200 $del_lines += $hunk->getDelLines();
201 $added_lines = $hunk->getChangedLines('new');
202 if ($added_lines) {
203 $first_line = min($first_line, head_key($added_lines));
206 $lines += $add_lines + $del_lines;
207 } else {
208 // This happens when you add empty files.
209 $changeset->attachHunks(array());
212 $metadata = $change->getAllMetadata();
213 if ($first_line != PHP_INT_MAX) {
214 $metadata['line:first'] = $first_line;
217 $changeset->setOldFile($change->getOldPath());
218 $changeset->setFilename($change->getCurrentPath());
219 $changeset->setChangeType($change->getType());
221 $changeset->setFileType($change->getFileType());
222 $changeset->setMetadata($metadata);
223 $changeset->setOldProperties($change->getOldProperties());
224 $changeset->setNewProperties($change->getNewProperties());
225 $changeset->setAwayPaths($change->getAwayPaths());
226 $changeset->setAddLines($add_lines);
227 $changeset->setDelLines($del_lines);
229 $diff->addUnsavedChangeset($changeset);
231 $diff->setLineCount($lines);
233 $changesets = $diff->getChangesets();
235 // TODO: This is "safe", but it would be better to propagate a real user
236 // down the stack.
237 $viewer = PhabricatorUser::getOmnipotentUser();
239 id(new DifferentialChangesetEngine())
240 ->setViewer($viewer)
241 ->rebuildChangesets($changesets);
243 return $diff;
246 public function getDiffDict() {
247 $dict = array(
248 'id' => $this->getID(),
249 'revisionID' => $this->getRevisionID(),
250 'dateCreated' => $this->getDateCreated(),
251 'dateModified' => $this->getDateModified(),
252 'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(),
253 'sourceControlPath' => $this->getSourceControlPath(),
254 'sourceControlSystem' => $this->getSourceControlSystem(),
255 'branch' => $this->getBranch(),
256 'bookmark' => $this->getBookmark(),
257 'creationMethod' => $this->getCreationMethod(),
258 'description' => $this->getDescription(),
259 'unitStatus' => $this->getUnitStatus(),
260 'lintStatus' => $this->getLintStatus(),
261 'changes' => array(),
264 $dict['changes'] = $this->buildChangesList();
266 return $dict + $this->getDiffAuthorshipDict();
269 public function getDiffAuthorshipDict() {
270 $dict = array('properties' => array());
272 $properties = id(new DifferentialDiffProperty())->loadAllWhere(
273 'diffID = %d',
274 $this->getID());
275 foreach ($properties as $property) {
276 $dict['properties'][$property->getName()] = $property->getData();
278 if ($property->getName() == 'local:commits') {
279 foreach ($property->getData() as $commit) {
280 $dict['authorName'] = $commit['author'];
281 $dict['authorEmail'] = idx($commit, 'authorEmail');
282 break;
287 return $dict;
290 public function buildChangesList() {
291 $changes = array();
292 foreach ($this->getChangesets() as $changeset) {
293 $hunks = array();
294 foreach ($changeset->getHunks() as $hunk) {
295 $hunks[] = array(
296 'oldOffset' => $hunk->getOldOffset(),
297 'newOffset' => $hunk->getNewOffset(),
298 'oldLength' => $hunk->getOldLen(),
299 'newLength' => $hunk->getNewLen(),
300 'addLines' => null,
301 'delLines' => null,
302 'isMissingOldNewline' => null,
303 'isMissingNewNewline' => null,
304 'corpus' => $hunk->getChanges(),
307 $change = array(
308 'id' => $changeset->getID(),
309 'metadata' => $changeset->getMetadata(),
310 'oldPath' => $changeset->getOldFile(),
311 'currentPath' => $changeset->getFilename(),
312 'awayPaths' => $changeset->getAwayPaths(),
313 'oldProperties' => $changeset->getOldProperties(),
314 'newProperties' => $changeset->getNewProperties(),
315 'type' => $changeset->getChangeType(),
316 'fileType' => $changeset->getFileType(),
317 'commitHash' => null,
318 'addLines' => $changeset->getAddLines(),
319 'delLines' => $changeset->getDelLines(),
320 'hunks' => $hunks,
322 $changes[] = $change;
324 return $changes;
327 public function hasRevision() {
328 return $this->revision !== self::ATTACHABLE;
331 public function getRevision() {
332 return $this->assertAttached($this->revision);
335 public function attachRevision(DifferentialRevision $revision = null) {
336 $this->revision = $revision;
337 return $this;
340 public function attachProperty($key, $value) {
341 if (!is_array($this->properties)) {
342 $this->properties = array();
344 $this->properties[$key] = $value;
345 return $this;
348 public function getProperty($key) {
349 return $this->assertAttachedKey($this->properties, $key);
352 public function hasDiffProperty($key) {
353 $properties = $this->getDiffProperties();
354 return array_key_exists($key, $properties);
357 public function attachDiffProperties(array $properties) {
358 $this->properties = $properties;
359 return $this;
362 public function getDiffProperties() {
363 return $this->assertAttached($this->properties);
366 public function attachBuildable(HarbormasterBuildable $buildable = null) {
367 $this->buildable = $buildable;
368 return $this;
371 public function getBuildable() {
372 return $this->assertAttached($this->buildable);
375 public function getBuildTargetPHIDs() {
376 $buildable = $this->getBuildable();
378 if (!$buildable) {
379 return array();
382 $target_phids = array();
383 foreach ($buildable->getBuilds() as $build) {
384 foreach ($build->getBuildTargets() as $target) {
385 $target_phids[] = $target->getPHID();
389 return $target_phids;
392 public function loadCoverageMap(PhabricatorUser $viewer) {
393 $target_phids = $this->getBuildTargetPHIDs();
394 if (!$target_phids) {
395 return array();
398 $unit = id(new HarbormasterBuildUnitMessageQuery())
399 ->setViewer($viewer)
400 ->withBuildTargetPHIDs($target_phids)
401 ->execute();
403 $map = array();
404 foreach ($unit as $message) {
405 $coverage = $message->getProperty('coverage', array());
406 foreach ($coverage as $path => $coverage_data) {
407 $map[$path][] = $coverage_data;
411 foreach ($map as $path => $coverage_items) {
412 $map[$path] = ArcanistUnitTestResult::mergeCoverage($coverage_items);
415 return $map;
418 public function getURI() {
419 $id = $this->getID();
420 return "/differential/diff/{$id}/";
424 public function attachUnitMessages(array $unit_messages) {
425 $this->unitMessages = $unit_messages;
426 return $this;
430 public function getUnitMessages() {
431 return $this->assertAttached($this->unitMessages);
435 /* -( PhabricatorPolicyInterface )----------------------------------------- */
438 public function getCapabilities() {
439 return array(
440 PhabricatorPolicyCapability::CAN_VIEW,
444 public function getPolicy($capability) {
445 if ($this->hasRevision()) {
446 return PhabricatorPolicies::getMostOpenPolicy();
449 return $this->viewPolicy;
452 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
453 if ($this->hasRevision()) {
454 return $this->getRevision()->hasAutomaticCapability($capability, $viewer);
457 return ($this->getAuthorPHID() == $viewer->getPHID());
460 public function describeAutomaticCapability($capability) {
461 if ($this->hasRevision()) {
462 return pht(
463 'This diff is attached to a revision, and inherits its policies.');
466 return pht('The author of a diff can see it.');
470 /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
473 public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
474 $extended = array();
476 switch ($capability) {
477 case PhabricatorPolicyCapability::CAN_VIEW:
478 if ($this->hasRevision()) {
479 $extended[] = array(
480 $this->getRevision(),
481 PhabricatorPolicyCapability::CAN_VIEW,
483 } else if ($this->getRepositoryPHID()) {
484 $extended[] = array(
485 $this->getRepositoryPHID(),
486 PhabricatorPolicyCapability::CAN_VIEW,
489 break;
492 return $extended;
496 /* -( HarbormasterBuildableInterface )------------------------------------- */
499 public function getHarbormasterBuildableDisplayPHID() {
500 $container_phid = $this->getHarbormasterContainerPHID();
501 if ($container_phid) {
502 return $container_phid;
505 return $this->getHarbormasterBuildablePHID();
508 public function getHarbormasterBuildablePHID() {
509 return $this->getPHID();
512 public function getHarbormasterContainerPHID() {
513 if ($this->getRevisionID()) {
514 $revision = id(new DifferentialRevision())->load($this->getRevisionID());
515 if ($revision) {
516 return $revision->getPHID();
520 return null;
523 public function getBuildVariables() {
524 $results = array();
526 $results['buildable.diff'] = $this->getID();
527 if ($this->revisionID) {
528 $revision = $this->getRevision();
529 $results['buildable.revision'] = $revision->getID();
530 $repo = $revision->getRepository();
532 if ($repo) {
533 $results['repository.callsign'] = $repo->getCallsign();
534 $results['repository.phid'] = $repo->getPHID();
535 $results['repository.vcs'] = $repo->getVersionControlSystem();
536 $results['repository.uri'] = $repo->getPublicCloneURI();
538 $results['repository.staging.uri'] = $repo->getStagingURI();
539 $results['repository.staging.ref'] = $this->getStagingRef();
543 return $results;
546 public function getAvailableBuildVariables() {
547 return array(
548 'buildable.diff' =>
549 pht('The differential diff ID, if applicable.'),
550 'buildable.revision' =>
551 pht('The differential revision ID, if applicable.'),
552 'repository.callsign' =>
553 pht('The callsign of the repository.'),
554 'repository.phid' =>
555 pht('The PHID of the repository.'),
556 'repository.vcs' =>
557 pht('The version control system, either "svn", "hg" or "git".'),
558 'repository.uri' =>
559 pht('The URI to clone or checkout the repository from.'),
560 'repository.staging.uri' =>
561 pht('The URI of the staging repository.'),
562 'repository.staging.ref' =>
563 pht('The ref name for this change in the staging repository.'),
567 public function newBuildableEngine() {
568 return new DifferentialBuildableEngine();
572 /* -( HarbormasterCircleCIBuildableInterface )----------------------------- */
575 public function getCircleCIGitHubRepositoryURI() {
576 $diff_phid = $this->getPHID();
577 $repository_phid = $this->getRepositoryPHID();
578 if (!$repository_phid) {
579 throw new Exception(
580 pht(
581 'This diff ("%s") is not associated with a repository. A diff '.
582 'must belong to a tracked repository to be built by CircleCI.',
583 $diff_phid));
586 $repository = id(new PhabricatorRepositoryQuery())
587 ->setViewer(PhabricatorUser::getOmnipotentUser())
588 ->withPHIDs(array($repository_phid))
589 ->executeOne();
590 if (!$repository) {
591 throw new Exception(
592 pht(
593 'This diff ("%s") is associated with a repository ("%s") which '.
594 'could not be loaded.',
595 $diff_phid,
596 $repository_phid));
599 $staging_uri = $repository->getStagingURI();
600 if (!$staging_uri) {
601 throw new Exception(
602 pht(
603 'This diff ("%s") is associated with a repository ("%s") that '.
604 'does not have a Staging Area configured. You must configure a '.
605 'Staging Area to use CircleCI integration.',
606 $diff_phid,
607 $repository_phid));
610 $path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath(
611 $staging_uri);
612 if (!$path) {
613 throw new Exception(
614 pht(
615 'This diff ("%s") is associated with a repository ("%s") that '.
616 'does not have a Staging Area ("%s") that is hosted on GitHub. '.
617 'CircleCI can only build from GitHub, so the Staging Area for '.
618 'the repository must be hosted there.',
619 $diff_phid,
620 $repository_phid,
621 $staging_uri));
624 return $staging_uri;
627 public function getCircleCIBuildIdentifierType() {
628 return 'tag';
631 public function getCircleCIBuildIdentifier() {
632 $ref = $this->getStagingRef();
633 $ref = preg_replace('(^refs/tags/)', '', $ref);
634 return $ref;
638 /* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */
640 public function getBuildkiteBranch() {
641 $ref = $this->getStagingRef();
643 // NOTE: Circa late January 2017, Buildkite fails with the error message
644 // "Tags have been disabled for this project" if we pass the "refs/tags/"
645 // prefix via the API and the project doesn't have GitHub tag builds
646 // enabled, even if GitHub builds are disabled. The tag builds fine
647 // without this prefix.
648 $ref = preg_replace('(^refs/tags/)', '', $ref);
650 return $ref;
653 public function getBuildkiteCommit() {
654 return 'HEAD';
658 public function getStagingRef() {
659 // TODO: We're just hoping to get lucky. Instead, `arc` should store
660 // where it sent changes and we should only provide staging details
661 // if we reasonably believe they are accurate.
662 return 'refs/tags/phabricator/diff/'.$this->getID();
665 public function loadTargetBranch() {
666 // TODO: This is sketchy, but just eat the query cost until this can get
667 // cleaned up.
669 // For now, we're only returning a target if there's exactly one and it's
670 // a branch, since we don't support landing to more esoteric targets like
671 // tags yet.
673 $property = id(new DifferentialDiffProperty())->loadOneWhere(
674 'diffID = %d AND name = %s',
675 $this->getID(),
676 'arc:onto');
677 if (!$property) {
678 return null;
681 $data = $property->getData();
683 if (!$data) {
684 return null;
687 if (!is_array($data)) {
688 return null;
691 if (count($data) != 1) {
692 return null;
695 $onto = head($data);
696 if (!is_array($onto)) {
697 return null;
700 $type = idx($onto, 'type');
701 if ($type != 'branch') {
702 return null;
705 return idx($onto, 'name');
709 /* -( PhabricatorApplicationTransactionInterface )------------------------- */
712 public function getApplicationTransactionEditor() {
713 return new DifferentialDiffEditor();
716 public function getApplicationTransactionTemplate() {
717 return new DifferentialDiffTransaction();
721 /* -( PhabricatorDestructibleInterface )----------------------------------- */
724 public function destroyObjectPermanently(
725 PhabricatorDestructionEngine $engine) {
727 $viewer = $engine->getViewer();
729 $this->openTransaction();
730 $this->delete();
732 foreach ($this->loadChangesets() as $changeset) {
733 $engine->destroyObject($changeset);
736 $properties = id(new DifferentialDiffProperty())->loadAllWhere(
737 'diffID = %d',
738 $this->getID());
739 foreach ($properties as $prop) {
740 $prop->delete();
743 $viewstate_query = id(new DifferentialViewStateQuery())
744 ->setViewer($viewer)
745 ->withObjectPHIDs(array($this->getPHID()));
746 $viewstates = new PhabricatorQueryIterator($viewstate_query);
747 foreach ($viewstates as $viewstate) {
748 $viewstate->delete();
751 $this->saveTransaction();
755 /* -( PhabricatorConduitResultInterface )---------------------------------- */
758 public function getFieldSpecificationsForConduit() {
759 return array(
760 id(new PhabricatorConduitSearchFieldSpecification())
761 ->setKey('revisionPHID')
762 ->setType('phid')
763 ->setDescription(pht('Associated revision PHID.')),
764 id(new PhabricatorConduitSearchFieldSpecification())
765 ->setKey('authorPHID')
766 ->setType('phid')
767 ->setDescription(pht('Revision author PHID.')),
768 id(new PhabricatorConduitSearchFieldSpecification())
769 ->setKey('repositoryPHID')
770 ->setType('phid')
771 ->setDescription(pht('Associated repository PHID.')),
772 id(new PhabricatorConduitSearchFieldSpecification())
773 ->setKey('refs')
774 ->setType('map<string, wild>')
775 ->setDescription(pht('List of related VCS references.')),
779 public function getFieldValuesForConduit() {
780 $refs = array();
782 $branch = $this->getBranch();
783 if (strlen($branch)) {
784 $refs[] = array(
785 'type' => 'branch',
786 'name' => $branch,
790 $onto = $this->loadTargetBranch();
791 if (strlen($onto)) {
792 $refs[] = array(
793 'type' => 'onto',
794 'name' => $onto,
798 $base = $this->getSourceControlBaseRevision();
799 if (strlen($base)) {
800 $refs[] = array(
801 'type' => 'base',
802 'identifier' => $base,
806 $bookmark = $this->getBookmark();
807 if (strlen($bookmark)) {
808 $refs[] = array(
809 'type' => 'bookmark',
810 'name' => $bookmark,
814 $revision_phid = null;
815 if ($this->getRevisionID()) {
816 $revision_phid = $this->getRevision()->getPHID();
819 return array(
820 'revisionPHID' => $revision_phid,
821 'authorPHID' => $this->getAuthorPHID(),
822 'repositoryPHID' => $this->getRepositoryPHID(),
823 'refs' => $refs,
827 public function getConduitSearchAttachments() {
828 return array(
829 id(new DifferentialCommitsSearchEngineAttachment())
830 ->setAttachmentKey('commits'),