3 final class DifferentialDiff
4 extends DifferentialDAO
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;
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() {
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?',
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;
101 public function attachChangesets(array $changesets) {
102 assert_instances_of($changesets, 'DifferentialChangeset');
103 $this->changesets
= $changesets;
107 public function getChangesets() {
108 return $this->assertAttached($this->changesets
);
111 public function loadChangesets() {
112 if (!$this->getID()) {
115 $changesets = id(new DifferentialChangeset())->loadAllWhere(
119 foreach ($changesets as $changeset) {
120 $changeset->attachDiff($this);
126 public function save() {
127 $this->openTransaction();
128 $ret = parent
::save();
129 foreach ($this->unsavedChangesets
as $changeset) {
130 $changeset->setDiffID($this->getID());
133 $this->saveTransaction();
137 public static function initializeNewDiff(PhabricatorUser
$actor) {
138 $app = id(new PhabricatorApplicationQuery())
140 ->withClasses(array('PhabricatorDifferentialApplication'))
142 $view_policy = $app->getPolicy(
143 DifferentialDefaultViewCapability
::CAPABILITY
);
145 $diff = id(new DifferentialDiff())
146 ->setViewPolicy($view_policy);
151 public static function newFromRawChanges(
152 PhabricatorUser
$actor,
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,
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());
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.
185 $changeset = new DifferentialChangeset();
188 $first_line = PHP_INT_MAX
;
189 $hunks = $change->getHunks();
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');
203 $first_line = min($first_line, head_key($added_lines));
206 $lines +
= $add_lines +
$del_lines;
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
237 $viewer = PhabricatorUser
::getOmnipotentUser();
239 id(new DifferentialChangesetEngine())
241 ->rebuildChangesets($changesets);
246 public function getDiffDict() {
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(
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');
290 public function buildChangesList() {
292 foreach ($this->getChangesets() as $changeset) {
294 foreach ($changeset->getHunks() as $hunk) {
296 'oldOffset' => $hunk->getOldOffset(),
297 'newOffset' => $hunk->getNewOffset(),
298 'oldLength' => $hunk->getOldLen(),
299 'newLength' => $hunk->getNewLen(),
302 'isMissingOldNewline' => null,
303 'isMissingNewNewline' => null,
304 'corpus' => $hunk->getChanges(),
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(),
322 $changes[] = $change;
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;
340 public function attachProperty($key, $value) {
341 if (!is_array($this->properties
)) {
342 $this->properties
= array();
344 $this->properties
[$key] = $value;
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;
362 public function getDiffProperties() {
363 return $this->assertAttached($this->properties
);
366 public function attachBuildable(HarbormasterBuildable
$buildable = null) {
367 $this->buildable
= $buildable;
371 public function getBuildable() {
372 return $this->assertAttached($this->buildable
);
375 public function getBuildTargetPHIDs() {
376 $buildable = $this->getBuildable();
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) {
398 $unit = id(new HarbormasterBuildUnitMessageQuery())
400 ->withBuildTargetPHIDs($target_phids)
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);
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;
430 public function getUnitMessages() {
431 return $this->assertAttached($this->unitMessages
);
435 /* -( PhabricatorPolicyInterface )----------------------------------------- */
438 public function getCapabilities() {
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()) {
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) {
476 switch ($capability) {
477 case PhabricatorPolicyCapability
::CAN_VIEW
:
478 if ($this->hasRevision()) {
480 $this->getRevision(),
481 PhabricatorPolicyCapability
::CAN_VIEW
,
483 } else if ($this->getRepositoryPHID()) {
485 $this->getRepositoryPHID(),
486 PhabricatorPolicyCapability
::CAN_VIEW
,
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());
516 return $revision->getPHID();
523 public function getBuildVariables() {
526 $results['buildable.diff'] = $this->getID();
527 if ($this->revisionID
) {
528 $revision = $this->getRevision();
529 $results['buildable.revision'] = $revision->getID();
530 $repo = $revision->getRepository();
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();
546 public function getAvailableBuildVariables() {
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.'),
555 pht('The PHID of the repository.'),
557 pht('The version control system, either "svn", "hg" or "git".'),
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) {
581 'This diff ("%s") is not associated with a repository. A diff '.
582 'must belong to a tracked repository to be built by CircleCI.',
586 $repository = id(new PhabricatorRepositoryQuery())
587 ->setViewer(PhabricatorUser
::getOmnipotentUser())
588 ->withPHIDs(array($repository_phid))
593 'This diff ("%s") is associated with a repository ("%s") which '.
594 'could not be loaded.',
599 $staging_uri = $repository->getStagingURI();
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.',
610 $path = HarbormasterCircleCIBuildStepImplementation
::getGitHubPath(
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.',
627 public function getCircleCIBuildIdentifierType() {
631 public function getCircleCIBuildIdentifier() {
632 $ref = $this->getStagingRef();
633 $ref = preg_replace('(^refs/tags/)', '', $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);
653 public function getBuildkiteCommit() {
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
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
673 $property = id(new DifferentialDiffProperty())->loadOneWhere(
674 'diffID = %d AND name = %s',
681 $data = $property->getData();
687 if (!is_array($data)) {
691 if (count($data) != 1) {
696 if (!is_array($onto)) {
700 $type = idx($onto, 'type');
701 if ($type != 'branch') {
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();
732 foreach ($this->loadChangesets() as $changeset) {
733 $engine->destroyObject($changeset);
736 $properties = id(new DifferentialDiffProperty())->loadAllWhere(
739 foreach ($properties as $prop) {
743 $viewstate_query = id(new DifferentialViewStateQuery())
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() {
760 id(new PhabricatorConduitSearchFieldSpecification())
761 ->setKey('revisionPHID')
763 ->setDescription(pht('Associated revision PHID.')),
764 id(new PhabricatorConduitSearchFieldSpecification())
765 ->setKey('authorPHID')
767 ->setDescription(pht('Revision author PHID.')),
768 id(new PhabricatorConduitSearchFieldSpecification())
769 ->setKey('repositoryPHID')
771 ->setDescription(pht('Associated repository PHID.')),
772 id(new PhabricatorConduitSearchFieldSpecification())
774 ->setType('map<string, wild>')
775 ->setDescription(pht('List of related VCS references.')),
779 public function getFieldValuesForConduit() {
782 $branch = $this->getBranch();
783 if (strlen($branch)) {
790 $onto = $this->loadTargetBranch();
798 $base = $this->getSourceControlBaseRevision();
802 'identifier' => $base,
806 $bookmark = $this->getBookmark();
807 if (strlen($bookmark)) {
809 'type' => 'bookmark',
814 $revision_phid = null;
815 if ($this->getRevisionID()) {
816 $revision_phid = $this->getRevision()->getPHID();
820 'revisionPHID' => $revision_phid,
821 'authorPHID' => $this->getAuthorPHID(),
822 'repositoryPHID' => $this->getRepositoryPHID(),
827 public function getConduitSearchAttachments() {
829 id(new DifferentialCommitsSearchEngineAttachment())
830 ->setAttachmentKey('commits'),