diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 3188de0052..7f9aaa4995 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,9 +9,9 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'e3c1a8f2', + 'core.pkg.css' => '34ce1741', 'core.pkg.js' => '2cda17a4', - 'differential.pkg.css' => 'ab23bd75', + 'differential.pkg.css' => '1755a478', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => 'd92bed0d', + 'rsrc/css/application/differential/changeset-view.css' => '4193eeff', 'rsrc/css/application/differential/core.css' => '7300a73e', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -127,7 +127,7 @@ return array( 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'ccd7e4e2', 'rsrc/css/phui/calendar/phui-calendar-month.css' => 'cb758c42', 'rsrc/css/phui/calendar/phui-calendar.css' => 'f11073aa', - 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '9e037c7a', + 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '534f1757', 'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0', 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc', 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e', @@ -540,7 +540,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => 'd92bed0d', + 'differential-changeset-view-css' => '4193eeff', 'differential-core-view-css' => '7300a73e', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -834,7 +834,7 @@ return array( 'phui-lightbox-css' => '4ebf22da', 'phui-list-view-css' => '470b1adb', 'phui-object-box-css' => 'f434b6be', - 'phui-oi-big-ui-css' => '9e037c7a', + 'phui-oi-big-ui-css' => '534f1757', 'phui-oi-color-css' => 'b517bfa0', 'phui-oi-drag-ui-css' => 'da15d3dc', 'phui-oi-flush-ui-css' => '490e2e2e', @@ -1220,6 +1220,9 @@ return array( 'javelin-behavior', 'javelin-uri', ), + '4193eeff' => array( + 'phui-inline-comment-view-css', + ), '4234f572' => array( 'syntax-default-css', ), @@ -1345,6 +1348,9 @@ return array( 'javelin-dom', 'javelin-fx', ), + '534f1757' => array( + 'phui-oi-list-view-css', + ), '541f81c3' => array( 'javelin-install', ), @@ -1721,9 +1727,6 @@ return array( 'javelin-uri', 'phabricator-textareautils', ), - '9e037c7a' => array( - 'phui-oi-list-view-css', - ), '9f081f05' => array( 'javelin-behavior', 'javelin-dom', @@ -1997,9 +2000,6 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), - 'd92bed0d' => array( - 'phui-inline-comment-view-css', - ), 'da15d3dc' => array( 'phui-oi-list-view-css', ), diff --git a/resources/sql/autopatches/20151221.search.3.reindex.php b/resources/sql/autopatches/20151221.search.3.reindex.php index 09556d5ea0..623ba7bf6a 100644 --- a/resources/sql/autopatches/20151221.search.3.reindex.php +++ b/resources/sql/autopatches/20151221.search.3.reindex.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160221.almanac.2.devicei.php b/resources/sql/autopatches/20160221.almanac.2.devicei.php index aea17d0ad6..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160221.almanac.2.devicei.php +++ b/resources/sql/autopatches/20160221.almanac.2.devicei.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160221.almanac.4.servicei.php b/resources/sql/autopatches/20160221.almanac.4.servicei.php index 97211ca7b5..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160221.almanac.4.servicei.php +++ b/resources/sql/autopatches/20160221.almanac.4.servicei.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160221.almanac.6.networki.php b/resources/sql/autopatches/20160221.almanac.6.networki.php index 263defbb33..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160221.almanac.6.networki.php +++ b/resources/sql/autopatches/20160221.almanac.6.networki.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160227.harbormaster.2.plani.php b/resources/sql/autopatches/20160227.harbormaster.2.plani.php index 6dea004c06..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160227.harbormaster.2.plani.php +++ b/resources/sql/autopatches/20160227.harbormaster.2.plani.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160303.drydock.2.bluei.php b/resources/sql/autopatches/20160303.drydock.2.bluei.php index c0b68c2262..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160303.drydock.2.bluei.php +++ b/resources/sql/autopatches/20160303.drydock.2.bluei.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160308.nuance.04.sourcei.php b/resources/sql/autopatches/20160308.nuance.04.sourcei.php index eb0d1da113..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160308.nuance.04.sourcei.php +++ b/resources/sql/autopatches/20160308.nuance.04.sourcei.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160406.badges.ngrams.php b/resources/sql/autopatches/20160406.badges.ngrams.php index ce8d8896ef..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160406.badges.ngrams.php +++ b/resources/sql/autopatches/20160406.badges.ngrams.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160927.phurl.ngrams.php b/resources/sql/autopatches/20160927.phurl.ngrams.php index 74cf61efa5..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160927.phurl.ngrams.php +++ b/resources/sql/autopatches/20160927.phurl.ngrams.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20161011.conpherence.ngrams.php b/resources/sql/autopatches/20161011.conpherence.ngrams.php index 457143f6c7..623ba7bf6a 100644 --- a/resources/sql/autopatches/20161011.conpherence.ngrams.php +++ b/resources/sql/autopatches/20161011.conpherence.ngrams.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20161216.dashboard.ngram.02.php b/resources/sql/autopatches/20161216.dashboard.ngram.02.php index a7abc99b23..623ba7bf6a 100644 --- a/resources/sql/autopatches/20161216.dashboard.ngram.02.php +++ b/resources/sql/autopatches/20161216.dashboard.ngram.02.php @@ -1,21 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} - -$table_dbp = new PhabricatorDashboardPanel(); - -foreach (new LiskMigrationIterator($table_dbp) as $panel) { - PhabricatorSearchWorker::queueDocumentForIndexing( - $panel->getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20170526.milestones.php b/resources/sql/autopatches/20170526.milestones.php index 2e30ac4775..623ba7bf6a 100644 --- a/resources/sql/autopatches/20170526.milestones.php +++ b/resources/sql/autopatches/20170526.milestones.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20171026.ferret.05.ponder.index.php b/resources/sql/autopatches/20171026.ferret.05.ponder.index.php index 20489846d2..623ba7bf6a 100644 --- a/resources/sql/autopatches/20171026.ferret.05.ponder.index.php +++ b/resources/sql/autopatches/20171026.ferret.05.ponder.index.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20190226.harbor.01.planprops.sql b/resources/sql/autopatches/20190226.harbor.01.planprops.sql new file mode 100644 index 0000000000..324139669e --- /dev/null +++ b/resources/sql/autopatches/20190226.harbor.01.planprops.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildplan + ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190226.harbor.02.planvalue.sql b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql new file mode 100644 index 0000000000..b1929abf59 --- /dev/null +++ b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildplan + SET properties = '{}' WHERE properties = ''; diff --git a/resources/sql/autopatches/20190307.herald.01.comments.sql b/resources/sql/autopatches/20190307.herald.01.comments.sql new file mode 100644 index 0000000000..ff9cb9af88 --- /dev/null +++ b/resources/sql/autopatches/20190307.herald.01.comments.sql @@ -0,0 +1 @@ +DROP TABLE {$NAMESPACE}_herald.herald_ruletransaction_comment; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f4ee380cc0..651fad9d12 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -653,6 +653,7 @@ phutil_register_library_map(array( 'DifferentialRevisionUpdateTransaction' => 'applications/differential/xaction/DifferentialRevisionUpdateTransaction.php', 'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php', 'DifferentialRevisionVoidTransaction' => 'applications/differential/xaction/DifferentialRevisionVoidTransaction.php', + 'DifferentialRevisionWrongBuildsTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php', 'DifferentialRevisionWrongStateTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php', 'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php', @@ -1328,18 +1329,26 @@ phutil_register_library_map(array( 'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php', 'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php', 'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php', + 'HarbormasterBuildPlanBehavior' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php', + 'HarbormasterBuildPlanBehaviorOption' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php', + 'HarbormasterBuildPlanBehaviorTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php', 'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php', 'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php', 'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php', + 'HarbormasterBuildPlanEditAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php', 'HarbormasterBuildPlanEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php', 'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php', 'HarbormasterBuildPlanNameNgrams' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanNameNgrams.php', + 'HarbormasterBuildPlanNameTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php', 'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php', + 'HarbormasterBuildPlanPolicyCodex' => 'applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php', 'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php', 'HarbormasterBuildPlanSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanSearchAPIMethod.php', 'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php', + 'HarbormasterBuildPlanStatusTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php', 'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php', 'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php', + 'HarbormasterBuildPlanTransactionType' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php', 'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.php', 'HarbormasterBuildRequest' => 'applications/harbormaster/engine/HarbormasterBuildRequest.php', 'HarbormasterBuildSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildSearchConduitAPIMethod.php', @@ -1366,6 +1375,7 @@ phutil_register_library_map(array( 'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php', 'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php', 'HarbormasterBuildUnitMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php', + 'HarbormasterBuildView' => 'applications/harbormaster/view/HarbormasterBuildView.php', 'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php', 'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php', 'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php', @@ -1419,6 +1429,7 @@ phutil_register_library_map(array( 'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php', 'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php', 'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php', + 'HarbormasterPlanBehaviorController' => 'applications/harbormaster/controller/HarbormasterPlanBehaviorController.php', 'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php', 'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php', 'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php', @@ -1432,6 +1443,7 @@ phutil_register_library_map(array( 'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php', 'HarbormasterQueryBuildsSearchEngineAttachment' => 'applications/harbormaster/engineextension/HarbormasterQueryBuildsSearchEngineAttachment.php', 'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php', + 'HarbormasterRestartException' => 'applications/harbormaster/exception/HarbormasterRestartException.php', 'HarbormasterRunBuildPlansHeraldAction' => 'applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php', 'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php', 'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php', @@ -1520,14 +1532,20 @@ phutil_register_library_map(array( 'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php', 'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', + 'HeraldRuleActionAffectsObjectEdgeType' => 'applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php', 'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php', 'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php', 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', 'HeraldRuleDatasource' => 'applications/herald/typeahead/HeraldRuleDatasource.php', + 'HeraldRuleDisableTransaction' => 'applications/herald/xaction/HeraldRuleDisableTransaction.php', + 'HeraldRuleEditTransaction' => 'applications/herald/xaction/HeraldRuleEditTransaction.php', 'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php', 'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php', 'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php', + 'HeraldRuleIndexEngineExtension' => 'applications/herald/engineextension/HeraldRuleIndexEngineExtension.php', 'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php', + 'HeraldRuleListView' => 'applications/herald/view/HeraldRuleListView.php', + 'HeraldRuleNameTransaction' => 'applications/herald/xaction/HeraldRuleNameTransaction.php', 'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php', 'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php', 'HeraldRuleReplyHandler' => 'applications/herald/mail/HeraldRuleReplyHandler.php', @@ -1535,7 +1553,7 @@ phutil_register_library_map(array( 'HeraldRuleSerializer' => 'applications/herald/editor/HeraldRuleSerializer.php', 'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php', 'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php', - 'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php', + 'HeraldRuleTransactionType' => 'applications/herald/xaction/HeraldRuleTransactionType.php', 'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php', 'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php', 'HeraldRuleTypeDatasource' => 'applications/herald/typeahead/HeraldRuleTypeDatasource.php', @@ -1770,6 +1788,7 @@ phutil_register_library_map(array( 'ManiphestTaskTitleTransaction' => 'applications/maniphest/xaction/ManiphestTaskTitleTransaction.php', 'ManiphestTaskTransactionType' => 'applications/maniphest/xaction/ManiphestTaskTransactionType.php', 'ManiphestTaskUnblockTransaction' => 'applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php', + 'ManiphestTaskUnlockEngine' => 'applications/maniphest/engine/ManiphestTaskUnlockEngine.php', 'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php', 'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php', 'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php', @@ -2960,6 +2979,7 @@ phutil_register_library_map(array( 'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php', 'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php', 'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php', + 'PhabricatorDefaultUnlockEngine' => 'applications/system/engine/PhabricatorDefaultUnlockEngine.php', 'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php', 'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php', 'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', @@ -4687,6 +4707,8 @@ phutil_register_library_map(array( 'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php', 'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php', 'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php', + 'PhabricatorUnlockEngine' => 'applications/system/engine/PhabricatorUnlockEngine.php', + 'PhabricatorUnlockableInterface' => 'applications/system/interface/PhabricatorUnlockableInterface.php', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUserApproveTransaction' => 'applications/people/xaction/PhabricatorUserApproveTransaction.php', @@ -6171,6 +6193,7 @@ phutil_register_library_map(array( 'DifferentialRevisionUpdateTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionViewController' => 'DifferentialController', 'DifferentialRevisionVoidTransaction' => 'DifferentialRevisionTransactionType', + 'DifferentialRevisionWrongBuildsTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionWrongStateTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod', @@ -6936,19 +6959,28 @@ phutil_register_library_map(array( 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', 'PhabricatorProjectInterface', + 'PhabricatorPolicyCodexInterface', ), + 'HarbormasterBuildPlanBehavior' => 'Phobject', + 'HarbormasterBuildPlanBehaviorOption' => 'Phobject', + 'HarbormasterBuildPlanBehaviorTransaction' => 'HarbormasterBuildPlanTransactionType', 'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource', 'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability', 'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability', + 'HarbormasterBuildPlanEditAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'HarbormasterBuildPlanEditEngine' => 'PhabricatorEditEngine', 'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildPlanNameNgrams' => 'PhabricatorSearchNgrams', + 'HarbormasterBuildPlanNameTransaction' => 'HarbormasterBuildPlanTransactionType', 'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType', + 'HarbormasterBuildPlanPolicyCodex' => 'PhabricatorPolicyCodex', 'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPlanSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction', + 'HarbormasterBuildPlanStatusTransaction' => 'HarbormasterBuildPlanTransactionType', + 'HarbormasterBuildPlanTransaction' => 'PhabricatorModularTransaction', 'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'HarbormasterBuildPlanTransactionType' => 'PhabricatorModularTransactionType', 'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildRequest' => 'Phobject', 'HarbormasterBuildSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', @@ -6991,6 +7023,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'HarbormasterBuildUnitMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'HarbormasterBuildView' => 'AphrontView', 'HarbormasterBuildViewController' => 'HarbormasterController', 'HarbormasterBuildWorker' => 'HarbormasterWorker', 'HarbormasterBuildable' => array( @@ -7047,6 +7080,7 @@ phutil_register_library_map(array( 'HarbormasterMessageType' => 'Phobject', 'HarbormasterObject' => 'HarbormasterDAO', 'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup', + 'HarbormasterPlanBehaviorController' => 'HarbormasterPlanController', 'HarbormasterPlanController' => 'HarbormasterController', 'HarbormasterPlanDisableController' => 'HarbormasterPlanController', 'HarbormasterPlanEditController' => 'HarbormasterPlanController', @@ -7060,6 +7094,7 @@ phutil_register_library_map(array( 'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterQueryBuildsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'HarbormasterRestartException' => 'Exception', 'HarbormasterRunBuildPlansHeraldAction' => 'HeraldAction', 'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'HarbormasterScratchTable' => 'HarbormasterDAO', @@ -7159,24 +7194,31 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorIndexableInterface', 'PhabricatorSubscribableInterface', ), + 'HeraldRuleActionAffectsObjectEdgeType' => 'PhabricatorEdgeType', 'HeraldRuleAdapter' => 'HeraldAdapter', 'HeraldRuleAdapterField' => 'HeraldRuleField', 'HeraldRuleController' => 'HeraldController', 'HeraldRuleDatasource' => 'PhabricatorTypeaheadDatasource', + 'HeraldRuleDisableTransaction' => 'HeraldRuleTransactionType', + 'HeraldRuleEditTransaction' => 'HeraldRuleTransactionType', 'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor', 'HeraldRuleField' => 'HeraldField', 'HeraldRuleFieldGroup' => 'HeraldFieldGroup', + 'HeraldRuleIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'HeraldRuleListController' => 'HeraldController', + 'HeraldRuleListView' => 'AphrontView', + 'HeraldRuleNameTransaction' => 'HeraldRuleTransactionType', 'HeraldRulePHIDType' => 'PhabricatorPHIDType', 'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldRuleReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HeraldRuleSerializer' => 'Phobject', 'HeraldRuleTestCase' => 'PhabricatorTestCase', - 'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction', - 'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment', + 'HeraldRuleTransaction' => 'PhabricatorModularTransaction', + 'HeraldRuleTransactionType' => 'PhabricatorModularTransactionType', 'HeraldRuleTranscript' => 'Phobject', 'HeraldRuleTypeConfig' => 'Phobject', 'HeraldRuleTypeDatasource' => 'PhabricatorTypeaheadDatasource', @@ -7388,6 +7430,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineLockableInterface', 'PhabricatorEditEngineMFAInterface', 'PhabricatorPolicyCodexInterface', + 'PhabricatorUnlockableInterface', ), 'ManiphestTaskAssignHeraldAction' => 'HeraldAction', 'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction', @@ -7465,6 +7508,7 @@ phutil_register_library_map(array( 'ManiphestTaskTitleTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskTransactionType' => 'PhabricatorModularTransactionType', 'ManiphestTaskUnblockTransaction' => 'ManiphestTaskTransactionType', + 'ManiphestTaskUnlockEngine' => 'PhabricatorUnlockEngine', 'ManiphestTransaction' => 'PhabricatorModularTransaction', 'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor', @@ -8842,6 +8886,7 @@ phutil_register_library_map(array( 'PhabricatorDebugController' => 'PhabricatorController', 'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle', + 'PhabricatorDefaultUnlockEngine' => 'PhabricatorUnlockEngine', 'PhabricatorDestructibleCodex' => 'Phobject', 'PhabricatorDestructionEngine' => 'Phobject', 'PhabricatorDestructionEngineExtension' => 'Phobject', @@ -10852,6 +10897,7 @@ phutil_register_library_map(array( 'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource', 'PhabricatorUnitsTestCase' => 'PhabricatorTestCase', 'PhabricatorUnknownContentSource' => 'PhabricatorContentSource', + 'PhabricatorUnlockEngine' => 'Phobject', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorUser' => array( 'PhabricatorUserDAO', diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 46d1266b08..48004a521f 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -591,15 +591,11 @@ final class AphrontRequest extends Phobject { } public function getRequestURI() { - $request_uri = idx($_SERVER, 'REQUEST_URI', '/'); + $uri_path = phutil_escape_uri($this->getPath()); + $uri_query = idx($_SERVER, 'QUERY_STRING', ''); - $uri = new PhutilURI($request_uri); - $uri->removeQueryParam('__path__'); - - $path = phutil_escape_uri($this->getPath()); - $uri->setPath($path); - - return $uri; + return id(new PhutilURI($uri_path.'?'.$uri_query)) + ->removeQueryParam('__path__'); } public function getAbsoluteRequestURI() { diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 8cd27fa62b..a479209125 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -776,7 +776,6 @@ final class AphrontApplicationConfiguration 'filler' => str_repeat('Q', 1024 * 16), ); - return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($result); diff --git a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php index f1cb45483e..70fc03345c 100644 --- a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php @@ -147,7 +147,7 @@ final class PhabricatorAuthInviteEngine extends Phobject { // no address. Users can use password recovery to access the other // account if they really control the address. throw id(new PhabricatorAuthInviteAccountException( - pht('Wrong Acount'), + pht('Wrong Account'), pht( 'You are logged in as %s, but the email address you just '. 'clicked a link from is already the primary email address '. diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index c052805224..38ae2201b8 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -714,7 +714,14 @@ final class PhabricatorAuthSessionEngine extends Phobject { if (isset($validation_results[$factor_phid])) { continue; } - $validation_results[$factor_phid] = new PhabricatorAuthFactorResult(); + + $issued_challenges = idx($challenge_map, $factor_phid, array()); + + $validation_results[$factor_phid] = $impl->getResultForPrompt( + $factor, + $viewer, + $request, + $issued_challenges); } throw id(new PhabricatorAuthHighSecurityRequiredException()) diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index d7e6e60ecc..fefd9b5fd1 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -221,6 +221,40 @@ abstract class PhabricatorAuthFactor extends Phobject { return $result; } + final public function getResultForPrompt( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges) { + assert_instances_of($challenges, 'PhabricatorAuthChallenge'); + + $result = $this->newResultForPrompt( + $config, + $viewer, + $request, + $challenges); + + if (!$this->isAuthResult($result)) { + throw new Exception( + pht( + 'Expected "newResultForPrompt()" to return an object of class "%s", '. + 'but it returned something else ("%s"; in "%s").', + 'PhabricatorAuthFactorResult', + phutil_describe_type($result), + get_class($this))); + } + + return $result; + } + + protected function newResultForPrompt( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges) { + return $this->newResult(); + } + abstract protected function newResultFromIssuedChallenges( PhabricatorAuthFactorConfig $config, PhabricatorUser $viewer, diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php index 66bd7c9ebd..a84337a764 100644 --- a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php @@ -681,6 +681,19 @@ final class PhabricatorDuoAuthFactor AphrontRequest $request, array $challenges) { + return $this->getResultForPrompt( + $config, + $viewer, + $request, + $challenges); + } + + protected function newResultForPrompt( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges) { + $result = $this->newResult() ->setIsContinue(true) ->setErrorMessage( diff --git a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php index f9ba48b372..dc241a04b4 100644 --- a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php +++ b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php @@ -58,6 +58,10 @@ final class PhabricatorConduitCallManagementWorkflow 'No such user "%s" exists.', $as)); } + + // Allow inline generation of user caches for the user we're acting + // as, since some calls may read user preferences. + $actor->setAllowInlineCacheGeneration(true); } else { $actor = $viewer; } diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index 05831a782d..0fbfaa2fc3 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -409,4 +409,19 @@ abstract class ConduitAPIMethod $capability); } + final protected function newRemarkupDocumentationView($remarkup) { + $viewer = $this->getViewer(); + + $view = new PHUIRemarkupView($viewer, $remarkup); + + $view->setRemarkupOptions( + array( + PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false, + )); + + return id(new PHUIBoxView()) + ->appendChild($view) + ->addPadding(PHUI::PADDING_LARGE); + } + } diff --git a/src/applications/config/check/PhabricatorWebServerSetupCheck.php b/src/applications/config/check/PhabricatorWebServerSetupCheck.php index 8f6885e8e8..284b5e2a5f 100644 --- a/src/applications/config/check/PhabricatorWebServerSetupCheck.php +++ b/src/applications/config/check/PhabricatorWebServerSetupCheck.php @@ -129,30 +129,16 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck { } $structure = null; - $caught = null; $extra_whitespace = ($body !== trim($body)); - if (!$extra_whitespace) { - try { - $structure = phutil_json_decode($body); - } catch (Exception $ex) { - $caught = $ex; - } + try { + $structure = phutil_json_decode(trim($body)); + } catch (Exception $ex) { + // Ignore the exception, we only care if the decode worked or not. } - if (!$structure) { - if ($extra_whitespace) { - $message = pht( - 'Phabricator sent itself a test request and expected to get a bare '. - 'JSON response back, but the response had extra whitespace at '. - 'the beginning or end.'. - "\n\n". - 'This usually means you have edited a file and left whitespace '. - 'characters before the opening %s tag, or after a closing %s tag. '. - 'Remove any leading whitespace, and prefer to omit closing tags.', - phutil_tag('tt', array(), '')); - } else { + if (!$structure || $extra_whitespace) { + if (!$structure) { $short = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(1024) ->truncateString($body); @@ -166,6 +152,17 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck { "\n\n". 'Something is misconfigured or otherwise mangling responses.', phutil_tag('pre', array(), $short)); + } else { + $message = pht( + 'Phabricator sent itself a test request and expected to get a bare '. + 'JSON response back. It received a JSON response, but the response '. + 'had extra whitespace at the beginning or end.'. + "\n\n". + 'This usually means you have edited a file and left whitespace '. + 'characters before the opening %s tag, or after a closing %s tag. '. + 'Remove any leading whitespace, and prefer to omit closing tags.', + phutil_tag('tt', array(), '')); } $this->newIssue('webserver.mangle') @@ -174,7 +171,9 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck { ->setMessage($message); // We can't run the other checks if we could not decode the response. - return; + if (!$structure) { + return; + } } $actual_user = idx($structure, 'user'); diff --git a/src/applications/config/type/PhabricatorSetConfigType.php b/src/applications/config/type/PhabricatorSetConfigType.php index 805ae50468..553ee614b8 100644 --- a/src/applications/config/type/PhabricatorSetConfigType.php +++ b/src/applications/config/type/PhabricatorSetConfigType.php @@ -43,7 +43,7 @@ final class PhabricatorSetConfigType } if ($value) { - if (array_keys($value) !== range(0, count($value) - 1)) { + if (!phutil_is_natural_list($value)) { throw $this->newException( pht( 'Option "%s" is of type "%s", and should be specified on the '. diff --git a/src/applications/differential/controller/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/DifferentialInlineCommentEditController.php index 9741cc93ee..1de156a9b7 100644 --- a/src/applications/differential/controller/DifferentialInlineCommentEditController.php +++ b/src/applications/differential/controller/DifferentialInlineCommentEditController.php @@ -204,9 +204,9 @@ final class DifferentialInlineCommentEditController queryfx( $conn_w, - 'INSERT IGNORE INTO %T (userPHID, commentID) VALUES %Q', + 'INSERT IGNORE INTO %T (userPHID, commentID) VALUES %LQ', $table->getTableName(), - implode(', ', $sql)); + $sql); } protected function showComments(array $ids) { diff --git a/src/applications/differential/editor/DifferentialDiffEditor.php b/src/applications/differential/editor/DifferentialDiffEditor.php index 261f6f1598..e78e08d808 100644 --- a/src/applications/differential/editor/DifferentialDiffEditor.php +++ b/src/applications/differential/editor/DifferentialDiffEditor.php @@ -208,15 +208,6 @@ final class DifferentialDiffEditor return $adapter; } - protected function didApplyHeraldRules( - PhabricatorLiskDAO $object, - HeraldAdapter $adapter, - HeraldTranscript $transcript) { - - $xactions = array(); - return $xactions; - } - private function updateDiffFromDict(DifferentialDiff $diff, $dict) { $diff ->setSourcePath(idx($dict, 'sourcePath')) diff --git a/src/applications/differential/engine/DifferentialChangesetEngine.php b/src/applications/differential/engine/DifferentialChangesetEngine.php index d72db025ad..23382e6a81 100644 --- a/src/applications/differential/engine/DifferentialChangesetEngine.php +++ b/src/applications/differential/engine/DifferentialChangesetEngine.php @@ -54,6 +54,12 @@ final class DifferentialChangesetEngine extends Phobject { if (strpos($new_data, '@'.'generated') !== false) { return true; } + + // See PHI1112. This is the official pattern for marking Go code as + // generated. + if (preg_match('(^// Code generated .* DO NOT EDIT\.$)m', $new_data)) { + return true; + } } return false; diff --git a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php index 861d2ad220..7b94b1958b 100644 --- a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php +++ b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php @@ -285,6 +285,24 @@ final class DifferentialDiffExtractionEngine extends Phobject { ->setNewValue($revision->getModernRevisionStatus()); } + $concerning_builds = $this->loadConcerningBuilds($revision); + if ($concerning_builds) { + $build_list = array(); + foreach ($concerning_builds as $build) { + $build_list[] = array( + 'phid' => $build->getPHID(), + 'status' => $build->getBuildStatus(), + ); + } + + $wrong_builds = + DifferentialRevisionWrongBuildsTransaction::TRANSACTIONTYPE; + + $xactions[] = id(new DifferentialTransaction()) + ->setTransactionType($wrong_builds) + ->setNewValue($build_list); + } + $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; $xactions[] = id(new DifferentialTransaction()) @@ -322,4 +340,81 @@ final class DifferentialDiffExtractionEngine extends Phobject { return $result_data; } + private function loadConcerningBuilds(DifferentialRevision $revision) { + $viewer = $this->getViewer(); + $diff = $revision->getActiveDiff(); + + $buildables = id(new HarbormasterBuildableQuery()) + ->setViewer($viewer) + ->withBuildablePHIDs(array($diff->getPHID())) + ->needBuilds(true) + ->withManualBuildables(false) + ->execute(); + if (!$buildables) { + return array(); + } + + + $land_key = HarbormasterBuildPlanBehavior::BEHAVIOR_LANDWARNING; + $behavior = HarbormasterBuildPlanBehavior::getBehavior($land_key); + + $key_never = HarbormasterBuildPlanBehavior::LANDWARNING_NEVER; + $key_building = HarbormasterBuildPlanBehavior::LANDWARNING_IF_BUILDING; + $key_complete = HarbormasterBuildPlanBehavior::LANDWARNING_IF_COMPLETE; + + $concerning_builds = array(); + foreach ($buildables as $buildable) { + $builds = $buildable->getBuilds(); + foreach ($builds as $build) { + $plan = $build->getBuildPlan(); + $option = $behavior->getPlanOption($plan); + $behavior_value = $option->getKey(); + + $if_never = ($behavior_value === $key_never); + if ($if_never) { + continue; + } + + $if_building = ($behavior_value === $key_building); + if ($if_building && $build->isComplete()) { + continue; + } + + $if_complete = ($behavior_value === $key_complete); + if ($if_complete) { + if (!$build->isComplete()) { + continue; + } + + // TODO: If you "arc land" and a build with "Warn: If Complete" + // is still running, you may not see a warning, and push the revision + // in good faith. The build may then complete before we get here, so + // we now see a completed, failed build. + + // For now, just err on the side of caution and assume these builds + // were in a good state when we prompted the user, even if they're in + // a bad state now. + + // We could refine this with a rule like "if the build finished + // within a couple of minutes before the push happened, assume it was + // in good faith", but we don't currently have an especially + // convenient way to check when the build finished or when the commit + // was pushed or discovered, and this would create some issues in + // cases where the repository is observed and the fetch pipeline + // stalls for a while. + + continue; + } + + if ($build->isPassed()) { + continue; + } + + $concerning_builds[] = $build; + } + } + + return $concerning_builds; + } + } diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index 7efd29519e..d803e92c6c 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -162,7 +162,20 @@ final class DifferentialChangesetTwoUpRenderer } else if (empty($new_lines[$ii])) { $o_class = 'old old-full'; } else { - $o_class = 'old'; + if (isset($depth_only[$ii])) { + if ($depth_only[$ii] == '>') { + // When a line has depth-only change, we only highlight the + // left side of the diff if the depth is decreasing. When the + // depth is increasing, the ">>" marker on the right hand side + // of the diff generally provides enough visibility on its own. + + $o_class = ''; + } else { + $o_class = 'old'; + } + } else { + $o_class = 'old'; + } } $o_classes = $o_class; } @@ -200,13 +213,10 @@ final class DifferentialChangesetTwoUpRenderer } else if (empty($old_lines[$ii])) { $n_class = 'new new-full'; } else { - - // NOTE: At least for the moment, I'm intentionally clearing the - // line highlighting only on the right side of the diff when a - // line has only depth changes. When a block depth is decreased, - // this gives us a large color block on the left (to make it easy - // to see the depth change) but a clean diff on the right (to make - // it easy to pick out actual code changes). + // When a line has a depth-only change, never highlight it on + // the right side. The ">>" marker generally provides enough + // visibility on its own for indent depth increases, and the left + // side is still highlighted for indent depth decreases. if (isset($depth_only[$ii])) { $n_class = ''; diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 3397f9cb03..a2a058568b 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -877,7 +877,7 @@ final class DifferentialRevision extends DifferentialDAO PhabricatorUser $viewer, array $phids) { - return id(new HarbormasterBuildQuery()) + $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs($phids) ->withAutobuilds(false) @@ -893,6 +893,41 @@ final class DifferentialRevision extends DifferentialDAO HarbormasterBuildStatus::STATUS_DEADLOCKED, )) ->execute(); + + // Filter builds based on the "Hold Drafts" behavior of their associated + // build plans. + + $hold_drafts = HarbormasterBuildPlanBehavior::BEHAVIOR_DRAFTS; + $behavior = HarbormasterBuildPlanBehavior::getBehavior($hold_drafts); + + $key_never = HarbormasterBuildPlanBehavior::DRAFTS_NEVER; + $key_building = HarbormasterBuildPlanBehavior::DRAFTS_IF_BUILDING; + + foreach ($builds as $key => $build) { + $plan = $build->getBuildPlan(); + $hold_key = $behavior->getPlanOption($plan)->getKey(); + + $hold_never = ($hold_key === $key_never); + $hold_building = ($hold_key === $key_building); + + // If the build "Never" holds drafts from promoting, we don't care what + // the status is. + if ($hold_never) { + unset($builds[$key]); + continue; + } + + // If the build holds drafts from promoting "While Building", we only + // care about the status until it completes. + if ($hold_building) { + if ($build->isComplete()) { + unset($builds[$key]); + continue; + } + } + } + + return $builds; } diff --git a/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php b/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php new file mode 100644 index 0000000000..260813b75b --- /dev/null +++ b/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php @@ -0,0 +1,37 @@ + $path, )); - $before_uri = $before_uri->alter('renamed', $renamed); - $before_uri = $before_uri->alter('follow', $follow); + if ($renamed === null) { + $before_uri->removeQueryParam('renamed'); + } else { + $before_uri->replaceQueryParam('renamed', $renamed); + } + + if ($follow === null) { + $before_uri->removeQueryParam('follow'); + } else { + $before_uri->replaceQueryParam('follow', $follow); + } return id(new AphrontRedirectResponse())->setURI($before_uri); } diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index cb4ad0ba95..aea901f100 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -192,7 +192,10 @@ final class DiffusionServeController extends DiffusionController { // Try Git LFS auth first since we can usually reject it without doing // any queries, since the username won't match the one we expect or the // request won't be LFS. - $viewer = $this->authenticateGitLFSUser($username, $password); + $viewer = $this->authenticateGitLFSUser( + $username, + $password, + $identifier); // If that failed, try normal auth. Note that we can use normal auth on // LFS requests, so this isn't strictly an alternative to LFS auth. @@ -655,7 +658,8 @@ final class DiffusionServeController extends DiffusionController { private function authenticateGitLFSUser( $username, - PhutilOpaqueEnvelope $password) { + PhutilOpaqueEnvelope $password, + $identifier) { // Never accept these credentials for requests which aren't LFS requests. if (!$this->getIsGitLFSRequest()) { @@ -668,11 +672,31 @@ final class DiffusionServeController extends DiffusionController { return null; } + // See PHI1123. We need to be able to constrain the token query with + // "withTokenResources(...)" to take advantage of the key on the table. + // In this case, the repository PHID is the "resource" we're after. + + // In normal workflows, we figure out the viewer first, then use the + // viewer to load the repository, but that won't work here. Load the + // repository as the omnipotent viewer, then use the repository PHID to + // look for a token. + + $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); + + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($omnipotent_viewer) + ->withIdentifiers(array($identifier)) + ->executeOne(); + if (!$repository) { + return null; + } + $lfs_pass = $password->openEnvelope(); $lfs_hash = PhabricatorHash::weakDigest($lfs_pass); $token = id(new PhabricatorAuthTemporaryTokenQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($omnipotent_viewer) + ->withTokenResources(array($repository->getPHID())) ->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE)) ->withTokenCodes(array($lfs_hash)) ->withExpired(false) @@ -682,7 +706,7 @@ final class DiffusionServeController extends DiffusionController { } $user = id(new PhabricatorPeopleQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($omnipotent_viewer) ->withPHIDs(array($token->getUserPHID())) ->executeOne(); diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php index bcf3ad6d74..d585c5774d 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php @@ -444,13 +444,15 @@ final class DiffusionRepositoryBasicsManagementPanel id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget( - pht('Missing Binary %s', phutil_tag('tt', array(), $binary))) - ->setNote(pht( - 'Unable to find this binary in `%s`. '. - 'You need to configure %s and include %s.', - 'environment.append-paths', - $this->getEnvConfigLink(), - $path))); + pht('Commit Hooks: %s', phutil_tag('tt', array(), $binary))) + ->setNote( + pht( + 'The directory containing the "svnlook" binary is not '. + 'listed in "environment.append-paths", so commit hooks '. + '(which execute with an empty "PATH") will not be able to '. + 'find "svnlook". Add `%s` to %s.', + $path, + $this->getEnvConfigLink()))); } } } diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index b2d1d25f44..57dc83953d 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -127,9 +127,9 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { // This is suppressing "added
to the list of known hosts" // messages, which are confusing and irrelevant when they arise from // proxied requests. It might also be suppressing lots of useful errors, - // of course. Ideally, we would enforce host keys eventually. + // of course. Ideally, we would enforce host keys eventually. See T13121. $options[] = '-o'; - $options[] = 'LogLevel=quiet'; + $options[] = 'LogLevel=ERROR'; // NOTE: We prefix the command with "@username", which the far end of the // connection will parse in order to act as the specified user. This diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php index 1aab14b57b..b1eebd92a1 100644 --- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php @@ -30,8 +30,11 @@ final class DrydockSSHCommandInterface extends DrydockCommandInterface { $full_command = call_user_func_array('csprintf', $argv); $flags = array(); + + // See T13121. Attempt to suppress the "Permanently added X to list of + // known hosts" message without suppressing anything important. $flags[] = '-o'; - $flags[] = 'LogLevel=quiet'; + $flags[] = 'LogLevel=ERROR'; $flags[] = '-o'; $flags[] = 'StrictHostKeyChecking=no'; diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php index 80be90b375..4b369e821e 100644 --- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php +++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php @@ -83,6 +83,8 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication { => 'HarbormasterPlanEditController', 'order/(?:(?P\d+)/)?' => 'HarbormasterPlanOrderController', 'disable/(?P\d+)/' => 'HarbormasterPlanDisableController', + 'behavior/(?P\d+)/(?P[^/]+)/' => + 'HarbormasterPlanBehaviorController', 'run/(?P\d+)/' => 'HarbormasterPlanRunController', '(?P\d+)/' => 'HarbormasterPlanViewController', ), diff --git a/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php b/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php new file mode 100644 index 0000000000..a17f2fb293 --- /dev/null +++ b/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php @@ -0,0 +1,38 @@ +getObject(); + $run_with_view = $object->canRunWithoutEditCapability(); + + $rules = array(); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setIsActive(!$run_with_view) + ->setDescription( + pht( + 'You must have edit permission on this build plan to pause, '. + 'abort, resume, or restart it.')); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setIsActive(!$run_with_view) + ->setDescription( + pht( + 'You must have edit permission on this build plan to run it '. + 'manually.')); + + return $rules; + } + + +} diff --git a/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php new file mode 100644 index 0000000000..5509cf189e --- /dev/null +++ b/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php @@ -0,0 +1,20 @@ +key === self::STATUS_PASSED); } + public function isFailed() { + return ($this->key === self::STATUS_FAILED); + } + /** * Get a human readable name for a build status constant. diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index 843ffd4702..6a4a2b1fee 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -65,13 +65,12 @@ final class HarbormasterBuildActionController 'restart build?'); $submit = pht('Restart Build'); } else { - $title = pht('Unable to Restart Build'); - if ($build->isRestarting()) { - $body = pht( - 'This build is already restarting. You can not reissue a '. - 'restart command to a restarting build.'); - } else { - $body = pht('You can not restart this build.'); + try { + $build->assertCanRestartBuild(); + throw new Exception(pht('Expected to be unable to restart build.')); + } catch (HarbormasterRestartException $ex) { + $title = $ex->getTitle(); + $body = $ex->getBody(); } } break; @@ -135,8 +134,7 @@ final class HarbormasterBuildActionController break; } - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) + $dialog = $this->newDialog() ->setTitle($title) ->appendChild($body) ->addCancelButton($return_uri); @@ -145,7 +143,7 @@ final class HarbormasterBuildActionController $dialog->addSubmitButton($submit); } - return id(new AphrontDialogResponse())->setDialog($dialog); + return $dialog; } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php new file mode 100644 index 0000000000..8f1fece691 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php @@ -0,0 +1,92 @@ +getViewer(); + + $plan = id(new HarbormasterBuildPlanQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$plan) { + return new Aphront404Response(); + } + + $behavior_key = $request->getURIData('behaviorKey'); + $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey(); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + $behavior = idx($behaviors, $behavior_key); + if (!$behavior) { + return new Aphront404Response(); + } + + $plan_uri = $plan->getURI(); + + $v_option = $behavior->getPlanOption($plan)->getKey(); + if ($request->isFormPost()) { + $v_option = $request->getStr('option'); + + $xactions = array(); + + $xactions[] = id(new HarbormasterBuildPlanTransaction()) + ->setTransactionType( + HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE) + ->setMetadataValue($metadata_key, $behavior_key) + ->setNewValue($v_option); + + $editor = id(new HarbormasterBuildPlanEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($plan, $xactions); + + return id(new AphrontRedirectResponse())->setURI($plan_uri); + } + + $select_control = id(new AphrontFormRadioButtonControl()) + ->setName('option') + ->setValue($v_option) + ->setLabel(pht('Option')); + + foreach ($behavior->getOptions() as $option) { + $icon = id(new PHUIIconView()) + ->setIcon($option->getIcon()); + + $select_control->addButton( + $option->getKey(), + array( + $icon, + ' ', + $option->getName(), + ), + $option->getDescription()); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendInstructions( + pht( + 'Choose a build plan behavior for "%s".', + phutil_tag('strong', array(), $behavior->getName()))) + ->appendRemarkupInstructions($behavior->getEditInstructions()) + ->appendControl($select_control); + + return $this->newDialog() + ->setTitle(pht('Edit Behavior: %s', $behavior->getName())) + ->appendForm($form) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->addSubmitButton(pht('Save Changes')) + ->addCancelButton($plan_uri); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php b/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php index ccf6b8986f..65a993396d 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php @@ -19,11 +19,11 @@ final class HarbormasterPlanDisableController return new Aphront404Response(); } - $plan_uri = $this->getApplicationURI('plan/'.$plan->getID().'/'); + $plan_uri = $plan->getURI(); if ($request->isFormPost()) { - $type_status = HarbormasterBuildPlanTransaction::TYPE_STATUS; + $type_status = HarbormasterBuildPlanStatusTransaction::TRANSACTIONTYPE; $v_status = $plan->isDisabled() ? HarbormasterBuildPlan::STATUS_ACTIVE diff --git a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php index fd227ee554..5d80d421aa 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php @@ -9,16 +9,13 @@ final class HarbormasterPlanRunController extends HarbormasterPlanController { $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($plan_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) ->executeOne(); if (!$plan) { return new Aphront404Response(); } + $plan->assertHasRunCapability($viewer); + $cancel_uri = $this->getApplicationURI("plan/{$plan_id}/"); if (!$plan->canRunManually()) { diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 6ebadf7a62..a9af90f2a5 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -18,11 +18,6 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { return new Aphront404Response(); } - $timeline = $this->buildTransactionTimeline( - $plan, - new HarbormasterBuildPlanTransactionQuery()); - $timeline->setShouldTerminate(true); - $title = $plan->getName(); $header = id(new PHUIHeaderView()) @@ -33,24 +28,30 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { $curtain = $this->buildCurtainView($plan); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Plan %d', $id)); - $crumbs->setBorder(true); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($plan->getObjectName()) + ->setBorder(true); - list($step_list, $has_any_conflicts, $would_deadlock) = + list($step_list, $has_any_conflicts, $would_deadlock, $steps) = $this->buildStepList($plan); $error = null; - if ($would_deadlock) { - $error = pht('This build plan will deadlock when executed, due to '. - 'circular dependencies present in the build plan. '. - 'Examine the step list and resolve the deadlock.'); + if (!$steps) { + $error = pht( + 'This build plan does not have any build steps yet, so it will '. + 'not do anything when run.'); + } else if ($would_deadlock) { + $error = pht( + 'This build plan will deadlock when executed, due to circular '. + 'dependencies present in the build plan. Examine the step list '. + 'and resolve the deadlock.'); } else if ($has_any_conflicts) { // A deadlocking build will also cause all the artifacts to be // invalid, so we just skip showing this message if that's the // case. - $error = pht('This build plan has conflicts in one or more build steps. '. - 'Examine the step list and resolve the listed errors.'); + $error = pht( + 'This build plan has conflicts in one or more build steps. '. + 'Examine the step list and resolve the listed errors.'); } if ($error) { @@ -59,18 +60,32 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->appendChild($error); } + $builds_view = $this->newBuildsView($plan); + $options_view = $this->newOptionsView($plan); + $rules_view = $this->newRulesView($plan); + + $timeline = $this->buildTransactionTimeline( + $plan, + new HarbormasterBuildPlanTransactionQuery()); + $timeline->setShouldTerminate(true); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) - ->setMainColumn(array( - $error, - $step_list, - $timeline, - )); + ->setMainColumn( + array( + $error, + $step_list, + $options_view, + $rules_view, + $builds_view, + $timeline, + )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setPageObjectPHIDs(array($plan->getPHID())) ->appendChild($view); } @@ -213,7 +228,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($step_list); - return array($step_box, $has_any_conflicts, $is_deadlocking); + return array($step_box, $has_any_conflicts, $is_deadlocking, $steps); } private function buildCurtainView(HarbormasterBuildPlan $plan) { @@ -253,7 +268,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->setIcon('fa-ban')); } - $can_run = ($can_edit && $plan->canRunManually()); + $can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually()); $curtain->addAction( id(new PhabricatorActionView()) @@ -263,11 +278,6 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->setDisabled(!$can_run) ->setIcon('fa-play-circle')); - $curtain->addPanel( - id(new PHUICurtainPanelView()) - ->setHeaderText(pht('Created')) - ->appendChild(phabricator_datetime($plan->getDateCreated(), $viewer))); - return $curtain; } @@ -381,7 +391,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { array $steps) { $has_conflicts = false; - if (count($step_phids) === 0) { + if (!$step_phids) { return null; } @@ -441,4 +451,149 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { return array($ui, $has_conflicts); } + + private function newBuildsView(HarbormasterBuildPlan $plan) { + $viewer = $this->getViewer(); + + $builds = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withBuildPlanPHIDs(array($plan->getPHID())) + ->setLimit(10) + ->execute(); + + $list = id(new HarbormasterBuildView()) + ->setViewer($viewer) + ->setBuilds($builds) + ->newObjectList(); + + $list->setNoDataString(pht('No recent builds.')); + + $more_href = new PhutilURI( + $this->getApplicationURI('/build/'), + array('plan' => $plan->getPHID())); + + $more_link = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list-ul') + ->setText(pht('View All Builds')) + ->setHref($more_href); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Builds')) + ->addActionLink($more_link); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + } + + private function newRulesView(HarbormasterBuildPlan $plan) { + $viewer = $this->getViewer(); + + $rules = id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withDisabled(false) + ->withAffectedObjectPHIDs(array($plan->getPHID())) + ->needValidateAuthors(true) + ->setLimit(10) + ->execute(); + + $list = id(new HeraldRuleListView()) + ->setViewer($viewer) + ->setRules($rules) + ->newObjectList(); + + $list->setNoDataString(pht('No active Herald rules trigger this build.')); + + $more_href = new PhutilURI( + '/herald/', + array('affectedPHID' => $plan->getPHID())); + + $more_link = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list-ul') + ->setText(pht('View All Rules')) + ->setHref($more_href); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Run By Herald Rules')) + ->addActionLink($more_link); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + } + + private function newOptionsView(HarbormasterBuildPlan $plan) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $plan, + PhabricatorPolicyCapability::CAN_EDIT); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + + $rows = array(); + foreach ($behaviors as $behavior) { + $option = $behavior->getPlanOption($plan); + + $icon = $option->getIcon(); + $icon = id(new PHUIIconView())->setIcon($icon); + + $edit_uri = new PhutilURI( + $this->getApplicationURI( + urisprintf( + 'plan/behavior/%d/%s/', + $plan->getID(), + $behavior->getKey()))); + + $edit_button = id(new PHUIButtonView()) + ->setTag('a') + ->setColor(PHUIButtonView::GREY) + ->setSize(PHUIButtonView::SMALL) + ->setDisabled(!$can_edit) + ->setWorkflow(true) + ->setText(pht('Edit')) + ->setHref($edit_uri); + + $rows[] = array( + $icon, + $behavior->getName(), + $option->getName(), + $option->getDescription(), + $edit_button, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + null, + pht('Name'), + pht('Behavior'), + pht('Details'), + null, + )) + ->setColumnClasses( + array( + null, + 'pri', + null, + 'wide', + null, + )); + + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Plan Behaviors')); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + } diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php index 11837051c3..c0fa80d71b 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php @@ -77,17 +77,48 @@ final class HarbormasterBuildPlanEditEngine } protected function buildCustomEditFields($object) { - return array( + $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setIsRequired(true) - ->setTransactionType(HarbormasterBuildPlanTransaction::TYPE_NAME) + ->setTransactionType( + HarbormasterBuildPlanNameTransaction::TRANSACTIONTYPE) ->setDescription(pht('The build plan name.')) ->setConduitDescription(pht('Rename the plan.')) ->setConduitTypeDescription(pht('New plan name.')) ->setValue($object->getName()), ); + + + $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey(); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + foreach ($behaviors as $behavior) { + $key = $behavior->getKey(); + + // Get the raw key off the object so that we don't reset stuff to + // default values by mistake if a behavior goes missing somehow. + $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey( + $key); + $behavior_option = $object->getPlanProperty($storage_key); + + if (!strlen($behavior_option)) { + $behavior_option = $behavior->getPlanOption($object)->getKey(); + } + + $fields[] = id(new PhabricatorSelectEditField()) + ->setIsFormField(false) + ->setKey(sprintf('behavior.%s', $behavior->getKey())) + ->setMetadataValue($metadata_key, $behavior->getKey()) + ->setLabel(pht('Behavior: %s', $behavior->getName())) + ->setTransactionType( + HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE) + ->setValue($behavior_option) + ->setOptions($behavior->getOptionMap()); + } + + return $fields; } } diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php index 71c9283ade..1b340b6524 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php @@ -11,100 +11,23 @@ final class HarbormasterBuildPlanEditor return pht('Harbormaster Build Plans'); } + public function getCreateObjectTitle($author, $object) { + return pht('%s created this build plan.', $author); + } + + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); + } + protected function supportsSearch() { return true; } public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = HarbormasterBuildPlanTransaction::TYPE_NAME; - $types[] = HarbormasterBuildPlanTransaction::TYPE_STATUS; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - if ($this->getIsNewObject()) { - return null; - } - return $object->getName(); - case HarbormasterBuildPlanTransaction::TYPE_STATUS: - return $object->getPlanStatus(); - } - - return parent::getCustomTransactionOldValue($object, $xaction); - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - return $xaction->getNewValue(); - case HarbormasterBuildPlanTransaction::TYPE_STATUS: - return $xaction->getNewValue(); - } - return parent::getCustomTransactionNewValue($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - case HarbormasterBuildPlanTransaction::TYPE_STATUS: - $object->setPlanStatus($xaction->getNewValue()); - return; - } - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - case HarbormasterBuildPlanTransaction::TYPE_STATUS: - return; - } - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('You must choose a name for your build plan.'), - last($xactions)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - } - - return $errors; - } - - } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php index 170e4c8a5c..447bd53704 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -497,9 +497,33 @@ final class HarbormasterBuildEngine extends Phobject { // passed everything it needs to. if (!$buildable->isPreparing()) { + $behavior_key = HarbormasterBuildPlanBehavior::BEHAVIOR_BUILDABLE; + $behavior = HarbormasterBuildPlanBehavior::getBehavior($behavior_key); + + $key_never = HarbormasterBuildPlanBehavior::BUILDABLE_NEVER; + $key_building = HarbormasterBuildPlanBehavior::BUILDABLE_IF_BUILDING; + $all_pass = true; $any_fail = false; foreach ($buildable->getBuilds() as $build) { + $plan = $build->getBuildPlan(); + $option = $behavior->getPlanOption($plan); + $option_key = $option->getKey(); + + $is_never = ($option_key === $key_never); + $is_building = ($option_key === $key_building); + + // If this build "Never" affects the buildable, ignore it. + if ($is_never) { + continue; + } + + // If this build affects the buildable "If Building", but is already + // complete, ignore it. + if ($is_building && $build->isComplete()) { + continue; + } + if (!$build->isPassed()) { $all_pass = false; } diff --git a/src/applications/harbormaster/exception/HarbormasterRestartException.php b/src/applications/harbormaster/exception/HarbormasterRestartException.php new file mode 100644 index 0000000000..bd0b86184a --- /dev/null +++ b/src/applications/harbormaster/exception/HarbormasterRestartException.php @@ -0,0 +1,33 @@ +setTitle($title); + $this->appendParagraph($body); + + parent::__construct($title); + } + + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function getTitle() { + return $this->title; + } + + public function appendParagraph($description) { + $this->body[] = $description; + return $this; + } + + public function getBody() { + return $this->body; + } + +} diff --git a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php index 8c718e5f5d..9fc053e8ae 100644 --- a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php +++ b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php @@ -91,4 +91,9 @@ final class HarbormasterRunBuildPlansHeraldAction 'Run build plans: %s.', $this->renderHandleList($value)); } + + public function getPHIDsAffectedByAction(HeraldActionRecord $record) { + return $record->getTarget(); + } + } diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php new file mode 100644 index 0000000000..d8e857e711 --- /dev/null +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -0,0 +1,394 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setEditInstructions($edit_instructions) { + $this->editInstructions = $edit_instructions; + return $this; + } + + public function getEditInstructions() { + return $this->editInstructions; + } + + public function getOptionMap() { + return mpull($this->options, 'getName', 'getKey'); + } + + public function setOptions(array $options) { + assert_instances_of($options, 'HarbormasterBuildPlanBehaviorOption'); + + $key_map = array(); + $default = null; + + foreach ($options as $option) { + $key = $option->getKey(); + + if (isset($key_map[$key])) { + throw new Exception( + pht( + 'Multiple behavior options (for behavior "%s") have the same '. + 'key ("%s"). Each option must have a unique key.', + $this->getKey(), + $key)); + } + $key_map[$key] = true; + + if ($option->getIsDefault()) { + if ($default === null) { + $default = $key; + } else { + throw new Exception( + pht( + 'Multiple behavior options (for behavior "%s") are marked as '. + 'default options ("%s" and "%s"). Exactly one option must be '. + 'marked as the default option.', + $this->getKey(), + $default, + $key)); + } + } + } + + if ($default === null) { + throw new Exception( + pht( + 'No behavior option is marked as the default option (for '. + 'behavior "%s"). Exactly one option must be marked as the '. + 'default option.', + $this->getKey())); + } + + $this->options = mpull($options, null, 'getKey'); + $this->defaultKey = $default; + + return $this; + } + + public function getOptions() { + return $this->options; + } + + public function getPlanOption(HarbormasterBuildPlan $plan) { + $behavior_key = $this->getKey(); + $storage_key = self::getStorageKeyForBehaviorKey($behavior_key); + + $plan_value = $plan->getPlanProperty($storage_key); + if (isset($this->options[$plan_value])) { + return $this->options[$plan_value]; + } + + return idx($this->options, $this->defaultKey); + } + + public static function getTransactionMetadataKey() { + return 'behavior-key'; + } + + public static function getStorageKeyForBehaviorKey($behavior_key) { + return sprintf('behavior.%s', $behavior_key); + } + + public static function getBehavior($key) { + $behaviors = self::newPlanBehaviors(); + + if (!isset($behaviors[$key])) { + throw new Exception( + pht( + 'No build plan behavior with key "%s" exists.', + $key)); + } + + return $behaviors[$key]; + } + + public static function newPlanBehaviors() { + $draft_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::DRAFTS_ALWAYS) + ->setIcon('fa-check-circle-o green') + ->setName(pht('Always')) + ->setIsDefault(true) + ->setDescription( + pht( + 'Revisions are not sent for review until the build completes, '. + 'and are returned to the author for updates if the build fails.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::DRAFTS_IF_BUILDING) + ->setIcon('fa-pause-circle-o yellow') + ->setName(pht('If Building')) + ->setDescription( + pht( + 'Revisions are not sent for review until the build completes, '. + 'but they will be sent for review even if it fails.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::DRAFTS_NEVER) + ->setIcon('fa-circle-o red') + ->setName(pht('Never')) + ->setDescription( + pht( + 'Revisions are sent for review regardless of the status of the '. + 'build.')), + ); + + $land_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::LANDWARNING_ALWAYS) + ->setIcon('fa-check-circle-o green') + ->setName(pht('Always')) + ->setIsDefault(true) + ->setDescription( + pht( + '"arc land" warns if the build is still running or has '. + 'failed.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::LANDWARNING_IF_BUILDING) + ->setIcon('fa-pause-circle-o yellow') + ->setName(pht('If Building')) + ->setDescription( + pht( + '"arc land" warns if the build is still running, but ignores '. + 'the build if it has failed.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::LANDWARNING_IF_COMPLETE) + ->setIcon('fa-dot-circle-o yellow') + ->setName(pht('If Complete')) + ->setDescription( + pht( + '"arc land" warns if the build has failed, but ignores the '. + 'build if it is still running.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::LANDWARNING_NEVER) + ->setIcon('fa-circle-o red') + ->setName(pht('Never')) + ->setDescription( + pht( + '"arc land" never warns that the build is still running or '. + 'has failed.')), + ); + + $aggregate_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::BUILDABLE_ALWAYS) + ->setIcon('fa-check-circle-o green') + ->setName(pht('Always')) + ->setIsDefault(true) + ->setDescription( + pht( + 'The buildable waits for the build, and fails if the '. + 'build fails.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::BUILDABLE_IF_BUILDING) + ->setIcon('fa-pause-circle-o yellow') + ->setName(pht('If Building')) + ->setDescription( + pht( + 'The buildable waits for the build, but does not fail '. + 'if the build fails.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::BUILDABLE_NEVER) + ->setIcon('fa-circle-o red') + ->setName(pht('Never')) + ->setDescription( + pht( + 'The buildable does not wait for the build.')), + ); + + $restart_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::RESTARTABLE_ALWAYS) + ->setIcon('fa-repeat green') + ->setName(pht('Always')) + ->setIsDefault(true) + ->setDescription( + pht('The build may be restarted.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::RESTARTABLE_IF_FAILED) + ->setIcon('fa-times-circle-o yellow') + ->setName(pht('If Failed')) + ->setDescription( + pht('The build may be restarted if it has failed.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::RESTARTABLE_NEVER) + ->setIcon('fa-times red') + ->setName(pht('Never')) + ->setDescription( + pht('The build may not be restarted.')), + ); + + $run_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::RUNNABLE_IF_EDITABLE) + ->setIcon('fa-pencil green') + ->setName(pht('If Editable')) + ->setIsDefault(true) + ->setDescription( + pht('Only users who can edit the plan can run it manually.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::RUNNABLE_IF_VIEWABLE) + ->setIcon('fa-exclamation-triangle yellow') + ->setName(pht('If Viewable')) + ->setDescription( + pht( + 'Any user who can view the plan can run it manually.')), + ); + + $behaviors = array( + id(new self()) + ->setKey(self::BEHAVIOR_DRAFTS) + ->setName(pht('Hold Drafts')) + ->setEditInstructions( + pht( + 'When users create revisions in Differential, the default '. + 'behavior is to hold them in the "Draft" state until all builds '. + 'pass. Once builds pass, the revisions promote and are sent for '. + 'review, which notifies reviewers.'. + "\n\n". + 'The general intent of this workflow is to make sure reviewers '. + 'are only spending time on review once changes survive automated '. + 'tests. If a change does not pass tests, it usually is not '. + 'really ready for review.'. + "\n\n". + 'If you want to promote revisions out of "Draft" before builds '. + 'pass, or promote revisions even when builds fail, you can '. + 'change the promotion behavior. This may be useful if you have '. + 'very long-running builds, or some builds which are not very '. + 'important.'. + "\n\n". + 'Users may always use "Request Review" to promote a "Draft" '. + 'revision, even if builds have failed or are still in progress.')) + ->setOptions($draft_options), + id(new self()) + ->setKey(self::BEHAVIOR_LANDWARNING) + ->setName(pht('Warn When Landing')) + ->setEditInstructions( + pht( + 'When a user attempts to `arc land` a revision and that revision '. + 'has ongoing or failed builds, the default behavior of `arc` is '. + 'to warn them about those builds and give them a chance to '. + 'reconsider: they may want to wait for ongoing builds to '. + 'complete, or fix failed builds before landing the change.'. + "\n\n". + 'If you do not want to warn users about this build, you can '. + 'change the warning behavior. This may be useful if the build '. + 'takes a long time to run (so you do not expect users to wait '. + 'for it) or the outcome is not important.'. + "\n\n". + 'This warning is only advisory. Users may always elect to ignore '. + 'this warning and continue, even if builds have failed.'. + "\n\n". + 'This setting also affects the warning that is published to '. + 'revisions when commits land with ongoing or failed builds.')) + ->setOptions($land_options), + id(new self()) + ->setKey(self::BEHAVIOR_BUILDABLE) + ->setEditInstructions( + pht( + 'The overall state of a buildable (like a commit or revision) is '. + 'normally the aggregation of the individual states of all builds '. + 'that have run against it.'. + "\n\n". + 'Buildables are "building" until all builds pass (which changes '. + 'them to "pass"), or any build fails (which changes them to '. + '"fail").'. + "\n\n". + 'You can change this behavior if you do not want to wait for this '. + 'build, or do not care if it fails.')) + ->setName(pht('Affects Buildable')) + ->setOptions($aggregate_options), + id(new self()) + ->setKey(self::BEHAVIOR_RESTARTABLE) + ->setEditInstructions( + pht( + 'Usually, builds may be restarted. This may be useful if you '. + 'suspect a build has failed for environmental or circumstantial '. + 'reasons unrelated to the actual code, and want to give it '. + 'another chance at glory.'. + "\n\n". + 'If you want to prevent a build from being restarted, you can '. + 'change the behavior here. This may be useful to prevent '. + 'accidents where a build with a dangerous side effect (like '. + 'deployment) is restarted improperly.')) + ->setName(pht('Restartable')) + ->setOptions($restart_options), + id(new self()) + ->setKey(self::BEHAVIOR_RUNNABLE) + ->setEditInstructions( + pht( + 'To run a build manually, you normally must have permission to '. + 'edit the related build plan. If you would prefer that anyone who '. + 'can see the build plan be able to run and restart the build, you '. + 'can change the behavior here.'. + "\n\n". + 'Note that this controls access to all build management actions: '. + '"Run Plan Manually", "Restart", "Abort", "Pause", and "Resume".'. + "\n\n". + 'WARNING: This may be unsafe, particularly if the build has '. + 'side effects like deployment.'. + "\n\n". + 'If you weaken this policy, an attacker with control of an '. + 'account that has "Can View" permission but not "Can Edit" '. + 'permission can manually run this build against any old version '. + 'of the code, including versions with known security issues.'. + "\n\n". + 'If running the build has a side effect like deploying code, '. + 'they can force deployment of a vulnerable version and then '. + 'escalate into an attack against the deployed service.')) + ->setName(pht('Runnable')) + ->setOptions($run_options), + ); + + return mpull($behaviors, null, 'getKey'); + } + +} diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php new file mode 100644 index 0000000000..65b9662b9f --- /dev/null +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php @@ -0,0 +1,57 @@ +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 setDescription($description) { + $this->description = $description; + return $this; + } + + public function getDescription() { + return $this->description; + } + + public function setIsDefault($is_default) { + $this->isDefault = $is_default; + return $this; + } + + public function getIsDefault() { + return $this->isDefault; + } + + public function setIcon($icon) { + $this->icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + +} diff --git a/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php b/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php index 4cf6a83701..b8140d84f6 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php +++ b/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php @@ -128,49 +128,14 @@ final class HarbormasterBuildSearchEngine $viewer = $this->requireViewer(); - $buildables = mpull($builds, 'getBuildable'); - $object_phids = mpull($buildables, 'getBuildablePHID'); - $initiator_phids = mpull($builds, 'getInitiatorPHID'); - $phids = array_mergev(array($initiator_phids, $object_phids)); - $phids = array_unique(array_filter($phids)); + $list = id(new HarbormasterBuildView()) + ->setViewer($viewer) + ->setBuilds($builds) + ->newObjectList(); - $handles = $viewer->loadHandles($phids); - - $list = new PHUIObjectItemListView(); - foreach ($builds as $build) { - $id = $build->getID(); - $initiator = $handles[$build->getInitiatorPHID()]; - $buildable_object = $handles[$build->getBuildable()->getBuildablePHID()]; - - $item = id(new PHUIObjectItemView()) - ->setViewer($viewer) - ->setObject($build) - ->setObjectName(pht('Build %d', $build->getID())) - ->setHeader($build->getName()) - ->setHref($build->getURI()) - ->setEpoch($build->getDateCreated()) - ->addAttribute($buildable_object->getName()); - - if ($initiator) { - $item->addHandleIcon($initiator, $initiator->getName()); - } - - $status = $build->getBuildStatus(); - - $status_icon = HarbormasterBuildStatus::getBuildStatusIcon($status); - $status_color = HarbormasterBuildStatus::getBuildStatusColor($status); - $status_label = HarbormasterBuildStatus::getBuildStatusName($status); - - $item->setStatusIcon("{$status_icon} {$status_color}", $status_label); - - $list->addItem($item); - } - - $result = new PhabricatorApplicationSearchResultView(); - $result->setObjectList($list); - $result->setNoDataString(pht('No builds found.')); - - return $result; + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No builds found.')); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 602e388477..70c26827ec 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -183,6 +183,10 @@ final class HarbormasterBuild extends HarbormasterDAO return $this->getBuildStatusObject()->isPassed(); } + public function isFailed() { + return $this->getBuildStatusObject()->isFailed(); + } + public function getURI() { $id = $this->getID(); return "/harbormaster/build/{$id}/"; @@ -193,6 +197,10 @@ final class HarbormasterBuild extends HarbormasterDAO return HarbormasterBuildStatus::newBuildStatusObject($status_key); } + public function getObjectName() { + return pht('Build %d', $this->getID()); + } + /* -( Build Commands )----------------------------------------------------- */ @@ -207,11 +215,60 @@ final class HarbormasterBuild extends HarbormasterDAO } public function canRestartBuild() { - if ($this->isAutobuild()) { + try { + $this->assertCanRestartBuild(); + return true; + } catch (HarbormasterRestartException $ex) { return false; } + } - return !$this->isRestarting(); + public function assertCanRestartBuild() { + if ($this->isAutobuild()) { + throw new HarbormasterRestartException( + pht('Can Not Restart Autobuild'), + pht( + 'This build can not be restarted because it is an automatic '. + 'build.')); + } + + $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE; + $plan = $this->getBuildPlan(); + + $option = HarbormasterBuildPlanBehavior::getBehavior($restartable) + ->getPlanOption($plan); + $option_key = $option->getKey(); + + $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER; + $is_never = ($option_key === $never_restartable); + if ($is_never) { + throw new HarbormasterRestartException( + pht('Build Plan Prevents Restart'), + pht( + 'This build can not be restarted because the build plan is '. + 'configured to prevent the build from restarting.')); + } + + $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED; + $is_failed = ($option_key === $failed_restartable); + if ($is_failed) { + if (!$this->isFailed()) { + throw new HarbormasterRestartException( + pht('Only Restartable if Failed'), + pht( + 'This build can not be restarted because the build plan is '. + 'configured to prevent the build from restarting unless it '. + 'has failed, and it has not failed.')); + } + } + + if ($this->isRestarting()) { + throw new HarbormasterRestartException( + pht('Already Restarting'), + pht( + 'This build is already restarting. You can not reissue a restart '. + 'command to a restarting build.')); + } } public function canPauseBuild() { @@ -330,14 +387,17 @@ final class HarbormasterBuild extends HarbormasterDAO } public function assertCanIssueCommand(PhabricatorUser $viewer, $command) { - $need_edit = false; + $plan = $this->getBuildPlan(); + + $need_edit = true; switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: - break; case HarbormasterBuildCommand::COMMAND_PAUSE: case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_ABORT: - $need_edit = true; + if ($plan->canRunWithoutEditCapability()) { + $need_edit = false; + } break; default: throw new Exception( @@ -351,7 +411,7 @@ final class HarbormasterBuild extends HarbormasterDAO if ($need_edit) { PhabricatorPolicyFilter::requireCapability( $viewer, - $this->getBuildPlan(), + $plan, PhabricatorPolicyCapability::CAN_EDIT); } } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 2e379aab23..798201f490 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -10,13 +10,15 @@ final class HarbormasterBuildPlan extends HarbormasterDAO PhabricatorSubscribableInterface, PhabricatorNgramsInterface, PhabricatorConduitResultInterface, - PhabricatorProjectInterface { + PhabricatorProjectInterface, + PhabricatorPolicyCodexInterface { protected $name; protected $planStatus; protected $planAutoKey; protected $viewPolicy; protected $editPolicy; + protected $properties = array(); const STATUS_ACTIVE = 'active'; const STATUS_DISABLED = 'disabled'; @@ -45,6 +47,9 @@ final class HarbormasterBuildPlan extends HarbormasterDAO protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'planStatus' => 'text32', @@ -84,6 +89,25 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return ($this->getPlanStatus() == self::STATUS_DISABLED); } + public function getURI() { + return urisprintf( + '/harbormaster/plan/%s/', + $this->getID()); + } + + public function getObjectName() { + return pht('Plan %d', $this->getID()); + } + + public function getPlanProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setPlanProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + /* -( Autoplans )---------------------------------------------------------- */ @@ -110,7 +134,6 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return true; } - public function getName() { $autoplan = $this->getAutoplan(); if ($autoplan) { @@ -120,6 +143,38 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return parent::getName(); } + public function hasRunCapability(PhabricatorUser $viewer) { + try { + $this->assertHasRunCapability($viewer); + return true; + } catch (PhabricatorPolicyException $ex) { + return false; + } + } + + public function canRunWithoutEditCapability() { + $runnable = HarbormasterBuildPlanBehavior::BEHAVIOR_RUNNABLE; + $if_viewable = HarbormasterBuildPlanBehavior::RUNNABLE_IF_VIEWABLE; + + $option = HarbormasterBuildPlanBehavior::getBehavior($runnable) + ->getPlanOption($this); + + return ($option->getKey() === $if_viewable); + } + + public function assertHasRunCapability(PhabricatorUser $viewer) { + if ($this->canRunWithoutEditCapability()) { + $capability = PhabricatorPolicyCapability::CAN_VIEW; + } else { + $capability = PhabricatorPolicyCapability::CAN_EDIT; + } + + PhabricatorPolicyFilter::requireCapability( + $viewer, + $this, + $capability); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ @@ -210,15 +265,31 @@ final class HarbormasterBuildPlan extends HarbormasterDAO ->setKey('status') ->setType('map') ->setDescription(pht('The current status of this build plan.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('behaviors') + ->setType('map') + ->setDescription(pht('Behavior configuration for the build plan.')), ); } public function getFieldValuesForConduit() { + $behavior_map = array(); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + foreach ($behaviors as $behavior) { + $option = $behavior->getPlanOption($this); + + $behavior_map[$behavior->getKey()] = array( + 'value' => $option->getKey(), + ); + } + return array( 'name' => $this->getName(), 'status' => array( 'value' => $this->getPlanStatus(), ), + 'behaviors' => $behavior_map, ); } @@ -226,4 +297,12 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return array(); } + +/* -( PhabricatorPolicyCodexInterface )------------------------------------ */ + + + public function newPolicyCodex() { + return new HarbormasterBuildPlanPolicyCodex(); + } + } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php index 130471e21b..6cd286343a 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php @@ -1,10 +1,7 @@ getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return 'fa-plus'; - } - break; - } - - return parent::getIcon(); - } - - public function getColor() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return 'green'; - } - break; - } - - return parent::getIcon(); - } - - public function getTitle() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - $author_handle = $this->renderHandleLink($this->getAuthorPHID()); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return pht( - '%s created this build plan.', - $author_handle); - } else { - return pht( - '%s renamed this build plan from "%s" to "%s".', - $author_handle, - $old, - $new); - } - case self::TYPE_STATUS: - if ($new == HarbormasterBuildPlan::STATUS_DISABLED) { - return pht( - '%s disabled this build plan.', - $author_handle); - } else { - return pht( - '%s enabled this build plan.', - $author_handle); - } - } - - return parent::getTitle(); + public function getBaseTransactionClass() { + return 'HarbormasterBuildPlanTransactionType'; } } diff --git a/src/applications/harbormaster/view/HarbormasterBuildView.php b/src/applications/harbormaster/view/HarbormasterBuildView.php new file mode 100644 index 0000000000..54f5abe093 --- /dev/null +++ b/src/applications/harbormaster/view/HarbormasterBuildView.php @@ -0,0 +1,67 @@ +builds = $builds; + return $this; + } + + public function getBuilds() { + return $this->builds; + } + + public function render() { + return $this->newObjectList(); + } + + public function newObjectList() { + $viewer = $this->getViewer(); + $builds = $this->getBuilds(); + + $buildables = mpull($builds, 'getBuildable'); + $object_phids = mpull($buildables, 'getBuildablePHID'); + $initiator_phids = mpull($builds, 'getInitiatorPHID'); + $phids = array_mergev(array($initiator_phids, $object_phids)); + $phids = array_unique(array_filter($phids)); + + $handles = $viewer->loadHandles($phids); + + $list = new PHUIObjectItemListView(); + foreach ($builds as $build) { + $id = $build->getID(); + $initiator = $handles[$build->getInitiatorPHID()]; + $buildable_object = $handles[$build->getBuildable()->getBuildablePHID()]; + + $item = id(new PHUIObjectItemView()) + ->setViewer($viewer) + ->setObject($build) + ->setObjectName($build->getObjectName()) + ->setHeader($build->getName()) + ->setHref($build->getURI()) + ->setEpoch($build->getDateCreated()) + ->addAttribute($buildable_object->getName()); + + if ($initiator) { + $item->addByline($initiator->renderLink()); + } + + $status = $build->getBuildStatus(); + + $status_icon = HarbormasterBuildStatus::getBuildStatusIcon($status); + $status_color = HarbormasterBuildStatus::getBuildStatusColor($status); + $status_label = HarbormasterBuildStatus::getBuildStatusName($status); + + $item->setStatusIcon("{$status_icon} {$status_color}", $status_label); + + $list->addItem($item); + } + + return $list; + } + +} diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php new file mode 100644 index 0000000000..7a65eefdfa --- /dev/null +++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php @@ -0,0 +1,127 @@ +getBehavior(); + return $behavior->getPlanOption($object)->getKey(); + } + + public function applyInternalEffects($object, $value) { + $key = $this->getStorageKey(); + return $object->setPlanProperty($key, $value); + } + + public function getTitle() { + $old_value = $this->getOldValue(); + $new_value = $this->getNewValue(); + + $behavior = $this->getBehavior(); + if ($behavior) { + $behavior_name = $behavior->getName(); + + $options = $behavior->getOptions(); + if (isset($options[$old_value])) { + $old_value = $options[$old_value]->getName(); + } + + if (isset($options[$new_value])) { + $new_value = $options[$new_value]->getName(); + } + } else { + $behavior_name = $this->getBehaviorKey(); + } + + return pht( + '%s changed the %s behavior for this plan from %s to %s.', + $this->renderAuthor(), + $this->renderValue($behavior_name), + $this->renderValue($old_value), + $this->renderValue($new_value)); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + $behaviors = mpull($behaviors, null, 'getKey'); + + foreach ($xactions as $xaction) { + $key = $this->getBehaviorKeyForTransaction($xaction); + + if (!isset($behaviors[$key])) { + $errors[] = $this->newInvalidError( + pht( + 'No behavior with key "%s" exists. Valid keys are: %s.', + $key, + implode(', ', array_keys($behaviors))), + $xaction); + continue; + } + + $behavior = $behaviors[$key]; + $options = $behavior->getOptions(); + + $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey( + $key); + $old = $object->getPlanProperty($storage_key); + $new = $xaction->getNewValue(); + + if ($old === $new) { + continue; + } + + if (!isset($options[$new])) { + $errors[] = $this->newInvalidError( + pht( + 'Value "%s" is not a valid option for behavior "%s". Valid '. + 'options are: %s.', + $new, + $key, + implode(', ', array_keys($options))), + $xaction); + continue; + } + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'behavior'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'key' => $this->getBehaviorKeyForTransaction($xaction), + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + + private function getBehaviorKeyForTransaction( + PhabricatorApplicationTransaction $xaction) { + $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey(); + return $xaction->getMetadataValue($metadata_key); + } + + private function getBehaviorKey() { + $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey(); + return $this->getMetadataValue($metadata_key); + } + + private function getBehavior() { + $behavior_key = $this->getBehaviorKey(); + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + return idx($behaviors, $behavior_key); + } + + private function getStorageKey() { + return HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey( + $this->getBehaviorKey()); + } + +} diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php new file mode 100644 index 0000000000..30fdbe72ca --- /dev/null +++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php @@ -0,0 +1,46 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this build plan from "%s" to "%s".', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('You must choose a name for your build plan.')); + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'name'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + +} diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php new file mode 100644 index 0000000000..e1c72b4183 --- /dev/null +++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php @@ -0,0 +1,67 @@ +getPlanStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setPlanStatus($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + if ($new === HarbormasterBuildPlan::STATUS_DISABLED) { + return pht( + '%s disabled this build plan.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this build plan.', + $this->renderAuthor()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $options = array( + HarbormasterBuildPlan::STATUS_DISABLED, + HarbormasterBuildPlan::STATUS_ACTIVE, + ); + $options = array_fuse($options); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if (!isset($options[$new])) { + $errors[] = $this->newInvalidError( + pht( + 'Status "%s" is not a valid build plan status. Valid '. + 'statuses are: %s.', + $new, + implode(', ', $options))); + continue; + } + + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'status'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + +} diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php new file mode 100644 index 0000000000..5545d1de38 --- /dev/null +++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php @@ -0,0 +1,4 @@ +getTarget(); + } + } diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index a266e21f39..69f538afcc 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -186,15 +186,16 @@ abstract class HeraldAdapter extends Phobject { return $this->appliedTransactions; } - public function queueTransaction($transaction) { + final public function queueTransaction( + PhabricatorApplicationTransaction $transaction) { $this->queuedTransactions[] = $transaction; } - public function getQueuedTransactions() { + final public function getQueuedTransactions() { return $this->queuedTransactions; } - public function newTransaction() { + final public function newTransaction() { $object = $this->newObject(); if (!($object instanceof PhabricatorApplicationTransactionInterface)) { @@ -205,7 +206,19 @@ abstract class HeraldAdapter extends Phobject { 'PhabricatorApplicationTransactionInterface')); } - return $object->getApplicationTransactionTemplate(); + $xaction = $object->getApplicationTransactionTemplate(); + + if (!($xaction instanceof PhabricatorApplicationTransaction)) { + throw new Exception( + pht( + 'Expected object (of class "%s") to return a transaction template '. + '(of class "%s"), but it returned something else ("%s").', + get_class($object), + 'PhabricatorApplicationTransaction', + phutil_describe_type($xaction))); + } + + return $xaction; } diff --git a/src/applications/herald/controller/HeraldDisableController.php b/src/applications/herald/controller/HeraldDisableController.php index def87049f7..765237930c 100644 --- a/src/applications/herald/controller/HeraldDisableController.php +++ b/src/applications/herald/controller/HeraldDisableController.php @@ -31,7 +31,7 @@ final class HeraldDisableController extends HeraldController { if ($request->isFormPost()) { $xaction = id(new HeraldRuleTransaction()) - ->setTransactionType(HeraldRuleTransaction::TYPE_DISABLE) + ->setTransactionType(HeraldRuleDisableTransaction::TRANSACTIONTYPE) ->setNewValue($is_disable); id(new HeraldRuleEditor()) diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index d400f8ae90..d05ed2d525 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -359,11 +359,21 @@ final class HeraldRuleController extends HeraldController { $repetition_policy); $xactions = array(); + + // Until this moves to EditEngine, manually add a "CREATE" transaction + // if we're creating a new rule. This improves rendering of the initial + // group of transactions. + $is_new = (bool)(!$rule->getID()); + if ($is_new) { + $xactions[] = id(new HeraldRuleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); + } + $xactions[] = id(new HeraldRuleTransaction()) - ->setTransactionType(HeraldRuleTransaction::TYPE_EDIT) + ->setTransactionType(HeraldRuleEditTransaction::TRANSACTIONTYPE) ->setNewValue($new_state); $xactions[] = id(new HeraldRuleTransaction()) - ->setTransactionType(HeraldRuleTransaction::TYPE_NAME) + ->setTransactionType(HeraldRuleNameTransaction::TRANSACTIONTYPE) ->setNewValue($new_name); try { diff --git a/src/applications/herald/controller/HeraldWebhookViewController.php b/src/applications/herald/controller/HeraldWebhookViewController.php index d8e5eb3c54..5f6be9816c 100644 --- a/src/applications/herald/controller/HeraldWebhookViewController.php +++ b/src/applications/herald/controller/HeraldWebhookViewController.php @@ -73,12 +73,15 @@ final class HeraldWebhookViewController ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($requests_table); + $rules_view = $this->newRulesView($hook); + $hook_view = id(new PHUITwoColumnView()) ->setHeader($header) ->setMainColumn( array( $warnings, $properties_view, + $rules_view, $requests_view, $timeline, )) @@ -194,4 +197,42 @@ final class HeraldWebhookViewController ->appendChild($properties); } + private function newRulesView(HeraldWebhook $hook) { + $viewer = $this->getViewer(); + + $rules = id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withDisabled(false) + ->withAffectedObjectPHIDs(array($hook->getPHID())) + ->needValidateAuthors(true) + ->setLimit(10) + ->execute(); + + $list = id(new HeraldRuleListView()) + ->setViewer($viewer) + ->setRules($rules) + ->newObjectList(); + + $list->setNoDataString(pht('No active Herald rules call this webhook.')); + + $more_href = new PhutilURI( + '/herald/', + array('affectedPHID' => $hook->getPHID())); + + $more_link = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list-ul') + ->setText(pht('View All Rules')) + ->setHref($more_href); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Called By Herald Rules')) + ->addActionLink($more_link); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + } + } diff --git a/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php b/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php new file mode 100644 index 0000000000..35a30773ac --- /dev/null +++ b/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php @@ -0,0 +1,8 @@ +getTransactionType()) { - case HeraldRuleTransaction::TYPE_DISABLE: - return (int)$object->getIsDisabled(); - case HeraldRuleTransaction::TYPE_EDIT: - return id(new HeraldRuleSerializer()) - ->serializeRule($object); - case HeraldRuleTransaction::TYPE_NAME: - return $object->getName(); - } - - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case HeraldRuleTransaction::TYPE_DISABLE: - return (int)$xaction->getNewValue(); - case HeraldRuleTransaction::TYPE_EDIT: - case HeraldRuleTransaction::TYPE_NAME: - return $xaction->getNewValue(); - } - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case HeraldRuleTransaction::TYPE_DISABLE: - return $object->setIsDisabled($xaction->getNewValue()); - case HeraldRuleTransaction::TYPE_NAME: - return $object->setName($xaction->getNewValue()); - case HeraldRuleTransaction::TYPE_EDIT: - $new_state = id(new HeraldRuleSerializer()) - ->deserializeRuleComponents($xaction->getNewValue()); - $object->setMustMatchAll((int)$new_state['match_all']); - $object->attachConditions($new_state['conditions']); - $object->attachActions($new_state['actions']); - - $new_repetition = $new_state['repetition_policy']; - $object->setRepetitionPolicyStringConstant($new_repetition); - - return $object; - } - - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HeraldRuleTransaction::TYPE_EDIT: - $object->saveConditions($object->getConditions()); - $object->saveActions($object->getActions()); - break; - } - return; - } - protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { @@ -137,7 +61,6 @@ final class HeraldRuleEditor return pht('[Herald]'); } - protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { @@ -151,4 +74,8 @@ final class HeraldRuleEditor return $body; } + protected function supportsSearch() { + return true; + } + } diff --git a/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php b/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php new file mode 100644 index 0000000000..7b7b2fb529 --- /dev/null +++ b/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php @@ -0,0 +1,92 @@ +getPHID(), + $edge_type); + $old_edges = array_fuse($old_edges); + + $new_edges = $this->getPHIDsAffectedByActions($object); + $new_edges = array_fuse($new_edges); + + $add_edges = array_diff_key($new_edges, $old_edges); + $rem_edges = array_diff_key($old_edges, $new_edges); + + if (!$add_edges && !$rem_edges) { + return; + } + + $editor = new PhabricatorEdgeEditor(); + + foreach ($add_edges as $phid) { + $editor->addEdge($object->getPHID(), $edge_type, $phid); + } + + foreach ($rem_edges as $phid) { + $editor->removeEdge($object->getPHID(), $edge_type, $phid); + } + + $editor->save(); + } + + public function getIndexVersion($object) { + $phids = $this->getPHIDsAffectedByActions($object); + sort($phids); + $phids = implode(':', $phids); + return PhabricatorHash::digestForIndex($phids); + } + + private function getPHIDsAffectedByActions(HeraldRule $rule) { + $viewer = $this->getViewer(); + + $rule = id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withIDs(array($rule->getID())) + ->needConditionsAndActions(true) + ->executeOne(); + if (!$rule) { + return array(); + } + + $phids = array(); + + $actions = HeraldAction::getAllActions(); + foreach ($rule->getActions() as $action_record) { + $action = idx($actions, $action_record->getAction()); + + if (!$action) { + continue; + } + + foreach ($action->getPHIDsAffectedByAction($action_record) as $phid) { + $phids[] = $phid; + } + } + + $phids = array_fuse($phids); + return array_keys($phids); + } + +} diff --git a/src/applications/herald/query/HeraldRuleQuery.php b/src/applications/herald/query/HeraldRuleQuery.php index e6dba43c7a..e346a998d4 100644 --- a/src/applications/herald/query/HeraldRuleQuery.php +++ b/src/applications/herald/query/HeraldRuleQuery.php @@ -11,6 +11,7 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $active; private $datasourceQuery; private $triggerObjectPHIDs; + private $affectedObjectPHIDs; private $needConditionsAndActions; private $needAppliedToPHIDs; @@ -61,6 +62,11 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } + public function withAffectedObjectPHIDs(array $phids) { + $this->affectedObjectPHIDs = $phids; + return $this; + } + public function needConditionsAndActions($need) { $this->needConditionsAndActions = $need; return $this; @@ -261,9 +267,31 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery { $this->triggerObjectPHIDs); } + if ($this->affectedObjectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'edge_affects.dst IN (%Ls)', + $this->affectedObjectPHIDs); + } + return $where; } + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->affectedObjectPHIDs !== null) { + $joins[] = qsprintf( + $conn, + 'JOIN %T edge_affects ON rule.phid = edge_affects.src + AND edge_affects.type = %d', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + HeraldRuleActionAffectsObjectEdgeType::EDGECONST); + } + + return $joins; + } + private function validateRuleAuthors(array $rules) { // "Global" and "Object" rules always have valid authors. foreach ($rules as $key => $rule) { diff --git a/src/applications/herald/query/HeraldRuleSearchEngine.php b/src/applications/herald/query/HeraldRuleSearchEngine.php index 47a6832731..95e3079717 100644 --- a/src/applications/herald/query/HeraldRuleSearchEngine.php +++ b/src/applications/herald/query/HeraldRuleSearchEngine.php @@ -55,6 +55,10 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine { pht('(Show All)'), pht('Show Only Disabled Rules'), pht('Show Only Enabled Rules')), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Affected Objects')) + ->setKey('affectedPHIDs') + ->setAliases(array('affectedPHID')), ); } @@ -81,6 +85,10 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine { $query->withActive($map['active']); } + if ($map['affectedPHIDs']) { + $query->withAffectedObjectPHIDs($map['affectedPHIDs']); + } + return $query; } @@ -127,54 +135,18 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine { PhabricatorSavedQuery $query, array $handles) { assert_instances_of($rules, 'HeraldRule'); - $viewer = $this->requireViewer(); - $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID')); - $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); - - $list = id(new PHUIObjectItemListView()) - ->setUser($viewer); - foreach ($rules as $rule) { - $monogram = $rule->getMonogram(); - - $item = id(new PHUIObjectItemView()) - ->setObjectName($monogram) - ->setHeader($rule->getName()) - ->setHref("/{$monogram}"); - - if ($rule->isPersonalRule()) { - $item->addIcon('fa-user', pht('Personal Rule')); - $item->addByline( - pht( - 'Authored by %s', - $handles[$rule->getAuthorPHID()]->renderLink())); - } else if ($rule->isObjectRule()) { - $item->addIcon('fa-briefcase', pht('Object Rule')); - } else { - $item->addIcon('fa-globe', pht('Global Rule')); - } - - if ($rule->getIsDisabled()) { - $item->setDisabled(true); - $item->addIcon('fa-lock grey', pht('Disabled')); - } else if (!$rule->hasValidAuthor()) { - $item->setDisabled(true); - $item->addIcon('fa-user grey', pht('Author Not Active')); - } - - $content_type_name = idx($content_type_map, $rule->getContentType()); - $item->addAttribute(pht('Affects: %s', $content_type_name)); - - $list->addItem($item); - } + $list = id(new HeraldRuleListView()) + ->setViewer($viewer) + ->setRules($rules) + ->newObjectList(); $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No rules found.')); return $result; - } protected function getNewUserBody() { diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index 1b005898cb..a9c131e717 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -6,6 +6,7 @@ final class HeraldRule extends HeraldDAO PhabricatorFlaggableInterface, PhabricatorPolicyInterface, PhabricatorDestructibleInterface, + PhabricatorIndexableInterface, PhabricatorSubscribableInterface { const TABLE_RULE_APPLIED = 'herald_ruleapplied'; diff --git a/src/applications/herald/storage/HeraldRuleTransaction.php b/src/applications/herald/storage/HeraldRuleTransaction.php index b1bd563749..7fa7667ec7 100644 --- a/src/applications/herald/storage/HeraldRuleTransaction.php +++ b/src/applications/herald/storage/HeraldRuleTransaction.php @@ -1,11 +1,9 @@ getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DISABLE: - if ($new) { - return 'red'; - } else { - return 'green'; - } - } - - return parent::getColor(); - } - - public function getActionName() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DISABLE: - if ($new) { - return pht('Disabled'); - } else { - return pht('Enabled'); - } - case self::TYPE_NAME: - return pht('Renamed'); - } - - return parent::getActionName(); - } - - public function getIcon() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DISABLE: - if ($new) { - return 'fa-ban'; - } else { - return 'fa-check'; - } - } - - return parent::getIcon(); - } - - - public function getTitle() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DISABLE: - if ($new) { - return pht( - '%s disabled this rule.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s enabled this rule.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_NAME: - if ($old == null) { - return pht( - '%s created this rule.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s renamed this rule from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - case self::TYPE_EDIT: - return pht( - '%s edited this rule.', - $this->renderHandleLink($author_phid)); - } - - return parent::getTitle(); - } - - public function hasChangeDetails() { - switch ($this->getTransactionType()) { - case self::TYPE_EDIT: - return true; - } - return parent::hasChangeDetails(); - } - - public function renderChangeDetails(PhabricatorUser $viewer) { - $json = new PhutilJSON(); - switch ($this->getTransactionType()) { - case self::TYPE_EDIT: - return $this->renderTextCorpusChangeDetails( - $viewer, - $json->encodeFormatted($this->getOldValue()), - $json->encodeFormatted($this->getNewValue())); - } - - return $this->renderTextCorpusChangeDetails( - $viewer, - $this->getOldValue(), - $this->getNewValue()); + public function getBaseTransactionClass() { + return 'HeraldRuleTransactionType'; } } diff --git a/src/applications/herald/storage/HeraldRuleTransactionComment.php b/src/applications/herald/storage/HeraldRuleTransactionComment.php deleted file mode 100644 index 56022ef863..0000000000 --- a/src/applications/herald/storage/HeraldRuleTransactionComment.php +++ /dev/null @@ -1,10 +0,0 @@ -rules = $rules; + return $this; + } + + public function render() { + return $this->newObjectList(); + } + + public function newObjectList() { + $viewer = $this->getViewer(); + $rules = $this->rules; + + $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID')); + + $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); + + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer); + foreach ($rules as $rule) { + $monogram = $rule->getMonogram(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName($monogram) + ->setHeader($rule->getName()) + ->setHref($rule->getURI()); + + if ($rule->isPersonalRule()) { + $item->addIcon('fa-user', pht('Personal Rule')); + $item->addByline( + pht( + 'Authored by %s', + $handles[$rule->getAuthorPHID()]->renderLink())); + } else if ($rule->isObjectRule()) { + $item->addIcon('fa-briefcase', pht('Object Rule')); + } else { + $item->addIcon('fa-globe', pht('Global Rule')); + } + + if ($rule->getIsDisabled()) { + $item->setDisabled(true); + $item->addIcon('fa-lock grey', pht('Disabled')); + } else if (!$rule->hasValidAuthor()) { + $item->setDisabled(true); + $item->addIcon('fa-user grey', pht('Author Not Active')); + } + + $content_type_name = idx($content_type_map, $rule->getContentType()); + $item->addAttribute(pht('Affects: %s', $content_type_name)); + + $list->addItem($item); + } + + return $list; + } + +} diff --git a/src/applications/herald/xaction/HeraldRuleDisableTransaction.php b/src/applications/herald/xaction/HeraldRuleDisableTransaction.php new file mode 100644 index 0000000000..5debab653b --- /dev/null +++ b/src/applications/herald/xaction/HeraldRuleDisableTransaction.php @@ -0,0 +1,32 @@ +getIsDisabled(); + } + + public function generateNewValue($object, $value) { + return (bool)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsDisabled((int)$value); + } + + public function getTitle() { + if ($this->getNewValue()) { + return pht( + '%s disabled this rule.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this rule.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/herald/xaction/HeraldRuleEditTransaction.php b/src/applications/herald/xaction/HeraldRuleEditTransaction.php new file mode 100644 index 0000000000..c4b03983fb --- /dev/null +++ b/src/applications/herald/xaction/HeraldRuleEditTransaction.php @@ -0,0 +1,56 @@ +serializeRule($object); + } + + public function applyInternalEffects($object, $value) { + $new_state = id(new HeraldRuleSerializer()) + ->deserializeRuleComponents($value); + + $object->setMustMatchAll((int)$new_state['match_all']); + $object->attachConditions($new_state['conditions']); + $object->attachActions($new_state['actions']); + + $new_repetition = $new_state['repetition_policy']; + $object->setRepetitionPolicyStringConstant($new_repetition); + } + + public function applyExternalEffects($object, $value) { + $object->saveConditions($object->getConditions()); + $object->saveActions($object->getActions()); + } + + public function getTitle() { + return pht( + '%s edited this rule.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $json = new PhutilJSON(); + $old_json = $json->encodeFormatted($old); + $new_json = $json->encodeFormatted($new); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($old_json) + ->setNewText($new_json); + } + +} diff --git a/src/applications/herald/xaction/HeraldRuleNameTransaction.php b/src/applications/herald/xaction/HeraldRuleNameTransaction.php new file mode 100644 index 0000000000..39ce289d34 --- /dev/null +++ b/src/applications/herald/xaction/HeraldRuleNameTransaction.php @@ -0,0 +1,48 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this rule from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Rules must have a name.')); + } + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Rule names can be no longer than %s characters.', + new PhutilNumber($max_length))); + } + } + + return $errors; + } + +} diff --git a/src/applications/herald/xaction/HeraldRuleTransactionType.php b/src/applications/herald/xaction/HeraldRuleTransactionType.php new file mode 100644 index 0000000000..81c6846b1f --- /dev/null +++ b/src/applications/herald/xaction/HeraldRuleTransactionType.php @@ -0,0 +1,4 @@ +newTransaction($object) + ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) + ->setNewValue($user->getPHID()), + ); + } + +} diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 0193830b39..400bace650 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -21,7 +21,8 @@ final class ManiphestTask extends ManiphestDAO PhabricatorEditEngineSubtypeInterface, PhabricatorEditEngineLockableInterface, PhabricatorEditEngineMFAInterface, - PhabricatorPolicyCodexInterface { + PhabricatorPolicyCodexInterface, + PhabricatorUnlockableInterface { const MARKUP_FIELD_DESCRIPTION = 'markup:desc'; @@ -649,4 +650,12 @@ final class ManiphestTask extends ManiphestDAO return new ManiphestTaskPolicyCodex(); } + +/* -( PhabricatorUnlockableInterface )------------------------------------- */ + + + public function newUnlockEngine() { + return new ManiphestTaskUnlockEngine(); + } + } diff --git a/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php index 8833e62b79..cb6c80604d 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php @@ -123,4 +123,14 @@ final class ManiphestTaskUnblockTransaction return parent::shouldHideForFeed(); } + public function getRequiredCapabilities( + $object, + PhabricatorApplicationTransaction $xaction) { + + // When you close a task, we want to apply this transaction to its parents + // even if you can not edit (or even see) those parents, so don't require + // any capabilities. See PHI1059. + + return null; + } } diff --git a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php index 33f7e209c2..64a32b7186 100644 --- a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php +++ b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php @@ -8,40 +8,72 @@ final class PhabricatorPolicyManagementUnlockWorkflow ->setName('unlock') ->setSynopsis( pht( - 'Unlock an object by setting its policies to allow anyone to view '. - 'and edit it.')) - ->setExamples('**unlock** D123') + 'Unlock an object which has policies that prevent it from being '. + 'viewed or edited.')) + ->setExamples('**unlock** --view __user__ __object__') ->setArguments( array( array( - 'name' => 'objects', - 'wildcard' => true, + 'name' => 'view', + 'param' => 'username', + 'help' => pht( + 'Change the view policy of an object so that the specified '. + 'user may view it.'), + ), + array( + 'name' => 'edit', + 'param' => 'username', + 'help' => pht( + 'Change the edit policy of an object so that the specified '. + 'user may edit it.'), + ), + array( + 'name' => 'owner', + 'param' => 'username', + 'help' => pht( + 'Change the owner of an object to the specified user.'), + ), + array( + 'name' => 'objects', + 'wildcard' => true, ), )); } public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); $viewer = $this->getViewer(); - $obj_names = $args->getArg('objects'); - if (!$obj_names) { + $object_names = $args->getArg('objects'); + if (!$object_names) { throw new PhutilArgumentUsageException( pht('Specify the name of an object to unlock.')); - } else if (count($obj_names) > 1) { + } else if (count($object_names) > 1) { throw new PhutilArgumentUsageException( pht('Specify the name of exactly one object to unlock.')); } + $object_name = head($object_names); + $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) - ->withNames($obj_names) + ->withNames(array($object_name)) ->executeOne(); - if (!$object) { - $name = head($obj_names); throw new PhutilArgumentUsageException( - pht("No such object '%s'!", $name)); + pht( + 'Unable to find any object with the specified name ("%s").', + $object_name)); + } + + $view_user = $this->loadUser($args->getArg('view')); + $edit_user = $this->loadUser($args->getArg('edit')); + $owner_user = $this->loadUser($args->getArg('owner')); + + if (!$view_user && !$edit_user && !$owner_user) { + throw new PhutilArgumentUsageException( + pht( + 'Choose which capabilities to unlock with "--view", "--edit", '. + 'or "--owner".')); } $handle = id(new PhabricatorHandleQuery()) @@ -49,84 +81,73 @@ final class PhabricatorPolicyManagementUnlockWorkflow ->withPHIDs(array($object->getPHID())) ->executeOne(); - if ($object instanceof PhabricatorApplication) { - $application = $object; + echo tsprintf( + "** %s ** %s\n", + pht('UNLOCKING'), + pht('Unlocking: %s', $handle->getFullName())); - $console->writeOut( - "%s\n", - pht('Unlocking Application: %s', $handle->getFullName())); + $engine = PhabricatorUnlockEngine::newUnlockEngineForObject($object); - // For applications, we can't unlock them in a normal way and don't want - // to unlock every capability, just view and edit. - $capabilities = array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - ); + $xactions = array(); + if ($view_user) { + $xactions[] = $engine->newUnlockViewTransactions($object, $view_user); + } + if ($edit_user) { + $xactions[] = $engine->newUnlockEditTransactions($object, $edit_user); + } + if ($owner_user) { + $xactions[] = $engine->newUnlockOwnerTransactions($object, $owner_user); + } + $xactions = array_mergev($xactions); - $key = 'phabricator.application-settings'; - $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); - $value = $config_entry->getValue(); + $policy_application = new PhabricatorPolicyApplication(); + $content_source = $this->newContentSource(); - foreach ($capabilities as $capability) { - if ($application->isCapabilityEditable($capability)) { - unset($value[$application->getPHID()]['policy'][$capability]); - } - } + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($policy_application->getPHID()) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSource($content_source); - $config_entry->setValue($value); - $config_entry->save(); + $editor->applyTransactions($object, $xactions); - $console->writeOut("%s\n", pht('Saved application.')); + echo tsprintf( + "** %s ** %s\n", + pht('UNLOCKED'), + pht('Modified object policies.')); - return 0; + $uri = $handle->getURI(); + if (strlen($uri)) { + echo tsprintf( + "\n **%s**: __%s__\n\n", + pht('Object URI'), + PhabricatorEnv::getURI($uri)); } - $console->writeOut("%s\n", pht('Unlocking: %s', $handle->getFullName())); + return 0; + } - $updated = false; - foreach ($object->getCapabilities() as $capability) { - switch ($capability) { - case PhabricatorPolicyCapability::CAN_VIEW: - try { - $object->setViewPolicy(PhabricatorPolicies::POLICY_USER); - $console->writeOut("%s\n", pht('Unlocked view policy.')); - $updated = true; - } catch (Exception $ex) { - $console->writeOut("%s\n", pht('View policy is not mutable.')); - } - break; - case PhabricatorPolicyCapability::CAN_EDIT: - try { - $object->setEditPolicy(PhabricatorPolicies::POLICY_USER); - $console->writeOut("%s\n", pht('Unlocked edit policy.')); - $updated = true; - } catch (Exception $ex) { - $console->writeOut("%s\n", pht('Edit policy is not mutable.')); - } - break; - case PhabricatorPolicyCapability::CAN_JOIN: - try { - $object->setJoinPolicy(PhabricatorPolicies::POLICY_USER); - $console->writeOut("%s\n", pht('Unlocked join policy.')); - $updated = true; - } catch (Exception $ex) { - $console->writeOut("%s\n", pht('Join policy is not mutable.')); - } - break; - } + private function loadUser($username) { + $viewer = $this->getViewer(); + + if ($username === null) { + return null; } - if ($updated) { - $object->save(); - $console->writeOut("%s\n", pht('Saved object.')); - } else { - $console->writeOut( - "%s\n", + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withUsernames(array($username)) + ->executeOne(); + + if (!$user) { + throw new PhutilArgumentUsageException( pht( - 'Object has no mutable policies. Try unlocking parent/container '. - 'object instead. For example, to gain access to a commit, unlock '. - 'the repository it belongs to.')); + 'No user with username "%s" exists.', + $username)); } + + return $user; } } diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php index ef038f045f..c64b1a296b 100644 --- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -124,29 +124,6 @@ final class PhabricatorRepositoryIdentityQuery return $where; } - protected function didFilterPage(array $identities) { - $user_ids = array_filter( - mpull($identities, 'getCurrentEffectiveUserPHID', 'getID')); - if (!$user_ids) { - return $identities; - } - - $users = id(new PhabricatorPeopleQuery()) - ->withPHIDs($user_ids) - ->setViewer($this->getViewer()) - ->execute(); - $users = mpull($users, null, 'getPHID'); - - foreach ($identities as $identity) { - if ($identity->hasEffectiveUser()) { - $user = idx($users, $identity->getCurrentEffectiveUserPHID()); - $identity->attachEffectiveUser($user); - } - } - - return $identities; - } - public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index 76c6aed9e0..e3833bd10e 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -14,17 +14,6 @@ final class PhabricatorRepositoryIdentity protected $manuallySetUserPHID; protected $currentEffectiveUserPHID; - private $effectiveUser = self::ATTACHABLE; - - public function attachEffectiveUser(PhabricatorUser $user) { - $this->effectiveUser = $user; - return $this; - } - - public function getEffectiveUser() { - return $this->assertAttached($this->effectiveUser); - } - protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php index 235d74d6f3..510ad91864 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php +++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php @@ -144,7 +144,7 @@ EOTEXT ->setHeaderText(pht('Builtin and Saved Queries')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)) + ->appendChild($this->newRemarkupDocumentationView($info)) ->appendChild($table); } @@ -223,7 +223,7 @@ EOTEXT ); if ($constants) { - $constant_lists[] = $this->buildRemarkup( + $constant_lists[] = $this->newRemarkupDocumentationView( pht( 'Constants supported by the `%s` constraint:', 'statuses')); @@ -283,7 +283,7 @@ EOTEXT ->setHeaderText(pht('Custom Query Constraints')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)) + ->appendChild($this->newRemarkupDocumentationView($info)) ->appendChild($table) ->appendChild($constant_lists); } @@ -391,9 +391,9 @@ EOTEXT ->setHeaderText(pht('Result Ordering')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($orders_info)) + ->appendChild($this->newRemarkupDocumentationView($orders_info)) ->appendChild($orders_table) - ->appendChild($this->buildRemarkup($columns_info)) + ->appendChild($this->newRemarkupDocumentationView($columns_info)) ->appendChild($columns_table); } @@ -472,7 +472,7 @@ EOTEXT ->setHeaderText(pht('Object Fields')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)) + ->appendChild($this->newRemarkupDocumentationView($info)) ->appendChild($table); } @@ -562,7 +562,7 @@ EOTEXT ->setHeaderText(pht('Attachments')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)) + ->appendChild($this->newRemarkupDocumentationView($info)) ->appendChild($table); } @@ -633,21 +633,7 @@ EOTEXT ->setHeaderText(pht('Paging and Limits')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)); + ->appendChild($this->newRemarkupDocumentationView($info)); } - private function buildRemarkup($remarkup) { - $viewer = $this->getViewer(); - - $view = new PHUIRemarkupView($viewer, $remarkup); - - $view->setRemarkupOptions( - array( - PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false, - )); - - return id(new PHUIBoxView()) - ->appendChild($view) - ->addPadding(PHUI::PADDING_LARGE); - } } diff --git a/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php b/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php new file mode 100644 index 0000000000..624191ad21 --- /dev/null +++ b/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php @@ -0,0 +1,4 @@ +newUnlockEngine(); + } else { + $engine = new PhabricatorDefaultUnlockEngine(); + } + + return $engine; + } + + public function newUnlockViewTransactions($object, $user) { + $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; + + if (!$this->canApplyTransactionType($object, $type_view)) { + throw new Exception( + pht( + 'Object view policy can not be unlocked because this object '. + 'does not have a mutable view policy.')); + } + + return array( + $this->newTransaction($object) + ->setTransactionType($type_view) + ->setNewValue($user->getPHID()), + ); + } + + public function newUnlockEditTransactions($object, $user) { + $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; + + if (!$this->canApplyTransactionType($object, $type_edit)) { + throw new Exception( + pht( + 'Object edit policy can not be unlocked because this object '. + 'does not have a mutable edit policy.')); + } + + return array( + $this->newTransaction($object) + ->setTransactionType($type_edit) + ->setNewValue($user->getPHID()), + ); + } + + public function newUnlockOwnerTransactions($object, $user) { + throw new Exception( + pht( + 'Object owner can not be unlocked: the unlocking engine ("%s") for '. + 'this object does not implement an owner unlocking mechanism.', + get_class($this))); + } + + final protected function canApplyTransactionType($object, $type) { + $xaction_types = $object->getApplicationTransactionEditor() + ->getTransactionTypesForObject($object); + + $xaction_types = array_fuse($xaction_types); + + return isset($xaction_types[$type]); + } + + final protected function newTransaction($object) { + return $object->getApplicationTransactionTemplate(); + } + + +} diff --git a/src/applications/system/interface/PhabricatorUnlockableInterface.php b/src/applications/system/interface/PhabricatorUnlockableInterface.php new file mode 100644 index 0000000000..1a95215e8c --- /dev/null +++ b/src/applications/system/interface/PhabricatorUnlockableInterface.php @@ -0,0 +1,18 @@ +>>UnlockEngine(); + } + +*/ diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 0edc0b3f5a..4ab5de519e 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -8,15 +8,58 @@ final class TransactionSearchConduitAPIMethod } public function getMethodDescription() { - return pht('Read transactions for an object.'); + return pht('Read transactions and comments for an object.'); } - public function getMethodStatus() { - return self::METHOD_STATUS_UNSTABLE; - } + public function getMethodDocumentation() { + $markup = pht(<<.// Find specific transactions by PHID. This + is most likely to be useful if you're responding to a webhook notification + and want to inspect only the related events. + - `authorPHIDs` //Optional list.// Find transactions with particular + authors. + +Transaction Format +================== + +Each transaction has custom data describing what the transaction did. The +format varies from transaction to transaction. The easiest way to figure out +exactly what a particular transaction looks like is to make the associated kind +of edit to a test object, then query that object. + +Not all transactions have data: by default, transactions have a `null` "type" +and no additional data. This API does not expose raw transaction data because +some of it is internal, oddly named, misspelled, confusing, not useful, or +could create security or policy problems to expose directly. + +New transactions are exposed (with correctly spelled, comprehensible types and +useful, reasonable fields) as we become aware of use cases for them. + +EOREMARKUP + ); + + $markup = $this->newRemarkupDocumentationView($markup); + + return id(new PHUIObjectBoxView()) + ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeaderText(pht('Method Details')) + ->appendChild($markup); } protected function defineParamTypes() { @@ -73,24 +116,8 @@ final class TransactionSearchConduitAPIMethod ->setViewer($viewer); $constraints = $request->getValue('constraints', array()); - PhutilTypeSpec::checkMap( - $constraints, - array( - 'phids' => 'optional list', - )); - $with_phids = idx($constraints, 'phids'); - - if ($with_phids === array()) { - throw new Exception( - pht( - 'Constraint "phids" to "transaction.search" requires nonempty list, '. - 'empty list provided.')); - } - - if ($with_phids) { - $xaction_query->withPHIDs($with_phids); - } + $xaction_query = $this->applyConstraints($constraints, $xaction_query); $xactions = $xaction_query->executeWithCursorPager($pager); @@ -218,6 +245,14 @@ final class TransactionSearchConduitAPIMethod case PhabricatorTransactions::TYPE_CREATE: $type = 'create'; break; + case PhabricatorTransactions::TYPE_EDGE: + switch ($xaction->getMetadataValue('edge:type')) { + case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST: + $type = 'projects'; + $fields = $this->newEdgeTransactionFields($xaction); + break; + } + break; } } @@ -240,4 +275,69 @@ final class TransactionSearchConduitAPIMethod return $this->addPagerResults($results, $pager); } + + private function applyConstraints( + array $constraints, + PhabricatorApplicationTransactionQuery $query) { + + PhutilTypeSpec::checkMap( + $constraints, + array( + 'phids' => 'optional list', + 'authorPHIDs' => 'optional list', + )); + + $with_phids = idx($constraints, 'phids'); + + if ($with_phids === array()) { + throw new Exception( + pht( + 'Constraint "phids" to "transaction.search" requires nonempty list, '. + 'empty list provided.')); + } + + if ($with_phids) { + $query->withPHIDs($with_phids); + } + + $with_authors = idx($constraints, 'authorPHIDs'); + if ($with_authors === array()) { + throw new Exception( + pht( + 'Constraint "authorPHIDs" to "transaction.search" requires '. + 'nonempty list, empty list provided.')); + } + + if ($with_authors) { + $query->withAuthorPHIDs($with_authors); + } + + return $query; + } + + private function newEdgeTransactionFields( + PhabricatorApplicationTransaction $xaction) { + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + + $operations = array(); + foreach ($record->getAddedPHIDs() as $phid) { + $operations[] = array( + 'operation' => 'add', + 'phid' => $phid, + ); + } + + foreach ($record->getRemovedPHIDs() as $phid) { + $operations[] = array( + 'operation' => 'remove', + 'phid' => $phid, + ); + } + + return array( + 'operations' => $operations, + ); + } + } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 9460dd3030..3a46784a33 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3779,9 +3779,14 @@ abstract class PhabricatorApplicationTransactionEditor $this->mustEncrypt = $adapter->getMustEncryptReasons(); + $apply_xactions = $this->didApplyHeraldRules($object, $adapter, $xscript); + assert_instances_of($apply_xactions, 'PhabricatorApplicationTransaction'); + + $queue_xactions = $adapter->getQueuedTransactions(); + return array_merge( - $this->didApplyHeraldRules($object, $adapter, $xscript), - $adapter->getQueuedTransactions()); + array_values($apply_xactions), + array_values($queue_xactions)); } protected function didApplyHeraldRules( diff --git a/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php b/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php index b23c987d6d..e94ca6dc49 100644 --- a/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php +++ b/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php @@ -15,6 +15,7 @@ final class PhabricatorWorkerBulkJobEditor $types = parent::getTransactionTypes(); $types[] = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS; + $types[] = PhabricatorTransactions::TYPE_EDGE; return $types; } diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php index 2acc8452ea..f3c6be520a 100644 --- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php +++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php @@ -11,23 +11,64 @@ final class PhabricatorWorkerManagementExecuteWorkflow pht( 'Execute a task explicitly. This command ignores leases, is '. 'dangerous, and may cause work to be performed twice.')) - ->setArguments($this->getTaskSelectionArguments()); + ->setArguments( + array_merge( + array( + array( + 'name' => 'retry', + 'help' => pht('Retry archived tasks.'), + ), + array( + 'name' => 'repeat', + 'help' => pht('Repeat archived, successful tasks.'), + ), + ), + $this->getTaskSelectionArguments())); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $tasks = $this->loadTasks($args); + $is_retry = $args->getArg('retry'); + $is_repeat = $args->getArg('repeat'); + foreach ($tasks as $task) { $can_execute = !$task->isArchived(); if (!$can_execute) { - $console->writeOut( + if (!$is_retry) { + $console->writeOut( + "** %s ** %s\n", + pht('ARCHIVED'), + pht( + '%s is already archived, and will not be executed. '. + 'Use "--retry" to execute archived tasks.', + $this->describeTask($task))); + continue; + } + + $result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS; + if ($task->getResult() == $result_success) { + if (!$is_repeat) { + $console->writeOut( + "** %s ** %s\n", + pht('SUCCEEDED'), + pht( + '%s has already succeeded, and will not be retried. '. + 'Use "--repeat" to repeat successful tasks.', + $this->describeTask($task))); + continue; + } + } + + echo tsprintf( "** %s ** %s\n", pht('ARCHIVED'), pht( - '%s is already archived, and can not be executed.', + 'Unarchiving %s.', $this->describeTask($task))); - continue; + + $task = $task->unarchiveTask(); } // NOTE: This ignores leases, maybe it should respect them without diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php index 6dbebd168d..538a70add8 100644 --- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php +++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php @@ -10,15 +10,24 @@ final class PhabricatorWorkerManagementRetryWorkflow ->setSynopsis( pht( 'Retry selected tasks which previously failed permanently or '. - 'were cancelled. Only archived, unsuccessful tasks can be '. - 'retried.')) - ->setArguments($this->getTaskSelectionArguments()); + 'were cancelled. Only archived tasks can be retried.')) + ->setArguments( + array_merge( + array( + array( + 'name' => 'repeat', + 'help' => pht( + 'Repeat tasks which already completed successfully.'), + ), + ), + $this->getTaskSelectionArguments())); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $tasks = $this->loadTasks($args); + $is_repeat = $args->getArg('repeat'); foreach ($tasks as $task) { if (!$task->isArchived()) { $console->writeOut( @@ -32,13 +41,16 @@ final class PhabricatorWorkerManagementRetryWorkflow $result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS; if ($task->getResult() == $result_success) { - $console->writeOut( - "** %s ** %s\n", - pht('SUCCEEDED'), - pht( - '%s has already succeeded, and can not be retried.', - $this->describeTask($task))); - continue; + if (!$is_repeat) { + $console->writeOut( + "** %s ** %s\n", + pht('SUCCEEDED'), + pht( + '%s has already succeeded, and will not be repeated. '. + 'Use "--repeat" to repeat successful tasks.', + $this->describeTask($task))); + continue; + } } $task->unarchiveTask(); diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 9f7a69909a..773f78b3a6 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -1233,6 +1233,8 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery 'index' => $index->getIndexKey(), 'alias' => $alias, 'value' => array($min, $max), + 'data' => null, + 'constraints' => null, ); return $this; diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 6ed939a2ee..844690abd3 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -126,6 +126,9 @@ background-size: 12px 12px; background-repeat: no-repeat; background-position: left center; + position: relative; + left: -8px; + opacity: 0.5; } .differential-diff td span.depth-out { diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css index a793c018c3..2d2163f9e9 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css @@ -13,7 +13,12 @@ } .phui-oi-list-big .phui-oi-image-icon { - margin: 8px 2px 12px; + margin: 12px 2px 12px; + text-align: center; +} + +.phui-oi-list-big .phui-oi-image-icon .phui-icon-view { + position: relative; } .phui-oi-list-big a.phui-oi-link { @@ -31,7 +36,7 @@ } .device-desktop .phui-oi-list-big .phui-oi { - margin-bottom: 4px; + margin-bottom: 8px; } .phui-oi-list-big .phui-oi-col0 { @@ -60,13 +65,28 @@ border-radius: 3px; } +.phui-oi-list-big .phui-oi-frame { + padding: 2px 8px; +} + +.phui-oi-list-big .phui-oi-linked-container { + border: 1px solid {$lightblueborder}; + border-radius: 4px; + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.035); +} + +.phui-oi-list-big .phui-oi-disabled { + border-radius: 4px; + background: {$lightgreybackground}; +} + .device-desktop .phui-oi-linked-container { cursor: pointer; } .device-desktop .phui-oi-linked-container:hover { background-color: {$hoverblue}; - border-radius: 3px; + border-color: {$blueborder}; } .device-desktop .phui-oi-linked-container a:hover {