diff --git a/bin/ssh-auth-key b/bin/ssh-auth-key deleted file mode 120000 index 7dff83c316..0000000000 --- a/bin/ssh-auth-key +++ /dev/null @@ -1 +0,0 @@ -../scripts/ssh/ssh-auth-key.php \ No newline at end of file diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 8585a22e85..8fba8d78a3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,10 +9,10 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '9d167919', - 'core.pkg.js' => '6e5c894f', + 'core.pkg.css' => '2d63dec5', + 'core.pkg.js' => '705aec2c', 'differential.pkg.css' => '607c84be', - 'differential.pkg.js' => 'a0212a0b', + 'differential.pkg.js' => '1b97518d', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => 'a98c0bf7', 'maniphest.pkg.css' => '35995d6d', @@ -30,13 +30,13 @@ return array( 'rsrc/css/aphront/notification.css' => '30240bd2', 'rsrc/css/aphront/panel-view.css' => '46923d46', 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf', - 'rsrc/css/aphront/table-view.css' => '5f13a9e4', + 'rsrc/css/aphront/table-view.css' => '0bb61df1', 'rsrc/css/aphront/tokenizer.css' => 'b52d0668', 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', 'rsrc/css/aphront/typeahead.css' => '8779483d', 'rsrc/css/application/almanac/almanac.css' => '2e050f4f', - 'rsrc/css/application/auth/auth.css' => 'add92fd8', + 'rsrc/css/application/auth/auth.css' => 'c2f23d74', 'rsrc/css/application/base/main-menu-view.css' => 'a663a13c', 'rsrc/css/application/base/notification-menu.css' => '4df1ee30', 'rsrc/css/application/base/phui-theme.css' => '63311e09', @@ -155,7 +155,7 @@ return array( 'rsrc/css/phui/phui-form-view.css' => 'a8e0a1ab', 'rsrc/css/phui/phui-form.css' => '159e2d9c', 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', - 'rsrc/css/phui/phui-header-view.css' => 'b500eeea', + 'rsrc/css/phui/phui-header-view.css' => 'be09cc83', 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', 'rsrc/css/phui/phui-icon.css' => '4cbc684a', @@ -430,7 +430,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' => 'c15122b4', + 'rsrc/js/application/repository/repository-crossreference.js' => '1c95ea63', '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', @@ -450,7 +450,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', 'rsrc/js/core/Busy.js' => '5202e831', 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', - 'rsrc/js/core/DraggableList.js' => 'c9ad6f70', + 'rsrc/js/core/DraggableList.js' => '0169e425', 'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/Hovercard.js' => '074f0783', @@ -537,12 +537,12 @@ return array( 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', - 'aphront-table-view-css' => '5f13a9e4', + 'aphront-table-view-css' => '0bb61df1', 'aphront-tokenizer-control-css' => 'b52d0668', 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', 'application-search-view-css' => '0f7c06d8', - 'auth-css' => 'add92fd8', + 'auth-css' => 'c2f23d74', 'bulk-job-css' => '73af99f5', 'conduit-api-css' => 'ce2cfc41', 'config-options-css' => '16c920ae', @@ -684,7 +684,7 @@ return array( 'javelin-behavior-reorder-applications' => 'aa371860', 'javelin-behavior-reorder-columns' => '8ac32fd9', 'javelin-behavior-reorder-profile-menu-items' => 'e5bdb730', - 'javelin-behavior-repository-crossreference' => 'c15122b4', + 'javelin-behavior-repository-crossreference' => '1c95ea63', 'javelin-behavior-scrollbar' => '92388bae', 'javelin-behavior-search-reorder-queries' => 'b86f297f', 'javelin-behavior-select-content' => 'e8240b50', @@ -779,7 +779,7 @@ return array( 'phabricator-diff-changeset-list' => '0f5c016d', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', - 'phabricator-draggable-list' => 'c9ad6f70', + 'phabricator-draggable-list' => '0169e425', 'phabricator-fatal-config-template-css' => '20babf50', 'phabricator-favicon' => '7930776a', 'phabricator-feed-css' => 'd8b6e3f8', @@ -846,7 +846,7 @@ return array( 'phui-form-css' => '159e2d9c', 'phui-form-view-css' => 'a8e0a1ab', 'phui-head-thing-view-css' => 'd7f293df', - 'phui-header-view-css' => 'b500eeea', + 'phui-header-view-css' => 'be09cc83', 'phui-hovercard' => '074f0783', 'phui-hovercard-view-css' => '6ca90fa0', 'phui-icon-set-selector-css' => '7aa5f3ec', @@ -923,6 +923,14 @@ return array( 'javelin-uri', 'phabricator-notification', ), + '0169e425' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '022516b4' => array( 'javelin-install', 'javelin-util', @@ -1034,6 +1042,12 @@ return array( 'javelin-install', 'javelin-util', ), + '1c95ea63' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-uri', + ), '1cab0e9a' => array( 'javelin-behavior', 'javelin-dom', @@ -1980,12 +1994,6 @@ return array( 'c03f2fb4' => array( 'javelin-install', ), - 'c15122b4' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-uri', - ), 'c2c500a7' => array( 'javelin-install', 'javelin-dom', @@ -2035,14 +2043,6 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), - 'c9ad6f70' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', diff --git a/resources/sql/autopatches/20191028.uriindex.01.rebuild.php b/resources/sql/autopatches/20191028.uriindex.01.rebuild.php new file mode 100644 index 0000000000..c9bd3d97ca --- /dev/null +++ b/resources/sql/autopatches/20191028.uriindex.01.rebuild.php @@ -0,0 +1,4 @@ +establishConnection('w'); + +$iterator = new LiskRawMigrationIterator($conn, $table->getTableName()); +foreach ($iterator as $row) { + $name = $row['identityNameRaw']; + $name = phutil_utf8ize($name); + + $email = new PhutilEmailAddress($name); + $address = $email->getAddress(); + + try { + queryfx( + $conn, + 'UPDATE %R SET emailAddress = %ns WHERE id = %d', + $table, + $address, + $row['id']); + } catch (Exception $ex) { + // We may occasionally run into issues with binary or very long addresses. + // Just skip over them. + continue; + } +} diff --git a/resources/sql/autopatches/20191113.identity.03.unassigned.sql b/resources/sql/autopatches/20191113.identity.03.unassigned.sql new file mode 100644 index 0000000000..768ca1d909 --- /dev/null +++ b/resources/sql/autopatches/20191113.identity.03.unassigned.sql @@ -0,0 +1,3 @@ +UPDATE {$NAMESPACE}_repository.repository_identity + SET currentEffectiveUserPHID = NULL + WHERE currentEffectiveUserPHID = 'unassigned()'; diff --git a/resources/sql/autopatches/20191114.email.01.phid.sql b/resources/sql/autopatches/20191114.email.01.phid.sql new file mode 100644 index 0000000000..3851d6e0ec --- /dev/null +++ b/resources/sql/autopatches/20191114.email.01.phid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.user_email + ADD phid VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20191114.email.02.populate.php b/resources/sql/autopatches/20191114.email.02.populate.php new file mode 100644 index 0000000000..96ef13ea58 --- /dev/null +++ b/resources/sql/autopatches/20191114.email.02.populate.php @@ -0,0 +1,18 @@ +establishConnection('w'); + +$iterator = new LiskRawMigrationIterator($conn, $table->getTableName()); +foreach ($iterator as $row) { + $phid = $row['phid']; + + if (!strlen($phid)) { + queryfx( + $conn, + 'UPDATE %R SET phid = %s WHERE id = %d', + $table, + $table->generatePHID(), + $row['id']); + } +} diff --git a/scripts/ssh/ssh-auth-key.php b/scripts/ssh/ssh-auth-key.php deleted file mode 100755 index 0c23a20edf..0000000000 --- a/scripts/ssh/ssh-auth-key.php +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env php -setViewer(PhabricatorUser::getOmnipotentUser()) - ->withKeys(array($public_key)) - ->withIsActive(true) - ->executeOne(); -if (!$key) { - exit(1); -} - -$object = $key->getObject(); -if (!($object instanceof PhabricatorUser)) { - exit(1); -} - -$bin = $root.'/bin/ssh-exec'; -$cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $object->getUsername()); -// This is additional escaping for the SSH 'command="..."' string. -$cmd = addcslashes($cmd, '"\\'); - -$options = array( - 'command="'.$cmd.'"', - 'no-port-forwarding', - 'no-X11-forwarding', - 'no-agent-forwarding', - 'no-pty', -); - -echo implode(',', $options); -exit(0); diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth.php index 3c4f3f2b33..19b1cc46b4 100755 --- a/scripts/ssh/ssh-auth.php +++ b/scripts/ssh/ssh-auth.php @@ -4,6 +4,24 @@ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/init/init-script.php'; +// TODO: For now, this is using "parseParital()", not "parse()". This allows +// the script to accept (and ignore) additional arguments. This preserves +// backward compatibility until installs have time to migrate to the new +// syntax. + +$args = id(new PhutilArgumentParser($argv)) + ->parsePartial( + array( + array( + 'name' => 'sshd-key', + 'param' => 'k', + 'help' => pht( + 'Accepts the "%%k" parameter from "AuthorizedKeysCommand".'), + ), + )); + +$sshd_key = $args->getArg('sshd-key'); + // NOTE: We are caching a datastructure rather than the flat key file because // the path on disk to "ssh-exec" is arbitrarily mutable at runtime. See T12397. @@ -85,6 +103,22 @@ if ($authstruct === null) { $cache->setKey($authstruct_key, $authstruct_raw, $ttl); } +// If we've received an "--sshd-key" argument and it matches some known key, +// only emit that key. (For now, if the key doesn't match, we'll fall back to +// emitting all keys.) +if ($sshd_key !== null) { + $matches = array(); + foreach ($authstruct['keys'] as $key => $key_struct) { + if ($key_struct['key'] === $sshd_key) { + $matches[$key] = $key_struct; + } + } + + if ($matches) { + $authstruct['keys'] = $matches; + } +} + $bin = $root.'/bin/ssh-exec'; $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c35ac91e1d..293954abed 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -984,7 +984,9 @@ phutil_register_library_map(array( 'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php', 'DiffusionRepositoryFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryFunctionDatasource.php', 'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php', + 'DiffusionRepositoryIdentityDestructionEngineExtension' => 'applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php', 'DiffusionRepositoryIdentityEditor' => 'applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php', + 'DiffusionRepositoryIdentityEngine' => 'applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php', 'DiffusionRepositoryIdentitySearchEngine' => 'applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php', 'DiffusionRepositoryLimitsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryLimitsManagementPanel.php', 'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php', @@ -996,6 +998,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryManagementOtherPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementOtherPanelGroup.php', 'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php', 'DiffusionRepositoryManagementPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementPanelGroup.php', + 'DiffusionRepositoryMetricsSearchEngineAttachment' => 'applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php', 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryPoliciesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php', 'DiffusionRepositoryProfilePictureController' => 'applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php', @@ -1337,6 +1340,8 @@ phutil_register_library_map(array( 'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php', 'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php', 'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php', + 'HarbormasterArtifactSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php', + 'HarbormasterArtifactSearchEngine' => 'applications/harbormaster/query/HarbormasterArtifactSearchEngine.php', 'HarbormasterAutotargetsTestCase' => 'applications/harbormaster/__tests__/HarbormasterAutotargetsTestCase.php', 'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php', 'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php', @@ -2437,6 +2442,14 @@ phutil_register_library_map(array( 'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php', 'PhabricatorAuthSSHKeyTransactionQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyTransactionQuery.php', 'PhabricatorAuthSSHKeyViewController' => 'applications/auth/controller/PhabricatorAuthSSHKeyViewController.php', + 'PhabricatorAuthSSHPrivateKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPrivateKey.php', + 'PhabricatorAuthSSHPrivateKeyException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php', + 'PhabricatorAuthSSHPrivateKeyFormatException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyFormatException.php', + 'PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException.php', + 'PhabricatorAuthSSHPrivateKeyMissingPassphraseException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyMissingPassphraseException.php', + 'PhabricatorAuthSSHPrivateKeyPassphraseException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyPassphraseException.php', + 'PhabricatorAuthSSHPrivateKeySurplusPassphraseException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeySurplusPassphraseException.php', + 'PhabricatorAuthSSHPrivateKeyUnknownException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyUnknownException.php', 'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php', 'PhabricatorAuthSSHRevoker' => 'applications/auth/revoker/PhabricatorAuthSSHRevoker.php', 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', @@ -4110,6 +4123,8 @@ phutil_register_library_map(array( 'PhabricatorPeopleTasksProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php', 'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php', 'PhabricatorPeopleTransactionQuery' => 'applications/people/query/PhabricatorPeopleTransactionQuery.php', + 'PhabricatorPeopleUserEmailPHIDType' => 'applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php', + 'PhabricatorPeopleUserEmailQuery' => 'applications/people/query/PhabricatorPeopleUserEmailQuery.php', 'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php', 'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php', 'PhabricatorPeopleUsernameMailEngine' => 'applications/people/mail/PhabricatorPeopleUsernameMailEngine.php', @@ -4208,7 +4223,6 @@ phutil_register_library_map(array( 'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', 'PhabricatorPolicyRulesView' => 'applications/policy/view/PhabricatorPolicyRulesView.php', 'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php', - 'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php', 'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', @@ -4646,10 +4660,12 @@ phutil_register_library_map(array( 'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php', 'PhabricatorSearchFerretNgramGarbageCollector' => 'applications/search/garbagecollector/PhabricatorSearchFerretNgramGarbageCollector.php', 'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php', + 'PhabricatorSearchHandleController' => 'applications/search/controller/PhabricatorSearchHandleController.php', 'PhabricatorSearchHost' => 'infrastructure/cluster/search/PhabricatorSearchHost.php', 'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php', 'PhabricatorSearchIndexVersion' => 'applications/search/storage/PhabricatorSearchIndexVersion.php', 'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchIndexVersionDestructionEngineExtension.php', + 'PhabricatorSearchIntField' => 'applications/search/field/PhabricatorSearchIntField.php', 'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php', 'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php', 'PhabricatorSearchManagementNgramsWorkflow' => 'applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php', @@ -4870,6 +4886,7 @@ phutil_register_library_map(array( 'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php', 'PhabricatorSystemApplication' => 'applications/system/application/PhabricatorSystemApplication.php', 'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php', + 'PhabricatorSystemDebugUIEventListener' => 'applications/system/events/PhabricatorSystemDebugUIEventListener.php', 'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php', 'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php', 'PhabricatorSystemObjectController' => 'applications/system/controller/PhabricatorSystemObjectController.php', @@ -5046,6 +5063,7 @@ phutil_register_library_map(array( 'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php', 'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php', 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', + 'PhabricatorWorkboardInterface' => 'applications/project/interface/PhabricatorWorkboardInterface.php', 'PhabricatorWorkboardViewState' => 'applications/project/state/PhabricatorWorkboardViewState.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php', @@ -6953,7 +6971,9 @@ phutil_register_library_map(array( 'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel', + 'DiffusionRepositoryIdentityDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'DiffusionRepositoryIdentityEditor' => 'PhabricatorApplicationTransactionEditor', + 'DiffusionRepositoryIdentityEngine' => 'Phobject', 'DiffusionRepositoryIdentitySearchEngine' => 'PhabricatorApplicationSearchEngine', 'DiffusionRepositoryLimitsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryListController' => 'DiffusionController', @@ -6965,6 +6985,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryManagementOtherPanelGroup' => 'DiffusionRepositoryManagementPanelGroup', 'DiffusionRepositoryManagementPanel' => 'Phobject', 'DiffusionRepositoryManagementPanelGroup' => 'Phobject', + 'DiffusionRepositoryMetricsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'DiffusionRepositoryPath' => 'Phobject', 'DiffusionRepositoryPoliciesManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryProfilePictureController' => 'DiffusionController', @@ -7368,6 +7389,8 @@ phutil_register_library_map(array( 'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArtifact' => 'Phobject', + 'HarbormasterArtifactSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', + 'HarbormasterArtifactSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase', 'HarbormasterBuild' => array( 'HarbormasterDAO', @@ -7383,6 +7406,7 @@ phutil_register_library_map(array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorConduitResultInterface', ), 'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', @@ -8671,6 +8695,14 @@ phutil_register_library_map(array( 'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthSSHKeyTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthSSHKeyViewController' => 'PhabricatorAuthSSHKeyController', + 'PhabricatorAuthSSHPrivateKey' => 'Phobject', + 'PhabricatorAuthSSHPrivateKeyException' => 'Exception', + 'PhabricatorAuthSSHPrivateKeyFormatException' => 'PhabricatorAuthSSHPrivateKeyException', + 'PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException' => 'PhabricatorAuthSSHPrivateKeyPassphraseException', + 'PhabricatorAuthSSHPrivateKeyMissingPassphraseException' => 'PhabricatorAuthSSHPrivateKeyPassphraseException', + 'PhabricatorAuthSSHPrivateKeyPassphraseException' => 'PhabricatorAuthSSHPrivateKeyException', + 'PhabricatorAuthSSHPrivateKeySurplusPassphraseException' => 'PhabricatorAuthSSHPrivateKeyPassphraseException', + 'PhabricatorAuthSSHPrivateKeyUnknownException' => 'PhabricatorAuthSSHPrivateKeyException', 'PhabricatorAuthSSHPublicKey' => 'Phobject', 'PhabricatorAuthSSHRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthSession' => array( @@ -10592,6 +10624,8 @@ phutil_register_library_map(array( 'PhabricatorPeopleTasksProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorPeopleUserEmailPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorPeopleUserEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleUsernameMailEngine' => 'PhabricatorPeopleMailEngine', @@ -10706,7 +10740,6 @@ phutil_register_library_map(array( 'PhabricatorPolicyRule' => 'Phobject', 'PhabricatorPolicyRulesView' => 'AphrontView', 'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension', - 'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants', 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyTestObject' => array( 'Phobject', @@ -10751,6 +10784,7 @@ phutil_register_library_map(array( 'PhabricatorColumnProxyInterface', 'PhabricatorSpacesInterface', 'PhabricatorEditEngineSubtypeInterface', + 'PhabricatorWorkboardInterface', ), 'PhabricatorProjectActivityChartEngine' => 'PhabricatorChartEngine', 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', @@ -11263,10 +11297,12 @@ phutil_register_library_map(array( 'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorSearchFerretNgramGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSearchField' => 'Phobject', + 'PhabricatorSearchHandleController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchHost' => 'Phobject', 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO', 'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', + 'PhabricatorSearchIntField' => 'PhabricatorSearchField', 'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementNgramsWorkflow' => 'PhabricatorSearchManagementWorkflow', @@ -11504,6 +11540,7 @@ phutil_register_library_map(array( 'PhabricatorSystemActionRateLimitException' => 'Exception', 'PhabricatorSystemApplication' => 'PhabricatorApplication', 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO', + 'PhabricatorSystemDebugUIEventListener' => 'PhabricatorEventListener', 'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO', 'PhabricatorSystemObjectController' => 'PhabricatorController', @@ -11654,7 +11691,11 @@ phutil_register_library_map(array( 'PhabricatorUserEditEngine' => 'PhabricatorEditEngine', 'PhabricatorUserEditor' => 'PhabricatorEditor', 'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase', - 'PhabricatorUserEmail' => 'PhabricatorUserDAO', + 'PhabricatorUserEmail' => array( + 'PhabricatorUserDAO', + 'PhabricatorDestructibleInterface', + 'PhabricatorPolicyInterface', + ), 'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase', 'PhabricatorUserEmpowerTransaction' => 'PhabricatorUserTransactionType', 'PhabricatorUserFerretEngine' => 'PhabricatorFerretEngine', diff --git a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php index 7d14df7239..cd621ce821 100644 --- a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php +++ b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php @@ -93,6 +93,12 @@ final class PhabricatorAuditManagementDeleteWorkflow if ($repos) { $query->withRepositoryIDs(mpull($repos, 'getID')); + + // See T13457. If we're iterating over commits in a single large + // repository, the lack of a "" key can slow things + // down. Iterate in a specific order to use a key which is present + // on the table (""). + $query->setOrderVector(array('-epoch', '-id')); } $auditor_map = array(); @@ -105,7 +111,11 @@ final class PhabricatorAuditManagementDeleteWorkflow $query->withPHIDs(mpull($commits, 'getPHID')); } - $commit_iterator = new PhabricatorQueryIterator($query); + $commit_iterator = id(new PhabricatorQueryIterator($query)); + + // See T13457. We may be examining many commits; each commit is small so + // we can safely increase the page size to improve performance a bit. + $commit_iterator->setPageSize(1000); $audits = array(); foreach ($commit_iterator as $commit) { diff --git a/src/applications/auth/adapter/PhutilAsanaAuthAdapter.php b/src/applications/auth/adapter/PhutilAsanaAuthAdapter.php index 5d9a9ec478..5fe343671e 100644 --- a/src/applications/auth/adapter/PhutilAsanaAuthAdapter.php +++ b/src/applications/auth/adapter/PhutilAsanaAuthAdapter.php @@ -14,7 +14,9 @@ final class PhutilAsanaAuthAdapter extends PhutilOAuthAuthAdapter { } public function getAccountID() { - return $this->getOAuthAccountData('id'); + // See T13453. The Asana API has changed to string IDs and now returns a + // "gid" field (previously, it returned an "id" field). + return $this->getOAuthAccountData('gid'); } public function getAccountEmail() { diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php index cda56d34b1..505620b3f3 100644 --- a/src/applications/auth/controller/PhabricatorAuthController.php +++ b/src/applications/auth/controller/PhabricatorAuthController.php @@ -286,4 +286,26 @@ abstract class PhabricatorAuthController extends PhabricatorController { ->appendChild($invite_list); } + + final protected function newCustomStartMessage() { + $viewer = $this->getViewer(); + + $text = PhabricatorAuthMessage::loadMessageText( + $viewer, + PhabricatorAuthLoginMessageType::MESSAGEKEY); + + if (!strlen($text)) { + return null; + } + + $remarkup_view = new PHUIRemarkupView($viewer, $text); + + return phutil_tag( + 'div', + array( + 'class' => 'auth-custom-message', + ), + $remarkup_view); + } + } diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index e7dabd9340..b46cf36d49 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -238,18 +238,24 @@ final class PhabricatorAuthLoginController $content) { $crumbs = $this->buildApplicationCrumbs(); + $viewer = $this->getViewer(); - if ($this->getRequest()->getUser()->isLoggedIn()) { + if ($viewer->isLoggedIn()) { $crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI()); } else { - $crumbs->addTextCrumb(pht('Log In'), $this->getApplicationURI('start/')); + $crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/')); + + $content = array( + $this->newCustomStartMessage(), + $content, + ); } $crumbs->addTextCrumb($provider->getProviderName()); $crumbs->setBorder(true); return $this->newPage() - ->setTitle(pht('Log In')) + ->setTitle(pht('Login')) ->setCrumbs($crumbs) ->appendChild($content); } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 72cbbea5a8..7e9b17feff 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -298,27 +298,6 @@ final class PhabricatorAuthStartController ->setURI($auto_uri); } - private function newCustomStartMessage() { - $viewer = $this->getViewer(); - - $text = PhabricatorAuthMessage::loadMessageText( - $viewer, - PhabricatorAuthLoginMessageType::MESSAGEKEY); - - if (!strlen($text)) { - return null; - } - - $remarkup_view = new PHUIRemarkupView($viewer, $text); - - return phutil_tag( - 'div', - array( - 'class' => 'auth-custom-message', - ), - $remarkup_view); - } - private function newEmailLoginView(array $configs) { assert_instances_of($configs, 'PhabricatorAuthProviderConfig'); diff --git a/src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php b/src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php new file mode 100644 index 0000000000..a86e7dafaa --- /dev/null +++ b/src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php @@ -0,0 +1,9 @@ + + } + + public function setPassphrase(PhutilOpaqueEnvelope $passphrase) { + $this->passphrase = $passphrase; + return $this; + } + + public function getPassphrase() { + return $this->passphrase; + } + + public static function newFromRawKey(PhutilOpaqueEnvelope $entire_key) { + $key = new self(); + + $key->body = $entire_key; + + return $key; + } + + public function getKeyBody() { + return $this->body; + } + + public function newBarePrivateKey() { + if (!Filesystem::binaryExists('ssh-keygen')) { + throw new Exception( + pht( + 'Analyzing or decrypting SSH keys requires the "ssh-keygen" binary, '. + 'but it is not available in "$PATH". Make it available to work with '. + 'SSH private keys.')); + } + + $old_body = $this->body; + + // Some versions of "ssh-keygen" are sensitive to trailing whitespace for + // some keys. Trim any trailing whitespace and replace it with a single + // newline. + $raw_body = $old_body->openEnvelope(); + $raw_body = rtrim($raw_body)."\n"; + $old_body = new PhutilOpaqueEnvelope($raw_body); + + $tmp = $this->newTemporaryPrivateKeyFile($old_body); + + // See T13454 for discussion of why this is so awkward. In broad strokes, + // we don't have a straightforward way to distinguish between keys with an + // invalid format and keys with a passphrase which we don't know. + + // First, try to extract the public key from the file using the (possibly + // empty) passphrase we were given. If everything is in good shape, this + // should work. + + $passphrase = $this->getPassphrase(); + if ($passphrase) { + list($err, $stdout, $stderr) = exec_manual( + 'ssh-keygen -y -P %P -f %R', + $passphrase, + $tmp); + } else { + list($err, $stdout, $stderr) = exec_manual( + 'ssh-keygen -y -P %s -f %R', + '', + $tmp); + } + + // If that worked, the key is good and the (possibly empty) passphrase is + // correct. Strip the passphrase if we have one, then return the bare key. + + if (!$err) { + if ($passphrase) { + execx( + 'ssh-keygen -y -P %P -N %s -f %R', + $passphrase, + '', + $tmp); + + $new_body = new PhutilOpaqueEnvelope(Filesystem::readFile($tmp)); + unset($tmp); + } else { + $new_body = $old_body; + } + + return self::newFromRawKey($new_body); + } + + // We were not able to extract the public key. Try to figure out why. The + // reasons we expect are: + // + // - We were given a passphrase, but the key has no passphrase. + // - We were given a passphrase, but the passphrase is wrong. + // - We were not given a passphrase, but the key has a passphrase. + // - The key format is invalid. + // + // Our ability to separate these cases varies a lot, particularly because + // some versions of "ssh-keygen" return very similar diagnostic messages + // for any error condition. Try our best. + + if ($passphrase) { + // First, test for "we were given a passphrase, but the key has no + // passphrase", since this is a conclusive test. + list($err) = exec_manual( + 'ssh-keygen -y -P %s -f %R', + '', + $tmp); + if (!$err) { + throw new PhabricatorAuthSSHPrivateKeySurplusPassphraseException( + pht( + 'A passphrase was provided for this private key, but it does '. + 'not require a passphrase. Check that you supplied the correct '. + 'key, or omit the passphrase.')); + } + } + + // We're out of conclusive tests, so try to guess why the error occurred. + // In some versions of "ssh-keygen", we get a usable diagnostic message. In + // other versions, not so much. + + $reason_format = 'format'; + $reason_passphrase = 'passphrase'; + $reason_unknown = 'unknown'; + + $patterns = array( + // macOS 10.14.6 + '/incorrect passphrase supplied to decrypt private key/' + => $reason_passphrase, + + // macOS 10.14.6 + '/invalid format/' => $reason_format, + + // Ubuntu 14 + '/load failed/' => $reason_unknown, + ); + + $reason = 'unknown'; + foreach ($patterns as $pattern => $pattern_reason) { + $ok = preg_match($pattern, $stderr); + + if ($ok === false) { + throw new Exception( + pht( + 'Pattern "%s" is not valid.', + $pattern)); + } + + if ($ok) { + $reason = $pattern_reason; + break; + } + } + + if ($reason === $reason_format) { + throw new PhabricatorAuthSSHPrivateKeyFormatException( + pht( + 'This private key is not formatted correctly. Check that you '. + 'have provided the complete text of a valid private key.')); + } + + if ($reason === $reason_passphrase) { + if ($passphrase) { + throw new PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException( + pht( + 'This private key requires a passphrase, but the wrong '. + 'passphrase was provided. Check that you supplied the correct '. + 'key and passphrase.')); + } else { + throw new PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException( + pht( + 'This private key requires a passphrase, but no passphrase was '. + 'provided. Check that you supplied the correct key, or provide '. + 'the passphrase.')); + } + } + + if ($passphrase) { + throw new PhabricatorAuthSSHPrivateKeyUnknownException( + pht( + 'This private key could not be opened with the provided passphrase. '. + 'This might mean that the passphrase is wrong or that the key is '. + 'not formatted correctly. Check that you have supplied the '. + 'complete text of a valid private key and the correct passphrase.')); + } else { + throw new PhabricatorAuthSSHPrivateKeyUnknownException( + pht( + 'This private key could not be opened. This might mean that the '. + 'key requires a passphrase, or might mean that the key is not '. + 'formatted correctly. Check that you have supplied the complete '. + 'text of a valid private key and the correct passphrase.')); + } + } + + private function newTemporaryPrivateKeyFile(PhutilOpaqueEnvelope $key_body) { + $tmp = new TempFile(); + + Filesystem::writeFile($tmp, $key_body->openEnvelope()); + + return $tmp; + } + +} diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index cab9276674..d34189125a 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -99,9 +99,6 @@ final class PhabricatorConduitAPIController list($error_code, $error_info) = $auth_error; } } catch (Exception $ex) { - if (!($ex instanceof ConduitMethodNotFoundException)) { - phlog($ex); - } $result = null; $error_code = ($ex instanceof ConduitException ? 'ERR-CONDUIT-CALL' diff --git a/src/applications/config/check/PhabricatorManualActivitySetupCheck.php b/src/applications/config/check/PhabricatorManualActivitySetupCheck.php index c5c756e49b..04457cb12a 100644 --- a/src/applications/config/check/PhabricatorManualActivitySetupCheck.php +++ b/src/applications/config/check/PhabricatorManualActivitySetupCheck.php @@ -113,7 +113,8 @@ final class PhabricatorManualActivitySetupCheck 'pre', array(), (string)csprintf( - 'phabricator/ $ ./bin/repository rebuild-identities --all')); + 'phabricator/ $ '. + './bin/repository rebuild-identities --all-repositories')); $message[] = pht( 'You can find more information about this new identity mapping '. diff --git a/src/applications/config/controller/PhabricatorConfigAllController.php b/src/applications/config/controller/PhabricatorConfigAllController.php index 3a19eff006..7452b29e21 100644 --- a/src/applications/config/controller/PhabricatorConfigAllController.php +++ b/src/applications/config/controller/PhabricatorConfigAllController.php @@ -64,13 +64,12 @@ final class PhabricatorConfigAllController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigApplicationController.php b/src/applications/config/controller/PhabricatorConfigApplicationController.php index b4f60982e7..a6b8cd38c6 100644 --- a/src/applications/config/controller/PhabricatorConfigApplicationController.php +++ b/src/applications/config/controller/PhabricatorConfigApplicationController.php @@ -18,9 +18,7 @@ final class PhabricatorConfigApplicationController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($apps_list); + ->setFooter($apps_list); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($title) @@ -29,6 +27,7 @@ final class PhabricatorConfigApplicationController return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigCacheController.php b/src/applications/config/controller/PhabricatorConfigCacheController.php index a23ab1f9c1..36642657c9 100644 --- a/src/applications/config/controller/PhabricatorConfigCacheController.php +++ b/src/applications/config/controller/PhabricatorConfigCacheController.php @@ -33,13 +33,12 @@ final class PhabricatorConfigCacheController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($page); + ->setFooter($page); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php index 43e5a15b9d..417fa9d3a1 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -26,13 +26,12 @@ final class PhabricatorConfigClusterDatabasesController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($status); + ->setFooter($status); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php index 443b51a903..e9f64d411a 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php @@ -28,13 +28,12 @@ final class PhabricatorConfigClusterNotificationsController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($status); + ->setFooter($status); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php b/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php index 471c4cedf0..eb83a28a2a 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php @@ -32,16 +32,16 @@ final class PhabricatorConfigClusterRepositoriesController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn(array( - $repo_status, - $repo_errors, - )); + ->setFooter( + array( + $repo_status, + $repo_errors, + )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterSearchController.php b/src/applications/config/controller/PhabricatorConfigClusterSearchController.php index cd00ef73a0..55caeb1cad 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterSearchController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterSearchController.php @@ -26,13 +26,12 @@ final class PhabricatorConfigClusterSearchController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($search_status); + ->setFooter($search_status); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php index ad5ea6cd27..1b6a5af8b5 100644 --- a/src/applications/config/controller/PhabricatorConfigController.php +++ b/src/applications/config/controller/PhabricatorConfigController.php @@ -7,8 +7,6 @@ abstract class PhabricatorConfigController extends PhabricatorController { } public function buildSideNavView($filter = null, $for_app = false) { - - $guide_href = new PhutilURI('/guides/'); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php index 708a708043..fa6bcab5e6 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php @@ -167,13 +167,12 @@ final class PhabricatorConfigDatabaseIssueController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php index 760317ae80..6831a048d5 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php @@ -142,13 +142,12 @@ final class PhabricatorConfigDatabaseStatusController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($body); + ->setFooter($body); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index 224705e181..381b54e046 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -237,9 +237,8 @@ final class PhabricatorConfigEditController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn(array( + ->setFooter( + array( $error_view, $form_box, $status_items, @@ -250,6 +249,7 @@ final class PhabricatorConfigEditController return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($view); } diff --git a/src/applications/config/controller/PhabricatorConfigGroupController.php b/src/applications/config/controller/PhabricatorConfigGroupController.php index 7a3f77dfea..f981c1c1a1 100644 --- a/src/applications/config/controller/PhabricatorConfigGroupController.php +++ b/src/applications/config/controller/PhabricatorConfigGroupController.php @@ -36,13 +36,12 @@ final class PhabricatorConfigGroupController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigHistoryController.php b/src/applications/config/controller/PhabricatorConfigHistoryController.php index 9157ecb8bb..495102b6a2 100644 --- a/src/applications/config/controller/PhabricatorConfigHistoryController.php +++ b/src/applications/config/controller/PhabricatorConfigHistoryController.php @@ -36,13 +36,12 @@ final class PhabricatorConfigHistoryController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($timeline); + ->setFooter($timeline); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigIssueListController.php b/src/applications/config/controller/PhabricatorConfigIssueListController.php index 0ca94abe04..6518ccec97 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueListController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueListController.php @@ -59,13 +59,12 @@ final class PhabricatorConfigIssueListController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($issue_list); + ->setFooter($issue_list); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigIssueViewController.php b/src/applications/config/controller/PhabricatorConfigIssueViewController.php index 29c9078413..2967169e38 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueViewController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueViewController.php @@ -47,13 +47,12 @@ final class PhabricatorConfigIssueViewController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($content); + ->setFooter($content); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigListController.php b/src/applications/config/controller/PhabricatorConfigListController.php index 1a136ea416..38a0afc328 100644 --- a/src/applications/config/controller/PhabricatorConfigListController.php +++ b/src/applications/config/controller/PhabricatorConfigListController.php @@ -22,13 +22,12 @@ final class PhabricatorConfigListController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($core_list); + ->setFooter($core_list); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigModuleController.php b/src/applications/config/controller/PhabricatorConfigModuleController.php index 63cc5b3843..fe919c57e4 100644 --- a/src/applications/config/controller/PhabricatorConfigModuleController.php +++ b/src/applications/config/controller/PhabricatorConfigModuleController.php @@ -28,13 +28,12 @@ final class PhabricatorConfigModuleController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigVersionController.php b/src/applications/config/controller/PhabricatorConfigVersionController.php index a9571a1f85..153d363062 100644 --- a/src/applications/config/controller/PhabricatorConfigVersionController.php +++ b/src/applications/config/controller/PhabricatorConfigVersionController.php @@ -23,15 +23,13 @@ final class PhabricatorConfigVersionController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); - } public function renderModuleStatus($viewer) { diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index 29a710209e..8969151c06 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -329,9 +329,16 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { $actions = array(); if ($panel) { - $panel_actions = $panel->newHeaderEditActions( - $viewer, - $context_phid); + try { + $panel_actions = $panel->newHeaderEditActions( + $viewer, + $context_phid); + } catch (Exception $ex) { + $error_action = id(new PhabricatorActionView()) + ->setIcon('fa-exclamation-triangle red') + ->setName(pht('')); + $panel_actions[] = $error_action; + } if ($panel_actions) { foreach ($panel_actions as $panel_action) { diff --git a/src/applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php b/src/applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php index 7b48022119..1822810c05 100644 --- a/src/applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php +++ b/src/applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php @@ -9,4 +9,14 @@ final class PhabricatorDashboardTextPanelTextTransaction return 'text'; } + public function newRemarkupChanges() { + $changes = array(); + + $changes[] = $this->newRemarkupChange() + ->setOldValue($this->getOldValue()) + ->setNewValue($this->getNewValue()); + + return $changes; + } + } diff --git a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php index 26632dff24..84a4cbc519 100644 --- a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php +++ b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php @@ -103,7 +103,7 @@ final class PhabricatorDifferentialRevisionTestDataGenerator $newcode2[] = $altcodearr[$randomlines_new[$c++]]; } } - return implode($newcode2, "\n"); + return implode("\n", $newcode2); } } diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 6c4ed3d14f..92d5f23c1a 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -1686,40 +1686,72 @@ final class DifferentialChangesetParser extends Phobject { break; } - $old_ref = id(new PhabricatorDocumentRef()) - ->setName($changeset->getOldFile()); - if ($old_file) { - $old_ref->setFile($old_file); - } else { - $old_data = $this->old; - $old_data = ipull($old_data, 'text'); - $old_data = implode('', $old_data); + $type_delete = DifferentialChangeType::TYPE_DELETE; + $type_add = DifferentialChangeType::TYPE_ADD; + $change_type = $changeset->getChangeType(); - $old_ref->setData($old_data); + $no_old = ($change_type == $type_add); + $no_new = ($change_type == $type_delete); + + if ($no_old) { + $old_ref = null; + } else { + $old_ref = id(new PhabricatorDocumentRef()) + ->setName($changeset->getOldFile()); + if ($old_file) { + $old_ref->setFile($old_file); + } else { + $old_data = $this->old; + $old_data = ipull($old_data, 'text'); + $old_data = implode('', $old_data); + + $old_ref->setData($old_data); + } } - $new_ref = id(new PhabricatorDocumentRef()) - ->setName($changeset->getFilename()); - if ($new_file) { - $new_ref->setFile($new_file); + if ($no_new) { + $new_ref = null; } else { - $new_data = $this->new; - $new_data = ipull($new_data, 'text'); - $new_data = implode('', $new_data); + $new_ref = id(new PhabricatorDocumentRef()) + ->setName($changeset->getFilename()); + if ($new_file) { + $new_ref->setFile($new_file); + } else { + $new_data = $this->new; + $new_data = ipull($new_data, 'text'); + $new_data = implode('', $new_data); - $new_ref->setData($new_data); + $new_ref->setData($new_data); + } } - $old_engines = PhabricatorDocumentEngine::getEnginesForRef( - $viewer, - $old_ref); - $new_engines = PhabricatorDocumentEngine::getEnginesForRef( - $viewer, - $new_ref); + $old_engines = null; + if ($old_ref) { + $old_engines = PhabricatorDocumentEngine::getEnginesForRef( + $viewer, + $old_ref); + } - $shared_engines = array_intersect_key($new_engines, $old_engines); - $default_engine = head_key($new_engines); + $new_engines = null; + if ($new_ref) { + $new_engines = PhabricatorDocumentEngine::getEnginesForRef( + $viewer, + $new_ref); + } + + if ($new_engines !== null && $old_engines !== null) { + $shared_engines = array_intersect_key($new_engines, $old_engines); + $default_engine = head_key($new_engines); + } else if ($new_engines !== null) { + $shared_engines = $new_engines; + $default_engine = head_key($shared_engines); + } else if ($old_engines !== null) { + $shared_engines = $old_engines; + $default_engine = head_key($shared_engines); + } else { + return null; + } foreach ($shared_engines as $key => $shared_engine) { if (!$shared_engine->canDiffDocuments($old_ref, $new_ref)) { diff --git a/src/applications/differential/phid/DifferentialRevisionPHIDType.php b/src/applications/differential/phid/DifferentialRevisionPHIDType.php index a117690d66..a7d3c9f4a7 100644 --- a/src/applications/differential/phid/DifferentialRevisionPHIDType.php +++ b/src/applications/differential/phid/DifferentialRevisionPHIDType.php @@ -44,15 +44,6 @@ final class DifferentialRevisionPHIDType extends PhabricatorPHIDType { if ($revision->isClosed()) { $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); } - - $icon = $revision->getStatusIcon(); - $color = $revision->getStatusIconColor(); - $name = $revision->getStatusDisplayName(); - - $handle - ->setStateIcon($icon) - ->setStateColor($color) - ->setStateName($name); } } diff --git a/src/applications/differential/query/DifferentialChangesetSearchEngine.php b/src/applications/differential/query/DifferentialChangesetSearchEngine.php index 0dfec94a53..3fe8957971 100644 --- a/src/applications/differential/query/DifferentialChangesetSearchEngine.php +++ b/src/applications/differential/query/DifferentialChangesetSearchEngine.php @@ -22,6 +22,10 @@ final class DifferentialChangesetSearchEngine return 'PhabricatorDifferentialApplication'; } + public function canUseInPanelContext() { + return false; + } + public function newQuery() { $query = id(new DifferentialChangesetQuery()); diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php index fc50b0d605..b3e6520fd9 100644 --- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php @@ -371,17 +371,27 @@ final class DifferentialChangesetOneUpRenderer $cell_classes = $block_diff->getNewClasses(); } } else if ($row_type === 'old') { + if (!$old_ref || !$old) { + continue; + } + $cell_content = $engine->newBlockContentView( $old_ref, $old); + $cell_classes[] = 'old'; $cell_classes[] = 'old-full'; $new_key = null; } else if ($row_type === 'new') { + if (!$new_ref || !$new) { + continue; + } + $cell_content = $engine->newBlockContentView( $new_ref, $new); + $cell_classes[] = 'new'; $cell_classes[] = 'new-full'; diff --git a/src/applications/diffusion/controller/DiffusionIdentityViewController.php b/src/applications/diffusion/controller/DiffusionIdentityViewController.php index 20efe2749f..1c78ec5992 100644 --- a/src/applications/diffusion/controller/DiffusionIdentityViewController.php +++ b/src/applications/diffusion/controller/DiffusionIdentityViewController.php @@ -22,11 +22,13 @@ final class DiffusionIdentityViewController $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($identity->getIdentityShortName()) - ->setHeaderIcon('fa-globe') - ->setPolicyObject($identity); + ->setHeaderIcon('fa-globe'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($identity->getID()); + $crumbs->addTextCrumb( + pht('Identities'), + $this->getApplicationURI('identity/')); + $crumbs->addTextCrumb($identity->getObjectName()); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( @@ -83,7 +85,11 @@ final class DiffusionIdentityViewController $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) - ->setUser($viewer); + ->setViewer($viewer); + + $properties->addProperty( + pht('Email Address'), + $identity->getEmailAddress()); $effective_phid = $identity->getCurrentEffectiveUserPHID(); $automatic_phid = $identity->getAutomaticGuessedUserPHID(); @@ -109,7 +115,7 @@ final class DiffusionIdentityViewController pht('Automatically Detected User'), $this->buildPropertyValue($automatic_phid)); $properties->addProperty( - pht('Manually Set User'), + pht('Assigned To'), $this->buildPropertyValue($manual_phid)); $header = id(new PHUIHeaderView()) @@ -127,7 +133,7 @@ final class DiffusionIdentityViewController if ($value == DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN) { return phutil_tag('em', array(), pht('Explicitly Unassigned')); } else if (!$value) { - return null; + return phutil_tag('em', array(), pht('None')); } else { return $viewer->renderHandle($value); } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryListController.php b/src/applications/diffusion/controller/DiffusionRepositoryListController.php index 5a21d2e3f1..66226e5eab 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryListController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryListController.php @@ -17,6 +17,14 @@ final class DiffusionRepositoryListController extends DiffusionController { ->setName(pht('Browse Commits')) ->setHref($this->getApplicationURI('commit/')); + $items[] = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL) + ->setName(pht('Identities')); + + $items[] = id(new PHUIListItemView()) + ->setName(pht('Browse Identities')) + ->setHref($this->getApplicationURI('identity/')); + return id(new PhabricatorRepositorySearchEngine()) ->setController($this) ->setNavigationItems($items) diff --git a/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php b/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php index 3ce95cb3c3..cd0b3cceab 100644 --- a/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php +++ b/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php @@ -26,20 +26,45 @@ final class DiffusionHovercardEngineExtension $viewer = $this->getViewer(); - $author_phid = $commit->getAuthorPHID(); - if ($author_phid) { - $author = $viewer->renderHandle($author_phid); - } else { - $commit_data = $commit->loadCommitData(); - $author = phutil_tag('em', array(), $commit_data->getAuthorName()); + $commit = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->needIdentities(true) + ->needCommitData(true) + ->withPHIDs(array($commit->getPHID())) + ->executeOne(); + if (!$commit) { + return; } + $author_phid = $commit->getAuthorDisplayPHID(); + $committer_phid = $commit->getCommitterDisplayPHID(); + $repository_phid = $commit->getRepository()->getPHID(); + + $phids = array(); + $phids[] = $author_phid; + $phids[] = $committer_phid; + $phids[] = $repository_phid; + + $handles = $viewer->loadHandles($phids); + $hovercard->setTitle($handle->getName()); $hovercard->setDetail($commit->getSummary()); - $hovercard->addField(pht('Author'), $author); - $hovercard->addField(pht('Date'), - phabricator_date($commit->getEpoch(), $viewer)); + $repository = $handles[$repository_phid]->renderLink(); + $hovercard->addField(pht('Repository'), $repository); + + $author = $handles[$author_phid]->renderLink(); + if ($author_phid) { + $hovercard->addField(pht('Author'), $author); + } + + if ($committer_phid && ($committer_phid !== $author_phid)) { + $committer = $handles[$committer_phid]->renderLink(); + $hovercard->addField(pht('Committer'), $committer); + } + + $date = phabricator_date($commit->getEpoch(), $viewer); + $hovercard->addField(pht('Date'), $date); if (!$commit->isAuditStatusNoAudit()) { $status = $commit->getAuditStatusObject(); diff --git a/src/applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php b/src/applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php new file mode 100644 index 0000000000..9711e72352 --- /dev/null +++ b/src/applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php @@ -0,0 +1,41 @@ +needCommitCounts(true) + ->needMostRecentCommits(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + $commit = $object->getMostRecentCommit(); + if ($commit !== null) { + $recent_commit = $commit->getFieldValuesForConduit(); + } else { + $recent_commit = null; + } + + $commit_count = $object->getCommitCount(); + if ($commit_count !== null) { + $commit_count = (int)$commit_count; + } + + return array( + 'commitCount' => $commit_count, + 'recentCommit' => $recent_commit, + ); + } + +} diff --git a/src/applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php b/src/applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php new file mode 100644 index 0000000000..effbe47981 --- /dev/null +++ b/src/applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php @@ -0,0 +1,40 @@ +getPHID(); + } + + if ($object instanceof PhabricatorUserEmail) { + $email_addresses[] = $object->getAddress(); + } + + if ($related_phids || $email_addresses) { + PhabricatorWorker::scheduleTask( + 'PhabricatorRepositoryIdentityChangeWorker', + array( + 'relatedPHIDs' => $related_phids, + 'emailAddresses' => $email_addresses, + )); + } + } + +} diff --git a/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php new file mode 100644 index 0000000000..8cd8a4bfa2 --- /dev/null +++ b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php @@ -0,0 +1,120 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setSourcePHID($source_phid) { + $this->sourcePHID = $source_phid; + return $this; + } + + public function getSourcePHID() { + if (!$this->sourcePHID) { + throw new PhutilInvalidStateException('setSourcePHID'); + } + + return $this->sourcePHID; + } + + public function setDryRun($dry_run) { + $this->dryRun = $dry_run; + return $this; + } + + public function getDryRun() { + return $this->dryRun; + } + + public function newResolvedIdentity($raw_identity) { + $identity = $this->loadRawIdentity($raw_identity); + + if (!$identity) { + $identity = $this->newIdentity($raw_identity); + } + + return $this->updateIdentity($identity); + } + + public function newUpdatedIdentity(PhabricatorRepositoryIdentity $identity) { + return $this->updateIdentity($identity); + } + + private function loadRawIdentity($raw_identity) { + $viewer = $this->getViewer(); + + return id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withIdentityNames(array($raw_identity)) + ->executeOne(); + } + + private function newIdentity($raw_identity) { + $source_phid = $this->getSourcePHID(); + + return id(new PhabricatorRepositoryIdentity()) + ->setAuthorPHID($source_phid) + ->setIdentityName($raw_identity); + } + + private function resolveIdentity(PhabricatorRepositoryIdentity $identity) { + $raw_identity = $identity->getIdentityName(); + + return id(new DiffusionResolveUserQuery()) + ->withName($raw_identity) + ->execute(); + } + + private function updateIdentity(PhabricatorRepositoryIdentity $identity) { + + // If we're updating an identity and it has a manual user PHID associated + // with it but the user is no longer valid, remove the value. This likely + // corresponds to a user that was destroyed. + + $assigned_phid = $identity->getManuallySetUserPHID(); + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; + if ($assigned_phid && ($assigned_phid !== $unassigned)) { + $viewer = $this->getViewer(); + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($assigned_phid)) + ->executeOne(); + if (!$user) { + $identity->setManuallySetUserPHID(null); + } + } + + $resolved_phid = $this->resolveIdentity($identity); + + $identity->setAutomaticGuessedUserPHID($resolved_phid); + + if ($this->getDryRun()) { + $identity->makeEphemeral(); + } else { + $identity->save(); + } + + return $identity; + } + + public function didUpdateEmailAddress($raw_address) { + PhabricatorWorker::scheduleTask( + 'PhabricatorRepositoryIdentityChangeWorker', + array( + 'emailAddresses' => array($raw_address), + )); + } + +} diff --git a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php index 43b4d31252..e24bae7da4 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php @@ -23,6 +23,7 @@ final class DiffusionRepositoryBranchesManagementPanel $has_any = $repository->getDetail('default-branch') || + $repository->getFetchRules() || $repository->getTrackOnlyRules() || $repository->getPermanentRefRules(); diff --git a/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php b/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php index 4d10ee44e0..f41b6b89a4 100644 --- a/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php +++ b/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php @@ -17,21 +17,35 @@ final class DiffusionRepositoryIdentitySearchEngine protected function buildCustomSearchFields() { return array( + id(new PhabricatorUsersSearchField()) + ->setLabel(pht('Matching Users')) + ->setKey('effectivePHIDs') + ->setAliases( + array( + 'effective', + 'effectivePHID', + )) + ->setDescription(pht('Search for identities by effective user.')), id(new DiffusionIdentityAssigneeSearchField()) ->setLabel(pht('Assigned To')) - ->setKey('assignee') - ->setDescription(pht('Search for identities by assignee.')), + ->setKey('assignedPHIDs') + ->setAliases( + array( + 'assigned', + 'assignedPHID', + )) + ->setDescription(pht('Search for identities by explicit assignee.')), id(new PhabricatorSearchTextField()) ->setLabel(pht('Identity Contains')) ->setKey('match') ->setDescription(pht('Search for identities by substring.')), id(new PhabricatorSearchThreeStateField()) - ->setLabel(pht('Is Assigned')) + ->setLabel(pht('Has Matching User')) ->setKey('hasEffectivePHID') ->setOptions( pht('(Show All)'), - pht('Show Only Assigned Identities'), - pht('Show Only Unassigned Identities')), + pht('Show Identities With Matching Users'), + pht('Show Identities Without Matching Users')), ); } @@ -46,8 +60,12 @@ final class DiffusionRepositoryIdentitySearchEngine $query->withIdentityNameLike($map['match']); } - if ($map['assignee']) { - $query->withAssigneePHIDs($map['assignee']); + if ($map['assignedPHIDs']) { + $query->withAssignedPHIDs($map['assignedPHIDs']); + } + + if ($map['effectivePHIDs']) { + $query->withEffectivePHIDs($map['effectivePHIDs']); } return $query; @@ -86,15 +104,54 @@ final class DiffusionRepositoryIdentitySearchEngine $viewer = $this->requireViewer(); - $list = new PHUIObjectItemListView(); - $list->setUser($viewer); + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer); + + $phids = array(); + foreach ($identities as $identity) { + $phids[] = $identity->getCurrentEffectiveUserPHID(); + $phids[] = $identity->getManuallySetUserPHID(); + } + + $handles = $viewer->loadHandles($phids); + + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; + foreach ($identities as $identity) { $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Identity %d', $identity->getID())) + ->setObjectName($identity->getObjectName()) ->setHeader($identity->getIdentityShortName()) ->setHref($identity->getURI()) ->setObject($identity); + $status_icon = 'fa-circle-o grey'; + + $effective_phid = $identity->getCurrentEffectiveUserPHID(); + if ($effective_phid) { + $item->addIcon( + 'fa-id-badge', + pht('Matches User: %s', $handles[$effective_phid]->getName())); + + $status_icon = 'fa-id-badge'; + } + + $assigned_phid = $identity->getManuallySetUserPHID(); + if ($assigned_phid) { + if ($assigned_phid === $unassigned) { + $item->addIcon( + 'fa-ban', + pht('Explicitly Unassigned')); + $status_icon = 'fa-ban'; + } else { + $item->addIcon( + 'fa-user', + pht('Assigned To: %s', $handles[$assigned_phid]->getName())); + $status_icon = 'fa-user'; + } + } + + $item->setStatusIcon($status_icon); + $list->addItem($item); } diff --git a/src/applications/diffusion/query/DiffusionResolveUserQuery.php b/src/applications/diffusion/query/DiffusionResolveUserQuery.php index a22408ce16..8ad13f660e 100644 --- a/src/applications/diffusion/query/DiffusionResolveUserQuery.php +++ b/src/applications/diffusion/query/DiffusionResolveUserQuery.php @@ -8,25 +8,14 @@ final class DiffusionResolveUserQuery extends Phobject { private $name; - private $commit; public function withName($name) { $this->name = $name; return $this; } - public function withCommit($commit) { - $this->commit = $commit; - return $this; - } - public function execute() { - $user_name = $this->name; - - $phid = $this->findUserPHID($this->name); - $phid = $this->fireLookupEvent($phid); - - return $phid; + return $this->findUserPHID($this->name); } private function findUserPHID($user_name) { @@ -75,33 +64,15 @@ final class DiffusionResolveUserQuery extends Phobject { } - /** - * Emit an event so installs can do custom lookup of commit authors who may - * not be naturally resolvable. - */ - private function fireLookupEvent($guess) { - - $type = PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER; - $data = array( - 'commit' => $this->commit, - 'query' => $this->name, - 'result' => $guess, - ); - - $event = new PhabricatorEvent($type, $data); - PhutilEventEngine::dispatchEvent($event); - - return $event->getValue('result'); - } - - private function findUserByUserName($user_name) { $by_username = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $user_name); + if ($by_username) { return $by_username->getPHID(); } + return null; } @@ -112,18 +83,22 @@ final class DiffusionResolveUserQuery extends Phobject { $by_realname = id(new PhabricatorUser())->loadAllWhere( 'realName = %s', $real_name); + if (count($by_realname) == 1) { - return reset($by_realname)->getPHID(); + return head($by_realname)->getPHID(); } + return null; } private function findUserByEmailAddress($email_address) { $by_email = PhabricatorUser::loadOneWithEmailAddress($email_address); + if ($by_email) { return $by_email->getPHID(); } + return null; } diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php index 05ee786337..59c3e4026d 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php @@ -123,8 +123,11 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge { } public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) { - $id = $result['id']; - $uri = "https://app.asana.com/0/{$id}/{$id}"; + $gid = $result['gid']; + $uri = urisprintf( + 'https://app.asana.com/0/%s/%s', + $gid, + $gid); $obj->setObjectURI($uri); } diff --git a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php index 1771a6615e..3a9c9abac5 100644 --- a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php +++ b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php @@ -102,7 +102,10 @@ final class PhabricatorAsanaConfigOptions pht('Workspace Name')); $out[] = '| ------------ | -------------- |'; foreach ($workspaces as $workspace) { - $out[] = sprintf('| `%s` | `%s` |', $workspace['id'], $workspace['name']); + $out[] = sprintf( + '| `%s` | `%s` |', + $workspace['gid'], + $workspace['name']); } $out = implode("\n", $out); diff --git a/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php b/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php index 1d293956b3..00b75b7a56 100644 --- a/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php +++ b/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php @@ -358,7 +358,7 @@ final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker { 'POST', $subtask_data + array( 'assignee' => $phid_aid_map[$user_phid], - 'completed' => $is_completed, + 'completed' => (int)$is_completed, 'parent' => $parent_ref->getObjectID(), )); @@ -393,7 +393,7 @@ final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker { 'PUT', $subtask_data + array( 'assignee' => $phid_aid_map[$user_phid], - 'completed' => $is_completed, + 'completed' => (int)$is_completed, )); } @@ -484,7 +484,7 @@ final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker { return array( 'name' => $title, 'notes' => $notes, - 'completed' => $is_completed, + 'completed' => (int)$is_completed, ); } @@ -632,7 +632,7 @@ final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker { ->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA) ->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA) ->setObjectType($type) - ->setObjectID($result['id']) + ->setObjectID($result['gid']) ->setIsVisible(true); $xobj = $ref->newExternalObject(); diff --git a/src/applications/fact/chart/PhabricatorFactChartFunction.php b/src/applications/fact/chart/PhabricatorFactChartFunction.php index 0e940d644d..1713282116 100644 --- a/src/applications/fact/chart/PhabricatorFactChartFunction.php +++ b/src/applications/fact/chart/PhabricatorFactChartFunction.php @@ -29,6 +29,7 @@ final class PhabricatorFactChartFunction $key_id = id(new PhabricatorFactKeyDimension()) ->newDimensionID($fact->getKey()); if (!$key_id) { + $this->map = array(); return; } diff --git a/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php b/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php index f8f3a414b1..47ddf194ee 100644 --- a/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php +++ b/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php @@ -15,7 +15,10 @@ final class PhabricatorDocumentEngineBlocks return $this->messages; } - public function addBlockList(PhabricatorDocumentRef $ref, array $blocks) { + public function addBlockList( + PhabricatorDocumentRef $ref = null, + array $blocks = array()) { + assert_instances_of($blocks, 'PhabricatorDocumentEngineBlock'); $this->lists[] = array( diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php index bc7960b4ad..9593e1d98d 100644 --- a/src/applications/files/document/PhabricatorDocumentEngine.php +++ b/src/applications/files/document/PhabricatorDocumentEngine.php @@ -32,8 +32,8 @@ abstract class PhabricatorDocumentEngine } public function canDiffDocuments( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { return false; } diff --git a/src/applications/files/document/PhabricatorImageDocumentEngine.php b/src/applications/files/document/PhabricatorImageDocumentEngine.php index fa678cc034..449d604370 100644 --- a/src/applications/files/document/PhabricatorImageDocumentEngine.php +++ b/src/applications/files/document/PhabricatorImageDocumentEngine.php @@ -18,21 +18,38 @@ final class PhabricatorImageDocumentEngine } public function canDiffDocuments( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { - // For now, we can only render a rich image diff if both documents have + // For now, we can only render a rich image diff if the documents have // their data stored in Files already. - return ($uref->getFile() && $vref->getFile()); + if ($uref && !$uref->getFile()) { + return false; + } + + if ($vref && !$vref->getFile()) { + return false; + } + + return true; } public function newEngineBlocks( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { - $u_blocks = $this->newDiffBlocks($uref); - $v_blocks = $this->newDiffBlocks($vref); + if ($uref) { + $u_blocks = $this->newDiffBlocks($uref); + } else { + $u_blocks = array(); + } + + if ($vref) { + $v_blocks = $this->newDiffBlocks($vref); + } else { + $v_blocks = array(); + } return id(new PhabricatorDocumentEngineBlocks()) ->addBlockList($uref, $u_blocks) diff --git a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php index d9d4c43abb..0c3e891c3c 100644 --- a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php +++ b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php @@ -36,20 +36,29 @@ final class PhabricatorJupyterDocumentEngine } public function canDiffDocuments( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { return true; } public function newEngineBlocks( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { $blocks = new PhabricatorDocumentEngineBlocks(); try { - $u_blocks = $this->newDiffBlocks($uref); - $v_blocks = $this->newDiffBlocks($vref); + if ($uref) { + $u_blocks = $this->newDiffBlocks($uref); + } else { + $u_blocks = array(); + } + + if ($vref) { + $v_blocks = $this->newDiffBlocks($vref); + } else { + $v_blocks = array(); + } $blocks->addBlockList($uref, $u_blocks); $blocks->addBlockList($vref, $v_blocks); diff --git a/src/applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php new file mode 100644 index 0000000000..63cba16af4 --- /dev/null +++ b/src/applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +setLabel(pht('Targets')) + ->setKey('buildTargetPHIDs') + ->setAliases( + array( + 'buildTargetPHID', + 'buildTargets', + 'buildTarget', + 'targetPHIDs', + 'targetPHID', + 'targets', + 'target', + )) + ->setDescription( + pht('Search for artifacts attached to particular build targets.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['buildTargetPHIDs']) { + $query->withBuildTargetPHIDs($map['buildTargetPHIDs']); + } + + return $query; + } + + protected function getURI($path) { + return '/harbormaster/artifact/'.$path; + } + + protected function getBuiltinQueryNames() { + return array( + 'all' => pht('All Artifacts'), + ); + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $artifacts, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($artifacts, 'HarbormasterBuildArtifact'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + foreach ($artifacts as $artifact) { + $id = $artifact->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Artifact %d', $id)); + + $list->addItem($item); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No artifacts found.')); + } + +} diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php index 7cd8d60b6a..8b4972c154 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php @@ -4,7 +4,8 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO implements PhabricatorPolicyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorConduitResultInterface { protected $buildTargetPHID; protected $artifactType; @@ -18,6 +19,7 @@ final class HarbormasterBuildArtifact public static function initializeNewBuildArtifact( HarbormasterBuildTarget $build_target) { + return id(new HarbormasterBuildArtifact()) ->attachBuildTarget($build_target) ->setBuildTargetPHID($build_target->getPHID()); @@ -53,9 +55,8 @@ final class HarbormasterBuildArtifact ) + parent::getConfiguration(); } - public function generatePHID() { - return PhabricatorPHID::generateNewPHID( - HarbormasterBuildArtifactPHIDType::TYPECONST); + public function getPHIDType() { + return HarbormasterBuildArtifactPHIDType::TYPECONST; } public function attachBuildTarget(HarbormasterBuildTarget $build_target) { @@ -147,7 +148,8 @@ final class HarbormasterBuildArtifact } public function describeAutomaticCapability($capability) { - return pht('Users must be able to see a buildable to see its artifacts.'); + return pht( + 'Users must be able to see a build target to see its artifacts.'); } @@ -165,4 +167,40 @@ final class HarbormasterBuildArtifact $this->saveTransaction(); } + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('buildTargetPHID') + ->setType('phid') + ->setDescription(pht('The build target this artifact is attached to.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('artifactType') + ->setType('string') + ->setDescription(pht('The artifact type.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('artifactKey') + ->setType('string') + ->setDescription(pht('The artifact key.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('isReleased') + ->setType('bool') + ->setDescription(pht('True if this artifact has been released.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'buildTargetPHID' => $this->getBuildTargetPHID(), + 'artifactType' => $this->getArtifactType(), + 'artifactKey' => $this->getArtifactKey(), + 'isReleased' => (bool)$this->getIsReleased(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } } diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 2832c6d9f4..70f6e3d5cb 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -399,7 +399,7 @@ abstract class HeraldAdapter extends Phobject { self::CONDITION_IS_NOT_ANY => pht('is not any of'), self::CONDITION_INCLUDE_ALL => pht('include all of'), self::CONDITION_INCLUDE_ANY => pht('include any of'), - self::CONDITION_INCLUDE_NONE => pht('do not include'), + self::CONDITION_INCLUDE_NONE => pht('include none of'), self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index c5dba7d3b5..b6985268db 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -179,11 +179,14 @@ final class ManiphestTaskDetailController extends ManiphestController { ->addTabGroup($tab_group); } + $changes_view = $this->newChangesView($task, $edges); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( + $changes_view, $tab_view, $timeline, $comment_view, @@ -395,58 +398,6 @@ final class ManiphestTaskDetailController extends ManiphestController { $source)); } - $edge_types = array( - ManiphestTaskHasRevisionEdgeType::EDGECONST - => pht('Differential Revisions'), - ); - - $revisions_commits = array(); - - $commit_phids = array_keys( - $edges[ManiphestTaskHasCommitEdgeType::EDGECONST]); - if ($commit_phids) { - $commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST; - $drev_edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs($commit_phids) - ->withEdgeTypes(array($commit_drev)) - ->execute(); - - foreach ($commit_phids as $phid) { - $revisions_commits[$phid] = $handles->renderHandle($phid) - ->setShowHovercard(true) - ->setShowStateIcon(true); - $revision_phid = key($drev_edges[$phid][$commit_drev]); - $revision_handle = $handles->getHandleIfExists($revision_phid); - if ($revision_handle) { - $task_drev = ManiphestTaskHasRevisionEdgeType::EDGECONST; - unset($edges[$task_drev][$revision_phid]); - $revisions_commits[$phid] = hsprintf( - '%s / %s', - $revision_handle->renderHovercardLink($revision_handle->getName()), - $revisions_commits[$phid]); - } - } - } - - foreach ($edge_types as $edge_type => $edge_name) { - if (!$edges[$edge_type]) { - continue; - } - - $edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type])); - - $edge_list = $edge_handles->renderList() - ->setShowStateIcons(true); - - $view->addProperty($edge_name, $edge_list); - } - - if ($revisions_commits) { - $view->addProperty( - pht('Commits'), - phutil_implode_html(phutil_tag('br'), $revisions_commits)); - } - $field_list->appendFieldsToPropertyList( $task, $viewer, @@ -596,5 +547,291 @@ final class ManiphestTaskDetailController extends ManiphestController { return $handles->newSublist($phids); } + private function newChangesView(ManiphestTask $task, array $edges) { + $viewer = $this->getViewer(); + + $revision_type = ManiphestTaskHasRevisionEdgeType::EDGECONST; + $commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST; + + $revision_phids = idx($edges, $revision_type, array()); + $revision_phids = array_keys($revision_phids); + $revision_phids = array_fuse($revision_phids); + + $commit_phids = idx($edges, $commit_type, array()); + $commit_phids = array_keys($commit_phids); + $commit_phids = array_fuse($commit_phids); + + if (!$revision_phids && !$commit_phids) { + return null; + } + + if ($commit_phids) { + $link_type = DiffusionCommitHasRevisionEdgeType::EDGECONST; + $link_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($commit_phids) + ->withEdgeTypes(array($link_type)); + $link_query->execute(); + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withPHIDs($commit_phids) + ->execute(); + $commits = mpull($commits, null, 'getPHID'); + } else { + $commits = array(); + } + + if ($revision_phids) { + $revisions = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withPHIDs($revision_phids) + ->execute(); + $revisions = mpull($revisions, null, 'getPHID'); + } else { + $revisions = array(); + } + + $handle_phids = array(); + $any_linked = false; + $any_status = false; + + $idx = 0; + $objects = array(); + foreach ($commit_phids as $commit_phid) { + $handle_phids[] = $commit_phid; + + $link_phids = $link_query->getDestinationPHIDs(array($commit_phid)); + foreach ($link_phids as $link_phid) { + $handle_phids[] = $link_phid; + unset($revision_phids[$link_phid]); + $any_linked = true; + } + + $commit = idx($commits, $commit_phid); + if ($commit) { + $repository_phid = $commit->getRepository()->getPHID(); + $handle_phids[] = $repository_phid; + } else { + $repository_phid = null; + } + + $status_view = null; + if ($commit) { + $status = $commit->getAuditStatusObject(); + if (!$status->isNoAudit()) { + $status_view = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setIcon($status->getIcon()) + ->setColor($status->getColor()) + ->setName($status->getName()); + } + } + + $object_link = null; + if ($commit) { + $commit_monogram = $commit->getDisplayName(); + $commit_monogram = phutil_tag( + 'span', + array( + 'class' => 'object-name', + ), + $commit_monogram); + + $commit_link = javelin_tag( + 'a', + array( + 'href' => $commit->getURI(), + 'sigil' => 'hovercard', + 'meta' => array( + 'hoverPHID' => $commit->getPHID(), + ), + ), + $commit->getSummary()); + + $object_link = array( + $commit_monogram, + ' ', + $commit_link, + ); + } + + $objects[] = array( + 'objectPHID' => $commit_phid, + 'objectLink' => $object_link, + 'repositoryPHID' => $repository_phid, + 'revisionPHIDs' => $link_phids, + 'status' => $status_view, + 'order' => id(new PhutilSortVector()) + ->addInt($repository_phid ? 1 : 0) + ->addString((string)$repository_phid) + ->addInt(1) + ->addInt($idx++), + ); + } + + foreach ($revision_phids as $revision_phid) { + $handle_phids[] = $revision_phid; + + $revision = idx($revisions, $revision_phid); + if ($revision) { + $repository_phid = $revision->getRepositoryPHID(); + $handle_phids[] = $repository_phid; + } else { + $repository_phid = null; + } + + if ($revision) { + $icon = $revision->getStatusIcon(); + $color = $revision->getStatusIconColor(); + $name = $revision->getStatusDisplayName(); + + $status_view = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setIcon($icon) + ->setColor($color) + ->setName($name); + } else { + $status_view = null; + } + + $object_link = null; + if ($revision) { + $revision_monogram = $revision->getMonogram(); + $revision_monogram = phutil_tag( + 'span', + array( + 'class' => 'object-name', + ), + $revision_monogram); + + $revision_link = javelin_tag( + 'a', + array( + 'href' => $revision->getURI(), + 'sigil' => 'hovercard', + 'meta' => array( + 'hoverPHID' => $revision->getPHID(), + ), + ), + $revision->getTitle()); + + $object_link = array( + $revision_monogram, + ' ', + $revision_link, + ); + } + + $objects[] = array( + 'objectPHID' => $revision_phid, + 'objectLink' => $object_link, + 'repositoryPHID' => $repository_phid, + 'revisionPHIDs' => array(), + 'status' => $status_view, + 'order' => id(new PhutilSortVector()) + ->addInt($repository_phid ? 1 : 0) + ->addString((string)$repository_phid) + ->addInt(0) + ->addInt($idx++), + ); + } + + $handles = $viewer->loadHandles($handle_phids); + + $order = ipull($objects, 'order'); + $order = msortv($order, 'getSelf'); + $objects = array_select_keys($objects, array_keys($order)); + + $last_repository = false; + $rows = array(); + $rowd = array(); + foreach ($objects as $object) { + $repository_phid = $object['repositoryPHID']; + if ($repository_phid !== $last_repository) { + $repository_link = null; + if ($repository_phid) { + $repository_handle = $handles[$repository_phid]; + $rows[] = array( + $repository_handle->renderLink(), + ); + $rowd[] = true; + } + + $last_repository = $repository_phid; + } + + $object_phid = $object['objectPHID']; + $handle = $handles[$object_phid]; + + $object_link = $object['objectLink']; + if ($object_link === null) { + $object_link = $handle->renderLink(); + } + + $object_icon = id(new PHUIIconView()) + ->setIcon($handle->getIcon()); + + $status_view = $object['status']; + if ($status_view) { + $any_status = true; + } + + $revision_tags = array(); + foreach ($object['revisionPHIDs'] as $link_phid) { + $revision_handle = $handles[$link_phid]; + + $revision_name = $revision_handle->getName(); + $revision_tags[] = $revision_handle + ->renderHovercardLink($revision_name); + } + $revision_tags = phutil_implode_html( + phutil_tag('br'), + $revision_tags); + + $rowd[] = false; + $rows[] = array( + $object_icon, + $status_view, + $revision_tags, + $object_link, + ); + } + + $changes_table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This task has no related commits or revisions.')) + ->setRowDividers($rowd) + ->setColumnClasses( + array( + 'indent center', + null, + null, + 'wide pri object-link', + )) + ->setColumnVisibility( + array( + true, + $any_status, + $any_linked, + true, + )) + ->setDeviceVisibility( + array( + false, + $any_status, + false, + true, + )); + + $changes_header = id(new PHUIHeaderView()) + ->setHeader(pht('Revisions and Commits')); + + $changes_view = id(new PHUIObjectBoxView()) + ->setHeader($changes_header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($changes_table); + + return $changes_view; + } + } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index ed98ad8ad8..01fc0af83d 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -226,7 +226,7 @@ final class ManiphestTransactionEditor $body->addLinkSection( pht('TASK DETAIL'), - PhabricatorEnv::getProductionURI('/T'.$object->getID())); + $this->getObjectLinkButtonURIForMail($object)); $board_phids = array(); diff --git a/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php b/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php index 638d9bfa60..4394331541 100644 --- a/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php +++ b/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php @@ -39,6 +39,15 @@ final class ManiphestTaskPolicyCodex $rules = array(); + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setDescription( + pht('The owner of a task can always view and edit it.')); + $rules[] = $this->newRule() ->setCapabilities( array( diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index c56d8fe57a..4f90de3507 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -358,10 +358,6 @@ final class ManiphestTask extends ManiphestDAO return false; } - public function describeAutomaticCapability($capability) { - return pht('The owner of a task can always view and edit it.'); - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/owners/storage/PhabricatorOwnersPath.php b/src/applications/owners/storage/PhabricatorOwnersPath.php index 5bb32f8407..4e85ffd1b9 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPath.php +++ b/src/applications/owners/storage/PhabricatorOwnersPath.php @@ -91,11 +91,22 @@ final class PhabricatorOwnersPath extends PhabricatorOwnersDAO { } private static function getScalarKeyForRef(array $ref) { + // See T13464. When building refs from raw transactions, the path has + // not been normalized yet and doesn't have a separate "display" path. + // If the "display" path isn't populated, just use the actual path to + // build the ref key. + + if (isset($ref['display'])) { + $display = $ref['display']; + } else { + $display = $ref['path']; + } + return sprintf( 'repository=%s path=%s display=%s excluded=%d', $ref['repositoryPHID'], $ref['path'], - $ref['display'], + $display, $ref['excluded']); } diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php index 91c8d93883..a3a346c20f 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -80,6 +80,7 @@ final class PassphraseCredentialEditController extends PassphraseController { $validation_exception = null; $errors = array(); $e_password = null; + $e_secret = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); @@ -97,22 +98,36 @@ final class PassphraseCredentialEditController extends PassphraseController { $env_secret = new PhutilOpaqueEnvelope($v_secret); $env_password = new PhutilOpaqueEnvelope($v_password); - if ($type->requiresPassword($env_secret)) { + $has_secret = !preg_match('/^('.$bullet.')+$/', trim($v_decrypt)); + + // Validate and repair SSH private keys, and apply passwords if they + // are provided. See T13454 for discussion. + + // This should eventually be refactored to be modular rather than a + // hard-coded set of behaviors here in the Controller, but this is + // likely a fairly extensive change. + + $is_ssh = ($type instanceof PassphraseSSHPrivateKeyTextCredentialType); + + if ($is_ssh && $has_secret) { + $old_object = PhabricatorAuthSSHPrivateKey::newFromRawKey($env_secret); + if (strlen($v_password)) { - $v_decrypt = $type->decryptSecret($env_secret, $env_password); - if ($v_decrypt === null) { - $e_password = pht('Incorrect'); - $errors[] = pht( - 'This key requires a password, but the password you provided '. - 'is incorrect.'); - } else { - $v_decrypt = $v_decrypt->openEnvelope(); + $old_object->setPassphrase($env_password); + } + + try { + $new_object = $old_object->newBarePrivateKey(); + $v_decrypt = $new_object->getKeyBody()->openEnvelope(); + } catch (PhabricatorAuthSSHPrivateKeyException $ex) { + $errors[] = $ex->getMessage(); + + if ($ex->isFormatException()) { + $e_secret = pht('Invalid'); + } + if ($ex->isPassphraseException()) { + $e_password = pht('Invalid'); } - } else { - $e_password = pht('Required'); - $errors[] = pht( - 'This key requires a password. You must provide the password '. - 'for the key.'); } } @@ -166,13 +181,14 @@ final class PassphraseCredentialEditController extends PassphraseController { ->setTransactionType($type_username) ->setNewValue($v_username); } + // If some value other than a sequence of bullets was provided for // the credential, update it. In particular, note that we are // explicitly allowing empty secrets: one use case is HTTP auth where // the username is a secret token which covers both identity and // authentication. - if (!preg_match('/^('.$bullet.')+$/', trim($v_decrypt))) { + if ($has_secret) { // If the credential was previously destroyed, restore it when it is // edited if a secret is provided. $xactions[] = id(new PassphraseCredentialTransaction()) @@ -182,6 +198,7 @@ final class PassphraseCredentialEditController extends PassphraseController { $new_secret = id(new PassphraseSecret()) ->setSecretData($v_decrypt) ->save(); + $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_secret_id) ->setNewValue($new_secret->getID()); @@ -287,7 +304,8 @@ final class PassphraseCredentialEditController extends PassphraseController { ->setName('secret') ->setLabel($type->getSecretLabel()) ->setDisabled($credential_is_locked) - ->setValue($v_secret)); + ->setValue($v_secret) + ->setError($e_secret)); if ($type->shouldShowPasswordField()) { $form->appendChild( diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php index 60cb9bb5ae..58c09fac00 100644 --- a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php @@ -102,35 +102,6 @@ abstract class PassphraseCredentialType extends Phobject { return pht('Password'); } - - /** - * Return true if the provided credential requires a password to decrypt. - * - * @param PhutilOpaqueEnvelope Credential secret value. - * @return bool True if the credential needs a password. - * - * @task password - */ - public function requiresPassword(PhutilOpaqueEnvelope $secret) { - return false; - } - - - /** - * Return the decrypted credential secret, or `null` if the password does - * not decrypt the credential. - * - * @param PhutilOpaqueEnvelope Credential secret value. - * @param PhutilOpaqueEnvelope Credential password. - * @return - * @task password - */ - public function decryptSecret( - PhutilOpaqueEnvelope $secret, - PhutilOpaqueEnvelope $password) { - return $secret; - } - public function shouldRequireUsername() { return true; } diff --git a/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php index fa686aac4c..1abe3b351d 100644 --- a/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php +++ b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php @@ -29,40 +29,4 @@ final class PassphraseSSHPrivateKeyTextCredentialType return pht('Password for Key'); } - public function requiresPassword(PhutilOpaqueEnvelope $secret) { - // According to the internet, this is the canonical test for an SSH private - // key with a password. - return preg_match('/ENCRYPTED/', $secret->openEnvelope()); - } - - public function decryptSecret( - PhutilOpaqueEnvelope $secret, - PhutilOpaqueEnvelope $password) { - - $tmp = new TempFile(); - Filesystem::writeFile($tmp, $secret->openEnvelope()); - - if (!Filesystem::binaryExists('ssh-keygen')) { - throw new Exception( - pht( - 'Decrypting SSH keys requires the `%s` binary, but it '. - 'is not available in %s. Either make it available or strip the '. - 'password from this SSH key manually before uploading it.', - 'ssh-keygen', - '$PATH')); - } - - list($err, $stdout, $stderr) = exec_manual( - 'ssh-keygen -p -P %P -N %s -f %s', - $password, - '', - (string)$tmp); - - if ($err) { - return null; - } else { - return new PhutilOpaqueEnvelope(Filesystem::readFile($tmp)); - } - } - } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 81f427ada8..2bc7a4f1d8 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -89,6 +89,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $this->didVerifyEmail($user, $email); } + id(new DiffusionRepositoryIdentityEngine()) + ->didUpdateEmailAddress($email->getAddress()); + return $this; } @@ -202,11 +205,8 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $user->endWriteLocking(); $user->saveTransaction(); - // Try and match this new address against unclaimed `RepositoryIdentity`s - PhabricatorWorker::scheduleTask( - 'PhabricatorRepositoryIdentityChangeWorker', - array('userPHID' => $user->getPHID()), - array('objectPHID' => $user->getPHID())); + id(new DiffusionRepositoryIdentityEngine()) + ->didUpdateEmailAddress($email->getAddress()); return $this; } @@ -241,7 +241,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { throw new Exception(pht('Email not owned by user!')); } - $email->delete(); + $destruction_engine = id(new PhabricatorDestructionEngine()) + ->setWaitToFinalizeDestruction(true) + ->destroyObject($email); $log = PhabricatorUserLog::initializeNewLog( $actor, @@ -254,6 +256,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $user->saveTransaction(); $this->revokePasswordResetLinks($user); + $destruction_engine->finalizeDestruction(); return $this; } @@ -326,7 +329,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { } $email->sendNewPrimaryEmail($user); - $this->revokePasswordResetLinks($user); return $this; @@ -440,6 +442,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $user->endWriteLocking(); $user->saveTransaction(); + + id(new DiffusionRepositoryIdentityEngine()) + ->didUpdateEmailAddress($email->getAddress()); } diff --git a/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php b/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php new file mode 100644 index 0000000000..e0f0478033 --- /dev/null +++ b/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php @@ -0,0 +1,41 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $email = $objects[$phid]; + $handle->setName($email->getAddress()); + } + + return null; + } + +} diff --git a/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php b/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php new file mode 100644 index 0000000000..6e2627a96d --- /dev/null +++ b/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php @@ -0,0 +1,81 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function newResultObject() { + return new PhabricatorUserEmail(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function getPrimaryTableAlias() { + return 'email'; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'email.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'email.phid IN (%Ls)', + $this->phids); + } + + return $where; + } + + protected function willLoadPage(array $page) { + + $user_phids = mpull($page, 'getUserPHID'); + + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($user_phids) + ->execute(); + $users = mpull($users, null, 'getPHID'); + + foreach ($page as $key => $address) { + $user = idx($users, $address->getUserPHID()); + + if (!$user) { + unset($page[$key]); + $this->didRejectResult($address); + continue; + } + + $address->attachUser($user); + } + + return $page; + } + + public function getQueryApplicationClass() { + return 'PhabricatorPeopleApplication'; + } + +} diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 687b757a3c..6ee6fb7e2e 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1222,7 +1222,7 @@ final class PhabricatorUser 'userPHID = %s', $this->getPHID()); foreach ($emails as $email) { - $email->delete(); + $engine->destroyObject($email); } $sessions = id(new PhabricatorAuthSession())->loadAllWhere( diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php index 572c7d6e8b..4e43b2fb41 100644 --- a/src/applications/people/storage/PhabricatorUserEmail.php +++ b/src/applications/people/storage/PhabricatorUserEmail.php @@ -4,7 +4,11 @@ * @task restrictions Domain Restrictions * @task email Email About Email */ -final class PhabricatorUserEmail extends PhabricatorUserDAO { +final class PhabricatorUserEmail + extends PhabricatorUserDAO + implements + PhabricatorDestructibleInterface, + PhabricatorPolicyInterface { protected $userPHID; protected $address; @@ -12,10 +16,13 @@ final class PhabricatorUserEmail extends PhabricatorUserDAO { protected $isPrimary; protected $verificationCode; + private $user = self::ATTACHABLE; + const MAX_ADDRESS_LENGTH = 128; protected function getConfiguration() { return array( + self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'address' => 'sort128', 'isVerified' => 'bool', @@ -34,6 +41,10 @@ final class PhabricatorUserEmail extends PhabricatorUserDAO { ) + parent::getConfiguration(); } + public function getPHIDType() { + return PhabricatorPeopleUserEmailPHIDType::TYPECONST; + } + public function getVerificationURI() { return '/emailverify/'.$this->getVerificationCode().'/'; } @@ -45,6 +56,15 @@ final class PhabricatorUserEmail extends PhabricatorUserDAO { return parent::save(); } + public function attachUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function getUser() { + return $this->assertAttached($this->user); + } + /* -( Domain Restrictions )------------------------------------------------ */ @@ -271,4 +291,37 @@ final class PhabricatorUserEmail extends PhabricatorUserDAO { return $this; } + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + $user = $this->getUser(); + + if ($this->getIsSystemAgent() || $this->getIsMailingList()) { + return PhabricatorPolicies::POLICY_ADMIN; + } + + return $user->getPHID(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + } diff --git a/src/applications/people/view/PhabricatorUserCardView.php b/src/applications/people/view/PhabricatorUserCardView.php index 21cb468ba8..54d0a41204 100644 --- a/src/applications/people/view/PhabricatorUserCardView.php +++ b/src/applications/people/view/PhabricatorUserCardView.php @@ -38,7 +38,7 @@ final class PhabricatorUserCardView extends AphrontTagView { } return array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ); } diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php index 338b296335..e30a131cff 100644 --- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php @@ -71,21 +71,30 @@ final class PhabricatorUserUsernameTransaction } if (!strlen($new)) { - $errors[] = $this->newRequiredError( - pht('New username is required.'), $xaction); + $errors[] = $this->newInvalidError( + pht('New username is required.'), + $xaction); } else if (!PhabricatorUser::validateUsername($new)) { $errors[] = $this->newInvalidError( - PhabricatorUser::describeValidUsername(), $xaction); + PhabricatorUser::describeValidUsername(), + $xaction); } $user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUsernames(array($new)) ->executeOne(); - if ($user) { - $errors[] = $this->newInvalidError( - pht('Another user already has that username.'), $xaction); + // See T13446. We may be changing the letter case of a username, which + // is a perfectly fine edit. + $is_self = ($user->getPHID() === $object->getPHID()); + if (!$is_self) { + $errors[] = $this->newInvalidError( + pht( + 'Another user already has the username "%s".', + $new), + $xaction); + } } } diff --git a/src/applications/phid/PhabricatorObjectHandle.php b/src/applications/phid/PhabricatorObjectHandle.php index a7ec1f5ddf..6b7f01d596 100644 --- a/src/applications/phid/PhabricatorObjectHandle.php +++ b/src/applications/phid/PhabricatorObjectHandle.php @@ -33,10 +33,6 @@ final class PhabricatorObjectHandle private $commandLineObjectName; private $mailStampName; - private $stateIcon; - private $stateColor; - private $stateName; - public function setIcon($icon) { $this->icon = $icon; return $this; @@ -299,55 +295,6 @@ final class PhabricatorObjectHandle return $this->complete; } - public function setStateIcon($state_icon) { - $this->stateIcon = $state_icon; - return $this; - } - - public function getStateIcon() { - return $this->stateIcon; - } - - public function setStateColor($state_color) { - $this->stateColor = $state_color; - return $this; - } - - public function getStateColor() { - return $this->stateColor; - } - - public function setStateName($state_name) { - $this->stateName = $state_name; - return $this; - } - - public function getStateName() { - return $this->stateName; - } - - public function renderStateIcon() { - $icon = $this->getStateIcon(); - if ($icon === null) { - $icon = 'fa-question-circle-o'; - } - - $color = $this->getStateColor(); - - $name = $this->getStateName(); - if ($name === null) { - $name = pht('Unknown'); - } - - return id(new PHUIIconView()) - ->setIcon($icon, $color) - ->addSigil('has-tooltip') - ->setMetadata( - array( - 'tip' => $name, - )); - } - public function renderLink($name = null) { return $this->renderLinkWithAttributes($name, array()); } diff --git a/src/applications/phid/view/PHUIHandleListView.php b/src/applications/phid/view/PHUIHandleListView.php index 24104fe76d..c5b2f19784 100644 --- a/src/applications/phid/view/PHUIHandleListView.php +++ b/src/applications/phid/view/PHUIHandleListView.php @@ -13,7 +13,6 @@ final class PHUIHandleListView private $handleList; private $asInline; private $asText; - private $showStateIcons; private $glyphLimit; public function setHandleList(PhabricatorHandleList $list) { @@ -39,15 +38,6 @@ final class PHUIHandleListView return $this->asText; } - public function setShowStateIcons($show_state_icons) { - $this->showStateIcons = $show_state_icons; - return $this; - } - - public function getShowStateIcons() { - return $this->showStateIcons; - } - public function setGlyphLimit($glyph_limit) { $this->glyphLimit = $glyph_limit; return $this; @@ -70,7 +60,6 @@ final class PHUIHandleListView protected function getTagContent() { $list = $this->handleList; - $show_state_icons = $this->getShowStateIcons(); $glyph_limit = $this->getGlyphLimit(); $items = array(); @@ -79,10 +68,6 @@ final class PHUIHandleListView ->setShowHovercard(true) ->setAsText($this->getAsText()); - if ($show_state_icons) { - $view->setShowStateIcon(true); - } - if ($glyph_limit) { $view->setGlyphLimit($glyph_limit); } diff --git a/src/applications/phid/view/PHUIHandleView.php b/src/applications/phid/view/PHUIHandleView.php index fe3c62a9ac..6cdf84f391 100644 --- a/src/applications/phid/view/PHUIHandleView.php +++ b/src/applications/phid/view/PHUIHandleView.php @@ -17,7 +17,6 @@ final class PHUIHandleView private $asText; private $useShortName; private $showHovercard; - private $showStateIcon; private $glyphLimit; public function setHandleList(PhabricatorHandleList $list) { @@ -50,15 +49,6 @@ final class PHUIHandleView return $this; } - public function setShowStateIcon($show_state_icon) { - $this->showStateIcon = $show_state_icon; - return $this; - } - - public function getShowStateIcon() { - return $this->showStateIcon; - } - public function setGlyphLimit($glyph_limit) { $this->glyphLimit = $glyph_limit; return $this; @@ -104,11 +94,6 @@ final class PHUIHandleView $link = $handle->renderLink($name); } - if ($this->showStateIcon) { - $icon = $handle->renderStateIcon(); - $link = array($icon, ' ', $link); - } - return $link; } diff --git a/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php b/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php index 748bec705b..f4536cad7c 100644 --- a/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php +++ b/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php @@ -41,23 +41,6 @@ final class PhrictionDocumentPolicyCodex ->executeOne(); } - public function compareToDefaultPolicy(PhabricatorPolicy $policy) { - $root_policy = $this->getDefaultPolicy(); - $strongest_policy = $this->getStrongestPolicy(); - - // Note that we never return 'weaker', because Phriction documents can - // never have weaker permissions than their parents. If this object has - // been set to weaker permissions anyway, return 'adjusted'. - if ($root_policy == $strongest_policy) { - $strength = null; - } else if ($strongest_policy->isStrongerThan($root_policy)) { - $strength = PhabricatorPolicyStrengthConstants::STRONGER; - } else { - $strength = PhabricatorPolicyStrengthConstants::ADJUSTED; - } - return $strength; - } - private function getStrongestPolicy() { $ancestors = $this->getObject()->getAncestors(); $ancestors[] = $this->getObject(); diff --git a/src/applications/phriction/markup/PhrictionRemarkupRule.php b/src/applications/phriction/markup/PhrictionRemarkupRule.php index d17de331e5..11994e0aa5 100644 --- a/src/applications/phriction/markup/PhrictionRemarkupRule.php +++ b/src/applications/phriction/markup/PhrictionRemarkupRule.php @@ -16,8 +16,23 @@ final class PhrictionRemarkupRule extends PhutilRemarkupRule { } public function markupDocumentLink(array $matches) { + $name = trim(idx($matches, 2, '')); + if (empty($matches[2])) { + $name = null; + } + + $path = trim($matches[1]); + + if (!$this->isFlatText($name)) { + return $matches[0]; + } + + if (!$this->isFlatText($path)) { + return $matches[0]; + } + // If the link contains an anchor, separate that off first. - $parts = explode('#', trim($matches[1]), 2); + $parts = explode('#', $path, 2); if (count($parts) == 2) { $link = $parts[0]; $anchor = $parts[1]; @@ -48,11 +63,6 @@ final class PhrictionRemarkupRule extends PhutilRemarkupRule { } } - $name = trim(idx($matches, 2, '')); - if (empty($matches[2])) { - $name = null; - } - // Link is now used for slug detection, so append a slash if one // is needed. $link = rtrim($link, '/').'/'; diff --git a/src/applications/policy/codex/PhabricatorPolicyCodex.php b/src/applications/policy/codex/PhabricatorPolicyCodex.php index 8dee2a38d1..9bccb842bb 100644 --- a/src/applications/policy/codex/PhabricatorPolicyCodex.php +++ b/src/applications/policy/codex/PhabricatorPolicyCodex.php @@ -40,10 +40,6 @@ abstract class PhabricatorPolicyCodex $this->capability); } - public function compareToDefaultPolicy(PhabricatorPolicy $policy) { - return null; - } - final protected function newRule() { return new PhabricatorPolicyCodexRuleDescription(); } diff --git a/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php b/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php deleted file mode 100644 index 9bc3c81ca2..0000000000 --- a/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php +++ /dev/null @@ -1,9 +0,0 @@ -getViewer(); - - - $strength = null; - if ($object instanceof PhabricatorPolicyCodexInterface) { - $codex = id(PhabricatorPolicyCodex::newFromObject($object, $viewer)) - ->setCapability($capability); - $strength = $codex->compareToDefaultPolicy($policy); - $default_policy = $codex->getDefaultPolicy(); - } else { - $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject( - $viewer, - $object, - $capability); - - if ($default_policy) { - if ($default_policy->getPHID() == $policy->getPHID()) { - return; - } - - if ($default_policy->getPHID() != $policy->getPHID()) { - if ($default_policy->isStrongerThan($policy)) { - $strength = PhabricatorPolicyStrengthConstants::WEAKER; - } else if ($policy->isStrongerThan($default_policy)) { - $strength = PhabricatorPolicyStrengthConstants::STRONGER; - } else { - $strength = PhabricatorPolicyStrengthConstants::ADJUSTED; - } - } - } - } - - if (!$strength) { - return; - } - - if ($strength == PhabricatorPolicyStrengthConstants::WEAKER) { - $info = pht( - 'This object has a less restrictive policy ("%s") than the default '. - 'policy for similar objects (which is "%s").', - $policy->getShortName(), - $default_policy->getShortName()); - } else if ($strength == PhabricatorPolicyStrengthConstants::STRONGER) { - $info = pht( - 'This object has a more restrictive policy ("%s") than the default '. - 'policy for similar objects (which is "%s").', - $policy->getShortName(), - $default_policy->getShortName()); - } else { - $info = pht( - 'This object has a different policy ("%s") than the default policy '. - 'for similar objects (which is "%s").', - $policy->getShortName(), - $default_policy->getShortName()); - } - - return $info; - } - private function getCapabilityName($capability) { $capability_name = $capability; $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); @@ -344,11 +281,6 @@ final class PhabricatorPolicyExplainController $object_section->appendRulesView($rules_view); } - $strength = $this->getStrengthInformation($object, $policy, $capability); - if ($strength) { - $object_section->appendHint($strength); - } - return $object_section; } diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php index 186ac7dea4..a31bf8853c 100644 --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -1537,8 +1537,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { PhabricatorProject $parent = null, $is_milestone = false) { - $project = PhabricatorProject::initializeNewProject($user); - + $project = PhabricatorProject::initializeNewProject($user, $parent); $name = pht('Test Project %d', mt_rand()); diff --git a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php index 1296f2eec8..16760d515f 100644 --- a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php +++ b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php @@ -53,7 +53,11 @@ final class PhabricatorProjectBurndownChartEngine $open_function = $this->newFunction( array( 'accumulate', - array('fact', 'tasks.open-count.create'), + array( + 'sum', + array('fact', 'tasks.open-count.create'), + array('fact', 'tasks.open-count.status'), + ), )); $closed_function = $this->newFunction( diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 386a649238..54e1d77d55 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -63,14 +63,14 @@ final class PhabricatorProjectProfileController $member_list = id(new PhabricatorProjectMemberListView()) ->setUser($viewer) ->setProject($project) - ->setLimit(5) + ->setLimit(10) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUserPHIDs($project->getMemberPHIDs()); $watcher_list = id(new PhabricatorProjectWatcherListView()) ->setUser($viewer) ->setProject($project) - ->setLimit(5) + ->setLimit(10) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUserPHIDs($project->getWatcherPHIDs()); diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index b714f66830..eb57c39b2c 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -336,40 +336,68 @@ final class PhabricatorProjectTransactionEditor $type_edge = PhabricatorTransactions::TYPE_EDGE; $edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; - $member_xaction = null; - foreach ($xactions as $xaction) { - if ($xaction->getTransactionType() !== $type_edge) { - continue; - } - - $edgetype = $xaction->getMetadataValue('edge:type'); - if ($edgetype !== $edgetype_member) { - continue; - } - - $member_xaction = $xaction; + // See T13462. If we're creating a milestone, set a dummy milestone + // number so the project behaves like a milestone and uses milestone + // policy rules. Otherwise, we'll end up checking the default policies + // (which are not relevant to milestones) instead of the parent project + // policies (which are the correct policies). + if ($this->getIsMilestone() && !$copy->isMilestone()) { + $copy->setMilestoneNumber(1); } - if ($member_xaction) { - $object_phid = $object->getPHID(); + $hint = null; + if ($this->getIsMilestone()) { + // See T13462. If we're creating a milestone, predict that the members + // of the newly created milestone will be the same as the members of the + // parent project, since this is the governing rule. - if ($object_phid) { - $project = id(new PhabricatorProjectQuery()) - ->setViewer($this->getActor()) - ->withPHIDs(array($object_phid)) - ->needMembers(true) - ->executeOne(); - $members = $project->getMemberPHIDs(); - } else { - $members = array(); + $parent = $copy->getParentProject(); + + $parent = id(new PhabricatorProjectQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($parent->getPHID())) + ->needMembers(true) + ->executeOne(); + $members = $parent->getMemberPHIDs(); + + $hint = array_fuse($members); + } else { + $member_xaction = null; + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() !== $type_edge) { + continue; + } + + $edgetype = $xaction->getMetadataValue('edge:type'); + if ($edgetype !== $edgetype_member) { + continue; + } + + $member_xaction = $xaction; } - $clone_xaction = clone $member_xaction; - $hint = $this->getPHIDTransactionNewValue($clone_xaction, $members); + if ($member_xaction) { + $object_phid = $object->getPHID(); + + if ($object_phid) { + $project = id(new PhabricatorProjectQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($object_phid)) + ->needMembers(true) + ->executeOne(); + $members = $project->getMemberPHIDs(); + } else { + $members = array(); + } + + $clone_xaction = clone $member_xaction; + $hint = $this->getPHIDTransactionNewValue($clone_xaction, $members); + $hint = array_fuse($hint); + } + } + + if ($hint !== null) { $rule = new PhabricatorProjectMembersPolicyRule(); - - $hint = array_fuse($hint); - PhabricatorPolicyRule::passTransactionHintToRule( $copy, $rule, diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 22aaed2d4b..a0be7e5775 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -301,6 +301,12 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->execute(); $boards = mpull($boards, null, 'getPHID'); + foreach ($boards as $key => $board) { + if (!($board instanceof PhabricatorWorkboardInterface)) { + unset($boards[$key]); + } + } + if (!$this->fetchAllBoards) { foreach ($boards as $key => $board) { if (!$board->getHasWorkboard()) { diff --git a/src/applications/project/interface/PhabricatorWorkboardInterface.php b/src/applications/project/interface/PhabricatorWorkboardInterface.php new file mode 100644 index 0000000000..2b760b4875 --- /dev/null +++ b/src/applications/project/interface/PhabricatorWorkboardInterface.php @@ -0,0 +1,3 @@ +setLabel(pht('Root Projects')) + ->setKey('isRoot') + ->setOptions( + pht('(Show All)'), + pht('Show Only Root Projects'), + pht('Hide Root Projects')) + ->setDescription( + pht( + 'Pass true to find only root projects, or false to omit '. + 'root projects.')), + id(new PhabricatorSearchIntField()) + ->setLabel(pht('Minimum Depth')) + ->setKey('minDepth') + ->setIsHidden(true) + ->setDescription( + pht( + 'Find projects with a given minimum depth. Root projects '. + 'have depth 0, their immediate children have depth 1, and '. + 'so on.')), + id(new PhabricatorSearchIntField()) + ->setLabel(pht('Maximum Depth')) + ->setKey('maxDepth') + ->setIsHidden(true) + ->setDescription( + pht( + 'Find projects with a given maximum depth. Root projects '. + 'have depth 0, their immediate children have depth 1, and '. + 'so on.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Subtypes')) ->setKey('subtypes') @@ -137,6 +166,42 @@ final class PhabricatorProjectSearchEngine $query->withIsMilestone($map['isMilestone']); } + $min_depth = $map['minDepth']; + $max_depth = $map['maxDepth']; + + if ($min_depth !== null || $max_depth !== null) { + if ($min_depth !== null && $max_depth !== null) { + if ($min_depth > $max_depth) { + throw new Exception( + pht( + 'Search constraint "minDepth" must be no larger than '. + 'search constraint "maxDepth".')); + } + } + } + + if ($map['isRoot'] !== null) { + if ($map['isRoot']) { + if ($max_depth === null) { + $max_depth = 0; + } else { + $max_depth = min(0, $max_depth); + } + + $query->withDepthBetween(null, 0); + } else { + if ($min_depth === null) { + $min_depth = 1; + } else { + $min_depth = max($min_depth, 1); + } + } + } + + if ($min_depth !== null || $max_depth !== null) { + $query->withDepthBetween($min_depth, $max_depth); + } + if ($map['parentPHIDs']) { $query->withParentProjectPHIDs($map['parentPHIDs']); } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 9bd21606e1..91f1727640 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -13,7 +13,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO PhabricatorConduitResultInterface, PhabricatorColumnProxyInterface, PhabricatorSpacesInterface, - PhabricatorEditEngineSubtypeInterface { + PhabricatorEditEngineSubtypeInterface, + PhabricatorWorkboardInterface { protected $name; protected $status = PhabricatorProjectStatus::STATUS_ACTIVE; @@ -106,7 +107,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO ->setHasMilestones(0) ->setHasSubprojects(0) ->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT) - ->attachParentProject(null); + ->attachParentProject($parent); } public function getCapabilities() { diff --git a/src/applications/project/view/PhabricatorProjectCardView.php b/src/applications/project/view/PhabricatorProjectCardView.php index f82ee8c99e..a56697ba7e 100644 --- a/src/applications/project/view/PhabricatorProjectCardView.php +++ b/src/applications/project/view/PhabricatorProjectCardView.php @@ -36,7 +36,7 @@ final class PhabricatorProjectCardView extends AphrontTagView { $classes[] = 'project-card-'.$color; return array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ); } diff --git a/src/applications/project/view/PhabricatorProjectUserListView.php b/src/applications/project/view/PhabricatorProjectUserListView.php index 51c2ced6d1..dd3e084c6e 100644 --- a/src/applications/project/view/PhabricatorProjectUserListView.php +++ b/src/applications/project/view/PhabricatorProjectUserListView.php @@ -1,6 +1,7 @@ getUserPHIDs(); $can_edit = $this->canEditList(); + $supports_edit = $project->supportsEditMembers(); $no_data = $this->getNoDataString(); $list = id(new PHUIObjectItemListView()) ->setNoDataString($no_data); $limit = $this->getLimit(); - - // If we're showing everything, show oldest to newest. If we're showing - // only a slice, show newest to oldest. - if (!$limit) { - $user_phids = array_reverse($user_phids); - } + $is_panel = (bool)$limit; $handles = $viewer->loadHandles($user_phids); - // Always put the viewer first if they are on the list. - $user_phids = array_fuse($user_phids); - $user_phids = - array_select_keys($user_phids, array($viewer->getPHID())) + - $user_phids; + // Reorder users in display order. We're going to put the viewer first + // if they're a member, then enabled users, then disabled/invalid users. - if ($limit) { - $render_phids = array_slice($user_phids, 0, $limit); - } else { - $render_phids = $user_phids; - } - - foreach ($render_phids as $user_phid) { + $phid_map = array(); + foreach ($user_phids as $user_phid) { $handle = $handles[$user_phid]; + $is_viewer = ($user_phid === $viewer->getPHID()); + $is_enabled = ($handle->isComplete() && !$handle->isDisabled()); + + // If we're showing the main member list, show oldest to newest. If we're + // showing only a slice in a panel, show newest to oldest. + if ($limit) { + $order_scalar = 1; + } else { + $order_scalar = -1; + } + + $phid_map[$user_phid] = id(new PhutilSortVector()) + ->addInt($is_viewer ? 0 : 1) + ->addInt($is_enabled ? 0 : 1) + ->addInt($order_scalar * count($phid_map)); + } + $phid_map = msortv($phid_map, 'getSelf'); + + $handles = iterator_to_array($handles); + $handles = array_select_keys($handles, array_keys($phid_map)); + + if ($limit) { + $handles = array_slice($handles, 0, $limit); + } + + foreach ($handles as $user_phid => $handle) { $item = id(new PHUIObjectItemView()) ->setHeader($handle->getFullName()) ->setHref($handle->getURI()) ->setImageURI($handle->getImageURI()); - $icon = id(new PHUIIconView()) - ->setIcon($handle->getIcon()); + if ($handle->isDisabled()) { + if ($is_panel) { + // Don't show disabled users in the panel view at all. + continue; + } - $subtitle = $handle->getSubtitle(); + $item + ->setDisabled(true) + ->addAttribute(pht('Disabled')); + } else { + $icon = id(new PHUIIconView()) + ->setIcon($handle->getIcon()); - $item->addAttribute(array($icon, ' ', $subtitle)); + $subtitle = $handle->getSubtitle(); - if ($can_edit && !$limit) { + $item->addAttribute(array($icon, ' ', $subtitle)); + } + + if ($supports_edit && !$is_panel) { $remove_uri = $this->getRemoveURI($user_phid); $item->addAction( @@ -107,6 +133,7 @@ abstract class PhabricatorProjectUserListView extends AphrontView { ->setIcon('fa-times') ->setName(pht('Remove')) ->setHref($remove_uri) + ->setDisabled(!$can_edit) ->setWorkflow(true)); } @@ -128,14 +155,9 @@ abstract class PhabricatorProjectUserListView extends AphrontView { ->setHeader($header_text); if ($limit) { - $header->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setIcon( - id(new PHUIIconView()) - ->setIcon('fa-list-ul')) - ->setText(pht('View All')) - ->setHref("/project/members/{$id}/")); + $list->newTailButton() + ->setText(pht('View All')) + ->setHref("/project/members/{$id}/"); } $box = id(new PHUIObjectBoxView()) diff --git a/src/applications/repository/data/PhabricatorRepositoryURINormalizer.php b/src/applications/repository/data/PhabricatorRepositoryURINormalizer.php index a29d110a67..935d78fea5 100644 --- a/src/applications/repository/data/PhabricatorRepositoryURINormalizer.php +++ b/src/applications/repository/data/PhabricatorRepositoryURINormalizer.php @@ -130,10 +130,35 @@ final class PhabricatorRepositoryURINormalizer extends Phobject { $domain = $uri->getDomain(); if (!strlen($domain)) { - $domain = ''; + return ''; } - return phutil_utf8_strtolower($domain); + $domain = phutil_utf8_strtolower($domain); + + // See T13435. If the domain for a repository URI is same as the install + // base URI, store it as a "" token instead of the actual domain + // so that the index does not fall out of date if the install moves. + + $base_uri = PhabricatorEnv::getURI('/'); + $base_uri = new PhutilURI($base_uri); + $base_domain = $base_uri->getDomain(); + $base_domain = phutil_utf8_strtolower($base_domain); + if ($domain === $base_domain) { + return ''; + } + + // Likewise, store a token for the "SSH Host" domain so it can be changed + // without requiring an index rebuild. + + $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host'); + if (strlen($ssh_host)) { + $ssh_host = phutil_utf8_strtolower($ssh_host); + if ($domain === $ssh_host) { + return ''; + } + } + + return $domain; } diff --git a/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php b/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php index 81dd735562..8ab54a23a4 100644 --- a/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php +++ b/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php @@ -31,6 +31,36 @@ final class PhabricatorRepositoryURINormalizerTestCase } } + public function testDomainURINormalizer() { + $base_domain = 'base.phabricator.example.com'; + $ssh_domain = 'ssh.phabricator.example.com'; + + $env = PhabricatorEnv::beginScopedEnv(); + $env->overrideEnvConfig('phabricator.base-uri', 'http://'.$base_domain); + $env->overrideEnvConfig('diffusion.ssh-host', $ssh_domain); + + $cases = array( + '/' => '', + '/path/to/local/repo.git' => '', + 'ssh://user@domain.com/path.git' => 'domain.com', + 'ssh://user@DOMAIN.COM/path.git' => 'domain.com', + 'http://'.$base_domain.'/diffusion/X/' => '', + 'ssh://'.$ssh_domain.'/diffusion/X/' => '', + 'git@'.$ssh_domain.':bananas.git' => '', + ); + + $type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; + + foreach ($cases as $input => $expect) { + $normal = new PhabricatorRepositoryURINormalizer($type_git, $input); + + $this->assertEqual( + $expect, + $normal->getNormalizedDomain(), + pht('Normalized domain for "%s".', $input)); + } + } + public function testSVNURINormalizer() { $cases = array( 'file:///path/to/repo' => 'path/to/repo', diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index ea70f380aa..2b008b630b 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -257,16 +257,15 @@ final class PhabricatorRepositoryPullEngine $path = rtrim($repository->getLocalPath(), '/'); - if ($repository->isHosted()) { - $repository->execxRemoteCommand( - 'init --bare -- %s', - $path); - } else { - $repository->execxRemoteCommand( - 'clone --bare -- %P %s', - $repository->getRemoteURIEnvelope(), - $path); - } + // See T13448. In all cases, we create repositories by using "git init" + // to build a bare, empty working copy. If we try to use "git clone" + // instead, we'll pull in too many refs if "Fetch Refs" is also + // configured. There's no apparent way to make "git clone" behave narrowly + // and no apparent reason to bother. + + $repository->execxRemoteCommand( + 'init --bare -- %s', + $path); } @@ -290,29 +289,27 @@ final class PhabricatorRepositoryPullEngine $files = Filesystem::listDirectory($path, $include_hidden = true); if (!$files) { $message = pht( - "Expected to find a git repository at '%s', but there ". - "is an empty directory there. Remove the directory: the daemon ". - "will run '%s' for you.", - $path, - 'git clone'); + 'Expected to find a Git repository at "%s", but there is an '. + 'empty directory there. Remove the directory. A daemon will '. + 'construct the working copy for you.', + $path); } else { $message = pht( - "Expected to find a git repository at '%s', but there is ". - "a non-repository directory (with other stuff in it) there. Move ". - "or remove this directory (or reconfigure the repository to use a ". - "different directory), and then either clone a repository ". - "yourself or let the daemon do it.", + 'Expected to find a Git repository at "%s", but there is '. + 'a non-repository directory (with other stuff in it) there. '. + 'Move or remove this directory. A daemon will construct '. + 'the working copy for you.', $path); } } else if (is_file($path)) { $message = pht( - "Expected to find a git repository at '%s', but there is a ". - "file there instead. Remove it and let the daemon clone a ". - "repository for you.", + 'Expected to find a Git repository at "%s", but there is a '. + 'file there instead. Move or remove this file. A daemon will '. + 'construct the working copy for you.', $path); } else { $message = pht( - "Expected to find a git repository at '%s', but did not.", + 'Expected to find a git repository at "%s", but did not.', $path); } } else { @@ -327,11 +324,10 @@ final class PhabricatorRepositoryPullEngine } else if (!Filesystem::pathsAreEquivalent($repo_path, $path)) { $err = true; $message = pht( - "Expected to find repo at '%s', but the actual git repository root ". - "for this directory is '%s'. Something is misconfigured. ". - "The repository's 'Local Path' should be set to some place where ". - "the daemon can check out a working copy, ". - "and should not be inside another git repository.", + 'Expected to find a Git repository at "%s", but the actual Git '. + 'repository root for this directory is "%s". Something is '. + 'misconfigured. This directory should be writable by the daemons '. + 'and not inside another Git repository.', $path, $repo_path); } @@ -353,13 +349,56 @@ final class PhabricatorRepositoryPullEngine // Load the refs we're planning to fetch from the remote repository. $remote_refs = $this->loadGitRemoteRefs( $repository, - $repository->getRemoteURIEnvelope()); + $repository->getRemoteURIEnvelope(), + $is_local = false); // Load the refs we're planning to fetch from the local repository, by // using the local working copy path as the "remote" repository URI. $local_refs = $this->loadGitRemoteRefs( $repository, - new PhutilOpaqueEnvelope($path)); + new PhutilOpaqueEnvelope($path), + $is_local = true); + + // See T13448. The "git fetch --prune ..." flag only prunes local refs + // matching the refspecs we pass it. If "Fetch Refs" is configured, we'll + // pass it a very narrow list of refspecs, and it won't prune older refs + // that aren't currently subject to fetching. + + // Since we want to prune everything that isn't (a) on the fetch list and + // (b) in the remote, handle pruning of any surplus leftover refs ourselves + // before we fetch anything. + + // (We don't have to do this if "Fetch Refs" isn't set up, since "--prune" + // will work in that case, but it's a little simpler to always go down the + // same code path.) + + $surplus_refs = array(); + foreach ($local_refs as $local_ref => $local_hash) { + $remote_hash = idx($remote_refs, $local_ref); + if ($remote_hash === null) { + $surplus_refs[] = $local_ref; + } + } + + if ($surplus_refs) { + $this->log( + pht( + 'Found %s surplus local ref(s) to delete.', + phutil_count($surplus_refs))); + foreach ($surplus_refs as $surplus_ref) { + $this->log( + pht( + 'Deleting surplus local ref "%s" ("%s").', + $surplus_ref, + $local_refs[$surplus_ref])); + + $repository->execLocalCommand( + 'update-ref -d %R --', + $surplus_ref); + + unset($local_refs[$surplus_ref]); + } + } if ($remote_refs === $local_refs) { $this->log( @@ -378,7 +417,7 @@ final class PhabricatorRepositoryPullEngine // checked out. See T13280. $future = $repository->getRemoteCommandFuture( - 'fetch --prune --update-head-ok -- %P %Ls', + 'fetch --no-tags --update-head-ok -- %P %Ls', $repository->getRemoteURIEnvelope(), $fetch_rules); @@ -474,21 +513,32 @@ final class PhabricatorRepositoryPullEngine private function loadGitRemoteRefs( PhabricatorRepository $repository, - PhutilOpaqueEnvelope $remote_envelope) { + PhutilOpaqueEnvelope $remote_envelope, + $is_local) { - $ref_rules = $this->getGitRefRules($repository); + // See T13448. When listing local remotes, we want to list everything, + // not just refs we expect to fetch. This allows us to detect that we have + // undesirable refs (which have been deleted in the remote, but are still + // present locally) so we can update our state to reflect the correct + // remote state. - // NOTE: "git ls-remote" does not support "--" until circa January 2016. - // See T12416. None of the flags to "ls-remote" appear dangerous, but - // refuse to list any refs beginning with "-" just in case. + if ($is_local) { + $ref_rules = array(); + } else { + $ref_rules = $this->getGitRefRules($repository); - foreach ($ref_rules as $ref_rule) { - if (preg_match('/^-/', $ref_rule)) { - throw new Exception( - pht( - 'Refusing to list potentially dangerous ref ("%s") beginning '. - 'with "-".', - $ref_rule)); + // NOTE: "git ls-remote" does not support "--" until circa January 2016. + // See T12416. None of the flags to "ls-remote" appear dangerous, but + // refuse to list any refs beginning with "-" just in case. + + foreach ($ref_rules as $ref_rule) { + if (preg_match('/^-/', $ref_rule)) { + throw new Exception( + pht( + 'Refusing to list potentially dangerous ref ("%s") beginning '. + 'with "-".', + $ref_rule)); + } } } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php index e02a8dc05c..ec65a8bcfa 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php @@ -99,7 +99,6 @@ final class PhabricatorRepositoryManagementLookupUsersWorkflow private function resolveUser(PhabricatorRepositoryCommit $commit, $name) { $phid = id(new DiffusionResolveUserQuery()) - ->withCommit($commit) ->withName($name) ->execute(); diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php index 86cdcaa462..bd906aa5da 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php @@ -3,6 +3,10 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow extends PhabricatorRepositoryManagementWorkflow { + private $identityCache = array(); + private $phidCache = array(); + private $dryRun; + protected function didConstruct() { $this ->setName('rebuild-identities') @@ -12,38 +16,189 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow ->setArguments( array( array( - 'name' => 'repositories', - 'wildcard' => true, + 'name' => 'all-repositories', + 'help' => pht('Rebuild identities across all repositories.'), ), array( - 'name' => 'all', - 'help' => pht('Rebuild identities across all repositories.'), - ), + 'name' => 'all-identities', + 'help' => pht('Rebuild all currently-known identities.'), + ), + array( + 'name' => 'repository', + 'param' => 'repository', + 'repeat' => true, + 'help' => pht('Rebuild identities in a repository.'), + ), + array( + 'name' => 'commit', + 'param' => 'commit', + 'repeat' => true, + 'help' => pht('Rebuild identities for a commit.'), + ), + array( + 'name' => 'user', + 'param' => 'user', + 'repeat' => true, + 'help' => pht('Rebuild identities for a user.'), + ), + array( + 'name' => 'email', + 'param' => 'email', + 'repeat' => true, + 'help' => pht('Rebuild identities for an email address.'), + ), + array( + 'name' => 'raw', + 'param' => 'raw', + 'repeat' => true, + 'help' => pht('Rebuild identities for a raw commit string.'), + ), + array( + 'name' => 'dry-run', + 'help' => pht('Show changes, but do not make any changes.'), + ), )); } public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); + $viewer = $this->getViewer(); - $all = $args->getArg('all'); - $repositories = $args->getArg('repositories'); + $rebuilt_anything = false; - if ($all xor empty($repositories)) { + $all_repositories = $args->getArg('all-repositories'); + $repositories = $args->getArg('repository'); + + if ($all_repositories && $repositories) { throw new PhutilArgumentUsageException( - pht('Specify --all or a list of repositories, but not both.')); + pht( + 'Flags "--all-repositories" and "--repository" are not '. + 'compatible.')); } - $query = id(new DiffusionCommitQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->needCommitData(true); - if ($repositories) { - $repos = $this->loadRepositories($args, 'repositories'); - $query->withRepositoryIDs(mpull($repos, 'getID')); + $all_identities = $args->getArg('all-identities'); + $raw = $args->getArg('raw'); + + if ($all_identities && $raw) { + throw new PhutilArgumentUsageException( + pht( + 'Flags "--all-identities" and "--raw" are not '. + 'compatible.')); } - $iterator = new PhabricatorQueryIterator($query); - foreach ($iterator as $commit) { + $dry_run = $args->getArg('dry-run'); + $this->dryRun = $dry_run; + + if ($this->dryRun) { + $this->logWarn( + pht('DRY RUN'), + pht('This is a dry run, so no changes will be written.')); + } + + if ($all_repositories || $repositories) { + $rebuilt_anything = true; + + if ($repositories) { + $repository_list = $this->loadRepositories($args, 'repository'); + } else { + $repository_query = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer); + $repository_list = new PhabricatorQueryIterator($repository_query); + } + + foreach ($repository_list as $repository) { + $commit_query = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->needCommitData(true) + ->withRepositoryIDs(array($repository->getID())); + + // See T13457. Adjust ordering to hit keys better and tweak page size + // to improve performance slightly, since these records are small. + $commit_query->setOrderVector(array('-epoch', '-id')); + + $commit_iterator = id(new PhabricatorQueryIterator($commit_query)) + ->setPageSize(1000); + + $this->rebuildCommits($commit_iterator); + } + } + + $commits = $args->getArg('commit'); + if ($commits) { + $rebuilt_anything = true; + $commit_list = $this->loadCommits($args, 'commit'); + + // Reload commits to get commit data. + $commit_list = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->needCommitData(true) + ->withIDs(mpull($commit_list, 'getID')) + ->execute(); + + $this->rebuildCommits($commit_list); + } + + $users = $args->getArg('user'); + if ($users) { + $rebuilt_anything = true; + + $user_list = $this->loadUsersFromArguments($users); + $this->rebuildUsers($user_list); + } + + $emails = $args->getArg('email'); + if ($emails) { + $rebuilt_anything = true; + $this->rebuildEmails($emails); + } + + if ($all_identities || $raw) { + $rebuilt_anything = true; + + if ($raw) { + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withIdentityNames($raw) + ->execute(); + + $identities = mpull($identities, null, 'getIdentityNameRaw'); + foreach ($raw as $raw_identity) { + if (!isset($identities[$raw_identity])) { + throw new PhutilArgumentUsageException( + pht( + 'No identity "%s" exists. When selecting identities with '. + '"--raw", the entire identity must match exactly.', + $raw_identity)); + } + } + + $identity_list = $identities; + } else { + $identity_query = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer); + + $identity_list = new PhabricatorQueryIterator($identity_query); + + $this->logInfo( + pht('REBUILD'), + pht('Rebuilding all existing identities.')); + } + + $this->rebuildIdentities($identity_list); + } + + if (!$rebuilt_anything) { + throw new PhutilArgumentUsageException( + pht( + 'Nothing specified to rebuild. Use flags to choose which '. + 'identities to rebuild, or "--help" for help.')); + } + + return 0; + } + + private function rebuildCommits($commits) { + foreach ($commits as $commit) { $needs_update = false; $data = $commit->getCommitData(); @@ -55,6 +210,8 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow $author_phid = $commit->getAuthorIdentityPHID(); $identity_phid = $author_identity->getPHID(); + + $aidentity_phid = $identity_phid; if ($author_phid !== $identity_phid) { $commit->setAuthorIdentityPHID($identity_phid); $data->setCommitDetail('authorIdentityPHID', $identity_phid); @@ -81,45 +238,196 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow if ($needs_update) { $commit->save(); $data->save(); - echo tsprintf( - "Rebuilt identities for %s.\n", - $commit->getDisplayName()); + + $this->logInfo( + pht('COMMIT'), + pht( + 'Rebuilt identities for "%s".', + $commit->getDisplayName())); } else { - echo tsprintf( - "No changes for %s.\n", - $commit->getDisplayName()); + $this->logInfo( + pht('SKIP'), + pht( + 'No changes for commit "%s".', + $commit->getDisplayName())); } } - } private function getIdentityForCommit( - PhabricatorRepositoryCommit $commit, $identity_name) { + PhabricatorRepositoryCommit $commit, + $raw_identity) { - static $seen = array(); - $identity_key = PhabricatorHash::digestForIndex($identity_name); - if (empty($seen[$identity_key])) { - try { - $user_phid = id(new DiffusionResolveUserQuery()) - ->withCommit($commit) - ->withName($identity_name) - ->execute(); + if (!isset($this->identityCache[$raw_identity])) { + $identity = $this->newIdentityEngine() + ->setSourcePHID($commit->getPHID()) + ->newResolvedIdentity($raw_identity); - $identity = id(new PhabricatorRepositoryIdentity()) - ->setAuthorPHID($commit->getPHID()) - ->setIdentityName($identity_name) - ->setAutomaticGuessedUserPHID($user_phid) - ->save(); - } catch (AphrontDuplicateKeyQueryException $ex) { - // Somehow this identity already exists? - $identity = id(new PhabricatorRepositoryIdentityQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIdentityNames(array($identity_name)) - ->executeOne(); - } - $seen[$identity_key] = $identity; + $this->identityCache[$raw_identity] = $identity; } - return $seen[$identity_key]; + return $this->identityCache[$raw_identity]; } + + + private function rebuildUsers($users) { + $viewer = $this->getViewer(); + + foreach ($users as $user) { + $this->logInfo( + pht('USER'), + pht( + 'Rebuilding identities for user "%s".', + $user->getMonogram())); + + $emails = id(new PhabricatorUserEmail())->loadAllWhere( + 'userPHID = %s', + $user->getPHID()); + if ($emails) { + $this->rebuildEmails(mpull($emails, 'getAddress')); + } + + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withRelatedPHIDs(array($user->getPHID())) + ->execute(); + + if (!$identities) { + $this->logWarn( + pht('NO IDENTITIES'), + pht('Found no identities directly related to user.')); + continue; + } + + $this->rebuildIdentities($identities); + } + } + + private function rebuildEmails($emails) { + $viewer = $this->getViewer(); + + foreach ($emails as $email) { + $this->logInfo( + pht('EMAIL'), + pht('Rebuilding identities for email address "%s".', $email)); + + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withEmailAddresses(array($email)) + ->execute(); + + if (!$identities) { + $this->logWarn( + pht('NO IDENTITIES'), + pht('Found no identities for email address "%s".', $email)); + continue; + } + + $this->rebuildIdentities($identities); + } + } + + private function rebuildIdentities($identities) { + $dry_run = $this->dryRun; + + foreach ($identities as $identity) { + $raw_identity = $identity->getIdentityName(); + + if (isset($this->identityCache[$raw_identity])) { + $this->logInfo( + pht('SKIP'), + pht( + 'Identity "%s" has already been rebuilt.', + $raw_identity)); + continue; + } + + $this->logInfo( + pht('IDENTITY'), + pht( + 'Rebuilding identity "%s".', + $raw_identity)); + + $old_auto = $identity->getAutomaticGuessedUserPHID(); + $old_assign = $identity->getManuallySetUserPHID(); + + $identity = $this->newIdentityEngine() + ->newUpdatedIdentity($identity); + + $this->identityCache[$raw_identity] = $identity; + + $new_auto = $identity->getAutomaticGuessedUserPHID(); + $new_assign = $identity->getManuallySetUserPHID(); + + $same_auto = ($old_auto === $new_auto); + $same_assign = ($old_assign === $new_assign); + + if ($same_auto && $same_assign) { + $this->logInfo( + pht('UNCHANGED'), + pht('No changes to identity.')); + } else { + if (!$same_auto) { + if ($dry_run) { + $this->logWarn( + pht('DETECTED PHID'), + pht( + '(Dry Run) Would update detected user from "%s" to "%s".', + $this->renderPHID($old_auto), + $this->renderPHID($new_auto))); + } else { + $this->logWarn( + pht('DETECTED PHID'), + pht( + 'Detected user updated from "%s" to "%s".', + $this->renderPHID($old_auto), + $this->renderPHID($new_auto))); + } + } + if (!$same_assign) { + if ($dry_run) { + $this->logWarn( + pht('ASSIGNED PHID'), + pht( + '(Dry Run) Would update assigned user from "%s" to "%s".', + $this->renderPHID($old_assign), + $this->renderPHID($new_assign))); + } else { + $this->logWarn( + pht('ASSIGNED PHID'), + pht( + 'Assigned user updated from "%s" to "%s".', + $this->renderPHID($old_assign), + $this->renderPHID($new_assign))); + } + } + } + } + } + + private function renderPHID($phid) { + if ($phid == null) { + return pht('NULL'); + } + + if (!isset($this->phidCache[$phid])) { + $viewer = $this->getViewer(); + $handles = $viewer->loadHandles(array($phid)); + $this->phidCache[$phid] = pht( + '%s <%s>', + $handles[$phid]->getFullName(), + $phid); + } + + return $this->phidCache[$phid]; + } + + private function newIdentityEngine() { + $viewer = $this->getViewer(); + + return id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer) + ->setDryRun($this->dryRun); + } + } diff --git a/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php index c37bdc04f9..df84f2dcfd 100644 --- a/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php +++ b/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php @@ -81,16 +81,6 @@ final class PhabricatorRepositoryCommitPHIDType extends PhabricatorPHIDType { $handle->setFullName($full_name); $handle->setURI($commit->getURI()); $handle->setTimestamp($commit->getEpoch()); - - $status = $commit->getAuditStatusObject(); - $icon = $status->getIcon(); - $color = $status->getColor(); - $name = $status->getName(); - - $handle - ->setStateIcon($icon) - ->setStateColor($color) - ->setStateName($name); } } diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php index c64b1a296b..7de97de4d6 100644 --- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -6,10 +6,12 @@ final class PhabricatorRepositoryIdentityQuery private $ids; private $phids; private $identityNames; - private $emailAddress; - private $assigneePHIDs; + private $emailAddresses; + private $assignedPHIDs; + private $effectivePHIDs; private $identityNameLike; private $hasEffectivePHID; + private $relatedPHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -31,13 +33,23 @@ final class PhabricatorRepositoryIdentityQuery return $this; } - public function withEmailAddress($address) { - $this->emailAddress = $address; + public function withEmailAddresses(array $addresses) { + $this->emailAddresses = $addresses; return $this; } - public function withAssigneePHIDs(array $assignees) { - $this->assigneePHIDs = $assignees; + public function withAssignedPHIDs(array $assigned) { + $this->assignedPHIDs = $assigned; + return $this; + } + + public function withEffectivePHIDs(array $effective) { + $this->effectivePHIDs = $effective; + return $this; + } + + public function withRelatedPHIDs(array $related) { + $this->relatedPHIDs = $related; return $this; } @@ -51,7 +63,7 @@ final class PhabricatorRepositoryIdentityQuery } protected function getPrimaryTableAlias() { - return 'repository_identity'; + return 'identity'; } protected function loadPage() { @@ -64,33 +76,40 @@ final class PhabricatorRepositoryIdentityQuery if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'repository_identity.id IN (%Ld)', + 'identity.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'repository_identity.phid IN (%Ls)', + 'identity.phid IN (%Ls)', $this->phids); } - if ($this->assigneePHIDs !== null) { + if ($this->assignedPHIDs !== null) { $where[] = qsprintf( $conn, - 'repository_identity.currentEffectiveUserPHID IN (%Ls)', - $this->assigneePHIDs); + 'identity.manuallySetUserPHID IN (%Ls)', + $this->assignedPHIDs); + } + + if ($this->effectivePHIDs !== null) { + $where[] = qsprintf( + $conn, + 'identity.currentEffectiveUserPHID IN (%Ls)', + $this->effectivePHIDs); } if ($this->hasEffectivePHID !== null) { if ($this->hasEffectivePHID) { $where[] = qsprintf( $conn, - 'repository_identity.currentEffectiveUserPHID IS NOT NULL'); + 'identity.currentEffectiveUserPHID IS NOT NULL'); } else { $where[] = qsprintf( $conn, - 'repository_identity.currentEffectiveUserPHID IS NULL'); + 'identity.currentEffectiveUserPHID IS NULL'); } } @@ -102,25 +121,35 @@ final class PhabricatorRepositoryIdentityQuery $where[] = qsprintf( $conn, - 'repository_identity.identityNameHash IN (%Ls)', + 'identity.identityNameHash IN (%Ls)', $name_hashes); } - if ($this->emailAddress !== null) { - $identity_style = "<{$this->emailAddress}>"; + if ($this->emailAddresses !== null) { $where[] = qsprintf( $conn, - 'repository_identity.identityNameRaw LIKE %<', - $identity_style); + 'identity.emailAddress IN (%Ls)', + $this->emailAddresses); } if ($this->identityNameLike != null) { $where[] = qsprintf( $conn, - 'repository_identity.identityNameRaw LIKE %~', + 'identity.identityNameRaw LIKE %~', $this->identityNameLike); } + if ($this->relatedPHIDs !== null) { + $where[] = qsprintf( + $conn, + '(identity.manuallySetUserPHID IN (%Ls) OR + identity.currentEffectiveUserPHID IN (%Ls) OR + identity.automaticGuessedUserPHID IN (%Ls))', + $this->relatedPHIDs, + $this->relatedPHIDs, + $this->relatedPHIDs); + } + return $where; } diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php index 9ae4115146..b0f87eb9ff 100644 --- a/src/applications/repository/query/PhabricatorRepositoryQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php @@ -215,6 +215,8 @@ final class PhabricatorRepositoryQuery $commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withIDs($commit_ids) + ->needCommitData(true) + ->needIdentities(true) ->execute(); } else { $commits = array(); diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index fdc9a695c4..d568c755c5 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2757,6 +2757,14 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO ->setDescription( pht( 'The "Fetch" and "Permanent Ref" rules for this repository.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('defaultBranch') + ->setType('string?') + ->setDescription(pht('Default branch name.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('description') + ->setType('remarkup') + ->setDescription(pht('Repository description.')), ); } @@ -2769,6 +2777,11 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $track_rules = $this->getStringListForConduit($track_rules); $permanent_rules = $this->getStringListForConduit($permanent_rules); + $default_branch = $this->getDefaultBranch(); + if (!strlen($default_branch)) { + $default_branch = null; + } + return array( 'name' => $this->getName(), 'vcs' => $this->getVersionControlSystem(), @@ -2782,6 +2795,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO 'trackRules' => $track_rules, 'permanentRefRules' => $permanent_rules, ), + 'defaultBranch' => $default_branch, + 'description' => array( + 'raw' => (string)$this->getDetail('description'), + ), ); } @@ -2804,6 +2821,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return array( id(new DiffusionRepositoryURIsSearchEngineAttachment()) ->setAttachmentKey('uris'), + id(new DiffusionRepositoryMetricsSearchEngineAttachment()) + ->setAttachmentKey('metrics'), ); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index e3833bd10e..74fbd06544 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -13,6 +13,7 @@ final class PhabricatorRepositoryIdentity protected $automaticGuessedUserPHID; protected $manuallySetUserPHID; protected $currentEffectiveUserPHID; + protected $emailAddress; protected function getConfiguration() { return array( @@ -26,12 +27,16 @@ final class PhabricatorRepositoryIdentity 'automaticGuessedUserPHID' => 'phid?', 'manuallySetUserPHID' => 'phid?', 'currentEffectiveUserPHID' => 'phid?', + 'emailAddress' => 'sort255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_identity' => array( 'columns' => array('identityNameHash'), 'unique' => true, ), + 'key_email' => array( + 'columns' => array('emailAddress(64)'), + ), ), ) + parent::getConfiguration(); } @@ -69,6 +74,10 @@ final class PhabricatorRepositoryIdentity return $this->getIdentityName(); } + public function getObjectName() { + return pht('Identity %d', $this->getID()); + } + public function getURI() { return '/diffusion/identity/view/'.$this->getID().'/'; } @@ -87,11 +96,37 @@ final class PhabricatorRepositoryIdentity public function save() { if ($this->manuallySetUserPHID) { - $this->currentEffectiveUserPHID = $this->manuallySetUserPHID; + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; + if ($this->manuallySetUserPHID === $unassigned) { + $effective_phid = null; + } else { + $effective_phid = $this->manuallySetUserPHID; + } } else { - $this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID; + $effective_phid = $this->automaticGuessedUserPHID; } + $this->setCurrentEffectiveUserPHID($effective_phid); + + $email_address = $this->getIdentityEmailAddress(); + + // Raw identities are unrestricted binary data, and may consequently + // have arbitrarily long, binary email address information. We can't + // store this kind of information in the "emailAddress" column, which + // has column type "sort255". + + // This kind of address almost certainly not legitimate and users can + // manually set the target of the identity, so just discard it rather + // than trying especially hard to make it work. + + $byte_limit = $this->getColumnMaximumByteLength('emailAddress'); + $email_address = phutil_utf8ize($email_address); + if (strlen($email_address) > $byte_limit) { + $email_address = null; + } + + $this->setEmailAddress($email_address); + return parent::save(); } @@ -111,7 +146,8 @@ final class PhabricatorRepositoryIdentity } public function hasAutomaticCapability( - $capability, PhabricatorUser $viewer) { + $capability, + PhabricatorUser $viewer) { return false; } diff --git a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php index 3c129845cd..3cc86d7f92 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php @@ -1,33 +1,59 @@ getTaskData(); - $user_phid = idx($task_data, 'userPHID'); + $related_phids = $this->getTaskDataValue('relatedPHIDs'); + $email_addresses = $this->getTaskDataValue('emailAddresses'); - $user = id(new PhabricatorPeopleQuery()) - ->withPHIDs(array($user_phid)) - ->setViewer($viewer) - ->executeOne(); + // Retain backward compatibility with older tasks which may still be in + // queue. Previously, this worker accepted a single "userPHID". See + // T13444. This can be removed in some future version of Phabricator once + // these tasks have likely flushed out of queue. + $legacy_phid = $this->getTaskDataValue('userPHID'); + if ($legacy_phid) { + if (!is_array($related_phids)) { + $related_phids = array(); + } + $related_phids[] = $legacy_phid; + } - $emails = id(new PhabricatorUserEmail())->loadAllWhere( - 'userPHID = %s ORDER BY address', - $user->getPHID()); + // Note that we may arrive in this worker after the associated objects + // have already been destroyed, so we can't (and shouldn't) verify that + // PHIDs correspond to real objects. If you "bin/remove destroy" a user, + // we'll end up here with a now-bogus user PHID that we should + // disassociate from identities. - foreach ($emails as $email) { + $identity_map = array(); + + if ($related_phids) { $identities = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) - ->withEmailAddress($email->getAddress()) + ->withRelatedPHIDs($related_phids) ->execute(); + $identity_map += mpull($identities, null, 'getPHID'); + } - foreach ($identities as $identity) { - $identity->setAutomaticGuessedUserPHID($user->getPHID()) - ->save(); - } + if ($email_addresses) { + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withEmailAddresses($email_addresses) + ->execute(); + $identity_map += mpull($identities, null, 'getPHID'); + } + + // If we didn't find any related identities, we're all set. + if (!$identity_map) { + return; + } + + $identity_engine = id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer); + foreach ($identity_map as $identity) { + $identity_engine->newUpdatedIdentity($identity); } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index c7f00df73e..fef1c2eeeb 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -65,37 +65,20 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $message = $ref->getMessage(); $committer = $ref->getCommitter(); $hashes = $ref->getHashes(); + $has_committer = (bool)strlen($committer); - $author_identity = id(new PhabricatorRepositoryIdentityQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIdentityNames(array($author)) - ->executeOne(); + $viewer = PhabricatorUser::getOmnipotentUser(); - if (!$author_identity) { - $author_identity = id(new PhabricatorRepositoryIdentity()) - ->setAuthorPHID($commit->getPHID()) - ->setIdentityName($author) - ->setAutomaticGuessedUserPHID( - $this->resolveUserPHID($commit, $author)) - ->save(); - } + $identity_engine = id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer) + ->setSourcePHID($commit->getPHID()); - $committer_identity = null; + $author_identity = $identity_engine->newResolvedIdentity($author); - if ($committer) { - $committer_identity = id(new PhabricatorRepositoryIdentityQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIdentityNames(array($committer)) - ->executeOne(); - - if (!$committer_identity) { - $committer_identity = id(new PhabricatorRepositoryIdentity()) - ->setAuthorPHID($commit->getPHID()) - ->setIdentityName($committer) - ->setAutomaticGuessedUserPHID( - $this->resolveUserPHID($commit, $committer)) - ->save(); - } + if ($has_committer) { + $committer_identity = $identity_engine->newResolvedIdentity($committer); + } else { + $committer_identity = null; } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( @@ -117,11 +100,11 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker 'authorIdentityPHID', $author_identity->getPHID()); $data->setCommitDetail( 'authorPHID', - $this->resolveUserPHID($commit, $author)); + $author_identity->getCurrentEffectiveUserPHID()); $data->setCommitMessage($message); - if (strlen($committer)) { + if ($has_committer) { $data->setCommitDetail('committer', $committer); $data->setCommitDetail('committerName', $ref->getCommitterName()); @@ -129,7 +112,8 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $data->setCommitDetail( 'committerPHID', - $this->resolveUserPHID($commit, $committer)); + $committer_identity->getCurrentEffectiveUserPHID()); + $data->setCommitDetail( 'committerIdentityPHID', $committer_identity->getPHID()); @@ -177,16 +161,6 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker PhabricatorRepositoryCommit::IMPORTED_MESSAGE); } - private function resolveUserPHID( - PhabricatorRepositoryCommit $commit, - $user_name) { - - return id(new DiffusionResolveUserQuery()) - ->withCommit($commit) - ->withName($user_name) - ->execute(); - } - private function closeRevisions( PhabricatorUser $actor, DiffusionCommitRef $ref, diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php index 3cf5923b9c..7547506258 100644 --- a/src/applications/search/application/PhabricatorSearchApplication.php +++ b/src/applications/search/application/PhabricatorSearchApplication.php @@ -33,6 +33,8 @@ final class PhabricatorSearchApplication extends PhabricatorApplication { 'index/(?P[^/]+)/' => 'PhabricatorSearchIndexController', 'hovercard/' => 'PhabricatorSearchHovercardController', + 'handle/(?P[^/]+)/' + => 'PhabricatorSearchHandleController', 'edit/' => array( 'key/(?P[^/]+)/' => 'PhabricatorSearchEditController', 'id/(?P[^/]+)/' => 'PhabricatorSearchEditController', diff --git a/src/applications/search/controller/PhabricatorSearchHandleController.php b/src/applications/search/controller/PhabricatorSearchHandleController.php new file mode 100644 index 0000000000..751b4e367d --- /dev/null +++ b/src/applications/search/controller/PhabricatorSearchHandleController.php @@ -0,0 +1,89 @@ +getViewer(); + $phid = $request->getURIData('phid'); + + $handles = $viewer->loadHandles(array($phid)); + $handle = $handles[$phid]; + + $cancel_uri = $handle->getURI(); + if (!$cancel_uri) { + $cancel_uri = '/'; + } + + $rows = array(); + + $rows[] = array( + pht('PHID'), + $phid, + ); + + $rows[] = array( + pht('PHID Type'), + phid_get_type($phid), + ); + + $rows[] = array( + pht('URI'), + $handle->getURI(), + ); + + $icon = $handle->getIcon(); + if ($icon !== null) { + $icon = id(new PHUIIconView()) + ->setIcon($handle->getIcon()); + } + + $rows[] = array( + pht('Icon'), + $icon, + ); + + $rows[] = array( + pht('Object Name'), + $handle->getObjectName(), + ); + + $rows[] = array( + pht('Name'), + $handle->getName(), + ); + + $rows[] = array( + pht('Full Name'), + $handle->getFullName(), + ); + + $rows[] = array( + pht('Tag'), + $handle->renderTag(), + ); + + $rows[] = array( + pht('Link'), + $handle->renderLink(), + ); + + $table = id(new AphrontTableView($rows)) + ->setColumnClasses( + array( + 'header', + 'wide', + )); + + return $this->newDialog() + ->setTitle(pht('Handle: %s', $phid)) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendChild($table) + ->addCancelButton($cancel_uri, pht('Done')); + } + +} diff --git a/src/applications/search/field/PhabricatorSearchIntField.php b/src/applications/search/field/PhabricatorSearchIntField.php new file mode 100644 index 0000000000..70af934470 --- /dev/null +++ b/src/applications/search/field/PhabricatorSearchIntField.php @@ -0,0 +1,22 @@ +getInt($key); + } + + protected function newControl() { + return new AphrontFormTextControl(); + } + + protected function newConduitParameterType() { + return new ConduitIntParameterType(); + } + +} diff --git a/src/applications/system/application/PhabricatorSystemApplication.php b/src/applications/system/application/PhabricatorSystemApplication.php index b6cc13050f..88f07ae17c 100644 --- a/src/applications/system/application/PhabricatorSystemApplication.php +++ b/src/applications/system/application/PhabricatorSystemApplication.php @@ -14,6 +14,12 @@ final class PhabricatorSystemApplication extends PhabricatorApplication { return true; } + public function getEventListeners() { + return array( + new PhabricatorSystemDebugUIEventListener(), + ); + } + public function getRoutes() { return array( '/status/' => 'PhabricatorStatusController', diff --git a/src/applications/system/engine/PhabricatorDestructionEngine.php b/src/applications/system/engine/PhabricatorDestructionEngine.php index 336df57756..9006073888 100644 --- a/src/applications/system/engine/PhabricatorDestructionEngine.php +++ b/src/applications/system/engine/PhabricatorDestructionEngine.php @@ -5,6 +5,9 @@ final class PhabricatorDestructionEngine extends Phobject { private $rootLogID; private $collectNotes; private $notes = array(); + private $depth = 0; + private $destroyedObjects = array(); + private $waitToFinalizeDestruction = false; public function setCollectNotes($collect_notes) { $this->collectNotes = $collect_notes; @@ -19,9 +22,20 @@ final class PhabricatorDestructionEngine extends Phobject { return PhabricatorUser::getOmnipotentUser(); } + public function setWaitToFinalizeDestruction($wait) { + $this->waitToFinalizeDestruction = $wait; + return $this; + } + + public function getWaitToFinalizeDestruction() { + return $this->waitToFinalizeDestruction; + } + public function destroyObject(PhabricatorDestructibleInterface $object) { + $this->depth++; + $log = id(new PhabricatorSystemDestructionLog()) - ->setEpoch(time()) + ->setEpoch(PhabricatorTime::getNow()) ->setObjectClass(get_class($object)); if ($this->rootLogID) { @@ -73,7 +87,42 @@ final class PhabricatorDestructionEngine extends Phobject { foreach ($extensions as $key => $extension) { $extension->destroyObject($this, $object); } + + $this->destroyedObjects[] = $object; } + + $this->depth--; + + // If this is a root-level invocation of "destroyObject()", flush the + // queue of destroyed objects and fire "didDestroyObject()" hooks. This + // hook allows extensions to do things like queue cache updates which + // might race if we fire them during object destruction. + + if (!$this->depth) { + if (!$this->getWaitToFinalizeDestruction()) { + $this->finalizeDestruction(); + } + } + + return $this; + } + + public function finalizeDestruction() { + $extensions = PhabricatorDestructionEngineExtension::getAllExtensions(); + + foreach ($this->destroyedObjects as $object) { + foreach ($extensions as $extension) { + if (!$extension->canDestroyObject($this, $object)) { + continue; + } + + $extension->didDestroyObject($this, $object); + } + } + + $this->destroyedObjects = array(); + + return $this; } private function getObjectPHID($object) { diff --git a/src/applications/system/engine/PhabricatorDestructionEngineExtension.php b/src/applications/system/engine/PhabricatorDestructionEngineExtension.php index 3146e5d611..17c5e9e5cb 100644 --- a/src/applications/system/engine/PhabricatorDestructionEngineExtension.php +++ b/src/applications/system/engine/PhabricatorDestructionEngineExtension.php @@ -14,9 +14,17 @@ abstract class PhabricatorDestructionEngineExtension extends Phobject { return true; } - abstract public function destroyObject( + public function destroyObject( PhabricatorDestructionEngine $engine, - $object); + $object) { + return null; + } + + public function didDestroyObject( + PhabricatorDestructionEngine $engine, + $object) { + return null; + } final public static function getAllExtensions() { return id(new PhutilClassMapQuery()) diff --git a/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php b/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php new file mode 100644 index 0000000000..18b94323b6 --- /dev/null +++ b/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php @@ -0,0 +1,58 @@ +listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); + } + + public function handleEvent(PhutilEvent $event) { + switch ($event->getType()) { + case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: + $this->handleActionEvent($event); + break; + } + } + + private function handleActionEvent($event) { + $viewer = $event->getUser(); + $object = $event->getValue('object'); + + if (!PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { + return; + } + + if (!$object || !$object->getPHID()) { + // If we have no object, or the object doesn't have a PHID, we can't + // do anything useful. + return; + } + + $phid = $object->getPHID(); + + $submenu = array(); + + $submenu[] = id(new PhabricatorActionView()) + ->setIcon('fa-asterisk') + ->setName(pht('View Handle')) + ->setHref(urisprintf('/search/handle/%s/', $phid)) + ->setWorkflow(true); + + $submenu[] = id(new PhabricatorActionView()) + ->setIcon('fa-address-card-o') + ->setName(pht('View Hovercard')) + ->setHref(urisprintf('/search/hovercard/?phids[]=%s', $phid)); + + $developer_action = id(new PhabricatorActionView()) + ->setName(pht('Advanced/Developer...')) + ->setIcon('fa-magic') + ->setOrder(9001) + ->setSubmenu($submenu); + + $actions = $event->getValue('actions'); + $actions[] = $developer_action; + $event->setValue('actions', $actions); + } + +} diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 6f7f713dab..bc0311eae4 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -103,8 +103,11 @@ EOREMARKUP if (!($object instanceof PhabricatorApplicationTransactionInterface)) { throw new Exception( pht( - 'Object "%s" does not implement "%s", so transactions can not '. - 'be loaded for it.')); + 'Object "%s" (of type "%s") does not implement "%s", so '. + 'transactions can not be loaded for it.', + $object_name, + get_class($object), + 'PhabricatorApplicationTransactionInterface')); } $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject( diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 7e65ac0a09..1ec29557da 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1133,6 +1133,8 @@ abstract class PhabricatorApplicationTransaction } else { $fragments = array(); foreach ($moves as $move) { + $to_column = $move['columnPHID']; + $board_phid = $move['boardPHID']; $fragments[] = pht( '%s (%s)', $this->renderHandleLink($board_phid), diff --git a/src/docs/user/userguide/events.diviner b/src/docs/user/userguide/events.diviner index dc9722a596..ea66448c8a 100644 --- a/src/docs/user/userguide/events.diviner +++ b/src/docs/user/userguide/events.diviner @@ -159,35 +159,6 @@ will be available yet. Data available on this event: - `repository` The @{class:PhabricatorRepository} the commit was discovered in. -== Diffusion: Lookup User == - -The constant for this event is -`PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER`. - -This event is dispatched when the daemons are trying to link a commit to a -Phabricator user account. You can listen for it to improve the accuracy of -associating users with their commits. - -By default, Phabricator will try to find matches based on usernames, real names, -or email addresses, but this can result in incorrect matches (e.g., if you have -several employees with the same name) or failures to match (e.g., if someone -changed their email address). Listening for this event allows you to intercept -the lookup and supplement the results from another datasource. - -Data available on this event: - - - `commit` The @{class:PhabricatorRepositoryCommit} that data is being looked - up for. - - `query` The author or committer string being looked up. This will usually - be something like "Abraham Lincoln ", but - comes from the commit metadata so it may not be well-formatted. - - `result` The current result from the lookup (Phabricator's best guess at - the user PHID of the user named in the "query"). To substitute the result - with a different result, replace this with the correct PHID in your event - listener. - -Using @{class@libphutil:PhutilEmailAddress} may be helpful in parsing the query. - == Test: Did Run Test == The constant for this event is diff --git a/src/infrastructure/events/constant/PhabricatorEventType.php b/src/infrastructure/events/constant/PhabricatorEventType.php index 3dea7b36e6..7d3a8981bf 100644 --- a/src/infrastructure/events/constant/PhabricatorEventType.php +++ b/src/infrastructure/events/constant/PhabricatorEventType.php @@ -9,7 +9,6 @@ final class PhabricatorEventType extends PhutilEventType { const TYPE_DIFFERENTIAL_WILLMARKGENERATED = 'differential.willMarkGenerated'; const TYPE_DIFFUSION_DIDDISCOVERCOMMIT = 'diffusion.didDiscoverCommit'; - const TYPE_DIFFUSION_LOOKUPUSER = 'diffusion.lookupUser'; const TYPE_TEST_DIDRUNTEST = 'test.didRunTest'; diff --git a/src/infrastructure/management/PhabricatorManagementWorkflow.php b/src/infrastructure/management/PhabricatorManagementWorkflow.php index ae33f5d6cc..b1a81201f7 100644 --- a/src/infrastructure/management/PhabricatorManagementWorkflow.php +++ b/src/infrastructure/management/PhabricatorManagementWorkflow.php @@ -67,4 +67,125 @@ abstract class PhabricatorManagementWorkflow extends PhutilArgumentWorkflow { fprintf(STDERR, '%s', $message); } + final protected function loadUsersFromArguments(array $identifiers) { + if (!$identifiers) { + return array(); + } + + $ids = array(); + $phids = array(); + $usernames = array(); + + $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; + + foreach ($identifiers as $identifier) { + // If the value is a user PHID, treat as a PHID. + if (phid_get_type($identifier) === $user_type) { + $phids[$identifier] = $identifier; + continue; + } + + // If the value is "@..." and then some text, treat it as a username. + if ((strlen($identifier) > 1) && ($identifier[0] == '@')) { + $usernames[$identifier] = substr($identifier, 1); + continue; + } + + // If the value is digits, treat it as both an ID and a username. + // Entirely numeric usernames, like "1234", are valid. + if (ctype_digit($identifier)) { + $ids[$identifier] = $identifier; + $usernames[$identifier] = $identifier; + continue; + } + + // Otherwise, treat it as an unescaped username. + $usernames[$identifier] = $identifier; + } + + $viewer = $this->getViewer(); + $results = array(); + + if ($phids) { + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs($phids) + ->execute(); + foreach ($users as $user) { + $phid = $user->getPHID(); + $results[$phid][] = $user; + } + } + + if ($usernames) { + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withUsernames($usernames) + ->execute(); + + $reverse_map = array(); + foreach ($usernames as $identifier => $username) { + $username = phutil_utf8_strtolower($username); + $reverse_map[$username][] = $identifier; + } + + foreach ($users as $user) { + $username = $user->getUsername(); + $username = phutil_utf8_strtolower($username); + + $reverse_identifiers = idx($reverse_map, $username, array()); + + if (count($reverse_identifiers) > 1) { + throw new PhutilArgumentUsageException( + pht( + 'Multiple user identifiers (%s) correspond to the same user. '. + 'Identify each user exactly once.', + implode(', ', $reverse_identifiers))); + } + + foreach ($reverse_identifiers as $reverse_identifier) { + $results[$reverse_identifier][] = $user; + } + } + } + + if ($ids) { + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + foreach ($users as $user) { + $id = $user->getID(); + $results[$id][] = $user; + } + } + + $list = array(); + foreach ($identifiers as $identifier) { + $users = idx($results, $identifier, array()); + if (!$users) { + throw new PhutilArgumentUsageException( + pht( + 'No user "%s" exists. Specify users by username, ID, or PHID.', + $identifier)); + } + + if (count($users) > 1) { + // This can happen if you have a user "@25", a user with ID 25, and + // specify "--user 25". You can disambiguate this by specifying + // "--user @25". + throw new PhutilArgumentUsageException( + pht( + 'Identifier "%s" matches multiple users. Specify each user '. + 'unambiguously with "@username" or by using user PHIDs.', + $identifier)); + } + + $list[] = head($users); + } + + return $list; + } + } diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php index a6effa00ac..2170d9ae5e 100644 --- a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php +++ b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php @@ -136,6 +136,14 @@ final class PhutilRemarkupDocumentLinkRule extends PhutilRemarkupRule { $uri = trim($matches[1]); $name = trim(idx($matches, 2)); + if (!$this->isFlatText($uri)) { + return $matches[0]; + } + + if (!$this->isFlatText($name)) { + return $matches[0]; + } + // If whatever is being linked to begins with "/" or "#", or has "://", // or is "mailto:" or "tel:", treat it as a URI instead of a wiki page. $is_uri = preg_match('@(^/)|(://)|(^#)|(^(?:mailto|tel):)@', $uri); diff --git a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php index 648b83863a..cc88678cdf 100644 --- a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php +++ b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php @@ -10,7 +10,12 @@ final class PhabricatorQueryIterator extends PhutilBufferedIterator { } protected function didRewind() { - $this->pager = new AphrontCursorPagerView(); + $pager = new AphrontCursorPagerView(); + + $page_size = $this->getPageSize(); + $pager->setPageSize($page_size); + + $this->pager = $pager; } public function key() { diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php index cae9dabec2..a3c0a49be4 100644 --- a/src/view/control/AphrontTableView.php +++ b/src/view/control/AphrontTableView.php @@ -24,6 +24,8 @@ final class AphrontTableView extends AphrontView { protected $sortValues = array(); private $deviceReadyTable; + private $rowDividers = array(); + public function __construct(array $data) { $this->data = $data; } @@ -53,6 +55,11 @@ final class AphrontTableView extends AphrontView { return $this; } + public function setRowDividers(array $dividers) { + $this->rowDividers = $dividers; + return $this; + } + public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; @@ -258,10 +265,15 @@ final class AphrontTableView extends AphrontView { } } + $dividers = $this->rowDividers; + $data = $this->data; if ($data) { $row_num = 0; + $row_idx = 0; foreach ($data as $row) { + $is_divider = !empty($dividers[$row_num]); + $row_size = count($row); while (count($row) > count($col_classes)) { $col_classes[] = null; @@ -289,6 +301,18 @@ final class AphrontTableView extends AphrontView { $class = trim($class.' '.$this->cellClasses[$row_num][$col_num]); } + if ($is_divider) { + $tr[] = phutil_tag( + 'td', + array( + 'class' => 'row-divider', + 'colspan' => count($visibility), + ), + $value); + $row_idx = -1; + break; + } + $tr[] = phutil_tag( 'td', array( @@ -299,7 +323,7 @@ final class AphrontTableView extends AphrontView { } $class = idx($this->rowClasses, $row_num); - if ($this->zebraStripes && ($row_num % 2)) { + if ($this->zebraStripes && ($row_idx % 2)) { if ($class !== null) { $class = 'alt alt-'.$class; } else { @@ -309,6 +333,7 @@ final class AphrontTableView extends AphrontView { $table[] = phutil_tag('tr', array('class' => $class), $tr); ++$row_num; + ++$row_idx; } } else { $colspan = max(count(array_filter($visibility)), 1); diff --git a/src/view/layout/PhabricatorActionListView.php b/src/view/layout/PhabricatorActionListView.php index 134c336735..22e995ab64 100644 --- a/src/view/layout/PhabricatorActionListView.php +++ b/src/view/layout/PhabricatorActionListView.php @@ -52,6 +52,14 @@ final class PhabricatorActionListView extends AphrontTagView { $action->setViewer($viewer); } + $sort = array(); + foreach ($actions as $key => $action) { + $sort[$key] = id(new PhutilSortVector()) + ->addInt($action->getOrder()); + } + $sort = msortv($sort, 'getSelf'); + $actions = array_select_keys($actions, array_keys($sort)); + require_celerity_resource('phabricator-action-list-view-css'); $items = array(); diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index 54f4fa58ee..465768ae16 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -469,53 +469,6 @@ final class PHUIHeaderView extends AphrontTagView { $container_classes[] = 'policy-header-callout'; $phid = $object->getPHID(); - // If we're going to show the object policy, try to determine if the object - // policy differs from the default policy. If it does, we'll call it out - // as changed. - if (!$use_space_policy) { - $strength = null; - if ($object instanceof PhabricatorPolicyCodexInterface) { - $codex = id(PhabricatorPolicyCodex::newFromObject($object, $viewer)) - ->setCapability($view_capability); - $strength = $codex->compareToDefaultPolicy($policy); - } else { - $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject( - $viewer, - $object, - $view_capability); - - if ($default_policy) { - if ($default_policy->getPHID() != $policy->getPHID()) { - if ($default_policy->isStrongerThan($policy)) { - $strength = PhabricatorPolicyStrengthConstants::WEAKER; - } else if ($policy->isStrongerThan($default_policy)) { - $strength = PhabricatorPolicyStrengthConstants::STRONGER; - } else { - $strength = PhabricatorPolicyStrengthConstants::ADJUSTED; - } - } - } - } - - if ($strength) { - if ($strength == PhabricatorPolicyStrengthConstants::WEAKER) { - // The policy has strictly been weakened. For example, the - // default might be "All Users" and the current policy is "Public". - $container_classes[] = 'policy-adjusted-weaker'; - } else if ($strength == PhabricatorPolicyStrengthConstants::STRONGER) { - // The policy has strictly been strengthened, and is now more - // restrictive than the default. For example, "All Users" has - // been replaced with "No One". - $container_classes[] = 'policy-adjusted-stronger'; - } else { - // The policy has been adjusted but not strictly strengthened - // or weakened. For example, "Members of X" has been replaced with - // "Members of Y". - $container_classes[] = 'policy-adjusted-different'; - } - } - } - $policy_name = array($policy->getShortName()); $policy_icon = $policy->getIcon().' bluegrey'; diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 05747c7ce6..b5ad5a7fd6 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -379,10 +379,11 @@ final class PHUIObjectItemView extends AphrontTagView { if ($this->objectName) { $header_name[] = array( - phutil_tag( + javelin_tag( 'span', array( 'class' => 'phui-oi-objname', + 'sigil' => 'ungrabbable', ), $this->objectName), ' ', diff --git a/src/view/phui/PHUIPropertyListView.php b/src/view/phui/PHUIPropertyListView.php index b806a7493a..126f568454 100644 --- a/src/view/phui/PHUIPropertyListView.php +++ b/src/view/phui/PHUIPropertyListView.php @@ -306,7 +306,7 @@ final class PHUIPropertyListView extends AphrontView { return phutil_tag( 'div', array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ), $part['content']); } @@ -317,7 +317,7 @@ final class PHUIPropertyListView extends AphrontView { return phutil_tag( 'div', array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ), $part['content']); } diff --git a/src/view/phui/PHUITwoColumnView.php b/src/view/phui/PHUITwoColumnView.php index 9240887f4b..cf4abe3a40 100644 --- a/src/view/phui/PHUITwoColumnView.php +++ b/src/view/phui/PHUITwoColumnView.php @@ -220,7 +220,7 @@ final class PHUITwoColumnView extends AphrontTagView { return phutil_tag( 'div', array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ), array( $navigation, diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index 3736ffe841..e92f499634 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -55,6 +55,16 @@ background-color: {$lightbluebackground}; } +.aphront-table-view td.row-divider { + background-color: {$bluebackground}; + font-weight: bold; + padding: 8px 12px; +} + +.aphront-table-view td.indent { + padding-left: 24px; +} + .aphront-table-view th { border-bottom: 1px solid {$thinblueborder}; } diff --git a/webroot/rsrc/css/application/auth/auth.css b/webroot/rsrc/css/application/auth/auth.css index 687aaf2bb4..28b18b85c5 100644 --- a/webroot/rsrc/css/application/auth/auth.css +++ b/webroot/rsrc/css/application/auth/auth.css @@ -57,7 +57,7 @@ } .auth-custom-message { - margin: 32px auto 64px; + margin: 32px auto 48px; max-width: 548px; background: #fff; padding: 16px; diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 1d851f04ee..e621d38134 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -213,33 +213,6 @@ body .phui-header-shell.phui-bleed-header -webkit-font-smoothing: auto; } -.policy-header-callout.policy-adjusted-weaker { - background: {$sh-greenbackground}; -} - -.policy-header-callout.policy-adjusted-weaker .policy-link, -.policy-header-callout.policy-adjusted-weaker .phui-icon-view { - color: {$sh-greentext}; -} - -.policy-header-callout.policy-adjusted-stronger { - background: {$sh-redbackground}; -} - -.policy-header-callout.policy-adjusted-stronger .policy-link, -.policy-header-callout.policy-adjusted-stronger .phui-icon-view { - color: {$sh-redtext}; -} - -.policy-header-callout.policy-adjusted-different { - background: {$sh-orangebackground}; -} - -.policy-header-callout.policy-adjusted-different .policy-link, -.policy-header-callout.policy-adjusted-different .phui-icon-view { - color: {$sh-orangetext}; -} - .policy-header-callout.policy-adjusted-special { background: {$sh-indigobackground}; } diff --git a/webroot/rsrc/js/application/repository/repository-crossreference.js b/webroot/rsrc/js/application/repository/repository-crossreference.js index d6ff2a06aa..ba522d5b47 100644 --- a/webroot/rsrc/js/application/repository/repository-crossreference.js +++ b/webroot/rsrc/js/application/repository/repository-crossreference.js @@ -152,7 +152,16 @@ JX.behavior('repository-crossreference', function(config, statics) { query.char = char; } - var uri = JX.$U('/diffusion/symbol/' + symbol + '/'); + var uri_symbol = symbol; + + // In some cases, lexers may include whitespace in symbol tags. Trim it, + // since symbols with semantic whitespace aren't supported. + uri_symbol = uri_symbol.trim(); + + // See T13437. Symbols like "#define" need to be encoded. + uri_symbol = encodeURIComponent(uri_symbol); + + var uri = JX.$U('/diffusion/symbol/' + uri_symbol + '/'); uri.addQueryParams(query); window.open(uri.toString()); diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 5f19b7061d..8930f43f94 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -181,6 +181,15 @@ JX.install('DraggableList', { return; } + // See T13452. If this is an ungrabble part of the item, don't start a + // drag. We use this to allow users to select text on cards. + var target = e.getTarget(); + if (target) { + if (JX.Stratcom.hasSigil(target, 'ungrabbable')) { + return; + } + } + if (JX.Stratcom.pass()) { // Let other handlers deal with this event before we do. return;