mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 08:42:41 +01:00
(stable) Promote 2019 Week 39
This commit is contained in:
commit
256507a14e
53 changed files with 3103 additions and 487 deletions
|
@ -9,10 +9,10 @@ return array(
|
|||
'names' => array(
|
||||
'conpherence.pkg.css' => '3c8a0668',
|
||||
'conpherence.pkg.js' => '020aebcf',
|
||||
'core.pkg.css' => 'c69171e6',
|
||||
'core.pkg.js' => '73a06a9f',
|
||||
'differential.pkg.css' => '8d8360fb',
|
||||
'differential.pkg.js' => '0b037a4f',
|
||||
'core.pkg.css' => '7ce5a944',
|
||||
'core.pkg.js' => '6e5c894f',
|
||||
'differential.pkg.css' => '607c84be',
|
||||
'differential.pkg.js' => 'a0212a0b',
|
||||
'diffusion.pkg.css' => '42c75c37',
|
||||
'diffusion.pkg.js' => 'a98c0bf7',
|
||||
'maniphest.pkg.css' => '35995d6d',
|
||||
|
@ -61,7 +61,7 @@ return array(
|
|||
'rsrc/css/application/dashboard/dashboard.css' => '5a205b9d',
|
||||
'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d',
|
||||
'rsrc/css/application/differential/add-comment.css' => '7e5900d9',
|
||||
'rsrc/css/application/differential/changeset-view.css' => 'bde53589',
|
||||
'rsrc/css/application/differential/changeset-view.css' => '489b6995',
|
||||
'rsrc/css/application/differential/core.css' => '7300a73e',
|
||||
'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b',
|
||||
'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d',
|
||||
|
@ -113,7 +113,7 @@ return array(
|
|||
'rsrc/css/application/uiexample/example.css' => 'b4795059',
|
||||
'rsrc/css/core/core.css' => '1b29ed61',
|
||||
'rsrc/css/core/remarkup.css' => 'f06cc20e',
|
||||
'rsrc/css/core/syntax.css' => '4234f572',
|
||||
'rsrc/css/core/syntax.css' => '220b85f9',
|
||||
'rsrc/css/core/z-index.css' => '99c0f5eb',
|
||||
'rsrc/css/diviner/diviner-shared.css' => '4bd263b0',
|
||||
'rsrc/css/font/font-awesome.css' => '3883938a',
|
||||
|
@ -169,7 +169,7 @@ return array(
|
|||
'rsrc/css/phui/phui-pager.css' => 'd022c7ad',
|
||||
'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8',
|
||||
'rsrc/css/phui/phui-policy-section-view.css' => '139fdc64',
|
||||
'rsrc/css/phui/phui-property-list-view.css' => 'cad62236',
|
||||
'rsrc/css/phui/phui-property-list-view.css' => '807b1632',
|
||||
'rsrc/css/phui/phui-remarkup-preview.css' => '91767007',
|
||||
'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370',
|
||||
'rsrc/css/phui/phui-spacing.css' => 'b05cadc3',
|
||||
|
@ -376,8 +376,8 @@ return array(
|
|||
'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => 'a2ab19be',
|
||||
'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9',
|
||||
'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8',
|
||||
'rsrc/js/application/diff/DiffChangeset.js' => 'd0a85a85',
|
||||
'rsrc/js/application/diff/DiffChangesetList.js' => '04023d82',
|
||||
'rsrc/js/application/diff/DiffChangeset.js' => 'a31ffc00',
|
||||
'rsrc/js/application/diff/DiffChangesetList.js' => '0f5c016d',
|
||||
'rsrc/js/application/diff/DiffInline.js' => 'a4a14a94',
|
||||
'rsrc/js/application/diff/behavior-preview-link.js' => 'f51e9c17',
|
||||
'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd',
|
||||
|
@ -506,7 +506,7 @@ return array(
|
|||
'rsrc/js/core/behavior-tokenizer.js' => '3b4899b0',
|
||||
'rsrc/js/core/behavior-tooltip.js' => '73ecc1f8',
|
||||
'rsrc/js/core/behavior-user-menu.js' => '60cd9241',
|
||||
'rsrc/js/core/behavior-watch-anchor.js' => '0e6d261f',
|
||||
'rsrc/js/core/behavior-watch-anchor.js' => '3972dadb',
|
||||
'rsrc/js/core/behavior-workflow.js' => '9623adc1',
|
||||
'rsrc/js/core/darkconsole/DarkLog.js' => '3b869402',
|
||||
'rsrc/js/core/darkconsole/DarkMessage.js' => '26cd4b73',
|
||||
|
@ -554,7 +554,7 @@ return array(
|
|||
'conpherence-thread-manager' => 'aec8e38c',
|
||||
'conpherence-transaction-css' => '3a3f5e7e',
|
||||
'd3' => '9d068042',
|
||||
'differential-changeset-view-css' => 'bde53589',
|
||||
'differential-changeset-view-css' => '489b6995',
|
||||
'differential-core-view-css' => '7300a73e',
|
||||
'differential-revision-add-comment-css' => '7e5900d9',
|
||||
'differential-revision-comment-css' => '7dbc8d1d',
|
||||
|
@ -655,7 +655,7 @@ return array(
|
|||
'javelin-behavior-phabricator-tooltips' => '73ecc1f8',
|
||||
'javelin-behavior-phabricator-transaction-comment-form' => '2bdadf1a',
|
||||
'javelin-behavior-phabricator-transaction-list' => '9cec214e',
|
||||
'javelin-behavior-phabricator-watch-anchor' => '0e6d261f',
|
||||
'javelin-behavior-phabricator-watch-anchor' => '3972dadb',
|
||||
'javelin-behavior-pholio-mock-edit' => '3eed1f2b',
|
||||
'javelin-behavior-pholio-mock-view' => '5aa1544e',
|
||||
'javelin-behavior-phui-dropdown-menu' => '5cf0501a',
|
||||
|
@ -773,8 +773,8 @@ return array(
|
|||
'phabricator-darklog' => '3b869402',
|
||||
'phabricator-darkmessage' => '26cd4b73',
|
||||
'phabricator-dashboard-css' => '5a205b9d',
|
||||
'phabricator-diff-changeset' => 'd0a85a85',
|
||||
'phabricator-diff-changeset-list' => '04023d82',
|
||||
'phabricator-diff-changeset' => 'a31ffc00',
|
||||
'phabricator-diff-changeset-list' => '0f5c016d',
|
||||
'phabricator-diff-inline' => 'a4a14a94',
|
||||
'phabricator-drag-and-drop-file-upload' => '4370900d',
|
||||
'phabricator-draggable-list' => 'c9ad6f70',
|
||||
|
@ -865,7 +865,7 @@ return array(
|
|||
'phui-pager-css' => 'd022c7ad',
|
||||
'phui-pinboard-view-css' => '1f08f5d8',
|
||||
'phui-policy-section-view-css' => '139fdc64',
|
||||
'phui-property-list-view-css' => 'cad62236',
|
||||
'phui-property-list-view-css' => '807b1632',
|
||||
'phui-remarkup-preview-css' => '91767007',
|
||||
'phui-segment-bar-view-css' => '5166b370',
|
||||
'phui-spacing-css' => 'b05cadc3',
|
||||
|
@ -900,7 +900,7 @@ return array(
|
|||
'sprite-login-css' => '18b368a6',
|
||||
'sprite-tokens-css' => 'f1896dc5',
|
||||
'syntax-default-css' => '055fc231',
|
||||
'syntax-highlighting-css' => '4234f572',
|
||||
'syntax-highlighting-css' => '220b85f9',
|
||||
'tokens-css' => 'ce5a50bd',
|
||||
'trigger-rule' => '41b7b4f6',
|
||||
'trigger-rule-control' => '5faf27b9',
|
||||
|
@ -944,10 +944,6 @@ return array(
|
|||
'03e8891f' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'04023d82' => array(
|
||||
'javelin-install',
|
||||
'phuix-button-view',
|
||||
),
|
||||
'04f8a1e3' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
|
@ -999,12 +995,6 @@ return array(
|
|||
'0d2490ce' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'0e6d261f' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
'javelin-vector',
|
||||
),
|
||||
'0eaa33a9' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -1015,6 +1005,10 @@ return array(
|
|||
'javelin-workflow',
|
||||
'phuix-icon-view',
|
||||
),
|
||||
'0f5c016d' => array(
|
||||
'javelin-install',
|
||||
'phuix-button-view',
|
||||
),
|
||||
'111bfd2d' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
|
@ -1075,6 +1069,9 @@ return array(
|
|||
'javelin-behavior',
|
||||
'javelin-request',
|
||||
),
|
||||
'220b85f9' => array(
|
||||
'syntax-default-css',
|
||||
),
|
||||
'225bbb98' => array(
|
||||
'javelin-install',
|
||||
'javelin-reactor',
|
||||
|
@ -1227,6 +1224,12 @@ return array(
|
|||
'javelin-install',
|
||||
'javelin-dom',
|
||||
),
|
||||
'3972dadb' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
'javelin-vector',
|
||||
),
|
||||
'398fdf13' => array(
|
||||
'javelin-behavior',
|
||||
'trigger-rule-editor',
|
||||
|
@ -1260,9 +1263,6 @@ return array(
|
|||
'javelin-behavior',
|
||||
'javelin-uri',
|
||||
),
|
||||
'4234f572' => array(
|
||||
'syntax-default-css',
|
||||
),
|
||||
'4370900d' => array(
|
||||
'javelin-install',
|
||||
'javelin-util',
|
||||
|
@ -1303,6 +1303,9 @@ return array(
|
|||
'javelin-dom',
|
||||
'phabricator-draggable-list',
|
||||
),
|
||||
'489b6995' => array(
|
||||
'phui-inline-comment-view-css',
|
||||
),
|
||||
'48fe33d0' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -1785,6 +1788,17 @@ return array(
|
|||
'javelin-workflow',
|
||||
'phabricator-draggable-list',
|
||||
),
|
||||
'a31ffc00' => array(
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-stratcom',
|
||||
'javelin-install',
|
||||
'javelin-workflow',
|
||||
'javelin-router',
|
||||
'javelin-behavior-device',
|
||||
'javelin-vector',
|
||||
'phabricator-diff-inline',
|
||||
),
|
||||
'a4356cde' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
|
@ -1960,9 +1974,6 @@ return array(
|
|||
'phabricator-drag-and-drop-file-upload',
|
||||
'javelin-workboard-board',
|
||||
),
|
||||
'bde53589' => array(
|
||||
'phui-inline-comment-view-css',
|
||||
),
|
||||
'c03f2fb4' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
|
@ -2034,17 +2045,6 @@ return array(
|
|||
'javelin-dom',
|
||||
'javelin-stratcom',
|
||||
),
|
||||
'd0a85a85' => array(
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-stratcom',
|
||||
'javelin-install',
|
||||
'javelin-workflow',
|
||||
'javelin-router',
|
||||
'javelin-behavior-device',
|
||||
'javelin-vector',
|
||||
'phabricator-diff-inline',
|
||||
),
|
||||
'd12d214f' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_repository.repository_refcursor
|
||||
ADD isPermanent BOOL NOT NULL;
|
|
@ -0,0 +1,2 @@
|
|||
UPDATE {$NAMESPACE}_repository.repository_refcursor
|
||||
SET isPermanent = 1;
|
|
@ -2150,6 +2150,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
|
||||
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
|
||||
'PhabricatorAmazonSNSFuture' => 'applications/metamta/future/PhabricatorAmazonSNSFuture.php',
|
||||
'PhabricatorAnchorTestCase' => 'infrastructure/markup/__tests__/PhabricatorAnchorTestCase.php',
|
||||
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
|
||||
'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php',
|
||||
'PhabricatorAphlictManagementNotifyWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php',
|
||||
|
@ -2267,6 +2268,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAuthChallengeStatusController' => 'applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php',
|
||||
'PhabricatorAuthChallengeUpdate' => 'applications/auth/view/PhabricatorAuthChallengeUpdate.php',
|
||||
'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php',
|
||||
'PhabricatorAuthChangeUsernameMessageType' => 'applications/auth/message/PhabricatorAuthChangeUsernameMessageType.php',
|
||||
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
|
||||
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
|
||||
'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php',
|
||||
|
@ -3136,6 +3138,9 @@ phutil_register_library_map(array(
|
|||
'PhabricatorDividerProfileMenuItem' => 'applications/search/menuitem/PhabricatorDividerProfileMenuItem.php',
|
||||
'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php',
|
||||
'PhabricatorDocumentEngine' => 'applications/files/document/PhabricatorDocumentEngine.php',
|
||||
'PhabricatorDocumentEngineBlock' => 'applications/files/diff/PhabricatorDocumentEngineBlock.php',
|
||||
'PhabricatorDocumentEngineBlockDiff' => 'applications/files/diff/PhabricatorDocumentEngineBlockDiff.php',
|
||||
'PhabricatorDocumentEngineBlocks' => 'applications/files/diff/PhabricatorDocumentEngineBlocks.php',
|
||||
'PhabricatorDocumentRef' => 'applications/files/document/PhabricatorDocumentRef.php',
|
||||
'PhabricatorDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorDocumentRenderingEngine.php',
|
||||
'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php',
|
||||
|
@ -4872,6 +4877,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php',
|
||||
'PhabricatorSystemSelectEncodingController' => 'applications/system/controller/PhabricatorSystemSelectEncodingController.php',
|
||||
'PhabricatorSystemSelectHighlightController' => 'applications/system/controller/PhabricatorSystemSelectHighlightController.php',
|
||||
'PhabricatorSystemSelectViewAsController' => 'applications/system/controller/PhabricatorSystemSelectViewAsController.php',
|
||||
'PhabricatorTOTPAuthFactor' => 'applications/auth/factor/PhabricatorTOTPAuthFactor.php',
|
||||
'PhabricatorTOTPAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php',
|
||||
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
|
||||
|
@ -5594,9 +5600,13 @@ phutil_register_library_map(array(
|
|||
'PhutilOAuthAuthAdapter' => 'applications/auth/adapter/PhutilOAuthAuthAdapter.php',
|
||||
'PhutilPHPCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilPHPCodeSnippetContextFreeGrammar.php',
|
||||
'PhutilPhabricatorAuthAdapter' => 'applications/auth/adapter/PhutilPhabricatorAuthAdapter.php',
|
||||
'PhutilProseDiff' => 'infrastructure/diff/prose/PhutilProseDiff.php',
|
||||
'PhutilProseDiffTestCase' => 'infrastructure/diff/prose/__tests__/PhutilProseDiffTestCase.php',
|
||||
'PhutilProseDifferenceEngine' => 'infrastructure/diff/prose/PhutilProseDifferenceEngine.php',
|
||||
'PhutilQsprintfInterface' => 'infrastructure/storage/xsprintf/PhutilQsprintfInterface.php',
|
||||
'PhutilQueryString' => 'infrastructure/storage/xsprintf/PhutilQueryString.php',
|
||||
'PhutilRealNameContextFreeGrammar' => 'infrastructure/lipsum/PhutilRealNameContextFreeGrammar.php',
|
||||
'PhutilRemarkupAnchorRule' => 'infrastructure/markup/markuprule/PhutilRemarkupAnchorRule.php',
|
||||
'PhutilRemarkupBlockInterpreter' => 'infrastructure/markup/blockrule/PhutilRemarkupBlockInterpreter.php',
|
||||
'PhutilRemarkupBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupBlockRule.php',
|
||||
'PhutilRemarkupBlockStorage' => 'infrastructure/markup/PhutilRemarkupBlockStorage.php',
|
||||
|
@ -8314,6 +8324,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
|
||||
'PhabricatorAmazonSNSFuture' => 'PhutilAWSFuture',
|
||||
'PhabricatorAnchorTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorAnchorView' => 'AphrontView',
|
||||
'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
|
||||
'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow',
|
||||
|
@ -8452,6 +8463,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAuthChallengeStatusController' => 'PhabricatorAuthController',
|
||||
'PhabricatorAuthChallengeUpdate' => 'Phobject',
|
||||
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
|
||||
'PhabricatorAuthChangeUsernameMessageType' => 'PhabricatorAuthMessageType',
|
||||
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
|
||||
'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
|
||||
|
@ -9466,6 +9478,9 @@ phutil_register_library_map(array(
|
|||
'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem',
|
||||
'PhabricatorDivinerApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorDocumentEngine' => 'Phobject',
|
||||
'PhabricatorDocumentEngineBlock' => 'Phobject',
|
||||
'PhabricatorDocumentEngineBlockDiff' => 'Phobject',
|
||||
'PhabricatorDocumentEngineBlocks' => 'Phobject',
|
||||
'PhabricatorDocumentRef' => 'Phobject',
|
||||
'PhabricatorDocumentRenderingEngine' => 'Phobject',
|
||||
'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication',
|
||||
|
@ -11494,6 +11509,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow',
|
||||
'PhabricatorSystemSelectEncodingController' => 'PhabricatorController',
|
||||
'PhabricatorSystemSelectHighlightController' => 'PhabricatorController',
|
||||
'PhabricatorSystemSelectViewAsController' => 'PhabricatorController',
|
||||
'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor',
|
||||
'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
|
||||
|
@ -12387,8 +12403,12 @@ phutil_register_library_map(array(
|
|||
'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter',
|
||||
'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
|
||||
'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilProseDiff' => 'Phobject',
|
||||
'PhutilProseDiffTestCase' => 'PhabricatorTestCase',
|
||||
'PhutilProseDifferenceEngine' => 'Phobject',
|
||||
'PhutilQueryString' => 'Phobject',
|
||||
'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
|
||||
'PhutilRemarkupAnchorRule' => 'PhutilRemarkupRule',
|
||||
'PhutilRemarkupBlockInterpreter' => 'Phobject',
|
||||
'PhutilRemarkupBlockRule' => 'Phobject',
|
||||
'PhutilRemarkupBlockStorage' => 'Phobject',
|
||||
|
|
|
@ -28,7 +28,7 @@ final class Aphront403Response extends AphrontHTMLResponse {
|
|||
$dialog = id(new AphrontDialogView())
|
||||
->setUser($user)
|
||||
->setTitle(pht('403 Forbidden'))
|
||||
->addCancelButton('/', pht('Peace Out'))
|
||||
->addCancelButton('/', pht('Yikes!'))
|
||||
->appendParagraph($forbidden_text);
|
||||
|
||||
$view = id(new PhabricatorStandardPageView())
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthChangeUsernameMessageType
|
||||
extends PhabricatorAuthMessageType {
|
||||
|
||||
const MESSAGEKEY = 'user.edit.username';
|
||||
|
||||
public function getDisplayName() {
|
||||
return pht('Username Change Instructions');
|
||||
}
|
||||
|
||||
public function getShortDescription() {
|
||||
return pht(
|
||||
'Guidance in the "Change Username" dialog for requesting a '.
|
||||
'username change.');
|
||||
}
|
||||
|
||||
public function getFullDescription() {
|
||||
return pht(
|
||||
'When users click the "Change Username" action on their profile pages '.
|
||||
'but do not have the required permissions, they will be presented with '.
|
||||
'a message explaining that they are not authorized to make the edit.'.
|
||||
"\n\n".
|
||||
'You can optionally provide additional instructions here to help users '.
|
||||
'request a username change, if there is someone specific they should '.
|
||||
'contact or a particular workflow they should use.');
|
||||
}
|
||||
|
||||
}
|
|
@ -3,12 +3,18 @@
|
|||
final class PhabricatorConduitLogQuery
|
||||
extends PhabricatorCursorPagedPolicyAwareQuery {
|
||||
|
||||
private $ids;
|
||||
private $callerPHIDs;
|
||||
private $methods;
|
||||
private $methodStatuses;
|
||||
private $epochMin;
|
||||
private $epochMax;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
$this->ids = $ids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withCallerPHIDs(array $phids) {
|
||||
$this->callerPHIDs = $phids;
|
||||
return $this;
|
||||
|
@ -41,6 +47,13 @@ final class PhabricatorConduitLogQuery
|
|||
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
||||
$where = parent::buildWhereClauseParts($conn);
|
||||
|
||||
if ($this->ids !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'id IN (%Ld)',
|
||||
$this->ids);
|
||||
}
|
||||
|
||||
if ($this->callerPHIDs !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
|
|
|
@ -166,6 +166,7 @@ final class DifferentialChangesetViewController extends DifferentialController {
|
|||
DifferentialChangesetParser::parseRangeSpecification($spec);
|
||||
|
||||
$parser = id(new DifferentialChangesetParser())
|
||||
->setViewer($viewer)
|
||||
->setCoverage($coverage)
|
||||
->setChangeset($changeset)
|
||||
->setRenderingReference($rendering_reference)
|
||||
|
|
|
@ -58,6 +58,8 @@ final class DifferentialChangesetParser extends Phobject {
|
|||
private $linesOfContext = 8;
|
||||
|
||||
private $highlightEngine;
|
||||
private $viewer;
|
||||
private $documentEngineKey;
|
||||
|
||||
public function setRange($start, $end) {
|
||||
$this->rangeStart = $start;
|
||||
|
@ -149,6 +151,24 @@ final class DifferentialChangesetParser extends Phobject {
|
|||
return $this->offsetMode;
|
||||
}
|
||||
|
||||
public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getViewer() {
|
||||
return $this->viewer;
|
||||
}
|
||||
|
||||
public function setDocumentEngineKey($document_engine_key) {
|
||||
$this->documentEngineKey = $document_engine_key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDocumentEngineKey() {
|
||||
return $this->documentEngineKey;
|
||||
}
|
||||
|
||||
public static function getDefaultRendererForViewer(PhabricatorUser $viewer) {
|
||||
$is_unified = $viewer->compareUserSetting(
|
||||
PhabricatorUnifiedDiffsSetting::SETTINGKEY,
|
||||
|
@ -164,6 +184,7 @@ final class DifferentialChangesetParser extends Phobject {
|
|||
public function readParametersFromRequest(AphrontRequest $request) {
|
||||
$this->setCharacterEncoding($request->getStr('encoding'));
|
||||
$this->setHighlightAs($request->getStr('highlight'));
|
||||
$this->setDocumentEngineKey($request->getStr('engine'));
|
||||
|
||||
$renderer = null;
|
||||
|
||||
|
@ -226,7 +247,7 @@ final class DifferentialChangesetParser extends Phobject {
|
|||
return $this->depthOnlyLines;
|
||||
}
|
||||
|
||||
public function setVisibileLinesMask(array $mask) {
|
||||
public function setVisibleLinesMask(array $mask) {
|
||||
$this->visible = $mask;
|
||||
return $this;
|
||||
}
|
||||
|
@ -678,13 +699,13 @@ final class DifferentialChangesetParser extends Phobject {
|
|||
$lines_context = $this->getLinesOfContext();
|
||||
|
||||
$hunk_parser->generateIntraLineDiffs();
|
||||
$hunk_parser->generateVisibileLinesMask($lines_context);
|
||||
$hunk_parser->generateVisibleLinesMask($lines_context);
|
||||
|
||||
$this->setOldLines($hunk_parser->getOldLines());
|
||||
$this->setNewLines($hunk_parser->getNewLines());
|
||||
$this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs());
|
||||
$this->setDepthOnlyLines($hunk_parser->getDepthOnlyLines());
|
||||
$this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask());
|
||||
$this->setVisibleLinesMask($hunk_parser->getVisibleLinesMask());
|
||||
$this->hunkStartLines = $hunk_parser->getHunkStartLines(
|
||||
$changeset->getHunks());
|
||||
|
||||
|
@ -847,8 +868,19 @@ final class DifferentialChangesetParser extends Phobject {
|
|||
->setHighlightingDisabled($this->highlightingDisabled)
|
||||
->setDepthOnlyLines($this->getDepthOnlyLines());
|
||||
|
||||
list($engine, $old_ref, $new_ref) = $this->newDocumentEngine();
|
||||
if ($engine) {
|
||||
$engine_blocks = $engine->newEngineBlocks(
|
||||
$old_ref,
|
||||
$new_ref);
|
||||
} else {
|
||||
$engine_blocks = null;
|
||||
}
|
||||
|
||||
$has_document_engine = ($engine_blocks !== null);
|
||||
|
||||
$shield = null;
|
||||
if ($this->isTopLevel && !$this->comments) {
|
||||
if ($this->isTopLevel && !$this->comments && !$has_document_engine) {
|
||||
if ($this->isGenerated()) {
|
||||
$shield = $renderer->renderShield(
|
||||
pht(
|
||||
|
@ -1003,79 +1035,39 @@ final class DifferentialChangesetParser extends Phobject {
|
|||
->setOldComments($old_comments)
|
||||
->setNewComments($new_comments);
|
||||
|
||||
if ($engine_blocks !== null) {
|
||||
$reference = $this->getRenderingReference();
|
||||
$parts = explode('/', $reference);
|
||||
if (count($parts) == 2) {
|
||||
list($id, $vs) = $parts;
|
||||
} else {
|
||||
$id = $parts[0];
|
||||
$vs = 0;
|
||||
}
|
||||
|
||||
// If we don't have an explicit "vs" changeset, it's the left side of
|
||||
// the "id" changeset.
|
||||
if (!$vs) {
|
||||
$vs = $id;
|
||||
}
|
||||
|
||||
$renderer
|
||||
->setDocumentEngine($engine)
|
||||
->setDocumentEngineBlocks($engine_blocks);
|
||||
|
||||
return $renderer->renderDocumentEngineBlocks(
|
||||
$engine_blocks,
|
||||
(string)$id,
|
||||
(string)$vs);
|
||||
}
|
||||
|
||||
// If we've made it here with a type of file we don't know how to render,
|
||||
// bail out with a default empty rendering. Normally, we'd expect a
|
||||
// document engine to catch these changes before we make it this far.
|
||||
switch ($this->changeset->getFileType()) {
|
||||
case DifferentialChangeType::FILE_IMAGE:
|
||||
$old = null;
|
||||
$new = null;
|
||||
// TODO: Improve the architectural issue as discussed in D955
|
||||
// https://secure.phabricator.com/D955
|
||||
$reference = $this->getRenderingReference();
|
||||
$parts = explode('/', $reference);
|
||||
if (count($parts) == 2) {
|
||||
list($id, $vs) = $parts;
|
||||
} else {
|
||||
$id = $parts[0];
|
||||
$vs = 0;
|
||||
}
|
||||
$id = (int)$id;
|
||||
$vs = (int)$vs;
|
||||
|
||||
if (!$vs) {
|
||||
$metadata = $this->changeset->getMetadata();
|
||||
$data = idx($metadata, 'attachment-data');
|
||||
|
||||
$old_phid = idx($metadata, 'old:binary-phid');
|
||||
$new_phid = idx($metadata, 'new:binary-phid');
|
||||
} else {
|
||||
$vs_changeset = id(new DifferentialChangeset())->load($vs);
|
||||
$old_phid = null;
|
||||
$new_phid = null;
|
||||
|
||||
// TODO: This is spooky, see D6851
|
||||
if ($vs_changeset) {
|
||||
$vs_metadata = $vs_changeset->getMetadata();
|
||||
$old_phid = idx($vs_metadata, 'new:binary-phid');
|
||||
}
|
||||
|
||||
$changeset = id(new DifferentialChangeset())->load($id);
|
||||
if ($changeset) {
|
||||
$metadata = $changeset->getMetadata();
|
||||
$new_phid = idx($metadata, 'new:binary-phid');
|
||||
}
|
||||
}
|
||||
|
||||
if ($old_phid || $new_phid) {
|
||||
// grab the files, (micro) optimization for 1 query not 2
|
||||
$file_phids = array();
|
||||
if ($old_phid) {
|
||||
$file_phids[] = $old_phid;
|
||||
}
|
||||
if ($new_phid) {
|
||||
$file_phids[] = $new_phid;
|
||||
}
|
||||
|
||||
$files = id(new PhabricatorFileQuery())
|
||||
->setViewer($this->getUser())
|
||||
->withPHIDs($file_phids)
|
||||
->execute();
|
||||
foreach ($files as $file) {
|
||||
if (empty($file)) {
|
||||
continue;
|
||||
}
|
||||
if ($file->getPHID() == $old_phid) {
|
||||
$old = $file;
|
||||
} else if ($file->getPHID() == $new_phid) {
|
||||
$new = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$renderer->attachOldFile($old);
|
||||
$renderer->attachNewFile($new);
|
||||
|
||||
return $renderer->renderFileChange($old, $new, $id, $vs);
|
||||
case DifferentialChangeType::FILE_DIRECTORY:
|
||||
case DifferentialChangeType::FILE_BINARY:
|
||||
case DifferentialChangeType::FILE_IMAGE:
|
||||
$output = $renderer->renderChangesetTable(null);
|
||||
return $output;
|
||||
}
|
||||
|
@ -1675,4 +1667,154 @@ final class DifferentialChangesetParser extends Phobject {
|
|||
return $prefix.$line;
|
||||
}
|
||||
|
||||
private function newDocumentEngine() {
|
||||
$changeset = $this->changeset;
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
// TODO: This should probably be made non-optional in the future.
|
||||
if (!$viewer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$old_file = null;
|
||||
$new_file = null;
|
||||
|
||||
switch ($changeset->getFileType()) {
|
||||
case DifferentialChangeType::FILE_IMAGE:
|
||||
case DifferentialChangeType::FILE_BINARY:
|
||||
list($old_file, $new_file) = $this->loadFileObjectsForChangeset();
|
||||
break;
|
||||
}
|
||||
|
||||
$old_ref = id(new PhabricatorDocumentRef())
|
||||
->setName($changeset->getOldFile());
|
||||
if ($old_file) {
|
||||
$old_ref->setFile($old_file);
|
||||
} else {
|
||||
$old_data = $this->old;
|
||||
$old_data = ipull($old_data, 'text');
|
||||
$old_data = implode('', $old_data);
|
||||
|
||||
$old_ref->setData($old_data);
|
||||
}
|
||||
|
||||
$new_ref = id(new PhabricatorDocumentRef())
|
||||
->setName($changeset->getFilename());
|
||||
if ($new_file) {
|
||||
$new_ref->setFile($new_file);
|
||||
} else {
|
||||
$new_data = $this->new;
|
||||
$new_data = ipull($new_data, 'text');
|
||||
$new_data = implode('', $new_data);
|
||||
|
||||
$new_ref->setData($new_data);
|
||||
}
|
||||
|
||||
$old_engines = PhabricatorDocumentEngine::getEnginesForRef(
|
||||
$viewer,
|
||||
$old_ref);
|
||||
|
||||
$new_engines = PhabricatorDocumentEngine::getEnginesForRef(
|
||||
$viewer,
|
||||
$new_ref);
|
||||
|
||||
$shared_engines = array_intersect_key($old_engines, $new_engines);
|
||||
|
||||
foreach ($shared_engines as $key => $shared_engine) {
|
||||
if (!$shared_engine->canDiffDocuments($old_ref, $new_ref)) {
|
||||
unset($shared_engines[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$engine_key = $this->getDocumentEngineKey();
|
||||
if (strlen($engine_key)) {
|
||||
if (isset($shared_engines[$engine_key])) {
|
||||
$document_engine = $shared_engines[$engine_key];
|
||||
} else {
|
||||
$document_engine = null;
|
||||
}
|
||||
} else {
|
||||
$document_engine = head($shared_engines);
|
||||
}
|
||||
|
||||
if ($document_engine) {
|
||||
return array(
|
||||
$document_engine,
|
||||
$old_ref,
|
||||
$new_ref);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function loadFileObjectsForChangeset() {
|
||||
$changeset = $this->changeset;
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$old_file = null;
|
||||
$new_file = null;
|
||||
|
||||
// TODO: Improve the architectural issue as discussed in D955
|
||||
// https://secure.phabricator.com/D955
|
||||
$reference = $this->getRenderingReference();
|
||||
$parts = explode('/', $reference);
|
||||
if (count($parts) == 2) {
|
||||
list($id, $vs) = $parts;
|
||||
} else {
|
||||
$id = $parts[0];
|
||||
$vs = 0;
|
||||
}
|
||||
$id = (int)$id;
|
||||
$vs = (int)$vs;
|
||||
|
||||
if (!$vs) {
|
||||
$metadata = $this->changeset->getMetadata();
|
||||
$data = idx($metadata, 'attachment-data');
|
||||
|
||||
$old_phid = idx($metadata, 'old:binary-phid');
|
||||
$new_phid = idx($metadata, 'new:binary-phid');
|
||||
} else {
|
||||
$vs_changeset = id(new DifferentialChangeset())->load($vs);
|
||||
$old_phid = null;
|
||||
$new_phid = null;
|
||||
|
||||
// TODO: This is spooky, see D6851
|
||||
if ($vs_changeset) {
|
||||
$vs_metadata = $vs_changeset->getMetadata();
|
||||
$old_phid = idx($vs_metadata, 'new:binary-phid');
|
||||
}
|
||||
|
||||
$changeset = id(new DifferentialChangeset())->load($id);
|
||||
if ($changeset) {
|
||||
$metadata = $changeset->getMetadata();
|
||||
$new_phid = idx($metadata, 'new:binary-phid');
|
||||
}
|
||||
}
|
||||
|
||||
if ($old_phid || $new_phid) {
|
||||
$file_phids = array();
|
||||
if ($old_phid) {
|
||||
$file_phids[] = $old_phid;
|
||||
}
|
||||
if ($new_phid) {
|
||||
$file_phids[] = $new_phid;
|
||||
}
|
||||
|
||||
$files = id(new PhabricatorFileQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs($file_phids)
|
||||
->execute();
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file->getPHID() == $old_phid) {
|
||||
$old_file = $file;
|
||||
} else if ($file->getPHID() == $new_phid) {
|
||||
$new_file = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array($old_file, $new_file);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ final class DifferentialHunkParser extends Phobject {
|
|||
}
|
||||
public function getVisibleLinesMask() {
|
||||
if ($this->visibleLinesMask === null) {
|
||||
throw new PhutilInvalidStateException('generateVisibileLinesMask');
|
||||
throw new PhutilInvalidStateException('generateVisibleLinesMask');
|
||||
}
|
||||
return $this->visibleLinesMask;
|
||||
}
|
||||
|
@ -354,7 +354,7 @@ final class DifferentialHunkParser extends Phobject {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function generateVisibileLinesMask($lines_context) {
|
||||
public function generateVisibleLinesMask($lines_context) {
|
||||
$old = $this->getOldLines();
|
||||
$new = $this->getNewLines();
|
||||
$max_length = max(count($old), count($new));
|
||||
|
|
|
@ -270,11 +270,20 @@ abstract class DifferentialChangesetHTMLRenderer
|
|||
}
|
||||
}
|
||||
|
||||
if ($this->getHighlightingDisabled()) {
|
||||
$messages[] = pht(
|
||||
'This file is larger than %s, so syntax highlighting is '.
|
||||
'disabled by default.',
|
||||
phutil_format_bytes(DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT));
|
||||
$blocks = $this->getDocumentEngineBlocks();
|
||||
if ($blocks) {
|
||||
foreach ($blocks->getMessages() as $message) {
|
||||
$messages[] = $message;
|
||||
}
|
||||
} else {
|
||||
if ($this->getHighlightingDisabled()) {
|
||||
$byte_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT;
|
||||
$byte_limit = phutil_format_bytes($byte_limit);
|
||||
$messages[] = pht(
|
||||
'This file is larger than %s, so syntax highlighting is '.
|
||||
'disabled by default.',
|
||||
$byte_limit);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->formatHeaderMessages($messages);
|
||||
|
@ -608,17 +617,4 @@ abstract class DifferentialChangesetHTMLRenderer
|
|||
return array($left_prefix, $right_prefix);
|
||||
}
|
||||
|
||||
protected function renderImageStage(PhabricatorFile $file) {
|
||||
return phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => 'differential-image-stage',
|
||||
),
|
||||
phutil_tag(
|
||||
'img',
|
||||
array(
|
||||
'src' => $file->getBestURI(),
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -31,14 +31,6 @@ final class DifferentialChangesetOneUpMailRenderer
|
|||
return null;
|
||||
}
|
||||
|
||||
public function renderFileChange(
|
||||
$old_file = null,
|
||||
$new_file = null,
|
||||
$id = 0,
|
||||
$vs = 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function renderTextChange(
|
||||
$range_start,
|
||||
$range_len,
|
||||
|
|
|
@ -228,60 +228,238 @@ final class DifferentialChangesetOneUpRenderer
|
|||
return null;
|
||||
}
|
||||
|
||||
public function renderFileChange(
|
||||
$old_file = null,
|
||||
$new_file = null,
|
||||
$id = 0,
|
||||
$vs = 0) {
|
||||
public function renderDocumentEngineBlocks(
|
||||
PhabricatorDocumentEngineBlocks $block_list,
|
||||
$old_changeset_key,
|
||||
$new_changeset_key) {
|
||||
|
||||
// TODO: This should eventually merge into the normal primitives pathway,
|
||||
// but fake it for now and just share as much code as possible.
|
||||
$engine = $this->getDocumentEngine();
|
||||
$layout = $block_list->newTwoUpLayout();
|
||||
|
||||
$primitives = array();
|
||||
if ($old_file) {
|
||||
$primitives[] = array(
|
||||
'type' => 'old-file',
|
||||
'htype' => ($new_file ? 'new-file' : null),
|
||||
'file' => $old_file,
|
||||
'line' => 1,
|
||||
'render' => $this->renderImageStage($old_file),
|
||||
);
|
||||
$old_comments = $this->getOldComments();
|
||||
$new_comments = $this->getNewComments();
|
||||
|
||||
$unchanged = array();
|
||||
foreach ($layout as $key => $row) {
|
||||
list($old, $new) = $row;
|
||||
|
||||
if (!$old) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$new) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($old->getDifferenceType() !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($new->getDifferenceType() !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$unchanged[$key] = true;
|
||||
}
|
||||
|
||||
if ($new_file) {
|
||||
$primitives[] = array(
|
||||
'type' => 'new-file',
|
||||
'htype' => ($old_file ? 'old-file' : null),
|
||||
'file' => $new_file,
|
||||
'line' => 1,
|
||||
'oline' => ($old_file ? 1 : null),
|
||||
'render' => $this->renderImageStage($new_file),
|
||||
);
|
||||
}
|
||||
$rows = array();
|
||||
$count = count($layout);
|
||||
for ($ii = 0; $ii < $count;) {
|
||||
$start = $ii;
|
||||
|
||||
// TODO: We'd like to share primitive code here, but buildPrimitives()
|
||||
// currently chokes on changesets with no textual data.
|
||||
foreach ($this->getOldComments() as $line => $group) {
|
||||
foreach ($group as $comment) {
|
||||
$primitives[] = array(
|
||||
'type' => 'inline',
|
||||
'comment' => $comment,
|
||||
'right' => false,
|
||||
for ($jj = $ii; $jj < $count; $jj++) {
|
||||
list($old, $new) = $layout[$jj];
|
||||
|
||||
if (empty($unchanged[$jj])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
'type' => 'unchanged',
|
||||
'layoutKey' => $jj,
|
||||
);
|
||||
}
|
||||
$ii = $jj;
|
||||
|
||||
for ($jj = $ii; $jj < $count; $jj++) {
|
||||
list($old, $new) = $layout[$jj];
|
||||
|
||||
if (!empty($unchanged[$jj])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
'type' => 'old',
|
||||
'layoutKey' => $jj,
|
||||
);
|
||||
}
|
||||
|
||||
for ($jj = $ii; $jj < $count; $jj++) {
|
||||
list($old, $new) = $layout[$jj];
|
||||
|
||||
if (!empty($unchanged[$jj])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
'type' => 'new',
|
||||
'layoutKey' => $jj,
|
||||
);
|
||||
}
|
||||
$ii = $jj;
|
||||
|
||||
// We always expect to consume at least one row when iterating through
|
||||
// the loop and make progress. If we don't, bail out to avoid spinning
|
||||
// to death.
|
||||
if ($ii === $start) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Failed to make progress during 1up diff layout.'));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->getNewComments() as $line => $group) {
|
||||
foreach ($group as $comment) {
|
||||
$primitives[] = array(
|
||||
'type' => 'inline',
|
||||
'comment' => $comment,
|
||||
'right' => true,
|
||||
);
|
||||
$old_ref = null;
|
||||
$new_ref = null;
|
||||
$refs = $block_list->getDocumentRefs();
|
||||
if ($refs) {
|
||||
list($old_ref, $new_ref) = $refs;
|
||||
}
|
||||
|
||||
$view = array();
|
||||
foreach ($rows as $row) {
|
||||
$row_type = $row['type'];
|
||||
$layout_key = $row['layoutKey'];
|
||||
$row_layout = $layout[$layout_key];
|
||||
list($old, $new) = $row_layout;
|
||||
|
||||
if ($old) {
|
||||
$old_key = $old->getBlockKey();
|
||||
} else {
|
||||
$old_key = null;
|
||||
}
|
||||
|
||||
if ($new) {
|
||||
$new_key = $new->getBlockKey();
|
||||
} else {
|
||||
$new_key = null;
|
||||
}
|
||||
|
||||
$cells = array();
|
||||
$cell_classes = array();
|
||||
|
||||
if ($row_type === 'unchanged') {
|
||||
$cell_content = $engine->newBlockContentView(
|
||||
$old_ref,
|
||||
$old);
|
||||
} else if ($old && $new) {
|
||||
$block_diff = $engine->newBlockDiffViews(
|
||||
$old_ref,
|
||||
$old,
|
||||
$new_ref,
|
||||
$new);
|
||||
|
||||
// TODO: We're currently double-rendering this: once when building
|
||||
// the old row, and once when building the new one. In both cases,
|
||||
// we throw away the other half of the output. We could cache this
|
||||
// to improve performance.
|
||||
|
||||
if ($row_type === 'old') {
|
||||
$cell_content = $block_diff->getOldContent();
|
||||
$cell_classes = $block_diff->getOldClasses();
|
||||
} else {
|
||||
$cell_content = $block_diff->getNewContent();
|
||||
$cell_classes = $block_diff->getNewClasses();
|
||||
}
|
||||
} else if ($row_type === 'old') {
|
||||
$cell_content = $engine->newBlockContentView(
|
||||
$old_ref,
|
||||
$old);
|
||||
$cell_classes[] = 'old';
|
||||
$cell_classes[] = 'old-full';
|
||||
|
||||
$new_key = null;
|
||||
} else if ($row_type === 'new') {
|
||||
$cell_content = $engine->newBlockContentView(
|
||||
$new_ref,
|
||||
$new);
|
||||
$cell_classes[] = 'new';
|
||||
$cell_classes[] = 'new-full';
|
||||
|
||||
$old_key = null;
|
||||
}
|
||||
|
||||
if ($old_key === null) {
|
||||
$old_id = null;
|
||||
} else {
|
||||
$old_id = "C{$old_changeset_key}OL{$old_key}";
|
||||
}
|
||||
|
||||
if ($new_key === null) {
|
||||
$new_id = null;
|
||||
} else {
|
||||
$new_id = "C{$new_changeset_key}NL{$new_key}";
|
||||
}
|
||||
|
||||
$cells[] = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'id' => $old_id,
|
||||
'data-n' => $old_key,
|
||||
'class' => 'n',
|
||||
));
|
||||
|
||||
$cells[] = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'id' => $new_id,
|
||||
'data-n' => $new_key,
|
||||
'class' => 'n',
|
||||
));
|
||||
|
||||
$cells[] = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'class' => 'copy',
|
||||
));
|
||||
|
||||
$cell_classes[] = 'diff-flush';
|
||||
$cell_classes = implode(' ', $cell_classes);
|
||||
|
||||
$cells[] = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'class' => $cell_classes,
|
||||
'data-copy-mode' => 'copy-unified',
|
||||
),
|
||||
$cell_content);
|
||||
|
||||
$view[] = phutil_tag(
|
||||
'tr',
|
||||
array(),
|
||||
$cells);
|
||||
|
||||
if ($old_key !== null) {
|
||||
$old_inlines = idx($old_comments, $old_key, array());
|
||||
foreach ($old_inlines as $inline) {
|
||||
$inline = $this->buildInlineComment(
|
||||
$inline,
|
||||
$on_right = false);
|
||||
$view[] = $this->getRowScaffoldForInline($inline);
|
||||
}
|
||||
}
|
||||
|
||||
if ($new_key !== null) {
|
||||
$new_inlines = idx($new_comments, $new_key, array());
|
||||
foreach ($new_inlines as $inline) {
|
||||
$inline = $this->buildInlineComment(
|
||||
$inline,
|
||||
$on_right = true);
|
||||
$view[] = $this->getRowScaffoldForInline($inline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$output = $this->renderPrimitives($primitives, 1);
|
||||
$output = $this->wrapChangeInTable($view);
|
||||
return $this->renderChangesetTable($output);
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@ abstract class DifferentialChangesetRenderer extends Phobject {
|
|||
private $scopeEngine = false;
|
||||
private $depthOnlyLines;
|
||||
|
||||
private $documentEngine;
|
||||
private $documentEngineBlocks;
|
||||
|
||||
private $oldFile = false;
|
||||
private $newFile = false;
|
||||
|
||||
|
@ -239,6 +242,25 @@ abstract class DifferentialChangesetRenderer extends Phobject {
|
|||
return $this->oldChangesetID;
|
||||
}
|
||||
|
||||
public function setDocumentEngine(PhabricatorDocumentEngine $engine) {
|
||||
$this->documentEngine = $engine;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDocumentEngine() {
|
||||
return $this->documentEngine;
|
||||
}
|
||||
|
||||
public function setDocumentEngineBlocks(
|
||||
PhabricatorDocumentEngineBlocks $blocks) {
|
||||
$this->documentEngineBlocks = $blocks;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDocumentEngineBlocks() {
|
||||
return $this->documentEngineBlocks;
|
||||
}
|
||||
|
||||
public function setNewComments(array $new_comments) {
|
||||
foreach ($new_comments as $line_number => $comments) {
|
||||
assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
|
||||
|
@ -355,6 +377,16 @@ abstract class DifferentialChangesetRenderer extends Phobject {
|
|||
$notice = null;
|
||||
if ($this->getIsTopLevel()) {
|
||||
$force = (!$content && !$props);
|
||||
|
||||
// If we have DocumentEngine messages about the blocks, assume they
|
||||
// explain why there's no content.
|
||||
$blocks = $this->getDocumentEngineBlocks();
|
||||
if ($blocks) {
|
||||
if ($blocks->getMessages()) {
|
||||
$force = false;
|
||||
}
|
||||
}
|
||||
|
||||
$notice = $this->renderChangeTypeHeader($force);
|
||||
}
|
||||
|
||||
|
@ -378,11 +410,13 @@ abstract class DifferentialChangesetRenderer extends Phobject {
|
|||
$range_start,
|
||||
$range_len,
|
||||
$rows);
|
||||
abstract public function renderFileChange(
|
||||
$old = null,
|
||||
$new = null,
|
||||
$id = 0,
|
||||
$vs = 0);
|
||||
|
||||
public function renderDocumentEngineBlocks(
|
||||
PhabricatorDocumentEngineBlocks $blocks,
|
||||
$old_changeset_key,
|
||||
$new_changeset_key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
abstract protected function renderChangeTypeHeader($force);
|
||||
abstract protected function renderUndershieldHeader();
|
||||
|
|
|
@ -134,14 +134,4 @@ abstract class DifferentialChangesetTestRenderer
|
|||
return phutil_safe_html($out);
|
||||
}
|
||||
|
||||
|
||||
public function renderFileChange(
|
||||
$old_file = null,
|
||||
$new_file = null,
|
||||
$id = 0,
|
||||
$vs = 0) {
|
||||
|
||||
throw new PhutilMethodNotImplementedException();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -364,76 +364,220 @@ final class DifferentialChangesetTwoUpRenderer
|
|||
return $this->wrapChangeInTable(phutil_implode_html('', $html));
|
||||
}
|
||||
|
||||
public function renderFileChange(
|
||||
$old_file = null,
|
||||
$new_file = null,
|
||||
$id = 0,
|
||||
$vs = 0) {
|
||||
public function renderDocumentEngineBlocks(
|
||||
PhabricatorDocumentEngineBlocks $block_list,
|
||||
$old_changeset_key,
|
||||
$new_changeset_key) {
|
||||
|
||||
$old = null;
|
||||
if ($old_file) {
|
||||
$old = $this->renderImageStage($old_file);
|
||||
$engine = $this->getDocumentEngine();
|
||||
|
||||
$old_ref = null;
|
||||
$new_ref = null;
|
||||
$refs = $block_list->getDocumentRefs();
|
||||
if ($refs) {
|
||||
list($old_ref, $new_ref) = $refs;
|
||||
}
|
||||
|
||||
$new = null;
|
||||
if ($new_file) {
|
||||
$new = $this->renderImageStage($new_file);
|
||||
}
|
||||
$old_comments = $this->getOldComments();
|
||||
$new_comments = $this->getNewComments();
|
||||
|
||||
// If we don't have an explicit "vs" changeset, it's the left side of the
|
||||
// "id" changeset.
|
||||
if (!$vs) {
|
||||
$vs = $id;
|
||||
}
|
||||
$gap_view = javelin_tag(
|
||||
'tr',
|
||||
array(
|
||||
'sigil' => 'context-target',
|
||||
),
|
||||
phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'colspan' => 6,
|
||||
'class' => 'show-more',
|
||||
),
|
||||
pht("\xE2\x80\xA2 \xE2\x80\xA2 \xE2\x80\xA2")));
|
||||
|
||||
$html_old = array();
|
||||
$html_new = array();
|
||||
foreach ($this->getOldComments() as $on_line => $comment_group) {
|
||||
foreach ($comment_group as $comment) {
|
||||
$inline = $this->buildInlineComment(
|
||||
$comment,
|
||||
$on_right = false);
|
||||
$html_old[] = $this->getRowScaffoldForInline($inline);
|
||||
$rows = array();
|
||||
$in_gap = false;
|
||||
foreach ($block_list->newTwoUpLayout() as $row) {
|
||||
list($old, $new) = $row;
|
||||
|
||||
if ($old) {
|
||||
$old_key = $old->getBlockKey();
|
||||
$is_visible = $old->getIsVisible();
|
||||
} else {
|
||||
$old_key = null;
|
||||
}
|
||||
}
|
||||
foreach ($this->getNewComments() as $lin_line => $comment_group) {
|
||||
foreach ($comment_group as $comment) {
|
||||
$inline = $this->buildInlineComment(
|
||||
$comment,
|
||||
$on_right = true);
|
||||
$html_new[] = $this->getRowScaffoldForInline($inline);
|
||||
|
||||
if ($new) {
|
||||
$new_key = $new->getBlockKey();
|
||||
$is_visible = $new->getIsVisible();
|
||||
} else {
|
||||
$new_key = null;
|
||||
}
|
||||
|
||||
if (!$is_visible) {
|
||||
if (!$in_gap) {
|
||||
$in_gap = true;
|
||||
$rows[] = $gap_view;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($in_gap) {
|
||||
$in_gap = false;
|
||||
}
|
||||
|
||||
if ($old) {
|
||||
$is_rem = ($old->getDifferenceType() === '-');
|
||||
} else {
|
||||
$is_rem = false;
|
||||
}
|
||||
|
||||
if ($new) {
|
||||
$is_add = ($new->getDifferenceType() === '+');
|
||||
} else {
|
||||
$is_add = false;
|
||||
}
|
||||
|
||||
if ($is_rem && $is_add) {
|
||||
$block_diff = $engine->newBlockDiffViews(
|
||||
$old_ref,
|
||||
$old,
|
||||
$new_ref,
|
||||
$new);
|
||||
|
||||
$old_content = $block_diff->getOldContent();
|
||||
$new_content = $block_diff->getNewContent();
|
||||
|
||||
$old_classes = $block_diff->getOldClasses();
|
||||
$new_classes = $block_diff->getNewClasses();
|
||||
} else {
|
||||
$old_classes = array();
|
||||
$new_classes = array();
|
||||
|
||||
if ($old) {
|
||||
$old_content = $engine->newBlockContentView(
|
||||
$old_ref,
|
||||
$old);
|
||||
|
||||
if ($is_rem) {
|
||||
$old_classes[] = 'old';
|
||||
$old_classes[] = 'old-full';
|
||||
}
|
||||
} else {
|
||||
$old_content = null;
|
||||
}
|
||||
|
||||
if ($new) {
|
||||
$new_content = $engine->newBlockContentView(
|
||||
$new_ref,
|
||||
$new);
|
||||
|
||||
if ($is_add) {
|
||||
$new_classes[] = 'new';
|
||||
$new_classes[] = 'new-full';
|
||||
}
|
||||
} else {
|
||||
$new_content = null;
|
||||
}
|
||||
}
|
||||
|
||||
$old_classes[] = 'diff-flush';
|
||||
$old_classes = implode(' ', $old_classes);
|
||||
|
||||
$new_classes[] = 'diff-flush';
|
||||
$new_classes = implode(' ', $new_classes);
|
||||
|
||||
$old_inline_rows = array();
|
||||
if ($old_key !== null) {
|
||||
$old_inlines = idx($old_comments, $old_key, array());
|
||||
foreach ($old_inlines as $inline) {
|
||||
$inline = $this->buildInlineComment(
|
||||
$inline,
|
||||
$on_right = false);
|
||||
$old_inline_rows[] = $this->getRowScaffoldForInline($inline);
|
||||
}
|
||||
}
|
||||
|
||||
$new_inline_rows = array();
|
||||
if ($new_key !== null) {
|
||||
$new_inlines = idx($new_comments, $new_key, array());
|
||||
foreach ($new_inlines as $inline) {
|
||||
$inline = $this->buildInlineComment(
|
||||
$inline,
|
||||
$on_right = true);
|
||||
$new_inline_rows[] = $this->getRowScaffoldForInline($inline);
|
||||
}
|
||||
}
|
||||
|
||||
if ($old_content === null) {
|
||||
$old_id = null;
|
||||
} else {
|
||||
$old_id = "C{$old_changeset_key}OL{$old_key}";
|
||||
}
|
||||
|
||||
$old_line_cell = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'id' => $old_id,
|
||||
'data-n' => $old_key,
|
||||
'class' => 'n',
|
||||
));
|
||||
|
||||
$old_content_cell = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'class' => $old_classes,
|
||||
'data-copy-mode' => 'copy-l',
|
||||
),
|
||||
$old_content);
|
||||
|
||||
if ($new_content === null) {
|
||||
$new_id = null;
|
||||
} else {
|
||||
$new_id = "C{$new_changeset_key}NL{$new_key}";
|
||||
}
|
||||
|
||||
$new_line_cell = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'id' => $new_id,
|
||||
'data-n' => $new_key,
|
||||
'class' => 'n',
|
||||
));
|
||||
|
||||
$copy_gutter = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'class' => 'copy',
|
||||
));
|
||||
|
||||
$new_content_cell = phutil_tag(
|
||||
'td',
|
||||
array(
|
||||
'class' => $new_classes,
|
||||
'colspan' => '2',
|
||||
'data-copy-mode' => 'copy-r',
|
||||
),
|
||||
$new_content);
|
||||
|
||||
$row_view = phutil_tag(
|
||||
'tr',
|
||||
array(),
|
||||
array(
|
||||
$old_line_cell,
|
||||
$old_content_cell,
|
||||
$new_line_cell,
|
||||
$copy_gutter,
|
||||
$new_content_cell,
|
||||
));
|
||||
|
||||
$rows[] = array(
|
||||
$row_view,
|
||||
$old_inline_rows,
|
||||
$new_inline_rows,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$old) {
|
||||
$th_old = phutil_tag('th', array());
|
||||
} else {
|
||||
$th_old = phutil_tag('th', array('id' => "C{$vs}OL1"), 1);
|
||||
}
|
||||
|
||||
if (!$new) {
|
||||
$th_new = phutil_tag('th', array());
|
||||
} else {
|
||||
$th_new = phutil_tag('th', array('id' => "C{$id}NL1"), 1);
|
||||
}
|
||||
|
||||
$output = hsprintf(
|
||||
'<tr class="differential-image-diff">'.
|
||||
'%s'.
|
||||
'<td class="differential-old-image">%s</td>'.
|
||||
'%s'.
|
||||
'<td class="differential-new-image" colspan="3">%s</td>'.
|
||||
'</tr>'.
|
||||
'%s'.
|
||||
'%s',
|
||||
$th_old,
|
||||
$old,
|
||||
$th_new,
|
||||
$new,
|
||||
phutil_implode_html('', $html_old),
|
||||
phutil_implode_html('', $html_new));
|
||||
|
||||
$output = $this->wrapChangeInTable($output);
|
||||
$output = $this->wrapChangeInTable($rows);
|
||||
|
||||
return $this->renderChangesetTable($output);
|
||||
}
|
||||
|
|
|
@ -240,6 +240,7 @@ final class DifferentialChangesetListView extends AphrontView {
|
|||
'View Unified' => pht('View Unified'),
|
||||
'Change Text Encoding...' => pht('Change Text Encoding...'),
|
||||
'Highlight As...' => pht('Highlight As...'),
|
||||
'View As...' => pht('View As...'),
|
||||
|
||||
'Loading...' => pht('Loading...'),
|
||||
|
||||
|
|
|
@ -195,13 +195,15 @@ final class DiffusionLowLevelResolveRefsQuery
|
|||
|
||||
$alternate = null;
|
||||
if ($type == 'tag') {
|
||||
$alternate = $identifier;
|
||||
$identifier = idx($tag_map, $ref);
|
||||
if (!$identifier) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
"Failed to look up tag '%s'!",
|
||||
$ref));
|
||||
$tag_identifier = idx($tag_map, $ref);
|
||||
if ($tag_identifier === null) {
|
||||
// This can happen when we're asked to resolve the hash of a "tag"
|
||||
// object created with "git tag --annotate" that isn't currently
|
||||
// reachable from any ref. Just leave things as they are.
|
||||
} else {
|
||||
// Otherwise, we have a normal named tag.
|
||||
$alternate = $identifier;
|
||||
$identifier = $tag_identifier;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorDocumentEngineBlock
|
||||
extends Phobject {
|
||||
|
||||
private $blockKey;
|
||||
private $content;
|
||||
private $differenceHash;
|
||||
private $differenceType;
|
||||
private $isVisible;
|
||||
|
||||
public function setContent($content) {
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent() {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setBlockKey($block_key) {
|
||||
$this->blockKey = $block_key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBlockKey() {
|
||||
return $this->blockKey;
|
||||
}
|
||||
|
||||
public function setDifferenceHash($difference_hash) {
|
||||
$this->differenceHash = $difference_hash;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDifferenceHash() {
|
||||
return $this->differenceHash;
|
||||
}
|
||||
|
||||
public function setDifferenceType($difference_type) {
|
||||
$this->differenceType = $difference_type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDifferenceType() {
|
||||
return $this->differenceType;
|
||||
}
|
||||
|
||||
public function setIsVisible($is_visible) {
|
||||
$this->isVisible = $is_visible;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsVisible() {
|
||||
return $this->isVisible;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorDocumentEngineBlockDiff
|
||||
extends Phobject {
|
||||
|
||||
private $oldContent;
|
||||
private $newContent;
|
||||
private $oldClasses = array();
|
||||
private $newClasses = array();
|
||||
|
||||
public function setOldContent($old_content) {
|
||||
$this->oldContent = $old_content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOldContent() {
|
||||
return $this->oldContent;
|
||||
}
|
||||
|
||||
public function setNewContent($new_content) {
|
||||
$this->newContent = $new_content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNewContent() {
|
||||
return $this->newContent;
|
||||
}
|
||||
|
||||
public function addOldClass($class) {
|
||||
$this->oldClasses[] = $class;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOldClasses() {
|
||||
return $this->oldClasses;
|
||||
}
|
||||
|
||||
public function addNewClass($class) {
|
||||
$this->newClasses[] = $class;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNewClasses() {
|
||||
return $this->newClasses;
|
||||
}
|
||||
|
||||
}
|
179
src/applications/files/diff/PhabricatorDocumentEngineBlocks.php
Normal file
179
src/applications/files/diff/PhabricatorDocumentEngineBlocks.php
Normal file
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorDocumentEngineBlocks
|
||||
extends Phobject {
|
||||
|
||||
private $lists = array();
|
||||
private $messages = array();
|
||||
|
||||
public function addMessage($message) {
|
||||
$this->messages[] = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMessages() {
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
public function addBlockList(PhabricatorDocumentRef $ref, array $blocks) {
|
||||
assert_instances_of($blocks, 'PhabricatorDocumentEngineBlock');
|
||||
|
||||
$this->lists[] = array(
|
||||
'ref' => $ref,
|
||||
'blocks' => array_values($blocks),
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDocumentRefs() {
|
||||
return ipull($this->lists, 'ref');
|
||||
}
|
||||
|
||||
public function newTwoUpLayout() {
|
||||
$rows = array();
|
||||
$lists = $this->lists;
|
||||
|
||||
if (count($lists) != 2) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$specs = array();
|
||||
foreach ($this->lists as $list) {
|
||||
$specs[] = $this->newDiffSpec($list['blocks']);
|
||||
}
|
||||
|
||||
$old_map = $specs[0]['map'];
|
||||
$new_map = $specs[1]['map'];
|
||||
|
||||
$old_list = $specs[0]['list'];
|
||||
$new_list = $specs[1]['list'];
|
||||
|
||||
$changeset = id(new PhabricatorDifferenceEngine())
|
||||
->generateChangesetFromFileContent($old_list, $new_list);
|
||||
|
||||
$hunk_parser = id(new DifferentialHunkParser())
|
||||
->parseHunksForLineData($changeset->getHunks())
|
||||
->reparseHunksForSpecialAttributes();
|
||||
|
||||
$hunk_parser->generateVisibleLinesMask(2);
|
||||
$mask = $hunk_parser->getVisibleLinesMask();
|
||||
|
||||
$old_lines = $hunk_parser->getOldLines();
|
||||
$new_lines = $hunk_parser->getNewLines();
|
||||
|
||||
$rows = array();
|
||||
|
||||
$count = count($old_lines);
|
||||
for ($ii = 0; $ii < $count; $ii++) {
|
||||
$old_line = idx($old_lines, $ii);
|
||||
$new_line = idx($new_lines, $ii);
|
||||
|
||||
$is_visible = !empty($mask[$ii + 1]);
|
||||
|
||||
// TODO: There's currently a bug where one-line files get incorrectly
|
||||
// masked. This causes images to completely fail to render. Just ignore
|
||||
// the mask if it came back empty.
|
||||
if (!$mask) {
|
||||
$is_visible = true;
|
||||
}
|
||||
|
||||
if ($old_line) {
|
||||
$old_hash = rtrim($old_line['text'], "\n");
|
||||
if (!strlen($old_hash)) {
|
||||
// This can happen when one of the sources has no blocks.
|
||||
$old_block = null;
|
||||
} else {
|
||||
$old_block = array_shift($old_map[$old_hash]);
|
||||
$old_block
|
||||
->setDifferenceType($old_line['type'])
|
||||
->setIsVisible($is_visible);
|
||||
}
|
||||
} else {
|
||||
$old_block = null;
|
||||
}
|
||||
|
||||
if ($new_line) {
|
||||
$new_hash = rtrim($new_line['text'], "\n");
|
||||
if (!strlen($new_hash)) {
|
||||
$new_block = null;
|
||||
} else {
|
||||
$new_block = array_shift($new_map[$new_hash]);
|
||||
$new_block
|
||||
->setDifferenceType($new_line['type'])
|
||||
->setIsVisible($is_visible);
|
||||
}
|
||||
} else {
|
||||
$new_block = null;
|
||||
}
|
||||
|
||||
// If both lists are empty, we may generate a row which has two empty
|
||||
// blocks.
|
||||
if (!$old_block && !$new_block) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
$old_block,
|
||||
$new_block,
|
||||
);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function newOneUpLayout() {
|
||||
$rows = array();
|
||||
$lists = $this->lists;
|
||||
|
||||
$idx = 0;
|
||||
while (true) {
|
||||
$found_any = false;
|
||||
|
||||
$row = array();
|
||||
foreach ($lists as $list) {
|
||||
$blocks = $list['blocks'];
|
||||
$cell = idx($blocks, $idx);
|
||||
|
||||
if ($cell !== null) {
|
||||
$found_any = true;
|
||||
}
|
||||
|
||||
if ($cell) {
|
||||
$rows[] = $cell;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found_any) {
|
||||
break;
|
||||
}
|
||||
|
||||
$idx++;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
||||
private function newDiffSpec(array $blocks) {
|
||||
$map = array();
|
||||
$list = array();
|
||||
|
||||
foreach ($blocks as $block) {
|
||||
$hash = $block->getDifferenceHash();
|
||||
|
||||
if (!isset($map[$hash])) {
|
||||
$map[$hash] = array();
|
||||
}
|
||||
$map[$hash][] = $block;
|
||||
|
||||
$list[] = $hash;
|
||||
}
|
||||
|
||||
return array(
|
||||
'map' => $map,
|
||||
'list' => implode("\n", $list)."\n",
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -31,6 +31,42 @@ abstract class PhabricatorDocumentEngine
|
|||
return $this->canRenderDocumentType($ref);
|
||||
}
|
||||
|
||||
public function canDiffDocuments(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentRef $vref) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function newBlockDiffViews(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentEngineBlock $ublock,
|
||||
PhabricatorDocumentRef $vref,
|
||||
PhabricatorDocumentEngineBlock $vblock) {
|
||||
|
||||
$u_content = $this->newBlockContentView($uref, $ublock);
|
||||
$v_content = $this->newBlockContentView($vref, $vblock);
|
||||
|
||||
return id(new PhabricatorDocumentEngineBlockDiff())
|
||||
->setOldContent($u_content)
|
||||
->addOldClass('old')
|
||||
->addOldClass('old-full')
|
||||
->setNewContent($v_content)
|
||||
->addNewClass('new')
|
||||
->addNewClass('new-full');
|
||||
}
|
||||
|
||||
public function newBlockContentView(
|
||||
PhabricatorDocumentRef $ref,
|
||||
PhabricatorDocumentEngineBlock $block) {
|
||||
return $block->getContent();
|
||||
}
|
||||
|
||||
public function newEngineBlocks(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentRef $vref) {
|
||||
throw new PhutilMethodNotImplementedException();
|
||||
}
|
||||
|
||||
public function canConfigureEncoding(PhabricatorDocumentRef $ref) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ final class PhabricatorDocumentRef
|
|||
private $symbolMetadata = array();
|
||||
private $blameURI;
|
||||
private $coverage = array();
|
||||
private $data;
|
||||
|
||||
public function setFile(PhabricatorFile $file) {
|
||||
$this->file = $file;
|
||||
|
@ -65,6 +66,10 @@ final class PhabricatorDocumentRef
|
|||
return $this->byteLength;
|
||||
}
|
||||
|
||||
if ($this->data !== null) {
|
||||
return strlen($this->data);
|
||||
}
|
||||
|
||||
if ($this->file) {
|
||||
return (int)$this->file->getByteSize();
|
||||
}
|
||||
|
@ -72,7 +77,26 @@ final class PhabricatorDocumentRef
|
|||
return null;
|
||||
}
|
||||
|
||||
public function setData($data) {
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function loadData($begin = null, $end = null) {
|
||||
if ($this->data !== null) {
|
||||
$data = $this->data;
|
||||
|
||||
if ($begin !== null && $end !== null) {
|
||||
$data = substr($data, $begin, $end - $begin);
|
||||
} else if ($begin !== null) {
|
||||
$data = substr($data, $begin);
|
||||
} else if ($end !== null) {
|
||||
$data = substr($data, 0, $end);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ($this->file) {
|
||||
$iterator = $this->file->getFileDataIterator($begin, $end);
|
||||
|
||||
|
|
|
@ -17,6 +17,71 @@ final class PhabricatorImageDocumentEngine
|
|||
return (1024 * 1024 * 64);
|
||||
}
|
||||
|
||||
public function canDiffDocuments(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentRef $vref) {
|
||||
|
||||
// For now, we can only render a rich image diff if both documents have
|
||||
// their data stored in Files already.
|
||||
|
||||
return ($uref->getFile() && $vref->getFile());
|
||||
}
|
||||
|
||||
public function newEngineBlocks(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentRef $vref) {
|
||||
|
||||
$u_blocks = $this->newDiffBlocks($uref);
|
||||
$v_blocks = $this->newDiffBlocks($vref);
|
||||
|
||||
return id(new PhabricatorDocumentEngineBlocks())
|
||||
->addBlockList($uref, $u_blocks)
|
||||
->addBlockList($vref, $v_blocks);
|
||||
}
|
||||
|
||||
public function newBlockDiffViews(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentEngineBlock $ublock,
|
||||
PhabricatorDocumentRef $vref,
|
||||
PhabricatorDocumentEngineBlock $vblock) {
|
||||
|
||||
$u_content = $this->newBlockContentView($uref, $ublock);
|
||||
$v_content = $this->newBlockContentView($vref, $vblock);
|
||||
|
||||
return id(new PhabricatorDocumentEngineBlockDiff())
|
||||
->setOldContent($u_content)
|
||||
->addOldClass('diff-image-cell')
|
||||
->setNewContent($v_content)
|
||||
->addNewClass('diff-image-cell');
|
||||
}
|
||||
|
||||
|
||||
private function newDiffBlocks(PhabricatorDocumentRef $ref) {
|
||||
$blocks = array();
|
||||
|
||||
$file = $ref->getFile();
|
||||
|
||||
$image_view = phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => 'differential-image-stage',
|
||||
),
|
||||
phutil_tag(
|
||||
'img',
|
||||
array(
|
||||
'src' => $file->getBestURI(),
|
||||
)));
|
||||
|
||||
$hash = $file->getContentHash();
|
||||
|
||||
$blocks[] = id(new PhabricatorDocumentEngineBlock())
|
||||
->setBlockKey('1')
|
||||
->setDifferenceHash($hash)
|
||||
->setContent($image_view);
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
|
||||
$file = $ref->getFile();
|
||||
if ($file) {
|
||||
|
|
|
@ -35,55 +35,198 @@ final class PhabricatorJupyterDocumentEngine
|
|||
return $ref->isProbablyJSON();
|
||||
}
|
||||
|
||||
public function canDiffDocuments(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentRef $vref) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function newEngineBlocks(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentRef $vref) {
|
||||
|
||||
$blocks = new PhabricatorDocumentEngineBlocks();
|
||||
|
||||
try {
|
||||
$u_blocks = $this->newDiffBlocks($uref);
|
||||
$v_blocks = $this->newDiffBlocks($vref);
|
||||
|
||||
$blocks->addBlockList($uref, $u_blocks);
|
||||
$blocks->addBlockList($vref, $v_blocks);
|
||||
} catch (Exception $ex) {
|
||||
$blocks->addMessage($ex->getMessage());
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
public function newBlockDiffViews(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentEngineBlock $ublock,
|
||||
PhabricatorDocumentRef $vref,
|
||||
PhabricatorDocumentEngineBlock $vblock) {
|
||||
|
||||
$ucell = $ublock->getContent();
|
||||
$vcell = $vblock->getContent();
|
||||
|
||||
$utype = idx($ucell, 'cell_type');
|
||||
$vtype = idx($vcell, 'cell_type');
|
||||
|
||||
if ($utype === $vtype) {
|
||||
switch ($utype) {
|
||||
case 'markdown':
|
||||
$usource = idx($ucell, 'source');
|
||||
$usource = implode('', $usource);
|
||||
|
||||
$vsource = idx($vcell, 'source');
|
||||
$vsource = implode('', $vsource);
|
||||
|
||||
$diff = id(new PhutilProseDifferenceEngine())
|
||||
->getDiff($usource, $vsource);
|
||||
|
||||
$u_content = $this->newProseDiffCell($diff, array('=', '-'));
|
||||
$v_content = $this->newProseDiffCell($diff, array('=', '+'));
|
||||
|
||||
$u_content = $this->newJupyterCell(null, $u_content, null);
|
||||
$v_content = $this->newJupyterCell(null, $v_content, null);
|
||||
|
||||
$u_content = $this->newCellContainer($u_content);
|
||||
$v_content = $this->newCellContainer($v_content);
|
||||
|
||||
return id(new PhabricatorDocumentEngineBlockDiff())
|
||||
->setOldContent($u_content)
|
||||
->addOldClass('old')
|
||||
->setNewContent($v_content)
|
||||
->addNewClass('new');
|
||||
}
|
||||
}
|
||||
|
||||
return parent::newBlockDiffViews($uref, $ublock, $vref, $vblock);
|
||||
}
|
||||
|
||||
public function newBlockContentView(
|
||||
PhabricatorDocumentRef $ref,
|
||||
PhabricatorDocumentEngineBlock $block) {
|
||||
|
||||
$viewer = $this->getViewer();
|
||||
$cell = $block->getContent();
|
||||
|
||||
$cell_content = $this->renderJupyterCell($viewer, $cell);
|
||||
|
||||
return $this->newCellContainer($cell_content);
|
||||
}
|
||||
|
||||
private function newCellContainer($cell_content) {
|
||||
$notebook_table = phutil_tag(
|
||||
'table',
|
||||
array(
|
||||
'class' => 'jupyter-notebook',
|
||||
),
|
||||
$cell_content);
|
||||
|
||||
$container = phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => 'document-engine-jupyter document-engine-diff',
|
||||
),
|
||||
$notebook_table);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
private function newProseDiffCell(PhutilProseDiff $diff, array $mask) {
|
||||
$mask = array_fuse($mask);
|
||||
|
||||
$result = array();
|
||||
foreach ($diff->getParts() as $part) {
|
||||
$type = $part['type'];
|
||||
$text = $part['text'];
|
||||
|
||||
if (!isset($mask[$type])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case '-':
|
||||
$result[] = phutil_tag(
|
||||
'span',
|
||||
array(
|
||||
'class' => 'bright',
|
||||
),
|
||||
$text);
|
||||
break;
|
||||
case '+':
|
||||
$result[] = phutil_tag(
|
||||
'span',
|
||||
array(
|
||||
'class' => 'bright',
|
||||
),
|
||||
$text);
|
||||
break;
|
||||
case '=':
|
||||
$result[] = $text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
null,
|
||||
phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => 'jupyter-cell-markdown',
|
||||
),
|
||||
$result),
|
||||
);
|
||||
}
|
||||
|
||||
private function newDiffBlocks(PhabricatorDocumentRef $ref) {
|
||||
$viewer = $this->getViewer();
|
||||
$content = $ref->loadData();
|
||||
|
||||
$cells = $this->newCells($content, true);
|
||||
|
||||
$idx = 1;
|
||||
$blocks = array();
|
||||
foreach ($cells as $cell) {
|
||||
// When the cell is a source code line, we can hash just the raw
|
||||
// input rather than all the cell metadata.
|
||||
|
||||
switch (idx($cell, 'cell_type')) {
|
||||
case 'code/line':
|
||||
$hash_input = $cell['raw'];
|
||||
break;
|
||||
case 'markdown':
|
||||
$hash_input = implode('', $cell['source']);
|
||||
break;
|
||||
default:
|
||||
$hash_input = serialize($cell);
|
||||
break;
|
||||
}
|
||||
|
||||
$hash = PhabricatorHash::digestWithNamedKey(
|
||||
$hash_input,
|
||||
'document-engine.content-digest');
|
||||
|
||||
$blocks[] = id(new PhabricatorDocumentEngineBlock())
|
||||
->setBlockKey($idx)
|
||||
->setDifferenceHash($hash)
|
||||
->setContent($cell);
|
||||
|
||||
$idx++;
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
protected function newDocumentContent(PhabricatorDocumentRef $ref) {
|
||||
$viewer = $this->getViewer();
|
||||
$content = $ref->loadData();
|
||||
|
||||
try {
|
||||
$data = phutil_json_decode($content);
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
return $this->newMessage(
|
||||
pht(
|
||||
'This is not a valid JSON document and can not be rendered as '.
|
||||
'a Jupyter notebook: %s.',
|
||||
$ex->getMessage()));
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
return $this->newMessage(
|
||||
pht(
|
||||
'This document does not encode a valid JSON object and can not '.
|
||||
'be rendered as a Jupyter notebook.'));
|
||||
}
|
||||
|
||||
|
||||
$nbformat = idx($data, 'nbformat');
|
||||
if (!strlen($nbformat)) {
|
||||
return $this->newMessage(
|
||||
pht(
|
||||
'This document is missing an "nbformat" field. Jupyter notebooks '.
|
||||
'must have this field.'));
|
||||
}
|
||||
|
||||
if ($nbformat !== 4) {
|
||||
return $this->newMessage(
|
||||
pht(
|
||||
'This Jupyter notebook uses an unsupported version of the file '.
|
||||
'format (found version %s, expected version 4).',
|
||||
$nbformat));
|
||||
}
|
||||
|
||||
$cells = idx($data, 'cells');
|
||||
if (!is_array($cells)) {
|
||||
return $this->newMessage(
|
||||
pht(
|
||||
'This Jupyter notebook does not specify a list of "cells".'));
|
||||
}
|
||||
|
||||
if (!$cells) {
|
||||
return $this->newMessage(
|
||||
pht(
|
||||
'This Jupyter notebook does not specify any notebook cells.'));
|
||||
$cells = $this->newCells($content, false);
|
||||
} catch (Exception $ex) {
|
||||
return $this->newMessage($ex->getMessage());
|
||||
}
|
||||
|
||||
$rows = array();
|
||||
|
@ -108,20 +251,169 @@ final class PhabricatorJupyterDocumentEngine
|
|||
return $container;
|
||||
}
|
||||
|
||||
private function newCells($content, $for_diff) {
|
||||
try {
|
||||
$data = phutil_json_decode($content);
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This is not a valid JSON document and can not be rendered as '.
|
||||
'a Jupyter notebook: %s.',
|
||||
$ex->getMessage()));
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This document does not encode a valid JSON object and can not '.
|
||||
'be rendered as a Jupyter notebook.'));
|
||||
}
|
||||
|
||||
|
||||
$nbformat = idx($data, 'nbformat');
|
||||
if (!strlen($nbformat)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This document is missing an "nbformat" field. Jupyter notebooks '.
|
||||
'must have this field.'));
|
||||
}
|
||||
|
||||
if ($nbformat !== 4) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This Jupyter notebook uses an unsupported version of the file '.
|
||||
'format (found version %s, expected version 4).',
|
||||
$nbformat));
|
||||
}
|
||||
|
||||
$cells = idx($data, 'cells');
|
||||
if (!is_array($cells)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This Jupyter notebook does not specify a list of "cells".'));
|
||||
}
|
||||
|
||||
if (!$cells) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This Jupyter notebook does not specify any notebook cells.'));
|
||||
}
|
||||
|
||||
if (!$for_diff) {
|
||||
return $cells;
|
||||
}
|
||||
|
||||
// If we're extracting cells to build a diff view, split code cells into
|
||||
// individual lines and individual outputs. We want users to be able to
|
||||
// add inline comments to each line and each output block.
|
||||
|
||||
$results = array();
|
||||
foreach ($cells as $cell) {
|
||||
$cell_type = idx($cell, 'cell_type');
|
||||
|
||||
if ($cell_type === 'markdown') {
|
||||
$source = $cell['source'];
|
||||
$source = implode('', $source);
|
||||
|
||||
// Attempt to split contiguous blocks of markdown into smaller
|
||||
// pieces.
|
||||
|
||||
$chunks = preg_split(
|
||||
'/\n\n+/',
|
||||
$source);
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
$result = $cell;
|
||||
$result['source'] = array($chunk);
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($cell_type !== 'code') {
|
||||
$results[] = $cell;
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = $this->newCellLabel($cell);
|
||||
|
||||
$lines = idx($cell, 'source');
|
||||
if (!is_array($lines)) {
|
||||
$lines = array();
|
||||
}
|
||||
|
||||
$content = $this->highlightLines($lines);
|
||||
|
||||
$count = count($lines);
|
||||
for ($ii = 0; $ii < $count; $ii++) {
|
||||
$is_head = ($ii === 0);
|
||||
$is_last = ($ii === ($count - 1));
|
||||
|
||||
if ($is_head) {
|
||||
$line_label = $label;
|
||||
} else {
|
||||
$line_label = null;
|
||||
}
|
||||
|
||||
$results[] = array(
|
||||
'cell_type' => 'code/line',
|
||||
'label' => $line_label,
|
||||
'raw' => $lines[$ii],
|
||||
'display' => idx($content, $ii),
|
||||
'head' => $is_head,
|
||||
'last' => $is_last,
|
||||
);
|
||||
}
|
||||
|
||||
$outputs = array();
|
||||
$output_list = idx($cell, 'outputs');
|
||||
if (is_array($output_list)) {
|
||||
foreach ($output_list as $output) {
|
||||
$results[] = array(
|
||||
'cell_type' => 'code/output',
|
||||
'output' => $output,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
private function renderJupyterCell(
|
||||
PhabricatorUser $viewer,
|
||||
array $cell) {
|
||||
|
||||
list($label, $content) = $this->renderJupyterCellContent($viewer, $cell);
|
||||
|
||||
$classes = null;
|
||||
switch (idx($cell, 'cell_type')) {
|
||||
case 'code/line':
|
||||
$classes = 'jupyter-cell-flush';
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->newJupyterCell(
|
||||
$label,
|
||||
$content,
|
||||
$classes);
|
||||
}
|
||||
|
||||
private function newJupyterCell($label, $content, $classes) {
|
||||
$label_cell = phutil_tag(
|
||||
'th',
|
||||
array(),
|
||||
'td',
|
||||
array(
|
||||
'class' => 'jupyter-label',
|
||||
),
|
||||
$label);
|
||||
|
||||
$content_cell = phutil_tag(
|
||||
'td',
|
||||
array(),
|
||||
array(
|
||||
'class' => $classes,
|
||||
),
|
||||
$content);
|
||||
|
||||
return phutil_tag(
|
||||
|
@ -142,10 +434,15 @@ final class PhabricatorJupyterDocumentEngine
|
|||
case 'markdown':
|
||||
return $this->newMarkdownCell($cell);
|
||||
case 'code':
|
||||
return $this->newCodeCell($cell);
|
||||
return $this->newCodeCell($cell);
|
||||
case 'code/line':
|
||||
return $this->newCodeLineCell($cell);
|
||||
case 'code/output':
|
||||
return $this->newCodeOutputCell($cell);
|
||||
}
|
||||
|
||||
return $this->newRawCell(id(new PhutilJSON())->encodeFormatted($cell));
|
||||
return $this->newRawCell(id(new PhutilJSON())
|
||||
->encodeFormatted($cell));
|
||||
}
|
||||
|
||||
private function newRawCell($content) {
|
||||
|
@ -166,8 +463,9 @@ final class PhabricatorJupyterDocumentEngine
|
|||
$content = array();
|
||||
}
|
||||
|
||||
$content = implode('', $content);
|
||||
$content = phutil_escape_html_newlines($content);
|
||||
// TODO: This should ideally highlight as Markdown, but the "md"
|
||||
// highlighter in Pygments is painfully slow and not terribly useful.
|
||||
$content = $this->highlightLines($content, 'txt');
|
||||
|
||||
return array(
|
||||
null,
|
||||
|
@ -181,23 +479,14 @@ final class PhabricatorJupyterDocumentEngine
|
|||
}
|
||||
|
||||
private function newCodeCell(array $cell) {
|
||||
$execution_count = idx($cell, 'execution_count');
|
||||
if ($execution_count) {
|
||||
$label = 'In ['.$execution_count.']:';
|
||||
} else {
|
||||
$label = null;
|
||||
}
|
||||
$label = $this->newCellLabel($cell);
|
||||
|
||||
$content = idx($cell, 'source');
|
||||
if (!is_array($content)) {
|
||||
$content = array();
|
||||
}
|
||||
|
||||
$content = implode('', $content);
|
||||
|
||||
$content = PhabricatorSyntaxHighlighter::highlightWithLanguage(
|
||||
'py',
|
||||
$content);
|
||||
$content = $this->highlightLines($content);
|
||||
|
||||
$outputs = array();
|
||||
$output_list = idx($cell, 'outputs');
|
||||
|
@ -213,7 +502,9 @@ final class PhabricatorJupyterDocumentEngine
|
|||
phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => 'jupyter-cell-code PhabricatorMonospaced remarkup-code',
|
||||
'class' =>
|
||||
'jupyter-cell-code jupyter-cell-code-block '.
|
||||
'PhabricatorMonospaced remarkup-code',
|
||||
),
|
||||
array(
|
||||
$content,
|
||||
|
@ -223,6 +514,45 @@ final class PhabricatorJupyterDocumentEngine
|
|||
);
|
||||
}
|
||||
|
||||
private function newCodeLineCell(array $cell) {
|
||||
$classes = array();
|
||||
$classes[] = 'PhabricatorMonospaced';
|
||||
$classes[] = 'remarkup-code';
|
||||
$classes[] = 'jupyter-cell-code';
|
||||
$classes[] = 'jupyter-cell-code-line';
|
||||
|
||||
if ($cell['head']) {
|
||||
$classes[] = 'jupyter-cell-code-head';
|
||||
}
|
||||
|
||||
if ($cell['last']) {
|
||||
$classes[] = 'jupyter-cell-code-last';
|
||||
}
|
||||
|
||||
$classes = implode(' ', $classes);
|
||||
|
||||
return array(
|
||||
$cell['label'],
|
||||
array(
|
||||
phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => $classes,
|
||||
),
|
||||
array(
|
||||
$cell['display'],
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private function newCodeOutputCell(array $cell) {
|
||||
return array(
|
||||
null,
|
||||
$this->newOutput($cell['output']),
|
||||
);
|
||||
}
|
||||
|
||||
private function newOutput(array $output) {
|
||||
if (!is_array($output)) {
|
||||
return pht('<Invalid Output>');
|
||||
|
@ -309,4 +639,50 @@ final class PhabricatorJupyterDocumentEngine
|
|||
$content);
|
||||
}
|
||||
|
||||
private function newCellLabel(array $cell) {
|
||||
$execution_count = idx($cell, 'execution_count');
|
||||
if ($execution_count) {
|
||||
$label = 'In ['.$execution_count.']:';
|
||||
} else {
|
||||
$label = null;
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
private function highlightLines(array $lines, $force_language = null) {
|
||||
if ($force_language === null) {
|
||||
$head = head($lines);
|
||||
$matches = null;
|
||||
if (preg_match('/^%%(.*)$/', $head, $matches)) {
|
||||
$restore = array_shift($lines);
|
||||
$lang = $matches[1];
|
||||
} else {
|
||||
$restore = null;
|
||||
$lang = 'py';
|
||||
}
|
||||
} else {
|
||||
$restore = null;
|
||||
$lang = $force_language;
|
||||
}
|
||||
|
||||
$content = PhabricatorSyntaxHighlighter::highlightWithLanguage(
|
||||
$lang,
|
||||
implode('', $lines));
|
||||
$content = phutil_split_lines($content);
|
||||
|
||||
if ($restore !== null) {
|
||||
$language_tag = phutil_tag(
|
||||
'span',
|
||||
array(
|
||||
'class' => 'language-tag',
|
||||
),
|
||||
$restore);
|
||||
|
||||
array_unshift($content, $language_tag);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -378,15 +378,15 @@ final class PhabricatorPasteQuery
|
|||
}
|
||||
|
||||
private function highlightSource($source, $title, $language) {
|
||||
if (empty($language)) {
|
||||
return PhabricatorSyntaxHighlighter::highlightWithFilename(
|
||||
$title,
|
||||
$source);
|
||||
} else {
|
||||
return PhabricatorSyntaxHighlighter::highlightWithLanguage(
|
||||
$language,
|
||||
$source);
|
||||
}
|
||||
if (empty($language)) {
|
||||
return PhabricatorSyntaxHighlighter::highlightWithFilename(
|
||||
$title,
|
||||
$source);
|
||||
} else {
|
||||
return PhabricatorSyntaxHighlighter::highlightWithLanguage(
|
||||
$language,
|
||||
$source);
|
||||
}
|
||||
}
|
||||
|
||||
public function getQueryApplicationClass() {
|
||||
|
|
|
@ -3,8 +3,13 @@
|
|||
final class PhabricatorPeopleRenameController
|
||||
extends PhabricatorPeopleController {
|
||||
|
||||
public function shouldRequireAdmin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$id = $request->getURIData('id');
|
||||
|
||||
$user = id(new PhabricatorPeopleQuery())
|
||||
|
@ -17,6 +22,25 @@ final class PhabricatorPeopleRenameController
|
|||
|
||||
$done_uri = $this->getApplicationURI("manage/{$id}/");
|
||||
|
||||
if (!$viewer->getIsAdmin()) {
|
||||
$dialog = $this->newDialog()
|
||||
->setTitle(pht('Change Username'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'You can not change usernames because you are not an '.
|
||||
'administrator. Only administrators can change usernames.'))
|
||||
->addCancelButton($done_uri, pht('Okay'));
|
||||
|
||||
$message_body = PhabricatorAuthMessage::loadMessageText(
|
||||
$viewer,
|
||||
PhabricatorAuthChangeUsernameMessageType::MESSAGEKEY);
|
||||
if (strlen($message_body)) {
|
||||
$dialog->appendRemarkup($message_body);
|
||||
}
|
||||
|
||||
return $dialog;
|
||||
}
|
||||
|
||||
$validation_exception = null;
|
||||
$username = $user->getUsername();
|
||||
if ($request->isFormOrHisecPost()) {
|
||||
|
@ -43,32 +67,25 @@ final class PhabricatorPeopleRenameController
|
|||
|
||||
}
|
||||
|
||||
$inst1 = pht(
|
||||
'Be careful when renaming users!');
|
||||
$instructions = array();
|
||||
|
||||
$inst2 = pht(
|
||||
'The old username will no longer be tied to the user, so anything '.
|
||||
'which uses it (like old commit messages) will no longer associate '.
|
||||
'correctly. (And, if you give a user a username which some other user '.
|
||||
'used to have, username lookups will begin returning the wrong user.)');
|
||||
$instructions[] = pht(
|
||||
'If you rename this user, the old username will no longer be tied '.
|
||||
'to the user account. Anything which uses the old username in raw '.
|
||||
'text (like old commit messages) may no longer associate correctly.');
|
||||
|
||||
$inst3 = pht(
|
||||
'It is generally safe to rename newly created users (and test users '.
|
||||
'and so on), but less safe to rename established users and unsafe to '.
|
||||
'reissue a username.');
|
||||
$instructions[] = pht(
|
||||
'It is generally safe to rename users, but changing usernames may '.
|
||||
'create occasional minor complications or confusion with text that '.
|
||||
'contains the old username.');
|
||||
|
||||
$inst4 = pht(
|
||||
'Users who rely on password authentication will need to reset their '.
|
||||
'password after their username is changed (their username is part of '.
|
||||
'the salt in the password hash).');
|
||||
|
||||
$inst5 = pht(
|
||||
$instructions[] = pht(
|
||||
'The user will receive an email notifying them that you changed their '.
|
||||
'username, with instructions for logging in and resetting their '.
|
||||
'password if necessary.');
|
||||
'username.');
|
||||
|
||||
$instructions[] = null;
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($viewer)
|
||||
->appendChild(
|
||||
id(new AphrontFormStaticControl())
|
||||
->setLabel(pht('Old Username'))
|
||||
|
@ -79,19 +96,20 @@ final class PhabricatorPeopleRenameController
|
|||
->setValue($username)
|
||||
->setName('username'));
|
||||
|
||||
return $this->newDialog()
|
||||
->setWidth(AphrontDialogView::WIDTH_FORM)
|
||||
$dialog = $this->newDialog()
|
||||
->setTitle(pht('Change Username'))
|
||||
->setValidationException($validation_exception)
|
||||
->appendParagraph($inst1)
|
||||
->appendParagraph($inst2)
|
||||
->appendParagraph($inst3)
|
||||
->appendParagraph($inst4)
|
||||
->appendParagraph($inst5)
|
||||
->appendParagraph(null)
|
||||
->setValidationException($validation_exception);
|
||||
|
||||
foreach ($instructions as $instruction) {
|
||||
$dialog->appendParagraph($instruction);
|
||||
}
|
||||
|
||||
$dialog
|
||||
->appendForm($form)
|
||||
->addSubmitButton(pht('Rename User'))
|
||||
->addCancelButton($done_uri);
|
||||
|
||||
return $dialog;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,16 @@ final class PhabricatorRepositoryRefEngine
|
|||
private $newPositions = array();
|
||||
private $deadPositions = array();
|
||||
private $closeCommits = array();
|
||||
private $hasNoCursors;
|
||||
private $rebuild;
|
||||
|
||||
public function setRebuild($rebuild) {
|
||||
$this->rebuild = $rebuild;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRebuild() {
|
||||
return $this->rebuild;
|
||||
}
|
||||
|
||||
public function updateRefs() {
|
||||
$this->newPositions = array();
|
||||
|
@ -60,15 +69,17 @@ final class PhabricatorRepositoryRefEngine
|
|||
->execute();
|
||||
$cursor_groups = mgroup($all_cursors, 'getRefType');
|
||||
|
||||
$this->hasNoCursors = (!$all_cursors);
|
||||
|
||||
// Find all the heads of closing refs.
|
||||
// Find all the heads of permanent refs.
|
||||
$all_closing_heads = array();
|
||||
foreach ($all_cursors as $cursor) {
|
||||
$should_close = $this->shouldCloseRef(
|
||||
$cursor->getRefType(),
|
||||
$cursor->getRefName());
|
||||
if (!$should_close) {
|
||||
|
||||
// See T13284. Note that we're considering whether this ref was a
|
||||
// permanent ref or not the last time we updated refs for this
|
||||
// repository. This allows us to handle things properly when a ref
|
||||
// is reconfigured from non-permanent to permanent.
|
||||
|
||||
$was_permanent = $cursor->getIsPermanent();
|
||||
if (!$was_permanent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -76,6 +87,7 @@ final class PhabricatorRepositoryRefEngine
|
|||
$all_closing_heads[] = $identifier;
|
||||
}
|
||||
}
|
||||
|
||||
$all_closing_heads = array_unique($all_closing_heads);
|
||||
$all_closing_heads = $this->removeMissingCommits($all_closing_heads);
|
||||
|
||||
|
@ -88,12 +100,18 @@ final class PhabricatorRepositoryRefEngine
|
|||
$this->setCloseFlagOnCommits($this->closeCommits);
|
||||
}
|
||||
|
||||
if ($this->newPositions || $this->deadPositions) {
|
||||
$save_cursors = $this->getCursorsForUpdate($all_cursors);
|
||||
|
||||
if ($this->newPositions || $this->deadPositions || $save_cursors) {
|
||||
$repository->openTransaction();
|
||||
|
||||
$this->saveNewPositions();
|
||||
$this->deleteDeadPositions();
|
||||
|
||||
foreach ($save_cursors as $cursor) {
|
||||
$cursor->save();
|
||||
}
|
||||
|
||||
$repository->saveTransaction();
|
||||
}
|
||||
|
||||
|
@ -103,6 +121,28 @@ final class PhabricatorRepositoryRefEngine
|
|||
}
|
||||
}
|
||||
|
||||
private function getCursorsForUpdate(array $cursors) {
|
||||
assert_instances_of($cursors, 'PhabricatorRepositoryRefCursor');
|
||||
|
||||
$results = array();
|
||||
|
||||
foreach ($cursors as $cursor) {
|
||||
$ref_type = $cursor->getRefType();
|
||||
$ref_name = $cursor->getRefName();
|
||||
|
||||
$is_permanent = $this->isPermanentRef($ref_type, $ref_name);
|
||||
|
||||
if ($is_permanent == $cursor->getIsPermanent()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cursor->setIsPermanent((int)$is_permanent);
|
||||
$results[] = $cursor;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function updateBranchStates(
|
||||
PhabricatorRepository $repository,
|
||||
array $branches) {
|
||||
|
@ -301,11 +341,41 @@ final class PhabricatorRepositoryRefEngine
|
|||
$this->markPositionNew($new_position);
|
||||
}
|
||||
|
||||
if ($this->shouldCloseRef($ref_type, $name)) {
|
||||
foreach ($added_commits as $identifier) {
|
||||
if ($this->isPermanentRef($ref_type, $name)) {
|
||||
|
||||
// See T13284. If this cursor was already marked as permanent, we
|
||||
// only need to publish the newly created ref positions. However, if
|
||||
// this cursor was not previously permanent but has become permanent,
|
||||
// we need to publish all the ref positions.
|
||||
|
||||
// This corresponds to users reconfiguring a branch to make it
|
||||
// permanent without pushing any new commits to it.
|
||||
|
||||
$is_rebuild = $this->getRebuild();
|
||||
$was_permanent = $ref_cursor->getIsPermanent();
|
||||
|
||||
if ($is_rebuild || !$was_permanent) {
|
||||
$update_all = true;
|
||||
} else {
|
||||
$update_all = false;
|
||||
}
|
||||
|
||||
if ($update_all) {
|
||||
$update_commits = $new_commits;
|
||||
} else {
|
||||
$update_commits = $added_commits;
|
||||
}
|
||||
|
||||
if ($is_rebuild) {
|
||||
$exclude = array();
|
||||
} else {
|
||||
$exclude = $all_closing_heads;
|
||||
}
|
||||
|
||||
foreach ($update_commits as $identifier) {
|
||||
$new_identifiers = $this->loadNewCommitIdentifiers(
|
||||
$identifier,
|
||||
$all_closing_heads);
|
||||
$exclude);
|
||||
|
||||
$this->markCloseCommits($new_identifiers);
|
||||
}
|
||||
|
@ -334,19 +404,11 @@ final class PhabricatorRepositoryRefEngine
|
|||
}
|
||||
}
|
||||
|
||||
private function shouldCloseRef($ref_type, $ref_name) {
|
||||
private function isPermanentRef($ref_type, $ref_name) {
|
||||
if ($ref_type !== PhabricatorRepositoryRefCursor::TYPE_BRANCH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasNoCursors) {
|
||||
// If we don't have any cursors, don't close things. Particularly, this
|
||||
// corresponds to the case where you've just updated to this code on an
|
||||
// existing repository: we don't want to requeue message steps for every
|
||||
// commit on a closeable ref.
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getRepository()->isBranchPermanentRef($ref_name);
|
||||
}
|
||||
|
||||
|
@ -505,10 +567,13 @@ final class PhabricatorRepositoryRefEngine
|
|||
$ref_type,
|
||||
$ref_name) {
|
||||
|
||||
$is_permanent = $this->isPermanentRef($ref_type, $ref_name);
|
||||
|
||||
$cursor = id(new PhabricatorRepositoryRefCursor())
|
||||
->setRepositoryPHID($repository->getPHID())
|
||||
->setRefType($ref_type)
|
||||
->setRefName($ref_name);
|
||||
->setRefName($ref_name)
|
||||
->setIsPermanent((int)$is_permanent);
|
||||
|
||||
try {
|
||||
return $cursor->save();
|
||||
|
|
|
@ -14,6 +14,12 @@ final class PhabricatorRepositoryManagementRefsWorkflow
|
|||
'name' => 'verbose',
|
||||
'help' => pht('Show additional debugging information.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'rebuild',
|
||||
'help' => pht(
|
||||
'Publish commits currently reachable from any permanent ref, '.
|
||||
'ignoring the cached ref state.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'repos',
|
||||
'wildcard' => true,
|
||||
|
@ -41,6 +47,7 @@ final class PhabricatorRepositoryManagementRefsWorkflow
|
|||
$engine = id(new PhabricatorRepositoryRefEngine())
|
||||
->setRepository($repo)
|
||||
->setVerbose($args->getArg('verbose'))
|
||||
->setRebuild($args->getArg('rebuild'))
|
||||
->updateRefs();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ final class PhabricatorRepositoryRefCursor
|
|||
protected $refNameHash;
|
||||
protected $refNameRaw;
|
||||
protected $refNameEncoding;
|
||||
protected $isPermanent;
|
||||
|
||||
private $repository = self::ATTACHABLE;
|
||||
private $positions = self::ATTACHABLE;
|
||||
|
@ -34,6 +35,7 @@ final class PhabricatorRepositoryRefCursor
|
|||
'refType' => 'text32',
|
||||
'refNameHash' => 'bytes12',
|
||||
'refNameEncoding' => 'text16?',
|
||||
'isPermanent' => 'bool',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_ref' => array(
|
||||
|
|
|
@ -22,6 +22,7 @@ final class PhabricatorSystemApplication extends PhabricatorApplication {
|
|||
'/services/' => array(
|
||||
'encoding/' => 'PhabricatorSystemSelectEncodingController',
|
||||
'highlight/' => 'PhabricatorSystemSelectHighlightController',
|
||||
'viewas/' => 'PhabricatorSystemSelectViewAsController',
|
||||
),
|
||||
'/readonly/' => array(
|
||||
'(?P<reason>[^/]+)/' => 'PhabricatorSystemReadOnlyController',
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorSystemSelectViewAsController
|
||||
extends PhabricatorController {
|
||||
|
||||
public function shouldRequireLogin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $this->getViewer();
|
||||
$v_engine = $request->getStr('engine');
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
$result = array('engine' => $v_engine);
|
||||
return id(new AphrontAjaxResponse())->setContent($result);
|
||||
}
|
||||
|
||||
$engines = PhabricatorDocumentEngine::getAllEngines();
|
||||
|
||||
|
||||
// TODO: This controller isn't very good because the valid options depend
|
||||
// on the file being rendered and most of them can't even diff anything,
|
||||
// and this ref is completely bogus.
|
||||
|
||||
// For now, we just show everything.
|
||||
$ref = new PhabricatorDocumentRef();
|
||||
|
||||
$map = array();
|
||||
foreach ($engines as $engine) {
|
||||
$key = $engine->getDocumentEngineKey();
|
||||
$label = $engine->getViewAsLabel($ref);
|
||||
|
||||
if (!strlen($label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$key] = $label;
|
||||
}
|
||||
|
||||
asort($map);
|
||||
|
||||
$map = array(
|
||||
'' => pht('(Use Default)'),
|
||||
) + $map;
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setViewer($viewer)
|
||||
->appendRemarkupInstructions(pht('Choose a document engine to use.'))
|
||||
->appendChild(
|
||||
id(new AphrontFormSelectControl())
|
||||
->setLabel(pht('View As'))
|
||||
->setName('engine')
|
||||
->setValue($v_engine)
|
||||
->setOptions($map));
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Select Document Engine'))
|
||||
->appendForm($form)
|
||||
->addSubmitButton(pht('Choose Engine'))
|
||||
->addCancelButton('/');
|
||||
}
|
||||
}
|
|
@ -22,7 +22,8 @@ final class BulkTokenizerParameterType
|
|||
$template = new AphrontTokenizerTemplateView();
|
||||
$template_markup = $template->render();
|
||||
|
||||
$datasource = $this->getDatasource();
|
||||
$datasource = $this->getDatasource()
|
||||
->setViewer($this->getViewer());
|
||||
|
||||
return array(
|
||||
'markup' => (string)hsprintf('%s', $template_markup),
|
||||
|
|
|
@ -2523,6 +2523,8 @@ abstract class PhabricatorEditEngine
|
|||
}
|
||||
|
||||
final public function newBulkEditMap() {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$config = $this->loadDefaultConfiguration();
|
||||
if (!$config) {
|
||||
throw new Exception(
|
||||
|
@ -2542,6 +2544,8 @@ abstract class PhabricatorEditEngine
|
|||
continue;
|
||||
}
|
||||
|
||||
$bulk_type->setViewer($viewer);
|
||||
|
||||
$bulk_label = $type->getBulkEditLabel();
|
||||
if ($bulk_label === null) {
|
||||
continue;
|
||||
|
|
|
@ -715,6 +715,18 @@ Press {key down down-right right LP} to activate the hadoken technique.
|
|||
> Press {key down down-right right LP} to activate the hadoken technique.
|
||||
|
||||
|
||||
Anchors
|
||||
========
|
||||
|
||||
You can use `{anchor #xyz}` to create a document anchor and later link to
|
||||
it directly with `#xyz` in the URI.
|
||||
|
||||
Headers also automatically create named anchors.
|
||||
|
||||
If you navigate to `#xyz` in your browser location bar, the page will scroll
|
||||
to the first anchor with "xyz" as a prefix of the anchor name.
|
||||
|
||||
|
||||
= Fullscreen Mode =
|
||||
|
||||
Remarkup editors provide a fullscreen composition mode. This can make it easier
|
||||
|
|
292
src/infrastructure/diff/prose/PhutilProseDiff.php
Normal file
292
src/infrastructure/diff/prose/PhutilProseDiff.php
Normal file
|
@ -0,0 +1,292 @@
|
|||
<?php
|
||||
|
||||
final class PhutilProseDiff extends Phobject {
|
||||
|
||||
private $parts = array();
|
||||
|
||||
public function addPart($type, $text) {
|
||||
$this->parts[] = array(
|
||||
'type' => $type,
|
||||
'text' => $text,
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParts() {
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diff parts, but replace large blocks of unchanged text with "."
|
||||
* parts representing missing context.
|
||||
*/
|
||||
public function getSummaryParts() {
|
||||
$parts = $this->getParts();
|
||||
|
||||
$head_key = head_key($parts);
|
||||
$last_key = last_key($parts);
|
||||
|
||||
$results = array();
|
||||
foreach ($parts as $key => $part) {
|
||||
$is_head = ($key == $head_key);
|
||||
$is_last = ($key == $last_key);
|
||||
|
||||
switch ($part['type']) {
|
||||
case '=':
|
||||
$pieces = $this->splitTextForSummary($part['text']);
|
||||
|
||||
if ($is_head || $is_last) {
|
||||
$need = 2;
|
||||
} else {
|
||||
$need = 3;
|
||||
}
|
||||
|
||||
// We don't have enough pieces to omit anything, so just continue.
|
||||
if (count($pieces) < $need) {
|
||||
$results[] = $part;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$is_head) {
|
||||
$results[] = array(
|
||||
'type' => '=',
|
||||
'text' => head($pieces),
|
||||
);
|
||||
}
|
||||
|
||||
$results[] = array(
|
||||
'type' => '.',
|
||||
'text' => null,
|
||||
);
|
||||
|
||||
if (!$is_last) {
|
||||
$results[] = array(
|
||||
'type' => '=',
|
||||
'text' => last($pieces),
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$results[] = $part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
public function reorderParts() {
|
||||
// Reorder sequences of removed and added sections to put all the "-"
|
||||
// parts together first, then all the "+" parts together. This produces
|
||||
// a more human-readable result than intermingling them.
|
||||
|
||||
$o_run = array();
|
||||
$n_run = array();
|
||||
$result = array();
|
||||
foreach ($this->parts as $part) {
|
||||
$type = $part['type'];
|
||||
switch ($type) {
|
||||
case '-':
|
||||
$o_run[] = $part;
|
||||
break;
|
||||
case '+':
|
||||
$n_run[] = $part;
|
||||
break;
|
||||
default:
|
||||
if ($o_run || $n_run) {
|
||||
foreach ($this->combineRuns($o_run, $n_run) as $merged_part) {
|
||||
$result[] = $merged_part;
|
||||
}
|
||||
$o_run = array();
|
||||
$n_run = array();
|
||||
}
|
||||
$result[] = $part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($o_run || $n_run) {
|
||||
foreach ($this->combineRuns($o_run, $n_run) as $part) {
|
||||
$result[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
// Now, combine consecuitive runs of the same type of change (like a
|
||||
// series of "-" parts) into a single run.
|
||||
$combined = array();
|
||||
|
||||
$last = null;
|
||||
$last_text = null;
|
||||
foreach ($result as $part) {
|
||||
$type = $part['type'];
|
||||
|
||||
if ($last !== $type) {
|
||||
if ($last !== null) {
|
||||
$combined[] = array(
|
||||
'type' => $last,
|
||||
'text' => $last_text,
|
||||
);
|
||||
}
|
||||
$last_text = null;
|
||||
$last = $type;
|
||||
}
|
||||
|
||||
$last_text .= $part['text'];
|
||||
}
|
||||
|
||||
if ($last_text !== null) {
|
||||
$combined[] = array(
|
||||
'type' => $last,
|
||||
'text' => $last_text,
|
||||
);
|
||||
}
|
||||
|
||||
$this->parts = $combined;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function combineRuns($o_run, $n_run) {
|
||||
$o_merge = $this->mergeParts($o_run);
|
||||
$n_merge = $this->mergeParts($n_run);
|
||||
|
||||
// When removed and added blocks share a prefix or suffix, we sometimes
|
||||
// want to count it as unchanged (for example, if it is whitespace) but
|
||||
// sometimes want to count it as changed (for example, if it is a word
|
||||
// suffix like "ing"). Find common prefixes and suffixes of these layout
|
||||
// characters and emit them as "=" (unchanged) blocks.
|
||||
|
||||
$layout_characters = array(
|
||||
' ' => true,
|
||||
"\n" => true,
|
||||
'.' => true,
|
||||
'!' => true,
|
||||
',' => true,
|
||||
'?' => true,
|
||||
']' => true,
|
||||
'[' => true,
|
||||
'(' => true,
|
||||
')' => true,
|
||||
'<' => true,
|
||||
'>' => true,
|
||||
);
|
||||
|
||||
$o_text = $o_merge['text'];
|
||||
$n_text = $n_merge['text'];
|
||||
$o_len = strlen($o_text);
|
||||
$n_len = strlen($n_text);
|
||||
$min_len = min($o_len, $n_len);
|
||||
|
||||
$prefix_len = 0;
|
||||
for ($pos = 0; $pos < $min_len; $pos++) {
|
||||
$o = $o_text[$pos];
|
||||
$n = $n_text[$pos];
|
||||
if ($o !== $n) {
|
||||
break;
|
||||
}
|
||||
if (empty($layout_characters[$o])) {
|
||||
break;
|
||||
}
|
||||
$prefix_len++;
|
||||
}
|
||||
|
||||
$suffix_len = 0;
|
||||
for ($pos = 0; $pos < ($min_len - $prefix_len); $pos++) {
|
||||
$o = $o_text[$o_len - ($pos + 1)];
|
||||
$n = $n_text[$n_len - ($pos + 1)];
|
||||
if ($o !== $n) {
|
||||
break;
|
||||
}
|
||||
if (empty($layout_characters[$o])) {
|
||||
break;
|
||||
}
|
||||
$suffix_len++;
|
||||
}
|
||||
|
||||
$results = array();
|
||||
|
||||
if ($prefix_len) {
|
||||
$results[] = array(
|
||||
'type' => '=',
|
||||
'text' => substr($o_text, 0, $prefix_len),
|
||||
);
|
||||
}
|
||||
|
||||
if ($prefix_len < $o_len) {
|
||||
$results[] = array(
|
||||
'type' => '-',
|
||||
'text' => substr(
|
||||
$o_text,
|
||||
$prefix_len,
|
||||
$o_len - $prefix_len - $suffix_len),
|
||||
);
|
||||
}
|
||||
|
||||
if ($prefix_len < $n_len) {
|
||||
$results[] = array(
|
||||
'type' => '+',
|
||||
'text' => substr(
|
||||
$n_text,
|
||||
$prefix_len,
|
||||
$n_len - $prefix_len - $suffix_len),
|
||||
);
|
||||
}
|
||||
|
||||
if ($suffix_len) {
|
||||
$results[] = array(
|
||||
'type' => '=',
|
||||
'text' => substr($o_text, -$suffix_len),
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function mergeParts(array $parts) {
|
||||
$text = '';
|
||||
$type = null;
|
||||
foreach ($parts as $part) {
|
||||
$part_type = $part['type'];
|
||||
if ($type === null) {
|
||||
$type = $part_type;
|
||||
}
|
||||
if ($type !== $part_type) {
|
||||
throw new Exception(pht('Can not merge parts of dissimilar types!'));
|
||||
}
|
||||
$text .= $part['text'];
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => $type,
|
||||
'text' => $text,
|
||||
);
|
||||
}
|
||||
|
||||
private function splitTextForSummary($text) {
|
||||
$matches = null;
|
||||
|
||||
$ok = preg_match('/^(\n*[^\n]+)\n/', $text, $matches);
|
||||
if (!$ok) {
|
||||
return array($text);
|
||||
}
|
||||
|
||||
$head = $matches[1];
|
||||
$text = substr($text, strlen($head));
|
||||
|
||||
$ok = preg_match('/\n([^\n]+\n*)\z/', $text, $matches);
|
||||
if (!$ok) {
|
||||
return array($text);
|
||||
}
|
||||
|
||||
$last = $matches[1];
|
||||
$text = substr($text, 0, -strlen($last));
|
||||
|
||||
if (!strlen(trim($text))) {
|
||||
return array($head, $last);
|
||||
} else {
|
||||
return array($head, $text, $last);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
275
src/infrastructure/diff/prose/PhutilProseDifferenceEngine.php
Normal file
275
src/infrastructure/diff/prose/PhutilProseDifferenceEngine.php
Normal file
|
@ -0,0 +1,275 @@
|
|||
<?php
|
||||
|
||||
final class PhutilProseDifferenceEngine extends Phobject {
|
||||
|
||||
public function getDiff($u, $v) {
|
||||
return $this->buildDiff($u, $v, 0);
|
||||
}
|
||||
|
||||
private function buildDiff($u, $v, $level) {
|
||||
$u_parts = $this->splitCorpus($u, $level);
|
||||
$v_parts = $this->splitCorpus($v, $level);
|
||||
|
||||
if ($level === 0) {
|
||||
$diff = $this->newHashDiff($u_parts, $v_parts);
|
||||
$too_large = false;
|
||||
} else {
|
||||
list($diff, $too_large) = $this->newEditDistanceMatrixDiff(
|
||||
$u_parts,
|
||||
$v_parts,
|
||||
$level);
|
||||
}
|
||||
|
||||
$diff->reorderParts();
|
||||
|
||||
// If we just built a character-level diff, we're all done and do not
|
||||
// need to go any deeper.
|
||||
if ($level == 3) {
|
||||
return $diff;
|
||||
}
|
||||
|
||||
$blocks = array();
|
||||
$block = null;
|
||||
foreach ($diff->getParts() as $part) {
|
||||
$type = $part['type'];
|
||||
$text = $part['text'];
|
||||
switch ($type) {
|
||||
case '=':
|
||||
if ($block) {
|
||||
$blocks[] = $block;
|
||||
$block = null;
|
||||
}
|
||||
$blocks[] = array(
|
||||
'type' => $type,
|
||||
'text' => $text,
|
||||
);
|
||||
break;
|
||||
case '-':
|
||||
if (!$block) {
|
||||
$block = array(
|
||||
'type' => '!',
|
||||
'old' => '',
|
||||
'new' => '',
|
||||
);
|
||||
}
|
||||
$block['old'] .= $text;
|
||||
break;
|
||||
case '+':
|
||||
if (!$block) {
|
||||
$block = array(
|
||||
'type' => '!',
|
||||
'old' => '',
|
||||
'new' => '',
|
||||
);
|
||||
}
|
||||
$block['new'] .= $text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($block) {
|
||||
$blocks[] = $block;
|
||||
}
|
||||
|
||||
$result = new PhutilProseDiff();
|
||||
foreach ($blocks as $block) {
|
||||
$type = $block['type'];
|
||||
if ($type == '=') {
|
||||
$result->addPart('=', $block['text']);
|
||||
} else {
|
||||
$old = $block['old'];
|
||||
$new = $block['new'];
|
||||
if (!strlen($old) && !strlen($new)) {
|
||||
// Nothing to do.
|
||||
} else if (!strlen($old)) {
|
||||
$result->addPart('+', $new);
|
||||
} else if (!strlen($new)) {
|
||||
$result->addPart('-', $old);
|
||||
} else {
|
||||
if ($too_large) {
|
||||
// If this text was too big to diff, don't try to subdivide it.
|
||||
$result->addPart('-', $old);
|
||||
$result->addPart('+', $new);
|
||||
} else {
|
||||
$subdiff = $this->buildDiff(
|
||||
$old,
|
||||
$new,
|
||||
$level + 1);
|
||||
|
||||
foreach ($subdiff->getParts() as $part) {
|
||||
$result->addPart($part['type'], $part['text']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result->reorderParts();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function splitCorpus($corpus, $level) {
|
||||
switch ($level) {
|
||||
case 0:
|
||||
// Level 0: Split into paragraphs.
|
||||
$expr = '/([\n]+)/';
|
||||
break;
|
||||
case 1:
|
||||
// Level 1: Split into sentences.
|
||||
$expr = '/([\n,!;?\.]+)/';
|
||||
break;
|
||||
case 2:
|
||||
// Level 2: Split into words.
|
||||
$expr = '/(\s+)/';
|
||||
break;
|
||||
case 3:
|
||||
// Level 3: Split into characters.
|
||||
return phutil_utf8v_combined($corpus);
|
||||
}
|
||||
|
||||
$pieces = preg_split($expr, $corpus, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
return $this->stitchPieces($pieces, $level);
|
||||
}
|
||||
|
||||
private function stitchPieces(array $pieces, $level) {
|
||||
$results = array();
|
||||
$count = count($pieces);
|
||||
for ($ii = 0; $ii < $count; $ii += 2) {
|
||||
$result = $pieces[$ii];
|
||||
if ($ii + 1 < $count) {
|
||||
$result .= $pieces[$ii + 1];
|
||||
}
|
||||
|
||||
if ($level < 2) {
|
||||
// Split pieces into separate text and whitespace sections: make one
|
||||
// piece out of all the whitespace at the beginning, one piece out of
|
||||
// all the actual text in the middle, and one piece out of all the
|
||||
// whitespace at the end.
|
||||
|
||||
$matches = null;
|
||||
preg_match('/^(\s*)(.*?)(\s*)\z/', $result, $matches);
|
||||
|
||||
if (strlen($matches[1])) {
|
||||
$results[] = $matches[1];
|
||||
}
|
||||
if (strlen($matches[2])) {
|
||||
$results[] = $matches[2];
|
||||
}
|
||||
if (strlen($matches[3])) {
|
||||
$results[] = $matches[3];
|
||||
}
|
||||
} else {
|
||||
$results[] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
// If the input ended with a delimiter, we can get an empty final piece.
|
||||
// Just discard it.
|
||||
if (last($results) == '') {
|
||||
array_pop($results);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function newEditDistanceMatrixDiff(
|
||||
array $u_parts,
|
||||
array $v_parts,
|
||||
$level) {
|
||||
|
||||
$matrix = id(new PhutilEditDistanceMatrix())
|
||||
->setMaximumLength(128)
|
||||
->setSequences($u_parts, $v_parts)
|
||||
->setComputeString(true);
|
||||
|
||||
// For word-level and character-level changes, smooth the output string
|
||||
// to reduce the choppiness of the diff.
|
||||
if ($level > 1) {
|
||||
$matrix->setApplySmoothing(PhutilEditDistanceMatrix::SMOOTHING_FULL);
|
||||
}
|
||||
|
||||
$u_pos = 0;
|
||||
$v_pos = 0;
|
||||
|
||||
$edits = $matrix->getEditString();
|
||||
$edits_length = strlen($edits);
|
||||
|
||||
$diff = new PhutilProseDiff();
|
||||
for ($ii = 0; $ii < $edits_length; $ii++) {
|
||||
$c = $edits[$ii];
|
||||
if ($c == 's') {
|
||||
$diff->addPart('=', $u_parts[$u_pos]);
|
||||
$u_pos++;
|
||||
$v_pos++;
|
||||
} else if ($c == 'd') {
|
||||
$diff->addPart('-', $u_parts[$u_pos]);
|
||||
$u_pos++;
|
||||
} else if ($c == 'i') {
|
||||
$diff->addPart('+', $v_parts[$v_pos]);
|
||||
$v_pos++;
|
||||
} else if ($c == 'x') {
|
||||
$diff->addPart('-', $u_parts[$u_pos]);
|
||||
$diff->addPart('+', $v_parts[$v_pos]);
|
||||
$u_pos++;
|
||||
$v_pos++;
|
||||
} else {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Unexpected character ("%s") in edit string.',
|
||||
$c));
|
||||
}
|
||||
}
|
||||
|
||||
return array($diff, $matrix->didReachMaximumLength());
|
||||
}
|
||||
|
||||
private function newHashDiff(array $u_parts, array $v_parts) {
|
||||
|
||||
$u_ref = new PhabricatorDocumentRef();
|
||||
$v_ref = new PhabricatorDocumentRef();
|
||||
|
||||
$u_blocks = $this->newDocumentEngineBlocks($u_parts);
|
||||
$v_blocks = $this->newDocumentEngineBlocks($v_parts);
|
||||
|
||||
$rows = id(new PhabricatorDocumentEngineBlocks())
|
||||
->addBlockList($u_ref, $u_blocks)
|
||||
->addBlockList($v_ref, $v_blocks)
|
||||
->newTwoUpLayout();
|
||||
|
||||
$diff = new PhutilProseDiff();
|
||||
foreach ($rows as $row) {
|
||||
list($u_block, $v_block) = $row;
|
||||
|
||||
if ($u_block && $v_block) {
|
||||
if ($u_block->getDifferenceType() === '-') {
|
||||
$diff->addPart('-', $u_block->getContent());
|
||||
$diff->addPart('+', $v_block->getContent());
|
||||
} else {
|
||||
$diff->addPart('=', $u_block->getContent());
|
||||
}
|
||||
} else if ($u_block) {
|
||||
$diff->addPart('-', $u_block->getContent());
|
||||
} else {
|
||||
$diff->addPart('+', $v_block->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
private function newDocumentEngineBlocks(array $parts) {
|
||||
$blocks = array();
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$hash = PhabricatorHash::digestForIndex($part);
|
||||
|
||||
$blocks[] = id(new PhabricatorDocumentEngineBlock())
|
||||
->setContent($part)
|
||||
->setDifferenceHash($hash);
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,246 @@
|
|||
<?php
|
||||
|
||||
final class PhutilProseDiffTestCase
|
||||
extends PhabricatorTestCase {
|
||||
|
||||
public function testProseDiffsDistance() {
|
||||
$this->assertProseParts(
|
||||
'',
|
||||
'',
|
||||
array(),
|
||||
pht('Empty'));
|
||||
|
||||
$this->assertProseParts(
|
||||
"xxx\nyyy",
|
||||
"xxx\nzzz\nyyy",
|
||||
array(
|
||||
"= xxx\n",
|
||||
"+ zzz\n",
|
||||
'= yyy',
|
||||
),
|
||||
pht('Add Paragraph'));
|
||||
|
||||
$this->assertProseParts(
|
||||
"xxx\nzzz\nyyy",
|
||||
"xxx\nyyy",
|
||||
array(
|
||||
"= xxx\n",
|
||||
"- zzz\n",
|
||||
'= yyy',
|
||||
),
|
||||
pht('Remove Paragraph'));
|
||||
|
||||
|
||||
// Without smoothing, the alogorithm identifies that "shark" and "cat"
|
||||
// both contain the letter "a" and tries to express this as a very
|
||||
// fine-grained edit which replaces "sh" with "c" and then "rk" with "t".
|
||||
// This is technically correct, but it is much easier for human viewers to
|
||||
// parse if we smooth this into a single removal and a single addition.
|
||||
|
||||
$this->assertProseParts(
|
||||
'They say the shark has nine lives.',
|
||||
'They say the cat has nine lives.',
|
||||
array(
|
||||
'= They say the ',
|
||||
'- shark',
|
||||
'+ cat',
|
||||
'= has nine lives.',
|
||||
),
|
||||
pht('"Shark/cat" word edit smoothenss.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
'Rising quickly, she says',
|
||||
'Rising quickly, she remarks:',
|
||||
array(
|
||||
'= Rising quickly, she ',
|
||||
'- says',
|
||||
'+ remarks:',
|
||||
),
|
||||
pht('"Says/remarks" word edit smoothenss.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
'See screenshots',
|
||||
'Viewed video files',
|
||||
array(
|
||||
'- See screenshots',
|
||||
'+ Viewed video files',
|
||||
),
|
||||
pht('Complete paragraph rewrite.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
'xaaax',
|
||||
'xbbbx',
|
||||
array(
|
||||
'- xaaax',
|
||||
'+ xbbbx',
|
||||
),
|
||||
pht('Whole word rewrite with common prefix and suffix.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
' aaa ',
|
||||
' bbb ',
|
||||
array(
|
||||
'= ',
|
||||
'- aaa',
|
||||
'+ bbb',
|
||||
'= ',
|
||||
),
|
||||
pht('Whole word rewrite with whitespace prefix and suffix.'));
|
||||
|
||||
$this->assertSummaryProseParts(
|
||||
"a\nb\nc\nd\ne\nf\ng\nh\n",
|
||||
"a\nb\nc\nd\nX\nf\ng\nh\n",
|
||||
array(
|
||||
'.',
|
||||
"= d\n",
|
||||
'- e',
|
||||
'+ X',
|
||||
"= \nf",
|
||||
'.',
|
||||
),
|
||||
pht('Summary diff with middle change.'));
|
||||
|
||||
$this->assertSummaryProseParts(
|
||||
"a\nb\nc\nd\ne\nf\ng\nh\n",
|
||||
"X\nb\nc\nd\ne\nf\ng\nh\n",
|
||||
array(
|
||||
'- a',
|
||||
'+ X',
|
||||
"= \nb",
|
||||
'.',
|
||||
),
|
||||
pht('Summary diff with head change.'));
|
||||
|
||||
$this->assertSummaryProseParts(
|
||||
"a\nb\nc\nd\ne\nf\ng\nh\n",
|
||||
"a\nb\nc\nd\ne\nf\ng\nX\n",
|
||||
array(
|
||||
'.',
|
||||
"= g\n",
|
||||
'- h',
|
||||
'+ X',
|
||||
"= \n",
|
||||
),
|
||||
pht('Summary diff with last change.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
'aaa aaa aaa aaa, bbb bbb bbb bbb.',
|
||||
"aaa aaa aaa aaa, bbb bbb bbb bbb.\n\n- ccc ccc ccc",
|
||||
array(
|
||||
'= aaa aaa aaa aaa, bbb bbb bbb bbb.',
|
||||
"+ \n\n- ccc ccc ccc",
|
||||
),
|
||||
pht('Diff with new trailing content.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
'aaa aaa aaa aaa, bbb bbb bbb bbb.',
|
||||
'aaa aaa aaa aaa bbb bbb bbb bbb.',
|
||||
array(
|
||||
'= aaa aaa aaa aaa',
|
||||
'- ,',
|
||||
'= bbb bbb bbb bbb.',
|
||||
),
|
||||
pht('Diff with a removed comma.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
'aaa aaa aaa aaa, bbb bbb bbb bbb.',
|
||||
"aaa aaa aaa aaa bbb bbb bbb bbb.\n\n- ccc ccc ccc!",
|
||||
array(
|
||||
'= aaa aaa aaa aaa',
|
||||
'- ,',
|
||||
'= bbb bbb bbb bbb.',
|
||||
"+ \n\n- ccc ccc ccc!",
|
||||
),
|
||||
pht('Diff with a removed comma and new trailing content.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
'[ ] Walnuts',
|
||||
'[X] Walnuts',
|
||||
array(
|
||||
'= [',
|
||||
'- ',
|
||||
'+ X',
|
||||
'= ] Walnuts',
|
||||
),
|
||||
pht('Diff adding a tickmark to a checkbox list.'));
|
||||
|
||||
$this->assertProseParts(
|
||||
'[[ ./week49 ]]',
|
||||
'[[ ./week50 ]]',
|
||||
array(
|
||||
'= [[ ./week',
|
||||
'- 49',
|
||||
'+ 50',
|
||||
'= ]]',
|
||||
),
|
||||
pht('Diff changing a remarkup wiki link target.'));
|
||||
|
||||
// Create a large corpus with many sentences and paragraphs.
|
||||
$large_paragraph = 'xyz. ';
|
||||
$large_paragraph = str_repeat($large_paragraph, 50);
|
||||
$large_paragraph = rtrim($large_paragraph);
|
||||
|
||||
$large_corpus = $large_paragraph."\n\n";
|
||||
$large_corpus = str_repeat($large_corpus, 50);
|
||||
$large_corpus = rtrim($large_corpus);
|
||||
|
||||
$this->assertProseParts(
|
||||
$large_corpus,
|
||||
"aaa\n\n".$large_corpus."\n\nzzz",
|
||||
array(
|
||||
"+ aaa\n\n",
|
||||
'= '.$large_corpus,
|
||||
"+ \n\nzzz",
|
||||
),
|
||||
pht('Adding initial and final lines to a large corpus.'));
|
||||
|
||||
}
|
||||
|
||||
private function assertProseParts($old, $new, array $expect_parts, $label) {
|
||||
$engine = new PhutilProseDifferenceEngine();
|
||||
$diff = $engine->getDiff($old, $new);
|
||||
|
||||
$parts = $diff->getParts();
|
||||
|
||||
$this->assertParts($expect_parts, $parts, $label);
|
||||
}
|
||||
|
||||
private function assertSummaryProseParts(
|
||||
$old,
|
||||
$new,
|
||||
array $expect_parts,
|
||||
$label) {
|
||||
|
||||
$engine = new PhutilProseDifferenceEngine();
|
||||
$diff = $engine->getDiff($old, $new);
|
||||
|
||||
$parts = $diff->getSummaryParts();
|
||||
|
||||
$this->assertParts($expect_parts, $parts, $label);
|
||||
}
|
||||
|
||||
private function assertParts(
|
||||
array $expect,
|
||||
array $actual_parts,
|
||||
$label) {
|
||||
|
||||
$actual = array();
|
||||
foreach ($actual_parts as $actual_part) {
|
||||
$type = $actual_part['type'];
|
||||
$text = $actual_part['text'];
|
||||
|
||||
switch ($type) {
|
||||
case '.':
|
||||
$actual[] = $type;
|
||||
break;
|
||||
default:
|
||||
$actual[] = "{$type} {$text}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertEqual($expect, $actual, $label);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -17,7 +17,7 @@ final class PHUIDiffOneUpInlineCommentRowScaffold
|
|||
$inline = head($inlines);
|
||||
|
||||
$attrs = array(
|
||||
'colspan' => 3,
|
||||
'colspan' => 2,
|
||||
'id' => $inline->getScaffoldCellID(),
|
||||
);
|
||||
|
||||
|
@ -32,6 +32,7 @@ final class PHUIDiffOneUpInlineCommentRowScaffold
|
|||
$cells = array(
|
||||
phutil_tag('td', array('class' => 'n'), $left_hidden),
|
||||
phutil_tag('td', array('class' => 'n'), $right_hidden),
|
||||
phutil_tag('td', array('class' => 'copy')),
|
||||
phutil_tag('td', $attrs, $inline),
|
||||
);
|
||||
|
||||
|
|
|
@ -84,9 +84,18 @@ final class ManiphestTaskGraph
|
|||
' ',
|
||||
$link,
|
||||
);
|
||||
|
||||
$subtype_tag = null;
|
||||
|
||||
$subtype = $object->newSubtypeObject();
|
||||
if ($subtype && $subtype->hasTagView()) {
|
||||
$subtype_tag = $subtype->newTagView()
|
||||
->setSlimShady(true);
|
||||
}
|
||||
} else {
|
||||
$status = null;
|
||||
$assigned = null;
|
||||
$subtype_tag = null;
|
||||
$link = $viewer->renderHandle($phid);
|
||||
}
|
||||
|
||||
|
@ -115,18 +124,23 @@ final class ManiphestTaskGraph
|
|||
$marker,
|
||||
$trace,
|
||||
$status,
|
||||
$subtype_tag,
|
||||
$assigned,
|
||||
$link,
|
||||
);
|
||||
}
|
||||
|
||||
protected function newTable(AphrontTableView $table) {
|
||||
$subtype_map = id(new ManiphestTask())->newEditEngineSubtypeMap();
|
||||
$has_subtypes = ($subtype_map->getCount() > 1);
|
||||
|
||||
return $table
|
||||
->setHeaders(
|
||||
array(
|
||||
null,
|
||||
null,
|
||||
pht('Status'),
|
||||
pht('Subtype'),
|
||||
pht('Assigned'),
|
||||
pht('Task'),
|
||||
))
|
||||
|
@ -136,12 +150,15 @@ final class ManiphestTaskGraph
|
|||
'threads',
|
||||
'graph-status',
|
||||
null,
|
||||
null,
|
||||
'wide pri object-link',
|
||||
))
|
||||
->setColumnVisibility(
|
||||
array(
|
||||
true,
|
||||
!$this->getRenderOnlyAdjacentNodes(),
|
||||
true,
|
||||
$has_subtypes,
|
||||
))
|
||||
->setDeviceVisibility(
|
||||
array(
|
||||
|
@ -150,6 +167,11 @@ final class ManiphestTaskGraph
|
|||
// On mobile, we only show the actual graph drawing if we're on the
|
||||
// standalone page, since it can take over the screen otherwise.
|
||||
$this->getIsStandalone(),
|
||||
true,
|
||||
|
||||
// On mobile, don't show subtypes since they're relatively less
|
||||
// important and we're more pressured for space.
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -180,6 +202,7 @@ final class ManiphestTaskGraph
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
pht("\xC2\xB7 \xC2\xB7 \xC2\xB7"),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -539,6 +539,7 @@ final class PhabricatorMarkupEngine extends Phobject {
|
|||
$rules[] = new PhutilRemarkupDelRule();
|
||||
$rules[] = new PhutilRemarkupUnderlineRule();
|
||||
$rules[] = new PhutilRemarkupHighlightRule();
|
||||
$rules[] = new PhutilRemarkupAnchorRule();
|
||||
|
||||
foreach (self::loadCustomInlineRules() as $rule) {
|
||||
$rules[] = clone $rule;
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAnchorTestCase
|
||||
extends PhabricatorTestCase {
|
||||
|
||||
public function testAnchors() {
|
||||
|
||||
$low_ascii = '';
|
||||
for ($ii = 19; $ii <= 127; $ii++) {
|
||||
$low_ascii .= chr($ii);
|
||||
}
|
||||
|
||||
$snowman = "\xE2\x9B\x84";
|
||||
|
||||
$map = array(
|
||||
'' => '',
|
||||
'Bells and Whistles' => 'bells-and-whistles',
|
||||
'Termination for Nonpayment' => 'termination-for-nonpayment',
|
||||
$low_ascii => '0123456789-abcdefghijklmnopqrstu',
|
||||
'xxxx xxxx xxxx xxxx xxxx on' => 'xxxx-xxxx-xxxx-xxxx-xxxx',
|
||||
'xxxx xxxx xxxx xxxx xxxx ox' => 'xxxx-xxxx-xxxx-xxxx-xxxx-ox',
|
||||
"So, You Want To Build A {$snowman}?" =>
|
||||
"so-you-want-to-build-a-{$snowman}",
|
||||
str_repeat($snowman, 128) => str_repeat($snowman, 32),
|
||||
);
|
||||
|
||||
foreach ($map as $input => $expect) {
|
||||
$anchor = PhutilRemarkupHeaderBlockRule::getAnchorNameFromHeaderText(
|
||||
$input);
|
||||
|
||||
$this->assertEqual(
|
||||
$expect,
|
||||
$anchor,
|
||||
pht('Anchor for "%s".', $input));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -73,24 +73,7 @@ final class PhutilRemarkupHeaderBlockRule extends PhutilRemarkupBlockRule {
|
|||
}
|
||||
|
||||
private function generateAnchor($level, $text) {
|
||||
$anchor = strtolower($text);
|
||||
$anchor = preg_replace('/[^a-z0-9]/', '-', $anchor);
|
||||
$anchor = preg_replace('/--+/', '-', $anchor);
|
||||
$anchor = trim($anchor, '-');
|
||||
$anchor = substr($anchor, 0, 24);
|
||||
$anchor = trim($anchor, '-');
|
||||
$base = $anchor;
|
||||
|
||||
$key = self::KEY_HEADER_TOC;
|
||||
$engine = $this->getEngine();
|
||||
$anchors = $engine->getTextMetadata($key, array());
|
||||
|
||||
$suffix = 1;
|
||||
while (!strlen($anchor) || isset($anchors[$anchor])) {
|
||||
$anchor = $base.'-'.$suffix;
|
||||
$anchor = trim($anchor, '-');
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
// When a document contains a link inside a header, like this:
|
||||
//
|
||||
|
@ -100,12 +83,30 @@ final class PhutilRemarkupHeaderBlockRule extends PhutilRemarkupBlockRule {
|
|||
// header itself. We push the 'toc' state so all the link rules generate
|
||||
// just names.
|
||||
$engine->pushState('toc');
|
||||
$text = $this->applyRules($text);
|
||||
$text = $engine->restoreText($text);
|
||||
|
||||
$anchors[$anchor] = array($level, $text);
|
||||
$plain_text = $text;
|
||||
$plain_text = $this->applyRules($plain_text);
|
||||
$plain_text = $engine->restoreText($plain_text);
|
||||
$engine->popState('toc');
|
||||
|
||||
$anchor = self::getAnchorNameFromHeaderText($plain_text);
|
||||
|
||||
if (!strlen($anchor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$base = $anchor;
|
||||
|
||||
$key = self::KEY_HEADER_TOC;
|
||||
$anchors = $engine->getTextMetadata($key, array());
|
||||
|
||||
$suffix = 1;
|
||||
while (isset($anchors[$anchor])) {
|
||||
$anchor = $base.'-'.$suffix;
|
||||
$anchor = trim($anchor, '-');
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
$anchors[$anchor] = array($level, $plain_text);
|
||||
$engine->setTextMetadata($key, $anchors);
|
||||
|
||||
return phutil_tag(
|
||||
|
@ -159,4 +160,26 @@ final class PhutilRemarkupHeaderBlockRule extends PhutilRemarkupBlockRule {
|
|||
return phutil_implode_html("\n", $toc);
|
||||
}
|
||||
|
||||
public static function getAnchorNameFromHeaderText($text) {
|
||||
$anchor = phutil_utf8_strtolower($text);
|
||||
$anchor = PhutilRemarkupAnchorRule::normalizeAnchor($anchor);
|
||||
|
||||
// Truncate the fragment to something reasonable.
|
||||
$anchor = id(new PhutilUTF8StringTruncator())
|
||||
->setMaximumGlyphs(32)
|
||||
->setTerminator('')
|
||||
->truncateString($anchor);
|
||||
|
||||
// If the fragment is terminated by a word which "The U.S. Government
|
||||
// Printing Office Style Manual" normally discourages capitalizing in
|
||||
// titles, discard it. This is an arbitrary heuristic intended to avoid
|
||||
// awkward hanging words in anchors.
|
||||
$anchor = preg_replace(
|
||||
'/-(a|an|the|at|by|for|in|of|on|per|to|up|and|as|but|if|or|nor)\z/',
|
||||
'',
|
||||
$anchor);
|
||||
|
||||
return $anchor;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
final class PhutilRemarkupAnchorRule extends PhutilRemarkupRule {
|
||||
|
||||
public function getPriority() {
|
||||
return 200.0;
|
||||
}
|
||||
|
||||
public function apply($text) {
|
||||
return preg_replace_callback(
|
||||
'/{anchor\s+#([^\s}]+)}/s',
|
||||
array($this, 'markupAnchor'),
|
||||
$text);
|
||||
}
|
||||
|
||||
protected function markupAnchor(array $matches) {
|
||||
$engine = $this->getEngine();
|
||||
|
||||
if ($engine->isTextMode()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($engine->isHTMLMailMode()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($engine->isAnchorMode()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->isFlatText($matches[0])) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
if (!self::isValidAnchorName($matches[1])) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
$tag_view = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'name' => $matches[1],
|
||||
),
|
||||
'');
|
||||
|
||||
return $this->getEngine()->storeText($tag_view);
|
||||
}
|
||||
|
||||
public static function isValidAnchorName($anchor_name) {
|
||||
$normal_anchor = self::normalizeAnchor($anchor_name);
|
||||
|
||||
if ($normal_anchor === $anchor_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function normalizeAnchor($anchor) {
|
||||
// Replace all latin characters which are not "a-z" or "0-9" with "-".
|
||||
// Preserve other characters, since non-latin letters and emoji work
|
||||
// fine in anchors.
|
||||
$anchor = preg_replace('/[\x00-\x2F\x3A-\x60\x7B-\x7F]+/', '-', $anchor);
|
||||
$anchor = trim($anchor, '-');
|
||||
|
||||
return $anchor;
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,10 @@ final class PhutilRemarkupBoldRule extends PhutilRemarkupRule {
|
|||
}
|
||||
|
||||
protected function applyCallback(array $matches) {
|
||||
if ($this->getEngine()->isAnchorMode()) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return hsprintf('<strong>%s</strong>', $matches[1]);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ final class PhutilRemarkupEngine extends PhutilMarkupEngine {
|
|||
return $this->mode & self::MODE_TEXT;
|
||||
}
|
||||
|
||||
public function isAnchorMode() {
|
||||
return $this->getState('toc');
|
||||
}
|
||||
|
||||
public function isHTMLMailMode() {
|
||||
return $this->mode & self::MODE_HTML_MAIL;
|
||||
}
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
|
||||
~~~~~~~~~~
|
||||
<ul>
|
||||
<li><a href="#http-www-example-com-lin">link_name</a></li>
|
||||
<li><a href="#link-name">link_name</a></li>
|
||||
<ul>
|
||||
<li><a href="#bold"><strong>bold</strong></a></li>
|
||||
<li><a href="#bold">bold</a></li>
|
||||
</ul>
|
||||
<li><a href="#http-www-example-com">http://www.example.com</a></li>
|
||||
</ul>
|
||||
|
||||
<h2 class="remarkup-header"><a name="http-www-example-com-lin"></a><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">link_name</a></h2>
|
||||
<h2 class="remarkup-header"><a name="link-name"></a><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">link_name</a></h2>
|
||||
|
||||
<h3 class="remarkup-header"><a name="bold"></a><strong>bold</strong></h3>
|
||||
|
||||
|
|
|
@ -63,6 +63,11 @@
|
|||
padding: 1px 8px;
|
||||
}
|
||||
|
||||
.differential-diff td.diff-flush {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.device .differential-diff td {
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
@ -282,18 +287,16 @@ td.cov-I {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.differential-diff .differential-image-diff {
|
||||
.differential-diff td.diff-image-cell {
|
||||
background-color: transparent;
|
||||
background-image: url(/rsrc/image/checker_light.png);
|
||||
}
|
||||
|
||||
.differential-diff .differential-image-diff:hover {
|
||||
background-image: url(/rsrc/image/checker_dark.png);
|
||||
}
|
||||
|
||||
.differential-diff .differential-image-diff td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.device-desktop .differential-diff .diff-image-cell:hover {
|
||||
background-image: url(/rsrc/image/checker_dark.png);
|
||||
}
|
||||
|
||||
.differential-image-stage {
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
color: #aa0066;
|
||||
}
|
||||
|
||||
.remarkup-code .language-tag {
|
||||
color: {$lightgreytext};
|
||||
}
|
||||
|
||||
.remarkup-code td > span {
|
||||
display: inline;
|
||||
word-break: break-all;
|
||||
|
|
|
@ -268,6 +268,10 @@ div.phui-property-list-stacked .phui-property-list-properties
|
|||
margin: 20px;
|
||||
}
|
||||
|
||||
.document-engine-jupyter.document-engine-diff {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.document-engine-in-flight {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
@ -294,22 +298,66 @@ div.phui-property-list-stacked .phui-property-list-properties
|
|||
|
||||
.jupyter-cell-code {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: {$lightgreybackground};
|
||||
padding: 8px;
|
||||
border: 1px solid {$lightgreyborder};
|
||||
border-radius: 2px;
|
||||
border-color: {$lightgreyborder};
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.jupyter-cell-code-block {
|
||||
padding: 8px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.jupyter-cell-code-line {
|
||||
padding: 2px 8px;
|
||||
border-width: 0 1px;
|
||||
}
|
||||
|
||||
td.new .jupyter-cell-code-line {
|
||||
background: {$new-background};
|
||||
border-color: {$new-bright};
|
||||
}
|
||||
|
||||
td.old .jupyter-cell-code-line {
|
||||
background: {$old-background};
|
||||
border-color: {$old-bright};
|
||||
}
|
||||
|
||||
.jupyter-cell-code-head {
|
||||
border-top-width: 1px;
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.jupyter-cell-code-last {
|
||||
border-bottom-width: 1px;
|
||||
margin-bottom: 4px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.jupyter-notebook > tbody > tr > th,
|
||||
.jupyter-notebook > tbody > tr > td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.jupyter-notebook > tbody > tr > th {
|
||||
.jupyter-notebook > tbody > tr > td.jupyter-cell-flush {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.jupyter-notebook,
|
||||
.jupyter-notebook > tbody > tr > td {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jupyter-notebook > tbody > tr > td.jupyter-label {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
min-width: 48px;
|
||||
min-width: 56px;
|
||||
font-weight: bold;
|
||||
width: auto;
|
||||
padding: 8px 8px 0;
|
||||
}
|
||||
|
||||
.jupyter-output {
|
||||
|
@ -326,3 +374,7 @@ div.phui-property-list-stacked .phui-property-list-properties
|
|||
.jupyter-output-html {
|
||||
background: {$sh-indigobackground};
|
||||
}
|
||||
|
||||
.jupyter-cell-markdown {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ JX.install('DiffChangeset', {
|
|||
this._ref = data.ref;
|
||||
this._renderer = data.renderer;
|
||||
this._highlight = data.highlight;
|
||||
this._documentEngine = data.documentEngine;
|
||||
this._encoding = data.encoding;
|
||||
this._loaded = data.loaded;
|
||||
this._treeNodeID = data.treeNodeID;
|
||||
|
@ -47,6 +48,7 @@ JX.install('DiffChangeset', {
|
|||
_ref: null,
|
||||
_renderer: null,
|
||||
_highlight: null,
|
||||
_documentEngine: null,
|
||||
_encoding: null,
|
||||
_undoTemplates: null,
|
||||
|
||||
|
@ -310,6 +312,7 @@ JX.install('DiffChangeset', {
|
|||
ref: this._ref,
|
||||
renderer: this.getRenderer() || '',
|
||||
highlight: this._highlight || '',
|
||||
engine: this._documentEngine || '',
|
||||
encoding: this._encoding || ''
|
||||
};
|
||||
},
|
||||
|
@ -366,6 +369,14 @@ JX.install('DiffChangeset', {
|
|||
return this._highlight;
|
||||
},
|
||||
|
||||
setDocumentEngine: function(engine) {
|
||||
this._documentEngine = engine;
|
||||
},
|
||||
|
||||
getDocumentEngine: function(engine) {
|
||||
return this._documentEngine;
|
||||
},
|
||||
|
||||
getSelectableItems: function() {
|
||||
var items = [];
|
||||
|
||||
|
|
|
@ -827,6 +827,26 @@ JX.install('DiffChangesetList', {
|
|||
});
|
||||
list.addItem(highlight_item);
|
||||
|
||||
var engine_item = new JX.PHUIXActionView()
|
||||
.setIcon('fa-file-image-o')
|
||||
.setName(pht('View As...'))
|
||||
.setHandler(function(e) {
|
||||
var params = {
|
||||
engine: changeset.getDocumentEngine(),
|
||||
};
|
||||
|
||||
new JX.Workflow('/services/viewas/', params)
|
||||
.setHandler(function(r) {
|
||||
changeset.setDocumentEngine(r.engine);
|
||||
changeset.reload();
|
||||
})
|
||||
.start();
|
||||
|
||||
e.prevent();
|
||||
menu.close();
|
||||
});
|
||||
list.addItem(engine_item);
|
||||
|
||||
add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI);
|
||||
add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI);
|
||||
add_link('fa-pencil', pht('Open in Editor'), data.editor, true);
|
||||
|
@ -860,6 +880,7 @@ JX.install('DiffChangesetList', {
|
|||
|
||||
encoding_item.setDisabled(!changeset.isLoaded());
|
||||
highlight_item.setDisabled(!changeset.isLoaded());
|
||||
engine_item.setDisabled(!changeset.isLoaded());
|
||||
|
||||
if (changeset.isLoaded()) {
|
||||
if (changeset.getRenderer() == '2up') {
|
||||
|
@ -1174,30 +1195,26 @@ JX.install('DiffChangesetList', {
|
|||
bot = tmp;
|
||||
}
|
||||
|
||||
// Find the leftmost cell that we're going to highlight: this is the next
|
||||
// <td /> in the row. In 2up views, it should be directly adjacent. In
|
||||
// 1up views, we may have to skip over the other line number column.
|
||||
var l = top;
|
||||
while (JX.DOM.isType(l, 'th')) {
|
||||
l = l.nextSibling;
|
||||
// Find the leftmost cell that we're going to highlight. This is the
|
||||
// next sibling with a "data-copy-mode" attribute, which is a marker
|
||||
// for the cell with actual content in it.
|
||||
var content_cell = top;
|
||||
while (content_cell && !content_cell.getAttribute('data-copy-mode')) {
|
||||
content_cell = content_cell.nextSibling;
|
||||
}
|
||||
|
||||
// Find the rightmost cell that we're going to highlight: this is the
|
||||
// farthest consecutive, adjacent <td /> in the row. Sometimes the left
|
||||
// and right nodes are the same (left side of 2up view); sometimes we're
|
||||
// going to highlight several nodes (copy + code + coverage).
|
||||
var r = l;
|
||||
while (r.nextSibling && JX.DOM.isType(r.nextSibling, 'td')) {
|
||||
r = r.nextSibling;
|
||||
// If we didn't find a cell to highlight, don't highlight anything.
|
||||
if (!content_cell) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pos = JX.$V(l)
|
||||
.add(JX.Vector.getAggregateScrollForNode(l));
|
||||
var pos = JX.$V(content_cell)
|
||||
.add(JX.Vector.getAggregateScrollForNode(content_cell));
|
||||
|
||||
var dim = JX.$V(r)
|
||||
.add(JX.Vector.getAggregateScrollForNode(r))
|
||||
var dim = JX.$V(content_cell)
|
||||
.add(JX.Vector.getAggregateScrollForNode(content_cell))
|
||||
.add(-pos.x, -pos.y)
|
||||
.add(JX.Vector.getDim(r));
|
||||
.add(JX.Vector.getDim(content_cell));
|
||||
|
||||
var bpos = JX.$V(bot)
|
||||
.add(JX.Vector.getAggregateScrollForNode(bot));
|
||||
|
|
|
@ -8,52 +8,102 @@
|
|||
|
||||
JX.behavior('phabricator-watch-anchor', function() {
|
||||
|
||||
var highlighted;
|
||||
// When the user loads a page with an "#anchor" or changes the "#anchor" on
|
||||
// an existing page, we try to scroll the page to the relevant location.
|
||||
|
||||
function highlight() {
|
||||
highlighted && JX.DOM.alterClass(highlighted, 'anchor-target', false);
|
||||
try {
|
||||
highlighted = JX.$('anchor-' + window.location.hash.replace('#', ''));
|
||||
} catch (ex) {
|
||||
highlighted = null;
|
||||
}
|
||||
highlighted && JX.DOM.alterClass(highlighted, 'anchor-target', true);
|
||||
}
|
||||
// Browsers do this on their own, but we have some additional rules to try
|
||||
// to match anchors more flexibly and handle cases where an anchor is not
|
||||
// yet present in the document because something is still loading or
|
||||
// rendering it, often via Ajax.
|
||||
|
||||
// Defer invocation so other listeners can update the document.
|
||||
function defer_highlight() {
|
||||
setTimeout(highlight, 0);
|
||||
}
|
||||
// Number of milliseconds we'll keep trying to find an anchor for.
|
||||
var wait_max = 5000;
|
||||
|
||||
// Wait between retries.
|
||||
var wait_ms = 100;
|
||||
|
||||
var target;
|
||||
var retry_ms;
|
||||
|
||||
// In some cases, we link to an anchor but the anchor target ajaxes in
|
||||
// later. If it pops in within the first few seconds, jump to it.
|
||||
function try_anchor() {
|
||||
retry_ms = wait_max;
|
||||
seek_anchor();
|
||||
}
|
||||
|
||||
function seek_anchor() {
|
||||
var anchor = window.location.hash.replace('#', '');
|
||||
try {
|
||||
// If the anchor exists, assume the browser handled the jump.
|
||||
if (anchor) {
|
||||
JX.$(anchor);
|
||||
|
||||
if (!anchor.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ii;
|
||||
var node = null;
|
||||
|
||||
// When the user navigates to "#abc", we'll try to find a node with
|
||||
// either ID "abc" or ID "anchor-abc".
|
||||
var ids = [anchor, 'anchor-' + anchor];
|
||||
|
||||
for (ii = 0; ii < ids.length; ii++) {
|
||||
try {
|
||||
node = JX.$(ids[ii]);
|
||||
break;
|
||||
} catch (e) {
|
||||
// Continue.
|
||||
}
|
||||
defer_highlight();
|
||||
} catch (e) {
|
||||
var n = 50;
|
||||
var try_anchor_again = function () {
|
||||
try {
|
||||
var node = JX.$(anchor);
|
||||
var pos = JX.Vector.getPosWithScroll(node);
|
||||
JX.DOM.scrollToPosition(0, pos.y - 60);
|
||||
defer_highlight();
|
||||
} catch (e) {
|
||||
if (n--) {
|
||||
setTimeout(try_anchor_again, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't found a matching node yet, look for an "<a />" tag with
|
||||
// a "name" attribute that has our anchor as a prefix. For example, you
|
||||
// can navigate to "#cat" and we'll match "#cat-and-mouse".
|
||||
|
||||
if (!node) {
|
||||
var anchor_nodes = JX.DOM.scry(document.body, 'a');
|
||||
for (ii = 0; ii < anchor_nodes.length; ii++) {
|
||||
if (!anchor_nodes[ii].name) {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
try_anchor_again();
|
||||
|
||||
if (anchor_nodes[ii].name.substring(0, anchor.length) === anchor) {
|
||||
node = anchor_nodes[ii];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we already have an anchor highlighted, unhighlight it and throw
|
||||
// it away if it doesn't match the new target.
|
||||
if (target && (target !== node)) {
|
||||
JX.DOM.alterClass(target, 'anchor-target', false);
|
||||
target = null;
|
||||
}
|
||||
|
||||
// If we didn't find a matching anchor, try again soon. This allows
|
||||
// rendering logic some time to complete Ajax requests and draw elements
|
||||
// onto the page.
|
||||
if (!node) {
|
||||
if (retry_ms > 0) {
|
||||
retry_ms -= wait_ms;
|
||||
setTimeout(try_anchor, wait_ms);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've found a new target, highlight it.
|
||||
if (target !== node) {
|
||||
target = node;
|
||||
JX.DOM.alterClass(target, 'anchor-target', true);
|
||||
}
|
||||
|
||||
// Try to scroll to the new target.
|
||||
try {
|
||||
var pos = JX.Vector.getPosWithScroll(node);
|
||||
JX.DOM.scrollToPosition(0, pos.y - 60);
|
||||
} catch (e) {
|
||||
// Ignore issues with scrolling the document.
|
||||
}
|
||||
}
|
||||
|
||||
JX.Stratcom.listen('hashchange', null, try_anchor);
|
||||
try_anchor();
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue