Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/differential/storage/DifferentialRevision.php
12256 views
1
<?php
2
3
final class DifferentialRevision extends DifferentialDAO
4
implements
5
PhabricatorTokenReceiverInterface,
6
PhabricatorPolicyInterface,
7
PhabricatorExtendedPolicyInterface,
8
PhabricatorFlaggableInterface,
9
PhrequentTrackableInterface,
10
HarbormasterBuildableInterface,
11
PhabricatorSubscribableInterface,
12
PhabricatorCustomFieldInterface,
13
PhabricatorApplicationTransactionInterface,
14
PhabricatorTimelineInterface,
15
PhabricatorMentionableInterface,
16
PhabricatorDestructibleInterface,
17
PhabricatorProjectInterface,
18
PhabricatorFulltextInterface,
19
PhabricatorFerretInterface,
20
PhabricatorConduitResultInterface,
21
PhabricatorDraftInterface {
22
23
protected $title = '';
24
protected $status;
25
26
protected $summary = '';
27
protected $testPlan = '';
28
29
protected $authorPHID;
30
protected $lastReviewerPHID;
31
32
protected $lineCount = 0;
33
protected $attached = array();
34
35
protected $mailKey;
36
protected $branchName;
37
protected $repositoryPHID;
38
protected $activeDiffPHID;
39
40
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
41
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
42
protected $properties = array();
43
44
private $commitPHIDs = self::ATTACHABLE;
45
private $activeDiff = self::ATTACHABLE;
46
private $diffIDs = self::ATTACHABLE;
47
private $hashes = self::ATTACHABLE;
48
private $repository = self::ATTACHABLE;
49
50
private $reviewerStatus = self::ATTACHABLE;
51
private $customFields = self::ATTACHABLE;
52
private $drafts = array();
53
private $flags = array();
54
private $forceMap = array();
55
56
const RELATION_REVIEWER = 'revw';
57
const RELATION_SUBSCRIBED = 'subd';
58
59
const PROPERTY_CLOSED_FROM_ACCEPTED = 'wasAcceptedBeforeClose';
60
const PROPERTY_DRAFT_HOLD = 'draft.hold';
61
const PROPERTY_SHOULD_BROADCAST = 'draft.broadcast';
62
const PROPERTY_LINES_ADDED = 'lines.added';
63
const PROPERTY_LINES_REMOVED = 'lines.removed';
64
const PROPERTY_BUILDABLES = 'buildables';
65
const PROPERTY_WRONG_BUILDS = 'wrong.builds';
66
67
public static function initializeNewRevision(PhabricatorUser $actor) {
68
$app = id(new PhabricatorApplicationQuery())
69
->setViewer($actor)
70
->withClasses(array('PhabricatorDifferentialApplication'))
71
->executeOne();
72
73
$view_policy = $app->getPolicy(
74
DifferentialDefaultViewCapability::CAPABILITY);
75
76
$initial_state = DifferentialRevisionStatus::DRAFT;
77
$should_broadcast = false;
78
79
return id(new DifferentialRevision())
80
->setViewPolicy($view_policy)
81
->setAuthorPHID($actor->getPHID())
82
->attachRepository(null)
83
->attachActiveDiff(null)
84
->attachReviewers(array())
85
->setModernRevisionStatus($initial_state)
86
->setShouldBroadcast($should_broadcast);
87
}
88
89
protected function getConfiguration() {
90
return array(
91
self::CONFIG_AUX_PHID => true,
92
self::CONFIG_SERIALIZATION => array(
93
'attached' => self::SERIALIZATION_JSON,
94
'unsubscribed' => self::SERIALIZATION_JSON,
95
'properties' => self::SERIALIZATION_JSON,
96
),
97
self::CONFIG_COLUMN_SCHEMA => array(
98
'title' => 'text255',
99
'status' => 'text32',
100
'summary' => 'text',
101
'testPlan' => 'text',
102
'authorPHID' => 'phid?',
103
'lastReviewerPHID' => 'phid?',
104
'lineCount' => 'uint32?',
105
'mailKey' => 'bytes40',
106
'branchName' => 'text255?',
107
'repositoryPHID' => 'phid?',
108
),
109
self::CONFIG_KEY_SCHEMA => array(
110
'authorPHID' => array(
111
'columns' => array('authorPHID', 'status'),
112
),
113
'repositoryPHID' => array(
114
'columns' => array('repositoryPHID'),
115
),
116
// If you (or a project you are a member of) is reviewing a significant
117
// fraction of the revisions on an install, the result set of open
118
// revisions may be smaller than the result set of revisions where you
119
// are a reviewer. In these cases, this key is better than keys on the
120
// edge table.
121
'key_status' => array(
122
'columns' => array('status', 'phid'),
123
),
124
'key_modified' => array(
125
'columns' => array('dateModified'),
126
),
127
),
128
) + parent::getConfiguration();
129
}
130
131
public function setProperty($key, $value) {
132
$this->properties[$key] = $value;
133
return $this;
134
}
135
136
public function getProperty($key, $default = null) {
137
return idx($this->properties, $key, $default);
138
}
139
140
public function hasRevisionProperty($key) {
141
return array_key_exists($key, $this->properties);
142
}
143
144
public function getMonogram() {
145
$id = $this->getID();
146
return "D{$id}";
147
}
148
149
public function getURI() {
150
return '/'.$this->getMonogram();
151
}
152
153
public function getCommitPHIDs() {
154
return $this->assertAttached($this->commitPHIDs);
155
}
156
157
public function getActiveDiff() {
158
// TODO: Because it's currently technically possible to create a revision
159
// without an associated diff, we allow an attached-but-null active diff.
160
// It would be good to get rid of this once we make diff-attaching
161
// transactional.
162
163
return $this->assertAttached($this->activeDiff);
164
}
165
166
public function attachActiveDiff($diff) {
167
$this->activeDiff = $diff;
168
return $this;
169
}
170
171
public function getDiffIDs() {
172
return $this->assertAttached($this->diffIDs);
173
}
174
175
public function attachDiffIDs(array $ids) {
176
rsort($ids);
177
$this->diffIDs = array_values($ids);
178
return $this;
179
}
180
181
public function attachCommitPHIDs(array $phids) {
182
$this->commitPHIDs = $phids;
183
return $this;
184
}
185
186
public function getAttachedPHIDs($type) {
187
return array_keys(idx($this->attached, $type, array()));
188
}
189
190
public function setAttachedPHIDs($type, array $phids) {
191
$this->attached[$type] = array_fill_keys($phids, array());
192
return $this;
193
}
194
195
public function generatePHID() {
196
return PhabricatorPHID::generateNewPHID(
197
DifferentialRevisionPHIDType::TYPECONST);
198
}
199
200
public function loadActiveDiff() {
201
return id(new DifferentialDiff())->loadOneWhere(
202
'revisionID = %d ORDER BY id DESC LIMIT 1',
203
$this->getID());
204
}
205
206
public function save() {
207
if (!$this->getMailKey()) {
208
$this->mailKey = Filesystem::readRandomCharacters(40);
209
}
210
return parent::save();
211
}
212
213
public function getHashes() {
214
return $this->assertAttached($this->hashes);
215
}
216
217
public function attachHashes(array $hashes) {
218
$this->hashes = $hashes;
219
return $this;
220
}
221
222
public function canReviewerForceAccept(
223
PhabricatorUser $viewer,
224
DifferentialReviewer $reviewer) {
225
226
if (!$reviewer->isPackage()) {
227
return false;
228
}
229
230
$map = $this->getReviewerForceAcceptMap($viewer);
231
if (!$map) {
232
return false;
233
}
234
235
if (isset($map[$reviewer->getReviewerPHID()])) {
236
return true;
237
}
238
239
return false;
240
}
241
242
private function getReviewerForceAcceptMap(PhabricatorUser $viewer) {
243
$fragment = $viewer->getCacheFragment();
244
245
if (!array_key_exists($fragment, $this->forceMap)) {
246
$map = $this->newReviewerForceAcceptMap($viewer);
247
$this->forceMap[$fragment] = $map;
248
}
249
250
return $this->forceMap[$fragment];
251
}
252
253
private function newReviewerForceAcceptMap(PhabricatorUser $viewer) {
254
$diff = $this->getActiveDiff();
255
if (!$diff) {
256
return null;
257
}
258
259
$repository_phid = $diff->getRepositoryPHID();
260
if (!$repository_phid) {
261
return null;
262
}
263
264
$paths = array();
265
266
try {
267
$changesets = $diff->getChangesets();
268
} catch (Exception $ex) {
269
$changesets = id(new DifferentialChangesetQuery())
270
->setViewer($viewer)
271
->withDiffs(array($diff))
272
->execute();
273
}
274
275
foreach ($changesets as $changeset) {
276
$paths[] = $changeset->getOwnersFilename();
277
}
278
279
if (!$paths) {
280
return null;
281
}
282
283
$reviewer_phids = array();
284
foreach ($this->getReviewers() as $reviewer) {
285
if (!$reviewer->isPackage()) {
286
continue;
287
}
288
289
$reviewer_phids[] = $reviewer->getReviewerPHID();
290
}
291
292
if (!$reviewer_phids) {
293
return null;
294
}
295
296
// Load all the reviewing packages which have control over some of the
297
// paths in the change. These are packages which the actor may be able
298
// to force-accept on behalf of.
299
$control_query = id(new PhabricatorOwnersPackageQuery())
300
->setViewer($viewer)
301
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
302
->withPHIDs($reviewer_phids)
303
->withControl($repository_phid, $paths);
304
$control_packages = $control_query->execute();
305
if (!$control_packages) {
306
return null;
307
}
308
309
// Load all the packages which have potential control over some of the
310
// paths in the change and are owned by the actor. These are packages
311
// which the actor may be able to use their authority over to gain the
312
// ability to force-accept for other packages. This query doesn't apply
313
// dominion rules yet, and we'll bypass those rules later on.
314
315
// See T13657. We ignore "watcher" packages which don't grant their owners
316
// permission to force accept anything.
317
318
$authority_query = id(new PhabricatorOwnersPackageQuery())
319
->setViewer($viewer)
320
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
321
->withAuthorityModes(
322
array(
323
PhabricatorOwnersPackage::AUTHORITY_STRONG,
324
))
325
->withAuthorityPHIDs(array($viewer->getPHID()))
326
->withControl($repository_phid, $paths);
327
$authority_packages = $authority_query->execute();
328
if (!$authority_packages) {
329
return null;
330
}
331
$authority_packages = mpull($authority_packages, null, 'getPHID');
332
333
// Build a map from each path in the revision to the reviewer packages
334
// which control it.
335
$control_map = array();
336
foreach ($paths as $path) {
337
$control_packages = $control_query->getControllingPackagesForPath(
338
$repository_phid,
339
$path);
340
341
// Remove packages which the viewer has authority over. We don't need
342
// to check these for force-accept because they can just accept them
343
// normally.
344
$control_packages = mpull($control_packages, null, 'getPHID');
345
foreach ($control_packages as $phid => $control_package) {
346
if (isset($authority_packages[$phid])) {
347
unset($control_packages[$phid]);
348
}
349
}
350
351
if (!$control_packages) {
352
continue;
353
}
354
355
$control_map[$path] = $control_packages;
356
}
357
358
if (!$control_map) {
359
return null;
360
}
361
362
// From here on out, we only care about paths which we have at least one
363
// controlling package for.
364
$paths = array_keys($control_map);
365
366
// Now, build a map from each path to the packages which would control it
367
// if there were no dominion rules.
368
$authority_map = array();
369
foreach ($paths as $path) {
370
$authority_packages = $authority_query->getControllingPackagesForPath(
371
$repository_phid,
372
$path,
373
$ignore_dominion = true);
374
375
$authority_map[$path] = mpull($authority_packages, null, 'getPHID');
376
}
377
378
// For each path, find the most general package that the viewer has
379
// authority over. For example, we'll prefer a package that owns "/" to a
380
// package that owns "/src/".
381
$force_map = array();
382
foreach ($authority_map as $path => $package_map) {
383
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
384
$fragment_count = count($path_fragments);
385
386
// Find the package that we have authority over which has the most
387
// general match for this path.
388
$best_match = null;
389
$best_package = null;
390
foreach ($package_map as $package_phid => $package) {
391
$package_paths = $package->getPathsForRepository($repository_phid);
392
foreach ($package_paths as $package_path) {
393
394
// NOTE: A strength of 0 means "no match". A strength of 1 means
395
// that we matched "/", so we can not possibly find another stronger
396
// match.
397
398
$strength = $package_path->getPathMatchStrength(
399
$path_fragments,
400
$fragment_count);
401
if (!$strength) {
402
continue;
403
}
404
405
if ($strength < $best_match || !$best_package) {
406
$best_match = $strength;
407
$best_package = $package;
408
if ($strength == 1) {
409
break 2;
410
}
411
}
412
}
413
}
414
415
if ($best_package) {
416
$force_map[$path] = array(
417
'strength' => $best_match,
418
'package' => $best_package,
419
);
420
}
421
}
422
423
// For each path which the viewer owns a package for, find other packages
424
// which that authority can be used to force-accept. Once we find a way to
425
// force-accept a package, we don't need to keep looking.
426
$has_control = array();
427
foreach ($force_map as $path => $spec) {
428
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
429
$fragment_count = count($path_fragments);
430
431
$authority_strength = $spec['strength'];
432
433
$control_packages = $control_map[$path];
434
foreach ($control_packages as $control_phid => $control_package) {
435
if (isset($has_control[$control_phid])) {
436
continue;
437
}
438
439
$control_paths = $control_package->getPathsForRepository(
440
$repository_phid);
441
foreach ($control_paths as $control_path) {
442
$strength = $control_path->getPathMatchStrength(
443
$path_fragments,
444
$fragment_count);
445
446
if (!$strength) {
447
continue;
448
}
449
450
if ($strength > $authority_strength) {
451
$authority = $spec['package'];
452
$has_control[$control_phid] = array(
453
'authority' => $authority,
454
'phid' => $authority->getPHID(),
455
);
456
break;
457
}
458
}
459
}
460
}
461
462
// Return a map from packages which may be force accepted to the packages
463
// which permit that forced acceptance.
464
return ipull($has_control, 'phid');
465
}
466
467
468
/* -( PhabricatorPolicyInterface )----------------------------------------- */
469
470
471
public function getCapabilities() {
472
return array(
473
PhabricatorPolicyCapability::CAN_VIEW,
474
PhabricatorPolicyCapability::CAN_EDIT,
475
);
476
}
477
478
public function getPolicy($capability) {
479
switch ($capability) {
480
case PhabricatorPolicyCapability::CAN_VIEW:
481
return $this->getViewPolicy();
482
case PhabricatorPolicyCapability::CAN_EDIT:
483
return $this->getEditPolicy();
484
}
485
}
486
487
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
488
// A revision's author (which effectively means "owner" after we added
489
// commandeering) can always view and edit it.
490
$author_phid = $this->getAuthorPHID();
491
if ($author_phid) {
492
if ($user->getPHID() == $author_phid) {
493
return true;
494
}
495
}
496
497
return false;
498
}
499
500
public function describeAutomaticCapability($capability) {
501
$description = array(
502
pht('The owner of a revision can always view and edit it.'),
503
);
504
505
switch ($capability) {
506
case PhabricatorPolicyCapability::CAN_VIEW:
507
$description[] = pht(
508
'If a revision belongs to a repository, other users must be able '.
509
'to view the repository in order to view the revision.');
510
break;
511
}
512
513
return $description;
514
}
515
516
517
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
518
519
520
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
521
$extended = array();
522
523
switch ($capability) {
524
case PhabricatorPolicyCapability::CAN_VIEW:
525
$repository_phid = $this->getRepositoryPHID();
526
$repository = $this->getRepository();
527
528
// Try to use the object if we have it, since it will save us some
529
// data fetching later on. In some cases, we might not have it.
530
$repository_ref = nonempty($repository, $repository_phid);
531
if ($repository_ref) {
532
$extended[] = array(
533
$repository_ref,
534
PhabricatorPolicyCapability::CAN_VIEW,
535
);
536
}
537
break;
538
}
539
540
return $extended;
541
}
542
543
544
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
545
546
547
public function getUsersToNotifyOfTokenGiven() {
548
return array(
549
$this->getAuthorPHID(),
550
);
551
}
552
553
public function getReviewers() {
554
return $this->assertAttached($this->reviewerStatus);
555
}
556
557
public function attachReviewers(array $reviewers) {
558
assert_instances_of($reviewers, 'DifferentialReviewer');
559
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
560
$this->reviewerStatus = $reviewers;
561
return $this;
562
}
563
564
public function hasAttachedReviewers() {
565
return ($this->reviewerStatus !== self::ATTACHABLE);
566
}
567
568
public function getReviewerPHIDs() {
569
$reviewers = $this->getReviewers();
570
return mpull($reviewers, 'getReviewerPHID');
571
}
572
573
public function getReviewerPHIDsForEdit() {
574
$reviewers = $this->getReviewers();
575
576
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
577
578
$value = array();
579
foreach ($reviewers as $reviewer) {
580
$phid = $reviewer->getReviewerPHID();
581
if ($reviewer->getReviewerStatus() == $status_blocking) {
582
$value[] = 'blocking('.$phid.')';
583
} else {
584
$value[] = $phid;
585
}
586
}
587
588
return $value;
589
}
590
591
public function getRepository() {
592
return $this->assertAttached($this->repository);
593
}
594
595
public function attachRepository(PhabricatorRepository $repository = null) {
596
$this->repository = $repository;
597
return $this;
598
}
599
600
public function setModernRevisionStatus($status) {
601
return $this->setStatus($status);
602
}
603
604
public function getModernRevisionStatus() {
605
return $this->getStatus();
606
}
607
608
public function getLegacyRevisionStatus() {
609
return $this->getStatusObject()->getLegacyKey();
610
}
611
612
public function isClosed() {
613
return $this->getStatusObject()->isClosedStatus();
614
}
615
616
public function isAbandoned() {
617
return $this->getStatusObject()->isAbandoned();
618
}
619
620
public function isAccepted() {
621
return $this->getStatusObject()->isAccepted();
622
}
623
624
public function isNeedsReview() {
625
return $this->getStatusObject()->isNeedsReview();
626
}
627
628
public function isNeedsRevision() {
629
return $this->getStatusObject()->isNeedsRevision();
630
}
631
632
public function isChangePlanned() {
633
return $this->getStatusObject()->isChangePlanned();
634
}
635
636
public function isPublished() {
637
return $this->getStatusObject()->isPublished();
638
}
639
640
public function isDraft() {
641
return $this->getStatusObject()->isDraft();
642
}
643
644
public function getStatusIcon() {
645
return $this->getStatusObject()->getIcon();
646
}
647
648
public function getStatusDisplayName() {
649
return $this->getStatusObject()->getDisplayName();
650
}
651
652
public function getStatusIconColor() {
653
return $this->getStatusObject()->getIconColor();
654
}
655
656
public function getStatusTagColor() {
657
return $this->getStatusObject()->getTagColor();
658
}
659
660
public function getStatusObject() {
661
$status = $this->getStatus();
662
return DifferentialRevisionStatus::newForStatus($status);
663
}
664
665
public function getFlag(PhabricatorUser $viewer) {
666
return $this->assertAttachedKey($this->flags, $viewer->getPHID());
667
}
668
669
public function attachFlag(
670
PhabricatorUser $viewer,
671
PhabricatorFlag $flag = null) {
672
$this->flags[$viewer->getPHID()] = $flag;
673
return $this;
674
}
675
676
public function getHasDraft(PhabricatorUser $viewer) {
677
return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment());
678
}
679
680
public function attachHasDraft(PhabricatorUser $viewer, $has_draft) {
681
$this->drafts[$viewer->getCacheFragment()] = $has_draft;
682
return $this;
683
}
684
685
public function getHoldAsDraft() {
686
return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false);
687
}
688
689
public function setHoldAsDraft($hold) {
690
return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold);
691
}
692
693
public function getShouldBroadcast() {
694
return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true);
695
}
696
697
public function setShouldBroadcast($should_broadcast) {
698
return $this->setProperty(
699
self::PROPERTY_SHOULD_BROADCAST,
700
$should_broadcast);
701
}
702
703
public function setAddedLineCount($count) {
704
return $this->setProperty(self::PROPERTY_LINES_ADDED, $count);
705
}
706
707
public function getAddedLineCount() {
708
return $this->getProperty(self::PROPERTY_LINES_ADDED);
709
}
710
711
public function setRemovedLineCount($count) {
712
return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count);
713
}
714
715
public function getRemovedLineCount() {
716
return $this->getProperty(self::PROPERTY_LINES_REMOVED);
717
}
718
719
public function hasLineCounts() {
720
// This data was not populated on older revisions, so it may not be
721
// present on all revisions.
722
return isset($this->properties[self::PROPERTY_LINES_ADDED]);
723
}
724
725
public function getRevisionScaleGlyphs() {
726
$add = $this->getAddedLineCount();
727
$rem = $this->getRemovedLineCount();
728
$all = ($add + $rem);
729
730
if (!$all) {
731
return ' ';
732
}
733
734
$map = array(
735
20 => 2,
736
50 => 3,
737
150 => 4,
738
375 => 5,
739
1000 => 6,
740
2500 => 7,
741
);
742
743
$n = 1;
744
foreach ($map as $size => $count) {
745
if ($size <= $all) {
746
$n = $count;
747
} else {
748
break;
749
}
750
}
751
752
$add_n = (int)ceil(($add / $all) * $n);
753
$rem_n = (int)ceil(($rem / $all) * $n);
754
755
while ($add_n + $rem_n > $n) {
756
if ($add_n > 1) {
757
$add_n--;
758
} else {
759
$rem_n--;
760
}
761
}
762
763
return
764
str_repeat('+', $add_n).
765
str_repeat('-', $rem_n).
766
str_repeat(' ', (7 - $n));
767
}
768
769
public function getBuildableStatus($phid) {
770
$buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
771
if (!is_array($buildables)) {
772
$buildables = array();
773
}
774
775
$buildable = idx($buildables, $phid);
776
if (!is_array($buildable)) {
777
$buildable = array();
778
}
779
780
return idx($buildable, 'status');
781
}
782
783
public function setBuildableStatus($phid, $status) {
784
$buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
785
if (!is_array($buildables)) {
786
$buildables = array();
787
}
788
789
$buildable = idx($buildables, $phid);
790
if (!is_array($buildable)) {
791
$buildable = array();
792
}
793
794
$buildable['status'] = $status;
795
796
$buildables[$phid] = $buildable;
797
798
return $this->setProperty(self::PROPERTY_BUILDABLES, $buildables);
799
}
800
801
public function newBuildableStatus(PhabricatorUser $viewer, $phid) {
802
// For Differential, we're ignoring autobuilds (local lint and unit)
803
// when computing build status. Differential only cares about remote
804
// builds when making publishing and undrafting decisions.
805
806
$builds = $this->loadImpactfulBuildsForBuildablePHIDs(
807
$viewer,
808
array($phid));
809
810
return $this->newBuildableStatusForBuilds($builds);
811
}
812
813
public function newBuildableStatusForBuilds(array $builds) {
814
// If we have nothing but passing builds, the buildable passes.
815
if (!$builds) {
816
return HarbormasterBuildableStatus::STATUS_PASSED;
817
}
818
819
// If we have any completed, non-passing builds, the buildable fails.
820
foreach ($builds as $build) {
821
if ($build->isComplete()) {
822
return HarbormasterBuildableStatus::STATUS_FAILED;
823
}
824
}
825
826
// Otherwise, we're still waiting for the build to pass or fail.
827
return null;
828
}
829
830
public function loadImpactfulBuilds(PhabricatorUser $viewer) {
831
$diff = $this->getActiveDiff();
832
833
// NOTE: We can't use `withContainerPHIDs()` here because the container
834
// update in Harbormaster is not synchronous.
835
$buildables = id(new HarbormasterBuildableQuery())
836
->setViewer($viewer)
837
->withBuildablePHIDs(array($diff->getPHID()))
838
->withManualBuildables(false)
839
->execute();
840
if (!$buildables) {
841
return array();
842
}
843
844
return $this->loadImpactfulBuildsForBuildablePHIDs(
845
$viewer,
846
mpull($buildables, 'getPHID'));
847
}
848
849
private function loadImpactfulBuildsForBuildablePHIDs(
850
PhabricatorUser $viewer,
851
array $phids) {
852
853
$builds = id(new HarbormasterBuildQuery())
854
->setViewer($viewer)
855
->withBuildablePHIDs($phids)
856
->withAutobuilds(false)
857
->withBuildStatuses(
858
array(
859
HarbormasterBuildStatus::STATUS_INACTIVE,
860
HarbormasterBuildStatus::STATUS_PENDING,
861
HarbormasterBuildStatus::STATUS_BUILDING,
862
HarbormasterBuildStatus::STATUS_FAILED,
863
HarbormasterBuildStatus::STATUS_ABORTED,
864
HarbormasterBuildStatus::STATUS_ERROR,
865
HarbormasterBuildStatus::STATUS_PAUSED,
866
HarbormasterBuildStatus::STATUS_DEADLOCKED,
867
))
868
->execute();
869
870
// Filter builds based on the "Hold Drafts" behavior of their associated
871
// build plans.
872
873
$hold_drafts = HarbormasterBuildPlanBehavior::BEHAVIOR_DRAFTS;
874
$behavior = HarbormasterBuildPlanBehavior::getBehavior($hold_drafts);
875
876
$key_never = HarbormasterBuildPlanBehavior::DRAFTS_NEVER;
877
$key_building = HarbormasterBuildPlanBehavior::DRAFTS_IF_BUILDING;
878
879
foreach ($builds as $key => $build) {
880
$plan = $build->getBuildPlan();
881
882
// See T13526. If the viewer can't see the build plan, pretend it has
883
// generic options. This is often wrong, but "often wrong" is better than
884
// "fatal".
885
if ($plan) {
886
$hold_key = $behavior->getPlanOption($plan)->getKey();
887
888
$hold_never = ($hold_key === $key_never);
889
$hold_building = ($hold_key === $key_building);
890
} else {
891
$hold_never = false;
892
$hold_building = false;
893
}
894
895
// If the build "Never" holds drafts from promoting, we don't care what
896
// the status is.
897
if ($hold_never) {
898
unset($builds[$key]);
899
continue;
900
}
901
902
// If the build holds drafts from promoting "While Building", we only
903
// care about the status until it completes.
904
if ($hold_building) {
905
if ($build->isComplete()) {
906
unset($builds[$key]);
907
continue;
908
}
909
}
910
}
911
912
return $builds;
913
}
914
915
916
/* -( HarbormasterBuildableInterface )------------------------------------- */
917
918
919
public function getHarbormasterBuildableDisplayPHID() {
920
return $this->getHarbormasterContainerPHID();
921
}
922
923
public function getHarbormasterBuildablePHID() {
924
return $this->loadActiveDiff()->getPHID();
925
}
926
927
public function getHarbormasterContainerPHID() {
928
return $this->getPHID();
929
}
930
931
public function getBuildVariables() {
932
return array();
933
}
934
935
public function getAvailableBuildVariables() {
936
return array();
937
}
938
939
public function newBuildableEngine() {
940
return new DifferentialBuildableEngine();
941
}
942
943
944
/* -( PhabricatorSubscribableInterface )----------------------------------- */
945
946
947
public function isAutomaticallySubscribed($phid) {
948
if ($phid == $this->getAuthorPHID()) {
949
return true;
950
}
951
952
// TODO: This only happens when adding or removing CCs, and is safe from a
953
// policy perspective, but the subscription pathway should have some
954
// opportunity to load this data properly. For now, this is the only case
955
// where implicit subscription is not an intrinsic property of the object.
956
if ($this->reviewerStatus == self::ATTACHABLE) {
957
$reviewers = id(new DifferentialRevisionQuery())
958
->setViewer(PhabricatorUser::getOmnipotentUser())
959
->withPHIDs(array($this->getPHID()))
960
->needReviewers(true)
961
->executeOne()
962
->getReviewers();
963
} else {
964
$reviewers = $this->getReviewers();
965
}
966
967
foreach ($reviewers as $reviewer) {
968
if ($reviewer->getReviewerPHID() !== $phid) {
969
continue;
970
}
971
972
if ($reviewer->isResigned()) {
973
continue;
974
}
975
976
return true;
977
}
978
979
return false;
980
}
981
982
983
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
984
985
986
public function getCustomFieldSpecificationForRole($role) {
987
return PhabricatorEnv::getEnvConfig('differential.fields');
988
}
989
990
public function getCustomFieldBaseClass() {
991
return 'DifferentialCustomField';
992
}
993
994
public function getCustomFields() {
995
return $this->assertAttached($this->customFields);
996
}
997
998
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
999
$this->customFields = $fields;
1000
return $this;
1001
}
1002
1003
1004
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
1005
1006
1007
public function getApplicationTransactionEditor() {
1008
return new DifferentialTransactionEditor();
1009
}
1010
1011
public function getApplicationTransactionTemplate() {
1012
return new DifferentialTransaction();
1013
}
1014
1015
1016
/* -( PhabricatorDestructibleInterface )----------------------------------- */
1017
1018
1019
public function destroyObjectPermanently(
1020
PhabricatorDestructionEngine $engine) {
1021
1022
$viewer = $engine->getViewer();
1023
1024
$this->openTransaction();
1025
$diffs = id(new DifferentialDiffQuery())
1026
->setViewer($viewer)
1027
->withRevisionIDs(array($this->getID()))
1028
->execute();
1029
foreach ($diffs as $diff) {
1030
$engine->destroyObject($diff);
1031
}
1032
1033
id(new DifferentialAffectedPathEngine())
1034
->setRevision($this)
1035
->destroyAffectedPaths();
1036
1037
$viewstate_query = id(new DifferentialViewStateQuery())
1038
->setViewer($viewer)
1039
->withObjectPHIDs(array($this->getPHID()));
1040
$viewstates = new PhabricatorQueryIterator($viewstate_query);
1041
foreach ($viewstates as $viewstate) {
1042
$viewstate->delete();
1043
}
1044
1045
$this->delete();
1046
$this->saveTransaction();
1047
}
1048
1049
1050
/* -( PhabricatorFulltextInterface )--------------------------------------- */
1051
1052
1053
public function newFulltextEngine() {
1054
return new DifferentialRevisionFulltextEngine();
1055
}
1056
1057
1058
/* -( PhabricatorFerretInterface )----------------------------------------- */
1059
1060
1061
public function newFerretEngine() {
1062
return new DifferentialRevisionFerretEngine();
1063
}
1064
1065
1066
/* -( PhabricatorConduitResultInterface )---------------------------------- */
1067
1068
1069
public function getFieldSpecificationsForConduit() {
1070
return array(
1071
id(new PhabricatorConduitSearchFieldSpecification())
1072
->setKey('title')
1073
->setType('string')
1074
->setDescription(pht('The revision title.')),
1075
id(new PhabricatorConduitSearchFieldSpecification())
1076
->setKey('uri')
1077
->setType('uri')
1078
->setDescription(pht('View URI for the revision.')),
1079
id(new PhabricatorConduitSearchFieldSpecification())
1080
->setKey('authorPHID')
1081
->setType('phid')
1082
->setDescription(pht('Revision author PHID.')),
1083
id(new PhabricatorConduitSearchFieldSpecification())
1084
->setKey('status')
1085
->setType('map<string, wild>')
1086
->setDescription(pht('Information about revision status.')),
1087
id(new PhabricatorConduitSearchFieldSpecification())
1088
->setKey('repositoryPHID')
1089
->setType('phid?')
1090
->setDescription(pht('Revision repository PHID.')),
1091
id(new PhabricatorConduitSearchFieldSpecification())
1092
->setKey('diffPHID')
1093
->setType('phid')
1094
->setDescription(pht('Active diff PHID.')),
1095
id(new PhabricatorConduitSearchFieldSpecification())
1096
->setKey('summary')
1097
->setType('string')
1098
->setDescription(pht('Revision summary.')),
1099
id(new PhabricatorConduitSearchFieldSpecification())
1100
->setKey('testPlan')
1101
->setType('string')
1102
->setDescription(pht('Revision test plan.')),
1103
id(new PhabricatorConduitSearchFieldSpecification())
1104
->setKey('isDraft')
1105
->setType('bool')
1106
->setDescription(
1107
pht(
1108
'True if this revision is in any draft state, and thus not '.
1109
'notifying reviewers and subscribers about changes.')),
1110
id(new PhabricatorConduitSearchFieldSpecification())
1111
->setKey('holdAsDraft')
1112
->setType('bool')
1113
->setDescription(
1114
pht(
1115
'True if this revision is being held as a draft. It will not be '.
1116
'automatically submitted for review even if tests pass.')),
1117
);
1118
}
1119
1120
public function getFieldValuesForConduit() {
1121
$status = $this->getStatusObject();
1122
$status_info = array(
1123
'value' => $status->getKey(),
1124
'name' => $status->getDisplayName(),
1125
'closed' => $status->isClosedStatus(),
1126
'color.ansi' => $status->getANSIColor(),
1127
);
1128
1129
return array(
1130
'title' => $this->getTitle(),
1131
'uri' => PhabricatorEnv::getURI($this->getURI()),
1132
'authorPHID' => $this->getAuthorPHID(),
1133
'status' => $status_info,
1134
'repositoryPHID' => $this->getRepositoryPHID(),
1135
'diffPHID' => $this->getActiveDiffPHID(),
1136
'summary' => $this->getSummary(),
1137
'testPlan' => $this->getTestPlan(),
1138
'isDraft' => !$this->getShouldBroadcast(),
1139
'holdAsDraft' => (bool)$this->getHoldAsDraft(),
1140
);
1141
}
1142
1143
public function getConduitSearchAttachments() {
1144
return array(
1145
id(new DifferentialReviewersSearchEngineAttachment())
1146
->setAttachmentKey('reviewers'),
1147
);
1148
}
1149
1150
1151
/* -( PhabricatorDraftInterface )------------------------------------------ */
1152
1153
1154
public function newDraftEngine() {
1155
return new DifferentialRevisionDraftEngine();
1156
}
1157
1158
1159
/* -( PhabricatorTimelineInterface )--------------------------------------- */
1160
1161
1162
public function newTimelineEngine() {
1163
return new DifferentialRevisionTimelineEngine();
1164
}
1165
1166
1167
}
1168
1169