1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-07 05:11:05 +01:00

(stable) Promote 2019 Week 8

This commit is contained in:
epriestley 2019-02-23 06:18:26 -08:00
commit 5d88eef26b
93 changed files with 2432 additions and 986 deletions

View file

@ -9,10 +9,10 @@ return array(
'names' => array(
'conpherence.pkg.css' => '3c8a0668',
'conpherence.pkg.js' => '020aebcf',
'core.pkg.css' => '85a1da99',
'core.pkg.js' => '5c737607',
'differential.pkg.css' => 'b8df73d4',
'differential.pkg.js' => '67c9ea4c',
'core.pkg.css' => 'e3c1a8f2',
'core.pkg.js' => '2cda17a4',
'differential.pkg.css' => 'ab23bd75',
'differential.pkg.js' => '67e02996',
'diffusion.pkg.css' => '42c75c37',
'diffusion.pkg.js' => '91192d85',
'maniphest.pkg.css' => '35995d6d',
@ -61,8 +61,8 @@ return array(
'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6',
'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d',
'rsrc/css/application/differential/add-comment.css' => '7e5900d9',
'rsrc/css/application/differential/changeset-view.css' => '73660575',
'rsrc/css/application/differential/core.css' => 'bdb93065',
'rsrc/css/application/differential/changeset-view.css' => 'd92bed0d',
'rsrc/css/application/differential/core.css' => '7300a73e',
'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b',
'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d',
'rsrc/css/application/differential/revision-history.css' => '8aa3eac5',
@ -112,7 +112,7 @@ return array(
'rsrc/css/application/uiexample/example.css' => 'b4795059',
'rsrc/css/core/core.css' => '1b29ed61',
'rsrc/css/core/remarkup.css' => '9e627d41',
'rsrc/css/core/syntax.css' => '8a16f91b',
'rsrc/css/core/syntax.css' => '4234f572',
'rsrc/css/core/z-index.css' => '99c0f5eb',
'rsrc/css/diviner/diviner-shared.css' => '4bd263b0',
'rsrc/css/font/font-awesome.css' => '3883938a',
@ -154,7 +154,7 @@ return array(
'rsrc/css/phui/phui-form-view.css' => '01b796c0',
'rsrc/css/phui/phui-form.css' => '159e2d9c',
'rsrc/css/phui/phui-head-thing.css' => 'd7f293df',
'rsrc/css/phui/phui-header-view.css' => '93cea4ec',
'rsrc/css/phui/phui-header-view.css' => '285c9139',
'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0',
'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec',
'rsrc/css/phui/phui-icon.css' => '4cbc684a',
@ -172,7 +172,7 @@ return array(
'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370',
'rsrc/css/phui/phui-spacing.css' => 'b05cadc3',
'rsrc/css/phui/phui-status.css' => 'e5ff8be0',
'rsrc/css/phui/phui-tag-view.css' => 'a42fe34f',
'rsrc/css/phui/phui-tag-view.css' => '29409667',
'rsrc/css/phui/phui-timeline-view.css' => '1e348e4b',
'rsrc/css/phui/phui-two-column-view.css' => '01e6991e',
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308',
@ -275,6 +275,8 @@ return array(
'rsrc/image/checker_dark.png' => '7fc8fa7b',
'rsrc/image/checker_light.png' => '3157a202',
'rsrc/image/checker_lighter.png' => 'c45928c1',
'rsrc/image/chevron-in.png' => '1aa2f88f',
'rsrc/image/chevron-out.png' => 'c815e272',
'rsrc/image/controls/checkbox-checked.png' => '1770d7a0',
'rsrc/image/controls/checkbox-unchecked.png' => 'e1deba0a',
'rsrc/image/d5d8e1.png' => '6764616e',
@ -372,13 +374,12 @@ return array(
'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '076bd092',
'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9',
'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '9b1cbd76',
'rsrc/js/application/diff/DiffChangeset.js' => 'e7cf10d6',
'rsrc/js/application/diff/DiffChangesetList.js' => 'b91204e9',
'rsrc/js/application/diff/DiffChangeset.js' => 'd0a85a85',
'rsrc/js/application/diff/DiffChangesetList.js' => '04023d82',
'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',
'rsrc/js/application/differential/behavior-populate.js' => 'dfa1d313',
'rsrc/js/application/differential/behavior-user-select.js' => 'e18685c0',
'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '94243d89',
'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'b7b73831',
'rsrc/js/application/diffusion/behavior-commit-branches.js' => '4b671572',
@ -418,7 +419,7 @@ return array(
'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68',
'rsrc/js/application/releeph/releeph-request-state-change.js' => '9f081f05',
'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'aa3a100c',
'rsrc/js/application/repository/repository-crossreference.js' => 'db0c0214',
'rsrc/js/application/repository/repository-crossreference.js' => 'c15122b4',
'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e5bdb730',
'rsrc/js/application/search/behavior-reorder-queries.js' => 'b86f297f',
'rsrc/js/application/transactions/behavior-comment-actions.js' => '4dffaeb2',
@ -441,7 +442,7 @@ return array(
'rsrc/js/core/KeyboardShortcutManager.js' => '37b8a04a',
'rsrc/js/core/MultirowRowManager.js' => '5b54c823',
'rsrc/js/core/Notification.js' => 'a9b91e3f',
'rsrc/js/core/Prefab.js' => 'bf457520',
'rsrc/js/core/Prefab.js' => '5793d835',
'rsrc/js/core/ShapedRequest.js' => 'abf88db8',
'rsrc/js/core/TextAreaUtils.js' => 'f340a484',
'rsrc/js/core/Title.js' => '43bc9360',
@ -471,7 +472,7 @@ return array(
'rsrc/js/core/behavior-linked-container.js' => '74446546',
'rsrc/js/core/behavior-more.js' => '506aa3f4',
'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a',
'rsrc/js/core/behavior-oncopy.js' => '418f6684',
'rsrc/js/core/behavior-oncopy.js' => 'ff7b3f22',
'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949',
'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f',
'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f',
@ -505,7 +506,7 @@ return array(
'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4',
'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f',
'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b',
'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8',
'rsrc/js/phuix/PHUIXAutocomplete.js' => '8f139ef0',
'rsrc/js/phuix/PHUIXButtonView.js' => '55a24e84',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bdce4d78',
'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7',
@ -539,8 +540,8 @@ return array(
'conpherence-thread-manager' => 'aec8e38c',
'conpherence-transaction-css' => '3a3f5e7e',
'd3' => 'd67475f5',
'differential-changeset-view-css' => '73660575',
'differential-core-view-css' => 'bdb93065',
'differential-changeset-view-css' => 'd92bed0d',
'differential-core-view-css' => '7300a73e',
'differential-revision-add-comment-css' => '7e5900d9',
'differential-revision-comment-css' => '7dbc8d1d',
'differential-revision-history-css' => '8aa3eac5',
@ -594,7 +595,6 @@ return array(
'javelin-behavior-diff-preview-link' => 'f51e9c17',
'javelin-behavior-differential-diff-radios' => '925fe8cd',
'javelin-behavior-differential-populate' => 'dfa1d313',
'javelin-behavior-differential-user-select' => 'e18685c0',
'javelin-behavior-diffusion-commit-branches' => '4b671572',
'javelin-behavior-diffusion-commit-graph' => '1c88f154',
'javelin-behavior-diffusion-locate-file' => '87428eb2',
@ -634,7 +634,7 @@ return array(
'javelin-behavior-phabricator-nav' => 'f166c949',
'javelin-behavior-phabricator-notification-example' => '29819b75',
'javelin-behavior-phabricator-object-selector' => 'a4af0b4a',
'javelin-behavior-phabricator-oncopy' => '418f6684',
'javelin-behavior-phabricator-oncopy' => 'ff7b3f22',
'javelin-behavior-phabricator-remarkup-assist' => '2f80333f',
'javelin-behavior-phabricator-reveal-content' => 'b105a3a6',
'javelin-behavior-phabricator-search-typeahead' => '1cb7d027',
@ -669,7 +669,7 @@ return array(
'javelin-behavior-reorder-applications' => 'aa371860',
'javelin-behavior-reorder-columns' => '8ac32fd9',
'javelin-behavior-reorder-profile-menu-items' => 'e5bdb730',
'javelin-behavior-repository-crossreference' => 'db0c0214',
'javelin-behavior-repository-crossreference' => 'c15122b4',
'javelin-behavior-scrollbar' => '92388bae',
'javelin-behavior-search-reorder-queries' => 'b86f297f',
'javelin-behavior-select-content' => 'e8240b50',
@ -751,8 +751,8 @@ return array(
'phabricator-darklog' => '3b869402',
'phabricator-darkmessage' => '26cd4b73',
'phabricator-dashboard-css' => '4267d6c6',
'phabricator-diff-changeset' => 'e7cf10d6',
'phabricator-diff-changeset-list' => 'b91204e9',
'phabricator-diff-changeset' => 'd0a85a85',
'phabricator-diff-changeset-list' => '04023d82',
'phabricator-diff-inline' => 'a4a14a94',
'phabricator-drag-and-drop-file-upload' => '4370900d',
'phabricator-draggable-list' => '3c6bd549',
@ -771,7 +771,7 @@ return array(
'phabricator-notification-menu-css' => 'e6962e89',
'phabricator-object-selector-css' => 'ee77366f',
'phabricator-phtize' => '2f1db1ed',
'phabricator-prefab' => 'bf457520',
'phabricator-prefab' => '5793d835',
'phabricator-remarkup-css' => '9e627d41',
'phabricator-search-results-css' => '9ea70ace',
'phabricator-shaped-request' => 'abf88db8',
@ -821,7 +821,7 @@ return array(
'phui-form-css' => '159e2d9c',
'phui-form-view-css' => '01b796c0',
'phui-head-thing-view-css' => 'd7f293df',
'phui-header-view-css' => '93cea4ec',
'phui-header-view-css' => '285c9139',
'phui-hovercard' => '074f0783',
'phui-hovercard-view-css' => '6ca90fa0',
'phui-icon-set-selector-css' => '7aa5f3ec',
@ -847,7 +847,7 @@ return array(
'phui-segment-bar-view-css' => '5166b370',
'phui-spacing-css' => 'b05cadc3',
'phui-status-list-view-css' => 'e5ff8be0',
'phui-tag-view-css' => 'a42fe34f',
'phui-tag-view-css' => '29409667',
'phui-theme-css' => '35883b37',
'phui-timeline-view-css' => '1e348e4b',
'phui-two-column-view-css' => '01e6991e',
@ -857,7 +857,7 @@ return array(
'phui-workpanel-view-css' => 'bd546a49',
'phuix-action-list-view' => 'c68f183f',
'phuix-action-view' => 'aaa08f3b',
'phuix-autocomplete' => '58cc4ab8',
'phuix-autocomplete' => '8f139ef0',
'phuix-button-view' => '55a24e84',
'phuix-dropdown-menu' => 'bdce4d78',
'phuix-form-control-view' => '38c1f3fb',
@ -876,7 +876,7 @@ return array(
'sprite-login-css' => '18b368a6',
'sprite-tokens-css' => 'f1896dc5',
'syntax-default-css' => '055fc231',
'syntax-highlighting-css' => '8a16f91b',
'syntax-highlighting-css' => '4234f572',
'tokens-css' => 'ce5a50bd',
'typeahead-browse-css' => 'b7ed02d2',
'unhandled-exception-css' => '9ecfc00d',
@ -905,6 +905,10 @@ return array(
'javelin-uri',
'javelin-util',
),
'04023d82' => array(
'javelin-install',
'phuix-button-view',
),
'04f8a1e3' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1216,9 +1220,8 @@ return array(
'javelin-behavior',
'javelin-uri',
),
'418f6684' => array(
'javelin-behavior',
'javelin-dom',
'4234f572' => array(
'syntax-default-css',
),
'42c7a5a7' => array(
'javelin-install',
@ -1354,6 +1357,18 @@ return array(
'javelin-stratcom',
'javelin-dom',
),
'5793d835' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-typeahead',
'javelin-tokenizer',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
),
'5803b9e7' => array(
'javelin-behavior',
'javelin-util',
@ -1362,12 +1377,6 @@ return array(
'javelin-vector',
'javelin-typeahead-static-source',
),
'58cc4ab8' => array(
'javelin-install',
'javelin-dom',
'phuix-icon-view',
'phabricator-prefab',
),
'5902260c' => array(
'javelin-util',
'javelin-magical-init',
@ -1484,9 +1493,6 @@ return array(
'javelin-dom',
'javelin-uri',
),
73660575 => array(
'phui-inline-comment-view-css',
),
'73ecc1f8' => array(
'javelin-behavior',
'javelin-behavior-device',
@ -1575,9 +1581,6 @@ return array(
'javelin-stratcom',
'javelin-install',
),
'8a16f91b' => array(
'syntax-default-css',
),
'8ac32fd9' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1608,6 +1611,12 @@ return array(
'8e2d9a28' => array(
'phui-theme-css',
),
'8f139ef0' => array(
'javelin-install',
'javelin-dom',
'phuix-icon-view',
'phabricator-prefab',
),
'8f959ad0' => array(
'javelin-behavior',
'javelin-dom',
@ -1881,10 +1890,6 @@ return array(
'javelin-uri',
'phabricator-notification',
),
'b91204e9' => array(
'javelin-install',
'phuix-button-view',
),
'bd546a49' => array(
'phui-workcard-view-css',
),
@ -1895,21 +1900,15 @@ return array(
'javelin-vector',
'javelin-stratcom',
),
'bf457520' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-typeahead',
'javelin-tokenizer',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
),
'c03f2fb4' => array(
'javelin-install',
),
'c15122b4' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-uri',
),
'c2c500a7' => array(
'javelin-install',
'javelin-dom',
@ -1971,6 +1970,17 @@ return array(
'javelin-stratcom',
'javelin-util',
),
'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',
@ -1987,6 +1997,9 @@ return array(
'javelin-util',
'phabricator-shaped-request',
),
'd92bed0d' => array(
'phui-inline-comment-view-css',
),
'da15d3dc' => array(
'phui-oi-list-view-css',
),
@ -1995,12 +2008,6 @@ return array(
'javelin-uri',
'phabricator-notification',
),
'db0c0214' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-uri',
),
'dfa1d313' => array(
'javelin-behavior',
'javelin-dom',
@ -2021,11 +2028,6 @@ return array(
'javelin-dom',
'javelin-history',
),
'e18685c0' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'e562708c' => array(
'javelin-install',
),
@ -2036,17 +2038,6 @@ return array(
'javelin-dom',
'phabricator-draggable-list',
),
'e7cf10d6' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
'javelin-workflow',
'javelin-router',
'javelin-behavior-device',
'javelin-vector',
'phabricator-diff-inline',
),
'e8240b50' => array(
'javelin-behavior',
'javelin-stratcom',
@ -2145,6 +2136,10 @@ return array(
'owners-path-editor',
'javelin-behavior',
),
'ff7b3f22' => array(
'javelin-behavior',
'javelin-dom',
),
),
'packages' => array(
'conpherence.pkg.css' => array(
@ -2337,7 +2332,6 @@ return array(
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
'phabricator-diff-inline',
'phabricator-diff-changeset',

View file

@ -199,7 +199,6 @@ return array(
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
'phabricator-diff-inline',

View file

@ -0,0 +1,21 @@
<?php
// See T6615. We're about to change the nullability on the "dataID" column,
// but it may have a UNIQUE KEY on it. Make sure we get rid of this key first
// so we don't run into trouble.
// There's no "IF EXISTS" modifier for "ALTER TABLE" so run this as a PHP patch
// instead of an SQL patch.
$table = new PhabricatorWorkerActiveTask();
$conn = $table->establishConnection('w');
try {
queryfx(
$conn,
'ALTER TABLE %R DROP KEY %T',
$table,
'dataID');
} catch (AphrontQueryException $ex) {
// Ignore.
}

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_worker.worker_activetask
CHANGE dataID dataID INT UNSIGNED NOT NULL;

View file

@ -0,0 +1,6 @@
CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_string (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
stringIndex BINARY(12) NOT NULL,
stringValue LONGTEXT NOT NULL,
UNIQUE KEY `key_string` (stringIndex)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildunitmessage
ADD nameIndex BINARY(12) NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_worker.worker_archivetask
ADD archivedEpoch INT UNSIGNED NULL;

View file

@ -0,0 +1,3 @@
ALTER TABLE {$NAMESPACE}_worker.worker_activetask
ADD dateCreated int unsigned NOT NULL,
ADD dateModified int unsigned NOT NULL;

View file

@ -1365,6 +1365,7 @@ phutil_register_library_map(array(
'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php',
'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php',
'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php',
'HarbormasterBuildUnitMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php',
'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php',
'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php',
'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php',
@ -1440,6 +1441,7 @@ phutil_register_library_map(array(
'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php',
'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php',
'HarbormasterStepViewController' => 'applications/harbormaster/controller/HarbormasterStepViewController.php',
'HarbormasterString' => 'applications/harbormaster/storage/HarbormasterString.php',
'HarbormasterTargetEngine' => 'applications/harbormaster/engine/HarbormasterTargetEngine.php',
'HarbormasterTargetSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterTargetSearchAPIMethod.php',
'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php',
@ -1742,6 +1744,7 @@ phutil_register_library_map(array(
'ManiphestTaskParentTransaction' => 'applications/maniphest/xaction/ManiphestTaskParentTransaction.php',
'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php',
'ManiphestTaskPointsTransaction' => 'applications/maniphest/xaction/ManiphestTaskPointsTransaction.php',
'ManiphestTaskPolicyCodex' => 'applications/maniphest/policy/ManiphestTaskPolicyCodex.php',
'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php',
'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php',
'ManiphestTaskPriorityHeraldAction' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php',
@ -2857,8 +2860,6 @@ phutil_register_library_map(array(
'PhabricatorDaemonLog' => 'applications/daemon/storage/PhabricatorDaemonLog.php',
'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php',
'PhabricatorDaemonLogEventGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogEventGarbageCollector.php',
'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php',
'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php',
'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php',
'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php',
'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php',
@ -2969,6 +2970,8 @@ phutil_register_library_map(array(
'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php',
'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php',
'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php',
'PhabricatorDiffScopeEngine' => 'infrastructure/diff/PhabricatorDiffScopeEngine.php',
'PhabricatorDiffScopeEngineTestCase' => 'infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php',
'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php',
'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php',
'PhabricatorDifferentialAttachCommitWorkflow' => 'applications/differential/management/PhabricatorDifferentialAttachCommitWorkflow.php',
@ -6983,7 +6986,11 @@ phutil_register_library_map(array(
'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildUnitMessage' => 'HarbormasterDAO',
'HarbormasterBuildUnitMessage' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
),
'HarbormasterBuildUnitMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildViewController' => 'HarbormasterController',
'HarbormasterBuildWorker' => 'HarbormasterWorker',
'HarbormasterBuildable' => array(
@ -7062,6 +7069,7 @@ phutil_register_library_map(array(
'HarbormasterStepDeleteController' => 'HarbormasterPlanController',
'HarbormasterStepEditController' => 'HarbormasterPlanController',
'HarbormasterStepViewController' => 'HarbormasterPlanController',
'HarbormasterString' => 'HarbormasterDAO',
'HarbormasterTargetEngine' => 'Phobject',
'HarbormasterTargetSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterTargetWorker' => 'HarbormasterWorker',
@ -7379,6 +7387,7 @@ phutil_register_library_map(array(
'PhabricatorEditEngineSubtypeInterface',
'PhabricatorEditEngineLockableInterface',
'PhabricatorEditEngineMFAInterface',
'PhabricatorPolicyCodexInterface',
),
'ManiphestTaskAssignHeraldAction' => 'HeraldAction',
'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction',
@ -7430,6 +7439,7 @@ phutil_register_library_map(array(
'ManiphestTaskParentTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPoints' => 'Phobject',
'ManiphestTaskPointsTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPolicyCodex' => 'PhabricatorPolicyCodex',
'ManiphestTaskPriority' => 'ManiphestConstants',
'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskPriorityHeraldAction' => 'HeraldAction',
@ -8713,8 +8723,6 @@ phutil_register_library_map(array(
),
'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO',
'PhabricatorDaemonLogEventGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController',
'PhabricatorDaemonLogEventsView' => 'AphrontView',
'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController',
'PhabricatorDaemonLogListView' => 'AphrontView',
@ -8842,6 +8850,8 @@ phutil_register_library_map(array(
'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDiffScopeEngine' => 'Phobject',
'PhabricatorDiffScopeEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorDifferenceEngine' => 'Phobject',
'PhabricatorDifferentialApplication' => 'PhabricatorApplication',
'PhabricatorDifferentialAttachCommitWorkflow' => 'PhabricatorDifferentialManagementWorkflow',

View file

@ -47,6 +47,14 @@ final class PhabricatorFacebookAuthProvider
return 'Facebook';
}
protected function getContentSecurityPolicyFormActions() {
return array(
// See T13254. After login with a mobile device, Facebook may redirect
// to the mobile site.
'https://m.facebook.com/',
);
}
public function readFormValuesFromProvider() {
$require_secure = $this->getProviderConfig()->getProperty(
self::KEY_REQUIRE_SECURE);
@ -114,15 +122,4 @@ final class PhabricatorFacebookAuthProvider
return parent::renderConfigPropertyTransactionTitle($xaction);
}
public static function getFacebookApplicationID() {
$providers = PhabricatorAuthProvider::getAllProviders();
$fb_provider = idx($providers, 'facebook:facebook.com');
if (!$fb_provider) {
return null;
}
return $fb_provider->getProviderConfig()->getProperty(
self::PROPERTY_APP_ID);
}
}

View file

@ -533,6 +533,8 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
'This ancient extension point has been replaced with other '.
'mechanisms, including "AphrontSite".'),
'differential.whitespace-matters' => pht(
'Whitespace rendering is now handled automatically.'),
);
return $ancient_config;

View file

@ -45,7 +45,6 @@ final class PhabricatorDaemonsApplication extends PhabricatorApplication {
'' => 'PhabricatorDaemonLogListController',
'(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogViewController',
),
'event/(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogEventViewController',
'bulk/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' =>
'PhabricatorDaemonBulkJobListController',

View file

@ -31,6 +31,7 @@ final class PhabricatorDaemonConsoleController
$completed_info[$class] = array(
'n' => 0,
'duration' => 0,
'queueTime' => 0,
);
}
$completed_info[$class]['n']++;
@ -41,16 +42,33 @@ final class PhabricatorDaemonConsoleController
// compute utilization.
$usage_total += $lease_overhead + ($duration / 1000000);
$usage_start = min($usage_start, $completed_task->getDateModified());
$date_archived = $completed_task->getArchivedEpoch();
$queue_seconds = $date_archived - $completed_task->getDateCreated();
// Don't measure queue time for tasks that completed in the same
// epoch-second they were created in.
if ($queue_seconds > 0) {
$sec_in_us = phutil_units('1 second in microseconds');
$queue_us = $queue_seconds * $sec_in_us;
$queue_exclusive_us = $queue_us - $duration;
$queue_exclusive_seconds = $queue_exclusive_us / $sec_in_us;
$rounded = floor($queue_exclusive_seconds);
$completed_info[$class]['queueTime'] += $rounded;
}
}
$completed_info = isort($completed_info, 'n');
$rows = array();
foreach ($completed_info as $class => $info) {
$duration_avg = new PhutilNumber((int)($info['duration'] / $info['n']));
$queue_avg = new PhutilNumber((int)($info['queueTime'] / $info['n']));
$rows[] = array(
$class,
number_format($info['n']),
pht('%s us', new PhutilNumber((int)($info['duration'] / $info['n']))),
pht('%s us', $duration_avg),
pht('%s s', $queue_avg),
);
}
@ -98,6 +116,7 @@ final class PhabricatorDaemonConsoleController
phutil_tag('em', array(), pht('Queue Utilization (Approximate)')),
sprintf('%.1f%%', 100 * $used_time),
null,
null,
);
}
@ -108,13 +127,15 @@ final class PhabricatorDaemonConsoleController
array(
pht('Class'),
pht('Count'),
pht('Avg'),
pht('Average Duration'),
pht('Average Queue Time'),
));
$completed_table->setColumnClasses(
array(
'wide',
'n',
'n',
'n',
));
$completed_panel = id(new PHUIObjectBoxView())

View file

@ -1,47 +0,0 @@
<?php
final class PhabricatorDaemonLogEventViewController
extends PhabricatorDaemonController {
public function handleRequest(AphrontRequest $request) {
$id = $request->getURIData('id');
$event = id(new PhabricatorDaemonLogEvent())->load($id);
if (!$event) {
return new Aphront404Response();
}
$event_view = id(new PhabricatorDaemonLogEventsView())
->setEvents(array($event))
->setUser($request->getUser())
->setCombinedLog(true)
->setShowFullMessage(true);
$log_panel = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($event_view);
$daemon_id = $event->getLogID();
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(
pht('Daemon %s', $daemon_id),
$this->getApplicationURI("log/{$daemon_id}/"))
->addTextCrumb(pht('Event %s', $event->getID()))
->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Combined Log'))
->setHeaderIcon('fa-file-text');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($log_panel);
return $this->newPage()
->setTitle(pht('Combined Daemon Log'))
->appendChild($view);
}
}

View file

@ -1,131 +0,0 @@
<?php
final class PhabricatorDaemonLogEventsView extends AphrontView {
private $events;
private $combinedLog;
private $showFullMessage;
public function setShowFullMessage($show_full_message) {
$this->showFullMessage = $show_full_message;
return $this;
}
public function setEvents(array $events) {
assert_instances_of($events, 'PhabricatorDaemonLogEvent');
$this->events = $events;
return $this;
}
public function setCombinedLog($is_combined) {
$this->combinedLog = $is_combined;
return $this;
}
public function render() {
$viewer = $this->getViewer();
$rows = array();
foreach ($this->events as $event) {
// Limit display log size. If a daemon gets stuck in an output loop this
// page can be like >100MB if we don't truncate stuff. Try to do cheap
// line-based truncation first, and fall back to expensive UTF-8 character
// truncation if that doesn't get things short enough.
$message = $event->getMessage();
$more = null;
if (!$this->showFullMessage) {
$more_lines = null;
$more_chars = null;
$line_limit = 12;
if (substr_count($message, "\n") > $line_limit) {
$message = explode("\n", $message);
$more_lines = count($message) - $line_limit;
$message = array_slice($message, 0, $line_limit);
$message = implode("\n", $message);
}
$char_limit = 8192;
if (strlen($message) > $char_limit) {
$message = phutil_utf8v($message);
$more_chars = count($message) - $char_limit;
$message = array_slice($message, 0, $char_limit);
$message = implode('', $message);
}
if ($more_chars) {
$more = new PhutilNumber($more_chars);
$more = pht('Show %d more character(s)...', $more);
} else if ($more_lines) {
$more = new PhutilNumber($more_lines);
$more = pht('Show %d more line(s)...', $more);
}
if ($more) {
$id = $event->getID();
$more = array(
"\n...\n",
phutil_tag(
'a',
array(
'href' => "/daemon/event/{$id}/",
),
$more),
);
}
}
$row = array(
$event->getLogType(),
phabricator_date($event->getEpoch(), $viewer),
phabricator_time($event->getEpoch(), $viewer),
array(
$message,
$more,
),
);
if ($this->combinedLog) {
array_unshift(
$row,
phutil_tag(
'a',
array(
'href' => '/daemon/log/'.$event->getLogID().'/',
),
pht('Daemon %s', $event->getLogID())));
}
$rows[] = $row;
}
$classes = array(
'',
'',
'right',
'wide prewrap',
);
$headers = array(
'Type',
'Date',
'Time',
'Message',
);
if ($this->combinedLog) {
array_unshift($classes, 'pri');
array_unshift($headers, 'Daemon');
}
$log_table = new AphrontTableView($rows);
$log_table->setHeaders($headers);
$log_table->setColumnClasses($classes);
return $log_table->render();
}
}

View file

@ -14,6 +14,10 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase {
}
$data = Filesystem::readFile($dir.$file);
// Strip trailing "~" characters from inputs so they may contain
// trailing whitespace.
$data = preg_replace('/~$/m', '', $data);
$opt_file = $dir.$file.'.options';
if (Filesystem::pathExists($opt_file)) {
$options = Filesystem::readFile($opt_file);
@ -31,7 +35,6 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase {
foreach (array('one', 'two') as $type) {
$this->runParser($type, $data, $file, 'expect');
$this->runParser($type, $data, $file, 'unshielded');
$this->runParser($type, $data, $file, 'whitespace');
}
}
}
@ -44,25 +47,20 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase {
}
$unshielded = false;
$whitespace = false;
switch ($extension) {
case 'unshielded':
$unshielded = true;
break;
case 'whitespace';
$unshielded = true;
$whitespace = true;
break;
}
$parsers = $this->buildChangesetParsers($type, $data, $file);
$actual = $this->renderParsers($parsers, $unshielded, $whitespace);
$actual = $this->renderParsers($parsers, $unshielded);
$expect = Filesystem::readFile($test_file);
$this->assertEqual($expect, $actual, basename($test_file));
}
private function renderParsers(array $parsers, $unshield, $whitespace) {
private function renderParsers(array $parsers, $unshield) {
$result = array();
foreach ($parsers as $parser) {
if ($unshield) {
@ -73,11 +71,6 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase {
$e_range = null;
}
if ($whitespace) {
$parser->setWhitespaceMode(
DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
}
$result[] = $parser->render($s_range, $e_range, array());
}
return implode(str_repeat('~', 80)."\n", $result);

View file

@ -4,7 +4,7 @@ index 5dcff7f..eff82ef 100644
+++ b/GENERATED
@@ -1,4 +1,4 @@
@generated
~
-This is a generated file.
+This is a generated file, full of generated code.

View file

@ -1,6 +1,5 @@
N 1 . @generated\n~
O 2 - \n~
N 2 . \n~
O 3 - This is a generated file.\n~
N 2 + \n~
N 3 + This is a generated file{(, full of generated code)}.\n~
N 4 . \n~

View file

@ -1,7 +1,7 @@
O 1 . @generated\n~
N 1 . @generated\n~
O 2 - \n~
N 2 + \n~
O 2 . \n~
N 2 . \n~
O 3 - This is a generated file.\n~
N 3 + This is a generated file{(, full of generated code)}.\n~
O 4 . \n~

View file

@ -2,4 +2,5 @@ CTYPE 2 1 (unforced)
WHITESPACE
WHITESPACE
-
SHIELD (whitespace) This file was changed only by adding or removing whitespace.
O 1 - -=[-Rocket-Ship>\n~
N 1 + {> )}-=[-Rocket-Ship>\n~

View file

@ -1,2 +1,2 @@
O 1 - -=[-Rocket-Ship>\n~
N 1 + {( )}-=[-Rocket-Ship>\n~
N 1 + {> )}-=[-Rocket-Ship>\n~

View file

@ -2,4 +2,5 @@ CTYPE 2 1 (unforced)
WHITESPACE
WHITESPACE
-
SHIELD (whitespace) This file was changed only by adding or removing whitespace.
O 1 - -=[-Rocket-Ship>\n~
N 1 + {> )}-=[-Rocket-Ship>\n~

View file

@ -1,2 +1,2 @@
O 1 - -=[-Rocket-Ship>\n~
N 1 + {( )}-=[-Rocket-Ship>\n~
N 1 + {> )}-=[-Rocket-Ship>\n~

View file

@ -80,18 +80,6 @@ EOHELP
"Select and reorder revision fields.\n\n".
"NOTE: This feature is under active development and subject ".
"to change.")),
$this->newOption(
'differential.whitespace-matters',
'list<regex>',
array(
'/\.py$/',
'/\.l?hs$/',
'/\.ya?ml$/',
))
->setDescription(
pht(
"List of file regexps where whitespace is meaningful and should ".
"not use 'ignore-all' by default")),
$this->newOption('differential.require-test-plan-field', 'bool', true)
->setBoolOptions(
array(

View file

@ -420,15 +420,17 @@ final class DifferentialChangesetViewController extends DifferentialController {
}
private function loadCoverage(DifferentialChangeset $changeset) {
$viewer = $this->getViewer();
$target_phids = $changeset->getDiff()->getBuildTargetPHIDs();
if (!$target_phids) {
return null;
}
$unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
$unit = id(new HarbormasterBuildUnitMessageQuery())
->setViewer($viewer)
->withBuildTargetPHIDs($target_phids)
->execute();
if (!$unit) {
return null;
}

View file

@ -192,9 +192,10 @@ abstract class DifferentialController extends PhabricatorController {
$all_target_phids = array_mergev($target_map);
if ($all_target_phids) {
$unit_messages = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$all_target_phids);
$unit_messages = id(new HarbormasterBuildUnitMessageQuery())
->setViewer($viewer)
->withBuildTargetPHIDs($all_target_phids)
->execute();
$unit_messages = mgroup($unit_messages, 'getBuildTargetPHID');
} else {
$unit_messages = array();

View file

@ -305,10 +305,6 @@ final class DifferentialRevisionViewController
$details = $this->buildDetails($revision, $field_list);
$curtain = $this->buildCurtain($revision);
$whitespace = $request->getStr(
'whitespace',
DifferentialChangesetParser::WHITESPACE_IGNORE_MOST);
$repository = $revision->getRepository();
if ($repository) {
$symbol_indexes = $this->buildSymbolIndexes(
@ -383,7 +379,6 @@ final class DifferentialRevisionViewController
->setDiff($target)
->setRenderingReferences($rendering_references)
->setVsMap($vs_map)
->setWhitespace($whitespace)
->setSymbolIndexes($symbol_indexes)
->setTitle(pht('Diff %s', $target->getID()))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
@ -412,7 +407,6 @@ final class DifferentialRevisionViewController
->setDiffUnitStatuses($broken_diffs)
->setSelectedVersusDiffID($diff_vs)
->setSelectedDiffID($target->getID())
->setSelectedWhitespace($whitespace)
->setCommitsForLinks($commits_for_links);
$local_table = id(new DifferentialLocalCommitsView())
@ -627,8 +621,6 @@ final class DifferentialRevisionViewController
->build($changesets);
}
Javelin::initBehavior('differential-user-select');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setSubheader($subheader)
@ -1095,7 +1087,7 @@ final class DifferentialRevisionViewController
// this ends up being something like
// D123.diff
// or the verbose
// D123.vs123.id123.whitespaceignore-all.diff
// D123.vs123.id123.highlightjs.diff
// lame but nice to include these options
$file_name = ltrim($request_uri->getPath(), '/').'.';
foreach ($request_uri->getQueryParamsAsPairList() as $pair) {

View file

@ -8,6 +8,7 @@ final class DifferentialChangesetParser extends Phobject {
protected $new = array();
protected $old = array();
protected $intra = array();
protected $depthOnlyLines = array();
protected $newRender = null;
protected $oldRender = null;
@ -18,7 +19,6 @@ final class DifferentialChangesetParser extends Phobject {
protected $specialAttributes = array();
protected $changeset;
protected $whitespaceMode = null;
protected $renderCacheKey = null;
@ -162,7 +162,6 @@ final class DifferentialChangesetParser extends Phobject {
}
public function readParametersFromRequest(AphrontRequest $request) {
$this->setWhitespaceMode($request->getStr('whitespace'));
$this->setCharacterEncoding($request->getStr('encoding'));
$this->setHighlightAs($request->getStr('highlight'));
@ -190,20 +189,14 @@ final class DifferentialChangesetParser extends Phobject {
return $this;
}
const CACHE_VERSION = 11;
const CACHE_VERSION = 14;
const CACHE_MAX_SIZE = 8e6;
const ATTR_GENERATED = 'attr:generated';
const ATTR_DELETED = 'attr:deleted';
const ATTR_UNCHANGED = 'attr:unchanged';
const ATTR_WHITELINES = 'attr:white';
const ATTR_MOVEAWAY = 'attr:moveaway';
const WHITESPACE_SHOW_ALL = 'show-all';
const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing';
const WHITESPACE_IGNORE_MOST = 'ignore-most';
const WHITESPACE_IGNORE_ALL = 'ignore-all';
public function setOldLines(array $lines) {
$this->old = $lines;
return $this;
@ -224,6 +217,15 @@ final class DifferentialChangesetParser extends Phobject {
return $this;
}
public function setDepthOnlyLines(array $lines) {
$this->depthOnlyLines = $lines;
return $this;
}
public function getDepthOnlyLines() {
return $this->depthOnlyLines;
}
public function setVisibileLinesMask(array $mask) {
$this->visible = $mask;
return $this;
@ -326,11 +328,6 @@ final class DifferentialChangesetParser extends Phobject {
return $this;
}
public function setWhitespaceMode($whitespace_mode) {
$this->whitespaceMode = $whitespace_mode;
return $this;
}
public function setRenderingReference($ref) {
$this->renderingReference = $ref;
return $this;
@ -450,6 +447,7 @@ final class DifferentialChangesetParser extends Phobject {
'new',
'old',
'intra',
'depthOnlyLines',
'newRender',
'oldRender',
'specialAttributes',
@ -563,10 +561,6 @@ final class DifferentialChangesetParser extends Phobject {
return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
}
public function isWhitespaceOnly() {
return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
}
public function isMoveAway() {
return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false);
}
@ -574,11 +568,17 @@ final class DifferentialChangesetParser extends Phobject {
private function applyIntraline(&$render, $intra, $corpus) {
foreach ($render as $key => $text) {
$result = $text;
if (isset($intra[$key])) {
$render[$key] = ArcanistDiffUtils::applyIntralineDiff(
$text,
$result = ArcanistDiffUtils::applyIntralineDiff(
$result,
$intra[$key]);
}
$result = $this->adjustRenderedLineForDisplay($result);
$render[$key] = $result;
}
}
@ -613,18 +613,8 @@ final class DifferentialChangesetParser extends Phobject {
}
private function tryCacheStuff() {
$whitespace_mode = $this->whitespaceMode;
switch ($whitespace_mode) {
case self::WHITESPACE_SHOW_ALL:
case self::WHITESPACE_IGNORE_TRAILING:
case self::WHITESPACE_IGNORE_ALL:
break;
default:
$whitespace_mode = self::WHITESPACE_IGNORE_MOST;
break;
}
$skip_cache = false;
$skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST);
if ($this->disableCache) {
$skip_cache = true;
}
@ -637,8 +627,6 @@ final class DifferentialChangesetParser extends Phobject {
$skip_cache = true;
}
$this->whitespaceMode = $whitespace_mode;
$changeset = $this->changeset;
if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT &&
@ -657,70 +645,12 @@ final class DifferentialChangesetParser extends Phobject {
}
private function process() {
$whitespace_mode = $this->whitespaceMode;
$changeset = $this->changeset;
$ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) ||
($whitespace_mode == self::WHITESPACE_IGNORE_ALL));
$force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL);
if (!$force_ignore) {
if ($ignore_all && $changeset->getWhitespaceMatters()) {
$ignore_all = false;
}
}
// The "ignore all whitespace" algorithm depends on rediffing the
// files, and we currently need complete representations of both
// files to do anything reasonable. If we only have parts of the files,
// don't use the "ignore all" algorithm.
if ($ignore_all) {
$hunks = $changeset->getHunks();
if (count($hunks) !== 1) {
$ignore_all = false;
} else {
$first_hunk = reset($hunks);
if ($first_hunk->getOldOffset() != 1 ||
$first_hunk->getNewOffset() != 1) {
$ignore_all = false;
}
}
}
if ($ignore_all) {
$old_file = $changeset->makeOldFile();
$new_file = $changeset->makeNewFile();
if ($old_file == $new_file) {
// If the old and new files are exactly identical, the synthetic
// diff below will give us nonsense and whitespace modes are
// irrelevant anyway. This occurs when you, e.g., copy a file onto
// itself in Subversion (see T271).
$ignore_all = false;
}
}
$hunk_parser = new DifferentialHunkParser();
$hunk_parser->setWhitespaceMode($whitespace_mode);
$hunk_parser->parseHunksForLineData($changeset->getHunks());
// Depending on the whitespace mode, we may need to compute a different
// set of changes than the set of changes in the hunk data (specifically,
// we might want to consider changed lines which have only whitespace
// changes as unchanged).
if ($ignore_all) {
$engine = new PhabricatorDifferenceEngine();
$engine->setIgnoreWhitespace(true);
$no_whitespace_changeset = $engine->generateChangesetFromFileContent(
$old_file,
$new_file);
$type_parser = new DifferentialHunkParser();
$type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks());
$hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());
$hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());
}
$this->realignDiff($changeset, $hunk_parser);
$hunk_parser->reparseHunksForSpecialAttributes();
@ -742,7 +672,6 @@ final class DifferentialChangesetParser extends Phobject {
$this->setSpecialAttributes(array(
self::ATTR_UNCHANGED => $unchanged,
self::ATTR_DELETED => $hunk_parser->getIsDeleted(),
self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(),
self::ATTR_MOVEAWAY => $moveaway,
));
@ -754,6 +683,7 @@ final class DifferentialChangesetParser extends Phobject {
$this->setOldLines($hunk_parser->getOldLines());
$this->setNewLines($hunk_parser->getNewLines());
$this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs());
$this->setDepthOnlyLines($hunk_parser->getDepthOnlyLines());
$this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask());
$this->hunkStartLines = $hunk_parser->getHunkStartLines(
$changeset->getHunks());
@ -914,7 +844,8 @@ final class DifferentialChangesetParser extends Phobject {
->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks())
->setCanMarkDone($this->getCanMarkDone())
->setObjectOwnerPHID($this->getObjectOwnerPHID())
->setHighlightingDisabled($this->highlightingDisabled);
->setHighlightingDisabled($this->highlightingDisabled)
->setDepthOnlyLines($this->getDepthOnlyLines());
$shield = null;
if ($this->isTopLevel && !$this->comments) {
@ -958,10 +889,6 @@ final class DifferentialChangesetParser extends Phobject {
pht('The contents of this file were not changed.'),
$type);
}
} else if ($this->isWhitespaceOnly()) {
$shield = $renderer->renderShield(
pht('This file was changed only by adding or removing whitespace.'),
'whitespace');
} else if ($this->isDeleted()) {
$shield = $renderer->renderShield(
pht('This file was completely deleted.'));
@ -1173,7 +1100,7 @@ final class DifferentialChangesetParser extends Phobject {
}
$range_len = min($range_len, $rows - $range_start);
list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths(
list($gaps, $mask) = $this->calculateGapsAndMask(
$mask_force,
$feedback_mask,
$range_start,
@ -1181,8 +1108,7 @@ final class DifferentialChangesetParser extends Phobject {
$renderer
->setGaps($gaps)
->setMask($mask)
->setDepths($depths);
->setMask($mask);
$html = $renderer->renderTextChange(
$range_start,
@ -1208,15 +1134,9 @@ final class DifferentialChangesetParser extends Phobject {
* "show more"). The $mask returned is a sparsely populated dictionary
* of $visible_line_number => true.
*
* Depths - compute how indented any given line is. The $depths returned
* is a sparsely populated dictionary of $visible_line_number => $depth.
*
* This function also has the side effect of modifying member variable
* new such that tabs are normalized to spaces for each line of the diff.
*
* @return array($gaps, $mask, $depths)
* @return array($gaps, $mask)
*/
private function calculateGapsMaskAndDepths(
private function calculateGapsAndMask(
$mask_force,
$feedback_mask,
$range_start,
@ -1224,7 +1144,6 @@ final class DifferentialChangesetParser extends Phobject {
$lines_context = $this->getLinesOfContext();
// Calculate gaps and mask first
$gaps = array();
$gap_start = 0;
$in_gap = false;
@ -1253,38 +1172,7 @@ final class DifferentialChangesetParser extends Phobject {
$gaps = array_reverse($gaps);
$mask = $base_mask;
// Time to calculate depth.
// We need to go backwards to properly indent whitespace in this code:
//
// 0: class C {
// 1:
// 1: function f() {
// 2:
// 2: return;
// 1:
// 1: }
// 0:
// 0: }
//
$depths = array();
$last_depth = 0;
$range_end = $range_start + $range_len;
if (!isset($this->new[$range_end])) {
$range_end--;
}
for ($ii = $range_end; $ii >= $range_start; $ii--) {
// We need to expand tabs to process mixed indenting and to round
// correctly later.
$line = str_replace("\t", ' ', $this->new[$ii]['text']);
$trimmed = ltrim($line);
if ($trimmed != '') {
// We round down to flatten "/**" and " *".
$last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
}
$depths[$ii] = $last_depth;
}
return array($gaps, $mask, $depths);
return array($gaps, $mask);
}
/**
@ -1487,4 +1375,229 @@ final class DifferentialChangesetParser extends Phobject {
return $key;
}
private function realignDiff(
DifferentialChangeset $changeset,
DifferentialHunkParser $hunk_parser) {
// Normalizing and realigning the diff depends on rediffing the files, and
// we currently need complete representations of both files to do anything
// reasonable. If we only have parts of the files, skip realignment.
// We have more than one hunk, so we're definitely missing part of the file.
$hunks = $changeset->getHunks();
if (count($hunks) !== 1) {
return null;
}
// The first hunk doesn't start at the beginning of the file, so we're
// missing some context.
$first_hunk = head($hunks);
if ($first_hunk->getOldOffset() != 1 || $first_hunk->getNewOffset() != 1) {
return null;
}
$old_file = $changeset->makeOldFile();
$new_file = $changeset->makeNewFile();
if ($old_file === $new_file) {
// If the old and new files are exactly identical, the synthetic
// diff below will give us nonsense and whitespace modes are
// irrelevant anyway. This occurs when you, e.g., copy a file onto
// itself in Subversion (see T271).
return null;
}
$engine = id(new PhabricatorDifferenceEngine())
->setNormalize(true);
$normalized_changeset = $engine->generateChangesetFromFileContent(
$old_file,
$new_file);
$type_parser = new DifferentialHunkParser();
$type_parser->parseHunksForLineData($normalized_changeset->getHunks());
$hunk_parser->setNormalized(true);
$hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());
$hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());
}
private function adjustRenderedLineForDisplay($line) {
// IMPORTANT: We're using "str_replace()" against raw HTML here, which can
// easily become unsafe. The input HTML has already had syntax highlighting
// and intraline diff highlighting applied, so it's full of "<span />" tags.
static $search;
static $replace;
if ($search === null) {
$rules = $this->newSuspiciousCharacterRules();
$map = array();
foreach ($rules as $key => $spec) {
$tag = phutil_tag(
'span',
array(
'data-copy-text' => $key,
'class' => $spec['class'],
'title' => $spec['title'],
),
$spec['replacement']);
$map[$key] = phutil_string_cast($tag);
}
$search = array_keys($map);
$replace = array_values($map);
}
$is_html = false;
if ($line instanceof PhutilSafeHTML) {
$is_html = true;
$line = hsprintf('%s', $line);
}
$line = phutil_string_cast($line);
if (strpos($line, "\t") !== false) {
$line = $this->replaceTabsWithSpaces($line);
}
$line = str_replace($search, $replace, $line);
if ($is_html) {
$line = phutil_safe_html($line);
}
return $line;
}
private function newSuspiciousCharacterRules() {
// The "title" attributes are cached in the database, so they're
// intentionally not wrapped in "pht(...)".
$rules = array(
"\xE2\x80\x8B" => array(
'title' => 'ZWS',
'class' => 'suspicious-character',
'replacement' => '!',
),
"\xC2\xA0" => array(
'title' => 'NBSP',
'class' => 'suspicious-character',
'replacement' => '!',
),
"\x7F" => array(
'title' => 'DEL (0x7F)',
'class' => 'suspicious-character',
'replacement' => "\xE2\x90\xA1",
),
);
// Unicode defines special pictures for the control characters in the
// range between "0x00" and "0x1F".
$control = array(
'NULL',
'SOH',
'STX',
'ETX',
'EOT',
'ENQ',
'ACK',
'BEL',
'BS',
null, // "\t" Tab
null, // "\n" New Line
'VT',
'FF',
null, // "\r" Carriage Return,
'SO',
'SI',
'DLE',
'DC1',
'DC2',
'DC3',
'DC4',
'NAK',
'SYN',
'ETB',
'CAN',
'EM',
'SUB',
'ESC',
'FS',
'GS',
'RS',
'US',
);
foreach ($control as $idx => $label) {
if ($label === null) {
continue;
}
$rules[chr($idx)] = array(
'title' => sprintf('%s (0x%02X)', $label, $idx),
'class' => 'suspicious-character',
'replacement' => "\xE2\x90".chr(0x80 + $idx),
);
}
return $rules;
}
private function replaceTabsWithSpaces($line) {
// TODO: This should be flexible, eventually.
$tab_width = 2;
static $tags;
if ($tags === null) {
$tags = array();
for ($ii = 1; $ii <= $tab_width; $ii++) {
$tag = phutil_tag(
'span',
array(
'data-copy-text' => "\t",
),
str_repeat(' ', $ii));
$tag = phutil_string_cast($tag);
$tags[$ii] = $tag;
}
}
// If the line is particularly long, don't try to vectorize it. Use a
// faster approximation of the correct tabstop expansion instead. This
// usually still arrives at the right result.
if (strlen($line) > 256) {
return str_replace("\t", $tags[$tab_width], $line);
}
$line = phutil_utf8v_combined($line);
$in_tag = false;
$pos = 0;
foreach ($line as $key => $char) {
if ($char === '<') {
$in_tag = true;
continue;
}
if ($char === '>') {
$in_tag = false;
continue;
}
if ($in_tag) {
continue;
}
if ($char === "\t") {
$count = $tab_width - ($pos % $tab_width);
$pos += $count;
$line[$key] = $tags[$count];
continue;
}
$pos++;
}
return implode('', $line);
}
}

View file

@ -5,8 +5,9 @@ final class DifferentialHunkParser extends Phobject {
private $oldLines;
private $newLines;
private $intraLineDiffs;
private $depthOnlyLines;
private $visibleLinesMask;
private $whitespaceMode;
private $normalized;
/**
* Get a map of lines on which hunks start, other than line 1. This
@ -115,20 +116,22 @@ final class DifferentialHunkParser extends Phobject {
return $this;
}
public function setWhitespaceMode($white_space_mode) {
$this->whitespaceMode = $white_space_mode;
public function setDepthOnlyLines(array $map) {
$this->depthOnlyLines = $map;
return $this;
}
private function getWhitespaceMode() {
if ($this->whitespaceMode === null) {
throw new Exception(
pht(
'You must %s before accessing this data.',
'setWhitespaceMode'));
}
return $this->whitespaceMode;
public function getDepthOnlyLines() {
return $this->depthOnlyLines;
}
public function setNormalized($normalized) {
$this->normalized = $normalized;
return $this;
}
public function getNormalized() {
return $this->normalized;
}
public function getIsDeleted() {
@ -150,13 +153,6 @@ final class DifferentialHunkParser extends Phobject {
return false;
}
/**
* Returns true if the hunks change any text, not just whitespace.
*/
public function getHasTextChanges() {
return $this->getHasChanges('text');
}
/**
* Returns true if the hunks change anything, including whitespace.
*/
@ -184,9 +180,6 @@ final class DifferentialHunkParser extends Phobject {
}
if ($o['type'] !== $n['type']) {
// The types are different, so either the underlying text is actually
// different or whatever whitespace rules we're using consider them
// different.
return true;
}
@ -269,62 +262,7 @@ final class DifferentialHunkParser extends Phobject {
$this->setOldLines($rebuild_old);
$this->setNewLines($rebuild_new);
$this->updateChangeTypesForWhitespaceMode();
return $this;
}
private function updateChangeTypesForWhitespaceMode() {
$mode = $this->getWhitespaceMode();
$mode_show_all = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
if ($mode === $mode_show_all) {
// If we're showing all whitespace, we don't need to perform any updates.
return;
}
$mode_trailing = DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING;
$is_trailing = ($mode === $mode_trailing);
$new = $this->getNewLines();
$old = $this->getOldLines();
foreach ($old as $key => $o) {
$n = $new[$key];
if (!$o || !$n) {
continue;
}
if ($is_trailing) {
// In "trailing" mode, we need to identify lines which are marked
// changed but differ only by trailing whitespace. We mark these lines
// unchanged.
if ($o['type'] != $n['type']) {
if (rtrim($o['text']) === rtrim($n['text'])) {
$old[$key]['type'] = null;
$new[$key]['type'] = null;
}
}
} else {
// In "ignore most" and "ignore all" modes, we need to identify lines
// which are marked unchanged but have internal whitespace changes.
// We want to ignore leading and trailing whitespace changes only, not
// internal whitespace changes (`diff` doesn't have a mode for this, so
// we have to fix it here). If the text is marked unchanged but the
// old and new text differs by internal space, mark the lines changed.
if ($o['type'] === null && $n['type'] === null) {
if ($o['text'] !== $n['text']) {
if (trim($o['text']) !== trim($n['text'])) {
$old[$key]['type'] = '-';
$new[$key]['type'] = '+';
}
}
}
}
}
$this->setOldLines($old);
$this->setNewLines($new);
$this->updateChangeTypesForNormalization();
return $this;
}
@ -334,6 +272,7 @@ final class DifferentialHunkParser extends Phobject {
$new = $this->getNewLines();
$diffs = array();
$depth_only = array();
foreach ($old as $key => $o) {
$n = $new[$key];
@ -342,13 +281,75 @@ final class DifferentialHunkParser extends Phobject {
}
if ($o['type'] != $n['type']) {
$diffs[$key] = ArcanistDiffUtils::generateIntralineDiff(
$o['text'],
$n['text']);
$o_segments = array();
$n_segments = array();
$tab_width = 2;
$o_text = $o['text'];
$n_text = $n['text'];
if ($o_text !== $n_text && (ltrim($o_text) === ltrim($n_text))) {
$o_depth = $this->getIndentDepth($o_text, $tab_width);
$n_depth = $this->getIndentDepth($n_text, $tab_width);
if ($o_depth < $n_depth) {
$segment_type = '>';
$segment_width = $this->getCharacterCountForVisualWhitespace(
$n_text,
($n_depth - $o_depth),
$tab_width);
if ($segment_width) {
$n_text = substr($n_text, $segment_width);
$n_segments[] = array(
$segment_type,
$segment_width,
);
}
} else if ($o_depth > $n_depth) {
$segment_type = '<';
$segment_width = $this->getCharacterCountForVisualWhitespace(
$o_text,
($o_depth - $n_depth),
$tab_width);
if ($segment_width) {
$o_text = substr($o_text, $segment_width);
$o_segments[] = array(
$segment_type,
$segment_width,
);
}
}
// If there are no remaining changes to this line after we've marked
// off the indent depth changes, this line was only modified by
// changing the indent depth. Mark it for later so we can change how
// it is displayed.
if ($o_text === $n_text) {
$depth_only[$key] = $segment_type;
}
}
$intraline_segments = ArcanistDiffUtils::generateIntralineDiff(
$o_text,
$n_text);
foreach ($intraline_segments[0] as $o_segment) {
$o_segments[] = $o_segment;
}
foreach ($intraline_segments[1] as $n_segment) {
$n_segments[] = $n_segment;
}
$diffs[$key] = array(
$o_segments,
$n_segments,
);
}
}
$this->setIntraLineDiffs($diffs);
$this->setDepthOnlyLines($depth_only);
return $this;
}
@ -671,4 +672,148 @@ final class DifferentialHunkParser extends Phobject {
return $offsets;
}
private function getIndentDepth($text, $tab_width) {
$len = strlen($text);
$depth = 0;
for ($ii = 0; $ii < $len; $ii++) {
$c = $text[$ii];
// If this is a space, increase the indent depth by 1.
if ($c == ' ') {
$depth++;
continue;
}
// If this is a tab, increase the indent depth to the next tabstop.
// For example, if the tab width is 4, these sequences both lead us to
// a visual width of 8, i.e. the cursor will be in the 8th column:
//
// <tab><tab>
// <space><tab><space><space><space><tab>
if ($c == "\t") {
$depth = ($depth + $tab_width);
$depth = $depth - ($depth % $tab_width);
continue;
}
break;
}
return $depth;
}
private function getCharacterCountForVisualWhitespace(
$text,
$depth,
$tab_width) {
// Here, we know the visual indent depth of a line has been increased by
// some amount (for example, 6 characters).
// We want to find the largest whitespace prefix of the string we can
// which still fits into that amount of visual space.
// In most cases, this is very easy. For example, if the string has been
// indented by two characters and the string begins with two spaces, that's
// a perfect match.
// However, if the string has been indented by 7 characters, the tab width
// is 8, and the string begins with "<space><space><tab>", we can only
// mark the two spaces as an indent change. These cases are unusual.
$character_depth = 0;
$visual_depth = 0;
$len = strlen($text);
for ($ii = 0; $ii < $len; $ii++) {
if ($visual_depth >= $depth) {
break;
}
$c = $text[$ii];
if ($c == ' ') {
$character_depth++;
$visual_depth++;
continue;
}
if ($c == "\t") {
// Figure out how many visual spaces we have until the next tabstop.
$tab_visual = ($visual_depth + $tab_width);
$tab_visual = $tab_visual - ($tab_visual % $tab_width);
$tab_visual = ($tab_visual - $visual_depth);
// If this tab would take us over the limit, we're all done.
$remaining_depth = ($depth - $visual_depth);
if ($remaining_depth < $tab_visual) {
break;
}
$character_depth++;
$visual_depth += $tab_visual;
continue;
}
break;
}
return $character_depth;
}
private function updateChangeTypesForNormalization() {
if (!$this->getNormalized()) {
return;
}
// If we've parsed based on a normalized diff alignment, we may currently
// believe some lines are unchanged when they have actually changed. This
// happens when:
//
// - a line changes;
// - the change is a kind of change we normalize away when aligning the
// diff, like an indentation change;
// - we normalize the change away to align the diff; and so
// - the old and new copies of the line are now aligned in the new
// normalized diff.
//
// Then we end up with an alignment where the two lines that differ only
// in some some trivial way are aligned. This is great, and exactly what
// we're trying to accomplish by doing all this alignment stuff in the
// first place.
//
// However, in this case the correctly-aligned lines will be incorrectly
// marked as unchanged because the diff alorithm was fed normalized copies
// of the lines, and these copies truly weren't any different.
//
// When lines are aligned and marked identical, but they're not actually
// identcal, we now mark them as changed. The rest of the processing will
// figure out how to render them appropritely.
$new = $this->getNewLines();
$old = $this->getOldLines();
foreach ($old as $key => $o) {
$n = $new[$key];
if (!$o || !$n) {
continue;
}
if ($o['type'] === null && $n['type'] === null) {
if ($o['text'] !== $n['text']) {
$old[$key]['type'] = '-';
$new[$key]['type'] = '+';
}
}
}
$this->setOldLines($old);
$this->setNewLines($new);
}
}

View file

@ -359,7 +359,6 @@ final class DifferentialLineAdjustmentMap extends Phobject {
}
$changeset = id(new PhabricatorDifferenceEngine())
->setIgnoreWhitespace(true)
->generateChangesetFromFileContent($u_old, $v_old);
$results[$u][$v] = self::newFromHunks(

View file

@ -367,7 +367,6 @@ abstract class DifferentialChangesetHTMLRenderer
$reference = $this->getRenderingReference();
if ($force !== 'text' &&
$force !== 'whitespace' &&
$force !== 'none' &&
$force !== 'default') {
throw new Exception(
@ -388,10 +387,6 @@ abstract class DifferentialChangesetHTMLRenderer
'range' => $range,
);
if ($force == 'whitespace') {
$meta['whitespace'] = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
}
$content = array();
$content[] = $message;
if ($force !== 'none') {
@ -437,11 +432,17 @@ abstract class DifferentialChangesetHTMLRenderer
$classes[] = 'PhabricatorMonospaced';
$classes[] = $this->getRendererTableClass();
$sigils = array();
$sigils[] = 'differential-diff';
foreach ($this->getTableSigils() as $sigil) {
$sigils[] = $sigil;
}
return javelin_tag(
'table',
array(
'class' => implode(' ', $classes),
'sigil' => 'differential-diff',
'sigil' => implode(' ', $sigils),
),
array(
$this->renderColgroup(),
@ -449,6 +450,10 @@ abstract class DifferentialChangesetHTMLRenderer
));
}
protected function getTableSigils() {
return array();
}
protected function buildInlineComment(
PhabricatorInlineCommentInterface $comment,
$on_right = false) {

View file

@ -92,19 +92,23 @@ final class DifferentialChangesetOneUpRenderer
$line = $p['line'];
$cells[] = phutil_tag(
'th',
'td',
array(
'id' => $left_id,
'class' => $class,
),
$line);
'class' => $class.' n',
'data-n' => $line,
));
$render = $p['render'];
if ($aural !== null) {
$render = array($aural, $render);
}
$cells[] = phutil_tag('th', array('class' => $class));
$cells[] = phutil_tag(
'td',
array(
'class' => $class.' n',
));
$cells[] = $no_copy;
$cells[] = phutil_tag('td', array('class' => $class), $render);
$cells[] = $no_coverage;
@ -115,7 +119,11 @@ final class DifferentialChangesetOneUpRenderer
} else {
$class = 'right new';
}
$cells[] = phutil_tag('th', array('class' => $class));
$cells[] = phutil_tag(
'td',
array(
'class' => $class.' n',
));
$aural = $aural_plus;
} else {
$class = 'right';
@ -127,7 +135,13 @@ final class DifferentialChangesetOneUpRenderer
$oline = $p['oline'];
$cells[] = phutil_tag('th', array('id' => $left_id), $oline);
$cells[] = phutil_tag(
'td',
array(
'id' => $left_id,
'class' => 'n',
'data-n' => $oline,
));
$aural = null;
}
@ -144,12 +158,12 @@ final class DifferentialChangesetOneUpRenderer
$line = $p['line'];
$cells[] = phutil_tag(
'th',
'td',
array(
'id' => $right_id,
'class' => $class,
),
$line);
'class' => $class.' n',
'data-n' => $line,
));
$render = $p['render'];
if ($aural !== null) {

View file

@ -28,12 +28,13 @@ abstract class DifferentialChangesetRenderer extends Phobject {
private $originalNew;
private $gaps;
private $mask;
private $depths;
private $originalCharacterEncoding;
private $showEditAndReplyLinks;
private $canMarkDone;
private $objectOwnerPHID;
private $highlightingDisabled;
private $scopeEngine = false;
private $depthOnlyLines;
private $oldFile = false;
private $newFile = false;
@ -76,14 +77,6 @@ abstract class DifferentialChangesetRenderer extends Phobject {
return $this->isUndershield;
}
public function setDepths($depths) {
$this->depths = $depths;
return $this;
}
protected function getDepths() {
return $this->depths;
}
public function setMask($mask) {
$this->mask = $mask;
return $this;
@ -100,6 +93,15 @@ abstract class DifferentialChangesetRenderer extends Phobject {
return $this->gaps;
}
public function setDepthOnlyLines(array $lines) {
$this->depthOnlyLines = $lines;
return $this;
}
public function getDepthOnlyLines() {
return $this->depthOnlyLines;
}
public function attachOldFile(PhabricatorFile $old = null) {
$this->oldFile = $old;
return $this;
@ -361,14 +363,14 @@ abstract class DifferentialChangesetRenderer extends Phobject {
$undershield = $this->renderUndershieldHeader();
}
$result = $notice.$props.$undershield.$content;
$result = array(
$notice,
$props,
$undershield,
$content,
);
// TODO: Let the user customize their tab width / display style.
// TODO: We should possibly post-process "\r" as well.
// TODO: Both these steps should happen earlier.
$result = str_replace("\t", ' ', $result);
return phutil_safe_html($result);
return hsprintf('%s', $result);
}
abstract public function isOneUpRenderer();
@ -404,9 +406,6 @@ abstract class DifferentialChangesetRenderer extends Phobject {
* important (e.g., generated code).
* - `"text"`: Force the text to be shown. This is probably only relevant
* when a file is not changed.
* - `"whitespace"`: Force the text to be shown, and the diff to be
* rendered with all whitespace shown. This is probably only relevant
* when a file is changed only by altering whitespace.
* - `"none"`: Don't show the link (e.g., text not available).
*
* @param string Message explaining why the diff is hidden.
@ -678,4 +677,42 @@ abstract class DifferentialChangesetRenderer extends Phobject {
return $views;
}
final protected function getScopeEngine() {
if ($this->scopeEngine === false) {
$hunk_starts = $this->getHunkStartLines();
// If this change is missing context, don't try to identify scopes, since
// we won't really be able to get anywhere.
$has_multiple_hunks = (count($hunk_starts) > 1);
$has_offset_hunks = (head_key($hunk_starts) != 1);
$missing_context = ($has_multiple_hunks || $has_offset_hunks);
if ($missing_context) {
$scope_engine = null;
} else {
$line_map = $this->getNewLineTextMap();
$scope_engine = id(new PhabricatorDiffScopeEngine())
->setLineTextMap($line_map);
}
$this->scopeEngine = $scope_engine;
}
return $this->scopeEngine;
}
private function getNewLineTextMap() {
$new = $this->getNewLines();
$text_map = array();
foreach ($new as $new_line) {
if (!isset($new_line['line'])) {
continue;
}
$text_map[$new_line['line']] = $new_line['text'];
}
return $text_map;
}
}

View file

@ -96,10 +96,14 @@ abstract class DifferentialChangesetTestRenderer
array(
'<span class="bright">',
'</span>',
'<span class="depth-out">',
'<span class="depth-in">',
),
array(
'{(',
')}',
'{<',
'{>',
),
$render);
@ -127,7 +131,7 @@ abstract class DifferentialChangesetTestRenderer
}
$out = implode("\n", $out)."\n";
return $out;
return phutil_safe_html($out);
}

View file

@ -3,6 +3,8 @@
final class DifferentialChangesetTwoUpRenderer
extends DifferentialChangesetHTMLRenderer {
private $newOffsetMap;
public function isOneUpRenderer() {
return false;
}
@ -66,9 +68,12 @@ final class DifferentialChangesetTwoUpRenderer
$new_render = $this->getNewRender();
$original_left = $this->getOriginalOld();
$original_right = $this->getOriginalNew();
$depths = $this->getDepths();
$mask = $this->getMask();
$scope_engine = $this->getScopeEngine();
$offset_map = null;
$depth_only = $this->getDepthOnlyLines();
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
// If we aren't going to show this line, we've just entered a gap.
@ -87,16 +92,19 @@ final class DifferentialChangesetTwoUpRenderer
$is_last_block = true;
}
$context = null;
$context_text = null;
$context_line = null;
if (!$is_last_block && $depths[$ii + $len]) {
for ($l = $ii + $len - 1; $l >= $ii; $l--) {
$line = $new_lines[$l]['text'];
if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') {
$context = $new_render[$l];
$context_line = $new_lines[$l]['line'];
break;
if (!$is_last_block && $scope_engine) {
$target_line = $new_lines[$ii + $len]['line'];
$context_line = $scope_engine->getScopeStart($target_line);
if ($context_line !== null) {
// The scope engine returns a line number in the file. We need
// to map that back to a display offset in the diff.
if (!$offset_map) {
$offset_map = $this->getNewLineToOffsetMap();
}
$offset = $offset_map[$context_line];
$context_text = $new_render[$offset];
}
}
@ -109,16 +117,20 @@ final class DifferentialChangesetTwoUpRenderer
phutil_tag(
'td',
array(
'colspan' => 2,
'class' => 'show-context-line n left-context',
)),
phutil_tag(
'td',
array(
'class' => 'show-more',
),
$contents),
phutil_tag(
'th',
'td',
array(
'class' => 'show-context-line',
),
$context_line ? (int)$context_line : null),
'class' => 'show-context-line n',
'data-n' => $context_line,
)),
phutil_tag(
'td',
array(
@ -126,7 +138,7 @@ final class DifferentialChangesetTwoUpRenderer
'class' => 'show-context',
),
// TODO: [HTML] Escaping model here isn't ideal.
phutil_safe_html($context)),
phutil_safe_html($context_text)),
));
$html[] = $container;
@ -188,12 +200,30 @@ final class DifferentialChangesetTwoUpRenderer
} else if (empty($old_lines[$ii])) {
$n_class = 'new new-full';
} else {
$n_class = 'new';
// NOTE: At least for the moment, I'm intentionally clearing the
// line highlighting only on the right side of the diff when a
// line has only depth changes. When a block depth is decreased,
// this gives us a large color block on the left (to make it easy
// to see the depth change) but a clean diff on the right (to make
// it easy to pick out actual code changes).
if (isset($depth_only[$ii])) {
$n_class = '';
} else {
$n_class = 'new';
}
}
$n_classes = $n_class;
if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) {
$n_copy = phutil_tag('td', array('class' => "copy {$n_class}"));
$not_copied =
// If this line only changed depth, copy markers are pointless.
(!isset($copy_lines[$n_num])) ||
(isset($depth_only[$ii])) ||
($new_lines[$ii]['type'] == '\\');
if ($not_copied) {
$n_copy = phutil_tag('td', array('class' => 'copy'));
} else {
list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num];
$title = ($orig_type == '-' ? 'Moved' : 'Copied').' from ';
@ -213,8 +243,7 @@ final class DifferentialChangesetTwoUpRenderer
'msg' => $title,
),
'class' => 'copy '.$class,
),
'');
));
}
}
}
@ -275,23 +304,41 @@ final class DifferentialChangesetTwoUpRenderer
}
}
// NOTE: This is a unicode zero-width space, which we use as a hint when
// intercepting 'copy' events to make sure sensible text ends up on the
// clipboard. See the 'phabricator-oncopy' behavior.
$zero_space = "\xE2\x80\x8B";
$old_number = phutil_tag(
'td',
array(
'id' => $o_id,
'class' => $o_classes.' n',
'data-n' => $o_num,
));
$new_number = phutil_tag(
'td',
array(
'id' => $n_id,
'class' => $n_classes.' n',
'data-n' => $n_num,
));
$html[] = phutil_tag('tr', array(), array(
phutil_tag('th', array('id' => $o_id, 'class' => $o_classes), $o_num),
phutil_tag('td', array('class' => $o_classes), $o_text),
phutil_tag('th', array('id' => $n_id, 'class' => $n_classes), $n_num),
$old_number,
phutil_tag(
'td',
array(
'class' => $o_classes,
'data-copy-mode' => 'copy-l',
),
$o_text),
$new_number,
$n_copy,
phutil_tag(
'td',
array('class' => $n_classes, 'colspan' => $n_colspan),
array(
phutil_tag('span', array('class' => 'zwsp'), $zero_space),
$n_text,
)),
'class' => $n_classes,
'colspan' => $n_colspan,
'data-copy-mode' => 'copy-r',
),
$n_text),
$n_cov,
));
@ -386,4 +433,28 @@ final class DifferentialChangesetTwoUpRenderer
->addInlineView($view);
}
private function getNewLineToOffsetMap() {
if ($this->newOffsetMap === null) {
$new = $this->getNewLines();
$map = array();
foreach ($new as $offset => $new_line) {
if ($new_line['line'] === null) {
continue;
}
$map[$new_line['line']] = $offset;
}
$this->newOffsetMap = $map;
}
return $this->newOffsetMap;
}
protected function getTableSigils() {
return array(
'intercept-copy',
);
}
}

View file

@ -249,17 +249,6 @@ final class DifferentialChangeset
return $path;
}
public function getWhitespaceMatters() {
$config = PhabricatorEnv::getEnvConfig('differential.whitespace-matters');
foreach ($config as $regexp) {
if (preg_match($regexp, $this->getFilename())) {
return true;
}
}
return false;
}
public function attachDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;

View file

@ -387,9 +387,10 @@ final class DifferentialDiff
return array();
}
$unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
$unit = id(new HarbormasterBuildUnitMessageQuery())
->setViewer($viewer)
->withBuildTargetPHIDs($target_phids)
->execute();
$map = array();
foreach ($unit as $message) {

View file

@ -9,7 +9,6 @@ final class DifferentialChangesetDetailView extends AphrontView {
private $id;
private $vsChangesetID;
private $renderURI;
private $whitespace;
private $renderingRef;
private $autoload;
private $loaded;
@ -42,15 +41,6 @@ final class DifferentialChangesetDetailView extends AphrontView {
return $this->renderingRef;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function getWhitespace() {
return $this->whitespace;
}
public function setRenderURI($render_uri) {
$this->renderURI = $render_uri;
return $this;
@ -196,7 +186,6 @@ final class DifferentialChangesetDetailView extends AphrontView {
'left' => $left_id,
'right' => $right_id,
'renderURI' => $this->getRenderURI(),
'whitespace' => $this->getWhitespace(),
'highlight' => null,
'renderer' => $this->getRenderer(),
'ref' => $this->getRenderingRef(),

View file

@ -7,7 +7,6 @@ final class DifferentialChangesetListView extends AphrontView {
private $references = array();
private $inlineURI;
private $renderURI = '/differential/changeset/';
private $whitespace;
private $background;
private $header;
private $isStandalone;
@ -100,11 +99,6 @@ final class DifferentialChangesetListView extends AphrontView {
return $this;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function setVsMap(array $vs_map) {
$this->vsMap = $vs_map;
return $this;
@ -180,7 +174,6 @@ final class DifferentialChangesetListView extends AphrontView {
$detail->setRenderingRef($ref);
$detail->setRenderURI($this->renderURI);
$detail->setWhitespace($this->whitespace);
$detail->setRenderer($renderer);
if ($this->getParser()) {
@ -352,8 +345,7 @@ final class DifferentialChangesetListView extends AphrontView {
$meta = array();
$qparams = array(
'ref' => $ref,
'whitespace' => $this->whitespace,
'ref' => $ref,
);
if ($this->standaloneURI) {

View file

@ -5,7 +5,6 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView {
private $diffs = array();
private $selectedVersusDiffID;
private $selectedDiffID;
private $selectedWhitespace;
private $commitsForLinks = array();
private $unitStatus = array();
@ -25,11 +24,6 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView {
return $this;
}
public function setSelectedWhitespace($whitespace) {
$this->selectedWhitespace = $whitespace;
return $this;
}
public function setCommitsForLinks(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$this->commitsForLinks = $commits;
@ -224,28 +218,6 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView {
'radios' => $radios,
));
$options = array(
DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => pht('Ignore All'),
DifferentialChangesetParser::WHITESPACE_IGNORE_MOST => pht('Ignore Most'),
DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING =>
pht('Ignore Trailing'),
DifferentialChangesetParser::WHITESPACE_SHOW_ALL => pht('Show All'),
);
foreach ($options as $value => $label) {
$options[$value] = phutil_tag(
'option',
array(
'value' => $value,
'selected' => ($value == $this->selectedWhitespace)
? 'selected'
: null,
),
$label);
}
$select = phutil_tag('select', array('name' => 'whitespace'), $options);
$table = id(new AphrontTableView($rows));
$table->setHeaders(
array(
@ -291,13 +263,6 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView {
'class' => 'differential-update-history-footer',
),
array(
phutil_tag(
'label',
array(),
array(
pht('Whitespace Changes:'),
$select,
)),
phutil_tag(
'button',
array(),

View file

@ -64,9 +64,6 @@ final class DiffusionChangeController extends DiffusionController {
$changeset_view->setRawFileURIs($left_uri, $right_uri);
$changeset_view->setRenderURI($repository->getPathURI('diff/'));
$changeset_view->setWhitespace(
DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
$changeset_view->setUser($viewer);
$changeset_view->setHeader($changeset_header);

View file

@ -88,9 +88,6 @@ final class DiffusionDiffController extends DiffusionController {
($viewer->getPHID() == $commit->getAuthorPHID()));
$parser->setObjectOwnerPHID($commit->getAuthorPHID());
$parser->setWhitespaceMode(
DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
$inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments(
$viewer,
$commit->getPHID(),

View file

@ -312,9 +312,10 @@ final class HarbormasterBuildableViewController
'buildTargetPHID IN (%Ls)',
$target_phids);
$unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
$unit_data = id(new HarbormasterBuildUnitMessageQuery())
->setViewer($viewer)
->withBuildTargetPHIDs($target_phids)
->execute();
if ($lint_data) {
$lint_table = id(new HarbormasterLintPropertyView())

View file

@ -31,9 +31,10 @@ final class HarbormasterUnitMessageListController
$unit_data = array();
if ($target_phids) {
$unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
$unit_data = id(new HarbormasterBuildUnitMessageQuery())
->setViewer($viewer)
->withBuildTargetPHIDs($target_phids)
->execute();
} else {
$unit_data = array();
}

View file

@ -12,7 +12,10 @@ final class HarbormasterUnitMessageViewController
$message_id = $request->getURIData('id');
$message = id(new HarbormasterBuildUnitMessage())->load($message_id);
$message = id(new HarbormasterBuildUnitMessageQuery())
->setViewer($viewer)
->withIDs(array($message_id))
->executeOne();
if (!$message) {
return new Aphront404Response();
}

View file

@ -0,0 +1,95 @@
<?php
final class HarbormasterBuildUnitMessageQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $targetPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBuildTargetPHIDs(array $target_phids) {
$this->targetPHIDs = $target_phids;
return $this;
}
public function newResultObject() {
return new HarbormasterBuildUnitMessage();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid in (%Ls)',
$this->phids);
}
if ($this->targetPHIDs !== null) {
$where[] = qsprintf(
$conn,
'buildTargetPHID in (%Ls)',
$this->targetPHIDs);
}
return $where;
}
protected function didFilterPage(array $messages) {
$indexes = array();
foreach ($messages as $message) {
$index = $message->getNameIndex();
if (strlen($index)) {
$indexes[$index] = $index;
}
}
if ($indexes) {
$map = HarbormasterString::newIndexMap($indexes);
foreach ($messages as $message) {
$index = $message->getNameIndex();
if (!strlen($index)) {
continue;
}
$name = idx($map, $index);
if ($name === null) {
$name = pht('Unknown Unit Message ("%s")', $index);
}
$message->setName($name);
}
}
return $messages;
}
public function getQueryApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
}

View file

@ -0,0 +1,54 @@
<?php
final class HarbormasterString
extends HarbormasterDAO {
protected $stringIndex;
protected $stringValue;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'stringIndex' => 'bytes12',
'stringValue' => 'text',
),
self::CONFIG_KEY_SCHEMA => array(
'key_string' => array(
'columns' => array('stringIndex'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public static function newIndex($string) {
$index = PhabricatorHash::digestForIndex($string);
$table = new self();
$conn = $table->establishConnection('w');
queryfx(
$conn,
'INSERT IGNORE INTO %R (stringIndex, stringValue) VALUES (%s, %s)',
$table,
$index,
$string);
return $index;
}
public static function newIndexMap(array $indexes) {
$table = new self();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT stringIndex, stringValue FROM %R WHERE stringIndex IN (%Ls)',
$table,
$indexes);
return ipull($rows, 'stringValue', 'stringIndex');
}
}

View file

@ -1,12 +1,14 @@
<?php
final class HarbormasterBuildUnitMessage
extends HarbormasterDAO {
extends HarbormasterDAO
implements PhabricatorPolicyInterface {
protected $buildTargetPHID;
protected $engine;
protected $namespace;
protected $name;
protected $nameIndex;
protected $result;
protected $duration;
protected $properties = array();
@ -131,6 +133,7 @@ final class HarbormasterBuildUnitMessage
'engine' => 'text255',
'namespace' => 'text255',
'name' => 'text255',
'nameIndex' => 'bytes12',
'result' => 'text32',
'duration' => 'double?',
),
@ -259,4 +262,52 @@ final class HarbormasterBuildUnitMessage
return implode("\0", $parts);
}
public function save() {
if ($this->nameIndex === null) {
$this->nameIndex = HarbormasterString::newIndex($this->getName());
}
// See T13088. While we're letting installs do online migrations to avoid
// downtime, don't populate the "name" column for new writes. New writes
// use the "HarbormasterString" table instead.
$old_name = $this->name;
$this->name = '';
$caught = null;
try {
$result = parent::save();
} catch (Exception $ex) {
$caught = $ex;
}
$this->name = $old_name;
if ($caught) {
throw $caught;
}
return $result;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}

View file

@ -210,8 +210,9 @@ The keys you can provide in a specification are:
- `claim` //Optional bool.// By default, closing an unassigned task claims
it. You can set this to `false` to disable this behavior for a particular
status.
- `locked` //Optional bool.// Lock tasks in this status, preventing users
from commenting.
- `locked` //Optional string.// Lock tasks in this status. Specify "comments"
to lock comments (users who can edit the task may override this lock).
Specify "edits" to prevent anyone except the task owner from making edits.
- `mfa` //Optional bool.// Require all edits to this task to be signed with
multi-factor authentication.
@ -342,6 +343,7 @@ dictionary with these keys:
- `icon` //Optional string.// Icon for the subtype.
- `children` //Optional map.// Configure options shown to the user when
they "Create Subtask". See below.
- `fields` //Optional map.// Configure field behaviors. See below.
Each subtype must have a unique key, and you must define a subtype with
the key "%s", which is used as a default subtype.
@ -397,6 +399,28 @@ be used when presenting options to the user.
If only one option would be presented, the user will be taken directly to the
appropriate form instead of being prompted to choose a form.
The `fields` key can configure the behavior of custom fields on specific
task subtypes. For example:
```
{
...
"fields": {
"custom.some-field": {
"disabled": true
}
}
...
}
```
Each field supports these options:
- `disabled` //Optional bool.// Allows you to disable fields on certain
subtypes.
- `name` //Optional string.// Custom name of this field for the subtype.
EOTEXT
,
$subtype_default_key));

View file

@ -16,6 +16,9 @@ final class ManiphestTaskStatus extends ManiphestConstants {
const SPECIAL_CLOSED = 'closed';
const SPECIAL_DUPLICATE = 'duplicate';
const LOCKED_COMMENTS = 'comments';
const LOCKED_EDITS = 'edits';
private static function getStatusConfig() {
return PhabricatorEnv::getEnvConfig('maniphest.statuses');
}
@ -156,8 +159,13 @@ final class ManiphestTaskStatus extends ManiphestConstants {
return !self::isOpenStatus($status);
}
public static function isLockedStatus($status) {
return self::getStatusAttribute($status, 'locked', false);
public static function areCommentsLockedInStatus($status) {
return (bool)self::getStatusAttribute($status, 'locked', false);
}
public static function areEditsLockedInStatus($status) {
$locked = self::getStatusAttribute($status, 'locked');
return ($locked === self::LOCKED_EDITS);
}
public static function isMFAStatus($status) {
@ -285,11 +293,35 @@ final class ManiphestTaskStatus extends ManiphestConstants {
'keywords' => 'optional list<string>',
'disabled' => 'optional bool',
'claim' => 'optional bool',
'locked' => 'optional bool',
'locked' => 'optional bool|string',
'mfa' => 'optional bool',
));
}
// Supported values are "comments" or "edits". For backward compatibility,
// "true" is an alias of "comments".
foreach ($config as $key => $value) {
$locked = idx($value, 'locked', false);
if ($locked === true || $locked === false) {
continue;
}
if ($locked === self::LOCKED_EDITS ||
$locked === self::LOCKED_COMMENTS) {
continue;
}
throw new Exception(
pht(
'Task status ("%s") has unrecognized value for "locked" '.
'configuration ("%s"). Supported values are: "%s", "%s".',
$key,
$locked,
self::LOCKED_COMMENTS,
self::LOCKED_EDITS));
}
$special_map = array();
foreach ($config as $key => $value) {
$special = idx($value, 'special');

View file

@ -552,6 +552,10 @@ final class ManiphestTransactionEditor
$errors = array_merge($errors, $this->moreValidationErrors);
}
foreach ($this->getLockValidationErrors($object, $xactions) as $error) {
$errors[] = $error;
}
return $errors;
}
@ -1011,5 +1015,86 @@ final class ManiphestTransactionEditor
}
private function getLockValidationErrors($object, array $xactions) {
$errors = array();
$old_owner = $object->getOwnerPHID();
$old_status = $object->getStatus();
$new_owner = $old_owner;
$new_status = $old_status;
$owner_xaction = null;
$status_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
$new_owner = $xaction->getNewValue();
$owner_xaction = $xaction;
break;
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$new_status = $xaction->getNewValue();
$status_xaction = $xaction;
break;
}
}
$actor_phid = $this->getActingAsPHID();
$was_locked = ManiphestTaskStatus::areEditsLockedInStatus(
$old_status);
$now_locked = ManiphestTaskStatus::areEditsLockedInStatus(
$new_status);
if (!$now_locked) {
// If we're not ending in an edit-locked status, everything is good.
} else if ($new_owner !== null) {
// If we ending the edit with some valid owner, this is allowed for
// now. We might need to revisit this.
} else {
// The edits end with the task locked and unowned. No one will be able
// to edit it, so we forbid this. We try to be specific about what the
// user did wrong.
$owner_changed = ($old_owner && !$new_owner);
$status_changed = ($was_locked !== $now_locked);
$message = null;
if ($status_changed && $owner_changed) {
$message = pht(
'You can not lock this task and unassign it at the same time '.
'because no one will be able to edit it anymore. Lock the task '.
'or remove the owner, but not both.');
$problem_xaction = $status_xaction;
} else if ($status_changed) {
$message = pht(
'You can not lock this task because it does not have an owner. '.
'No one would be able to edit the task. Assign the task to an '.
'owner before locking it.');
$problem_xaction = $status_xaction;
} else if ($owner_changed) {
$message = pht(
'You can not remove the owner of this task because it is locked '.
'and no one would be able to edit the task. Reassign the task or '.
'unlock it before removing the owner.');
$problem_xaction = $owner_xaction;
} else {
// If the task was already broken, we don't have a transaction to
// complain about so just let it through. In theory, this is
// impossible since policy rules should kick in before we get here.
}
if ($message) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$problem_xaction->getTransactionType(),
pht('Lock Error'),
$message,
$problem_xaction);
}
}
return $errors;
}
}

View file

@ -0,0 +1,70 @@
<?php
final class ManiphestTaskPolicyCodex
extends PhabricatorPolicyCodex {
public function getPolicyShortName() {
$object = $this->getObject();
if ($object->areEditsLocked()) {
return pht('Edits Locked');
}
return null;
}
public function getPolicyIcon() {
$object = $this->getObject();
if ($object->areEditsLocked()) {
return 'fa-lock';
}
return null;
}
public function getPolicyTagClasses() {
$object = $this->getObject();
$classes = array();
if ($object->areEditsLocked()) {
$classes[] = 'policy-adjusted-locked';
}
return $classes;
}
public function getPolicySpecialRuleDescriptions() {
$object = $this->getObject();
$rules = array();
$rules[] = $this->newRule()
->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->setIsActive($object->areEditsLocked())
->setDescription(
pht(
'Tasks with edits locked may only be edited by their owner.'));
return $rules;
}
public function getPolicyForEdit($capability) {
// When a task has its edits locked, the effective edit policy is locked
// to "No One". However, the task owner may still bypass the lock and edit
// the task. When they do, we want the control in the UI to have the
// correct value. Return the real value stored on the object.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getObject()->getEditPolicy();
}
return parent::getPolicyForEdit($capability);
}
}

View file

@ -618,9 +618,9 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
$joins = array();
if ($this->hasOpenParents !== null) {
if ($this->hasOpenParents) {
$join_type = 'JOIN';
$join_type = qsprintf($conn, 'JOIN');
} else {
$join_type = 'LEFT JOIN';
$join_type = qsprintf($conn, 'LEFT JOIN');
}
$joins[] = qsprintf(

View file

@ -20,7 +20,8 @@ final class ManiphestTask extends ManiphestDAO
DoorkeeperBridgedObjectInterface,
PhabricatorEditEngineSubtypeInterface,
PhabricatorEditEngineLockableInterface,
PhabricatorEditEngineMFAInterface {
PhabricatorEditEngineMFAInterface,
PhabricatorPolicyCodexInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
@ -217,8 +218,16 @@ final class ManiphestTask extends ManiphestDAO
return ManiphestTaskStatus::isClosedStatus($this->getStatus());
}
public function isLocked() {
return ManiphestTaskStatus::isLockedStatus($this->getStatus());
public function areCommentsLocked() {
if ($this->areEditsLocked()) {
return true;
}
return ManiphestTaskStatus::areCommentsLockedInStatus($this->getStatus());
}
public function areEditsLocked() {
return ManiphestTaskStatus::areEditsLockedInStatus($this->getStatus());
}
public function setProperty($key, $value) {
@ -371,13 +380,17 @@ final class ManiphestTask extends ManiphestDAO
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_INTERACT:
if ($this->isLocked()) {
if ($this->areCommentsLocked()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getViewPolicy();
}
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
if ($this->areEditsLocked()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getEditPolicy();
}
}
}
@ -628,4 +641,12 @@ final class ManiphestTask extends ManiphestDAO
return new ManiphestTaskMFAEngine();
}
/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
public function newPolicyCodex() {
return new ManiphestTaskPolicyCodex();
}
}

View file

@ -19,7 +19,8 @@ final class PhabricatorPeopleDatasource
$viewer = $this->getViewer();
$query = id(new PhabricatorPeopleQuery())
->setOrderVector(array('username'));
->setOrderVector(array('username'))
->needAvailability(true);
if ($this->getPhase() == self::PHASE_PREFIX) {
$prefix = $this->getPrefixQuery();
@ -96,6 +97,14 @@ final class PhabricatorPeopleDatasource
$result->setDisplayType($display_type);
}
$until = $user->getAwayUntil();
if ($until) {
$availability = $user->getDisplayAvailability();
$color = PhabricatorCalendarEventInvitee::getAvailabilityColor(
$availability);
$result->setAvailabilityColor($color);
}
$results[] = $result;
}

View file

@ -29,6 +29,10 @@ abstract class PhabricatorPolicyCodex
return array();
}
public function getPolicyForEdit($capability) {
return $this->getObject()->getPolicy($capability);
}
public function getDefaultPolicy() {
return PhabricatorPolicyQuery::getDefaultPolicyForObject(
$this->viewer,

View file

@ -68,6 +68,14 @@ final class PhabricatorPolicyEditEngineExtension
),
);
if ($object instanceof PhabricatorPolicyCodexInterface) {
$codex = PhabricatorPolicyCodex::newFromObject(
$object,
$viewer);
} else {
$codex = null;
}
$fields = array();
foreach ($map as $type => $spec) {
if (empty($types[$type])) {
@ -82,6 +90,18 @@ final class PhabricatorPolicyEditEngineExtension
$conduit_description = $spec['description.conduit'];
$edit = $spec['edit'];
// Objects may present a policy value to the edit workflow that is
// different from their nominal policy value: for example, when tasks
// are locked, they appear as "Editable By: No One" to other applications
// but we still want to edit the actual policy stored in the database
// when we show the user a form with a policy control in it.
if ($codex) {
$policy_value = $codex->getPolicyForEdit($capability);
} else {
$policy_value = $object->getPolicy($capability);
}
$policy_field = id(new PhabricatorPolicyEditField())
->setKey($key)
->setLabel($label)
@ -94,7 +114,7 @@ final class PhabricatorPolicyEditEngineExtension
->setDescription($description)
->setConduitDescription($conduit_description)
->setConduitTypeDescription(pht('New policy PHID or constant.'))
->setValue($object->getPolicy($capability));
->setValue($policy_value);
$fields[] = $policy_field;
if ($object instanceof PhabricatorSpacesInterface) {

View file

@ -116,6 +116,10 @@ final class PhabricatorSearchManagementIndexWorkflow
// them a hint that they might want to use "--force".
$track_skips = (!$is_background && !$is_force);
// Activate "strict" error reporting if we're running in the foreground
// so we'll report a wider range of conditions as errors.
$is_strict = !$is_background;
$count_updated = 0;
$count_skipped = 0;
@ -125,7 +129,10 @@ final class PhabricatorSearchManagementIndexWorkflow
$old_versions = $this->loadIndexVersions($phid);
}
PhabricatorSearchWorker::queueDocumentForIndexing($phid, $parameters);
PhabricatorSearchWorker::queueDocumentForIndexing(
$phid,
$parameters,
$is_strict);
if ($track_skips) {
$new_versions = $this->loadIndexVersions($phid);

View file

@ -2,7 +2,11 @@
final class PhabricatorSearchWorker extends PhabricatorWorker {
public static function queueDocumentForIndexing($phid, $parameters = null) {
public static function queueDocumentForIndexing(
$phid,
$parameters = null,
$is_strict = false) {
if ($parameters === null) {
$parameters = array();
}
@ -12,6 +16,7 @@ final class PhabricatorSearchWorker extends PhabricatorWorker {
array(
'documentPHID' => $phid,
'parameters' => $parameters,
'strict' => $is_strict,
),
array(
'priority' => parent::PRIORITY_INDEX,
@ -23,7 +28,25 @@ final class PhabricatorSearchWorker extends PhabricatorWorker {
$data = $this->getTaskData();
$object_phid = idx($data, 'documentPHID');
$object = $this->loadObjectForIndexing($object_phid);
// See T12425. By the time we run an indexing task, the object it indexes
// may have been deleted. This is unusual, but not concerning, and failing
// to index these objects is correct.
// To avoid showing these non-actionable errors to users, don't report
// indexing exceptions unless we're in "strict" mode. This mode is set by
// the "bin/search index" tool.
$is_strict = idx($data, 'strict', false);
try {
$object = $this->loadObjectForIndexing($object_phid);
} catch (PhabricatorWorkerPermanentFailureException $ex) {
if ($is_strict) {
throw $ex;
} else {
return;
}
}
$engine = id(new PhabricatorIndexEngine())
->setObject($object);
@ -35,8 +58,11 @@ final class PhabricatorSearchWorker extends PhabricatorWorker {
return;
}
$key = "index.{$object_phid}";
$lock = PhabricatorGlobalLock::newLock($key);
$lock = PhabricatorGlobalLock::newLock(
'index',
array(
'objectPHID' => $object_phid,
));
try {
$lock->lock(1);
@ -48,29 +74,34 @@ final class PhabricatorSearchWorker extends PhabricatorWorker {
throw new PhabricatorWorkerYieldException(15);
}
$caught = null;
try {
// Reload the object now that we have a lock, to make sure we have the
// most current version.
$object = $this->loadObjectForIndexing($object->getPHID());
$engine->setObject($object);
$engine->indexObject();
} catch (Exception $ex) {
$lock->unlock();
$caught = $ex;
}
if (!($ex instanceof PhabricatorWorkerPermanentFailureException)) {
$ex = new PhabricatorWorkerPermanentFailureException(
// Release the lock before we deal with the exception.
$lock->unlock();
if ($caught) {
if (!($caught instanceof PhabricatorWorkerPermanentFailureException)) {
$caught = new PhabricatorWorkerPermanentFailureException(
pht(
'Failed to update search index for document "%s": %s',
$object_phid,
$ex->getMessage()));
$caught->getMessage()));
}
throw $ex;
if ($is_strict) {
throw $caught;
}
}
$lock->unlock();
}
private function loadObjectForIndexing($phid) {

View file

@ -47,15 +47,6 @@ final class PhabricatorSubscriptionsEditController
$handle->getURI());
}
if (!PhabricatorPolicyFilter::canInteract($viewer, $object)) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
$dialog = $this->newDialog()
->addCancelButton($handle->getURI());
return $lock->willBlockUserInteractionWithDialog($dialog);
}
if ($object instanceof PhabricatorApplicationTransactionInterface) {
if ($is_add) {
$xaction_value = array(

View file

@ -73,24 +73,20 @@ final class PhabricatorSubscriptionsUIEventListener
->setName(pht('Automatically Subscribed'))
->setIcon('fa-check-circle lightgreytext');
} else {
$can_interact = PhabricatorPolicyFilter::canInteract($user, $object);
if ($is_subscribed) {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setRenderAsForm(true)
->setHref('/subscriptions/delete/'.$object->getPHID().'/')
->setName(pht('Unsubscribe'))
->setIcon('fa-minus-circle')
->setDisabled(!$can_interact);
->setIcon('fa-minus-circle');
} else {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setRenderAsForm(true)
->setHref('/subscriptions/add/'.$object->getPHID().'/')
->setName(pht('Subscribe'))
->setIcon('fa-plus-circle')
->setDisabled(!$can_interact);
->setIcon('fa-plus-circle');
}
if (!$user->isLoggedIn()) {

View file

@ -165,14 +165,29 @@ abstract class PhabricatorEditEngine
$extensions = array();
}
// See T13248. Create a template object to provide to extensions. We
// adjust the template to have the intended subtype, so that extensions
// may change behavior based on the form subtype.
$template_object = clone $object;
if ($this->getIsCreate()) {
if ($this->supportsSubtypes()) {
$config = $this->getEditEngineConfiguration();
$subtype = $config->getSubtype();
$template_object->setSubtype($subtype);
}
}
foreach ($extensions as $extension) {
$extension->setViewer($viewer);
if (!$extension->supportsObject($this, $object)) {
if (!$extension->supportsObject($this, $template_object)) {
continue;
}
$extension_fields = $extension->buildCustomEditFields($this, $object);
$extension_fields = $extension->buildCustomEditFields(
$this,
$template_object);
// TODO: Validate this in more detail with a more tailored error.
assert_instances_of($extension_fields, 'PhabricatorEditField');

View file

@ -13,6 +13,7 @@ final class PhabricatorEditEngineSubtype
private $color;
private $childSubtypes = array();
private $childIdentifiers = array();
private $fieldConfiguration = array();
public function setKey($key) {
$this->key = $key;
@ -94,6 +95,17 @@ final class PhabricatorEditEngineSubtype
return $view;
}
public function setSubtypeFieldConfiguration(
$subtype_key,
array $configuration) {
$this->fieldConfiguration[$subtype_key] = $configuration;
return $this;
}
public function getSubtypeFieldConfiguration($subtype_key) {
return idx($this->fieldConfiguration, $subtype_key);
}
public static function validateSubtypeKey($subtype) {
if (strlen($subtype) > 64) {
throw new Exception(
@ -139,6 +151,7 @@ final class PhabricatorEditEngineSubtype
'color' => 'optional string',
'icon' => 'optional string',
'children' => 'optional map<string, wild>',
'fields' => 'optional map<string, wild>',
));
$key = $value['key'];
@ -183,6 +196,18 @@ final class PhabricatorEditEngineSubtype
'or the other, but not both.'));
}
}
$fields = idx($value, 'fields');
if ($fields) {
foreach ($fields as $field_key => $configuration) {
PhutilTypeSpec::checkMap(
$configuration,
array(
'disabled' => 'optional bool',
'name' => 'optional string',
));
}
}
}
if (!isset($map[self::SUBTYPE_DEFAULT])) {
@ -233,6 +258,15 @@ final class PhabricatorEditEngineSubtype
$subtype->setChildFormIdentifiers($child_forms);
}
$field_configurations = idx($entry, 'fields');
if ($field_configurations) {
foreach ($field_configurations as $field_key => $field_configuration) {
$subtype->setSubtypeFieldConfiguration(
$field_key,
$field_configuration);
}
}
$map[$key] = $subtype;
}

View file

@ -1648,9 +1648,48 @@ abstract class PhabricatorApplicationTransactionEditor
// don't enforce it here.
return null;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
// TODO: Removing subscribers other than yourself should probably
// require CAN_EDIT permission. You can do this via the API but
// generally can not via the web interface.
// Anyone can subscribe to or unsubscribe from anything they can view,
// with no other permissions.
$old = array_fuse($xaction->getOldValue());
$new = array_fuse($xaction->getNewValue());
// To remove users other than yourself, you must be able to edit the
// object.
$rem = array_diff_key($old, $new);
foreach ($rem as $phid) {
if ($phid !== $this->getActingAsPHID()) {
return PhabricatorPolicyCapability::CAN_EDIT;
}
}
// To add users other than yourself, you must be able to interact.
// This allows "@mentioning" users to work as long as you can comment
// on objects.
// If you can edit, we return that policy instead so that you can
// override a soft lock and still make edits.
// TODO: This is a little bit hacky. We really want to be able to say
// "this requires either interact or edit", but there's currently no
// way to specify this kind of requirement.
$can_edit = PhabricatorPolicyFilter::hasCapability(
$this->getActor(),
$this->object,
PhabricatorPolicyCapability::CAN_EDIT);
$add = array_diff_key($new, $old);
foreach ($add as $phid) {
if ($phid !== $this->getActingAsPHID()) {
if ($can_edit) {
return PhabricatorPolicyCapability::CAN_EDIT;
} else {
return PhabricatorPolicyCapability::CAN_INTERACT;
}
}
}
return null;
case PhabricatorTransactions::TYPE_TOKEN:
// TODO: This technically requires CAN_INTERACT, like comments.

View file

@ -19,6 +19,7 @@ final class PhabricatorTypeaheadResult extends Phobject {
private $autocomplete;
private $attributes = array();
private $phase;
private $availabilityColor;
public function setIcon($icon) {
$this->icon = $icon;
@ -156,6 +157,7 @@ final class PhabricatorTypeaheadResult extends Phobject {
$this->unique ? 1 : null,
$this->autocomplete,
$this->phase,
$this->availabilityColor,
);
while (end($data) === null) {
array_pop($data);
@ -222,4 +224,13 @@ final class PhabricatorTypeaheadResult extends Phobject {
return $this->phase;
}
public function setAvailabilityColor($availability_color) {
$this->availabilityColor = $availability_color;
return $this;
}
public function getAvailabilityColor() {
return $this->availabilityColor;
}
}

View file

@ -14,6 +14,7 @@ final class PhabricatorTypeaheadTokenView
private $inputName;
private $value;
private $tokenType = self::TYPE_OBJECT;
private $availabilityColor;
public static function newFromTypeaheadResult(
PhabricatorTypeaheadResult $result) {
@ -41,6 +42,21 @@ final class PhabricatorTypeaheadTokenView
$token->setColor($handle->getTagColor());
}
$availability = $handle->getAvailability();
$color = null;
switch ($availability) {
case PhabricatorObjectHandle::AVAILABILITY_PARTIAL:
$color = PHUITagView::COLOR_ORANGE;
break;
case PhabricatorObjectHandle::AVAILABILITY_NONE:
$color = PHUITagView::COLOR_RED;
break;
}
if ($color !== null) {
$token->setAvailabilityColor($color);
}
return $token;
}
@ -106,6 +122,15 @@ final class PhabricatorTypeaheadTokenView
return 'a';
}
public function setAvailabilityColor($availability_color) {
$this->availabilityColor = $availability_color;
return $this;
}
public function getAvailabilityColor() {
return $this->availabilityColor;
}
protected function getTagAttributes() {
$classes = array();
$classes[] = 'jx-tokenizer-token';
@ -139,20 +164,32 @@ final class PhabricatorTypeaheadTokenView
$value = $this->getValue();
$availability = null;
$availability_color = $this->getAvailabilityColor();
if ($availability_color) {
$availability = phutil_tag(
'span',
array(
'class' => 'phui-tag-dot phui-tag-color-'.$availability_color,
));
}
$icon_view = null;
$icon = $this->getIcon();
if ($icon) {
$value = array(
phutil_tag(
'span',
array(
'class' => 'phui-icon-view phui-font-fa '.$icon,
)),
$value,
);
$icon_view = phutil_tag(
'span',
array(
'class' => 'phui-icon-view phui-font-fa '.$icon,
));
}
return array(
$value,
array(
$icon_view,
$availability,
$value,
),
phutil_tag(
'input',
array(

View file

@ -51,22 +51,6 @@ You need to install and configure **Pygments** to highlight anything else than
PHP. See the `pygments.enabled` configuration setting.
= What do the whitespace options mean? =
Most of these are pretty straightforward, but "Ignore Most" is not:
- **Show All**: Show all whitespace.
- **Ignore Trailing**: Ignore changes which only affect trailing whitespace.
- **Ignore Most**: Ignore changes which only affect leading or trailing
whitespace (but not whitespace changes between non-whitespace characters)
in files which are not marked as having significant whitespace.
In those files, show whitespace changes. By default, Python (.py) and
Haskell (.lhs, .hs) are marked as having significant whitespace, but this
can be changed in the `differential.whitespace-matters` configuration
setting.
- **Ignore All**: Ignore all whitespace changes in all files.
= What do the very light green and red backgrounds mean? =
Differential uses these colors to mark changes coming from rebase: they are

View file

@ -74,9 +74,22 @@ abstract class PhabricatorCustomField extends Phobject {
$spec,
$object);
$fields = self::adjustCustomFieldsForObjectSubtype(
$object,
$role,
$fields);
foreach ($fields as $key => $field) {
// NOTE: We perform this filtering in "buildFieldList()", but may need
// to filter again after subtype adjustment.
if (!$field->isFieldEnabled()) {
unset($fields[$key]);
continue;
}
if (!$field->shouldEnableForRole($role)) {
unset($fields[$key]);
continue;
}
}
@ -1622,4 +1635,78 @@ abstract class PhabricatorCustomField extends Phobject {
return null;
}
private static function adjustCustomFieldsForObjectSubtype(
PhabricatorCustomFieldInterface $object,
$role,
array $fields) {
assert_instances_of($fields, __CLASS__);
// We only apply subtype adjustment for some roles. For example, when
// writing Herald rules or building a Search interface, we always want to
// show all the fields in their default state, so we do not apply any
// adjustments.
$subtype_roles = array(
self::ROLE_EDITENGINE,
self::ROLE_VIEW,
);
$subtype_roles = array_fuse($subtype_roles);
if (!isset($subtype_roles[$role])) {
return $fields;
}
// If the object doesn't support subtypes, we can't possibly make
// any adjustments based on subtype.
if (!($object instanceof PhabricatorEditEngineSubtypeInterface)) {
return $fields;
}
$subtype_map = $object->newEditEngineSubtypeMap();
$subtype_key = $object->getEditEngineSubtype();
$subtype_object = $subtype_map->getSubtype($subtype_key);
$map = array();
foreach ($fields as $field) {
$modern_key = $field->getModernFieldKey();
if (!strlen($modern_key)) {
continue;
}
$map[$modern_key] = $field;
}
foreach ($map as $field_key => $field) {
// For now, only support overriding standard custom fields. In the
// future there's no technical or product reason we couldn't let you
// override (some properites of) other fields like "Title", but they
// don't usually support appropriate "setX()" methods today.
if (!($field instanceof PhabricatorStandardCustomField)) {
// For fields that are proxies on top of StandardCustomField, which
// is how most application custom fields work today, we can reconfigure
// the proxied field instead.
$field = $field->getProxy();
if (!$field || !($field instanceof PhabricatorStandardCustomField)) {
continue;
}
}
$subtype_config = $subtype_object->getSubtypeFieldConfiguration(
$field_key);
if (!$subtype_config) {
continue;
}
if (isset($subtype_config['disabled'])) {
$field->setIsEnabled(!$subtype_config['disabled']);
}
if (isset($subtype_config['name'])) {
$field->setFieldName($subtype_config['name']);
}
}
return $fields;
}
}

View file

@ -18,6 +18,7 @@ abstract class PhabricatorStandardCustomField
private $isCopyable;
private $hasStorageValue;
private $isBuiltin;
private $isEnabled = true;
abstract public function getFieldType();
@ -175,6 +176,19 @@ abstract class PhabricatorStandardCustomField
return $this->rawKey;
}
public function setIsEnabled($is_enabled) {
$this->isEnabled = $is_enabled;
return $this;
}
public function getIsEnabled() {
return $this->isEnabled;
}
public function isFieldEnabled() {
return $this->getIsEnabled();
}
/* -( PhabricatorCustomField )--------------------------------------------- */

View file

@ -12,37 +12,22 @@ final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask {
$config = array(
self::CONFIG_IDS => self::IDS_COUNTER,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_KEY_SCHEMA => array(
'dataID' => array(
'columns' => array('dataID'),
'unique' => true,
),
'taskClass' => array(
'columns' => array('taskClass'),
),
'leaseExpires' => array(
'columns' => array('leaseExpires'),
),
'leaseOwner' => array(
'columns' => array('leaseOwner(16)'),
),
'key_failuretime' => array(
'columns' => array('failureTime'),
),
'leaseOwner_2' => array(
'key_owner' => array(
'columns' => array('leaseOwner', 'priority', 'id'),
),
) + $parent[self::CONFIG_KEY_SCHEMA],
);
$config[self::CONFIG_COLUMN_SCHEMA] = array(
// T6203/NULLABILITY
// This isn't nullable in the archive table, so at a minimum these
// should be the same.
'dataID' => 'uint32?',
) + $parent[self::CONFIG_COLUMN_SCHEMA];
return $config + $parent;
}
@ -74,7 +59,7 @@ final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask {
$this->failureCount = 0;
}
if ($is_new && ($this->getData() !== null)) {
if ($is_new) {
$data = new PhabricatorWorkerTaskData();
$data->setData($this->getData());
$data->save();
@ -132,7 +117,9 @@ final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask {
->setPriority($this->getPriority())
->setObjectPHID($this->getObjectPHID())
->setResult($result)
->setDuration($duration);
->setDuration($duration)
->setDateCreated($this->getDateCreated())
->setArchivedEpoch(PhabricatorTime::getNow());
// NOTE: This deletes the active task (this object)!
$archive->save();

View file

@ -8,6 +8,7 @@ final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask {
protected $duration;
protected $result;
protected $archivedEpoch;
protected function getConfiguration() {
$parent = parent::getConfiguration();
@ -22,15 +23,13 @@ final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask {
$config[self::CONFIG_COLUMN_SCHEMA] = array(
'result' => 'uint32',
'duration' => 'uint64',
'archivedEpoch' => 'epoch?',
) + $config[self::CONFIG_COLUMN_SCHEMA];
$config[self::CONFIG_KEY_SCHEMA] = array(
'dateCreated' => array(
'columns' => array('dateCreated'),
),
'leaseOwner' => array(
'columns' => array('leaseOwner', 'priority', 'id'),
),
'key_modified' => array(
'columns' => array('dateModified'),
),
@ -88,6 +87,7 @@ final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask {
->setDataID($this->getDataID())
->setPriority($this->getPriority())
->setObjectPHID($this->getObjectPHID())
->setDateCreated($this->getDateCreated())
->insert();
$this->setDataID(null);

View file

@ -0,0 +1,156 @@
<?php
final class PhabricatorDiffScopeEngine
extends Phobject {
private $lineTextMap;
private $lineDepthMap;
public function setLineTextMap(array $map) {
if (array_key_exists(0, $map)) {
throw new Exception(
pht('ScopeEngine text map must be a 1-based map of lines.'));
}
$expect = 1;
foreach ($map as $key => $value) {
if ($key === $expect) {
$expect++;
continue;
}
throw new Exception(
pht(
'ScopeEngine text map must be a contiguous map of '.
'lines, but is not: found key "%s" where key "%s" was expected.',
$key,
$expect));
}
$this->lineTextMap = $map;
return $this;
}
public function getLineTextMap() {
if ($this->lineTextMap === null) {
throw new PhutilInvalidStateException('setLineTextMap');
}
return $this->lineTextMap;
}
public function getScopeStart($line) {
$text_map = $this->getLineTextMap();
$depth_map = $this->getLineDepthMap();
$length = count($text_map);
// Figure out the effective depth of the line we're getting scope for.
// If the line is just whitespace, it may have no depth on its own. In
// this case, we look for the next line.
$line_depth = null;
for ($ii = $line; $ii <= $length; $ii++) {
if ($depth_map[$ii] !== null) {
$line_depth = $depth_map[$ii];
break;
}
}
// If we can't find a line depth for the target line, just bail.
if ($line_depth === null) {
return null;
}
// Limit the maximum number of lines we'll examine. If a user has a
// million-line diff of nonsense, scanning the whole thing is a waste
// of time.
$search_range = 1000;
$search_until = max(0, $ii - $search_range);
for ($ii = $line - 1; $ii > $search_until; $ii--) {
$line_text = $text_map[$ii];
// This line is in missing context: the diff was diffed with partial
// context, and we ran out of context before finding a good scope line.
// Bail out, we don't want to jump across missing context blocks.
if ($line_text === null) {
return null;
}
$depth = $depth_map[$ii];
// This line is all whitespace. This isn't a possible match.
if ($depth === null) {
continue;
}
// The depth is the same as (or greater than) the depth we started with,
// so this isn't a possible match.
if ($depth >= $line_depth) {
continue;
}
// Reject lines which begin with "}" or "{". These lines are probably
// never good matches.
if (preg_match('/^\s*[{}]/i', $line_text)) {
continue;
}
return $ii;
}
return null;
}
private function getLineDepthMap() {
if (!$this->lineDepthMap) {
$this->lineDepthMap = $this->newLineDepthMap();
}
return $this->lineDepthMap;
}
private function newLineDepthMap() {
$text_map = $this->getLineTextMap();
// TODO: This should be configurable once we handle tab widths better.
$tab_width = 2;
$depth_map = array();
foreach ($text_map as $line_number => $line_text) {
if ($line_text === null) {
$depth_map[$line_number] = null;
continue;
}
$len = strlen($line_text);
// If the line has no actual text, don't assign it a depth.
if (!$len || !strlen(trim($line_text))) {
$depth_map[$line_number] = null;
continue;
}
$count = 0;
for ($ii = 0; $ii < $len; $ii++) {
$c = $line_text[$ii];
if ($c == ' ') {
$count++;
} else if ($c == "\t") {
$count += $tab_width;
} else {
break;
}
}
// Round down to cheat our way through the " *" parts of docblock
// comments. This is generally a reasonble heuristic because odd tab
// widths are exceptionally rare.
$depth = ($count >> 1);
$depth_map[$line_number] = $depth;
}
return $depth_map;
}
}

View file

@ -10,27 +10,14 @@
final class PhabricatorDifferenceEngine extends Phobject {
private $ignoreWhitespace;
private $oldName;
private $newName;
private $normalize;
/* -( Configuring the Engine )--------------------------------------------- */
/**
* If true, ignore whitespace when computing differences.
*
* @param bool Ignore whitespace?
* @return this
* @task config
*/
public function setIgnoreWhitespace($ignore_whitespace) {
$this->ignoreWhitespace = $ignore_whitespace;
return $this;
}
/**
* Set the name to identify the old file with. Primarily cosmetic.
*
@ -57,6 +44,16 @@ final class PhabricatorDifferenceEngine extends Phobject {
}
public function setNormalize($normalize) {
$this->normalize = $normalize;
return $this;
}
public function getNormalize() {
return $this->normalize;
}
/* -( Generating Diffs )--------------------------------------------------- */
@ -73,9 +70,6 @@ final class PhabricatorDifferenceEngine extends Phobject {
public function generateRawDiffFromFileContent($old, $new) {
$options = array();
if ($this->ignoreWhitespace) {
$options[] = '-bw';
}
// Generate diffs with full context.
$options[] = '-U65535';
@ -88,6 +82,12 @@ final class PhabricatorDifferenceEngine extends Phobject {
$options[] = '-L';
$options[] = $new_name;
$normalize = $this->getNormalize();
if ($normalize) {
$old = $this->normalizeFile($old);
$new = $this->normalizeFile($new);
}
$old_tmp = new TempFile();
$new_tmp = new TempFile();
@ -100,12 +100,10 @@ final class PhabricatorDifferenceEngine extends Phobject {
$new_tmp);
if (!$err) {
// This indicates that the two files are the same (or, possibly, the
// same modulo whitespace differences, which is why we can't do this
// check trivially before running `diff`). Build a synthetic, changeless
// diff so that we can still render the raw, unchanged file instead of
// being forced to just say "this file didn't change" since we don't have
// the content.
// This indicates that the two files are the same. Build a synthetic,
// changeless diff so that we can still render the raw, unchanged file
// instead of being forced to just say "this file didn't change" since we
// don't have the content.
$entire_file = explode("\n", $old);
foreach ($entire_file as $k => $line) {
@ -123,26 +121,6 @@ final class PhabricatorDifferenceEngine extends Phobject {
"+++ {$new_name}\n".
"@@ -1,{$len} +1,{$len} @@\n".
$entire_file."\n";
} else {
if ($this->ignoreWhitespace) {
// Under "-bw", `diff` is inconsistent about emitting "\ No newline
// at end of file". For instance, a long file with a change in the
// middle will emit a contextless "\ No newline..." at the end if a
// newline is removed, but not if one is added. A file with a change
// at the end will emit the "old" "\ No newline..." block only, even
// if the newline was not removed. Since we're ostensibly ignoring
// whitespace changes, just drop these lines if they appear anywhere
// in the diff.
$lines = explode("\n", $diff);
foreach ($lines as $key => $line) {
if (isset($line[0]) && $line[0] == '\\') {
unset($lines[$key]);
}
}
$diff = implode("\n", $lines);
}
}
return $diff;
@ -168,4 +146,27 @@ final class PhabricatorDifferenceEngine extends Phobject {
return head($diff->getChangesets());
}
private function normalizeFile($corpus) {
// We can freely apply any other transformations we want to here: we have
// no constraints on what we need to preserve. If we normalize every line
// to "cat", the diff will still work, the alignment of the "-" / "+"
// lines will just be very hard to read.
// In general, we'll make the diff better if we normalize two lines that
// humans think are the same.
// We'll make the diff worse if we normalize two lines that humans think
// are different.
// Strip all whitespace present anywhere in the diff, since humans never
// consider whitespace changes to alter the line into a "different line"
// even when they're semantic (e.g., in a string constant). This covers
// indentation changes, trailing whitepspace, and formatting changes
// like "+/-".
$corpus = preg_replace('/[ \t]/', '', $corpus);
return $corpus;
}
}

View file

@ -0,0 +1,51 @@
<?php
final class PhabricatorDiffScopeEngineTestCase
extends PhabricatorTestCase {
private $engines = array();
public function testScopeEngine() {
$this->assertScopeStart('zebra.c', 4, 2);
}
private function assertScopeStart($file, $line, $expect) {
$engine = $this->getScopeTestEngine($file);
$actual = $engine->getScopeStart($line);
$this->assertEqual(
$expect,
$actual,
pht(
'Expect scope for line %s to start on line %s (actual: %s) in "%s".',
$line,
$expect,
$actual,
$file));
}
private function getScopeTestEngine($file) {
if (!isset($this->engines[$file])) {
$this->engines[$file] = $this->newScopeTestEngine($file);
}
return $this->engines[$file];
}
private function newScopeTestEngine($file) {
$path = dirname(__FILE__).'/data/'.$file;
$data = Filesystem::readFile($path);
$lines = phutil_split_lines($data);
$map = array();
foreach ($lines as $key => $line) {
$map[$key + 1] = $line;
}
$engine = id(new PhabricatorDiffScopeEngine())
->setLineTextMap($map);
return $engine;
}
}

View file

@ -0,0 +1,5 @@
void
ZebraTamer::TameAZebra(nsPoint where, const nsRect& zone, nsAtom* material)
{
zebra.tame = true;
}

View file

@ -18,7 +18,6 @@ final class PHUIDiffOneUpInlineCommentRowScaffold
$attrs = array(
'colspan' => 3,
'class' => 'right3',
'id' => $inline->getScaffoldCellID(),
);
@ -31,8 +30,8 @@ final class PHUIDiffOneUpInlineCommentRowScaffold
}
$cells = array(
phutil_tag('th', array(), $left_hidden),
phutil_tag('th', array(), $right_hidden),
phutil_tag('td', array('class' => 'n'), $left_hidden),
phutil_tag('td', array('class' => 'n'), $right_hidden),
phutil_tag('td', $attrs, $inline),
);

View file

@ -65,15 +65,15 @@ final class PHUIDiffTwoUpInlineCommentRowScaffold
);
$right_attrs = array(
'colspan' => 3,
'class' => 'right3',
'colspan' => 2,
'id' => ($right_side ? $right_side->getScaffoldCellID() : null),
);
$cells = array(
phutil_tag('th', array(), $left_hidden),
phutil_tag('td', array('class' => 'n'), $left_hidden),
phutil_tag('td', $left_attrs, $left_side),
phutil_tag('th', array(), $right_hidden),
phutil_tag('td', array('class' => 'n'), $right_hidden),
phutil_tag('td', array('class' => 'copy')),
phutil_tag('td', $right_attrs, $right_side),
);

View file

@ -108,6 +108,10 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
'icons' => mpull($tokens, 'getIcon', 'getKey'),
'types' => mpull($tokens, 'getTokenType', 'getKey'),
'colors' => mpull($tokens, 'getColor', 'getKey'),
'availabilityColors' => mpull(
$tokens,
'getAvailabilityColor',
'getKey'),
'limit' => $this->limit,
'username' => $username,
'placeholder' => $placeholder,

View file

@ -4,12 +4,12 @@
"freeze": true,
"immed": true,
"indent": 2,
"latedef": true,
"latedef": "nofunc",
"newcap": true,
"noarg": true,
"quotmark": "single",
"undef": true,
"unused": true,
"unused": "vars",
"expr": true,
"loopfunc": true,

View file

@ -67,28 +67,6 @@
padding: 1px 4px;
}
.differential-diff td .zwsp {
position: absolute;
width: 0;
}
.differential-diff th {
text-align: right;
padding: 1px 6px 1px 0;
vertical-align: top;
background: {$lightbluebackground};
color: {$bluetext};
cursor: pointer;
border-right: 1px solid {$thinblueborder};
overflow: hidden;
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.prose-diff {
padding: 12px 0;
white-space: pre-wrap;
@ -109,7 +87,7 @@
color: {$darkgreytext};
}
.differential-changeset-immutable .differential-diff th {
.differential-changeset-immutable .differential-diff td {
cursor: auto;
}
@ -135,16 +113,38 @@
background: {$old-bright};
}
.differential-diff td.new span.bright,
.differential-diff td.new-full,
.prose-diff span.new {
background: {$new-bright};
}
.differential-diff td span.depth-out,
.differential-diff td span.depth-in {
padding: 2px 0;
background-size: 12px 12px;
background-repeat: no-repeat;
background-position: left center;
}
.differential-diff td span.depth-out {
background-image: url(/rsrc/image/chevron-out.png);
background-color: {$old-bright};
}
.differential-diff td span.depth-in {
background-position: 1px center;
background-image: url(/rsrc/image/chevron-in.png);
background-color: {$new-bright};
}
.differential-diff td.copy {
min-width: 0.5%;
width: 0.5%;
padding: 0;
background: {$lightbluebackground};
}
.differential-diff td.new-copy,
@ -161,6 +161,36 @@
background: #dddddd;
}
.differential-diff .inline > td {
padding: 0;
}
/* Specify line number behaviors after other behaviors because line numbers
should always have a boring grey background. */
.differential-diff td.n {
text-align: right;
padding: 1px 6px 1px 0;
vertical-align: top;
background: {$lightbluebackground};
color: {$bluetext};
cursor: pointer;
border-right: 1px solid {$thinblueborder};
overflow: hidden;
}
.differential-diff td + td.n {
border-left: 1px solid {$thinblueborder};
}
.differential-diff td.n::before {
content: attr(data-n);
}
.differential-diff td.show-context-line.n {
cursor: auto;
}
.differential-diff td.cov {
padding: 0;
}
@ -201,7 +231,7 @@ td.cov-I {
}
.differential-diff td.show-more,
.differential-diff th.show-context-line,
.differential-diff td.show-context-line,
.differential-diff td.show-context,
.differential-diff td.differential-shield {
background: {$lightbluebackground};
@ -211,7 +241,7 @@ td.cov-I {
}
.device .differential-diff td.show-more,
.device .differential-diff th.show-context-line,
.device .differential-diff td.show-context-line,
.device .differential-diff td.show-context,
.device .differential-diff td.differential-shield {
padding: 6px 0;
@ -229,10 +259,14 @@ td.cov-I {
color: {$bluetext};
}
.differential-diff th.show-context-line {
.differential-diff td.show-context-line {
padding-right: 6px;
}
.differential-diff td.show-context-line.left-context {
border-right: none;
}
.differential-diff td.show-context {
padding-left: 14px;
}
@ -295,10 +329,6 @@ td.cov-I {
pointer-events: none;
}
.differential-diff .inline > td {
padding: 0;
}
.differential-loading {
border-top: 1px solid {$gentle.highlight.border};
border-bottom: 1px solid {$gentle.highlight.border};
@ -407,3 +437,49 @@ tr.differential-inline-loading {
.diff-banner-buttons {
float: right;
}
/* In Firefox, making the table unselectable and then making cells selectable
does not work: the cells remain unselectable. Narrowly mark the cells as
unselectable. */
.differential-diff.copy-l > tbody > tr > td,
.differential-diff.copy-r > tbody > tr > td {
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
opacity: 0.5;
}
.differential-diff.copy-l > tbody > tr > td:nth-child(2) {
-moz-user-select: auto;
-ms-user-select: auto;
-webkit-user-select: auto;
user-select: auto;
opacity: 1;
}
.differential-diff.copy-l > tbody > tr > td.show-more:nth-child(2) {
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
opacity: 0.5;
}
.differential-diff.copy-r > tbody > tr > td:nth-child(5) {
-moz-user-select: auto;
-ms-user-select: auto;
-webkit-user-select: auto;
user-select: auto;
opacity: 1;
}
.differential-diff.copy-l > tbody > tr.inline > td,
.differential-diff.copy-r > tbody > tr.inline > td {
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
opacity: 0.5;
}

View file

@ -16,14 +16,6 @@
margin-bottom: 8px;
}
.differential-unselectable tr td:nth-of-type(1) {
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.differential-content-hidden {
margin: 0 0 24px 0;
}

View file

@ -29,3 +29,9 @@ span.crossreference-item {
color: #222222;
background: #dddddd;
}
.suspicious-character {
background: #ff7700;
color: #ffffff;
cursor: default;
}

View file

@ -249,6 +249,16 @@ body .phui-header-shell.phui-bleed-header
color: {$sh-indigotext};
}
.policy-header-callout.policy-adjusted-locked {
background: {$sh-pinkbackground};
}
.policy-header-callout.policy-adjusted-locked .policy-link,
.policy-header-callout.policy-adjusted-locked .phui-icon-view {
color: {$sh-pinktext};
}
.policy-header-callout .policy-space-container {
font-weight: bold;
color: {$sh-redtext};

View file

@ -54,6 +54,14 @@ a.phui-tag-view:hover {
border: 1px solid transparent;
}
.tokenizer-result .phui-tag-dot {
margin-right: 6px;
}
.jx-tokenizer-token .phui-tag-dot {
margin-left: 2px;
}
.phui-tag-type-state {
color: #ffffff;
text-shadow: rgba(100, 100, 100, 0.40) 0px -1px 1px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -22,7 +22,6 @@ JX.install('DiffChangeset', {
this._renderURI = data.renderURI;
this._ref = data.ref;
this._whitespace = data.whitespace;
this._renderer = data.renderer;
this._highlight = data.highlight;
this._encoding = data.encoding;
@ -46,7 +45,6 @@ JX.install('DiffChangeset', {
_renderURI: null,
_ref: null,
_whitespace: null,
_renderer: null,
_highlight: null,
_encoding: null,
@ -310,7 +308,6 @@ JX.install('DiffChangeset', {
_getViewParameters: function() {
return {
ref: this._ref,
whitespace: this._whitespace || '',
renderer: this.getRenderer() || '',
highlight: this._highlight || '',
encoding: this._encoding || ''

View file

@ -70,13 +70,13 @@ JX.install('DiffChangesetList', {
var onrangedown = JX.bind(this, this._ifawake, this._onrangedown);
JX.Stratcom.listen(
'mousedown',
['differential-changeset', 'tag:th'],
['differential-changeset', 'tag:td'],
onrangedown);
var onrangemove = JX.bind(this, this._ifawake, this._onrangemove);
JX.Stratcom.listen(
['mouseover', 'mouseout'],
['differential-changeset', 'tag:th'],
['differential-changeset', 'tag:td'],
onrangemove);
var onrangeup = JX.bind(this, this._ifawake, this._onrangeup);
@ -360,7 +360,7 @@ JX.install('DiffChangesetList', {
while (row) {
var header = row.firstChild;
while (header) {
if (JX.DOM.isType(header, 'th')) {
if (this.getLineNumberFromHeader(header)) {
if (header.className.indexOf('old') !== -1) {
old_list.push(header);
} else if (header.className.indexOf('new') !== -1) {
@ -1246,12 +1246,24 @@ JX.install('DiffChangesetList', {
return changeset.getInlineForRow(inline_row);
},
getLineNumberFromHeader: function(th) {
try {
return parseInt(th.id.match(/^C\d+[ON]L(\d+)$/)[1], 10);
} catch (x) {
getLineNumberFromHeader: function(node) {
var n = parseInt(node.getAttribute('data-n'));
if (!n) {
return null;
}
// If this is a line number that's part of a row showing more context,
// we don't want to let users leave inlines here.
try {
JX.DOM.findAbove(node, 'tr', 'context-target');
return null;
} catch (ex) {
// Ignore.
}
return n;
},
getDisplaySideFromHeader: function(th) {
@ -1299,7 +1311,7 @@ JX.install('DiffChangesetList', {
},
_updateRange: function(target, is_out) {
// Don't update the range if this "<th />" doesn't correspond to a line
// Don't update the range if this target doesn't correspond to a line
// number. For instance, this may be a dead line number, like the empty
// line numbers on the left hand side of a newly added file.
var number = this.getLineNumberFromHeader(target);

View file

@ -1,33 +0,0 @@
/**
* @provides javelin-behavior-differential-user-select
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
*/
JX.behavior('differential-user-select', function() {
var unselectable;
function isOnRight(node) {
return node.previousSibling &&
node.parentNode.firstChild != node.previousSibling;
}
JX.Stratcom.listen(
'mousedown',
null,
function(e) {
var key = 'differential-unselectable';
if (unselectable) {
JX.DOM.alterClass(unselectable, key, false);
}
var diff = e.getNode('differential-diff');
var td = e.getNode('tag:td');
if (diff && td && isOnRight(td)) {
unselectable = diff;
JX.DOM.alterClass(diff, key, true);
}
});
});

View file

@ -237,11 +237,6 @@ JX.behavior('repository-crossreference', function(config, statics) {
}
var content = '' + node.textContent;
// Strip off any ZWS characters. These are marker characters used to
// improve copy/paste behavior.
content = content.replace(/\u200B/g, '');
char += content.length;
}

View file

@ -125,15 +125,18 @@ JX.install('Prefab', {
var icon;
var type;
var color;
var availability_color;
if (result) {
icon = result.icon;
value = result.displayName;
type = result.tokenType;
color = result.color;
availability_color = result.availabilityColor;
} else {
icon = (config.icons || {})[key];
type = (config.types || {})[key];
color = (config.colors || {})[key];
availability_color = (config.availabilityColors || {})[key];
}
if (icon) {
@ -147,7 +150,16 @@ JX.install('Prefab', {
JX.DOM.alterClass(container, color, true);
}
return [icon, value];
var dot;
if (availability_color) {
dot = JX.$N(
'span',
{
className: 'phui-tag-dot phui-tag-color-' + availability_color
});
}
return [icon, dot, value];
});
if (config.placeholder) {
@ -275,10 +287,20 @@ JX.install('Prefab', {
icon_ui = JX.Prefab._renderIcon(icon);
}
var availability_ui;
var availability_color = fields[16];
if (availability_color) {
availability_ui = JX.$N(
'span',
{
className: 'phui-tag-dot phui-tag-color-' + availability_color
});
}
var display = JX.$N(
'div',
{className: 'tokenizer-result'},
[icon_ui, fields[4] || fields[0], closed_ui]);
[icon_ui, availability_ui, fields[4] || fields[0], closed_ui]);
if (closed) {
JX.DOM.alterClass(display, 'tokenizer-result-closed', true);
}
@ -300,7 +322,8 @@ JX.install('Prefab', {
tokenType: fields[12],
unique: fields[13] || false,
autocomplete: fields[14],
sort: JX.TypeaheadNormalizer.normalize(fields[0])
sort: JX.TypeaheadNormalizer.normalize(fields[0]),
availabilityColor: availability_color
};
},

View file

@ -4,62 +4,319 @@
* javelin-dom
*/
/**
* Tools like Paste and Differential don't normally respond to the clipboard
* 'copy' operation well, because when a user copies text they'll get line
* numbers and other metadata.
*
* To improve this behavior, applications can embed markers that delimit
* metadata (left of the marker) from content (right of the marker). When
* we get a copy event, we strip out all the metadata and just copy the
* actual text.
*/
JX.behavior('phabricator-oncopy', function() {
var copy_root;
var copy_mode;
var zws = '\u200B'; // Unicode Zero-Width Space
function onstartselect(e) {
var target = e.getTarget();
JX.enableDispatch(document.body, 'copy');
JX.Stratcom.listen(
['copy'],
null,
function(e) {
var container;
try {
// NOTE: For now, all elements with custom oncopy behavior are tables,
// so this tag selection will hit everything we need it to.
container = JX.DOM.findAbove(target, 'table', 'intercept-copy');
} catch (ex) {
container = null;
}
var selection;
var text;
if (window.getSelection) {
selection = window.getSelection();
text = selection.toString();
var old_mode = copy_mode;
clear_selection_mode();
if (!container) {
return;
}
// If the potential selection is starting inside an inline comment,
// don't do anything special.
try {
if (JX.DOM.findAbove(target, 'div', 'differential-inline-comment')) {
return;
}
} catch (ex) {
// Continue.
}
// Find the row and cell we're copying from. If we don't find anything,
// don't do anything special.
var row;
var cell;
try {
// The target may be the cell we're after, particularly if you click
// in the white area to the right of the text, towards the end of a line.
if (JX.DOM.isType(target, 'td')) {
cell = target;
} else {
selection = document.selection;
text = selection.createRange().text;
cell = JX.DOM.findAbove(target, 'td');
}
row = JX.DOM.findAbove(target, 'tr');
} catch (ex) {
return;
}
// If the row doesn't have enough nodes, bail out. Note that it's okay
// to begin a selection in the whitespace on the opposite side of an inline
// comment. For example, if there's an inline comment on the right side of
// a diff, it's okay to start selecting the left side of the diff by
// clicking the corresponding empty space on the left side.
if (row.childNodes.length < 4) {
return;
}
// If the selection's cell is in the "old" diff or the "new" diff, we'll
// activate an appropriate copy mode.
var mode;
if (cell === row.childNodes[1]) {
mode = 'copy-l';
} else if ((row.childNodes.length >= 4) && (cell === row.childNodes[4])) {
mode = 'copy-r';
} else {
return;
}
// We found a copy mode, so set it as the current active mode.
copy_root = container;
copy_mode = mode;
// If the user makes a selection, then clicks again inside the same
// selection, browsers retain the selection. This is because the user may
// want to drag-and-drop the text to another window.
// Handle special cases when the click is inside an existing selection.
var ranges = get_selected_ranges();
if (ranges.length) {
// We'll have an existing selection if the user selects text on the right
// side of a diff, then clicks the selection on the left side of the
// diff, even if the second click is clicking part of the selection
// range where the selection highlight is currently invisible because
// of CSS rules.
// This behavior looks and feels glitchy: an invisible selection range
// suddenly pops into existence and there's a bunch of flicker. If we're
// switching selection modes, clear the old selection to avoid this:
// assume the user is not trying to drag-and-drop text which is not
// visually selected.
if (old_mode !== copy_mode) {
window.getSelection().removeAllRanges();
}
if (text.indexOf(zws) == -1) {
// If there's no marker in the text, just let it copy normally.
// In the more mundane case, if the user selects some text on one side
// of a diff and then clicks that same selection in a normal way (in
// the visible part of the highlighted text), we may either be altering
// the selection range or may be initiating a text drag depending on how
// long they hold the button for. Regardless of what we're doing, we're
// still in a selection mode, so keep the visual hints active.
JX.DOM.alterClass(copy_root, copy_mode, true);
}
// We've chosen a mode and saved it now, but we don't actually update to
// apply any visual changes until the user actually starts making some
// kind of selection.
}
// When the selection range changes, apply CSS classes if the selection is
// nonempty. We don't want to make visual changes to the document immediately
// when the user press the mouse button, since we aren't yet sure that
// they are starting a selection: instead, wait for them to actually select
// something.
function onchangeselect() {
if (!copy_mode) {
return;
}
var ranges = get_selected_ranges();
JX.DOM.alterClass(copy_root, copy_mode, !!ranges.length);
}
// When the user releases the mouse, get rid of the selection mode if we
// don't have a selection.
function onendselect(e) {
if (!copy_mode) {
return;
}
var ranges = get_selected_ranges();
if (!ranges.length) {
clear_selection_mode();
}
}
function get_selected_ranges() {
var ranges = [];
if (!window.getSelection) {
return ranges;
}
var selection = window.getSelection();
for (var ii = 0; ii < selection.rangeCount; ii++) {
var range = selection.getRangeAt(ii);
if (range.collapsed) {
continue;
}
ranges.push(range);
}
return ranges;
}
function clear_selection_mode() {
if (!copy_root) {
return;
}
JX.DOM.alterClass(copy_root, copy_mode, false);
copy_root = null;
copy_mode = null;
}
function oncopy(e) {
// If we aren't in a special copy mode, just fall back to default
// behavior.
if (!copy_mode) {
return;
}
var ranges = get_selected_ranges();
if (!ranges.length) {
return;
}
var text = [];
for (var ii = 0; ii < ranges.length; ii++) {
var range = ranges[ii];
var fragment = range.cloneContents();
if (!fragment.childNodes.length) {
continue;
}
// In Chrome and Firefox, because we've already applied "user-select"
// CSS to everything we don't intend to copy, the text in the selection
// range is correct, and the range will include only the correct text
// nodes.
// However, in Safari, "user-select" does not apply to clipboard
// operations, so we get everything in the document between the beginning
// and end of the selection, even if it isn't visibly selected.
// Even in Chrome and Firefox, we can get partial empty nodes: for
// example, where a "<tr>" is selectable but no content in the node is
// selectable. (We have to leave the "<tr>" itself selectable because
// of how Firefox applies "user-select" rules.)
// The nodes we get here can also start and end more or less anywhere.
// One saving grace is that we use "content: attr(data-n);" to render
// the line numbers and no browsers copy this content, so we don't have
// to worry about figuring out when text is line numbers.
for (var jj = 0; jj < fragment.childNodes.length; jj++) {
var node = fragment.childNodes[jj];
text.push(extract_text(node));
}
}
text = flatten_list(text);
text = text.join('');
var rawEvent = e.getRawEvent();
var data;
if ('clipboardData' in rawEvent) {
data = rawEvent.clipboardData;
} else {
data = window.clipboardData;
}
data.setData('Text', text);
e.prevent();
}
function extract_text(node) {
var ii;
var text = [];
if (JX.DOM.isType(node, 'tr')) {
// This is an inline comment row, so we never want to copy any
// content inside of it.
if (JX.Stratcom.hasSigil(node, 'inline-row')) {
return null;
}
// This is a "Show More Context" row, so we never want to copy any
// of the content inside.
if (JX.Stratcom.hasSigil(node, 'context-target')) {
return null;
}
// Assume anything else is a source code row. Keep only "<td>" cells
// with the correct mode.
for (ii = 0; ii < node.childNodes.length; ii++) {
text.push(extract_text(node.childNodes[ii]));
}
return text;
}
if (JX.DOM.isType(node, 'td')) {
var node_mode = node.getAttribute('data-copy-mode');
if (node_mode !== copy_mode) {
return;
}
var result = [];
// Otherwise, fall through and extract this node's text normally.
}
// Strip everything before the marker (and the marker itself) out of the
// text. If a line doesn't have the marker, throw it away (the assumption
// is that it's a line number or part of some other meta-text).
var lines = text.split('\n');
var pos;
for (var ii = 0; ii < lines.length; ii++) {
pos = lines[ii].indexOf(zws);
if (pos == -1 && ii !== 0) {
continue;
}
result.push(lines[ii].substring(pos + 1));
if (node.getAttribute) {
var copy_text = node.getAttribute('data-copy-text');
if (copy_text) {
return copy_text;
}
result = result.join('\n');
}
var rawEvent = e.getRawEvent();
var clipboardData = 'clipboardData' in rawEvent ?
rawEvent.clipboardData :
window.clipboardData;
clipboardData.setData('Text', result);
e.prevent();
});
if (!node.childNodes || !node.childNodes.length) {
return node.textContent;
}
for (ii = 0; ii < node.childNodes.length; ii++) {
var child = node.childNodes[ii];
text.push(extract_text(child));
}
return text;
}
function flatten_list(list) {
var stack = [list];
var result = [];
while (stack.length) {
var next = stack.pop();
if (JX.isArray(next)) {
for (var ii = 0; ii < next.length; ii++) {
stack.push(next[ii]);
}
} else if (next === null) {
continue;
} else if (next === undefined) {
continue;
} else {
result.push(next);
}
}
return result.reverse();
}
JX.enableDispatch(document.body, 'copy');
JX.enableDispatch(window, 'selectionchange');
JX.Stratcom.listen('mousedown', null, onstartselect);
JX.Stratcom.listen('selectionchange', null, onchangeselect);
JX.Stratcom.listen('mouseup', null, onendselect);
JX.Stratcom.listen('copy', null, oncopy);
});

View file

@ -185,7 +185,16 @@ JX.install('PHUIXAutocomplete', {
.getNode();
}
var display = JX.$N('span', {}, [icon, map.displayName]);
var dot;
if (map.availabilityColor) {
dot = JX.$N(
'span',
{
className: 'phui-tag-dot phui-tag-color-' + map.availabilityColor
});
}
var display = JX.$N('span', {}, [icon, dot, map.displayName]);
JX.DOM.alterClass(display, 'tokenizer-result-closed', !!map.closed);
map.display = display;