@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 DiffusionBrowseController extends DiffusionController {
4
5 private $lintCommit;
6 private $lintMessages;
7 private $corpusButtons = array();
8
9 public function shouldAllowPublic() {
10 return true;
11 }
12
13 public function handleRequest(AphrontRequest $request) {
14 $response = $this->loadDiffusionContext();
15 if ($response) {
16 return $response;
17 }
18
19 $drequest = $this->getDiffusionRequest();
20
21 // Figure out if we're browsing a directory, a file, or a search result
22 // list.
23
24 $grep = $request->getStr('grep');
25 if (phutil_nonempty_string($grep)) {
26 return $this->browseSearch();
27 }
28
29 $pager = id(new PHUIPagerView())
30 ->readFromRequest($request);
31
32 $results = DiffusionBrowseResultSet::newFromConduit(
33 $this->callConduitWithDiffusionRequest(
34 'diffusion.browsequery',
35 array(
36 'path' => $drequest->getPath(),
37 'commit' => $drequest->getStableCommit(),
38 'offset' => $pager->getOffset(),
39 'limit' => $pager->getPageSize() + 1,
40 )));
41
42 $reason = $results->getReasonForEmptyResultSet();
43 $is_file = ($reason == DiffusionBrowseResultSet::REASON_IS_FILE);
44
45 if ($is_file) {
46 return $this->browseFile();
47 }
48
49 $paths = $results->getPaths();
50 $paths = $pager->sliceResults($paths);
51 $results->setPaths($paths);
52
53 return $this->browseDirectory($results, $pager);
54 }
55
56 private function browseSearch() {
57 $drequest = $this->getDiffusionRequest();
58 $header = $this->buildHeaderView($drequest);
59 $path = nonempty(basename($drequest->getPath()), '/');
60
61 $search_results = $this->renderSearchResults();
62 $search_form = $this->renderSearchForm($path);
63
64 $search_form = phutil_tag(
65 'div',
66 array(
67 'class' => 'diffusion-mobile-search-form',
68 ),
69 $search_form);
70
71 $crumbs = $this->buildCrumbs(
72 array(
73 'branch' => true,
74 'path' => true,
75 'view' => 'browse',
76 ));
77 $crumbs->setBorder(true);
78
79 $tabs = $this->buildTabsView('code');
80
81 $view = id(new PHUITwoColumnView())
82 ->setHeader($header)
83 ->setTabs($tabs)
84 ->setFooter(
85 array(
86 $search_form,
87 $search_results,
88 ));
89
90 return $this->newPage()
91 ->setTitle(
92 array(
93 nonempty(basename($drequest->getPath()), '/'),
94 $drequest->getRepository()->getDisplayName(),
95 ))
96 ->setCrumbs($crumbs)
97 ->appendChild($view);
98 }
99
100 private function browseFile() {
101 $viewer = $this->getViewer();
102 $request = $this->getRequest();
103 $drequest = $this->getDiffusionRequest();
104 $repository = $drequest->getRepository();
105
106 $before = $request->getStr('before');
107 if ($before) {
108 return $this->buildBeforeResponse($before);
109 }
110
111 $path = $drequest->getPath();
112 $params = array(
113 'commit' => $drequest->getCommit(),
114 'path' => $drequest->getPath(),
115 );
116
117 $view = $request->getStr('view');
118
119 $byte_limit = null;
120 if ($view !== 'raw') {
121 $byte_limit = PhabricatorFileStorageEngine::getChunkThreshold();
122 $time_limit = 10;
123
124 $params += array(
125 'timeout' => $time_limit,
126 'byteLimit' => $byte_limit,
127 );
128 }
129
130 $response = $this->callConduitWithDiffusionRequest(
131 'diffusion.filecontentquery',
132 $params);
133
134 $hit_byte_limit = $response['tooHuge'];
135 $hit_time_limit = $response['tooSlow'];
136
137 $file_phid = $response['filePHID'];
138 $show_editor = false;
139 if ($hit_byte_limit) {
140 $corpus = $this->buildErrorCorpus(
141 pht(
142 'This file is larger than %s bytes, and too large to display '.
143 'in the web UI.',
144 phutil_format_bytes($byte_limit)));
145 } else if ($hit_time_limit) {
146 $corpus = $this->buildErrorCorpus(
147 pht(
148 'This file took too long to load from the repository (more than '.
149 '%s second(s)).',
150 new PhutilNumber($time_limit)));
151 } else {
152 $file = id(new PhabricatorFileQuery())
153 ->setViewer($viewer)
154 ->withPHIDs(array($file_phid))
155 ->executeOne();
156 if (!$file) {
157 throw new Exception(pht('Failed to load content file!'));
158 }
159
160 if ($view === 'raw') {
161 return $file->getRedirectResponse();
162 }
163
164 $data = $file->loadFileData();
165
166 $lfs_ref = $this->getGitLFSRef($repository, $data);
167 if ($lfs_ref) {
168 if ($view == 'git-lfs') {
169 $file = $this->loadGitLFSFile($lfs_ref);
170
171 // Rename the file locally so we generate a better vanity URI for
172 // it. In storage, it just has a name like "lfs-13f9a94c0923...",
173 // since we don't get any hints about possible human-readable names
174 // at upload time.
175 $basename = basename($drequest->getPath());
176 $file->makeEphemeral();
177 $file->setName($basename);
178
179 return $file->getRedirectResponse();
180 }
181
182 $corpus = $this->buildGitLFSCorpus($lfs_ref);
183 } else {
184 $show_editor = true;
185
186 $ref = id(new PhabricatorDocumentRef())
187 ->setFile($file);
188
189 $engine = id(new DiffusionDocumentRenderingEngine())
190 ->setRequest($request)
191 ->setDiffusionRequest($drequest);
192
193 $corpus = $engine->newDocumentView($ref);
194
195 $this->corpusButtons[] = $this->renderFileButton();
196 }
197 }
198
199 $bar = $this->buildButtonBar($drequest, $show_editor);
200 $header = $this->buildHeaderView($drequest);
201 $header->setHeaderIcon('fa-file-code-o');
202
203 $follow = $request->getStr('follow');
204 $follow_notice = null;
205 if ($follow) {
206 $follow_notice = id(new PHUIInfoView())
207 ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
208 ->setTitle(pht('Unable to Continue'));
209 switch ($follow) {
210 case 'first':
211 $follow_notice->appendChild(
212 pht(
213 'Unable to continue tracing the history of this file because '.
214 'this commit is the first commit in the repository.'));
215 break;
216 case 'created':
217 $follow_notice->appendChild(
218 pht(
219 'Unable to continue tracing the history of this file because '.
220 'this commit created the file.'));
221 break;
222 }
223 }
224
225 $renamed = $request->getStr('renamed');
226 $renamed_notice = null;
227 if ($renamed) {
228 $renamed_notice = id(new PHUIInfoView())
229 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
230 ->setTitle(pht('File Renamed'))
231 ->appendChild(
232 pht(
233 'File history passes through a rename from "%s" to "%s".',
234 $drequest->getPath(),
235 $renamed));
236 }
237
238 $open_revisions = $this->buildOpenRevisions();
239 $owners_list = $this->buildOwnersList($drequest);
240
241 $crumbs = $this->buildCrumbs(
242 array(
243 'branch' => true,
244 'path' => true,
245 'view' => 'browse',
246 ));
247 $crumbs->setBorder(true);
248
249 $basename = basename($this->getDiffusionRequest()->getPath());
250 $tabs = $this->buildTabsView('code');
251 $bar->setRight($this->corpusButtons);
252
253 $view = id(new PHUITwoColumnView())
254 ->setHeader($header)
255 ->setTabs($tabs)
256 ->setFooter(array(
257 $bar,
258 $follow_notice,
259 $renamed_notice,
260 $corpus,
261 $open_revisions,
262 $owners_list,
263 ));
264
265 $title = array($basename, $repository->getDisplayName());
266
267 return $this->newPage()
268 ->setTitle($title)
269 ->setCrumbs($crumbs)
270 ->appendChild(
271 array(
272 $view,
273 ));
274
275 }
276
277 public function browseDirectory(
278 DiffusionBrowseResultSet $results,
279 PHUIPagerView $pager) {
280
281 $request = $this->getRequest();
282 $drequest = $this->getDiffusionRequest();
283 $repository = $drequest->getRepository();
284
285 $reason = $results->getReasonForEmptyResultSet();
286
287 $this->buildActionButtons($drequest, true);
288 $details = $this->buildPropertyView($drequest);
289
290 $header = $this->buildHeaderView($drequest);
291 $header->setHeaderIcon('fa-folder-open');
292
293 $empty_result = null;
294 $browse_panel = null;
295 if (!$results->isValidResults()) {
296 $empty_result = new DiffusionEmptyResultView();
297 $empty_result->setDiffusionRequest($drequest);
298 $empty_result->setDiffusionBrowseResultSet($results);
299 $empty_result->setView($request->getStr('view'));
300 } else {
301 $browse_table = id(new DiffusionBrowseTableView())
302 ->setDiffusionRequest($drequest)
303 ->setPaths($results->getPaths())
304 ->setUser($request->getUser());
305
306 $title = nonempty(basename($drequest->getPath()), '/');
307 $icon = 'fa-folder-open';
308 $browse_header = $this->buildPanelHeaderView($title, $icon);
309
310 $browse_panel = id(new PHUIObjectBoxView())
311 ->setHeader($browse_header)
312 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
313 ->setTable($browse_table)
314 ->addClass('diffusion-mobile-view')
315 ->setPager($pager);
316 }
317
318 $open_revisions = $this->buildOpenRevisions();
319 $readme = $this->renderDirectoryReadme($results);
320
321 $crumbs = $this->buildCrumbs(
322 array(
323 'branch' => true,
324 'path' => true,
325 'view' => 'browse',
326 ));
327
328 $crumbs->setBorder(true);
329 $locate_file = $this->buildLocateFile();
330 $tabs = $this->buildTabsView('code');
331 $owners_list = $this->buildOwnersList($drequest);
332 $bar = id(new PHUILeftRightView())
333 ->setLeft($locate_file)
334 ->setRight($this->corpusButtons)
335 ->addClass('diffusion-action-bar');
336
337 $view = id(new PHUITwoColumnView())
338 ->setHeader($header)
339 ->setTabs($tabs)
340 ->setFooter(
341 array(
342 $bar,
343 $empty_result,
344 $browse_panel,
345 $open_revisions,
346 $owners_list,
347 $readme,
348 ));
349
350 if ($details) {
351 $view->addPropertySection(pht('Details'), $details);
352 }
353
354 return $this->newPage()
355 ->setTitle(array(
356 nonempty(basename($drequest->getPath()), '/'),
357 $repository->getDisplayName(),
358 ))
359 ->setCrumbs($crumbs)
360 ->appendChild(
361 array(
362 $view,
363 ));
364 }
365
366 private function renderSearchResults() {
367 $request = $this->getRequest();
368
369 $drequest = $this->getDiffusionRequest();
370 $repository = $drequest->getRepository();
371 $results = array();
372
373 $pager = id(new PHUIPagerView())
374 ->readFromRequest($request);
375
376 $search_mode = null;
377 switch ($repository->getVersionControlSystem()) {
378 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
379 $results = array();
380 break;
381 default:
382 if (strlen($this->getRequest()->getStr('grep'))) {
383 $search_mode = 'grep';
384 $query_string = $request->getStr('grep');
385 $results = $this->callConduitWithDiffusionRequest(
386 'diffusion.searchquery',
387 array(
388 'grep' => $query_string,
389 'commit' => $drequest->getStableCommit(),
390 'path' => $drequest->getPath(),
391 'limit' => $pager->getPageSize() + 1,
392 'offset' => $pager->getOffset(),
393 ));
394 }
395 break;
396 }
397 $results = $pager->sliceResults($results);
398
399 $table = null;
400 $header = null;
401 if ($search_mode == 'grep') {
402 $table = $this->renderGrepResults($results, $query_string);
403 $title = pht(
404 'File content matching "%s" under "%s"',
405 $query_string,
406 nonempty($drequest->getPath(), '/'));
407 $header = id(new PHUIHeaderView())
408 ->setHeader($title)
409 ->addClass('diffusion-search-result-header');
410 }
411
412 return array($header, $table, $pager);
413
414 }
415
416 private function renderGrepResults(array $results, $pattern) {
417 $drequest = $this->getDiffusionRequest();
418 require_celerity_resource('phabricator-search-results-css');
419
420 if (!$results) {
421 return id(new PHUIInfoView())
422 ->setSeverity(PHUIInfoView::SEVERITY_NODATA)
423 ->appendChild(
424 pht(
425 'The pattern you searched for was not found in the content of any '.
426 'files.'));
427 }
428
429 $grouped = array();
430 foreach ($results as $file) {
431 list($path, $line, $string) = $file;
432 $grouped[$path][] = array($line, $string);
433 }
434
435 $view = array();
436 foreach ($grouped as $path => $matches) {
437 $view[] = id(new DiffusionPatternSearchView())
438 ->setPath($path)
439 ->setMatches($matches)
440 ->setPattern($pattern)
441 ->setDiffusionRequest($drequest)
442 ->render();
443 }
444
445 return $view;
446 }
447
448 private function buildButtonBar(
449 DiffusionRequest $drequest,
450 $show_editor) {
451
452 $viewer = $this->getViewer();
453 $base_uri = $this->getRequest()->getRequestURI();
454
455 $repository = $drequest->getRepository();
456 $path = $drequest->getPath();
457 $line = nonempty((int)$drequest->getLine(), 1);
458 $buttons = array();
459
460 $editor_uri = null;
461 $editor_template = null;
462
463 $link_engine = PhabricatorEditorURIEngine::newForViewer($viewer);
464 if ($link_engine) {
465 $link_engine->setRepository($repository);
466
467 $editor_uri = $link_engine->getURIForPath($path, $line);
468 $editor_template = $link_engine->getURITokensForPath($path);
469 }
470
471 $buttons[] =
472 id(new PHUIButtonView())
473 ->setTag('a')
474 ->setText(pht('Last Change'))
475 ->setColor(PHUIButtonView::GREY)
476 ->setHref(
477 $drequest->generateURI(
478 array(
479 'action' => 'change',
480 )))
481 ->setIcon('fa-backward');
482
483 if ($editor_uri) {
484 $buttons[] =
485 id(new PHUIButtonView())
486 ->setTag('a')
487 ->setText(pht('Open File'))
488 ->setHref($editor_uri)
489 ->setIcon('fa-pencil')
490 ->setID('editor_link')
491 ->setMetadata(array('template' => $editor_template))
492 ->setDisabled(!$editor_uri)
493 ->setColor(PHUIButtonView::GREY);
494 }
495
496 $bar = id(new PHUILeftRightView())
497 ->setLeft($buttons)
498 ->addClass('diffusion-action-bar full-mobile-buttons');
499 return $bar;
500 }
501
502 private function buildOwnersList(DiffusionRequest $drequest) {
503 $viewer = $this->getViewer();
504
505 $have_owners = PhabricatorApplication::isClassInstalledForViewer(
506 PhabricatorOwnersApplication::class,
507 $viewer);
508 if (!$have_owners) {
509 return null;
510 }
511
512 $repository = $drequest->getRepository();
513
514 $package_query = id(new PhabricatorOwnersPackageQuery())
515 ->setViewer($viewer)
516 ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
517 ->withControl(
518 $repository->getPHID(),
519 array(
520 $drequest->getPath(),
521 ));
522
523 $package_query->execute();
524
525 $packages = $package_query->getControllingPackagesForPath(
526 $repository->getPHID(),
527 $drequest->getPath());
528
529 $ownership = id(new PHUIObjectItemListView())
530 ->setUser($viewer)
531 ->setNoDataString(pht('No Owners'));
532
533 if ($packages) {
534 foreach ($packages as $package) {
535 $item = id(new PHUIObjectItemView())
536 ->setObject($package)
537 ->setObjectName($package->getMonogram())
538 ->setHeader($package->getName())
539 ->setHref($package->getURI());
540
541 $owners = $package->getOwners();
542 if ($owners) {
543 $owner_list = $viewer->renderHandleList(
544 mpull($owners, 'getUserPHID'));
545 } else {
546 $owner_list = phutil_tag('em', array(), pht('None'));
547 }
548 $item->addAttribute(pht('Owners: %s', $owner_list));
549
550 $auto = $package->getAutoReview();
551 $autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
552 $spec = idx($autoreview_map, $auto, array());
553 $name = idx($spec, 'name', $auto);
554 $item->addIcon('fa-code', $name);
555
556 $rule = $package->newAuditingRule();
557 $item->addIcon($rule->getIconIcon(), $rule->getDisplayName());
558
559 if ($package->isArchived()) {
560 $item->setDisabled(true);
561 }
562
563 $ownership->addItem($item);
564 }
565 }
566
567 $view = id(new PHUIObjectBoxView())
568 ->setHeaderText(pht('Owner Packages'))
569 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
570 ->addClass('diffusion-mobile-view')
571 ->setObjectList($ownership);
572
573 return $view;
574 }
575
576 private function renderFileButton($file_uri = null, $label = null) {
577
578 $base_uri = $this->getRequest()->getRequestURI();
579
580 if ($file_uri) {
581 $text = pht('Download File');
582 $href = $file_uri;
583 $icon = 'fa-download';
584 } else {
585 $text = pht('Raw File');
586 $href = $base_uri->alter('view', 'raw');
587 $icon = 'fa-file-text';
588 }
589
590 if ($label !== null) {
591 $text = $label;
592 }
593
594 $button = id(new PHUIButtonView())
595 ->setTag('a')
596 ->setText($text)
597 ->setHref($href)
598 ->setIcon($icon)
599 ->setColor(PHUIButtonView::GREY);
600
601 return $button;
602 }
603
604 private function renderGitLFSButton() {
605 $viewer = $this->getViewer();
606
607 $uri = $this->getRequest()->getRequestURI();
608 $href = $uri->alter('view', 'git-lfs');
609
610 $text = pht('Download from Git LFS');
611 $icon = 'fa-download';
612
613 return id(new PHUIButtonView())
614 ->setTag('a')
615 ->setText($text)
616 ->setHref($href)
617 ->setIcon($icon)
618 ->setColor(PHUIButtonView::GREY);
619 }
620
621 private function buildErrorCorpus($message) {
622 $text = id(new PHUIBoxView())
623 ->addPadding(PHUI::PADDING_LARGE)
624 ->appendChild($message);
625
626 $header = id(new PHUIHeaderView())
627 ->setHeader(pht('Details'));
628
629 $box = id(new PHUIObjectBoxView())
630 ->setHeader($header)
631 ->appendChild($text);
632
633 return $box;
634 }
635
636 private function buildBeforeResponse($before) {
637 $request = $this->getRequest();
638 $drequest = $this->getDiffusionRequest();
639
640 // NOTE: We need to get the grandparent so we can capture filename changes
641 // in the parent.
642
643 $parent = $this->loadParentCommitOf($before);
644 $old_filename = null;
645 $was_created = false;
646 if ($parent) {
647 $grandparent = $this->loadParentCommitOf($parent);
648
649 if ($grandparent) {
650 $rename_query = new DiffusionRenameHistoryQuery();
651 $rename_query->setRequest($drequest);
652 $rename_query->setOldCommit($grandparent);
653 $rename_query->setViewer($request->getUser());
654 $old_filename = $rename_query->loadOldFilename();
655 $was_created = $rename_query->getWasCreated();
656 }
657 }
658
659 $follow = null;
660 if ($was_created) {
661 // If the file was created in history, that means older commits won't
662 // have it. Since we know it existed at 'before', it must have been
663 // created then; jump there.
664 $target_commit = $before;
665 $follow = 'created';
666 } else if ($parent) {
667 // If we found a parent, jump to it. This is the normal case.
668 $target_commit = $parent;
669 } else {
670 // If there's no parent, this was probably created in the initial commit?
671 // And the "was_created" check will fail because we can't identify the
672 // grandparent. Keep the user at 'before'.
673 $target_commit = $before;
674 $follow = 'first';
675 }
676
677 $path = $drequest->getPath();
678 $renamed = null;
679 if ($old_filename !== null &&
680 $old_filename !== '/'.$path) {
681 $renamed = $path;
682 $path = $old_filename;
683 }
684
685 $line = null;
686 // If there's a follow error, drop the line so the user sees the message.
687 if (!$follow) {
688 $line = $this->getBeforeLineNumber($target_commit);
689 }
690
691 $before_uri = $drequest->generateURI(
692 array(
693 'action' => 'browse',
694 'commit' => $target_commit,
695 'line' => $line,
696 'path' => $path,
697 ));
698
699 if ($renamed === null) {
700 $before_uri->removeQueryParam('renamed');
701 } else {
702 $before_uri->replaceQueryParam('renamed', $renamed);
703 }
704
705 if ($follow === null) {
706 $before_uri->removeQueryParam('follow');
707 } else {
708 $before_uri->replaceQueryParam('follow', $follow);
709 }
710
711 return id(new AphrontRedirectResponse())->setURI($before_uri);
712 }
713
714 private function getBeforeLineNumber($target_commit) {
715 $drequest = $this->getDiffusionRequest();
716 $viewer = $this->getViewer();
717
718 $line = $drequest->getLine();
719 if (!$line) {
720 return null;
721 }
722
723 $diff_info = $this->callConduitWithDiffusionRequest(
724 'diffusion.rawdiffquery',
725 array(
726 'commit' => $drequest->getCommit(),
727 'path' => $drequest->getPath(),
728 'againstCommit' => $target_commit,
729 ));
730
731 $file_phid = $diff_info['filePHID'];
732 $file = id(new PhabricatorFileQuery())
733 ->setViewer($viewer)
734 ->withPHIDs(array($file_phid))
735 ->executeOne();
736 if (!$file) {
737 throw new Exception(
738 pht(
739 'Failed to load file ("%s") returned by "%s".',
740 $file_phid,
741 'diffusion.rawdiffquery.'));
742 }
743
744 $raw_diff = $file->loadFileData();
745
746 $old_line = 0;
747 $new_line = 0;
748
749 foreach (explode("\n", $raw_diff) as $text) {
750 if ($text[0] == '-' || $text[0] == ' ') {
751 $old_line++;
752 }
753 if ($text[0] == '+' || $text[0] == ' ') {
754 $new_line++;
755 }
756 if ($new_line == $line) {
757 return $old_line;
758 }
759 }
760
761 // We didn't find the target line.
762 return $line;
763 }
764
765 private function loadParentCommitOf($commit) {
766 $drequest = $this->getDiffusionRequest();
767 $user = $this->getRequest()->getUser();
768
769 $before_req = DiffusionRequest::newFromDictionary(
770 array(
771 'user' => $user,
772 'repository' => $drequest->getRepository(),
773 'commit' => $commit,
774 ));
775
776 $parents = DiffusionQuery::callConduitWithDiffusionRequest(
777 $user,
778 $before_req,
779 'diffusion.commitparentsquery',
780 array(
781 'commit' => $commit,
782 ));
783
784 return head($parents);
785 }
786
787 protected function markupText($text) {
788 $engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine();
789 $engine->setConfig('viewer', $this->getRequest()->getUser());
790 $text = $engine->markupText($text);
791
792 $text = phutil_tag(
793 'div',
794 array(
795 'class' => 'phabricator-remarkup',
796 ),
797 $text);
798
799 return $text;
800 }
801
802 protected function buildHeaderView(DiffusionRequest $drequest) {
803 $viewer = $this->getViewer();
804 $repository = $drequest->getRepository();
805
806 $commit_tag = $this->renderCommitHashTag($drequest);
807
808 $path = nonempty($drequest->getPath(), '/');
809
810 $search = $this->renderSearchForm($path);
811
812 $header = id(new PHUIHeaderView())
813 ->setUser($viewer)
814 ->setHeader($this->renderPathLinks($drequest, $mode = 'browse'))
815 ->addActionItem($search)
816 ->addTag($commit_tag)
817 ->addClass('diffusion-browse-header');
818
819 if (!$repository->isSVN()) {
820 $branch_tag = $this->renderBranchTag($drequest);
821 $header->addTag($branch_tag);
822 }
823
824 return $header;
825 }
826
827 protected function buildPanelHeaderView($title, $icon) {
828
829 $header = id(new PHUIHeaderView())
830 ->setHeader($title)
831 ->setHeaderIcon($icon)
832 ->addClass('diffusion-panel-header-view');
833
834 return $header;
835
836 }
837
838 protected function buildActionButtons(
839 DiffusionRequest $drequest,
840 $is_directory = false) {
841
842 $viewer = $this->getViewer();
843 $repository = $drequest->getRepository();
844 $history_uri = $drequest->generateURI(array('action' => 'history'));
845 $behind_head = $drequest->getSymbolicCommit();
846 $compare = null;
847 $head_uri = $drequest->generateURI(
848 array(
849 'commit' => '',
850 'action' => 'browse',
851 ));
852
853 if ($repository->supportsBranchComparison() && $is_directory) {
854 $compare_uri = $drequest->generateURI(array('action' => 'compare'));
855 $compare = id(new PHUIButtonView())
856 ->setText(pht('Compare'))
857 ->setIcon('fa-code-fork')
858 ->setWorkflow(true)
859 ->setTag('a')
860 ->setHref($compare_uri)
861 ->setColor(PHUIButtonView::GREY);
862 $this->corpusButtons[] = $compare;
863 }
864
865 $head = null;
866 if ($behind_head) {
867 $head = id(new PHUIButtonView())
868 ->setTag('a')
869 ->setText(pht('Back to HEAD'))
870 ->setHref($head_uri)
871 ->setIcon('fa-home')
872 ->setColor(PHUIButtonView::GREY);
873 $this->corpusButtons[] = $head;
874 }
875
876 $history = id(new PHUIButtonView())
877 ->setText(pht('History'))
878 ->setHref($history_uri)
879 ->setTag('a')
880 ->setIcon('fa-history')
881 ->setColor(PHUIButtonView::GREY);
882 $this->corpusButtons[] = $history;
883
884 }
885
886 protected function buildPropertyView(
887 DiffusionRequest $drequest) {
888
889 $viewer = $this->getViewer();
890 $view = id(new PHUIPropertyListView())
891 ->setUser($viewer);
892
893 if ($drequest->getSymbolicType() == 'tag') {
894 $symbolic = $drequest->getSymbolicCommit();
895 $view->addProperty(pht('Tag'), $symbolic);
896
897 $tags = $this->callConduitWithDiffusionRequest(
898 'diffusion.tagsquery',
899 array(
900 'names' => array($symbolic),
901 'needMessages' => true,
902 ));
903 $tags = DiffusionRepositoryTag::newFromConduit($tags);
904
905 $tags = mpull($tags, null, 'getName');
906 $tag = idx($tags, $symbolic);
907
908 if ($tag && strlen($tag->getMessage())) {
909 $view->addSectionHeader(
910 pht('Tag Content'), 'fa-tag');
911 $view->addTextContent($this->markupText($tag->getMessage()));
912 }
913 }
914
915 if ($view->hasAnyProperties()) {
916 return $view;
917 }
918
919 return null;
920 }
921
922 private function buildOpenRevisions() {
923 $viewer = $this->getViewer();
924
925 $drequest = $this->getDiffusionRequest();
926 $repository = $drequest->getRepository();
927 $path = $drequest->getPath();
928
929 $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds'));
930
931 $revisions = id(new DifferentialRevisionQuery())
932 ->setViewer($viewer)
933 ->withPaths(array($path))
934 ->withRepositoryPHIDs(array($repository->getPHID()))
935 ->withIsOpen(true)
936 ->withUpdatedEpochBetween($recent, null)
937 ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED)
938 ->setLimit(10)
939 ->needReviewers(true)
940 ->needFlags(true)
941 ->needDrafts(true)
942 ->execute();
943
944 if (!$revisions) {
945 return null;
946 }
947
948 $header = id(new PHUIHeaderView())
949 ->setHeader(pht('Recent Open Revisions'));
950
951 $list = id(new DifferentialRevisionListView())
952 ->setViewer($viewer)
953 ->setRevisions($revisions)
954 ->setNoBox(true);
955
956 $view = id(new PHUIObjectBoxView())
957 ->setHeader($header)
958 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
959 ->addClass('diffusion-mobile-view')
960 ->appendChild($list);
961
962 return $view;
963 }
964
965 private function getGitLFSRef(PhabricatorRepository $repository, $data) {
966 if (!$repository->canUseGitLFS()) {
967 return null;
968 }
969
970 $lfs_pattern = '(^version https://git-lfs\\.github\\.com/spec/v1[\r\n])';
971 if (!preg_match($lfs_pattern, $data)) {
972 return null;
973 }
974
975 $matches = null;
976 if (!preg_match('(^oid sha256:(.*)$)m', $data, $matches)) {
977 return null;
978 }
979
980 $hash = $matches[1];
981 $hash = trim($hash);
982
983 return id(new PhabricatorRepositoryGitLFSRefQuery())
984 ->setViewer($this->getViewer())
985 ->withRepositoryPHIDs(array($repository->getPHID()))
986 ->withObjectHashes(array($hash))
987 ->executeOne();
988 }
989
990 private function buildGitLFSCorpus(PhabricatorRepositoryGitLFSRef $ref) {
991 // TODO: We should probably test if we can load the file PHID here and
992 // show the user an error if we can't, rather than making them click
993 // through to hit an error.
994
995 $title = basename($this->getDiffusionRequest()->getPath());
996 $icon = 'fa-archive';
997 $drequest = $this->getDiffusionRequest();
998 $this->buildActionButtons($drequest);
999 $header = $this->buildPanelHeaderView($title, $icon);
1000
1001 $severity = PHUIInfoView::SEVERITY_NOTICE;
1002
1003 $messages = array();
1004 $messages[] = pht(
1005 'This %s file is stored in Git Large File Storage.',
1006 phutil_format_bytes($ref->getByteSize()));
1007
1008 try {
1009 $file = $this->loadGitLFSFile($ref);
1010 $this->corpusButtons[] = $this->renderGitLFSButton();
1011 } catch (Exception $ex) {
1012 $severity = PHUIInfoView::SEVERITY_ERROR;
1013 $messages[] = pht('The data for this file could not be loaded.');
1014 }
1015
1016 $this->corpusButtons[] = $this->renderFileButton(
1017 null, pht('View Raw LFS Pointer'));
1018
1019 $corpus = id(new PHUIObjectBoxView())
1020 ->setHeader($header)
1021 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
1022 ->addClass('diffusion-mobile-view')
1023 ->setCollapsed(true)
1024 ->setInfoView(
1025 id(new PHUIInfoView())
1026 ->setSeverity($severity)
1027 ->setErrors($messages));
1028
1029 return $corpus;
1030 }
1031
1032 private function loadGitLFSFile(PhabricatorRepositoryGitLFSRef $ref) {
1033 $viewer = $this->getViewer();
1034
1035 $file = id(new PhabricatorFileQuery())
1036 ->setViewer($viewer)
1037 ->withPHIDs(array($ref->getFilePHID()))
1038 ->executeOne();
1039 if (!$file) {
1040 throw new Exception(
1041 pht(
1042 'Failed to load file object for Git LFS ref "%s"!',
1043 $ref->getObjectHash()));
1044 }
1045
1046 return $file;
1047 }
1048
1049}