@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

Make "Highlight As..." sticky across reloads in Diffusion and Differential

Summary:
Ref T13455. Add container-level storage for persistent view state, and persist "Highlight As..." inside it.

The storage generates a "PhabricatorChangesetViewState" configuration object as an output.

When preferences are expressed on a diff and that diff is later attached to a revision, we attempt to copy the preferences.

The internal storage tracks per-changeset settings, but currently always uses "last update wins" to apply the settings in the UI.

Test Plan:
- Viewed revisions, changed highlighting, reloaded. Saw highlighting stick in revision view and standalone view.
- Viewed commits, changed highlighting, reloaded. Saw highlighting stick.
- Created a diff, changed highlighting, turned it into a revision, saw highlighting persist.

Subscribers: jmeador, PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13455

Differential Revision: https://secure.phabricator.com/D21137

+469 -45
+9
resources/sql/autopatches/20200417.viewstate.01.storage.sql
··· 1 + CREATE TABLE {$NAMESPACE}_differential.differential_viewstate ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + viewerPHID VARBINARY(64) NOT NULL, 4 + objectPHID VARBINARY(64) NOT NULL, 5 + viewState LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, 6 + dateCreated INT UNSIGNED NOT NULL, 7 + dateModified INT UNSIGNED NOT NULL, 8 + UNIQUE KEY `key_viewer` (viewerPHID, objectPHID) 9 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+11
src/__phutil_library_map__.php
··· 713 713 'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php', 714 714 'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php', 715 715 'DifferentialUpdateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php', 716 + 'DifferentialViewState' => 'applications/differential/storage/DifferentialViewState.php', 717 + 'DifferentialViewStateQuery' => 'applications/differential/query/DifferentialViewStateQuery.php', 716 718 'DiffusionAuditorDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorDatasource.php', 717 719 'DiffusionAuditorFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorFunctionDatasource.php', 718 720 'DiffusionAuditorsAddAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php', ··· 2735 2737 'PhabricatorChangePasswordUserLogType' => 'applications/people/userlog/PhabricatorChangePasswordUserLogType.php', 2736 2738 'PhabricatorChangesetCachePurger' => 'applications/cache/purger/PhabricatorChangesetCachePurger.php', 2737 2739 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 2740 + 'PhabricatorChangesetViewState' => 'infrastructure/diff/viewstate/PhabricatorChangesetViewState.php', 2741 + 'PhabricatorChangesetViewStateEngine' => 'infrastructure/diff/viewstate/PhabricatorChangesetViewStateEngine.php', 2738 2742 'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php', 2739 2743 'PhabricatorChartDataQuery' => 'applications/fact/chart/PhabricatorChartDataQuery.php', 2740 2744 'PhabricatorChartDataset' => 'applications/fact/chart/PhabricatorChartDataset.php', ··· 6783 6787 'DifferentialUnitStatus' => 'Phobject', 6784 6788 'DifferentialUnitTestResult' => 'Phobject', 6785 6789 'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod', 6790 + 'DifferentialViewState' => array( 6791 + 'DifferentialDAO', 6792 + 'PhabricatorPolicyInterface', 6793 + ), 6794 + 'DifferentialViewStateQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 6786 6795 'DiffusionAuditorDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 6787 6796 'DiffusionAuditorFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 6788 6797 'DiffusionAuditorsAddAuditorsHeraldAction' => 'DiffusionAuditorsHeraldAction', ··· 9130 9139 'PhabricatorChangePasswordUserLogType' => 'PhabricatorUserLogType', 9131 9140 'PhabricatorChangesetCachePurger' => 'PhabricatorCachePurger', 9132 9141 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 9142 + 'PhabricatorChangesetViewState' => 'Phobject', 9143 + 'PhabricatorChangesetViewStateEngine' => 'Phobject', 9133 9144 'PhabricatorChartAxis' => 'Phobject', 9134 9145 'PhabricatorChartDataQuery' => 'Phobject', 9135 9146 'PhabricatorChartDataset' => 'Phobject',
+8 -5
src/applications/differential/__tests__/DifferentialParseRenderTestCase.php
··· 89 89 $engine = new PhabricatorMarkupEngine(); 90 90 $engine->setViewer(new PhabricatorUser()); 91 91 92 + $viewstate = new PhabricatorChangesetViewState(); 93 + 92 94 $parsers = array(); 93 95 foreach ($changesets as $changeset) { 94 - $cparser = new DifferentialChangesetParser(); 95 - $cparser->setUser(new PhabricatorUser()); 96 - $cparser->setDisableCache(true); 97 - $cparser->setChangeset($changeset); 98 - $cparser->setMarkupEngine($engine); 96 + $cparser = id(new DifferentialChangesetParser()) 97 + ->setViewer(new PhabricatorUser()) 98 + ->setDisableCache(true) 99 + ->setChangeset($changeset) 100 + ->setMarkupEngine($engine) 101 + ->setViewState($viewstate); 99 102 100 103 if ($type == 'one') { 101 104 $cparser->setRenderer(new DifferentialChangesetOneUpTestRenderer());
+30 -16
src/applications/differential/controller/DifferentialChangesetViewController.php
··· 165 165 list($range_s, $range_e, $mask) = 166 166 DifferentialChangesetParser::parseRangeSpecification($spec); 167 167 168 - $parser = id(new DifferentialChangesetParser()) 169 - ->setViewer($viewer) 170 - ->setCoverage($coverage) 171 - ->setChangeset($changeset) 172 - ->setRenderingReference($rendering_reference) 173 - ->setRenderCacheKey($render_cache_key) 174 - ->setRightSideCommentMapping($right_source, $right_new) 175 - ->setLeftSideCommentMapping($left_source, $left_new); 176 - 177 - $parser->readParametersFromRequest($request); 178 - 179 - if ($left && $right) { 180 - $parser->setOriginals($left, $right); 181 - } 182 - 183 168 $diff = $changeset->getDiff(); 184 169 $revision_id = $diff->getRevisionID(); 185 170 ··· 195 180 $can_mark = ($revision->getAuthorPHID() == $viewer->getPHID()); 196 181 $object_owner_phid = $revision->getAuthorPHID(); 197 182 } 183 + } 184 + 185 + if ($revision) { 186 + $container_phid = $revision->getPHID(); 187 + } else { 188 + $container_phid = $diff->getPHID(); 189 + } 190 + 191 + $viewstate_engine = id(new PhabricatorChangesetViewStateEngine()) 192 + ->setViewer($viewer) 193 + ->setObjectPHID($container_phid) 194 + ->setChangeset($changeset); 195 + 196 + $viewstate = $viewstate_engine->newViewStateFromRequest($request); 197 + 198 + $parser = id(new DifferentialChangesetParser()) 199 + ->setViewer($viewer) 200 + ->setViewState($viewstate) 201 + ->setCoverage($coverage) 202 + ->setChangeset($changeset) 203 + ->setRenderingReference($rendering_reference) 204 + ->setRenderCacheKey($render_cache_key) 205 + ->setRightSideCommentMapping($right_source, $right_new) 206 + ->setLeftSideCommentMapping($left_source, $left_new); 207 + 208 + $parser->readParametersFromRequest($request); 209 + 210 + if ($left && $right) { 211 + $parser->setOriginals($left, $right); 198 212 } 199 213 200 214 // Load both left-side and right-side inline comments. ··· 249 263 $engine->process(); 250 264 251 265 $parser 252 - ->setUser($viewer) 266 + ->setViewer($viewer) 253 267 ->setMarkupEngine($engine) 254 268 ->setShowEditAndReplyLinks(true) 255 269 ->setCanMarkDone($can_mark)
+12 -18
src/applications/differential/parser/DifferentialChangesetParser.php
··· 45 45 private $disableCache; 46 46 private $renderer; 47 47 private $characterEncoding; 48 - private $highlightAs; 49 48 private $highlightingDisabled; 50 49 private $showEditAndReplyLinks = true; 51 50 private $canMarkDone; ··· 60 59 private $highlightEngine; 61 60 private $viewer; 62 61 private $documentEngineKey; 62 + 63 + private $viewState; 63 64 64 65 public function setRange($start, $end) { 65 66 $this->rangeStart = $start; ··· 85 86 return $this->showEditAndReplyLinks; 86 87 } 87 88 88 - public function setHighlightAs($highlight_as) { 89 - $this->highlightAs = $highlight_as; 89 + public function setViewState(PhabricatorChangesetViewState $view_state) { 90 + $this->viewState = $view_state; 90 91 return $this; 91 92 } 92 93 93 - public function getHighlightAs() { 94 - return $this->highlightAs; 94 + public function getViewState() { 95 + return $this->viewState; 95 96 } 96 97 97 98 public function setCharacterEncoding($character_encoding) { ··· 183 184 184 185 public function readParametersFromRequest(AphrontRequest $request) { 185 186 $this->setCharacterEncoding($request->getStr('encoding')); 186 - $this->setHighlightAs($request->getStr('highlight')); 187 187 $this->setDocumentEngineKey($request->getStr('engine')); 188 188 189 189 $renderer = null; ··· 378 378 return $this; 379 379 } 380 380 381 - public function setUser(PhabricatorUser $user) { 382 - $this->user = $user; 383 - return $this; 384 - } 385 - 386 - public function getUser() { 387 - return $this->user; 388 - } 389 - 390 381 public function setCoverage($coverage) { 391 382 $this->coverage = $coverage; 392 383 return $this; ··· 604 595 } 605 596 606 597 private function getHighlightFuture($corpus) { 607 - $language = $this->highlightAs; 598 + $language = $this->getViewState()->getHighlightLanguage(); 608 599 609 600 if (!$language) { 610 601 $language = $this->highlightEngine->getLanguageFromFilename( ··· 634 625 } 635 626 636 627 private function tryCacheStuff() { 628 + $viewstate = $this->getViewState(); 629 + 637 630 $skip_cache = false; 638 631 639 632 if ($this->disableCache) { ··· 644 637 $skip_cache = true; 645 638 } 646 639 647 - if ($this->highlightAs) { 640 + $highlight_language = $viewstate->getHighlightLanguage(); 641 + if ($highlight_language !== null) { 648 642 $skip_cache = true; 649 643 } 650 644 ··· 844 838 count($this->new)); 845 839 846 840 $renderer = $this->getRenderer() 847 - ->setUser($this->getUser()) 841 + ->setUser($this->getViewer()) 848 842 ->setChangeset($this->changeset) 849 843 ->setRenderPropertyChangeHeader($render_pch) 850 844 ->setIsTopLevel($this->isTopLevel)
+64
src/applications/differential/query/DifferentialViewStateQuery.php
··· 1 + <?php 2 + 3 + final class DifferentialViewStateQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $viewerPHIDs; 8 + private $objectPHIDs; 9 + 10 + public function withIDs(array $ids) { 11 + $this->ids = $ids; 12 + return $this; 13 + } 14 + 15 + public function withViewerPHIDs(array $phids) { 16 + $this->viewerPHIDs = $phids; 17 + return $this; 18 + } 19 + 20 + public function withObjectPHIDs(array $phids) { 21 + $this->objectPHIDs = $phids; 22 + return $this; 23 + } 24 + 25 + public function newResultObject() { 26 + return new DifferentialViewState(); 27 + } 28 + 29 + protected function loadPage() { 30 + return $this->loadStandardPage($this->newResultObject()); 31 + } 32 + 33 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 34 + $where = parent::buildWhereClauseParts($conn); 35 + 36 + if ($this->ids !== null) { 37 + $where[] = qsprintf( 38 + $conn, 39 + 'id IN (%Ld)', 40 + $this->ids); 41 + } 42 + 43 + if ($this->viewerPHIDs !== null) { 44 + $where[] = qsprintf( 45 + $conn, 46 + 'viewerPHID IN (%Ls)', 47 + $this->viewerPHIDs); 48 + } 49 + 50 + if ($this->objectPHIDs !== null) { 51 + $where[] = qsprintf( 52 + $conn, 53 + 'objectPHID IN (%Ls)', 54 + $this->objectPHIDs); 55 + } 56 + 57 + return $where; 58 + } 59 + 60 + public function getQueryApplicationClass() { 61 + return 'PhabricatorDifferentialApplication'; 62 + } 63 + 64 + }
+9
src/applications/differential/storage/DifferentialDiff.php
··· 716 716 public function destroyObjectPermanently( 717 717 PhabricatorDestructionEngine $engine) { 718 718 719 + $viewer = $engine->getViewer(); 720 + 719 721 $this->openTransaction(); 720 722 $this->delete(); 721 723 ··· 728 730 $this->getID()); 729 731 foreach ($properties as $prop) { 730 732 $prop->delete(); 733 + } 734 + 735 + $viewstates = id(new DifferentialViewStateQuery()) 736 + ->setViewer($viewer) 737 + ->withObjectPHIDs(array($this->getPHID())); 738 + foreach ($viewstates as $viewstate) { 739 + $viewstate->delete(); 731 740 } 732 741 733 742 $this->saveTransaction();
+10 -1
src/applications/differential/storage/DifferentialRevision.php
··· 1002 1002 public function destroyObjectPermanently( 1003 1003 PhabricatorDestructionEngine $engine) { 1004 1004 1005 + $viewer = $engine->getViewer(); 1006 + 1005 1007 $this->openTransaction(); 1006 1008 $diffs = id(new DifferentialDiffQuery()) 1007 - ->setViewer($engine->getViewer()) 1009 + ->setViewer($viewer) 1008 1010 ->withRevisionIDs(array($this->getID())) 1009 1011 ->execute(); 1010 1012 foreach ($diffs as $diff) { ··· 1021 1023 'DELETE FROM %T WHERE revisionID = %d', 1022 1024 $dummy_path->getTableName(), 1023 1025 $this->getID()); 1026 + 1027 + $viewstates = id(new DifferentialViewStateQuery()) 1028 + ->setViewer($viewer) 1029 + ->withObjectPHIDs(array($this->getPHID())); 1030 + foreach ($viewstates as $viewstate) { 1031 + $viewstate->delete(); 1032 + } 1024 1033 1025 1034 $this->delete(); 1026 1035 $this->saveTransaction();
+132
src/applications/differential/storage/DifferentialViewState.php
··· 1 + <?php 2 + 3 + final class DifferentialViewState 4 + extends DifferentialDAO 5 + implements PhabricatorPolicyInterface { 6 + 7 + protected $viewerPHID; 8 + protected $objectPHID; 9 + protected $viewState = array(); 10 + 11 + private $hasModifications; 12 + 13 + protected function getConfiguration() { 14 + return array( 15 + self::CONFIG_SERIALIZATION => array( 16 + 'viewState' => self::SERIALIZATION_JSON, 17 + ), 18 + self::CONFIG_KEY_SCHEMA => array( 19 + 'key_viewer' => array( 20 + 'columns' => array('viewerPHID', 'objectPHID'), 21 + 'unique' => true, 22 + ), 23 + 'key_object' => array( 24 + 'columns' => array('objectPHID'), 25 + ), 26 + ), 27 + ) + parent::getConfiguration(); 28 + } 29 + 30 + public function setChangesetProperty( 31 + DifferentialChangeset $changeset, 32 + $key, 33 + $value) { 34 + 35 + if ($this->getChangesetProperty($changeset, $key) === $value) { 36 + return; 37 + } 38 + 39 + $properties = array( 40 + 'value' => $value, 41 + 'epoch' => PhabricatorTime::getNow(), 42 + ); 43 + 44 + $diff_id = $changeset->getDiffID(); 45 + if ($diff_id !== null) { 46 + $properties['diffID'] = (int)$diff_id; 47 + } 48 + 49 + $path_hash = $this->getChangesetPathHash($changeset); 50 + $changeset_phid = $this->getChangesetKey($changeset); 51 + 52 + $this->hasModifications = true; 53 + 54 + $this->viewState['changesets'][$path_hash][$key][$changeset_phid] = 55 + $properties; 56 + } 57 + 58 + public function getChangesetProperty( 59 + DifferentialChangeset $changeset, 60 + $key, 61 + $default = null) { 62 + 63 + $path_hash = $this->getChangesetPathHash($changeset); 64 + 65 + $entries = idxv($this->viewState, array('changesets', $path_hash, $key)); 66 + if (!is_array($entries)) { 67 + $entries = array(); 68 + } 69 + 70 + $entries = isort($entries, 'epoch'); 71 + 72 + $entry = last($entries); 73 + if (!is_array($entry)) { 74 + $entry = array(); 75 + } 76 + 77 + return idx($entry, 'value', $default); 78 + } 79 + 80 + public function getHasModifications() { 81 + return $this->hasModifications; 82 + } 83 + 84 + private function getChangesetPathHash(DifferentialChangeset $changeset) { 85 + $path = $changeset->getFilename(); 86 + return PhabricatorHash::digestForIndex($path); 87 + } 88 + 89 + private function getChangesetKey(DifferentialChangeset $changeset) { 90 + $key = $changeset->getID(); 91 + 92 + if ($key === null) { 93 + return '*'; 94 + } 95 + 96 + return (string)$key; 97 + } 98 + 99 + public static function copyViewStatesToObject($src_phid, $dst_phid) { 100 + $table = new self(); 101 + $conn = $table->establishConnection('w'); 102 + 103 + queryfx( 104 + $conn, 105 + 'INSERT IGNORE INTO %R 106 + (viewerPHID, objectPHID, viewState, dateCreated, dateModified) 107 + SELECT viewerPHID, %s, viewState, dateCreated, dateModified 108 + FROM %R WHERE objectPHID = %s', 109 + $table, 110 + $dst_phid, 111 + $table, 112 + $src_phid); 113 + } 114 + 115 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 116 + 117 + 118 + public function getCapabilities() { 119 + return array( 120 + PhabricatorPolicyCapability::CAN_VIEW, 121 + ); 122 + } 123 + 124 + public function getPolicy($capability) { 125 + return PhabricatorPolicies::POLICY_NOONE; 126 + } 127 + 128 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 129 + return ($viewer->getPHID() === $this->getViewerPHID()); 130 + } 131 + 132 + }
+8
src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php
··· 100 100 HarbormasterMessageType::BUILDABLE_CONTAINER, 101 101 true); 102 102 } 103 + 104 + // See T13455. If users have set view properites on a diff and the diff 105 + // is then attached to a revision, attempt to copy their view preferences 106 + // to the revision. 107 + 108 + DifferentialViewState::copyViewStatesToObject( 109 + $diff->getPHID(), 110 + $object->getPHID()); 103 111 } 104 112 105 113 public function getColor() {
+14 -5
src/applications/diffusion/controller/DiffusionDiffController.php
··· 60 60 return new Aphront404Response(); 61 61 } 62 62 63 - $parser = new DifferentialChangesetParser(); 64 - $parser->setUser($viewer); 65 - $parser->setChangeset($changeset); 63 + $commit = $drequest->loadCommit(); 64 + 65 + $viewstate_engine = id(new PhabricatorChangesetViewStateEngine()) 66 + ->setViewer($viewer) 67 + ->setObjectPHID($commit->getPHID()) 68 + ->setChangeset($changeset); 69 + 70 + $viewstate = $viewstate_engine->newViewStateFromRequest($request); 71 + 72 + $parser = id(new DifferentialChangesetParser()) 73 + ->setViewer($viewer) 74 + ->setChangeset($changeset) 75 + ->setViewState($viewstate); 76 + 66 77 $parser->setRenderingReference($drequest->generateURI( 67 78 array( 68 79 'action' => 'rendering-ref', ··· 74 85 if ($coverage) { 75 86 $parser->setCoverage($coverage); 76 87 } 77 - 78 - $commit = $drequest->loadCommit(); 79 88 80 89 $pquery = new DiffusionPathIDQuery(array($changeset->getFilename())); 81 90 $ids = $pquery->loadPathIDs();
+17
src/infrastructure/diff/viewstate/PhabricatorChangesetViewState.php
··· 1 + <?php 2 + 3 + final class PhabricatorChangesetViewState 4 + extends Phobject { 5 + 6 + private $highlightLanguage; 7 + 8 + public function setHighlightLanguage($highlight_language) { 9 + $this->highlightLanguage = $highlight_language; 10 + return $this; 11 + } 12 + 13 + public function getHighlightLanguage() { 14 + return $this->highlightLanguage; 15 + } 16 + 17 + }
+145
src/infrastructure/diff/viewstate/PhabricatorChangesetViewStateEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorChangesetViewStateEngine 4 + extends Phobject { 5 + 6 + private $viewer; 7 + private $objectPHID; 8 + private $changeset; 9 + private $storage; 10 + 11 + public function setViewer(PhabricatorUser $viewer) { 12 + $this->viewer = $viewer; 13 + return $this; 14 + } 15 + 16 + public function getViewer() { 17 + return $this->viewer; 18 + } 19 + 20 + public function setObjectPHID($object_phid) { 21 + $this->objectPHID = $object_phid; 22 + return $this; 23 + } 24 + 25 + public function getObjectPHID() { 26 + return $this->objectPHID; 27 + } 28 + 29 + public function setChangeset(DifferentialChangeset $changeset) { 30 + $this->changeset = $changeset; 31 + return $this; 32 + } 33 + 34 + public function getChangeset() { 35 + return $this->changeset; 36 + } 37 + 38 + public function newViewStateFromRequest(AphrontRequest $request) { 39 + $storage = $this->loadViewStateStorage(); 40 + 41 + $this->setStorage($storage); 42 + 43 + $highlight = $request->getStr('highlight'); 44 + if ($highlight !== null && strlen($highlight)) { 45 + $this->setChangesetProperty('highlight', $highlight); 46 + } 47 + 48 + $this->saveViewStateStorage(); 49 + 50 + $state = new PhabricatorChangesetViewState(); 51 + 52 + $highlight_language = $this->getChangesetProperty('highlight'); 53 + $state->setHighlightLanguage($highlight_language); 54 + 55 + return $state; 56 + } 57 + 58 + private function setStorage(DifferentialViewState $storage) { 59 + $this->storage = $storage; 60 + return $this; 61 + } 62 + 63 + private function getStorage() { 64 + return $this->storage; 65 + } 66 + 67 + private function setChangesetProperty( 68 + $key, 69 + $value) { 70 + 71 + $storage = $this->getStorage(); 72 + $changeset = $this->getChangeset(); 73 + 74 + $storage->setChangesetProperty($changeset, $key, $value); 75 + } 76 + 77 + private function getChangesetProperty( 78 + $key, 79 + $default = null) { 80 + 81 + $storage = $this->getStorage(); 82 + $changeset = $this->getChangeset(); 83 + 84 + return $storage->getChangesetProperty($changeset, $key, $default); 85 + } 86 + 87 + private function loadViewStateStorage() { 88 + $viewer = $this->getViewer(); 89 + 90 + $object_phid = $this->getObjectPHID(); 91 + $viewer_phid = $viewer->getPHID(); 92 + 93 + $storage = null; 94 + 95 + if ($viewer_phid !== null) { 96 + $storage = id(new DifferentialViewStateQuery()) 97 + ->setViewer($viewer) 98 + ->withViewerPHIDs(array($viewer_phid)) 99 + ->withObjectPHIDs(array($object_phid)) 100 + ->executeOne(); 101 + } 102 + 103 + if ($storage === null) { 104 + $storage = id(new DifferentialViewState()) 105 + ->setObjectPHID($object_phid); 106 + 107 + if ($viewer_phid !== null) { 108 + $storage->setViewerPHID($viewer_phid); 109 + } else { 110 + $storage->makeEphemeral(); 111 + } 112 + } 113 + 114 + return $storage; 115 + } 116 + 117 + private function saveViewStateStorage() { 118 + if (PhabricatorEnv::isReadOnly()) { 119 + return; 120 + } 121 + 122 + $storage = $this->getStorage(); 123 + 124 + $viewer_phid = $storage->getViewerPHID(); 125 + if ($viewer_phid === null) { 126 + return; 127 + } 128 + 129 + if (!$storage->getHasModifications()) { 130 + return; 131 + } 132 + 133 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 134 + 135 + try { 136 + $storage->save(); 137 + } catch (AphrontDuplicateKeyQueryException $ex) { 138 + // We may race another process to save view state. For now, just discard 139 + // our state if we do. 140 + } 141 + 142 + unset($unguarded); 143 + } 144 + 145 + }