Merge branch 'upstream_master' into blender-tweaks
This commit is contained in:
@@ -9,10 +9,10 @@ return array(
|
||||
'names' => array(
|
||||
'conpherence.pkg.css' => '3c8a0668',
|
||||
'conpherence.pkg.js' => '020aebcf',
|
||||
'core.pkg.css' => '242f9ce6',
|
||||
'core.pkg.js' => '73a06a9f',
|
||||
'differential.pkg.css' => '8d8360fb',
|
||||
'differential.pkg.js' => '0b037a4f',
|
||||
'core.pkg.css' => '4451f9fd',
|
||||
'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' => 'ad841f1c',
|
||||
'rsrc/css/phui/phui-remarkup-preview.css' => '91767007',
|
||||
'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370',
|
||||
'rsrc/css/phui/phui-spacing.css' => 'b05cadc3',
|
||||
@@ -378,8 +378,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',
|
||||
@@ -508,7 +508,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',
|
||||
@@ -556,7 +556,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',
|
||||
@@ -657,7 +657,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',
|
||||
@@ -775,8 +775,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',
|
||||
@@ -868,7 +868,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' => 'ad841f1c',
|
||||
'phui-remarkup-preview-css' => '91767007',
|
||||
'phui-segment-bar-view-css' => '5166b370',
|
||||
'phui-spacing-css' => 'b05cadc3',
|
||||
@@ -903,7 +903,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',
|
||||
@@ -947,10 +947,6 @@ return array(
|
||||
'03e8891f' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'04023d82' => array(
|
||||
'javelin-install',
|
||||
'phuix-button-view',
|
||||
),
|
||||
'04f8a1e3' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
@@ -1002,12 +998,6 @@ return array(
|
||||
'0d2490ce' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'0e6d261f' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
'javelin-vector',
|
||||
),
|
||||
'0eaa33a9' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
@@ -1018,6 +1008,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',
|
||||
@@ -1963,9 +1977,6 @@ return array(
|
||||
'phabricator-drag-and-drop-file-upload',
|
||||
'javelin-workboard-board',
|
||||
),
|
||||
'bde53589' => array(
|
||||
'phui-inline-comment-view-css',
|
||||
),
|
||||
'c03f2fb4' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
@@ -2037,17 +2048,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;
|
@@ -2151,6 +2151,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',
|
||||
@@ -2270,6 +2271,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',
|
||||
@@ -3140,6 +3142,8 @@ 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',
|
||||
'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',
|
||||
@@ -4877,6 +4881,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',
|
||||
@@ -5599,9 +5604,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',
|
||||
@@ -8322,6 +8331,7 @@ phutil_register_library_map(array(
|
||||
'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
|
||||
'PhabricatorAmazonSNSFuture' => 'PhutilAWSFuture',
|
||||
'PhabricatorAnchorTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorAnchorView' => 'AphrontView',
|
||||
'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
|
||||
'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow',
|
||||
@@ -8462,6 +8472,7 @@ phutil_register_library_map(array(
|
||||
'PhabricatorAuthChallengeStatusController' => 'PhabricatorAuthController',
|
||||
'PhabricatorAuthChallengeUpdate' => 'Phobject',
|
||||
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
|
||||
'PhabricatorAuthChangeUsernameMessageType' => 'PhabricatorAuthMessageType',
|
||||
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
|
||||
'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
|
||||
@@ -9477,6 +9488,8 @@ phutil_register_library_map(array(
|
||||
'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem',
|
||||
'PhabricatorDivinerApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorDocumentEngine' => 'Phobject',
|
||||
'PhabricatorDocumentEngineBlock' => 'Phobject',
|
||||
'PhabricatorDocumentEngineBlocks' => 'Phobject',
|
||||
'PhabricatorDocumentRef' => 'Phobject',
|
||||
'PhabricatorDocumentRenderingEngine' => 'Phobject',
|
||||
'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication',
|
||||
@@ -11506,6 +11519,7 @@ phutil_register_library_map(array(
|
||||
'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow',
|
||||
'PhabricatorSystemSelectEncodingController' => 'PhabricatorController',
|
||||
'PhabricatorSystemSelectHighlightController' => 'PhabricatorController',
|
||||
'PhabricatorSystemSelectViewAsController' => 'PhabricatorController',
|
||||
'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor',
|
||||
'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
|
||||
@@ -12399,8 +12413,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',
|
||||
|
@@ -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;
|
||||
|
||||
@@ -1003,79 +1024,36 @@ final class DifferentialChangesetParser extends Phobject {
|
||||
->setOldComments($old_comments)
|
||||
->setNewComments($new_comments);
|
||||
|
||||
$engine_blocks = $this->newDocumentEngineChangesetView();
|
||||
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;
|
||||
}
|
||||
|
||||
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 +1653,153 @@ final class DifferentialChangesetParser extends Phobject {
|
||||
return $prefix.$line;
|
||||
}
|
||||
|
||||
private function newDocumentEngineChangesetView() {
|
||||
$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 $document_engine->newDiffView(
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -608,17 +608,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,34 +228,21 @@ 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.
|
||||
|
||||
$primitives = array();
|
||||
if ($old_file) {
|
||||
foreach ($block_list->newOneUpLayout() as $block) {
|
||||
$primitives[] = array(
|
||||
'type' => 'old-file',
|
||||
'htype' => ($new_file ? 'new-file' : null),
|
||||
'file' => $old_file,
|
||||
'line' => 1,
|
||||
'render' => $this->renderImageStage($old_file),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
'htype' => '',
|
||||
'line' => $block->getBlockKey(),
|
||||
'render' => $block->newContentView(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -378,11 +378,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,149 @@ 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);
|
||||
}
|
||||
$old_comments = $this->getOldComments();
|
||||
$new_comments = $this->getNewComments();
|
||||
|
||||
$new = null;
|
||||
if ($new_file) {
|
||||
$new = $this->renderImageStage($new_file);
|
||||
}
|
||||
$rows = array();
|
||||
foreach ($block_list->newTwoUpLayout() as $row) {
|
||||
list($old, $new) = $row;
|
||||
|
||||
// If we don't have an explicit "vs" changeset, it's the left side of the
|
||||
// "id" changeset.
|
||||
if (!$vs) {
|
||||
$vs = $id;
|
||||
}
|
||||
if ($old) {
|
||||
$old_content = $old->newContentView();
|
||||
$old_key = $old->getBlockKey();
|
||||
|
||||
$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);
|
||||
$old_classes = $old->getClasses();
|
||||
|
||||
if ($old->getDifferenceType() === '-') {
|
||||
$old_classes[] = 'old';
|
||||
$old_classes[] = 'old-full';
|
||||
}
|
||||
|
||||
$old_classes[] = 'diff-flush';
|
||||
|
||||
$old_classes = implode(' ', $old_classes);
|
||||
} else {
|
||||
$old_content = null;
|
||||
$old_key = null;
|
||||
$old_classes = 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_content = $new->newContentView();
|
||||
$new_key = $new->getBlockKey();
|
||||
$new_classes = $new->getClasses();
|
||||
|
||||
if ($new->getDifferenceType() === '+') {
|
||||
$new_classes[] = 'new';
|
||||
$new_classes[] = 'new-full';
|
||||
}
|
||||
|
||||
$new_classes[] = 'diff-flush';
|
||||
|
||||
$new_classes = implode(' ', $new_classes);
|
||||
} else {
|
||||
$new_content = null;
|
||||
$new_key = null;
|
||||
$new_classes = null;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -22,15 +22,10 @@ final class PhabricatorFactApplication extends PhabricatorApplication {
|
||||
return self::GROUP_UTILITIES;
|
||||
}
|
||||
|
||||
public function isPrototype() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getRoutes() {
|
||||
return array(
|
||||
'/fact/' => array(
|
||||
'' => 'PhabricatorFactHomeController',
|
||||
'chart/' => 'PhabricatorFactChartController',
|
||||
'chart/(?P<chartKey>[^/]+)/(?:(?P<mode>draw)/)?' =>
|
||||
'PhabricatorFactChartController',
|
||||
'object/(?<phid>[^/]+)/' => 'PhabricatorFactObjectController',
|
||||
|
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
final class PhabricatorFactChartController extends PhabricatorFactController {
|
||||
final class PhabricatorFactChartController
|
||||
extends PhabricatorFactController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $request->getViewer();
|
||||
|
||||
$chart_key = $request->getURIData('chartKey');
|
||||
if ($chart_key === null) {
|
||||
return $this->newDemoChart();
|
||||
if (!$chart_key) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$engine = id(new PhabricatorChartRenderingEngine())
|
||||
@@ -24,21 +25,28 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
|
||||
$mode = $request->getURIData('mode');
|
||||
$is_draw_mode = ($mode === 'draw');
|
||||
|
||||
// TODO: For now, always pull the data. We'll throw it away if we're just
|
||||
// drawing the frame, but this makes errors easier to debug.
|
||||
$chart_data = $engine->newChartData();
|
||||
$want_data = $is_draw_mode;
|
||||
|
||||
if ($is_draw_mode) {
|
||||
return id(new AphrontAjaxResponse())->setContent($chart_data);
|
||||
// In developer mode, always pull the data in the main request. We'll
|
||||
// throw it away if we're just drawing the chart frame, but this currently
|
||||
// makes errors quite a bit easier to debug.
|
||||
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
|
||||
$want_data = true;
|
||||
}
|
||||
|
||||
if ($want_data) {
|
||||
$chart_data = $engine->newChartData();
|
||||
if ($is_draw_mode) {
|
||||
return id(new AphrontAjaxResponse())->setContent($chart_data);
|
||||
}
|
||||
}
|
||||
|
||||
$chart_view = $engine->newChartView();
|
||||
$tabular_view = $engine->newTabularView();
|
||||
|
||||
return $this->newChartResponse($chart_view, $tabular_view);
|
||||
return $this->newChartResponse($chart_view);
|
||||
}
|
||||
|
||||
private function newChartResponse($chart_view, $tabular_view) {
|
||||
private function newChartResponse($chart_view) {
|
||||
$box = id(new PHUIObjectBoxView())
|
||||
->setHeaderText(pht('Chart'))
|
||||
->appendChild($chart_view);
|
||||
@@ -55,18 +63,7 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
|
||||
->appendChild(
|
||||
array(
|
||||
$box,
|
||||
$tabular_view,
|
||||
));
|
||||
}
|
||||
|
||||
private function newDemoChart() {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$chart = id(new PhabricatorDemoChartEngine())
|
||||
->setViewer($viewer)
|
||||
->newStoredChart();
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($chart->getURI());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,59 +1,20 @@
|
||||
<?php
|
||||
|
||||
final class PhabricatorFactHomeController extends PhabricatorFactController {
|
||||
final class PhabricatorFactHomeController
|
||||
extends PhabricatorFactController {
|
||||
|
||||
public function shouldAllowPublic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $request->getViewer();
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
$uri = new PhutilURI('/fact/chart/');
|
||||
$uri->replaceQueryParam('y1', $request->getStr('y1'));
|
||||
return id(new AphrontRedirectResponse())->setURI($uri);
|
||||
}
|
||||
$chart = id(new PhabricatorDemoChartEngine())
|
||||
->setViewer($viewer)
|
||||
->newStoredChart();
|
||||
|
||||
$chart_form = $this->buildChartForm();
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs();
|
||||
$crumbs->addTextCrumb(pht('Home'));
|
||||
|
||||
$title = pht('Facts');
|
||||
|
||||
return $this->newPage()
|
||||
->setTitle($title)
|
||||
->setCrumbs($crumbs)
|
||||
->appendChild(
|
||||
array(
|
||||
$chart_form,
|
||||
));
|
||||
}
|
||||
|
||||
private function buildChartForm() {
|
||||
$request = $this->getRequest();
|
||||
$viewer = $request->getUser();
|
||||
|
||||
$specs = PhabricatorFact::getAllFacts();
|
||||
$options = mpull($specs, 'getName', 'getKey');
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($viewer)
|
||||
->appendChild(
|
||||
id(new AphrontFormSelectControl())
|
||||
->setLabel(pht('Y-Axis'))
|
||||
->setName('y1')
|
||||
->setOptions($options))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue(pht('Plot Chart')));
|
||||
|
||||
$panel = new PHUIObjectBoxView();
|
||||
$panel->setForm($form);
|
||||
$panel->setHeaderText(pht('Plot Chart'));
|
||||
|
||||
return $panel;
|
||||
return id(new AphrontRedirectResponse())->setURI($chart->getURI());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ final class PhabricatorDemoChartEngine
|
||||
array('shift', 256));
|
||||
|
||||
$function->getFunctionLabel()
|
||||
->setKey('cos-x')
|
||||
->setName(pht('cos(x)'))
|
||||
->setColor('rgba(0, 200, 0, 1)')
|
||||
->setFillColor('rgba(0, 200, 0, 0.15)');
|
||||
@@ -27,6 +28,7 @@ final class PhabricatorDemoChartEngine
|
||||
array('constant', 345));
|
||||
|
||||
$function->getFunctionLabel()
|
||||
->setKey('constant-345')
|
||||
->setName(pht('constant(345)'))
|
||||
->setColor('rgba(0, 0, 200, 1)')
|
||||
->setFillColor('rgba(0, 0, 200, 0.15)');
|
||||
|
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
final class PhabricatorDocumentEngineBlock
|
||||
extends Phobject {
|
||||
|
||||
private $blockKey;
|
||||
private $content;
|
||||
private $classes = array();
|
||||
private $differenceHash;
|
||||
private $differenceType;
|
||||
|
||||
public function setContent($content) {
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent() {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function newContentView() {
|
||||
return $this->getContent();
|
||||
}
|
||||
|
||||
public function setBlockKey($block_key) {
|
||||
$this->blockKey = $block_key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBlockKey() {
|
||||
return $this->blockKey;
|
||||
}
|
||||
|
||||
public function addClass($class) {
|
||||
$this->classes[] = $class;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getClasses() {
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
145
src/applications/files/diff/PhabricatorDocumentEngineBlocks.php
Normal file
145
src/applications/files/diff/PhabricatorDocumentEngineBlocks.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
final class PhabricatorDocumentEngineBlocks
|
||||
extends Phobject {
|
||||
|
||||
private $lists = array();
|
||||
|
||||
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 newTwoUpLayout() {
|
||||
$rows = array();
|
||||
$lists = $this->lists;
|
||||
|
||||
$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();
|
||||
|
||||
$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);
|
||||
|
||||
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']);
|
||||
}
|
||||
} 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']);
|
||||
}
|
||||
} 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,18 @@ abstract class PhabricatorDocumentEngine
|
||||
return $this->canRenderDocumentType($ref);
|
||||
}
|
||||
|
||||
public function canDiffDocuments(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentRef $vref) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function newDiffView(
|
||||
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,55 @@ 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 newDiffView(
|
||||
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);
|
||||
}
|
||||
|
||||
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')
|
||||
->addClass('diff-image-cell')
|
||||
->setDifferenceHash($hash)
|
||||
->setContent($image_view);
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
|
||||
$file = $ref->getFile();
|
||||
if ($file) {
|
||||
|
@@ -35,55 +35,84 @@ final class PhabricatorJupyterDocumentEngine
|
||||
return $ref->isProbablyJSON();
|
||||
}
|
||||
|
||||
public function canDiffDocuments(
|
||||
PhabricatorDocumentRef $uref,
|
||||
PhabricatorDocumentRef $vref) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function newDiffView(
|
||||
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);
|
||||
}
|
||||
|
||||
private function newDiffBlocks(PhabricatorDocumentRef $ref) {
|
||||
$viewer = $this->getViewer();
|
||||
$content = $ref->loadData();
|
||||
|
||||
$cells = $this->newCells($content, true);
|
||||
|
||||
$idx = 1;
|
||||
$blocks = array();
|
||||
foreach ($cells as $cell) {
|
||||
$cell_content = $this->renderJupyterCell($viewer, $cell);
|
||||
|
||||
$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);
|
||||
|
||||
// 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;
|
||||
default:
|
||||
$hash_input = serialize($cell);
|
||||
break;
|
||||
}
|
||||
|
||||
$hash = PhabricatorHash::digestWithNamedKey(
|
||||
$hash_input,
|
||||
'document-engine.content-digest');
|
||||
|
||||
$blocks[] = id(new PhabricatorDocumentEngineBlock())
|
||||
->setBlockKey($idx)
|
||||
->setDifferenceHash($hash)
|
||||
->setContent($container);
|
||||
|
||||
$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,6 +137,117 @@ 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 !== '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) {
|
||||
@@ -115,13 +255,24 @@ final class PhabricatorJupyterDocumentEngine
|
||||
list($label, $content) = $this->renderJupyterCellContent($viewer, $cell);
|
||||
|
||||
$label_cell = phutil_tag(
|
||||
'th',
|
||||
array(),
|
||||
'td',
|
||||
array(
|
||||
'class' => 'jupyter-label',
|
||||
),
|
||||
$label);
|
||||
|
||||
$classes = null;
|
||||
switch (idx($cell, 'cell_type')) {
|
||||
case 'code/line':
|
||||
$classes = 'jupyter-cell-flush';
|
||||
break;
|
||||
}
|
||||
|
||||
$content_cell = phutil_tag(
|
||||
'td',
|
||||
array(),
|
||||
array(
|
||||
'class' => $classes,
|
||||
),
|
||||
$content);
|
||||
|
||||
return phutil_tag(
|
||||
@@ -142,7 +293,11 @@ 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));
|
||||
@@ -181,23 +336,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 +359,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 +371,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 +496,45 @@ 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) {
|
||||
$head = head($lines);
|
||||
$matches = null;
|
||||
if (preg_match('/^%%(.*)$/', $head, $matches)) {
|
||||
$restore = array_shift($lines);
|
||||
$lang = $matches[1];
|
||||
} else {
|
||||
$restore = null;
|
||||
$lang = 'py';
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -35,8 +35,12 @@ final class ManiphestReportController extends ManiphestController {
|
||||
$nav->addLabel(pht('Open Tasks'));
|
||||
$nav->addFilter('user', pht('By User'));
|
||||
$nav->addFilter('project', pht('By Project'));
|
||||
$nav->addLabel(pht('Burnup'));
|
||||
$nav->addFilter('burn', pht('Burnup Rate'));
|
||||
|
||||
$class = 'PhabricatorFactApplication';
|
||||
if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
|
||||
$nav->addLabel(pht('Burnup'));
|
||||
$nav->addFilter('burn', pht('Burnup Rate'));
|
||||
}
|
||||
|
||||
$this->view = $nav->selectFilter($this->view, 'user');
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -34,6 +34,11 @@ final class PhabricatorProjectReportsProfileMenuItem
|
||||
return false;
|
||||
}
|
||||
|
||||
$class = 'PhabricatorFactApplication';
|
||||
if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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,56 @@ 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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@@ -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();
|
||||
|
||||
});
|
||||
|
Reference in New Issue
Block a user