1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-24 15:52:41 +01:00

(stable) Promote 2016 Week 21

This commit is contained in:
epriestley 2016-05-20 16:24:08 -07:00
commit 77d543ba44
121 changed files with 3658 additions and 951 deletions

View file

@ -7,10 +7,10 @@
*/
return array(
'names' => array(
'core.pkg.css' => 'b7b8d101',
'core.pkg.css' => '204cabae',
'core.pkg.js' => '6972d365',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => '7ba78475',
'differential.pkg.css' => '33da0633',
'differential.pkg.js' => 'd0cd0df6',
'diffusion.pkg.css' => '91c5d3a6',
'diffusion.pkg.js' => '3a9a8bfa',
@ -57,7 +57,7 @@ return array(
'rsrc/css/application/dashboard/dashboard.css' => 'bc6f2127',
'rsrc/css/application/diff/inline-comment-summary.css' => '51efda3a',
'rsrc/css/application/differential/add-comment.css' => 'c47f8c40',
'rsrc/css/application/differential/changeset-view.css' => '3e3b0b76',
'rsrc/css/application/differential/changeset-view.css' => '7bcbe615',
'rsrc/css/application/differential/core.css' => '5b7b8ff4',
'rsrc/css/application/differential/phui-inline-comment.css' => '5953c28e',
'rsrc/css/application/differential/revision-comment.css' => '14b8565a',
@ -82,7 +82,7 @@ return array(
'rsrc/css/application/paste/paste.css' => '1898e534',
'rsrc/css/application/people/people-profile.css' => '2473d929',
'rsrc/css/application/phame/phame.css' => '737792d6',
'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee',
'rsrc/css/application/pholio/pholio-edit.css' => 'b15fec4a',
'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49',
'rsrc/css/application/pholio/pholio.css' => 'ca89d380',
'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02',
@ -105,7 +105,7 @@ return array(
'rsrc/css/application/uiexample/example.css' => '528b19de',
'rsrc/css/core/core.css' => 'd0801452',
'rsrc/css/core/remarkup.css' => '787105d6',
'rsrc/css/core/syntax.css' => '5101175d',
'rsrc/css/core/syntax.css' => '9fc496d5',
'rsrc/css/core/z-index.css' => '5b6fcf3f',
'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa',
'rsrc/css/font/font-aleo.css' => '8bdb2835',
@ -159,7 +159,7 @@ return array(
'rsrc/css/phui/phui-two-column-view.css' => 'b9538af1',
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7',
'rsrc/css/phui/workboards/phui-workboard.css' => 'e6d89647',
'rsrc/css/phui/workboards/phui-workcard.css' => '3646fb96',
'rsrc/css/phui/workboards/phui-workcard.css' => '0c62d7c5',
'rsrc/css/phui/workboards/phui-workpanel.css' => '92197373',
'rsrc/css/sprite-login.css' => '60e8560e',
'rsrc/css/sprite-menu.css' => '9dd65b92',
@ -550,7 +550,7 @@ return array(
'conpherence-update-css' => 'faf6be09',
'conpherence-widget-pane-css' => '775eaaba',
'd3' => 'a11a5ff2',
'differential-changeset-view-css' => '3e3b0b76',
'differential-changeset-view-css' => '7bcbe615',
'differential-core-view-css' => '5b7b8ff4',
'differential-inline-comment-editor' => '64a5550f',
'differential-revision-add-comment-css' => 'c47f8c40',
@ -804,7 +804,7 @@ return array(
'phabricator-zindex-css' => '5b6fcf3f',
'phame-css' => '737792d6',
'pholio-css' => 'ca89d380',
'pholio-edit-css' => '3ad9d1ee',
'pholio-edit-css' => 'b15fec4a',
'pholio-inline-comments-css' => '8e545e49',
'phortune-credit-card-form' => '2290aeef',
'phortune-credit-card-form-css' => '8391eb02',
@ -858,7 +858,7 @@ return array(
'phui-two-column-view-css' => 'b9538af1',
'phui-workboard-color-css' => 'ac6fe6a7',
'phui-workboard-view-css' => 'e6d89647',
'phui-workcard-view-css' => '3646fb96',
'phui-workcard-view-css' => '0c62d7c5',
'phui-workpanel-view-css' => '92197373',
'phuix-action-list-view' => 'b5c256b8',
'phuix-action-view' => '8cf6d262',
@ -881,7 +881,7 @@ return array(
'sprite-menu-css' => '9dd65b92',
'sprite-tokens-css' => '4f399012',
'syntax-default-css' => '9923583c',
'syntax-highlighting-css' => '5101175d',
'syntax-highlighting-css' => '9fc496d5',
'tokens-css' => '3d0f239e',
'typeahead-browse-css' => 'd8581d2c',
'unhandled-exception-css' => '4c96257a',
@ -1148,9 +1148,6 @@ return array(
'javelin-util',
'javelin-uri',
),
'3e3b0b76' => array(
'phui-inline-comment-view-css',
),
'3f5d6dbf' => array(
'javelin-behavior',
'javelin-dom',
@ -1243,9 +1240,6 @@ return array(
'javelin-typeahead-source',
'javelin-util',
),
'5101175d' => array(
'syntax-default-css',
),
'519705ea' => array(
'javelin-install',
'javelin-dom',
@ -1495,6 +1489,9 @@ return array(
'javelin-stratcom',
'javelin-util',
),
'7bcbe615' => array(
'phui-inline-comment-view-css',
),
'7cbe244b' => array(
'javelin-install',
'javelin-util',
@ -1660,6 +1657,9 @@ return array(
'javelin-dom',
'javelin-vector',
),
'9fc496d5' => array(
'syntax-default-css',
),
'a0b57eb8' => array(
'javelin-behavior',
'javelin-dom',

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_owners.owners_package
ADD autoReview VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_owners.owners_package
SET autoReview = 'none' WHERE autoReview = '';

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_owners.owners_package
ADD dominion VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_owners.owners_package
SET dominion = 'strong' WHERE dominion = '';

View file

@ -0,0 +1,16 @@
CREATE TABLE {$NAMESPACE}_oauth_server.edge (
src VARBINARY(64) NOT NULL,
type INT UNSIGNED NOT NULL,
dst VARBINARY(64) NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
seq INT UNSIGNED NOT NULL,
dataID INT UNSIGNED,
PRIMARY KEY (src, type, dst),
KEY `src` (src, type, dateCreated, seq),
UNIQUE KEY `key_dst` (dst, type, src)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
CREATE TABLE {$NAMESPACE}_oauth_server.edgedata (
id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_sshkey
ADD isActive BOOL;

View file

@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_auth.auth_sshkey
SET isActive = 1;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_sshkey
ADD UNIQUE KEY `key_activeunique` (keyIndex, isActive);

View file

@ -0,0 +1,19 @@
CREATE TABLE {$NAMESPACE}_auth.auth_sshkeytransaction (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
viewPolicy VARBINARY(64) NOT NULL,
editPolicy VARBINARY(64) NOT NULL,
commentPHID VARBINARY(64) DEFAULT NULL,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (`phid`),
KEY `key_object` (`objectPHID`)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -14,6 +14,7 @@ try {
$key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withKeys(array($public_key))
->withIsActive(true)
->executeOne();
if (!$key) {
exit(1);

View file

@ -6,6 +6,7 @@ require_once $root.'/scripts/__init_script__.php';
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIsActive(true)
->execute();
if (!$keys) {

View file

@ -361,6 +361,7 @@ phutil_register_library_map(array(
'DifferentialAuthorField' => 'applications/differential/customfield/DifferentialAuthorField.php',
'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php',
'DifferentialBlockHeraldAction' => 'applications/differential/herald/DifferentialBlockHeraldAction.php',
'DifferentialBlockingReviewerDatasource' => 'applications/differential/typeahead/DifferentialBlockingReviewerDatasource.php',
'DifferentialBranchField' => 'applications/differential/customfield/DifferentialBranchField.php',
'DifferentialChangeDetailMailView' => 'applications/differential/mail/DifferentialChangeDetailMailView.php',
'DifferentialChangeHeraldFieldGroup' => 'applications/differential/herald/DifferentialChangeHeraldFieldGroup.php',
@ -435,6 +436,7 @@ phutil_register_library_map(array(
'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php',
'DifferentialDraft' => 'applications/differential/storage/DifferentialDraft.php',
'DifferentialEditPolicyField' => 'applications/differential/customfield/DifferentialEditPolicyField.php',
'DifferentialExactUserFunctionDatasource' => 'applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php',
'DifferentialFieldParseException' => 'applications/differential/exception/DifferentialFieldParseException.php',
'DifferentialFieldValidationException' => 'applications/differential/exception/DifferentialFieldValidationException.php',
'DifferentialFindConduitAPIMethod' => 'applications/differential/conduit/DifferentialFindConduitAPIMethod.php',
@ -491,9 +493,13 @@ phutil_register_library_map(array(
'DifferentialRepositoryField' => 'applications/differential/customfield/DifferentialRepositoryField.php',
'DifferentialRepositoryLookup' => 'applications/differential/query/DifferentialRepositoryLookup.php',
'DifferentialRequiredSignaturesField' => 'applications/differential/customfield/DifferentialRequiredSignaturesField.php',
'DifferentialResponsibleDatasource' => 'applications/differential/typeahead/DifferentialResponsibleDatasource.php',
'DifferentialResponsibleUserDatasource' => 'applications/differential/typeahead/DifferentialResponsibleUserDatasource.php',
'DifferentialResponsibleViewerFunctionDatasource' => 'applications/differential/typeahead/DifferentialResponsibleViewerFunctionDatasource.php',
'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php',
'DifferentialReviewedByField' => 'applications/differential/customfield/DifferentialReviewedByField.php',
'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php',
'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php',
'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php',
'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php',
'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php',
@ -533,6 +539,8 @@ phutil_register_library_map(array(
'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php',
'DifferentialRevisionRepositoryHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryHeraldField.php',
'DifferentialRevisionRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryProjectsHeraldField.php',
'DifferentialRevisionRequiredActionResultBucket' => 'applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php',
'DifferentialRevisionResultBucket' => 'applications/differential/query/DifferentialRevisionResultBucket.php',
'DifferentialRevisionReviewersHeraldField' => 'applications/differential/herald/DifferentialRevisionReviewersHeraldField.php',
'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php',
'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php',
@ -1869,12 +1877,19 @@ phutil_register_library_map(array(
'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php',
'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php',
'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php',
'PhabricatorAuthSSHKeyDeleteController' => 'applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php',
'PhabricatorAuthSSHKeyDeactivateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyDeactivateController.php',
'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php',
'PhabricatorAuthSSHKeyEditor' => 'applications/auth/editor/PhabricatorAuthSSHKeyEditor.php',
'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php',
'PhabricatorAuthSSHKeyListController' => 'applications/auth/controller/PhabricatorAuthSSHKeyListController.php',
'PhabricatorAuthSSHKeyPHIDType' => 'applications/auth/phid/PhabricatorAuthSSHKeyPHIDType.php',
'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php',
'PhabricatorAuthSSHKeyReplyHandler' => 'applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php',
'PhabricatorAuthSSHKeySearchEngine' => 'applications/auth/query/PhabricatorAuthSSHKeySearchEngine.php',
'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php',
'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php',
'PhabricatorAuthSSHKeyTransactionQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyTransactionQuery.php',
'PhabricatorAuthSSHKeyViewController' => 'applications/auth/controller/PhabricatorAuthSSHKeyViewController.php',
'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php',
'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php',
'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php',
@ -2161,7 +2176,6 @@ phutil_register_library_map(array(
'PhabricatorCountdownTransactionQuery' => 'applications/countdown/query/PhabricatorCountdownTransactionQuery.php',
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
'PhabricatorCredentialsUsedByObjectEdgeType' => 'applications/passphrase/edge/PhabricatorCredentialsUsedByObjectEdgeType.php',
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php',
@ -2779,6 +2793,7 @@ phutil_register_library_map(array(
'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/PhabricatorOAuthServerDAO.php',
'PhabricatorOAuthServerEditEngine' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditEngine.php',
'PhabricatorOAuthServerEditor' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditor.php',
'PhabricatorOAuthServerSchemaSpec' => 'applications/oauthserver/query/PhabricatorOAuthServerSchemaSpec.php',
'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php',
'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php',
'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php',
@ -2802,7 +2817,6 @@ phutil_register_library_map(array(
'PhabricatorObjectQuery' => 'applications/phid/query/PhabricatorObjectQuery.php',
'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php',
'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php',
'PhabricatorObjectUsesCredentialsEdgeType' => 'applications/transactions/edges/PhabricatorObjectUsesCredentialsEdgeType.php',
'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php',
'PhabricatorOldWorldContentSource' => 'infrastructure/contentsource/PhabricatorOldWorldContentSource.php',
'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php',
@ -2833,6 +2847,7 @@ phutil_register_library_map(array(
'PhabricatorOwnersPackageOwnerDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageOwnerDatasource.php',
'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php',
'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php',
'PhabricatorOwnersPackageRemarkupRule' => 'applications/owners/remarkup/PhabricatorOwnersPackageRemarkupRule.php',
'PhabricatorOwnersPackageSearchEngine' => 'applications/owners/query/PhabricatorOwnersPackageSearchEngine.php',
'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php',
'PhabricatorOwnersPackageTransaction' => 'applications/owners/storage/PhabricatorOwnersPackageTransaction.php',
@ -3316,6 +3331,8 @@ phutil_register_library_map(array(
'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php',
'PhabricatorSearchPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php',
'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php',
'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php',
'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php',
'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php',
'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php',
@ -4554,6 +4571,7 @@ phutil_register_library_map(array(
'DifferentialAuthorField' => 'DifferentialCustomField',
'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField',
'DifferentialBlockHeraldAction' => 'HeraldAction',
'DifferentialBlockingReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialBranchField' => 'DifferentialCustomField',
'DifferentialChangeDetailMailView' => 'DifferentialMailView',
'DifferentialChangeHeraldFieldGroup' => 'HeraldFieldGroup',
@ -4638,6 +4656,7 @@ phutil_register_library_map(array(
'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
'DifferentialDraft' => 'DifferentialDAO',
'DifferentialEditPolicyField' => 'DifferentialCoreCustomField',
'DifferentialExactUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialFieldParseException' => 'Exception',
'DifferentialFieldValidationException' => 'Exception',
'DifferentialFindConduitAPIMethod' => 'DifferentialConduitAPIMethod',
@ -4700,9 +4719,13 @@ phutil_register_library_map(array(
'DifferentialRepositoryField' => 'DifferentialCoreCustomField',
'DifferentialRepositoryLookup' => 'Phobject',
'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField',
'DifferentialResponsibleDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialResponsibleUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialResponsibleViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevertPlanField' => 'DifferentialStoredCustomField',
'DifferentialReviewedByField' => 'DifferentialCoreCustomField',
'DifferentialReviewer' => 'Phobject',
'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialReviewerStatus' => 'Phobject',
'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'DifferentialReviewersHeraldAction',
@ -4757,6 +4780,8 @@ phutil_register_library_map(array(
'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialRevisionRepositoryHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionRepositoryProjectsHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionRequiredActionResultBucket' => 'DifferentialRevisionResultBucket',
'DifferentialRevisionResultBucket' => 'PhabricatorSearchResultBucket',
'DifferentialRevisionReviewersHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialRevisionStatus' => 'Phobject',
@ -6286,14 +6311,22 @@ phutil_register_library_map(array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController',
'PhabricatorAuthSSHKeyDeleteController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyDeactivateController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyListController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSSHKeyReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorAuthSSHKeySearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorAuthSSHKeyTableView' => 'AphrontView',
'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthSSHKeyTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthSSHKeyViewController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHPublicKey' => 'Phobject',
'PhabricatorAuthSession' => array(
'PhabricatorAuthDAO',
@ -6643,7 +6676,6 @@ phutil_register_library_map(array(
'PhabricatorCountdownTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCountdownView' => 'AphrontView',
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
'PhabricatorCredentialsUsedByObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorCustomField' => 'Phobject',
'PhabricatorCustomFieldAttachment' => 'Phobject',
@ -7342,6 +7374,7 @@ phutil_register_library_map(array(
'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO',
'PhabricatorOAuthServerEditEngine' => 'PhabricatorEditEngine',
'PhabricatorOAuthServerEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorOAuthServerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOAuthServerScope' => 'Phobject',
'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase',
'PhabricatorOAuthServerTokenController' => 'PhabricatorOAuthServerController',
@ -7368,7 +7401,6 @@ phutil_register_library_map(array(
'PhabricatorObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorObjectSelectorDialog' => 'Phobject',
'PhabricatorObjectUsesCredentialsEdgeType' => 'PhabricatorEdgeType',
'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery',
'PhabricatorOldWorldContentSource' => 'PhabricatorContentSource',
'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock',
@ -7411,6 +7443,7 @@ phutil_register_library_map(array(
'PhabricatorOwnersPackageOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType',
'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOwnersPackageRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorOwnersPackageSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase',
'PhabricatorOwnersPackageTransaction' => 'PhabricatorApplicationTransaction',
@ -8007,6 +8040,8 @@ phutil_register_library_map(array(
'PhabricatorSearchOrderField' => 'PhabricatorSearchField',
'PhabricatorSearchPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSearchRelationship' => 'Phobject',
'PhabricatorSearchResultBucket' => 'Phobject',
'PhabricatorSearchResultBucketGroup' => 'Phobject',
'PhabricatorSearchResultView' => 'AphrontView',
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController',

View file

@ -146,6 +146,7 @@ final class AlmanacDeviceViewController
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer)
->withObjectPHIDs(array($device_phid))
->withIsActive(true)
->execute();
$table = id(new PhabricatorAuthSSHKeyTableView())
@ -156,38 +157,13 @@ final class AlmanacDeviceViewController
->setShowTrusted(true)
->setNoDataString(pht('This device has no associated SSH public keys.'));
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
$can_generate = true;
} catch (Exception $ex) {
$can_generate = false;
}
$generate_uri = '/auth/sshkey/generate/?objectPHID='.$device_phid;
$upload_uri = '/auth/sshkey/upload/?objectPHID='.$device_phid;
$menu_button = PhabricatorAuthSSHKeyTableView::newKeyActionsMenu(
$viewer,
$device);
$header = id(new PHUIHeaderView())
->setHeader(pht('SSH Public Keys'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($generate_uri)
->setWorkflow(true)
->setDisabled(!$can_edit || !$can_generate)
->setText(pht('Generate Keypair'))
->setIcon(
id(new PHUIIconView())
->setIcon('fa-lock')))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($upload_uri)
->setWorkflow(true)
->setDisabled(!$can_edit)
->setText(pht('Upload Public Key'))
->setIcon(
id(new PHUIIconView())
->setIcon('fa-upload')));
->addActionLink($menu_button);
return id(new PHUIObjectBoxView())
->setHeader($header)

View file

@ -141,6 +141,7 @@ final class AlmanacManagementRegisterWorkflow
$public_key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($this->getViewer())
->withKeys(array($key_object))
->withIsActive(true)
->executeOne();
if (!$public_key) {

View file

@ -35,6 +35,11 @@ final class AlmanacManagementTrustKeyWorkflow
pht('No public key exists with ID "%s".', $id));
}
if (!$key->getIsActive()) {
throw new PhutilArgumentUsageException(
pht('Public key "%s" is not an active key.', $id));
}
if ($key->getIsTrusted()) {
throw new PhutilArgumentUsageException(
pht('Public key with ID %s is already trusted.', $id));

View file

@ -227,6 +227,14 @@ final class AlmanacDevice
return $this->getName();
}
public function getSSHKeyNotifyPHIDs() {
// Devices don't currently have anyone useful to notify about SSH key
// edits, and they're usually a difficult vector to attack since you need
// access to a cluster host. However, it would be nice to make them
// subscribable at some point.
return array();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -159,7 +159,17 @@ final class PhabricatorAuditEditor
$requests = mpull($requests, null, 'getAuditorPHID');
foreach ($add as $phid) {
if (isset($requests[$phid])) {
continue;
$request = $requests[$phid];
// Only update an existing request if the current status is not
// an interesting status.
if ($request->isInteresting()) {
continue;
}
} else {
$request = id(new PhabricatorRepositoryAuditRequest())
->setCommitPHID($object->getPHID())
->setAuditorPHID($phid);
}
if ($this->getIsHeraldEditor()) {
@ -170,12 +180,13 @@ final class PhabricatorAuditEditor
$audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED;
$audit_reason = $this->getAuditReasons($phid);
}
$requests[] = id(new PhabricatorRepositoryAuditRequest())
->setCommitPHID($object->getPHID())
->setAuditorPHID($phid)
$request
->setAuditStatus($audit_requested)
->setAuditReasons($audit_reason)
->save();
$requests[$phid] = $request;
}
$object->attachAudits($requests);

View file

@ -75,10 +75,14 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
'multifactor/'
=> 'PhabricatorAuthNeedsMultiFactorController',
'sshkey/' => array(
$this->getQueryRoutePattern('for/(?P<forPHID>[^/]+)/')
=> 'PhabricatorAuthSSHKeyListController',
'generate/' => 'PhabricatorAuthSSHKeyGenerateController',
'upload/' => 'PhabricatorAuthSSHKeyEditController',
'edit/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyEditController',
'delete/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyDeleteController',
'deactivate/(?P<id>\d+)/'
=> 'PhabricatorAuthSSHKeyDeactivateController',
'view/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyViewController',
),
),

View file

@ -28,7 +28,8 @@ final class PhabricatorAuthQueryPublicKeysConduitAPIMethod
$viewer = $request->getUser();
$query = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer);
->setViewer($viewer)
->withIsActive(true);
$ids = $request->getValue('ids');
if ($ids !== null) {

View file

@ -3,18 +3,34 @@
abstract class PhabricatorAuthSSHKeyController
extends PhabricatorAuthController {
protected function newKeyForObjectPHID($object_phid) {
private $keyObject;
public function setSSHKeyObject(PhabricatorSSHPublicKeyInterface $object) {
$this->keyObject = $object;
return $this;
}
public function getSSHKeyObject() {
return $this->keyObject;
}
protected function loadSSHKeyObject($object_phid, $need_edit) {
$viewer = $this->getViewer();
$object = id(new PhabricatorObjectQuery())
$query = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->requireCapabilities(
->withPHIDs(array($object_phid));
if ($need_edit) {
$query->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
));
}
$object = $query->executeOne();
if (!$object) {
return null;
}
@ -25,9 +41,38 @@ abstract class PhabricatorAuthSSHKeyController
return null;
}
return id(new PhabricatorAuthSSHKey())
->setObjectPHID($object_phid)
->attachObject($object);
$this->keyObject = $object;
return $object;
}
protected function newKeyForObjectPHID($object_phid) {
$viewer = $this->getViewer();
$object = $this->loadSSHKeyObject($object_phid, true);
if (!$object) {
return null;
}
return PhabricatorAuthSSHKey::initializeNewSSHKey($viewer, $object);
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$viewer = $this->getViewer();
$key_object = $this->getSSHKeyObject();
if ($key_object) {
$object_phid = $key_object->getPHID();
$handles = $viewer->loadHandles(array($object_phid));
$handle = $handles[$object_phid];
$uri = $key_object->getSSHPublicKeyManagementURI($viewer);
$crumbs->addTextCrumb($handle->getObjectName(), $uri);
}
return $crumbs;
}
}

View file

@ -1,6 +1,6 @@
<?php
final class PhabricatorAuthSSHKeyDeleteController
final class PhabricatorAuthSSHKeyDeactivateController
extends PhabricatorAuthSSHKeyController {
public function handleRequest(AphrontRequest $request) {
@ -19,7 +19,7 @@ final class PhabricatorAuthSSHKeyDeleteController
return new Aphront404Response();
}
$cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer);
$cancel_uri = $key->getURI();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
@ -27,21 +27,33 @@ final class PhabricatorAuthSSHKeyDeleteController
$cancel_uri);
if ($request->isFormPost()) {
// TODO: It would be nice to write an edge transaction here or something.
$key->delete();
$xactions = array();
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE)
->setNewValue(true);
id(new PhabricatorAuthSSHKeyEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($key, $xactions);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$name = phutil_tag('strong', array(), $key->getName());
return $this->newDialog()
->setTitle(pht('Really delete SSH Public Key?'))
->setTitle(pht('Deactivate SSH Public Key'))
->appendParagraph(
pht(
'The key "%s" will be permanently deleted, and you will not longer '.
'be able to use the corresponding private key to authenticate.',
'The key "%s" will be permanently deactivated, and you will no '.
'longer be able to use the corresponding private key to '.
'authenticate.',
$name))
->addSubmitButton(pht('Delete Public Key'))
->addSubmitButton(pht('Deactivate Public Key'))
->addCancelButton($cancel_uri);
}

View file

@ -59,51 +59,45 @@ final class PhabricatorAuthSSHKeyEditController
$v_key = $key->getEntireKey();
$e_key = strlen($v_key) ? null : true;
$errors = array();
$validation_exception = null;
if ($request->isFormPost()) {
$type_create = PhabricatorTransactions::TYPE_CREATE;
$type_name = PhabricatorAuthSSHKeyTransaction::TYPE_NAME;
$type_key = PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
$e_name = null;
$e_key = null;
$v_name = $request->getStr('name');
$v_key = $request->getStr('key');
if (!strlen($v_name)) {
$errors[] = pht('You must provide a name for this public key.');
$e_name = pht('Required');
} else {
$key->setName($v_name);
$xactions = array();
if (!$key->getID()) {
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
}
if (!strlen($v_key)) {
$errors[] = pht('You must provide a public key.');
$e_key = pht('Required');
} else {
try {
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($v_key);
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType($type_name)
->setNewValue($v_name);
$type = $public_key->getType();
$body = $public_key->getBody();
$comment = $public_key->getComment();
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType($type_key)
->setNewValue($v_key);
$key->setKeyType($type);
$key->setKeyBody($body);
$key->setKeyComment($comment);
$editor = id(new PhabricatorAuthSSHKeyEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
$e_key = null;
} catch (Exception $ex) {
$e_key = pht('Invalid');
$errors[] = $ex->getMessage();
}
}
if (!$errors) {
try {
$key->save();
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
} catch (Exception $ex) {
$e_key = pht('Duplicate');
$errors[] = pht(
'This public key is already associated with another user or '.
'device. Each key must unambiguously identify a single unique '.
'owner.');
}
try {
$editor->applyTransactions($key, $xactions);
return id(new AphrontRedirectResponse())->setURI($key->getURI());
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_name = $ex->getShortMessage($type_name);
$e_key = $ex->getShortMessage($type_key);
}
}
@ -134,7 +128,7 @@ final class PhabricatorAuthSSHKeyEditController
return $this->newDialog()
->setTitle($title)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->setValidationException($validation_exception)
->appendForm($form)
->addSubmitButton($save_button)
->addCancelButton($cancel_uri);

View file

@ -36,13 +36,31 @@ final class PhabricatorAuthSSHKeyGenerateController
$type = $public_key->getType();
$body = $public_key->getBody();
$comment = pht('Generated');
$key
->setName($default_name)
->setKeyType($type)
->setKeyBody($body)
->setKeyComment(pht('Generated'))
->save();
$entire_key = "{$type} {$body} {$comment}";
$type_create = PhabricatorTransactions::TYPE_CREATE;
$type_name = PhabricatorAuthSSHKeyTransaction::TYPE_NAME;
$type_key = PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
$xactions = array();
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType($type_name)
->setNewValue($default_name);
$xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
->setTransactionType($type_key)
->setNewValue($entire_key);
$editor = id(new PhabricatorAuthSSHKeyEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->applyTransactions($key, $xactions);
// NOTE: We're disabling workflow on submit so the download works. We're
// disabling workflow on cancel so the page reloads, showing the new

View file

@ -0,0 +1,25 @@
<?php
final class PhabricatorAuthSSHKeyListController
extends PhabricatorAuthSSHKeyController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$object_phid = $request->getURIData('forPHID');
$object = $this->loadSSHKeyObject($object_phid, false);
if (!$object) {
return new Aphront404Response();
}
$engine = id(new PhabricatorAuthSSHKeySearchEngine())
->setSSHKeyObject($object);
return id($engine)
->setController($this)
->buildResponse();
}
}

View file

@ -0,0 +1,124 @@
<?php
final class PhabricatorAuthSSHKeyViewController
extends PhabricatorAuthSSHKeyController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$ssh_key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$ssh_key) {
return new Aphront404Response();
}
$this->setSSHKeyObject($ssh_key->getObject());
$title = pht('SSH Key %d', $ssh_key->getID());
$curtain = $this->buildCurtain($ssh_key);
$details = $this->buildPropertySection($ssh_key);
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($ssh_key->getName())
->setHeaderIcon('fa-key');
if ($ssh_key->getIsActive()) {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
} else {
$header->setStatus('fa-ban', 'dark', pht('Deactivated'));
}
$header->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('View Active Keys'))
->setHref($ssh_key->getObject()->getSSHPublicKeyManagementURI($viewer))
->setIcon('fa-list-ul'));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$ssh_key,
new PhabricatorAuthSSHKeyTransactionQuery());
$timeline->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$details,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildCurtain(PhabricatorAuthSSHKey $ssh_key) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$ssh_key,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $ssh_key->getID();
$edit_uri = $this->getApplicationURI("sshkey/edit/{$id}/");
$deactivate_uri = $this->getApplicationURI("sshkey/deactivate/{$id}/");
$curtain = $this->newCurtainView($ssh_key);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit SSH Key'))
->setHref($edit_uri)
->setWorkflow(true)
->setDisabled(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-times')
->setName(pht('Deactivate SSH Key'))
->setHref($deactivate_uri)
->setWorkflow(true)
->setDisabled(!$can_edit));
return $curtain;
}
private function buildPropertySection(
PhabricatorAuthSSHKey $ssh_key) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$properties->addProperty(pht('SSH Key Type'), $ssh_key->getKeyType());
$properties->addProperty(
pht('Created'),
phabricator_datetime($ssh_key->getDateCreated(), $viewer));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
}

View file

@ -0,0 +1,244 @@
<?php
final class PhabricatorAuthSSHKeyEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorAuthApplication';
}
public function getEditorObjectsDescription() {
return pht('SSH Keys');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorAuthSSHKeyTransaction::TYPE_NAME;
$types[] = PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
$types[] = PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
return $object->getName();
case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
return $object->getEntireKey();
case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
return !$object->getIsActive();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
return $xaction->getNewValue();
case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
return (bool)$xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$value = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
$object->setName($value);
return;
case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($value);
$type = $public_key->getType();
$body = $public_key->getBody();
$comment = $public_key->getComment();
$object->setKeyType($type);
$object->setKeyBody($body);
$object->setKeyComment($comment);
return;
case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
if ($value) {
$new = null;
} else {
$new = 1;
}
$object->setIsActive($new);
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('SSH key name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
case PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('SSH key material is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
} else {
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
try {
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($new);
} catch (Exception $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
}
}
}
break;
case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
foreach ($xactions as $xaction) {
if (!$xaction->getNewValue()) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('SSH keys can not be reactivated.'),
$xaction);
}
}
break;
}
return $errors;
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
$errors = array();
$errors[] = new PhabricatorApplicationTransactionValidationError(
PhabricatorAuthSSHKeyTransaction::TYPE_KEY,
pht('Duplicate'),
pht(
'This public key is already associated with another user or device. '.
'Each key must unambiguously identify a single unique owner.'),
null);
throw new PhabricatorApplicationTransactionValidationException($errors);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[SSH Key]');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'ssh-key-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return $object->getObject()->getSSHKeyNotifyPHIDs();
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return array();
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorAuthSSHKeyReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
$phid = $object->getPHID();
$mail = id(new PhabricatorMetaMTAMail())
->setSubject(pht('SSH Key %d: %s', $id, $name))
->addHeader('Thread-Topic', $phid);
// The primary value of this mail is alerting users to account compromises,
// so force delivery. In particular, this mail should still be delievered
// even if "self mail" is disabled.
$mail->setForceDelivery(true);
return $mail;
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addTextSection(
pht('SECURITY WARNING'),
pht(
'If you do not recognize this change, it may indicate your account '.
'has been compromised.'));
$detail_uri = $object->getURI();
$detail_uri = PhabricatorEnv::getProductionURI($detail_uri);
$body->addLinkSection(pht('SSH KEY DETAIL'), $detail_uri);
return $body;
}
}

View file

@ -0,0 +1,17 @@
<?php
final class PhabricatorAuthSSHKeyReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorAuthSSHKey)) {
throw new Exception(
pht('Mail receiver is not a %s!', 'PhabricatorAuthSSHKey'));
}
}
public function getObjectPrefix() {
return 'SSHKEY';
}
}

View file

@ -32,6 +32,10 @@ final class PhabricatorAuthSSHKeyPHIDType
foreach ($handles as $phid => $handle) {
$key = $objects[$phid];
$handle->setName(pht('SSH Key %d', $key->getID()));
if (!$key->getIsActive()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}
}

View file

@ -7,6 +7,7 @@ final class PhabricatorAuthSSHKeyQuery
private $phids;
private $objectPHIDs;
private $keys;
private $isActive;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -29,6 +30,11 @@ final class PhabricatorAuthSSHKeyQuery
return $this;
}
public function withIsActive($active) {
$this->isActive = $active;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthSSHKey();
}
@ -100,6 +106,19 @@ final class PhabricatorAuthSSHKeyQuery
$where[] = implode(' OR ', $sql);
}
if ($this->isActive !== null) {
if ($this->isActive) {
$where[] = qsprintf(
$conn,
'isActive = %d',
1);
} else {
$where[] = qsprintf(
$conn,
'isActive IS NULL');
}
}
return $where;
}

View file

@ -0,0 +1,105 @@
<?php
final class PhabricatorAuthSSHKeySearchEngine
extends PhabricatorApplicationSearchEngine {
private $sshKeyObject;
public function setSSHKeyObject(PhabricatorSSHPublicKeyInterface $object) {
$this->sshKeyObject = $object;
return $this;
}
public function getSSHKeyObject() {
return $this->sshKeyObject;
}
public function canUseInPanelContext() {
return false;
}
public function getResultTypeDescription() {
return pht('SSH Keys');
}
public function getApplicationClassName() {
return 'PhabricatorAuthApplication';
}
public function newQuery() {
$object = $this->getSSHKeyObject();
$object_phid = $object->getPHID();
return id(new PhabricatorAuthSSHKeyQuery())
->withObjectPHIDs(array($object_phid));
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
return $query;
}
protected function buildCustomSearchFields() {
return array();
}
protected function getURI($path) {
$object = $this->getSSHKeyObject();
$object_phid = $object->getPHID();
return "/auth/sshkey/for/{$object_phid}/{$path}";
}
protected function getBuiltinQueryNames() {
$names = array(
'all' => pht('All Keys'),
);
return $names;
}
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 $keys,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($keys, 'PhabricatorAuthSSHKey');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
$list->setUser($viewer);
foreach ($keys as $key) {
$item = id(new PHUIObjectItemView())
->setObjectName(pht('SSH Key %d', $key->getID()))
->setHeader($key->getName())
->setHref($key->getURI());
if (!$key->getIsActive()) {
$item->setDisabled(true);
}
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No matching SSH keys.'));
return $result;
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorAuthSSHKeyTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhabricatorAuthSSHKeyTransaction();
}
}

View file

@ -17,4 +17,6 @@ interface PhabricatorSSHPublicKeyInterface {
*/
public function getSSHKeyDefaultName();
public function getSSHKeyNotifyPHIDs();
}

View file

@ -4,7 +4,8 @@ final class PhabricatorAuthSSHKey
extends PhabricatorAuthDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $objectPHID;
protected $name;
@ -13,9 +14,28 @@ final class PhabricatorAuthSSHKey
protected $keyBody;
protected $keyComment = '';
protected $isTrusted = 0;
protected $isActive;
private $object = self::ATTACHABLE;
public static function initializeNewSSHKey(
PhabricatorUser $viewer,
PhabricatorSSHPublicKeyInterface $object) {
// You must be able to edit an object to create a new key on it.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$object_phid = $object->getPHID();
return id(new self())
->setIsActive(1)
->setObjectPHID($object_phid)
->attachObject($object);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
@ -26,13 +46,19 @@ final class PhabricatorAuthSSHKey
'keyBody' => 'text',
'keyComment' => 'text255',
'isTrusted' => 'bool',
'isActive' => 'bool?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
'key_unique' => array(
'columns' => array('keyIndex'),
'key_active' => array(
'columns' => array('isActive', 'objectPHID'),
),
// NOTE: This unique key includes a nullable column, effectively
// constraining uniqueness on active keys only.
'key_activeunique' => array(
'columns' => array('keyIndex', 'isActive'),
'unique' => true,
),
),
@ -44,6 +70,12 @@ final class PhabricatorAuthSSHKey
return parent::save();
}
public function getMailKey() {
// NOTE: We don't actually receive mail for these objects. It's OK for
// the mail key to be predictable until we do.
return PhabricatorHash::digestForIndex($this->getPHID());
}
public function toPublicKey() {
return PhabricatorAuthSSHPublicKey::newFromStoredKey($this);
}
@ -71,6 +103,11 @@ final class PhabricatorAuthSSHKey
PhabricatorAuthSSHKeyPHIDType::TYPECONST);
}
public function getURI() {
$id = $this->getID();
return "/auth/sshkey/view/{$id}/";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
@ -82,14 +119,29 @@ final class PhabricatorAuthSSHKey
}
public function getPolicy($capability) {
if (!$this->getIsActive()) {
if ($capability == PhabricatorPolicyCapability::CAN_EDIT) {
return PhabricatorPolicies::POLICY_NOONE;
}
}
return $this->getObject()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if (!$this->getIsActive()) {
return false;
}
return $this->getObject()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
if (!$this->getIsACtive()) {
return pht(
'Deactivated SSH keys can not be edited or reactivated.');
}
return pht(
'SSH keys inherit the policies of the user or object they authenticate.');
}
@ -105,4 +157,26 @@ final class PhabricatorAuthSSHKey
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthSSHKeyEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthSSHKeyTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}

View file

@ -0,0 +1,59 @@
<?php
final class PhabricatorAuthSSHKeyTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'sshkey.name';
const TYPE_KEY = 'sshkey.key';
const TYPE_DEACTIVATE = 'sshkey.deactivate';
public function getApplicationName() {
return 'auth';
}
public function getApplicationTransactionType() {
return PhabricatorAuthSSHKeyPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this key.',
$this->renderHandleLink($author_phid));
case self::TYPE_NAME:
return pht(
'%s renamed this key from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
case self::TYPE_KEY:
return pht(
'%s updated the public key material for this SSH key.',
$this->renderHandleLink($author_phid));
case self::TYPE_DEACTIVATE:
if ($new) {
return pht(
'%s deactivated this key.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s activated this key.',
$this->renderHandleLink($author_phid));
}
}
return parent::getTitle();
}
}

View file

@ -8,6 +8,58 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
private $showTrusted;
private $showID;
public static function newKeyActionsMenu(
PhabricatorUser $viewer,
PhabricatorSSHPublicKeyInterface $object) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
$can_generate = true;
} catch (Exception $ex) {
$can_generate = false;
}
$object_phid = $object->getPHID();
$generate_uri = "/auth/sshkey/generate/?objectPHID={$object_phid}";
$upload_uri = "/auth/sshkey/upload/?objectPHID={$object_phid}";
$view_uri = "/auth/sshkey/for/{$object_phid}/";
$action_view = id(new PhabricatorActionListView())
->setUser($viewer)
->addAction(
id(new PhabricatorActionView())
->setHref($upload_uri)
->setWorkflow(true)
->setDisabled(!$can_edit)
->setName(pht('Upload Public Key'))
->setIcon('fa-upload'))
->addAction(
id(new PhabricatorActionView())
->setHref($generate_uri)
->setWorkflow(true)
->setDisabled(!$can_edit || !$can_generate)
->setName(pht('Generate Keypair'))
->setIcon('fa-lock'))
->addAction(
id(new PhabricatorActionView())
->setHref($view_uri)
->setName(pht('View History'))
->setIcon('fa-list-ul'));
return id(new PHUIButtonView())
->setTag('a')
->setText(pht('SSH Key Actions'))
->setHref('#')
->setIcon('fa-gear')
->setDropdownMenu($action_view);
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
@ -38,12 +90,6 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
$keys = $this->keys;
$viewer = $this->getUser();
if ($this->canEdit) {
$delete_class = 'small grey button';
} else {
$delete_class = 'small grey button disabled';
}
$trusted_icon = id(new PHUIIconView())
->setIcon('fa-star blue');
$untrusted_icon = id(new PHUIIconView())
@ -56,22 +102,13 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
javelin_tag(
'a',
array(
'href' => '/auth/sshkey/edit/'.$key->getID().'/',
'sigil' => 'workflow',
'href' => $key->getURI(),
),
$key->getName()),
$key->getIsTrusted() ? $trusted_icon : $untrusted_icon,
$key->getKeyComment(),
$key->getKeyType(),
phabricator_datetime($key->getDateCreated(), $viewer),
javelin_tag(
'a',
array(
'href' => '/auth/sshkey/delete/'.$key->getID().'/',
'class' => $delete_class,
'sigil' => 'workflow',
),
pht('Delete')),
);
}
@ -85,7 +122,6 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
pht('Comment'),
pht('Type'),
pht('Added'),
null,
))
->setColumnVisibility(
array(
@ -101,7 +137,6 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
'',
'',
'right',
'action',
));
return $table;

View file

@ -21,22 +21,4 @@ final class PhabricatorCalendarHoliday extends PhabricatorCalendarDAO {
) + parent::getConfiguration();
}
public static function getNthBusinessDay($epoch, $n) {
// Sadly, there are not many holidays. So we can load all of them.
$holidays = id(new PhabricatorCalendarHoliday())->loadAll();
$holidays = mpull($holidays, null, 'getDay');
$interval = ($n > 0 ? 1 : -1) * 24 * 60 * 60;
$return = $epoch;
for ($i = abs($n); $i > 0; ) {
$return += $interval;
$weekday = date('w', $return);
if ($weekday != 0 && $weekday != 6 && // Sunday and Saturday
!isset($holidays[date('Y-m-d', $return)])) {
$i--;
}
}
return $return;
}
}

View file

@ -16,24 +16,4 @@ final class PhabricatorCalendarHolidayTestCase extends PhabricatorTestCase {
->save();
}
public function testNthBusinessDay() {
$map = array(
array('2011-12-30', 1, '2012-01-03'),
array('2012-01-01', 1, '2012-01-03'),
array('2012-01-01', 0, '2012-01-01'),
array('2012-01-01', -1, '2011-12-30'),
array('2012-01-04', -1, '2012-01-03'),
);
foreach ($map as $val) {
list($date, $n, $expect) = $val;
$actual = PhabricatorCalendarHoliday::getNthBusinessDay(
strtotime($date),
$n);
$this->assertEqual(
$expect,
date('Y-m-d', $actual),
pht("%d business days since '%s'", $n, $date));
}
}
}

View file

@ -204,6 +204,7 @@ final class PhabricatorConduitAPIController
$stored_key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withKeys(array($public_key))
->withIsActive(true)
->executeOne();
if (!$stored_key) {
return array(

View file

@ -186,6 +186,10 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
'Configuration of the notification server has changed substantially. '.
'For discussion, see T10794.');
$stale_reason = pht(
'The Differential revision list view age UI elements have been removed '.
'to simplify the interface.');
$ancient_config += array(
'phid.external-loaders' =>
pht(
@ -314,6 +318,9 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
'metamta.differential.unified-comment-context' => pht(
'Inline comments are now always rendered with a limited amount '.
'of context.'),
'differential.days-fresh' => $stale_reason,
'differential.days-stale' => $stale_reason,
);
return $ancient_config;

View file

@ -102,68 +102,78 @@ final class PhabricatorDifferentialApplication extends PhabricatorApplication {
);
}
public static function loadNeedAttentionRevisions(PhabricatorUser $viewer) {
if (!$viewer->isLoggedIn()) {
return array();
}
$viewer_phid = $viewer->getPHID();
$responsible_phids = id(new DifferentialResponsibleDatasource())
->setViewer($viewer)
->evaluateTokens(array($viewer_phid));
$revision_query = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->withResponsibleUsers($responsible_phids)
->needReviewerStatus(true)
->needRelationships(true)
->needFlags(true)
->needDrafts(true)
->setLimit(self::MAX_STATUS_ITEMS);
$revisions = $revision_query->execute();
$query = id(new PhabricatorSavedQuery())
->attachParameterMap(
array(
'responsiblePHIDs' => $responsible_phids,
));
$groups = id(new DifferentialRevisionRequiredActionResultBucket())
->setViewer($viewer)
->newResultGroups($query, $revisions);
$include = array();
foreach ($groups as $group) {
switch ($group->getKey()) {
case DifferentialRevisionRequiredActionResultBucket::KEY_MUSTREVIEW:
case DifferentialRevisionRequiredActionResultBucket::KEY_SHOULDREVIEW:
foreach ($group->getObjects() as $object) {
$include[] = $object;
}
break;
default:
break;
}
}
return $include;
}
public function loadStatus(PhabricatorUser $user) {
$revisions = self::loadNeedAttentionRevisions($user);
$limit = self::MAX_STATUS_ITEMS;
$revisions = id(new DifferentialRevisionQuery())
->setViewer($user)
->withResponsibleUsers(array($user->getPHID()))
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->needRelationships(true)
->setLimit($limit)
->execute();
if (count($revisions) >= $limit) {
$display_count = ($limit - 1);
$display_label = pht(
'%s+ Active Review(s)',
new PhutilNumber($display_count));
} else {
$display_count = count($revisions);
$display_label = pht(
'%s Review(s) Need Attention',
new PhutilNumber($display_count));
}
$status = array();
if (count($revisions) >= $limit) {
$all_count = count($revisions);
$all_count_str = pht(
'%s+ Active Review(s)',
new PhutilNumber($limit - 1));
$type = PhabricatorApplicationStatusView::TYPE_WARNING;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText($all_count_str)
->setCount($all_count);
} else {
list($blocking, $active, $waiting) =
DifferentialRevisionQuery::splitResponsible(
$revisions,
array($user->getPHID()));
$blocking = count($blocking);
$blocking_str = pht(
'%s Review(s) Blocking Others',
new PhutilNumber($blocking));
$type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText($blocking_str)
->setCount($blocking);
$active = count($active);
$active_str = pht(
'%s Review(s) Need Attention',
new PhutilNumber($active));
$type = PhabricatorApplicationStatusView::TYPE_WARNING;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText($active_str)
->setCount($active);
$waiting = count($waiting);
$waiting_str = pht(
'%s Review(s) Waiting on Others',
new PhutilNumber($waiting));
$type = PhabricatorApplicationStatusView::TYPE_INFO;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText($waiting_str)
->setCount($waiting);
}
$status[] = id(new PhabricatorApplicationStatusView())
->setType(PhabricatorApplicationStatusView::TYPE_WARNING)
->setText($display_label)
->setCount($display_count);
return $status;
}

View file

@ -229,25 +229,6 @@ final class PhabricatorDifferentialConfigOptions
"\n\n".
'This sort of workflow is very unusual. Very few installs should '.
'need to change this option.')),
$this->newOption('differential.days-fresh', 'int', 1)
->setSummary(
pht(
"For how many business days should a revision be considered ".
"'fresh'?"))
->setDescription(
pht(
'Revisions newer than this number of days are marked as fresh in '.
'Action Required and Revisions Waiting on You views. Only work '.
'days (not weekends and holidays) are included. Set to 0 to '.
'disable this feature.')),
$this->newOption('differential.days-stale', 'int', 3)
->setSummary(
pht("After this many days, a revision will be considered 'stale'."))
->setDescription(
pht(
"Similar to `%s` but marks stale revisions. ".
"If the revision is even older than it is when marked as 'old'.",
'differential.days-fresh')),
$this->newOption(
'metamta.differential.subject-prefix',
'string',

View file

@ -35,27 +35,39 @@ abstract class DifferentialCustomField
protected function parseObjectList(
$value,
array $types,
$allow_partial = false) {
$allow_partial = false,
array $suffixes = array()) {
return id(new PhabricatorObjectListQuery())
->setViewer($this->getViewer())
->setAllowedTypes($types)
->setObjectList($value)
->setAllowPartialResults($allow_partial)
->setSuffixes($suffixes)
->execute();
}
protected function renderObjectList(array $handles) {
protected function renderObjectList(
array $handles,
array $suffixes = array()) {
if (!$handles) {
return null;
}
$out = array();
foreach ($handles as $handle) {
$phid = $handle->getPHID();
if ($handle->getPolicyFiltered()) {
$out[] = $handle->getPHID();
$token = $phid;
} else if ($handle->isComplete()) {
$out[] = $handle->getObjectName();
$token = $handle->getCommandLineObjectName();
}
$suffix = idx($suffixes, $phid);
$token = $token.$suffix;
$out[] = $token;
}
return implode(', ', $out);

View file

@ -8,7 +8,7 @@ final class DifferentialProjectReviewersField
}
public function getFieldName() {
return pht('Project Reviewers');
return pht('Group Reviewers');
}
public function getFieldDescription() {

View file

@ -36,43 +36,82 @@ final class DifferentialReviewersField
}
public function readValueFromRequest(AphrontRequest $request) {
// Compute a new set of reviewer objects. For reviewers who haven't been
// added or removed, retain their existing status. Also, respect the new
// order.
$old_status = $this->getValue();
$old_status = mpull($old_status, null, 'getReviewerPHID');
$datasource = id(new DifferentialBlockingReviewerDatasource())
->setViewer($request->getViewer());
$new_phids = $request->getArr($this->getFieldKey());
$new_phids = array_fuse($new_phids);
$new_phids = $datasource->evaluateTokens($new_phids);
$new_status = array();
foreach ($new_phids as $new_phid) {
if (empty($old_status[$new_phid])) {
$new_status[$new_phid] = new DifferentialReviewer(
$new_phid,
array(
'status' => DifferentialReviewerStatus::STATUS_ADDED,
));
$reviewers = array();
foreach ($new_phids as $spec) {
if (!is_array($spec)) {
$reviewers[$spec] = DifferentialReviewerStatus::STATUS_ADDED;
} else {
$new_status[$new_phid] = $old_status[$new_phid];
$reviewers[$spec['phid']] = $spec['type'];
}
}
$this->setValue($new_status);
$this->updateReviewers($this->getValue(), $reviewers);
}
private function updateReviewers(array $old_reviewers, array $new_map) {
// Compute a new set of reviewer objects. We're going to respect the new
// reviewer order, add or remove any new or missing reviewers, and respect
// any blocking or unblocking changes. For reviewers who were there before
// and are still there, we're going to keep the old value because it
// may be something like "Accept", "Reject", etc.
$old_map = mpull($old_reviewers, 'getStatus', 'getReviewerPHID');
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
$new_reviewers = array();
foreach ($new_map as $phid => $new) {
$old = idx($old_map, $phid);
// If we have an old status and this didn't make the reviewer blocking
// or nonblocking, just retain the old status. This makes sure we don't
// throw away rejects, accepts, etc.
if ($old) {
$is_block = ($old !== $status_blocking && $new === $status_blocking);
$is_unblock = ($old === $status_blocking && $new !== $status_blocking);
if (!$is_block && !$is_unblock) {
$new_reviewers[$phid] = $old;
continue;
}
}
$new_reviewers[$phid] = $new;
}
foreach ($new_reviewers as $phid => $status) {
$new_reviewers[$phid] = new DifferentialReviewer(
$phid,
array(
'status' => $status,
));
}
$this->setValue($new_reviewers);
}
public function renderEditControl(array $handles) {
$phids = array();
if ($this->getValue()) {
$phids = mpull($this->getValue(), 'getReviewerPHID');
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
$value = array();
foreach ($this->getValue() as $reviewer) {
$phid = $reviewer->getReviewerPHID();
if ($reviewer->getStatus() == $status_blocking) {
$value[] = 'blocking('.$phid.')';
} else {
$value[] = $phid;
}
}
return id(new AphrontFormTokenizerControl())
->setUser($this->getViewer())
->setName($this->getFieldKey())
->setDatasource(new PhabricatorProjectOrUserDatasource())
->setValue($phids)
->setDatasource(new DifferentialReviewerDatasource())
->setValue($value)
->setError($this->getFieldError())
->setLabel($this->getFieldName());
}
@ -141,12 +180,17 @@ final class DifferentialReviewersField
}
public function parseValueFromCommitMessage($value) {
return $this->parseObjectList(
$results = $this->parseObjectList(
$value,
array(
PhabricatorPeopleUserPHIDType::TYPECONST,
PhabricatorProjectProjectPHIDType::TYPECONST,
));
PhabricatorOwnersPackagePHIDType::TYPECONST,
),
false,
array('!'));
return $this->flattenReviewers($results);
}
public function getRequiredHandlePHIDsForCommitMessage() {
@ -154,29 +198,42 @@ final class DifferentialReviewersField
}
public function readValueFromCommitMessage($value) {
$current_reviewers = $this->getObject()->getReviewerStatus();
$current_reviewers = mpull($current_reviewers, null, 'getReviewerPHID');
$value = $this->inflateReviewers($value);
$reviewers = array();
foreach ($value as $phid) {
$reviewer = idx($current_reviewers, $phid);
if ($reviewer) {
$reviewers[] = $reviewer;
foreach ($value as $spec) {
$phid = $spec['phid'];
$is_blocking = isset($spec['suffixes']['!']);
if ($is_blocking) {
$status = DifferentialReviewerStatus::STATUS_BLOCKING;
} else {
$data = array(
'status' => DifferentialReviewerStatus::STATUS_ADDED,
);
$reviewers[] = new DifferentialReviewer($phid, $data);
$status = DifferentialReviewerStatus::STATUS_ADDED;
}
$reviewers[$phid] = $status;
}
$this->setValue($reviewers);
$this->updateReviewers(
$this->getObject()->getReviewerStatus(),
$reviewers);
return $this;
}
public function renderCommitMessageValue(array $handles) {
return $this->renderObjectList($handles);
$suffixes = array();
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
foreach ($this->getValue() as $reviewer) {
if ($reviewer->getStatus() == $status_blocking) {
$phid = $reviewer->getReviewerPHID();
$suffixes[$phid] = '!';
}
}
return $this->renderObjectList($handles, $suffixes);
}
public function validateCommitMessageValue($value) {
@ -185,7 +242,9 @@ final class DifferentialReviewersField
$config_self_accept_key = 'differential.allow-self-accept';
$allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
foreach ($value as $phid) {
foreach ($value as $spec) {
$phid = $spec['phid'];
if (($phid == $author_phid) && !$allow_self_accept) {
throw new DifferentialFieldValidationException(
pht('The author of a revision can not be a reviewer.'));
@ -224,4 +283,44 @@ final class DifferentialReviewersField
return $warnings;
}
public function getProTips() {
return array(
pht(
'You can mark a reviewer as blocking by adding an exclamation '.
'mark ("!") after their name.'),
);
}
private function flattenReviewers(array $values) {
// NOTE: For now, `arc` relies on this field returning only scalars, so we
// need to reduce the results into scalars. See T10981.
$result = array();
foreach ($values as $value) {
$result[] = $value['phid'].implode('', array_keys($value['suffixes']));
}
return $result;
}
private function inflateReviewers(array $values) {
$result = array();
foreach ($values as $value) {
if (substr($value, -1) == '!') {
$value = substr($value, 0, -1);
$suffixes = array('!' => '!');
} else {
$suffixes = array();
}
$result[] = array(
'phid' => $value,
'suffixes' => $suffixes,
);
}
return $result;
}
}

View file

@ -78,6 +78,7 @@ final class DifferentialSubscribersField
array(
PhabricatorPeopleUserPHIDType::TYPECONST,
PhabricatorProjectProjectPHIDType::TYPECONST,
PhabricatorOwnersPackagePHIDType::TYPECONST,
));
}

View file

@ -7,6 +7,7 @@ final class DifferentialTransactionEditor
private $isCloseByCommit;
private $repositoryPHIDOverride = false;
private $didExpandInlineState = false;
private $affectedPaths;
public function getEditorApplicationClass() {
return 'PhabricatorDifferentialApplication';
@ -1200,7 +1201,13 @@ final class DifferentialTransactionEditor
$body = new PhabricatorMetaMTAMailBody();
$body->setViewer($this->requireActor());
$this->addHeadersAndCommentsToMailBody($body, $xactions);
$revision_uri = PhabricatorEnv::getProductionURI('/D'.$object->getID());
$this->addHeadersAndCommentsToMailBody(
$body,
$xactions,
pht('View Revision'),
$revision_uri);
$type_inline = DifferentialTransaction::TYPE_INLINE;
@ -1226,7 +1233,7 @@ final class DifferentialTransactionEditor
$body->addLinkSection(
pht('REVISION DETAIL'),
PhabricatorEnv::getProductionURI('/D'.$object->getID()));
$revision_uri);
$update_xaction = null;
foreach ($xactions as $xaction) {
@ -1481,6 +1488,181 @@ final class DifferentialTransactionEditor
return parent::shouldApplyHeraldRules($object, $xactions);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$repository = $object->getRepository();
if (!$repository) {
return array();
}
if (!$this->affectedPaths) {
return array();
}
$packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$this->affectedPaths);
foreach ($packages as $key => $package) {
if ($package->isArchived()) {
unset($packages[$key]);
}
}
if (!$packages) {
return array();
}
// Remove packages that the revision author is an owner of. If you own
// code, you don't need another owner to review it.
$authority = id(new PhabricatorOwnersPackageQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(mpull($packages, 'getPHID'))
->withAuthorityPHIDs(array($object->getAuthorPHID()))
->execute();
$authority = mpull($authority, null, 'getPHID');
foreach ($packages as $key => $package) {
$package_phid = $package->getPHID();
if ($authority[$package_phid]) {
unset($packages[$key]);
continue;
}
}
if (!$packages) {
return array();
}
$auto_subscribe = array();
$auto_review = array();
$auto_block = array();
foreach ($packages as $package) {
switch ($package->getAutoReview()) {
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE:
$auto_subscribe[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW:
$auto_review[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK:
$auto_block[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_NONE:
default:
break;
}
}
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
$xactions = array();
if ($auto_subscribe) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setAuthorPHID($owners_phid)
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => mpull($auto_subscribe, 'getPHID'),
));
}
$specs = array(
array($auto_review, false),
array($auto_block, true),
);
foreach ($specs as $spec) {
list($reviewers, $blocking) = $spec;
if (!$reviewers) {
continue;
}
$phids = mpull($reviewers, 'getPHID');
$xaction = $this->newAutoReviewTransaction($object, $phids, $blocking);
if ($xaction) {
$xactions[] = $xaction;
}
}
return $xactions;
}
private function newAutoReviewTransaction(
PhabricatorLiskDAO $object,
array $phids,
$is_blocking) {
// TODO: This is substantially similar to DifferentialReviewersHeraldAction
// and both are needlessly complex. This logic should live in the normal
// transaction application pipeline. See T10967.
$reviewers = $object->getReviewerStatus();
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
if ($is_blocking) {
$new_status = DifferentialReviewerStatus::STATUS_BLOCKING;
} else {
$new_status = DifferentialReviewerStatus::STATUS_ADDED;
}
$new_strength = DifferentialReviewerStatus::getStatusStrength(
$new_status);
$current = array();
foreach ($phids as $phid) {
if (!isset($reviewers[$phid])) {
continue;
}
// If we're applying a stronger status (usually, upgrading a reviewer
// into a blocking reviewer), skip this check so we apply the change.
$old_strength = DifferentialReviewerStatus::getStatusStrength(
$reviewers[$phid]->getStatus());
if ($old_strength <= $new_strength) {
continue;
}
$current[] = $phid;
}
$phids = array_diff($phids, $current);
if (!$phids) {
return null;
}
$phids = array_fuse($phids);
$value = array();
foreach ($phids as $phid) {
$value[$phid] = array(
'data' => array(
'status' => $new_status,
),
);
}
$edgetype_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
return $object->getApplicationTransactionTemplate()
->setAuthorPHID($owners_phid)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edgetype_reviewer)
->setNewValue(
array(
'+' => $value,
));
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
@ -1547,6 +1729,10 @@ final class DifferentialTransactionEditor
$paths[] = $path_prefix.'/'.$changeset->getFilename();
}
// Save the affected paths; we'll use them later to query Owners. This
// uses the un-expanded paths.
$this->affectedPaths = $paths;
// Mark this as also touching all parent paths, so you can see all pending
// changes to any file within a directory.
$all_paths = array();

View file

@ -22,7 +22,7 @@ final class DifferentialReviewersAddBlockingReviewersHeraldAction
}
protected function getDatasource() {
return new PhabricatorMetaMTAMailableDatasource();
return new DiffusionAuditorDatasource();
}
public function renderActionDescription($value) {

View file

@ -22,7 +22,7 @@ final class DifferentialReviewersAddReviewersHeraldAction
}
protected function getDatasource() {
return new PhabricatorMetaMTAMailableDatasource();
return new DiffusionAuditorDatasource();
}
public function renderActionDescription($value) {

View file

@ -69,6 +69,7 @@ abstract class DifferentialReviewersHeraldAction
$allowed_types = array(
PhabricatorPeopleUserPHIDType::TYPECONST,
PhabricatorProjectProjectPHIDType::TYPECONST,
PhabricatorOwnersPackagePHIDType::TYPECONST,
);
$targets = $this->loadStandardTargets($phids, $allowed_types, $current);

View file

@ -91,18 +91,6 @@ final class DifferentialRevisionQuery
return $this;
}
/**
* Filter results to revisions with comments authored by the given PHIDs.
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withDraftRepliesByAuthors(array $author_phids) {
$this->draftAuthors = $author_phids;
return $this;
}
/**
* Filter results to revisions which CC one of the listed people. Calling this
* function will clear anything set by previous calls to @{method:withCCs}.
@ -239,27 +227,6 @@ final class DifferentialRevisionQuery
}
/**
* Set result ordering. Provide a class constant, such as
* `DifferentialRevisionQuery::ORDER_CREATED`.
*
* @task config
*/
public function setOrder($order_constant) {
switch ($order_constant) {
case self::ORDER_CREATED:
$this->setOrderVector(array('id'));
break;
case self::ORDER_MODIFIED:
$this->setOrderVector(array('updated', 'id'));
break;
default:
throw new Exception(pht('Unknown order "%s".', $order_constant));
}
return $this;
}
/**
* Set whether or not the query will load and attach relationships.
@ -371,6 +338,11 @@ final class DifferentialRevisionQuery
/* -( Query Execution )---------------------------------------------------- */
public function newResultObject() {
return new DifferentialRevision();
}
/**
* Execute the query as configured, returning matching
* @{class:DifferentialRevision} objects.
@ -379,11 +351,9 @@ final class DifferentialRevisionQuery
* @task exec
*/
protected function loadPage() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
$data = $this->loadData();
$table = $this->newResultObject();
return $table->loadAllFromArray($data);
}
@ -519,7 +489,7 @@ final class DifferentialRevisionQuery
}
private function loadData() {
$table = new DifferentialRevision();
$table = $this->newResultObject();
$conn_r = $table->establishConnection('r');
$selects = array();
@ -531,25 +501,17 @@ final class DifferentialRevisionQuery
$basic_authors = $this->authors;
$basic_reviewers = $this->reviewers;
$authority_projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs($this->responsibles)
->execute();
$authority_phids = mpull($authority_projects, 'getPHID');
try {
// Build the query where the responsible users are authors.
$this->authors = array_merge($basic_authors, $this->responsibles);
$this->reviewers = $basic_reviewers;
$selects[] = $this->buildSelectStatement($conn_r);
// Build the query where the responsible users are reviewers, or
// projects they are members of are reviewers.
$this->authors = $basic_authors;
$this->reviewers = array_merge(
$basic_reviewers,
$this->responsibles,
$authority_phids);
$this->reviewers = array_merge($basic_reviewers, $this->responsibles);
$selects[] = $this->buildSelectStatement($conn_r);
// Put everything back like it was.
@ -591,7 +553,7 @@ final class DifferentialRevisionQuery
$joins = $this->buildJoinsClause($conn_r);
$where = $this->buildWhereClause($conn_r);
$group_by = $this->buildGroupByClause($conn_r);
$group_by = $this->buildGroupClause($conn_r);
$having = $this->buildHavingClause($conn_r);
$this->buildingGlobalOrder = false;
@ -835,19 +797,37 @@ final class DifferentialRevisionQuery
/**
* @task internal
*/
private function buildGroupByClause($conn_r) {
protected function shouldGroupQueryResultRows() {
$join_triggers = array_merge(
$this->pathIDs,
$this->ccs,
$this->reviewers);
$needs_distinct = (count($join_triggers) > 1);
if ($needs_distinct) {
return 'GROUP BY r.id';
} else {
return '';
if (count($join_triggers) > 1) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getBuiltinOrders() {
$orders = parent::getBuiltinOrders() + array(
'updated' => array(
'vector' => array('updated', 'id'),
'name' => pht('Date Updated (Latest First)'),
'aliases' => array(self::ORDER_MODIFIED),
),
'outdated' => array(
'vector' => array('-updated', '-id'),
'name' => pht('Date Updated (Oldest First)'),
),
);
// Alias the "newest" builtin to the historical key for it.
$orders['newest']['aliases'][] = self::ORDER_CREATED;
return $orders;
}
protected function getDefaultOrderVector() {
@ -1053,50 +1033,6 @@ final class DifferentialRevisionQuery
}
}
public static function splitResponsible(array $revisions, array $user_phids) {
$blocking = array();
$active = array();
$waiting = array();
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
// Bucket revisions into $blocking (revisions where you are blocking
// others), $active (revisions you need to do something about) and $waiting
// (revisions you're waiting on someone else to do something about).
foreach ($revisions as $revision) {
$needs_review = ($revision->getStatus() == $status_review);
$filter_is_author = in_array($revision->getAuthorPHID(), $user_phids);
if (!$revision->getReviewers()) {
$needs_review = false;
$author_is_reviewer = false;
} else {
$author_is_reviewer = in_array(
$revision->getAuthorPHID(),
$revision->getReviewers());
}
// If exactly one of "needs review" and "the user is the author" is
// true, the user needs to act on it. Otherwise, they're waiting on
// it.
if ($needs_review ^ $filter_is_author) {
if ($needs_review) {
array_unshift($blocking, $revision);
} else {
$active[] = $revision;
}
// User is author **and** reviewer. An exotic but configurable workflow.
// User needs to act on it double.
} else if ($needs_review && $author_is_reviewer) {
array_unshift($blocking, $revision);
$active[] = $revision;
} else {
$waiting[] = $revision;
}
}
return array($blocking, $active, $waiting);
}
private function loadReviewerAuthority(
array $revisions,
array $edges,
@ -1105,9 +1041,13 @@ final class DifferentialRevisionQuery
$revision_map = mpull($revisions, null, 'getPHID');
$viewer_phid = $this->getViewer()->getPHID();
// Find all the project reviewers which the user may have authority over.
// Find all the project/package reviewers which the user may have authority
// over.
$project_phids = array();
$package_phids = array();
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
$package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
$edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
foreach ($edges as $src => $types) {
if (!$allow_self) {
@ -1121,14 +1061,20 @@ final class DifferentialRevisionQuery
}
$edge_data = idx($types, $edge_type, array());
foreach ($edge_data as $dst => $data) {
if (phid_get_type($dst) == $project_type) {
$phid_type = phid_get_type($dst);
if ($phid_type == $project_type) {
$project_phids[] = $dst;
}
if ($phid_type == $package_type) {
$package_phids[] = $dst;
}
}
}
// Now, figure out which of these projects the viewer is actually a
// member of.
// The viewer has authority over themselves.
$user_authority = array_fuse(array($viewer_phid));
// And over any projects they are a member of.
$project_authority = array();
if ($project_phids) {
$project_authority = id(new PhabricatorProjectQuery())
@ -1137,12 +1083,22 @@ final class DifferentialRevisionQuery
->withMemberPHIDs(array($viewer_phid))
->execute();
$project_authority = mpull($project_authority, 'getPHID');
$project_authority = array_fuse($project_authority);
}
// Finally, the viewer has authority over themselves.
return array(
$viewer_phid => true,
) + array_fuse($project_authority);
// And over any packages they own.
$package_authority = array();
if ($package_phids) {
$package_authority = id(new PhabricatorOwnersPackageQuery())
->setViewer($this->getViewer())
->withPHIDs($package_phids)
->withAuthorityPHIDs(array($viewer_phid))
->execute();
$package_authority = mpull($package_authority, 'getPHID');
$package_authority = array_fuse($package_authority);
}
return $user_authority + $project_authority + $package_authority;
}
public function getQueryApplicationClass() {

View file

@ -0,0 +1,206 @@
<?php
final class DifferentialRevisionRequiredActionResultBucket
extends DifferentialRevisionResultBucket {
const BUCKETKEY = 'action';
const KEY_MUSTREVIEW = 'must-review';
const KEY_SHOULDREVIEW = 'should-review';
private $objects;
public function getResultBucketName() {
return pht('Bucket by Required Action');
}
protected function buildResultGroups(
PhabricatorSavedQuery $query,
array $objects) {
$this->objects = $objects;
$phids = $query->getEvaluatedParameter('responsiblePHIDs', array());
if (!$phids) {
throw new Exception(
pht(
'You can not bucket results by required action without '.
'specifying "Responsible Users".'));
}
$phids = array_fuse($phids);
$groups = array();
$groups[] = $this->newGroup()
->setName(pht('Must Review'))
->setKey(self::KEY_MUSTREVIEW)
->setNoDataString(pht('No revisions are blocked on your review.'))
->setObjects($this->filterMustReview($phids));
$groups[] = $this->newGroup()
->setName(pht('Ready to Review'))
->setKey(self::KEY_SHOULDREVIEW)
->setNoDataString(pht('No revisions are waiting on you to review them.'))
->setObjects($this->filterShouldReview($phids));
$groups[] = $this->newGroup()
->setName(pht('Ready to Land'))
->setNoDataString(pht('No revisions are ready to land.'))
->setObjects($this->filterShouldLand($phids));
$groups[] = $this->newGroup()
->setName(pht('Ready to Update'))
->setNoDataString(pht('No revisions are waiting for updates.'))
->setObjects($this->filterShouldUpdate($phids));
$groups[] = $this->newGroup()
->setName(pht('Waiting on Review'))
->setNoDataString(pht('None of your revisions are waiting on review.'))
->setObjects($this->filterWaitingForReview($phids));
$groups[] = $this->newGroup()
->setName(pht('Waiting on Authors'))
->setNoDataString(pht('No revisions are waiting on author action.'))
->setObjects($this->filterWaitingOnAuthors($phids));
// Because you can apply these buckets to queries which include revisions
// that have been closed, add an "Other" bucket if we still have stuff
// that didn't get filtered into any of the previous buckets.
if ($this->objects) {
$groups[] = $this->newGroup()
->setName(pht('Other Revisions'))
->setObjects($this->objects);
}
return $groups;
}
private function filterMustReview(array $phids) {
$blocking = array(
DifferentialReviewerStatus::STATUS_BLOCKING,
DifferentialReviewerStatus::STATUS_REJECTED,
DifferentialReviewerStatus::STATUS_REJECTED_OLDER,
);
$blocking = array_fuse($blocking);
$objects = $this->getRevisionsUnderReview($this->objects, $phids);
$results = array();
foreach ($objects as $key => $object) {
if (!$this->hasReviewersWithStatus($object, $phids, $blocking)) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
private function filterShouldReview(array $phids) {
$reviewing = array(
DifferentialReviewerStatus::STATUS_ADDED,
DifferentialReviewerStatus::STATUS_COMMENTED,
);
$reviewing = array_fuse($reviewing);
$objects = $this->getRevisionsUnderReview($this->objects, $phids);
$results = array();
foreach ($objects as $key => $object) {
if (!$this->hasReviewersWithStatus($object, $phids, $reviewing)) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
private function filterShouldLand(array $phids) {
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
$objects = $this->getRevisionsAuthored($this->objects, $phids);
$results = array();
foreach ($objects as $key => $object) {
if ($object->getStatus() != $status_accepted) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
private function filterShouldUpdate(array $phids) {
$statuses = array(
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::CHANGES_PLANNED,
ArcanistDifferentialRevisionStatus::IN_PREPARATION,
);
$statuses = array_fuse($statuses);
$objects = $this->getRevisionsAuthored($this->objects, $phids);
$results = array();
foreach ($objects as $key => $object) {
if (empty($statuses[$object->getStatus()])) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
private function filterWaitingForReview(array $phids) {
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
$objects = $this->getRevisionsAuthored($this->objects, $phids);
$results = array();
foreach ($objects as $key => $object) {
if ($object->getStatus() != $status_review) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
private function filterWaitingOnAuthors(array $phids) {
$statuses = array(
ArcanistDifferentialRevisionStatus::ACCEPTED,
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::CHANGES_PLANNED,
ArcanistDifferentialRevisionStatus::IN_PREPARATION,
);
$statuses = array_fuse($statuses);
$objects = $this->getRevisionsNotAuthored($this->objects, $phids);
$results = array();
foreach ($objects as $key => $object) {
if (empty($statuses[$object->getStatus()])) {
continue;
}
$results[$key] = $object;
unset($this->objects[$key]);
}
return $results;
}
}

View file

@ -0,0 +1,77 @@
<?php
abstract class DifferentialRevisionResultBucket
extends PhabricatorSearchResultBucket {
public static function getAllResultBuckets() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getResultBucketKey')
->execute();
}
protected function getRevisionsUnderReview(array $objects, array $phids) {
$results = array();
$objects = $this->getRevisionsNotAuthored($objects, $phids);
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
foreach ($objects as $key => $object) {
if ($object->getStatus() != $status_review) {
continue;
}
$results[$key] = $object;
}
return $results;
}
protected function getRevisionsAuthored(array $objects, array $phids) {
$results = array();
foreach ($objects as $key => $object) {
if (isset($phids[$object->getAuthorPHID()])) {
$results[$key] = $object;
}
}
return $results;
}
protected function getRevisionsNotAuthored(array $objects, array $phids) {
$results = array();
foreach ($objects as $key => $object) {
if (empty($phids[$object->getAuthorPHID()])) {
$results[$key] = $object;
}
}
return $results;
}
protected function hasReviewersWithStatus(
DifferentialRevision $revision,
array $phids,
array $statuses) {
foreach ($revision->getReviewerStatus() as $reviewer) {
$reviewer_phid = $reviewer->getReviewerPHID();
if (empty($phids[$reviewer_phid])) {
continue;
}
$status = $reviewer->getStatus();
if (empty($statuses[$status])) {
continue;
}
return true;
}
return false;
}
}

View file

@ -11,203 +11,80 @@ final class DifferentialRevisionSearchEngine
return 'PhabricatorDifferentialApplication';
}
protected function newResultBuckets() {
return DifferentialRevisionResultBucket::getAllResultBuckets();
}
public function newQuery() {
return id(new DifferentialRevisionQuery())
->needFlags(true)
->needDrafts(true)
->needRelationships(true);
->needRelationships(true)
->needReviewerStatus(true);
}
public function getPageSize(PhabricatorSavedQuery $saved) {
if ($saved->getQueryKey() == 'active') {
return 0xFFFF;
}
return parent::getPageSize($saved);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter(
'responsiblePHIDs',
$this->readUsersFromRequest($request, 'responsibles'));
$saved->setParameter(
'authorPHIDs',
$this->readUsersFromRequest($request, 'authors'));
$saved->setParameter(
'reviewerPHIDs',
$this->readUsersFromRequest(
$request,
'reviewers',
array(
PhabricatorProjectProjectPHIDType::TYPECONST,
)));
$saved->setParameter(
'subscriberPHIDs',
$this->readSubscribersFromRequest($request, 'subscribers'));
$saved->setParameter(
'repositoryPHIDs',
$request->getArr('repositories'));
$saved->setParameter(
'projects',
$this->readProjectsFromRequest($request, 'projects'));
$saved->setParameter(
'draft',
$request->getBool('draft'));
$saved->setParameter(
'order',
$request->getStr('order'));
$saved->setParameter(
'status',
$request->getStr('status'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new DifferentialRevisionQuery())
->needFlags(true)
->needDrafts(true)
->needRelationships(true);
$user_datasource = id(new PhabricatorPeopleUserFunctionDatasource())
->setViewer($this->requireViewer());
$responsible_phids = $saved->getParameter('responsiblePHIDs', array());
$responsible_phids = $user_datasource->evaluateTokens($responsible_phids);
if ($responsible_phids) {
$query->withResponsibleUsers($responsible_phids);
if ($map['responsiblePHIDs']) {
$query->withResponsibleUsers($map['responsiblePHIDs']);
}
$this->setQueryProjects($query, $saved);
$author_phids = $saved->getParameter('authorPHIDs', array());
$author_phids = $user_datasource->evaluateTokens($author_phids);
if ($author_phids) {
$query->withAuthors($author_phids);
if ($map['authorPHIDs']) {
$query->withAuthors($map['authorPHIDs']);
}
$reviewer_phids = $saved->getParameter('reviewerPHIDs', array());
if ($reviewer_phids) {
$query->withReviewers($reviewer_phids);
if ($map['reviewerPHIDs']) {
$query->withReviewers($map['reviewerPHIDs']);
}
$sub_datasource = id(new PhabricatorMetaMTAMailableFunctionDatasource())
->setViewer($this->requireViewer());
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$subscriber_phids = $sub_datasource->evaluateTokens($subscriber_phids);
if ($subscriber_phids) {
$query->withCCs($subscriber_phids);
if ($map['repositoryPHIDs']) {
$query->withRepositoryPHIDs($map['repositoryPHIDs']);
}
$repository_phids = $saved->getParameter('repositoryPHIDs', array());
if ($repository_phids) {
$query->withRepositoryPHIDs($repository_phids);
}
$draft = $saved->getParameter('draft', false);
if ($draft && $this->requireViewer()->isLoggedIn()) {
$query->withDraftRepliesByAuthors(
array($this->requireViewer()->getPHID()));
}
$status = $saved->getParameter('status');
if (idx($this->getStatusOptions(), $status)) {
$query->withStatus($status);
}
$order = $saved->getParameter('order');
if (idx($this->getOrderOptions(), $order)) {
$query->setOrder($order);
} else {
$query->setOrder(DifferentialRevisionQuery::ORDER_CREATED);
if ($map['status']) {
$query->withStatus($map['status']);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$responsible_phids = $saved->getParameter('responsiblePHIDs', array());
$author_phids = $saved->getParameter('authorPHIDs', array());
$reviewer_phids = $saved->getParameter('reviewerPHIDs', array());
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$repository_phids = $saved->getParameter('repositoryPHIDs', array());
$only_draft = $saved->getParameter('draft', false);
$projects = $saved->getParameter('projects', array());
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Responsible Users'))
->setName('responsibles')
->setDatasource(new PhabricatorPeopleUserFunctionDatasource())
->setValue($responsible_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Authors'))
->setName('authors')
->setDatasource(new PhabricatorPeopleUserFunctionDatasource())
->setValue($author_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Reviewers'))
->setName('reviewers')
->setDatasource(new PhabricatorProjectOrUserDatasource())
->setValue($reviewer_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Subscribers'))
->setName('subscribers')
->setDatasource(new PhabricatorMetaMTAMailableFunctionDatasource())
->setValue($subscriber_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Repositories'))
->setName('repositories')
->setDatasource(new DiffusionRepositoryDatasource())
->setValue($repository_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Tags'))
->setName('projects')
->setDatasource(new PhabricatorProjectLogicalDatasource())
->setValue($projects))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setOptions($this->getStatusOptions())
->setValue($saved->getParameter('status')));
if ($this->requireViewer()->isLoggedIn()) {
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'draft',
1,
pht('Show only revisions with a draft comment.'),
$only_draft));
}
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Order'))
->setName('order')
->setOptions($this->getOrderOptions())
->setValue($saved->getParameter('order')));
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Responsible Users'))
->setKey('responsiblePHIDs')
->setAliases(array('responsiblePHID', 'responsibles', 'responsible'))
->setDatasource(new DifferentialResponsibleDatasource())
->setDescription(
pht('Find revisions that a given user is responsible for.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Authors'))
->setKey('authorPHIDs')
->setAliases(array('author', 'authors', 'authorPHID'))
->setDescription(
pht('Find revisions with specific authors.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Reviewers'))
->setKey('reviewerPHIDs')
->setAliases(array('reviewer', 'reviewers', 'reviewerPHID'))
->setDatasource(new DiffusionAuditorFunctionDatasource())
->setDescription(
pht('Find revisions with specific reviewers.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Repositories'))
->setKey('repositoryPHIDs')
->setAliases(array('repository', 'repositories', 'repositoryPHID'))
->setDatasource(new DiffusionRepositoryDatasource())
->setDescription(
pht('Find revisions from specific repositories.')),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Status'))
->setKey('status')
->setOptions($this->getStatusOptions())
->setDescription(
pht('Find revisions with particular statuses.')),
);
}
protected function getURI($path) {
@ -235,9 +112,12 @@ final class DifferentialRevisionSearchEngine
switch ($query_key) {
case 'active':
$bucket_key = DifferentialRevisionRequiredActionResultBucket::BUCKETKEY;
return $query
->setParameter('responsiblePHIDs', array($viewer->getPHID()))
->setParameter('status', DifferentialRevisionQuery::STATUS_OPEN);
->setParameter('status', DifferentialRevisionQuery::STATUS_OPEN)
->setParameter('bucket', $bucket_key);
case 'authored':
return $query
->setParameter('authorPHIDs', array($viewer->getPHID()));
@ -260,13 +140,6 @@ final class DifferentialRevisionSearchEngine
);
}
private function getOrderOptions() {
return array(
DifferentialRevisionQuery::ORDER_CREATED => pht('Created'),
DifferentialRevisionQuery::ORDER_MODIFIED => pht('Updated'),
);
}
protected function renderResultList(
array $revisions,
PhabricatorSavedQuery $query,
@ -278,35 +151,26 @@ final class DifferentialRevisionSearchEngine
->setUser($viewer)
->setNoBox($this->isPanelContext());
$bucket = $this->getResultBucket($query);
$unlanded = $this->loadUnlandedDependencies($revisions);
$views = array();
if ($query->getQueryKey() == 'active') {
$split = DifferentialRevisionQuery::splitResponsible(
$revisions,
$query->getParameter('responsiblePHIDs'));
list($blocking, $active, $waiting) = $split;
if ($bucket) {
$bucket->setViewer($viewer);
$views[] = id(clone $template)
->setHeader(pht('Blocking Others'))
->setNoDataString(
pht('No revisions are blocked on your action.'))
->setHighlightAge(true)
->setRevisions($blocking)
->setHandles(array());
try {
$groups = $bucket->newResultGroups($query, $revisions);
$views[] = id(clone $template)
->setHeader(pht('Action Required'))
->setNoDataString(
pht('No revisions require your action.'))
->setHighlightAge(true)
->setRevisions($active)
->setHandles(array());
$views[] = id(clone $template)
->setHeader(pht('Waiting on Others'))
->setNoDataString(
pht('You have no revisions waiting on others.'))
->setRevisions($waiting)
->setHandles(array());
foreach ($groups as $group) {
$views[] = id(clone $template)
->setHeader($group->getName())
->setNoDataString($group->getNoDataString())
->setRevisions($group->getObjects());
}
} catch (Exception $ex) {
$this->addError($ex->getMessage());
}
} else {
$views[] = id(clone $template)
->setRevisions($revisions)
@ -325,6 +189,7 @@ final class DifferentialRevisionSearchEngine
foreach ($views as $view) {
$view->setHandles($handles);
$view->setUnlandedDependencies($unlanded);
}
if (count($views) == 1) {
@ -361,4 +226,56 @@ final class DifferentialRevisionSearchEngine
return $view;
}
private function loadUnlandedDependencies(array $revisions) {
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
$phids = array();
foreach ($revisions as $revision) {
if ($revision->getStatus() != $status_accepted) {
continue;
}
$phids[] = $revision->getPHID();
}
if (!$phids) {
return array();
}
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($phids)
->withEdgeTypes(
array(
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
));
$query->execute();
$revision_phids = $query->getDestinationPHIDs();
if (!$revision_phids) {
return array();
}
$viewer = $this->requireViewer();
$blocking_revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withPHIDs($revision_phids)
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->execute();
$blocking_revisions = mpull($blocking_revisions, null, 'getPHID');
$result = array();
foreach ($revisions as $revision) {
$revision_phid = $revision->getPHID();
$blocking_phids = $query->getDestinationPHIDs(array($revision_phid));
$blocking = array_select_keys($blocking_revisions, $blocking_phids);
if ($blocking) {
$result[$revision_phid] = $blocking;
}
}
return $result;
}
}

View file

@ -0,0 +1,128 @@
<?php
final class DifferentialBlockingReviewerDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Blocking Reviewers');
}
public function getPlaceholderText() {
return pht('Type a user, project, or package name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getComponentDatasources() {
return array(
new PhabricatorPeopleDatasource(),
new PhabricatorProjectDatasource(),
new PhabricatorOwnersPackageDatasource(),
);
}
public function getDatasourceFunctions() {
return array(
'blocking' => array(
'name' => pht('Blocking: ...'),
'arguments' => pht('reviewer'),
'summary' => pht('Select a blocking reviewer.'),
'description' => pht(
"This function allows you to add a reviewer as a blocking ".
"reviewer. For example, this will add `%s` as a blocking reviewer: ".
"\n\n%s\n\n",
'alincoln',
'> blocking(alincoln)'),
),
);
}
protected function didLoadResults(array $results) {
foreach ($results as $result) {
$result
->setColor('red')
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setIcon('fa-asterisk')
->setPHID('blocking('.$result->getPHID().')')
->setDisplayName(pht('Blocking: %s', $result->getDisplayName()))
->setName($result->getName().' blocking');
}
return $results;
}
protected function evaluateFunction($function, array $argv_list) {
$phids = array();
foreach ($argv_list as $argv) {
$phids[] = head($argv);
}
$phids = $this->resolvePHIDs($phids);
$results = array();
foreach ($phids as $phid) {
$results[] = array(
'type' => DifferentialReviewerStatus::STATUS_BLOCKING,
'phid' => $phid,
);
}
return $results;
}
public function renderFunctionTokens($function, array $argv_list) {
$phids = array();
foreach ($argv_list as $argv) {
$phids[] = head($argv);
}
$phids = $this->resolvePHIDs($phids);
$tokens = $this->renderTokens($phids);
foreach ($tokens as $token) {
$token->setColor(null);
if ($token->isInvalid()) {
$token
->setValue(pht('Blocking: Invalid Reviewer'));
} else {
$token
->setIcon('fa-asterisk')
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setColor('red')
->setKey('blocking('.$token->getKey().')')
->setValue(pht('Blocking: %s', $token->getValue()));
}
}
return $tokens;
}
private function resolvePHIDs(array $phids) {
$usernames = array();
foreach ($phids as $key => $phid) {
if (phid_get_type($phid) != PhabricatorPeopleUserPHIDType::TYPECONST) {
$usernames[$key] = $phid;
}
}
if ($usernames) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames($usernames)
->execute();
$users = mpull($users, null, 'getUsername');
foreach ($usernames as $key => $username) {
$user = idx($users, $username);
if ($user) {
$phids[$key] = $user->getPHID();
}
}
}
return $phids;
}
}

View file

@ -0,0 +1,115 @@
<?php
final class DifferentialExactUserFunctionDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Users');
}
public function getPlaceholderText() {
return pht('Type exact(<user>)...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getComponentDatasources() {
return array(
new PhabricatorPeopleDatasource(),
);
}
public function getDatasourceFunctions() {
return array(
'exact' => array(
'name' => pht('Exact: ...'),
'arguments' => pht('username'),
'summary' => pht('Find results matching users exactly.'),
'description' => pht(
"This function allows you to find results associated only with ".
"a user, exactly, and not any of their projects or packages. For ".
"example, this will find results associated with only `%s`:".
"\n\n%s\n\n",
'alincoln',
'> exact(alincoln)'),
),
);
}
protected function didLoadResults(array $results) {
foreach ($results as $result) {
$result
->setColor(null)
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setIcon('fa-asterisk')
->setPHID('exact('.$result->getPHID().')')
->setDisplayName(pht('Exact User: %s', $result->getDisplayName()))
->setName($result->getName().' exact');
}
return $results;
}
protected function evaluateFunction($function, array $argv_list) {
$phids = array();
foreach ($argv_list as $argv) {
$phids[] = head($argv);
}
return $this->resolvePHIDs($phids);
}
public function renderFunctionTokens($function, array $argv_list) {
$phids = array();
foreach ($argv_list as $argv) {
$phids[] = head($argv);
}
$phids = $this->resolvePHIDs($phids);
$tokens = $this->renderTokens($phids);
foreach ($tokens as $token) {
$token->setColor(null);
if ($token->isInvalid()) {
$token
->setValue(pht('Exact User: Invalid User'));
} else {
$token
->setIcon('fa-asterisk')
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setKey('exact('.$token->getKey().')')
->setValue(pht('Exact User: %s', $token->getValue()));
}
}
return $tokens;
}
private function resolvePHIDs(array $phids) {
$usernames = array();
foreach ($phids as $key => $phid) {
if (phid_get_type($phid) != PhabricatorPeopleUserPHIDType::TYPECONST) {
$usernames[$key] = $phid;
}
}
if ($usernames) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames($usernames)
->execute();
$users = mpull($users, null, 'getUsername');
foreach ($usernames as $key => $username) {
$user = idx($users, $username);
if ($user) {
$phids[$key] = $user->getPHID();
}
}
}
return $phids;
}
}

View file

@ -0,0 +1,63 @@
<?php
final class DifferentialResponsibleDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Responsible Users');
}
public function getPlaceholderText() {
return pht('Type a user, project, or package name, or function...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getComponentDatasources() {
return array(
new DifferentialResponsibleUserDatasource(),
new DifferentialResponsibleViewerFunctionDatasource(),
new DifferentialExactUserFunctionDatasource(),
new PhabricatorProjectDatasource(),
new PhabricatorOwnersPackageDatasource(),
);
}
public static function expandResponsibleUsers(
PhabricatorUser $viewer,
array $values) {
$phids = array();
foreach ($values as $value) {
if (phid_get_type($value) == PhabricatorPeopleUserPHIDType::TYPECONST) {
$phids[] = $value;
}
}
if (!$phids) {
return $values;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs($phids)
->execute();
foreach ($projects as $project) {
$phids[] = $project->getPHID();
$values[] = $project->getPHID();
}
$packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withOwnerPHIDs($phids)
->execute();
foreach ($packages as $package) {
$values[] = $package->getPHID();
}
return $values;
}
}

View file

@ -0,0 +1,30 @@
<?php
final class DifferentialResponsibleUserDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Users');
}
public function getPlaceholderText() {
return pht('Type a user name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getComponentDatasources() {
return array(
new PhabricatorPeopleDatasource(),
);
}
protected function evaluateValues(array $values) {
return DifferentialResponsibleDatasource::expandResponsibleUsers(
$this->getViewer(),
$values);
}
}

View file

@ -0,0 +1,77 @@
<?php
final class DifferentialResponsibleViewerFunctionDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Viewer');
}
public function getPlaceholderText() {
return pht('Type viewer()...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function getDatasourceFunctions() {
return array(
'viewer' => array(
'name' => pht('Current Viewer'),
'summary' => pht('Use the current viewing user.'),
'description' => pht(
'Show revisions the current viewer is responsible for. This '.
'function inclues revisions the viewer is responsible for through '.
'membership in projects and packages.'),
),
);
}
public function loadResults() {
if ($this->getViewer()->getPHID()) {
$results = array($this->renderViewerFunctionToken());
} else {
$results = array();
}
return $this->filterResultsAgainstTokens($results);
}
protected function canEvaluateFunction($function) {
if (!$this->getViewer()->getPHID()) {
return false;
}
return parent::canEvaluateFunction($function);
}
protected function evaluateFunction($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = $this->getViewer()->getPHID();
}
return DifferentialResponsibleDatasource::expandResponsibleUsers(
$this->getViewer(),
$results);
}
public function renderFunctionTokens($function, array $argv_list) {
$tokens = array();
foreach ($argv_list as $argv) {
$tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$this->renderViewerFunctionToken());
}
return $tokens;
}
private function renderViewerFunctionToken() {
return $this->newFunctionResult()
->setName(pht('Current Viewer'))
->setPHID('viewer()')
->setIcon('fa-user')
->setUnique(true);
}
}

View file

@ -0,0 +1,27 @@
<?php
final class DifferentialReviewerDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Reviewers');
}
public function getPlaceholderText() {
return pht('Type a user, project, or package name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getComponentDatasources() {
return array(
new PhabricatorPeopleDatasource(),
new PhabricatorProjectDatasource(),
new PhabricatorOwnersPackageDatasource(),
new DifferentialBlockingReviewerDatasource(),
);
}
}

View file

@ -69,7 +69,9 @@ final class DifferentialAddCommentView extends AphrontView {
);
$mailable_source = new PhabricatorMetaMTAMailableDatasource();
$reviewer_source = new PhabricatorProjectOrUserDatasource();
// TODO: This should be a reviewers datasource, but it's a mess.
$reviewer_source = new PhabricatorMetaMTAMailableDatasource();
$form = new AphrontFormView();
$form

View file

@ -12,6 +12,16 @@ final class DifferentialRevisionListView extends AphrontView {
private $noDataString;
private $noBox;
private $background = null;
private $unlandedDependencies = array();
public function setUnlandedDependencies(array $unlanded_dependencies) {
$this->unlandedDependencies = $unlanded_dependencies;
return $this;
}
public function getUnlandedDependencies() {
return $this->unlandedDependencies;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
@ -65,20 +75,6 @@ final class DifferentialRevisionListView extends AphrontView {
public function render() {
$viewer = $this->getViewer();
$fresh = PhabricatorEnv::getEnvConfig('differential.days-fresh');
if ($fresh) {
$fresh = PhabricatorCalendarHoliday::getNthBusinessDay(
time(),
-$fresh);
}
$stale = PhabricatorEnv::getEnvConfig('differential.days-stale');
if ($stale) {
$stale = PhabricatorCalendarHoliday::getNthBusinessDay(
time(),
-$stale);
}
$this->initBehavior('phabricator-tooltips', array());
$this->requireResource('aphront-tooltip-css');
@ -109,18 +105,6 @@ final class DifferentialRevisionListView extends AphrontView {
$modified = $revision->getDateModified();
$status = $revision->getStatus();
$show_age = ($fresh || $stale) &&
$this->highlightAge &&
!$revision->isClosed();
if ($stale && $modified < $stale) {
$object_age = PHUIObjectItemView::AGE_OLD;
} else if ($fresh && $modified < $fresh) {
$object_age = PHUIObjectItemView::AGE_STALE;
} else {
$object_age = PHUIObjectItemView::AGE_FRESH;
}
$status_name =
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status);
@ -143,15 +127,20 @@ final class DifferentialRevisionListView extends AphrontView {
$item->addAttribute($draft);
}
/* Most things 'Need Review', so accept it's the default */
if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
$item->addAttribute($status_name);
}
// Author
$author_handle = $this->handles[$revision->getAuthorPHID()];
$item->addByline(pht('Author: %s', $author_handle->renderLink()));
$unlanded = idx($this->unlandedDependencies, $phid);
if ($unlanded) {
$item->addAttribute(
array(
id(new PHUIIconView())->setIcon('fa-chain-broken', 'red'),
' ',
pht('Open Dependencies'),
));
}
$reviewers = array();
// TODO: As above, this should be based on `getReviewerStatus()`.
foreach ($revision->getReviewers() as $reviewer) {
@ -164,7 +153,7 @@ final class DifferentialRevisionListView extends AphrontView {
}
$item->addAttribute(pht('Reviewers: %s', $reviewers));
$item->setEpoch($revision->getDateModified(), $object_age);
$item->setEpoch($revision->getDateModified());
switch ($status) {
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:

View file

@ -455,7 +455,12 @@ final class DiffusionCommitController extends DiffusionController {
if ($audit_requests) {
$user_requests = array();
$other_requests = array();
foreach ($audit_requests as $audit_request) {
if (!$audit_request->isInteresting()) {
continue;
}
if ($audit_request->isUser()) {
$user_requests[] = $audit_request;
} else {
@ -471,7 +476,7 @@ final class DiffusionCommitController extends DiffusionController {
if ($other_requests) {
$view->addProperty(
pht('Project/Package Auditors'),
pht('Group Auditors'),
$this->renderAuditStatusView($other_requests));
}
}

View file

@ -18,8 +18,13 @@ abstract class DiffusionAuditorsHeraldAction
$object = $adapter->getObject();
$auditors = $object->getAudits();
$auditors = mpull($auditors, null, 'getAuditorPHID');
$current = array_keys($auditors);
$current = array();
foreach ($auditors as $auditor) {
if ($auditor->isInteresting()) {
$current[] = $auditor->getAuditorPHID();
}
}
$allowed_types = array(
PhabricatorPeopleUserPHIDType::TYPECONST,

View file

@ -205,40 +205,29 @@ final class PhabricatorHomeMainController extends PhabricatorHomeController {
}
private function buildRevisionPanel() {
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
$viewer = $this->getViewer();
$revision_query = id(new DifferentialRevisionQuery())
->setViewer($user)
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->withResponsibleUsers(array($user_phid))
->needRelationships(true)
->needFlags(true)
->needDrafts(true);
$revisions = PhabricatorDifferentialApplication::loadNeedAttentionRevisions(
$viewer);
$revisions = $revision_query->execute();
list($blocking, $active) = DifferentialRevisionQuery::splitResponsible(
$revisions,
array($user_phid));
if (!$blocking && !$active) {
if (!$revisions) {
return $this->renderMiniPanel(
pht('No Waiting Revisions'),
pht('No revisions are waiting on you.'));
}
$title = pht('Revisions Waiting on You');
$href = '/differential';
$href = '/differential/';
$panel = new PHUIObjectBoxView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$revision_view = id(new DifferentialRevisionListView())
->setHighlightAge(true)
->setRevisions(array_merge($blocking, $active))
->setUser($user);
->setRevisions($revisions)
->setUser($viewer);
$phids = array_merge(
array($user_phid),
array($viewer->getPHID()),
$revision_view->getRequiredHandlePHIDs());
$handles = $this->loadViewerHandles($phids);

View file

@ -148,8 +148,7 @@ final class ManiphestTaskSearchEngine
}
protected function buildQueryFromParameters(array $map) {
$query = id(new ManiphestTaskQuery())
->needProjectPHIDs(true);
$query = $this->newQuery();
if ($map['assignedPHIDs']) {
$query->withOwners($map['assignedPHIDs']);

View file

@ -24,6 +24,8 @@ final class PhabricatorMetaMTAMemberQuery extends PhabricatorQuery {
}
public function execute() {
$viewer = $this->getViewer();
$phids = array_fuse($this->phids);
$actors = array();
$type_map = array();
@ -33,6 +35,33 @@ final class PhabricatorMetaMTAMemberQuery extends PhabricatorQuery {
// TODO: Generalize this somewhere else.
// If we have packages, break them down into their constituent user and
// project owners first. Then we'll resolve those and build the packages
// back up from the pieces.
$package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
$package_phids = idx($type_map, $package_type, array());
unset($type_map[$package_type]);
$package_map = array();
if ($package_phids) {
$packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withPHIDs($package_phids)
->execute();
foreach ($packages as $package) {
$package_owners = array();
foreach ($package->getOwners() as $owner) {
$owner_phid = $owner->getUserPHID();
$owner_type = phid_get_type($owner_phid);
$type_map[$owner_type][] = $owner_phid;
$package_owners[] = $owner_phid;
}
$package_map[$package->getPHID()] = $package_owners;
}
}
$results = array();
foreach ($type_map as $type => $phids) {
switch ($type) {
@ -40,7 +69,7 @@ final class PhabricatorMetaMTAMemberQuery extends PhabricatorQuery {
// NOTE: We're loading the projects here in order to respect policies.
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->setViewer($viewer)
->withPHIDs($phids)
->needMembers(true)
->needWatchers(true)
@ -96,6 +125,21 @@ final class PhabricatorMetaMTAMemberQuery extends PhabricatorQuery {
}
}
// For any packages, stitch them back together from the resolved users
// and projects.
if ($package_map) {
foreach ($package_map as $package_phid => $owner_phids) {
$resolved = array();
foreach ($owner_phids as $owner_phid) {
$resolved_phids = idx($results, $owner_phid, array());
foreach ($resolved_phids as $resolved_phid) {
$resolved[] = $resolved_phid;
}
}
$results[$package_phid] = $resolved;
}
}
return $results;
}

View file

@ -107,11 +107,15 @@ final class PhabricatorMailTarget extends Phobject {
$cc_handles = iterator_to_array($cc_handles);
$body = '';
if ($to_handles) {
$body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n";
$to_names = mpull($to_handles, 'getCommandLineObjectName');
$body .= "To: ".implode(', ', $to_names)."\n";
}
if ($cc_handles) {
$body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n";
$cc_names = mpull($cc_handles, 'getCommandLineObjectName');
$body .= "Cc: ".implode(', ', $cc_names)."\n";
}
return $body;

View file

@ -634,7 +634,7 @@ final class PhabricatorMetaMTAMail
}
$mailer->setBody($body);
$html_emails = false;
$html_emails = true;
if ($use_prefs && $prefs) {
$html_emails = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS,

View file

@ -8,7 +8,7 @@ final class PhabricatorMetaMTAMailableDatasource
}
public function getPlaceholderText() {
return pht('Type a user, project, or mailing list name...');
return pht('Type a user, project, package, or mailing list name...');
}
public function getDatasourceApplicationClass() {
@ -19,6 +19,7 @@ final class PhabricatorMetaMTAMailableDatasource
return array(
new PhabricatorPeopleDatasource(),
new PhabricatorProjectDatasource(),
new PhabricatorOwnersPackageDatasource(),
);
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorOAuthServerSchemaSpec
extends PhabricatorConfigSchemaSpec {
public function buildSchemata() {
$this->buildEdgeSchemata(new PhabricatorOAuthServerClient());
}
}

View file

@ -39,6 +39,12 @@ final class PhabricatorOwnersApplication extends PhabricatorApplication {
return self::GROUP_UTILITIES;
}
public function getRemarkupRules() {
return array(
new PhabricatorOwnersPackageRemarkupRule(),
);
}
public function getRoutes() {
return array(
'/owners/' => array(

View file

@ -144,7 +144,7 @@ final class PhabricatorOwnersDetailController
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($package->getName());
$crumbs->addTextCrumb($package->getMonogram());
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
@ -184,6 +184,19 @@ final class PhabricatorOwnersDetailController
}
$view->addProperty(pht('Owners'), $owner_list);
$dominion = $package->getDominion();
$dominion_map = PhabricatorOwnersPackage::getDominionOptionsMap();
$spec = idx($dominion_map, $dominion, array());
$name = idx($spec, 'short', $dominion);
$view->addProperty(pht('Dominion'), $name);
$auto = $package->getAutoReview();
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$spec = idx($autoreview_map, $auto, array());
$name = idx($spec, 'name', $auto);
$view->addProperty(pht('Auto Review'), $name);
if ($package->getAuditingEnabled()) {
$auditing = pht('Enabled');
} else {

View file

@ -64,7 +64,7 @@ final class PhabricatorOwnersPathsController
$editor->applyTransactions($package, $xactions);
return id(new AphrontRedirectResponse())
->setURI('/owners/package/'.$package->getID().'/');
->setURI($package->getURI());
} else {
$paths = $package->getPaths();
$path_refs = mpull($paths, 'getRef');
@ -106,7 +106,7 @@ final class PhabricatorOwnersPathsController
require_celerity_resource('owners-path-editor-css');
$cancel_uri = '/owners/package/'.$package->getID().'/';
$cancel_uri = $package->getURI();
$form = id(new AphrontFormView())
->setUser($viewer)

View file

@ -51,8 +51,7 @@ final class PhabricatorOwnersPackageEditEngine
}
protected function getObjectViewURI($object) {
$id = $object->getID();
return "/owners/package/{$id}/";
return $object->getURI();
}
protected function buildCustomEditFields($object) {
@ -85,6 +84,12 @@ applying a transaction of this type.
EOTEXT
);
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$autoreview_map = ipull($autoreview_map, 'name');
$dominion_map = PhabricatorOwnersPackage::getDominionOptionsMap();
$dominion_map = ipull($dominion_map, 'name');
return array(
id(new PhabricatorTextEditField())
->setKey('name')
@ -101,6 +106,28 @@ EOTEXT
->setDatasource(new PhabricatorProjectOrUserDatasource())
->setIsCopyable(true)
->setValue($object->getOwnerPHIDs()),
id(new PhabricatorSelectEditField())
->setKey('dominion')
->setLabel(pht('Dominion'))
->setDescription(
pht('Change package dominion rules.'))
->setTransactionType(
PhabricatorOwnersPackageTransaction::TYPE_DOMINION)
->setIsCopyable(true)
->setValue($object->getDominion())
->setOptions($dominion_map),
id(new PhabricatorSelectEditField())
->setKey('autoReview')
->setLabel(pht('Auto Review'))
->setDescription(
pht(
'Automatically trigger reviews for commits affecting files in '.
'this package.'))
->setTransactionType(
PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW)
->setIsCopyable(true)
->setValue($object->getAutoReview())
->setOptions($autoreview_map),
id(new PhabricatorSelectEditField())
->setKey('auditing')
->setLabel(pht('Auditing'))

View file

@ -20,6 +20,8 @@ final class PhabricatorOwnersPackageTransactionEditor
$types[] = PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION;
$types[] = PhabricatorOwnersPackageTransaction::TYPE_PATHS;
$types[] = PhabricatorOwnersPackageTransaction::TYPE_STATUS;
$types[] = PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW;
$types[] = PhabricatorOwnersPackageTransaction::TYPE_DOMINION;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
@ -47,6 +49,10 @@ final class PhabricatorOwnersPackageTransactionEditor
return mpull($paths, 'getRef');
case PhabricatorOwnersPackageTransaction::TYPE_STATUS:
return $object->getStatus();
case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW:
return $object->getAutoReview();
case PhabricatorOwnersPackageTransaction::TYPE_DOMINION:
return $object->getDominion();
}
}
@ -58,6 +64,8 @@ final class PhabricatorOwnersPackageTransactionEditor
case PhabricatorOwnersPackageTransaction::TYPE_NAME:
case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION:
case PhabricatorOwnersPackageTransaction::TYPE_STATUS:
case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW:
case PhabricatorOwnersPackageTransaction::TYPE_DOMINION:
return $xaction->getNewValue();
case PhabricatorOwnersPackageTransaction::TYPE_PATHS:
$new = $xaction->getNewValue();
@ -113,6 +121,12 @@ final class PhabricatorOwnersPackageTransactionEditor
case PhabricatorOwnersPackageTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW:
$object->setAutoReview($xaction->getNewValue());
return;
case PhabricatorOwnersPackageTransaction::TYPE_DOMINION:
$object->setDominion($xaction->getNewValue());
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
@ -127,6 +141,8 @@ final class PhabricatorOwnersPackageTransactionEditor
case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION:
case PhabricatorOwnersPackageTransaction::TYPE_AUDITING:
case PhabricatorOwnersPackageTransaction::TYPE_STATUS:
case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW:
case PhabricatorOwnersPackageTransaction::TYPE_DOMINION:
return;
case PhabricatorOwnersPackageTransaction::TYPE_OWNERS:
$old = $xaction->getOldValue();
@ -205,6 +221,61 @@ final class PhabricatorOwnersPackageTransactionEditor
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if (preg_match('([,!])', $new)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Package names may not contain commas (",") or exclamation '.
'marks ("!"). These characters are ambiguous when package '.
'names are parsed from the command line.'),
$xaction);
}
}
break;
case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW:
$map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if (empty($map[$new])) {
$valid = array_keys($map);
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Autoreview setting "%s" is not valid. '.
'Valid settings are: %s.',
$new,
implode(', ', $valid)),
$xaction);
}
}
break;
case PhabricatorOwnersPackageTransaction::TYPE_DOMINION:
$map = PhabricatorOwnersPackage::getDominionOptionsMap();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if (empty($map[$new])) {
$valid = array_keys($map);
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Dominion setting "%s" is not valid. '.
'Valid settings are: %s.',
$new,
implode(', ', $valid)),
$xaction);
}
}
break;
case PhabricatorOwnersPackageTransaction::TYPE_PATHS:
if (!$xactions) {
@ -331,8 +402,7 @@ final class PhabricatorOwnersPackageTransactionEditor
$body = parent::buildMailBody($object, $xactions);
$detail_uri = PhabricatorEnv::getProductionURI(
'/owners/package/'.$object->getID().'/');
$detail_uri = PhabricatorEnv::getProductionURI($object->getURI());
$body->addLinkSection(
pht('PACKAGE DETAIL'),

View file

@ -9,7 +9,7 @@ final class PhabricatorOwnersPackagePHIDType extends PhabricatorPHIDType {
}
public function getTypeIcon() {
return 'fa-list-alt';
return 'fa-shopping-bag';
}
public function newObject() {
@ -36,12 +36,50 @@ final class PhabricatorOwnersPackagePHIDType extends PhabricatorPHIDType {
foreach ($handles as $phid => $handle) {
$package = $objects[$phid];
$monogram = $package->getMonogram();
$name = $package->getName();
$id = $package->getID();
$uri = $package->getURI();
$handle->setName($name);
$handle->setURI("/owners/package/{$id}/");
$handle
->setName($monogram)
->setFullName("{$monogram}: {$name}")
->setCommandLineObjectName("{$monogram} {$name}")
->setURI($uri);
if ($package->isArchived()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}
}
public function canLoadNamedObject($name) {
return preg_match('/^O\d*[1-9]\d*$/i', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$id_map = array();
foreach ($names as $name) {
$id = (int)substr($name, 1);
$id_map[$id][] = $name;
}
$objects = id(new PhabricatorOwnersPackageQuery())
->setViewer($query->getViewer())
->withIDs(array_keys($id_map))
->execute();
$results = array();
foreach ($objects as $id => $object) {
foreach (idx($id_map, $id, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
}
}

View file

@ -351,6 +351,7 @@ final class PhabricatorOwnersPackageQuery
}
$packages = $this->controlResults;
$weak_dominion = PhabricatorOwnersPackage::DOMINION_WEAK;
$matches = array();
foreach ($packages as $package_id => $package) {
@ -373,6 +374,7 @@ final class PhabricatorOwnersPackageQuery
if ($best_match && $include) {
$matches[$package_id] = array(
'strength' => $best_match,
'weak' => ($package->getDominion() == $weak_dominion),
'package' => $package,
);
}
@ -381,6 +383,18 @@ final class PhabricatorOwnersPackageQuery
$matches = isort($matches, 'strength');
$matches = array_reverse($matches);
$first_id = null;
foreach ($matches as $package_id => $match) {
if ($first_id === null) {
$first_id = $package_id;
continue;
}
if ($match['weak']) {
unset($matches[$package_id]);
}
}
return array_values(ipull($matches, 'package'));
}

View file

@ -136,9 +136,9 @@ final class PhabricatorOwnersPackageSearchEngine
$item = id(new PHUIObjectItemView())
->setObject($package)
->setObjectName(pht('Package %d', $id))
->setObjectName($package->getMonogram())
->setHeader($package->getName())
->setHref('/owners/package/'.$id.'/');
->setHref($package->getURI());
if ($package->isArchived()) {
$item->setDisabled(true);

View file

@ -0,0 +1,19 @@
<?php
final class PhabricatorOwnersPackageRemarkupRule
extends PhabricatorObjectRemarkupRule {
protected function getObjectNamePrefix() {
return 'O';
}
protected function loadObjects(array $ids) {
$viewer = $this->getEngine()->getConfig('viewer');
return id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withIDs($ids)
->execute();
}
}

View file

@ -14,12 +14,14 @@ final class PhabricatorOwnersPackage
protected $name;
protected $originalName;
protected $auditingEnabled;
protected $autoReview;
protected $description;
protected $primaryOwnerPHID;
protected $mailKey;
protected $status;
protected $viewPolicy;
protected $editPolicy;
protected $dominion;
private $paths = self::ATTACHABLE;
private $owners = self::ATTACHABLE;
@ -28,6 +30,14 @@ final class PhabricatorOwnersPackage
const STATUS_ACTIVE = 'active';
const STATUS_ARCHIVED = 'archived';
const AUTOREVIEW_NONE = 'none';
const AUTOREVIEW_SUBSCRIBE = 'subscribe';
const AUTOREVIEW_REVIEW = 'review';
const AUTOREVIEW_BLOCK = 'block';
const DOMINION_STRONG = 'strong';
const DOMINION_WEAK = 'weak';
public static function initializeNewPackage(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
@ -41,6 +51,8 @@ final class PhabricatorOwnersPackage
return id(new PhabricatorOwnersPackage())
->setAuditingEnabled(0)
->setAutoReview(self::AUTOREVIEW_NONE)
->setDominion(self::DOMINION_STRONG)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->attachPaths(array())
@ -56,6 +68,36 @@ final class PhabricatorOwnersPackage
);
}
public static function getAutoreviewOptionsMap() {
return array(
self::AUTOREVIEW_NONE => array(
'name' => pht('No Autoreview'),
),
self::AUTOREVIEW_SUBSCRIBE => array(
'name' => pht('Subscribe to Changes'),
),
self::AUTOREVIEW_REVIEW => array(
'name' => pht('Review Changes'),
),
self::AUTOREVIEW_BLOCK => array(
'name' => pht('Review Changes (Blocking)'),
),
);
}
public static function getDominionOptionsMap() {
return array(
self::DOMINION_STRONG => array(
'name' => pht('Strong (Control All Paths)'),
'short' => pht('Strong'),
),
self::DOMINION_WEAK => array(
'name' => pht('Weak (Control Unowned Paths)'),
'short' => pht('Weak'),
),
);
}
protected function getConfiguration() {
return array(
// This information is better available from the history table.
@ -69,6 +111,8 @@ final class PhabricatorOwnersPackage
'auditingEnabled' => 'bool',
'mailKey' => 'bytes20',
'status' => 'text32',
'autoReview' => 'text32',
'dominion' => 'text32',
),
) + parent::getConfiguration();
}
@ -165,7 +209,7 @@ final class PhabricatorOwnersPackage
foreach (array_chunk(array_keys($fragments), 128) as $chunk) {
$rows[] = queryfx_all(
$conn,
'SELECT pkg.id, p.excluded, p.path
'SELECT pkg.id, pkg.dominion, p.excluded, p.path
FROM %T pkg JOIN %T p ON p.packageID = pkg.id
WHERE p.path IN (%Ls) %Q',
$package->getTableName(),
@ -207,35 +251,100 @@ final class PhabricatorOwnersPackage
}
public static function findLongestPathsPerPackage(array $rows, array $paths) {
$ids = array();
foreach (igroup($rows, 'id') as $id => $package_paths) {
$relevant_paths = array_select_keys(
$paths,
ipull($package_paths, 'path'));
// Build a map from each path to all the package paths which match it.
$path_hits = array();
$weak = array();
foreach ($rows as $row) {
$id = $row['id'];
$path = $row['path'];
$length = strlen($path);
$excluded = $row['excluded'];
// For every package, remove all excluded paths.
$remove = array();
foreach ($package_paths as $package_path) {
if ($package_path['excluded']) {
$remove += idx($relevant_paths, $package_path['path'], array());
unset($relevant_paths[$package_path['path']]);
}
if ($row['dominion'] === self::DOMINION_WEAK) {
$weak[$id] = true;
}
if ($remove) {
foreach ($relevant_paths as $fragment => $fragment_paths) {
$relevant_paths[$fragment] = array_diff_key($fragment_paths, $remove);
}
}
$relevant_paths = array_filter($relevant_paths);
if ($relevant_paths) {
$ids[$id] = max(array_map('strlen', array_keys($relevant_paths)));
$matches = $paths[$path];
foreach ($matches as $match => $ignored) {
$path_hits[$match][] = array(
'id' => $id,
'excluded' => $excluded,
'length' => $length,
);
}
}
return $ids;
// For each path, process the matching package paths to figure out which
// packages actually own it.
$path_packages = array();
foreach ($path_hits as $match => $hits) {
$hits = isort($hits, 'length');
$packages = array();
foreach ($hits as $hit) {
$package_id = $hit['id'];
if ($hit['excluded']) {
unset($packages[$package_id]);
} else {
$packages[$package_id] = $hit;
}
}
$path_packages[$match] = $packages;
}
// Remove packages with weak dominion rules that should cede control to
// a more specific package.
if ($weak) {
foreach ($path_packages as $match => $packages) {
$packages = isort($packages, 'length');
$packages = array_reverse($packages, true);
$first = null;
foreach ($packages as $package_id => $package) {
// If this is the first package we've encountered, note it and
// continue. We're iterating over the packages from longest to
// shortest match, so this package always has the strongest claim
// on the path.
if ($first === null) {
$first = $package_id;
continue;
}
// If this is the first package we saw, its claim stands even if it
// is a weak package.
if ($first === $package_id) {
continue;
}
// If this is a weak package and not the first package we saw,
// cede its claim to the stronger package.
if (isset($weak[$package_id])) {
unset($packages[$package_id]);
}
}
$path_packages[$match] = $packages;
}
}
// For each package that owns at least one path, identify the longest
// path it owns.
$package_lengths = array();
foreach ($path_packages as $match => $hits) {
foreach ($hits as $hit) {
$length = $hit['length'];
$id = $hit['id'];
if (empty($package_lengths[$id])) {
$package_lengths[$id] = $length;
} else {
$package_lengths[$id] = max($package_lengths[$id], $length);
}
}
}
return $package_lengths;
}
public static function splitPath($path) {
@ -289,6 +398,14 @@ final class PhabricatorOwnersPackage
return isset($owner_phids[$phid]);
}
public function getMonogram() {
return 'O'.$this->getID();
}
public function getURI() {
// TODO: Move these to "/O123" for consistency.
return '/owners/package/'.$this->getID().'/';
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -10,6 +10,8 @@ final class PhabricatorOwnersPackageTransaction
const TYPE_DESCRIPTION = 'owners.description';
const TYPE_PATHS = 'owners.paths';
const TYPE_STATUS = 'owners.status';
const TYPE_AUTOREVIEW = 'owners.autoreview';
const TYPE_DOMINION = 'owners.dominion';
public function getApplicationName() {
return 'owners';
@ -143,6 +145,30 @@ final class PhabricatorOwnersPackageTransaction
'%s archived this package.',
$this->renderHandleLink($author_phid));
}
case self::TYPE_AUTOREVIEW:
$map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$map = ipull($map, 'name');
$old = idx($map, $old, $old);
$new = idx($map, $new, $new);
return pht(
'%s adjusted autoreview from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
case self::TYPE_DOMINION:
$map = PhabricatorOwnersPackage::getDominionOptionsMap();
$map = ipull($map, 'short');
$old = idx($map, $old, $old);
$new = idx($map, $new, $new);
return pht(
'%s adjusted package dominion rules from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
}
return parent::getTitle();

View file

@ -4,9 +4,24 @@ final class PhabricatorOwnersPackageTestCase extends PhabricatorTestCase {
public function testFindLongestPathsPerPackage() {
$rows = array(
array('id' => 1, 'excluded' => 0, 'path' => 'src/'),
array('id' => 1, 'excluded' => 1, 'path' => 'src/releeph/'),
array('id' => 2, 'excluded' => 0, 'path' => 'src/releeph/'),
array(
'id' => 1,
'excluded' => 0,
'dominion' => PhabricatorOwnersPackage::DOMINION_STRONG,
'path' => 'src/',
),
array(
'id' => 1,
'excluded' => 1,
'dominion' => PhabricatorOwnersPackage::DOMINION_STRONG,
'path' => 'src/releeph/',
),
array(
'id' => 2,
'excluded' => 0,
'dominion' => PhabricatorOwnersPackage::DOMINION_STRONG,
'path' => 'src/releeph/',
),
);
$paths = array(
@ -29,6 +44,62 @@ final class PhabricatorOwnersPackageTestCase extends PhabricatorTestCase {
2 => strlen('src/releeph/'),
),
PhabricatorOwnersPackage::findLongestPathsPerPackage($rows, $paths));
// Test packages with weak dominion. Here, only package #2 should own the
// path. Package #1's claim is ceded to Package #2 because it uses weak
// rules. Package #2 gets the claim even though it also has weak rules
// because there is no more-specific package.
$rows = array(
array(
'id' => 1,
'excluded' => 0,
'dominion' => PhabricatorOwnersPackage::DOMINION_WEAK,
'path' => 'src/',
),
array(
'id' => 2,
'excluded' => 0,
'dominion' => PhabricatorOwnersPackage::DOMINION_WEAK,
'path' => 'src/applications/',
),
);
$pvalue = array('src/applications/main/main.c' => true);
$paths = array(
'src/' => $pvalue,
'src/applications/' => $pvalue,
);
$this->assertEqual(
array(
2 => strlen('src/applications/'),
),
PhabricatorOwnersPackage::findLongestPathsPerPackage($rows, $paths));
// Now, add a more specific path to Package #1. This tests nested ownership
// in packages with weak dominion rules. This time, Package #1 should end
// up back on top, with Package #2 cedeing control to its more specific
// path.
$rows[] = array(
'id' => 1,
'excluded' => 0,
'dominion' => PhabricatorOwnersPackage::DOMINION_WEAK,
'path' => 'src/applications/main/',
);
$paths['src/applications/main/'] = $pvalue;
$this->assertEqual(
array(
1 => strlen('src/applications/main/'),
),
PhabricatorOwnersPackage::findLongestPathsPerPackage($rows, $paths));
}
}

View file

@ -27,9 +27,12 @@ final class PhabricatorOwnersPackageDatasource
$packages = $this->executeQuery($query);
foreach ($packages as $package) {
$name = $package->getName();
$monogram = $package->getMonogram();
$results[] = id(new PhabricatorTypeaheadResult())
->setName($package->getName())
->setURI('/owners/package/'.$package->getID().'/')
->setName("{$monogram}: {$name}")
->setURI($package->getURI())
->setPHID($package->getPHID());
}

View file

@ -63,9 +63,12 @@ final class PassphraseQueryConduitAPIMethod
$material = array();
$is_locked = $credential->getIsLocked();
$allow_api = ($credential->getAllowConduit() && !$is_locked);
$secret = null;
if ($request->getValue('needSecrets')) {
if ($credential->getAllowConduit()) {
if ($allow_api) {
$secret = $credential->getSecret();
if ($secret) {
$secret = $secret->openEnvelope();
@ -102,7 +105,7 @@ final class PassphraseQueryConduitAPIMethod
break;
}
if (!$credential->getAllowConduit()) {
if (!$allow_api) {
$material['noAPIAccess'] = pht(
'This private material for this credential is not accessible via '.
'API calls.');

View file

@ -33,8 +33,22 @@ final class PassphraseCredentialConduitController
throw new Exception(pht('Credential has invalid type "%s"!', $type));
}
$is_locked = $credential->getIsLocked();
if ($is_locked) {
return $this->newDialog()
->setUser($viewer)
->setTitle(pht('Credential Locked'))
->appendChild(
pht(
'This credential can not be made available via Conduit because '.
'it is locked.'))
->addCancelButton($view_uri);
}
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PassphraseCredentialTransaction())
->setTransactionType(PassphraseCredentialTransaction::TYPE_CONDUIT)
->setNewValue(!$credential->getAllowConduit());

View file

@ -270,8 +270,7 @@ final class PassphraseCredentialEditController extends PassphraseController {
}
if ($type->shouldRequireUsername()) {
$form
->appendChild(
$form->appendChild(
id(new AphrontFormTextControl())
->setName('username')
->setLabel(pht('Login/Username'))
@ -279,13 +278,13 @@ final class PassphraseCredentialEditController extends PassphraseController {
->setDisabled($credential_is_locked)
->setError($e_username));
}
$form
->appendChild(
$secret_control
->setName('secret')
->setLabel($type->getSecretLabel())
->setDisabled($credential_is_locked)
->setValue($v_secret));
$form->appendChild(
$secret_control
->setName('secret')
->setLabel($type->getSecretLabel())
->setDisabled($credential_is_locked)
->setValue($v_secret));
if ($type->shouldShowPasswordField()) {
$form->appendChild(

View file

@ -32,15 +32,17 @@ final class PassphraseCredentialLockController
return $this->newDialog()
->setTitle(pht('Credential Already Locked'))
->appendChild(
pht(
'This credential has been locked and the secret is '.
'hidden forever. Anything relying on this credential will '.
'still function. This operation can not be undone.'))
pht('This credential is already locked.'))
->addCancelButton($view_uri, pht('Close'));
}
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PassphraseCredentialTransaction())
->setTransactionType(PassphraseCredentialTransaction::TYPE_CONDUIT)
->setNewValue(0);
$xactions[] = id(new PassphraseCredentialTransaction())
->setTransactionType(PassphraseCredentialTransaction::TYPE_LOCK)
->setNewValue(1);
@ -48,6 +50,7 @@ final class PassphraseCredentialLockController
$editor = id(new PassphraseCredentialTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($credential, $xactions);
@ -55,12 +58,13 @@ final class PassphraseCredentialLockController
}
return $this->newDialog()
->setTitle(pht('Really lock credential?'))
->setTitle(pht('Lock Credential'))
->appendChild(
pht(
'This credential will be locked and the secret will be '.
'hidden forever. Anything relying on this credential will '.
'still function. This operation can not be undone.'))
'This credential will be locked and the secret will be hidden '.
'forever. If Conduit access is enabled, it will be revoked. '.
'Anything relying on this credential will still function. This '.
'operation can not be undone.'))
->addSubmitButton(pht('Lock Credential'))
->addCancelButton($view_uri);
}

View file

@ -119,6 +119,8 @@ final class PassphraseCredentialViewController extends PassphraseController {
$credential,
PhabricatorPolicyCapability::CAN_EDIT);
$can_conduit = ($can_edit && !$is_locked);
$curtain = $this->newCurtainView($credential);
$curtain->addAction(
@ -161,7 +163,7 @@ final class PassphraseCredentialViewController extends PassphraseController {
->setName($credential_conduit_text)
->setIcon($credential_conduit_icon)
->setHref($this->getApplicationURI("conduit/{$id}/"))
->setDisabled(!$can_edit)
->setDisabled(!$can_conduit)
->setWorkflow(true));
$curtain->addAction(
@ -202,16 +204,6 @@ final class PassphraseCredentialViewController extends PassphraseController {
$credential->getUsername());
}
$used_by_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$credential->getPHID(),
PhabricatorCredentialsUsedByObjectEdgeType::EDGECONST);
if ($used_by_phids) {
$properties->addProperty(
pht('Used By'),
$viewer->renderHandleList($used_by_phids));
}
$description = $credential->getDescription();
if (strlen($description)) {
$properties->addSectionHeader(

View file

@ -1,16 +0,0 @@
<?php
final class PhabricatorCredentialsUsedByObjectEdgeType
extends PhabricatorEdgeType {
const EDGECONST = 40;
public function getInverseEdgeConstant() {
return PhabricatorObjectUsesCredentialsEdgeType::EDGECONST;
}
public function shouldWriteInverseTransactions() {
return true;
}
}

View file

@ -1291,11 +1291,12 @@ final class PhabricatorUser
$profile->delete();
}
$keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
'objectPHID = %s',
$this->getPHID());
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($engine->getViewer())
->withObjectPHIDs(array($this->getPHID()))
->execute();
foreach ($keys as $key) {
$key->delete();
$engine->destroyObject($key);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
@ -1341,6 +1342,12 @@ final class PhabricatorUser
return 'id_rsa_phabricator';
}
public function getSSHKeyNotifyPHIDs() {
return array(
$this->getPHID(),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */

View file

@ -29,6 +29,7 @@ final class PhabricatorObjectHandle
private $policyFiltered;
private $subtitle;
private $tokenIcon;
private $commandLineObjectName;
public function setIcon($icon) {
$this->icon = $icon;
@ -196,6 +197,19 @@ final class PhabricatorObjectHandle
return $this->getName();
}
public function setCommandLineObjectName($command_line_object_name) {
$this->commandLineObjectName = $command_line_object_name;
return $this;
}
public function getCommandLineObjectName() {
if ($this->commandLineObjectName !== null) {
return $this->commandLineObjectName;
}
return $this->getObjectName();
}
public function setTitle($title) {
$this->title = $title;
return $this;

View file

@ -6,6 +6,7 @@ final class PhabricatorObjectListQuery extends Phobject {
private $objectList;
private $allowedTypes = array();
private $allowPartialResults;
private $suffixes = array();
public function setAllowPartialResults($allow_partial_results) {
$this->allowPartialResults = $allow_partial_results;
@ -16,6 +17,15 @@ final class PhabricatorObjectListQuery extends Phobject {
return $this->allowPartialResults;
}
public function setSuffixes(array $suffixes) {
$this->suffixes = $suffixes;
return $this;
}
public function getSuffixes() {
return $this->suffixes;
}
public function setAllowedTypes(array $allowed_types) {
$this->allowedTypes = $allowed_types;
return $this;
@ -45,9 +55,80 @@ final class PhabricatorObjectListQuery extends Phobject {
public function execute() {
$names = $this->getObjectList();
$names = array_unique(array_filter(preg_split('/[\s,]+/', $names)));
$objects = $this->loadObjects($names);
// First, normalize any internal whitespace so we don't get weird results
// if linebreaks hit in weird spots.
$names = preg_replace('/\s+/', ' ', $names);
// Split the list on commas.
$names = explode(',', $names);
// Trim and remove empty tokens.
foreach ($names as $key => $name) {
$name = trim($name);
if (!strlen($name)) {
unset($names[$key]);
continue;
}
$names[$key] = $name;
}
// Remove duplicates.
$names = array_unique($names);
$name_map = array();
foreach ($names as $name) {
$parts = explode(' ', $name);
// If this looks like a monogram, ignore anything after the first token.
// This allows us to parse "O123 Package Name" as though it was "O123",
// which we can look up.
if (preg_match('/^[A-Z]\d+\z/', $parts[0])) {
$name_map[$parts[0]] = $name;
} else {
// For anything else, split it on spaces and use each token as a
// value. This means "alincoln htaft", separated with a space instead
// of with a comma, is two different users.
foreach ($parts as $part) {
$name_map[$part] = $part;
}
}
}
// If we're parsing with suffixes, strip them off any tokens and keep
// track of them for later.
$suffixes = $this->getSuffixes();
if ($suffixes) {
$suffixes = array_fuse($suffixes);
$suffix_map = array();
$stripped_map = array();
foreach ($name_map as $key => $name) {
$found_suffixes = array();
do {
$has_any_suffix = false;
foreach ($suffixes as $suffix) {
if (!$this->hasSuffix($name, $suffix)) {
continue;
}
$key = $this->stripSuffix($key, $suffix);
$name = $this->stripSuffix($name, $suffix);
$found_suffixes[] = $suffix;
$has_any_suffix = true;
break;
}
} while ($has_any_suffix);
$stripped_map[$key] = $name;
$suffix_map[$key] = array_fuse($found_suffixes);
}
$name_map = $stripped_map;
}
$objects = $this->loadObjects(array_keys($name_map));
$types = array();
foreach ($objects as $name => $object) {
@ -66,8 +147,8 @@ final class PhabricatorObjectListQuery extends Phobject {
$invalid = array_mergev($invalid);
$missing = array();
foreach ($names as $name) {
if (empty($objects[$name])) {
foreach ($name_map as $key => $name) {
if (empty($objects[$key])) {
$missing[] = $name;
}
}
@ -100,7 +181,18 @@ final class PhabricatorObjectListQuery extends Phobject {
}
}
return array_values(array_unique(mpull($objects, 'getPHID')));
$result = array_unique(mpull($objects, 'getPHID'));
if ($suffixes) {
foreach ($result as $key => $phid) {
$result[$key] = array(
'phid' => $phid,
'suffixes' => idx($suffix_map, $key, array()),
);
}
}
return array_values($result);
}
private function loadObjects($names) {
@ -146,5 +238,16 @@ final class PhabricatorObjectListQuery extends Phobject {
return $results;
}
private function hasSuffix($key, $suffix) {
return (substr($key, -strlen($suffix)) === $suffix);
}
private function stripSuffix($key, $suffix) {
if ($this->hasSuffix($key, $suffix)) {
return substr($key, 0, -strlen($suffix));
}
return $key;
}
}

View file

@ -13,7 +13,6 @@ final class PhabricatorObjectListQueryTestCase extends PhabricatorTestCase {
$name = $user->getUsername();
$phid = $user->getPHID();
$result = $this->parseObjectList("@{$name}");
$this->assertEqual(array($phid), $result);
@ -29,6 +28,47 @@ final class PhabricatorObjectListQueryTestCase extends PhabricatorTestCase {
$result = $this->parseObjectList('');
$this->assertEqual(array(), $result);
$result = $this->parseObjectList("{$name}!", array(), false, array('!'));
$this->assertEqual(
array(
array(
'phid' => $phid,
'suffixes' => array('!' => '!'),
),
),
$result);
$package = PhabricatorOwnersPackage::initializeNewPackage($user)
->setName(pht('Query Test Package'))
->save();
$package_phid = $package->getPHID();
$package_mono = $package->getMonogram();
$result = $this->parseObjectList("{$package_mono} Any Ignored Text");
$this->assertEqual(array($package_phid), $result);
$result = $this->parseObjectList("{$package_mono} Any Text, {$name}");
$this->assertEqual(array($package_phid, $phid), $result);
$result = $this->parseObjectList(
"{$package_mono} Any Text!, {$name}",
array(),
false,
array('!'));
$this->assertEqual(
array(
array(
'phid' => $package_phid,
'suffixes' => array('!' => '!'),
),
array(
'phid' => $phid,
'suffixes' => array(),
),
),
$result);
// Expect failure when loading a user if objects must be of type "DUCK".
$caught = null;
try {
@ -67,7 +107,8 @@ final class PhabricatorObjectListQueryTestCase extends PhabricatorTestCase {
private function parseObjectList(
$list,
array $types = array(),
$allow_partial = false) {
$allow_partial = false,
$suffixes = array()) {
$query = id(new PhabricatorObjectListQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
@ -81,6 +122,10 @@ final class PhabricatorObjectListQueryTestCase extends PhabricatorTestCase {
$query->setAllowPartialResults(true);
}
if ($suffixes) {
$query->setSuffixes($suffixes);
}
return $query->execute();
}

View file

@ -42,12 +42,19 @@ final class PholioUploadedImageView extends AphrontView {
PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
$thumbnail_uri = $file->getURIForTransform($xform);
$thumb_img = phutil_tag(
'img',
array(
'class' => 'pholio-thumb-img',
'src' => $thumbnail_uri,
));
$thumb_frame = phutil_tag(
'div',
array(
'class' => 'pholio-thumb-frame',
'style' => 'background-image: url('.$thumbnail_uri.');',
));
),
$thumb_img);
$handle = javelin_tag(
'div',

View file

@ -100,9 +100,10 @@ final class ProjectBoardTaskCard extends Phobject {
if ($points !== null) {
$points_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setShade(PHUITagView::COLOR_BLUE)
->setShade(PHUITagView::COLOR_GREY)
->setSlimShady(true)
->setName($points);
->setName($points)
->addClass('phui-workcard-points');
$card->addAttribute($points_tag);
}
}

View file

@ -49,6 +49,16 @@ final class PhabricatorRepositoryAuditRequest
return $this->assertAttached($this->commit);
}
public function isInteresting() {
switch ($this->getAuditStatus()) {
case PhabricatorAuditStatusConstants::NONE:
case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED:
return false;
}
return true;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -33,108 +33,132 @@ final class PhabricatorRepositoryCommitOwnersWorker
$repository,
$commit,
PhabricatorUser::getOmnipotentUser());
$affected_packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$affected_paths);
if ($affected_packages) {
$requests = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere(
'commitPHID = %s',
$commit->getPHID());
$requests = mpull($requests, null, 'getAuditorPHID');
foreach ($affected_packages as $package) {
$request = idx($requests, $package->getPHID());
if ($request) {
// Don't update request if it exists already.
continue;
}
if ($package->isArchived()) {
// Don't trigger audits if the package is archived.
continue;
}
if ($package->getAuditingEnabled()) {
$reasons = $this->checkAuditReasons($commit, $package);
if ($reasons) {
$audit_status =
PhabricatorAuditStatusConstants::AUDIT_REQUIRED;
} else {
$audit_status =
PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED;
}
} else {
$reasons = array();
$audit_status = PhabricatorAuditStatusConstants::NONE;
}
$relationship = new PhabricatorRepositoryAuditRequest();
$relationship->setAuditorPHID($package->getPHID());
$relationship->setCommitPHID($commit->getPHID());
$relationship->setAuditReasons($reasons);
$relationship->setAuditStatus($audit_status);
$relationship->save();
$requests[$package->getPHID()] = $relationship;
}
$commit->updateAuditStatus($requests);
$commit->save();
if (!$affected_packages) {
return;
}
}
private function checkAuditReasons(
PhabricatorRepositoryCommit $commit,
PhabricatorOwnersPackage $package) {
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
$commit->attachCommitData($data);
$reasons = array();
if ($data->getCommitDetail('vsDiff')) {
$reasons[] = pht('Changed After Revision Was Accepted');
}
$commit_author_phid = $data->getCommitDetail('authorPHID');
if (!$commit_author_phid) {
$reasons[] = pht('Commit Author Not Recognized');
}
$author_phid = $data->getCommitDetail('authorPHID');
$revision_id = $data->getCommitDetail('differential.revisionID');
$revision_author_phid = null;
$commit_reviewedby_phid = null;
if ($revision_id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($revision_id))
->needReviewerStatus(true)
->executeOne();
if ($revision) {
$revision_author_phid = $revision->getAuthorPHID();
$commit_reviewedby_phid = $data->getCommitDetail('reviewerPHID');
if ($revision_author_phid !== $commit_author_phid) {
$reasons[] = pht('Author Not Matching with Revision');
}
} else {
$reasons[] = pht('Revision Not Found');
}
} else {
$reasons[] = pht('No Revision Specified');
$revision = null;
}
$owners_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
array($package->getID()));
$requests = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere(
'commitPHID = %s',
$commit->getPHID());
$requests = mpull($requests, null, 'getAuditorPHID');
if (!($commit_author_phid && in_array($commit_author_phid, $owners_phids) ||
$commit_reviewedby_phid && in_array($commit_reviewedby_phid,
$owners_phids))) {
foreach ($affected_packages as $package) {
$request = idx($requests, $package->getPHID());
if ($request) {
// Don't update request if it exists already.
continue;
}
if ($package->isArchived()) {
// Don't trigger audits if the package is archived.
continue;
}
if ($package->getAuditingEnabled()) {
$reasons = $this->checkAuditReasons(
$commit,
$package,
$author_phid,
$revision);
if ($reasons) {
$audit_status = PhabricatorAuditStatusConstants::AUDIT_REQUIRED;
} else {
$audit_status = PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED;
}
} else {
$reasons = array();
$audit_status = PhabricatorAuditStatusConstants::NONE;
}
$relationship = new PhabricatorRepositoryAuditRequest();
$relationship->setAuditorPHID($package->getPHID());
$relationship->setCommitPHID($commit->getPHID());
$relationship->setAuditReasons($reasons);
$relationship->setAuditStatus($audit_status);
$relationship->save();
$requests[$package->getPHID()] = $relationship;
}
$commit->updateAuditStatus($requests);
$commit->save();
}
private function checkAuditReasons(
PhabricatorRepositoryCommit $commit,
PhabricatorOwnersPackage $package,
$author_phid,
$revision) {
$owner_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
array(
$package->getID(),
));
$owner_phids = array_fuse($owner_phids);
$reasons = array();
if (!$author_phid) {
$reasons[] = pht('Commit Author Not Recognized');
} else if (isset($owner_phids[$author_phid])) {
return $reasons;
}
if (!$revision) {
$reasons[] = pht('No Revision Specified');
return $reasons;
}
$accepted_statuses = array(
DifferentialReviewerStatus::STATUS_ACCEPTED,
DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER,
);
$accepted_statuses = array_fuse($accepted_statuses);
$found_accept = false;
foreach ($revision->getReviewerStatus() as $reviewer) {
$reviewer_phid = $reviewer->getReviewerPHID();
// If this reviewer isn't a package owner, just ignore them.
if (empty($owner_phids[$reviewer_phid])) {
continue;
}
// If this reviewer accepted the revision and owns the package, we're
// all clear and do not need to trigger an audit.
if (isset($accepted_statuses[$reviewer->getStatus()])) {
$found_accept = true;
break;
}
}
if (!$found_accept) {
$reasons[] = pht('Owners Not Involved');
}

View file

@ -0,0 +1,54 @@
<?php
abstract class PhabricatorSearchResultBucket
extends Phobject {
private $viewer;
private $pageSize;
final public function setPageSize($page_size) {
$this->pageSize = $page_size;
return $this;
}
final public function getPageSize() {
if ($this->pageSize === null) {
return $this->getDefaultPageSize();
}
return $this->pageSize;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
protected function getDefaultPageSize() {
return 1000;
}
abstract public function getResultBucketName();
abstract protected function buildResultGroups(
PhabricatorSavedQuery $query,
array $objects);
final public function newResultGroups(
PhabricatorSavedQuery $query,
array $objects) {
return $this->buildResultGroups($query, $objects);
}
final public function getResultBucketKey() {
return $this->getPhobjectClassConstant('BUCKETKEY');
}
final protected function newGroup() {
return new PhabricatorSearchResultBucketGroup();
}
}

View file

@ -0,0 +1,47 @@
<?php
final class PhabricatorSearchResultBucketGroup
extends Phobject {
private $name;
private $key;
private $noDataString;
private $objects;
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function getNoDataString() {
return $this->noDataString;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObjects() {
return $this->objects;
}
}

Some files were not shown because too many files have changed in this diff Show more