Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/differential/controller/DifferentialRevisionViewController.php
12256 views
1
<?php
2
3
final class DifferentialRevisionViewController
4
extends DifferentialController {
5
6
private $revisionID;
7
private $changesetCount;
8
private $hiddenChangesets;
9
private $warnings = array();
10
11
public function shouldAllowPublic() {
12
return true;
13
}
14
15
public function isLargeDiff() {
16
return ($this->getChangesetCount() > $this->getLargeDiffLimit());
17
}
18
19
public function isVeryLargeDiff() {
20
return ($this->getChangesetCount() > $this->getVeryLargeDiffLimit());
21
}
22
23
public function getLargeDiffLimit() {
24
return 200;
25
}
26
27
public function getVeryLargeDiffLimit() {
28
return 2000;
29
}
30
31
public function getChangesetCount() {
32
if ($this->changesetCount === null) {
33
throw new PhutilInvalidStateException('setChangesetCount');
34
}
35
return $this->changesetCount;
36
}
37
38
public function setChangesetCount($count) {
39
$this->changesetCount = $count;
40
return $this;
41
}
42
43
public function handleRequest(AphrontRequest $request) {
44
$viewer = $this->getViewer();
45
$this->revisionID = $request->getURIData('id');
46
47
$viewer_is_anonymous = !$viewer->isLoggedIn();
48
49
$revision = id(new DifferentialRevisionQuery())
50
->withIDs(array($this->revisionID))
51
->setViewer($viewer)
52
->needReviewers(true)
53
->needReviewerAuthority(true)
54
->needCommitPHIDs(true)
55
->executeOne();
56
if (!$revision) {
57
return new Aphront404Response();
58
}
59
60
$diffs = id(new DifferentialDiffQuery())
61
->setViewer($viewer)
62
->withRevisionIDs(array($this->revisionID))
63
->execute();
64
$diffs = array_reverse($diffs, $preserve_keys = true);
65
66
if (!$diffs) {
67
throw new Exception(
68
pht('This revision has no diffs. Something has gone quite wrong.'));
69
}
70
71
$revision->attachActiveDiff(last($diffs));
72
73
$diff_vs = $this->getOldDiffID($revision, $diffs);
74
if ($diff_vs instanceof AphrontResponse) {
75
return $diff_vs;
76
}
77
78
$target_id = $this->getNewDiffID($revision, $diffs);
79
if ($target_id instanceof AphrontResponse) {
80
return $target_id;
81
}
82
83
$target = $diffs[$target_id];
84
85
$target_manual = $target;
86
if (!$target_id) {
87
foreach ($diffs as $diff) {
88
if ($diff->getCreationMethod() != 'commit') {
89
$target_manual = $diff;
90
}
91
}
92
}
93
94
$repository = null;
95
$repository_phid = $target->getRepositoryPHID();
96
if ($repository_phid) {
97
if ($repository_phid == $revision->getRepositoryPHID()) {
98
$repository = $revision->getRepository();
99
} else {
100
$repository = id(new PhabricatorRepositoryQuery())
101
->setViewer($viewer)
102
->withPHIDs(array($repository_phid))
103
->executeOne();
104
}
105
}
106
107
list($changesets, $vs_map, $vs_changesets, $rendering_references) =
108
$this->loadChangesetsAndVsMap(
109
$target,
110
idx($diffs, $diff_vs),
111
$repository);
112
113
$this->setChangesetCount(count($rendering_references));
114
115
if ($request->getExists('download')) {
116
return $this->buildRawDiffResponse(
117
$revision,
118
$changesets,
119
$vs_changesets,
120
$vs_map,
121
$repository);
122
}
123
124
$map = $vs_map;
125
if (!$map) {
126
$map = array_fill_keys(array_keys($changesets), 0);
127
}
128
129
$old_ids = array();
130
$new_ids = array();
131
foreach ($map as $id => $vs) {
132
if ($vs <= 0) {
133
$old_ids[] = $id;
134
$new_ids[] = $id;
135
} else {
136
$new_ids[] = $id;
137
$new_ids[] = $vs;
138
}
139
}
140
141
$this->loadDiffProperties($diffs);
142
$props = $target_manual->getDiffProperties();
143
144
$subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(
145
$revision->getPHID());
146
147
$object_phids = array_merge(
148
$revision->getReviewerPHIDs(),
149
$subscriber_phids,
150
$revision->getCommitPHIDs(),
151
array(
152
$revision->getAuthorPHID(),
153
$viewer->getPHID(),
154
));
155
156
foreach ($revision->getAttached() as $type => $phids) {
157
foreach ($phids as $phid => $info) {
158
$object_phids[] = $phid;
159
}
160
}
161
162
$field_list = PhabricatorCustomField::getObjectFields(
163
$revision,
164
PhabricatorCustomField::ROLE_VIEW);
165
166
$field_list->setViewer($viewer);
167
$field_list->readFieldsFromStorage($revision);
168
169
$warning_handle_map = array();
170
foreach ($field_list->getFields() as $key => $field) {
171
$req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings();
172
foreach ($req as $phid) {
173
$warning_handle_map[$key][] = $phid;
174
$object_phids[] = $phid;
175
}
176
}
177
178
$handles = $this->loadViewerHandles($object_phids);
179
$warnings = $this->warnings;
180
181
$request_uri = $request->getRequestURI();
182
183
$large = $request->getStr('large');
184
185
$large_warning =
186
($this->isLargeDiff()) &&
187
(!$this->isVeryLargeDiff()) &&
188
(!$large);
189
190
if ($large_warning) {
191
$count = $this->getChangesetCount();
192
193
$expand_uri = $request_uri
194
->alter('large', 'true')
195
->setFragment('toc');
196
197
$message = array(
198
pht(
199
'This large diff affects %s files. Files without inline '.
200
'comments have been collapsed.',
201
new PhutilNumber($count)),
202
' ',
203
phutil_tag(
204
'strong',
205
array(),
206
phutil_tag(
207
'a',
208
array(
209
'href' => $expand_uri,
210
),
211
pht('Expand All Files'))),
212
);
213
214
$warnings[] = id(new PHUIInfoView())
215
->setTitle(pht('Large Diff'))
216
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
217
->appendChild($message);
218
219
$folded_changesets = $changesets;
220
} else {
221
$folded_changesets = array();
222
}
223
224
// Don't hide or fold changesets which have inline comments.
225
$hidden_changesets = $this->hiddenChangesets;
226
if ($hidden_changesets || $folded_changesets) {
227
$old = array_select_keys($changesets, $old_ids);
228
$new = array_select_keys($changesets, $new_ids);
229
230
$inlines = id(new DifferentialDiffInlineCommentQuery())
231
->setViewer($viewer)
232
->withRevisionPHIDs(array($revision->getPHID()))
233
->withPublishableComments(true)
234
->withPublishedComments(true)
235
->execute();
236
237
$inlines = mpull($inlines, 'newInlineCommentObject');
238
239
$inlines = id(new PhabricatorInlineCommentAdjustmentEngine())
240
->setViewer($viewer)
241
->setRevision($revision)
242
->setOldChangesets($old)
243
->setNewChangesets($new)
244
->setInlines($inlines)
245
->execute();
246
247
foreach ($inlines as $inline) {
248
$changeset_id = $inline->getChangesetID();
249
if (!isset($changesets[$changeset_id])) {
250
continue;
251
}
252
253
unset($hidden_changesets[$changeset_id]);
254
unset($folded_changesets[$changeset_id]);
255
}
256
}
257
258
// If we would hide only one changeset, don't hide anything. The notice
259
// we'd render about it is about the same size as the changeset.
260
if (count($hidden_changesets) < 2) {
261
$hidden_changesets = array();
262
}
263
264
// Update the set of hidden changesets, since we may have just un-hidden
265
// some of them.
266
if ($hidden_changesets) {
267
$warnings[] = id(new PHUIInfoView())
268
->setTitle(pht('Showing Only Differences'))
269
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
270
->appendChild(
271
pht(
272
'This revision modifies %s more files that are hidden because '.
273
'they were not modified between selected diffs and they have no '.
274
'inline comments.',
275
phutil_count($hidden_changesets)));
276
}
277
278
// Compute the unfolded changesets. By default, everything is unfolded.
279
$unfolded_changesets = $changesets;
280
foreach ($folded_changesets as $changeset_id => $changeset) {
281
unset($unfolded_changesets[$changeset_id]);
282
}
283
284
// Throw away any hidden changesets.
285
foreach ($hidden_changesets as $changeset_id => $changeset) {
286
unset($changesets[$changeset_id]);
287
unset($unfolded_changesets[$changeset_id]);
288
}
289
290
$commit_hashes = mpull($diffs, 'getSourceControlBaseRevision');
291
$local_commits = idx($props, 'local:commits', array());
292
foreach ($local_commits as $local_commit) {
293
$commit_hashes[] = idx($local_commit, 'tree');
294
$commit_hashes[] = idx($local_commit, 'local');
295
}
296
$commit_hashes = array_unique(array_filter($commit_hashes));
297
if ($commit_hashes) {
298
$commits_for_links = id(new DiffusionCommitQuery())
299
->setViewer($viewer)
300
->withIdentifiers($commit_hashes)
301
->execute();
302
$commits_for_links = mpull(
303
$commits_for_links,
304
null,
305
'getCommitIdentifier');
306
} else {
307
$commits_for_links = array();
308
}
309
310
$header = $this->buildHeader($revision);
311
$subheader = $this->buildSubheaderView($revision);
312
$details = $this->buildDetails($revision, $field_list);
313
$curtain = $this->buildCurtain($revision);
314
315
$repository = $revision->getRepository();
316
if ($repository) {
317
$symbol_indexes = $this->buildSymbolIndexes(
318
$repository,
319
$unfolded_changesets);
320
} else {
321
$symbol_indexes = array();
322
}
323
324
$revision_warnings = $this->buildRevisionWarnings(
325
$revision,
326
$field_list,
327
$warning_handle_map,
328
$handles);
329
$info_view = null;
330
if ($revision_warnings) {
331
$info_view = id(new PHUIInfoView())
332
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
333
->setErrors($revision_warnings);
334
}
335
336
$detail_diffs = array_select_keys(
337
$diffs,
338
array($diff_vs, $target->getID()));
339
$detail_diffs = mpull($detail_diffs, null, 'getPHID');
340
341
$this->loadHarbormasterData($detail_diffs);
342
343
$diff_detail_box = $this->buildDiffDetailView(
344
$detail_diffs,
345
$revision,
346
$field_list);
347
348
$unit_box = $this->buildUnitMessagesView(
349
$target,
350
$revision);
351
352
$timeline = $this->buildTransactions(
353
$revision,
354
$diff_vs ? $diffs[$diff_vs] : $target,
355
$target,
356
$old_ids,
357
$new_ids);
358
359
$timeline->setQuoteRef($revision->getMonogram());
360
361
if ($this->isVeryLargeDiff()) {
362
$messages = array();
363
364
$messages[] = pht(
365
'This very large diff affects more than %s files. Use the %s to '.
366
'browse changes.',
367
new PhutilNumber($this->getVeryLargeDiffLimit()),
368
phutil_tag(
369
'a',
370
array(
371
'href' => '/differential/diff/'.$target->getID().'/changesets/',
372
),
373
phutil_tag('strong', array(), pht('Changeset List'))));
374
375
$changeset_view = id(new PHUIInfoView())
376
->setErrors($messages);
377
} else {
378
$changeset_view = id(new DifferentialChangesetListView())
379
->setChangesets($changesets)
380
->setVisibleChangesets($unfolded_changesets)
381
->setStandaloneURI('/differential/changeset/')
382
->setRawFileURIs(
383
'/differential/changeset/?view=old',
384
'/differential/changeset/?view=new')
385
->setUser($viewer)
386
->setDiff($target)
387
->setRenderingReferences($rendering_references)
388
->setVsMap($vs_map)
389
->setSymbolIndexes($symbol_indexes)
390
->setTitle(pht('Diff %s', $target->getID()))
391
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
392
393
$revision_id = $revision->getID();
394
$inline_list_uri = "/revision/inlines/{$revision_id}/";
395
$inline_list_uri = $this->getApplicationURI($inline_list_uri);
396
$changeset_view->setInlineListURI($inline_list_uri);
397
398
if ($repository) {
399
$changeset_view->setRepository($repository);
400
}
401
402
if (!$viewer_is_anonymous) {
403
$changeset_view->setInlineCommentControllerURI(
404
'/differential/comment/inline/edit/'.$revision->getID().'/');
405
}
406
}
407
408
$broken_diffs = $this->loadHistoryDiffStatus($diffs);
409
410
$history = id(new DifferentialRevisionUpdateHistoryView())
411
->setUser($viewer)
412
->setDiffs($diffs)
413
->setDiffUnitStatuses($broken_diffs)
414
->setSelectedVersusDiffID($diff_vs)
415
->setSelectedDiffID($target->getID())
416
->setCommitsForLinks($commits_for_links);
417
418
$local_table = id(new DifferentialLocalCommitsView())
419
->setUser($viewer)
420
->setLocalCommits(idx($props, 'local:commits'))
421
->setCommitsForLinks($commits_for_links);
422
423
if ($repository && !$this->isVeryLargeDiff()) {
424
$other_revisions = $this->loadOtherRevisions(
425
$changesets,
426
$target,
427
$repository);
428
} else {
429
$other_revisions = array();
430
}
431
432
$other_view = null;
433
if ($other_revisions) {
434
$other_view = $this->renderOtherRevisions($other_revisions);
435
}
436
437
if ($this->isVeryLargeDiff()) {
438
$toc_view = null;
439
440
// When rendering a "very large" diff, we skip computation of owners
441
// that own no files because it is significantly expensive and not very
442
// valuable.
443
foreach ($revision->getReviewers() as $reviewer) {
444
// Give each reviewer a dummy nonempty value so the UI does not render
445
// the "(Owns No Changed Paths)" note. If that behavior becomes more
446
// sophisticated in the future, this behavior might also need to.
447
$reviewer->attachChangesets($changesets);
448
}
449
} else {
450
$this->buildPackageMaps($changesets);
451
452
$toc_view = $this->buildTableOfContents(
453
$changesets,
454
$unfolded_changesets,
455
$target->loadCoverageMap($viewer));
456
457
// Attach changesets to each reviewer so we can show which Owners package
458
// reviewers own no files.
459
foreach ($revision->getReviewers() as $reviewer) {
460
$reviewer_phid = $reviewer->getReviewerPHID();
461
$reviewer_changesets = $this->getPackageChangesets($reviewer_phid);
462
$reviewer->attachChangesets($reviewer_changesets);
463
}
464
465
$authority_packages = $this->getAuthorityPackages();
466
foreach ($changesets as $changeset) {
467
$changeset_packages = $this->getChangesetPackages($changeset);
468
469
$changeset
470
->setAuthorityPackages($authority_packages)
471
->setChangesetPackages($changeset_packages);
472
}
473
}
474
475
$tab_group = new PHUITabGroupView();
476
477
if ($toc_view) {
478
$tab_group->addTab(
479
id(new PHUITabView())
480
->setName(pht('Files'))
481
->setKey('files')
482
->appendChild($toc_view));
483
}
484
485
$tab_group->addTab(
486
id(new PHUITabView())
487
->setName(pht('History'))
488
->setKey('history')
489
->appendChild($history));
490
491
$filetree = id(new DifferentialFileTreeEngine())
492
->setViewer($viewer);
493
$filetree_collapsed = !$filetree->getIsVisible();
494
495
// See PHI811. If the viewer has the file tree on, the files tab with the
496
// table of contents is redundant, so default to the "History" tab instead.
497
if (!$filetree_collapsed) {
498
$tab_group->selectTab('history');
499
}
500
501
$tab_group->addTab(
502
id(new PHUITabView())
503
->setName(pht('Commits'))
504
->setKey('commits')
505
->appendChild($local_table));
506
507
$stack_graph = id(new DifferentialRevisionGraph())
508
->setViewer($viewer)
509
->setSeedPHID($revision->getPHID())
510
->setLoadEntireGraph(true)
511
->loadGraph();
512
if (!$stack_graph->isEmpty()) {
513
// See PHI1900. The graph UI element now tries to figure out the correct
514
// height automatically, but currently can't in this case because the
515
// element is not visible when the page loads. Set an explicit height.
516
$stack_graph->setHeight(34);
517
518
$stack_table = $stack_graph->newGraphTable();
519
520
$parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
521
$reachable = $stack_graph->getReachableObjects($parent_type);
522
523
foreach ($reachable as $key => $reachable_revision) {
524
if ($reachable_revision->isClosed()) {
525
unset($reachable[$key]);
526
}
527
}
528
529
if ($reachable) {
530
$stack_name = pht('Stack (%s Open)', phutil_count($reachable));
531
$stack_color = PHUIListItemView::STATUS_FAIL;
532
} else {
533
$stack_name = pht('Stack');
534
$stack_color = null;
535
}
536
537
$tab_group->addTab(
538
id(new PHUITabView())
539
->setName($stack_name)
540
->setKey('stack')
541
->setColor($stack_color)
542
->appendChild($stack_table));
543
}
544
545
if ($other_view) {
546
$tab_group->addTab(
547
id(new PHUITabView())
548
->setName(pht('Similar'))
549
->setKey('similar')
550
->appendChild($other_view));
551
}
552
553
$view_button = id(new PHUIButtonView())
554
->setTag('a')
555
->setText(pht('Changeset List'))
556
->setHref('/differential/diff/'.$target->getID().'/changesets/')
557
->setIcon('fa-align-left');
558
559
$tab_header = id(new PHUIHeaderView())
560
->setHeader(pht('Revision Contents'))
561
->addActionLink($view_button);
562
563
$tab_view = id(new PHUIObjectBoxView())
564
->setHeader($tab_header)
565
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
566
->addTabGroup($tab_group);
567
568
$signatures = DifferentialRequiredSignaturesField::loadForRevision(
569
$revision);
570
$missing_signatures = false;
571
foreach ($signatures as $phid => $signed) {
572
if (!$signed) {
573
$missing_signatures = true;
574
}
575
}
576
577
$footer = array();
578
$signature_message = null;
579
if ($missing_signatures) {
580
$signature_message = id(new PHUIInfoView())
581
->setTitle(pht('Content Hidden'))
582
->appendChild(
583
pht(
584
'The content of this revision is hidden until the author has '.
585
'signed all of the required legal agreements.'));
586
} else {
587
$anchor = id(new PhabricatorAnchorView())
588
->setAnchorName('toc')
589
->setNavigationMarker(true);
590
591
$footer[] = array(
592
$anchor,
593
$warnings,
594
$tab_view,
595
$changeset_view,
596
);
597
}
598
599
$comment_view = id(new DifferentialRevisionEditEngine())
600
->setViewer($viewer)
601
->buildEditEngineCommentView($revision);
602
603
$comment_view->setTransactionTimeline($timeline);
604
605
$review_warnings = array();
606
foreach ($field_list->getFields() as $field) {
607
$review_warnings[] = $field->getWarningsForDetailView();
608
}
609
$review_warnings = array_mergev($review_warnings);
610
611
if ($review_warnings) {
612
$warnings_view = id(new PHUIInfoView())
613
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
614
->setErrors($review_warnings);
615
616
$comment_view->setInfoView($warnings_view);
617
}
618
619
$footer[] = $comment_view;
620
621
$monogram = $revision->getMonogram();
622
$operations_box = $this->buildOperationsBox($revision);
623
624
$crumbs = $this->buildApplicationCrumbs();
625
$crumbs->addTextCrumb($monogram);
626
$crumbs->setBorder(true);
627
628
$filetree
629
->setChangesets($changesets)
630
->setDisabled($this->isVeryLargeDiff());
631
632
$view = id(new PHUITwoColumnView())
633
->setHeader($header)
634
->setSubheader($subheader)
635
->setCurtain($curtain)
636
->setMainColumn(
637
array(
638
$operations_box,
639
$info_view,
640
$details,
641
$diff_detail_box,
642
$unit_box,
643
$timeline,
644
$signature_message,
645
))
646
->setFooter($footer);
647
648
$main_content = array(
649
$crumbs,
650
$view,
651
);
652
653
$main_content = $filetree->newView($main_content);
654
655
if (!$filetree->getDisabled()) {
656
$changeset_view->setFormationView($main_content);
657
}
658
659
$page = $this->newPage()
660
->setTitle($monogram.' '.$revision->getTitle())
661
->setPageObjectPHIDs(array($revision->getPHID()))
662
->appendChild($main_content);
663
664
return $page;
665
}
666
667
private function buildHeader(DifferentialRevision $revision) {
668
$view = id(new PHUIHeaderView())
669
->setHeader($revision->getTitle($revision))
670
->setUser($this->getViewer())
671
->setPolicyObject($revision)
672
->setHeaderIcon('fa-cog');
673
674
$status_tag = id(new PHUITagView())
675
->setName($revision->getStatusDisplayName())
676
->setIcon($revision->getStatusIcon())
677
->setColor($revision->getStatusTagColor())
678
->setType(PHUITagView::TYPE_SHADE);
679
680
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_tag);
681
682
// If the revision is in a status other than "Draft", but not broadcasting,
683
// add an additional "Draft" tag to the header to make it clear that this
684
// revision hasn't promoted yet.
685
if (!$revision->getShouldBroadcast() && !$revision->isDraft()) {
686
$draft_status = DifferentialRevisionStatus::newForStatus(
687
DifferentialRevisionStatus::DRAFT);
688
689
$draft_tag = id(new PHUITagView())
690
->setName($draft_status->getDisplayName())
691
->setIcon($draft_status->getIcon())
692
->setColor($draft_status->getTagColor())
693
->setType(PHUITagView::TYPE_SHADE);
694
695
$view->addTag($draft_tag);
696
}
697
698
return $view;
699
}
700
701
private function buildSubheaderView(DifferentialRevision $revision) {
702
$viewer = $this->getViewer();
703
704
$author_phid = $revision->getAuthorPHID();
705
706
$author = $viewer->renderHandle($author_phid)->render();
707
$date = phabricator_datetime($revision->getDateCreated(), $viewer);
708
$author = phutil_tag('strong', array(), $author);
709
710
$handles = $viewer->loadHandles(array($author_phid));
711
$image_uri = $handles[$author_phid]->getImageURI();
712
$image_href = $handles[$author_phid]->getURI();
713
714
$content = pht('Authored by %s on %s.', $author, $date);
715
716
return id(new PHUIHeadThingView())
717
->setImage($image_uri)
718
->setImageHref($image_href)
719
->setContent($content);
720
}
721
722
private function buildDetails(
723
DifferentialRevision $revision,
724
$custom_fields) {
725
$viewer = $this->getViewer();
726
$properties = id(new PHUIPropertyListView())
727
->setUser($viewer);
728
729
if ($custom_fields) {
730
$custom_fields->appendFieldsToPropertyList(
731
$revision,
732
$viewer,
733
$properties);
734
}
735
736
$header = id(new PHUIHeaderView())
737
->setHeader(pht('Details'));
738
739
return id(new PHUIObjectBoxView())
740
->setHeader($header)
741
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
742
->appendChild($properties);
743
}
744
745
private function buildCurtain(DifferentialRevision $revision) {
746
$viewer = $this->getViewer();
747
$revision_id = $revision->getID();
748
$revision_phid = $revision->getPHID();
749
$curtain = $this->newCurtainView($revision);
750
751
$can_edit = PhabricatorPolicyFilter::hasCapability(
752
$viewer,
753
$revision,
754
PhabricatorPolicyCapability::CAN_EDIT);
755
756
$curtain->addAction(
757
id(new PhabricatorActionView())
758
->setIcon('fa-pencil')
759
->setHref("/differential/revision/edit/{$revision_id}/")
760
->setName(pht('Edit Revision'))
761
->setDisabled(!$can_edit)
762
->setWorkflow(!$can_edit));
763
764
$curtain->addAction(
765
id(new PhabricatorActionView())
766
->setIcon('fa-upload')
767
->setHref("/differential/revision/update/{$revision_id}/")
768
->setName(pht('Update Diff'))
769
->setDisabled(!$can_edit)
770
->setWorkflow(!$can_edit));
771
772
$request_uri = $this->getRequest()->getRequestURI();
773
$curtain->addAction(
774
id(new PhabricatorActionView())
775
->setIcon('fa-download')
776
->setName(pht('Download Raw Diff'))
777
->setHref($request_uri->alter('download', 'true')));
778
779
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
780
$viewer,
781
$revision);
782
783
$revision_actions = array(
784
DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY,
785
DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY,
786
);
787
788
$revision_submenu = $relationship_list->newActionSubmenu($revision_actions)
789
->setName(pht('Edit Related Revisions...'))
790
->setIcon('fa-cog');
791
792
$curtain->addAction($revision_submenu);
793
794
$relationship_submenu = $relationship_list->newActionMenu();
795
if ($relationship_submenu) {
796
$curtain->addAction($relationship_submenu);
797
}
798
799
$repository = $revision->getRepository();
800
if ($repository && $repository->canPerformAutomation()) {
801
$revision_id = $revision->getID();
802
803
$op = new DrydockLandRepositoryOperation();
804
$barrier = $op->getBarrierToLanding($viewer, $revision);
805
806
if ($barrier) {
807
$can_land = false;
808
} else {
809
$can_land = true;
810
}
811
812
$action = id(new PhabricatorActionView())
813
->setName(pht('Land Revision'))
814
->setIcon('fa-fighter-jet')
815
->setHref("/differential/revision/operation/{$revision_id}/")
816
->setWorkflow(true)
817
->setDisabled(!$can_land);
818
819
$curtain->addAction($action);
820
}
821
822
return $curtain;
823
}
824
825
private function loadHistoryDiffStatus(array $diffs) {
826
assert_instances_of($diffs, 'DifferentialDiff');
827
828
$diff_phids = mpull($diffs, 'getPHID');
829
$bad_unit_status = array(
830
ArcanistUnitTestResult::RESULT_FAIL,
831
ArcanistUnitTestResult::RESULT_BROKEN,
832
);
833
834
$message = new HarbormasterBuildUnitMessage();
835
$target = new HarbormasterBuildTarget();
836
$build = new HarbormasterBuild();
837
$buildable = new HarbormasterBuildable();
838
839
$broken_diffs = queryfx_all(
840
$message->establishConnection('r'),
841
'SELECT distinct a.buildablePHID
842
FROM %T m
843
JOIN %T t ON m.buildTargetPHID = t.phid
844
JOIN %T b ON t.buildPHID = b.phid
845
JOIN %T a ON b.buildablePHID = a.phid
846
WHERE a.buildablePHID IN (%Ls)
847
AND m.result in (%Ls)',
848
$message->getTableName(),
849
$target->getTableName(),
850
$build->getTableName(),
851
$buildable->getTableName(),
852
$diff_phids,
853
$bad_unit_status);
854
855
$unit_status = array();
856
foreach ($broken_diffs as $broken) {
857
$phid = $broken['buildablePHID'];
858
$unit_status[$phid] = DifferentialUnitStatus::UNIT_FAIL;
859
}
860
861
return $unit_status;
862
}
863
864
private function loadChangesetsAndVsMap(
865
DifferentialDiff $target,
866
DifferentialDiff $diff_vs = null,
867
PhabricatorRepository $repository = null) {
868
$viewer = $this->getViewer();
869
870
$load_diffs = array($target);
871
if ($diff_vs) {
872
$load_diffs[] = $diff_vs;
873
}
874
875
$raw_changesets = id(new DifferentialChangesetQuery())
876
->setViewer($viewer)
877
->withDiffs($load_diffs)
878
->execute();
879
$changeset_groups = mgroup($raw_changesets, 'getDiffID');
880
881
$changesets = idx($changeset_groups, $target->getID(), array());
882
$changesets = mpull($changesets, null, 'getID');
883
884
$refs = array();
885
$vs_map = array();
886
$vs_changesets = array();
887
$must_compare = array();
888
if ($diff_vs) {
889
$vs_id = $diff_vs->getID();
890
$vs_changesets_path_map = array();
891
foreach (idx($changeset_groups, $vs_id, array()) as $changeset) {
892
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs);
893
$vs_changesets_path_map[$path] = $changeset;
894
$vs_changesets[$changeset->getID()] = $changeset;
895
}
896
897
foreach ($changesets as $key => $changeset) {
898
$path = $changeset->getAbsoluteRepositoryPath($repository, $target);
899
if (isset($vs_changesets_path_map[$path])) {
900
$vs_map[$changeset->getID()] =
901
$vs_changesets_path_map[$path]->getID();
902
$refs[$changeset->getID()] =
903
$changeset->getID().'/'.$vs_changesets_path_map[$path]->getID();
904
unset($vs_changesets_path_map[$path]);
905
906
$must_compare[] = $changeset->getID();
907
908
} else {
909
$refs[$changeset->getID()] = $changeset->getID();
910
}
911
}
912
913
foreach ($vs_changesets_path_map as $path => $changeset) {
914
$changesets[$changeset->getID()] = $changeset;
915
$vs_map[$changeset->getID()] = -1;
916
$refs[$changeset->getID()] = $changeset->getID().'/-1';
917
}
918
919
} else {
920
foreach ($changesets as $changeset) {
921
$refs[$changeset->getID()] = $changeset->getID();
922
}
923
}
924
925
$changesets = msort($changesets, 'getSortKey');
926
927
// See T13137. When displaying the diff between two updates, hide any
928
// changesets which haven't actually changed.
929
$this->hiddenChangesets = array();
930
foreach ($must_compare as $changeset_id) {
931
$changeset = $changesets[$changeset_id];
932
$vs_changeset = $vs_changesets[$vs_map[$changeset_id]];
933
934
if ($changeset->hasSameEffectAs($vs_changeset)) {
935
$this->hiddenChangesets[$changeset_id] = $changesets[$changeset_id];
936
}
937
}
938
939
return array($changesets, $vs_map, $vs_changesets, $refs);
940
}
941
942
private function buildSymbolIndexes(
943
PhabricatorRepository $repository,
944
array $unfolded_changesets) {
945
assert_instances_of($unfolded_changesets, 'DifferentialChangeset');
946
947
$engine = PhabricatorSyntaxHighlighter::newEngine();
948
949
$langs = $repository->getSymbolLanguages();
950
$langs = nonempty($langs, array());
951
952
$sources = $repository->getSymbolSources();
953
$sources = nonempty($sources, array());
954
955
$symbol_indexes = array();
956
957
if ($langs && $sources) {
958
$have_symbols = id(new DiffusionSymbolQuery())
959
->existsSymbolsInRepository($repository->getPHID());
960
if (!$have_symbols) {
961
return $symbol_indexes;
962
}
963
}
964
965
$repository_phids = array_merge(
966
array($repository->getPHID()),
967
$sources);
968
969
$indexed_langs = array_fill_keys($langs, true);
970
foreach ($unfolded_changesets as $key => $changeset) {
971
$lang = $engine->getLanguageFromFilename($changeset->getFilename());
972
if (empty($indexed_langs) || isset($indexed_langs[$lang])) {
973
$symbol_indexes[$key] = array(
974
'lang' => $lang,
975
'repositories' => $repository_phids,
976
);
977
}
978
}
979
980
return $symbol_indexes;
981
}
982
983
private function loadOtherRevisions(
984
array $changesets,
985
DifferentialDiff $target,
986
PhabricatorRepository $repository) {
987
assert_instances_of($changesets, 'DifferentialChangeset');
988
989
$viewer = $this->getViewer();
990
991
$paths = array();
992
foreach ($changesets as $changeset) {
993
$paths[] = $changeset->getAbsoluteRepositoryPath(
994
$repository,
995
$target);
996
}
997
998
if (!$paths) {
999
return array();
1000
}
1001
1002
$recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds'));
1003
1004
$query = id(new DifferentialRevisionQuery())
1005
->setViewer($viewer)
1006
->withIsOpen(true)
1007
->withUpdatedEpochBetween($recent, null)
1008
->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED)
1009
->setLimit(10)
1010
->needFlags(true)
1011
->needDrafts(true)
1012
->needReviewers(true)
1013
->withRepositoryPHIDs(
1014
array(
1015
$repository->getPHID(),
1016
))
1017
->withPaths($paths);
1018
1019
$results = $query->execute();
1020
1021
// Strip out *this* revision.
1022
foreach ($results as $key => $result) {
1023
if ($result->getID() == $this->revisionID) {
1024
unset($results[$key]);
1025
break;
1026
}
1027
}
1028
1029
return $results;
1030
}
1031
1032
private function renderOtherRevisions(array $revisions) {
1033
assert_instances_of($revisions, 'DifferentialRevision');
1034
$viewer = $this->getViewer();
1035
1036
$header = id(new PHUIHeaderView())
1037
->setHeader(pht('Recent Similar Revisions'));
1038
1039
return id(new DifferentialRevisionListView())
1040
->setViewer($viewer)
1041
->setRevisions($revisions)
1042
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
1043
->setNoBox(true);
1044
}
1045
1046
1047
private function buildRawDiffResponse(
1048
DifferentialRevision $revision,
1049
array $changesets,
1050
array $vs_changesets,
1051
array $vs_map,
1052
PhabricatorRepository $repository = null) {
1053
1054
assert_instances_of($changesets, 'DifferentialChangeset');
1055
assert_instances_of($vs_changesets, 'DifferentialChangeset');
1056
1057
$viewer = $this->getViewer();
1058
1059
id(new DifferentialHunkQuery())
1060
->setViewer($viewer)
1061
->withChangesets($changesets)
1062
->needAttachToChangesets(true)
1063
->execute();
1064
1065
$diff = new DifferentialDiff();
1066
$diff->attachChangesets($changesets);
1067
$raw_changes = $diff->buildChangesList();
1068
$changes = array();
1069
foreach ($raw_changes as $changedict) {
1070
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
1071
}
1072
1073
$loader = id(new PhabricatorFileBundleLoader())
1074
->setViewer($viewer);
1075
1076
$bundle = ArcanistBundle::newFromChanges($changes);
1077
$bundle->setLoadFileDataCallback(array($loader, 'loadFileData'));
1078
1079
$vcs = $repository ? $repository->getVersionControlSystem() : null;
1080
switch ($vcs) {
1081
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
1082
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
1083
$raw_diff = $bundle->toGitPatch();
1084
break;
1085
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
1086
default:
1087
$raw_diff = $bundle->toUnifiedDiff();
1088
break;
1089
}
1090
1091
$request_uri = $this->getRequest()->getRequestURI();
1092
1093
// this ends up being something like
1094
// D123.diff
1095
// or the verbose
1096
// D123.vs123.id123.highlightjs.diff
1097
// lame but nice to include these options
1098
$file_name = ltrim($request_uri->getPath(), '/').'.';
1099
foreach ($request_uri->getQueryParamsAsPairList() as $pair) {
1100
list($key, $value) = $pair;
1101
if ($key == 'download') {
1102
continue;
1103
}
1104
$file_name .= $key.$value.'.';
1105
}
1106
$file_name .= 'diff';
1107
1108
$iterator = new ArrayIterator(array($raw_diff));
1109
1110
$source = id(new PhabricatorIteratorFileUploadSource())
1111
->setName($file_name)
1112
->setMIMEType('text/plain')
1113
->setRelativeTTL(phutil_units('24 hours in seconds'))
1114
->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
1115
->setIterator($iterator);
1116
1117
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
1118
$file = $source->uploadFile();
1119
$file->attachToObject($revision->getPHID());
1120
unset($unguarded);
1121
1122
return $file->getRedirectResponse();
1123
}
1124
1125
private function buildTransactions(
1126
DifferentialRevision $revision,
1127
DifferentialDiff $left_diff,
1128
DifferentialDiff $right_diff,
1129
array $old_ids,
1130
array $new_ids) {
1131
1132
$timeline = $this->buildTransactionTimeline(
1133
$revision,
1134
new DifferentialTransactionQuery(),
1135
$engine = null,
1136
array(
1137
'left' => $left_diff->getID(),
1138
'right' => $right_diff->getID(),
1139
'old' => implode(',', $old_ids),
1140
'new' => implode(',', $new_ids),
1141
));
1142
1143
return $timeline;
1144
}
1145
1146
private function buildRevisionWarnings(
1147
DifferentialRevision $revision,
1148
PhabricatorCustomFieldList $field_list,
1149
array $warning_handle_map,
1150
array $handles) {
1151
1152
$warnings = array();
1153
foreach ($field_list->getFields() as $key => $field) {
1154
$phids = idx($warning_handle_map, $key, array());
1155
$field_handles = array_select_keys($handles, $phids);
1156
$field_warnings = $field->getWarningsForRevisionHeader($field_handles);
1157
foreach ($field_warnings as $warning) {
1158
$warnings[] = $warning;
1159
}
1160
}
1161
1162
return $warnings;
1163
}
1164
1165
private function buildDiffDetailView(
1166
array $diffs,
1167
DifferentialRevision $revision,
1168
PhabricatorCustomFieldList $field_list) {
1169
$viewer = $this->getViewer();
1170
1171
$fields = array();
1172
foreach ($field_list->getFields() as $field) {
1173
if ($field->shouldAppearInDiffPropertyView()) {
1174
$fields[] = $field;
1175
}
1176
}
1177
1178
if (!$fields) {
1179
return null;
1180
}
1181
1182
$property_lists = array();
1183
foreach ($this->getDiffTabLabels($diffs) as $tab) {
1184
list($label, $diff) = $tab;
1185
1186
$property_lists[] = array(
1187
$label,
1188
$this->buildDiffPropertyList($diff, $revision, $fields),
1189
);
1190
}
1191
1192
$tab_group = id(new PHUITabGroupView())
1193
->setHideSingleTab(true);
1194
1195
foreach ($property_lists as $key => $property_list) {
1196
list($tab_name, $list_view) = $property_list;
1197
1198
$tab = id(new PHUITabView())
1199
->setKey($key)
1200
->setName($tab_name)
1201
->appendChild($list_view);
1202
1203
$tab_group->addTab($tab);
1204
$tab_group->selectTab($key);
1205
}
1206
1207
return id(new PHUIObjectBoxView())
1208
->setHeaderText(pht('Diff Detail'))
1209
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
1210
->setUser($viewer)
1211
->addTabGroup($tab_group);
1212
}
1213
1214
private function buildDiffPropertyList(
1215
DifferentialDiff $diff,
1216
DifferentialRevision $revision,
1217
array $fields) {
1218
$viewer = $this->getViewer();
1219
1220
$view = id(new PHUIPropertyListView())
1221
->setUser($viewer)
1222
->setObject($diff);
1223
1224
foreach ($fields as $field) {
1225
$label = $field->renderDiffPropertyViewLabel($diff);
1226
$value = $field->renderDiffPropertyViewValue($diff);
1227
if ($value !== null) {
1228
$view->addProperty($label, $value);
1229
}
1230
}
1231
1232
return $view;
1233
}
1234
1235
private function buildOperationsBox(DifferentialRevision $revision) {
1236
$viewer = $this->getViewer();
1237
1238
// Save a query if we can't possibly have pending operations.
1239
$repository = $revision->getRepository();
1240
if (!$repository || !$repository->canPerformAutomation()) {
1241
return null;
1242
}
1243
1244
$operations = id(new DrydockRepositoryOperationQuery())
1245
->setViewer($viewer)
1246
->withObjectPHIDs(array($revision->getPHID()))
1247
->withIsDismissed(false)
1248
->withOperationTypes(
1249
array(
1250
DrydockLandRepositoryOperation::OPCONST,
1251
))
1252
->execute();
1253
if (!$operations) {
1254
return null;
1255
}
1256
1257
$state_fail = DrydockRepositoryOperation::STATE_FAIL;
1258
1259
// We're going to show the oldest operation which hasn't failed, or the
1260
// most recent failure if they're all failures.
1261
$operations = msort($operations, 'getID');
1262
foreach ($operations as $operation) {
1263
if ($operation->getOperationState() != $state_fail) {
1264
break;
1265
}
1266
}
1267
1268
// If we found a completed operation, don't render anything. We don't want
1269
// to show an older error after the thing worked properly.
1270
if ($operation->isDone()) {
1271
return null;
1272
}
1273
1274
$box_view = id(new PHUIObjectBoxView())
1275
->setHeaderText(pht('Active Operations'))
1276
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
1277
1278
return id(new DrydockRepositoryOperationStatusView())
1279
->setUser($viewer)
1280
->setBoxView($box_view)
1281
->setOperation($operation);
1282
}
1283
1284
private function buildUnitMessagesView(
1285
DifferentialDiff $diff,
1286
DifferentialRevision $revision) {
1287
$viewer = $this->getViewer();
1288
1289
if (!$diff->getBuildable()) {
1290
return null;
1291
}
1292
1293
if (!$diff->getUnitMessages()) {
1294
return null;
1295
}
1296
1297
$interesting_messages = array();
1298
foreach ($diff->getUnitMessages() as $message) {
1299
switch ($message->getResult()) {
1300
case ArcanistUnitTestResult::RESULT_PASS:
1301
case ArcanistUnitTestResult::RESULT_SKIP:
1302
break;
1303
default:
1304
$interesting_messages[] = $message;
1305
break;
1306
}
1307
}
1308
1309
if (!$interesting_messages) {
1310
return null;
1311
}
1312
1313
return id(new HarbormasterUnitSummaryView())
1314
->setViewer($viewer)
1315
->setBuildable($diff->getBuildable())
1316
->setUnitMessages($diff->getUnitMessages())
1317
->setLimit(5)
1318
->setShowViewAll(true);
1319
}
1320
1321
private function getOldDiffID(DifferentialRevision $revision, array $diffs) {
1322
assert_instances_of($diffs, 'DifferentialDiff');
1323
$request = $this->getRequest();
1324
1325
$diffs = mpull($diffs, null, 'getID');
1326
1327
$is_new = ($request->getURIData('filter') === 'new');
1328
$old_id = $request->getInt('vs');
1329
1330
// This is ambiguous, so just 404 rather than trying to figure out what
1331
// the user expects.
1332
if ($is_new && $old_id) {
1333
return new Aphront404Response();
1334
}
1335
1336
if ($is_new) {
1337
$viewer = $this->getViewer();
1338
1339
$xactions = id(new DifferentialTransactionQuery())
1340
->setViewer($viewer)
1341
->withObjectPHIDs(array($revision->getPHID()))
1342
->withAuthorPHIDs(array($viewer->getPHID()))
1343
->setOrder('newest')
1344
->setLimit(1)
1345
->execute();
1346
1347
if (!$xactions) {
1348
$this->warnings[] = id(new PHUIInfoView())
1349
->setTitle(pht('No Actions'))
1350
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
1351
->appendChild(
1352
pht(
1353
'Showing all changes because you have never taken an '.
1354
'action on this revision.'));
1355
} else {
1356
$xaction = head($xactions);
1357
1358
// Find the transactions which updated this revision. We want to
1359
// figure out which diff was active when you last took an action.
1360
$updates = id(new DifferentialTransactionQuery())
1361
->setViewer($viewer)
1362
->withObjectPHIDs(array($revision->getPHID()))
1363
->withTransactionTypes(
1364
array(
1365
DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE,
1366
))
1367
->setOrder('oldest')
1368
->execute();
1369
1370
// Sort the diffs into two buckets: those older than your last action
1371
// and those newer than your last action.
1372
$older = array();
1373
$newer = array();
1374
foreach ($updates as $update) {
1375
// If you updated the revision with "arc diff", try to count that
1376
// update as "before your last action".
1377
if ($update->getDateCreated() <= $xaction->getDateCreated()) {
1378
$older[] = $update->getNewValue();
1379
} else {
1380
$newer[] = $update->getNewValue();
1381
}
1382
}
1383
1384
if (!$newer) {
1385
$this->warnings[] = id(new PHUIInfoView())
1386
->setTitle(pht('No Recent Updates'))
1387
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
1388
->appendChild(
1389
pht(
1390
'Showing all changes because the diff for this revision '.
1391
'has not been updated since your last action.'));
1392
} else {
1393
$older = array_fuse($older);
1394
1395
// Find the most recent diff from before the last action.
1396
$old = null;
1397
foreach ($diffs as $diff) {
1398
if (!isset($older[$diff->getPHID()])) {
1399
break;
1400
}
1401
1402
$old = $diff;
1403
}
1404
1405
// It's possible we may not find such a diff: transactions may have
1406
// been removed from the database, for example. If we miss, just
1407
// fail into some reasonable state since 404'ing would be perplexing.
1408
if ($old) {
1409
$this->warnings[] = id(new PHUIInfoView())
1410
->setTitle(pht('New Changes Shown'))
1411
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
1412
->appendChild(
1413
pht(
1414
'Showing changes since the last action you took on this '.
1415
'revision.'));
1416
1417
$old_id = $old->getID();
1418
}
1419
}
1420
}
1421
}
1422
1423
if (isset($diffs[$old_id])) {
1424
return $old_id;
1425
}
1426
1427
return null;
1428
}
1429
1430
private function getNewDiffID(DifferentialRevision $revision, array $diffs) {
1431
assert_instances_of($diffs, 'DifferentialDiff');
1432
$request = $this->getRequest();
1433
1434
$diffs = mpull($diffs, null, 'getID');
1435
1436
$is_new = ($request->getURIData('filter') === 'new');
1437
$new_id = $request->getInt('id');
1438
1439
if ($is_new && $new_id) {
1440
return new Aphront404Response();
1441
}
1442
1443
if (isset($diffs[$new_id])) {
1444
return $new_id;
1445
}
1446
1447
return (int)last_key($diffs);
1448
}
1449
1450
}
1451
1452