@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class DifferentialChangesetViewController extends DifferentialController {
4
5 public function shouldAllowPublic() {
6 return true;
7 }
8
9 public function handleRequest(AphrontRequest $request) {
10 $viewer = $this->getViewer();
11
12 $rendering_reference = $request->getStr('ref');
13 $parts = explode('/', $rendering_reference);
14 if (count($parts) == 2) {
15 list($id, $vs) = $parts;
16 } else {
17 $id = $parts[0];
18 $vs = 0;
19 }
20
21 $id = (int)$id;
22 $vs = (int)$vs;
23
24 $load_ids = array($id);
25 if ($vs && ($vs != -1)) {
26 $load_ids[] = $vs;
27 }
28
29 $changesets = id(new DifferentialChangesetQuery())
30 ->setViewer($viewer)
31 ->withIDs($load_ids)
32 ->needHunks(true)
33 ->execute();
34 $changesets = mpull($changesets, null, 'getID');
35
36 $changeset = idx($changesets, $id);
37 if (!$changeset) {
38 return new Aphront404Response();
39 }
40
41 $vs_changeset = null;
42 if ($vs && ($vs != -1)) {
43 $vs_changeset = idx($changesets, $vs);
44 if (!$vs_changeset) {
45 return new Aphront404Response();
46 }
47 }
48
49 $view = $request->getStr('view');
50 if ($view) {
51 $phid = idx($changeset->getMetadata(), "$view:binary-phid");
52 if ($phid) {
53 return id(new AphrontRedirectResponse())->setURI("/file/info/$phid/");
54 }
55 switch ($view) {
56 case 'new':
57 return $this->buildRawFileResponse($changeset, $is_new = true);
58 case 'old':
59 if ($vs_changeset) {
60 return $this->buildRawFileResponse($vs_changeset, $is_new = true);
61 }
62 return $this->buildRawFileResponse($changeset, $is_new = false);
63 default:
64 return new Aphront400Response();
65 }
66 }
67
68 $old = array();
69 $new = array();
70 if (!$vs) {
71 $right = $changeset;
72 $left = null;
73
74 $right_source = $right->getID();
75 $right_new = true;
76 $left_source = $right->getID();
77 $left_new = false;
78
79 $render_cache_key = $right->getID();
80
81 $old[] = $changeset;
82 $new[] = $changeset;
83 } else if ($vs == -1) {
84 $right = null;
85 $left = $changeset;
86
87 $right_source = $left->getID();
88 $right_new = false;
89 $left_source = $left->getID();
90 $left_new = true;
91
92 $render_cache_key = null;
93
94 $old[] = $changeset;
95 $new[] = $changeset;
96 } else {
97 $right = $changeset;
98 $left = $vs_changeset;
99
100 $right_source = $right->getID();
101 $right_new = true;
102 $left_source = $left->getID();
103 $left_new = true;
104
105 $render_cache_key = null;
106
107 $new[] = $left;
108 $new[] = $right;
109 }
110
111 if ($left) {
112 $changeset = $left->newComparisonChangeset($right);
113 }
114
115 if ($left_new || $right_new) {
116 $diff_map = array();
117 if ($left) {
118 $diff_map[] = $left->getDiff();
119 }
120 if ($right) {
121 $diff_map[] = $right->getDiff();
122 }
123 $diff_map = mpull($diff_map, null, 'getPHID');
124
125 $buildables = id(new HarbormasterBuildableQuery())
126 ->setViewer($viewer)
127 ->withBuildablePHIDs(array_keys($diff_map))
128 ->withManualBuildables(false)
129 ->needBuilds(true)
130 ->needTargets(true)
131 ->execute();
132 $buildables = mpull($buildables, null, 'getBuildablePHID');
133 foreach ($diff_map as $diff_phid => $changeset_diff) {
134 $changeset_diff->attachBuildable(idx($buildables, $diff_phid));
135 }
136 }
137
138 $coverage = null;
139 if ($right_new) {
140 $coverage = $this->loadCoverage($right);
141 }
142
143 $spec = $request->getStr('range');
144 list($range_s, $range_e, $mask) =
145 DifferentialChangesetParser::parseRangeSpecification($spec);
146
147 $diff = $changeset->getDiff();
148 $revision_id = $diff->getRevisionID();
149
150 $can_mark = false;
151 $object_owner_phid = null;
152 $revision = null;
153 if ($revision_id) {
154 $revision = id(new DifferentialRevisionQuery())
155 ->setViewer($viewer)
156 ->withIDs(array($revision_id))
157 ->executeOne();
158 if ($revision) {
159 $can_mark = ($revision->getAuthorPHID() == $viewer->getPHID());
160 $object_owner_phid = $revision->getAuthorPHID();
161 }
162 }
163
164 if ($revision) {
165 $container_phid = $revision->getPHID();
166 } else {
167 $container_phid = $diff->getPHID();
168 }
169
170 $viewstate_engine = id(new PhabricatorChangesetViewStateEngine())
171 ->setViewer($viewer)
172 ->setObjectPHID($container_phid)
173 ->setChangeset($changeset);
174
175 $viewstate = $viewstate_engine->newViewStateFromRequest($request);
176
177 if ($viewstate->getDiscardResponse()) {
178 return new AphrontAjaxResponse();
179 }
180
181 $parser = id(new DifferentialChangesetParser())
182 ->setViewer($viewer)
183 ->setViewState($viewstate)
184 ->setCoverage($coverage)
185 ->setChangeset($changeset)
186 ->setRenderingReference($rendering_reference)
187 ->setRenderCacheKey($render_cache_key)
188 ->setRightSideCommentMapping($right_source, $right_new)
189 ->setLeftSideCommentMapping($left_source, $left_new);
190
191 if ($left && $right) {
192 $parser->setOriginals($left, $right);
193 }
194
195 // Load both left-side and right-side inline comments.
196 if ($revision) {
197 $inlines = id(new DifferentialDiffInlineCommentQuery())
198 ->setViewer($viewer)
199 ->withRevisionPHIDs(array($revision->getPHID()))
200 ->withPublishableComments(true)
201 ->withPublishedComments(true)
202 ->needHidden(true)
203 ->needInlineContext(true)
204 ->execute();
205
206 $inlines = mpull($inlines, 'newInlineCommentObject');
207
208 $inlines = id(new PhabricatorInlineCommentAdjustmentEngine())
209 ->setViewer($viewer)
210 ->setRevision($revision)
211 ->setOldChangesets($old)
212 ->setNewChangesets($new)
213 ->setInlines($inlines)
214 ->execute();
215 } else {
216 $inlines = array();
217 }
218
219 if ($left_new) {
220 $inlines = array_merge(
221 $inlines,
222 $this->buildLintInlineComments($left));
223 }
224
225 if ($right_new) {
226 $inlines = array_merge(
227 $inlines,
228 $this->buildLintInlineComments($right));
229 }
230
231 $phids = array();
232 foreach ($inlines as $inline) {
233 $parser->parseInlineComment($inline);
234 if ($inline->getAuthorPHID()) {
235 $phids[$inline->getAuthorPHID()] = true;
236 }
237 }
238 $phids = array_keys($phids);
239
240 $handles = $this->loadViewerHandles($phids);
241 $parser->setHandles($handles);
242
243 $engine = new PhabricatorMarkupEngine();
244 $engine->setViewer($viewer);
245
246 foreach ($inlines as $inline) {
247 $engine->addObject(
248 $inline,
249 PhabricatorInlineComment::MARKUP_FIELD_BODY);
250 }
251
252 $engine->process();
253
254 $parser
255 ->setViewer($viewer)
256 ->setMarkupEngine($engine)
257 ->setShowEditAndReplyLinks(true)
258 ->setCanMarkDone($can_mark)
259 ->setObjectOwnerPHID($object_owner_phid)
260 ->setRange($range_s, $range_e)
261 ->setMask($mask);
262
263 if ($request->isAjax()) {
264 // NOTE: We must render the changeset before we render coverage
265 // information, since it builds some caches.
266 $response = $parser->newChangesetResponse();
267
268 $mcov = $parser->renderModifiedCoverage();
269
270 $coverage_data = array(
271 'differential-mcoverage-'.md5($changeset->getFilename()) => $mcov,
272 );
273
274 $response->setCoverage($coverage_data);
275
276 return $response;
277 }
278
279 $detail = id(new DifferentialChangesetListView())
280 ->setUser($this->getViewer())
281 ->setChangesets(array($changeset))
282 ->setVisibleChangesets(array($changeset))
283 ->setRenderingReferences(array($rendering_reference))
284 ->setRenderURI('/differential/changeset/')
285 ->setDiff($diff)
286 ->setTitle(pht('Standalone View'))
287 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
288 ->setIsStandalone(true)
289 ->setParser($parser);
290
291 if ($revision_id) {
292 $detail->setInlineCommentControllerURI(
293 '/differential/comment/inline/edit/'.$revision_id.'/');
294 }
295
296 $crumbs = $this->buildApplicationCrumbs();
297
298 if ($revision_id) {
299 $crumbs->addTextCrumb('D'.$revision_id, '/D'.$revision_id);
300 }
301
302 $diff_id = $diff->getID();
303 if ($diff_id) {
304 $crumbs->addTextCrumb(
305 pht('Diff %d', $diff_id),
306 $this->getApplicationURI('diff/'.$diff_id));
307 }
308
309 $crumbs->addTextCrumb($changeset->getDisplayFilename());
310 $crumbs->setBorder(true);
311
312 $header = id(new PHUIHeaderView())
313 ->setHeader(pht('Changeset View'))
314 ->setHeaderIcon('fa-gear');
315
316 $view = id(new PHUITwoColumnView())
317 ->setHeader($header)
318 ->setFooter($detail);
319
320 return $this->newPage()
321 ->setTitle(pht('Changeset View'))
322 ->setCrumbs($crumbs)
323 ->appendChild($view);
324 }
325
326 private function buildRawFileResponse(
327 DifferentialChangeset $changeset,
328 $is_new) {
329
330 $viewer = $this->getViewer();
331
332 if ($is_new) {
333 $key = 'raw:new:phid';
334 } else {
335 $key = 'raw:old:phid';
336 }
337
338 $metadata = $changeset->getMetadata();
339
340 $file = null;
341 $phid = idx($metadata, $key);
342 if ($phid) {
343 $file = id(new PhabricatorFileQuery())
344 ->setViewer($viewer)
345 ->withPHIDs(array($phid))
346 ->execute();
347 if ($file) {
348 $file = head($file);
349 }
350 }
351
352 if (!$file) {
353 // This is just building a cache of the changeset content in the file
354 // tool, and is safe to run on a read pathway.
355 $unguard = AphrontWriteGuard::beginScopedUnguardedWrites();
356
357 if ($is_new) {
358 $data = $changeset->makeNewFile();
359 } else {
360 $data = $changeset->makeOldFile();
361 }
362
363 $diff = $changeset->getDiff();
364
365 $file = PhabricatorFile::newFromFileData(
366 $data,
367 array(
368 'name' => $changeset->getFilename(),
369 'mime-type' => 'text/plain',
370 'ttl.relative' => phutil_units('24 hours in seconds'),
371 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
372 ));
373
374 $file->attachToObject($diff->getPHID());
375
376 $metadata[$key] = $file->getPHID();
377 $changeset->setMetadata($metadata);
378 $changeset->save();
379
380 unset($unguard);
381 }
382
383 return $file->getRedirectResponse();
384 }
385
386 private function buildLintInlineComments($changeset) {
387 $diff = $changeset->getDiff();
388
389 $target_phids = $diff->getBuildTargetPHIDs();
390 if (!$target_phids) {
391 return array();
392 }
393
394 $messages = id(new HarbormasterBuildLintMessage())->loadAllWhere(
395 'buildTargetPHID IN (%Ls) AND path = %s',
396 $target_phids,
397 $changeset->getFilename());
398
399 if (!$messages) {
400 return array();
401 }
402
403 $change_type = $changeset->getChangeType();
404 if (DifferentialChangeType::isDeleteChangeType($change_type)) {
405 // If this is a lint message on a deleted file, show it on the left
406 // side of the UI because there are no source code lines on the right
407 // side of the UI so inlines don't have anywhere to render. See PHI416.
408 $is_new = 0;
409 } else {
410 $is_new = 1;
411 }
412
413 $template = id(new DifferentialInlineComment())
414 ->setChangesetID($changeset->getID())
415 ->setIsNewFile($is_new)
416 ->setLineLength(0);
417
418 $inlines = array();
419 foreach ($messages as $message) {
420 $description = $message->getProperty('description');
421
422 $inlines[] = id(clone $template)
423 ->setSyntheticAuthor(pht('Lint: %s', $message->getName()))
424 ->setLineNumber($message->getLine())
425 ->setContent($description);
426 }
427
428 return $inlines;
429 }
430
431 private function loadCoverage(DifferentialChangeset $changeset) {
432 $viewer = $this->getViewer();
433
434 $target_phids = $changeset->getDiff()->getBuildTargetPHIDs();
435 if (!$target_phids) {
436 return null;
437 }
438
439 $unit = id(new HarbormasterBuildUnitMessageQuery())
440 ->setViewer($viewer)
441 ->withBuildTargetPHIDs($target_phids)
442 ->execute();
443 if (!$unit) {
444 return null;
445 }
446
447 $coverage = array();
448 foreach ($unit as $message) {
449 $test_coverage = $message->getProperty('coverage');
450 if ($test_coverage === null) {
451 continue;
452 }
453 $coverage_data = idx($test_coverage, $changeset->getFileName());
454 if (phutil_nonempty_string($coverage_data)) {
455 $coverage[] = $coverage_data;
456 }
457 }
458
459 if (!$coverage) {
460 return null;
461 }
462
463 return ArcanistUnitTestResult::mergeCoverage($coverage);
464 }
465
466}