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 {