diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6c1240fea0..dd4d978fed 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => '39061f68', + 'core.pkg.css' => 'cb8ae4dc', 'core.pkg.js' => 'e1f0f7bd', 'differential.pkg.css' => '06dc617c', 'differential.pkg.js' => 'c2ca903a', @@ -38,7 +38,7 @@ return array( 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', 'rsrc/css/application/auth/auth.css' => '0877ed6e', 'rsrc/css/application/base/main-menu-view.css' => '1802a242', - 'rsrc/css/application/base/notification-menu.css' => '10685bd4', + 'rsrc/css/application/base/notification-menu.css' => 'ef480927', 'rsrc/css/application/base/phui-theme.css' => '9f261c6b', 'rsrc/css/application/base/standard-page-view.css' => '34ee718b', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', @@ -119,7 +119,7 @@ return array( 'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/phui-font-icon-base.css' => '870a7360', 'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97', - 'rsrc/css/layout/phabricator-source-code-view.css' => '09368218', + 'rsrc/css/layout/phabricator-source-code-view.css' => '2ab25dfa', 'rsrc/css/phui/button/phui-button-bar.css' => 'f1ff5494', 'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68', 'rsrc/css/phui/button/phui-button.css' => '1863cc6e', @@ -131,7 +131,7 @@ return array( 'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77', 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3', 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6', - 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'ae1404ba', + 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '7c5c1291', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea', 'rsrc/css/phui/phui-action-list.css' => '0bcd9a45', 'rsrc/css/phui/phui-action-panel.css' => 'b4798122', @@ -384,12 +384,11 @@ return array( 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'd835b03a', 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04', 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '75b83cbb', - 'rsrc/js/application/diffusion/behavior-diffusion-browse-file.js' => '054a0f0b', 'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947', 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', - 'rsrc/js/application/files/behavior-document-engine.js' => '0333c0b6', + 'rsrc/js/application/files/behavior-document-engine.js' => 'ee0deff8', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909', @@ -597,12 +596,11 @@ return array( 'javelin-behavior-differential-feedback-preview' => '51c5ad07', 'javelin-behavior-differential-populate' => '419998ab', 'javelin-behavior-differential-user-select' => 'a8d8459d', - 'javelin-behavior-diffusion-browse-file' => '054a0f0b', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', 'javelin-behavior-diffusion-commit-graph' => '75b83cbb', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', - 'javelin-behavior-document-engine' => '0333c0b6', + 'javelin-behavior-document-engine' => 'ee0deff8', 'javelin-behavior-doorkeeper-tag' => '1db13e70', 'javelin-behavior-drydock-live-operation-status' => '901935ef', 'javelin-behavior-durable-column' => '2ae077e1', @@ -770,7 +768,7 @@ return array( 'phabricator-nav-view-css' => '694d7723', 'phabricator-notification' => '4f774dac', 'phabricator-notification-css' => '457861ec', - 'phabricator-notification-menu-css' => '10685bd4', + 'phabricator-notification-menu-css' => 'ef480927', 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '77b0ae28', @@ -778,7 +776,7 @@ return array( 'phabricator-search-results-css' => '505dd8cf', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-slowvote-css' => 'a94b7230', - 'phabricator-source-code-view-css' => '09368218', + 'phabricator-source-code-view-css' => '2ab25dfa', 'phabricator-standard-page-view' => '34ee718b', 'phabricator-textareautils' => '320810c8', 'phabricator-title' => '485aaa6c', @@ -840,7 +838,7 @@ return array( 'phui-oi-color-css' => 'cd2b9b77', 'phui-oi-drag-ui-css' => '08f4ccc3', 'phui-oi-flush-ui-css' => '9d9685d6', - 'phui-oi-list-view-css' => 'ae1404ba', + 'phui-oi-list-view-css' => '7c5c1291', 'phui-oi-simple-ui-css' => 'a8beebea', 'phui-pager-css' => 'edcbc226', 'phui-pinboard-view-css' => '2495140e', @@ -907,11 +905,6 @@ return array( 'javelin-behavior', 'javelin-uri', ), - '0333c0b6' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), '040fce04' => array( 'javelin-behavior', 'javelin-request', @@ -932,12 +925,6 @@ return array( 'javelin-util', 'javelin-magical-init', ), - '054a0f0b' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'phabricator-tooltip', - ), '065227cc' => array( 'javelin-behavior', 'javelin-dom', @@ -2118,6 +2105,11 @@ return array( 'javelin-behavior', 'javelin-uri', ), + 'ee0deff8' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + ), 'efe49472' => array( 'javelin-install', 'javelin-util', diff --git a/resources/sql/autopatches/20180418.alamanc.interface.unique.php b/resources/sql/autopatches/20180418.alamanc.interface.unique.php new file mode 100644 index 0000000000..0ad4fbea12 --- /dev/null +++ b/resources/sql/autopatches/20180418.alamanc.interface.unique.php @@ -0,0 +1,85 @@ +establishConnection('w'); + +queryfx( + $interface_conn, + 'LOCK TABLES %T WRITE, %T WRITE', + $interface_table->getTableName(), + $binding_table->getTableName()); + +$seen = array(); +foreach (new LiskMigrationIterator($interface_table) as $interface) { + $device = $interface->getDevicePHID(); + $network = $interface->getNetworkPHID(); + $address = $interface->getAddress(); + $port = $interface->getPort(); + $key = "{$device}/{$network}/{$address}/{$port}"; + + // If this is the first copy of this row we've seen, mark it as seen and + // move on. + if (empty($seen[$key])) { + $seen[$key] = $interface->getID(); + continue; + } + + $survivor = queryfx_one( + $interface_conn, + 'SELECT * FROM %T WHERE id = %d', + $interface_table->getTableName(), + $seen[$key]); + + $bindings = queryfx_all( + $interface_conn, + 'SELECT * FROM %T WHERE interfacePHID = %s', + $binding_table->getTableName(), + $interface->getPHID()); + + // Repoint bindings to the survivor. + foreach ($bindings as $binding) { + // Check if there's already a binding to the survivor. + $existing = queryfx_one( + $interface_conn, + 'SELECT * FROM %T WHERE interfacePHID = %s and devicePHID = %s and '. + 'servicePHID = %s', + $binding_table->getTableName(), + $survivor['phid'], + $binding['devicePHID'], + $binding['servicePHID']); + + if (!$existing) { + // Reattach this binding to the survivor. + queryfx( + $interface_conn, + 'UPDATE %T SET interfacePHID = %s WHERE id = %d', + $binding_table->getTableName(), + $survivor['phid'], + $binding['id']); + } else { + // Binding to survivor already exists. Remove this now-redundant binding. + queryfx( + $interface_conn, + 'DELETE FROM %T WHERE id = %d', + $binding_table->getTableName(), + $binding['id']); + } + } + + queryfx( + $interface_conn, + 'DELETE FROM %T WHERE id = %d', + $interface_table->getTableName(), + $interface->getID()); +} + +queryfx( + $interface_conn, + 'ALTER TABLE %T ADD UNIQUE KEY `key_unique` '. + '(devicePHID, networkPHID, address, port)', + $interface_table->getTableName()); + +queryfx( + $interface_conn, + 'UNLOCK TABLES'); diff --git a/resources/sql/autopatches/20180418.almanac.network.unique.php b/resources/sql/autopatches/20180418.almanac.network.unique.php new file mode 100644 index 0000000000..c81c59823e --- /dev/null +++ b/resources/sql/autopatches/20180418.almanac.network.unique.php @@ -0,0 +1,46 @@ +establishConnection('w'); + +queryfx( + $conn, + 'LOCK TABLES %T WRITE', + $table->getTableName()); + +$seen = array(); +foreach (new LiskMigrationIterator($table) as $network) { + $name = $network->getName(); + + // If this is the first copy of this row we've seen, mark it as seen and + // move on. + if (empty($seen[$name])) { + $seen[$name] = 1; + continue; + } + + // Otherwise, rename this row. + while (true) { + $new_name = $name.'-'.$seen[$name]; + if (empty($seen[$new_name])) { + $network->setName($new_name); + try { + $network->save(); + break; + } catch (AphrontDuplicateKeyQueryException $ex) { + // New name is a dupe of a network we haven't seen yet. + } + } + $seen[$name]++; + } + $seen[$new_name] = 1; +} + +queryfx( + $conn, + 'ALTER TABLE %T ADD UNIQUE KEY `key_name` (name)', + $table->getTableName()); + +queryfx( + $conn, + 'UNLOCK TABLES'); diff --git a/resources/sql/autopatches/20180419.phlux.edges.sql b/resources/sql/autopatches/20180419.phlux.edges.sql new file mode 100644 index 0000000000..1a63aa4d1f --- /dev/null +++ b/resources/sql/autopatches/20180419.phlux.edges.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_phlux.edge ( + src VARBINARY(64) NOT NULL, + type INT UNSIGNED NOT NULL, + dst VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + PRIMARY KEY (src, type, dst), + KEY `src` (src, type, dateCreated, seq), + UNIQUE KEY `key_dst` (dst, type, src) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_phlux.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT} +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index da5674cbbd..beec5058a6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -455,6 +455,7 @@ phutil_register_library_map(array( 'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php', 'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php', 'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php', + 'DifferentialCommitsSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php', 'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php', 'DifferentialConflictsCommitMessageField' => 'applications/differential/field/DifferentialConflictsCommitMessageField.php', 'DifferentialController' => 'applications/differential/controller/DifferentialController.php', @@ -1263,6 +1264,7 @@ phutil_register_library_map(array( 'FundInitiativeTransactionType' => 'applications/fund/xaction/FundInitiativeTransactionType.php', 'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php', 'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php', + 'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php', 'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php', 'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php', 'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php', @@ -1358,6 +1360,7 @@ phutil_register_library_map(array( 'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php', 'HarbormasterCircleCIHookController' => 'applications/harbormaster/controller/HarbormasterCircleCIHookController.php', 'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php', + 'HarbormasterControlBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php', 'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php', 'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php', 'HarbormasterCreatePlansCapability' => 'applications/harbormaster/capability/HarbormasterCreatePlansCapability.php', @@ -4714,6 +4717,7 @@ phutil_register_library_map(array( 'PhluxDAO' => 'applications/phlux/storage/PhluxDAO.php', 'PhluxEditController' => 'applications/phlux/controller/PhluxEditController.php', 'PhluxListController' => 'applications/phlux/controller/PhluxListController.php', + 'PhluxSchemaSpec' => 'applications/phlux/storage/PhluxSchemaSpec.php', 'PhluxTransaction' => 'applications/phlux/storage/PhluxTransaction.php', 'PhluxTransactionQuery' => 'applications/phlux/query/PhluxTransactionQuery.php', 'PhluxVariable' => 'applications/phlux/storage/PhluxVariable.php', @@ -5730,6 +5734,7 @@ phutil_register_library_map(array( 'DifferentialCommitMessageParser' => 'Phobject', 'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase', 'DifferentialCommitsField' => 'DifferentialCustomField', + 'DifferentialCommitsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'DifferentialConduitAPIMethod' => 'ConduitAPIMethod', 'DifferentialConflictsCommitMessageField' => 'DifferentialCommitMessageField', 'DifferentialController' => 'PhabricatorController', @@ -6636,6 +6641,7 @@ phutil_register_library_map(array( 'FundInitiativeTransactionType' => 'PhabricatorModularTransactionType', 'FundInitiativeViewController' => 'FundController', 'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArtifact' => 'Phobject', @@ -6771,6 +6777,7 @@ phutil_register_library_map(array( 'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterCircleCIHookController' => 'HarbormasterController', 'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod', + 'HarbormasterControlBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterController' => 'PhabricatorController', 'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterCreatePlansCapability' => 'PhabricatorPolicyCapability', @@ -10689,6 +10696,7 @@ phutil_register_library_map(array( 'PhluxDAO' => 'PhabricatorLiskDAO', 'PhluxEditController' => 'PhluxController', 'PhluxListController' => 'PhluxController', + 'PhluxSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhluxTransaction' => 'PhabricatorApplicationTransaction', 'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhluxVariable' => array( diff --git a/src/applications/almanac/editor/AlmanacInterfaceEditor.php b/src/applications/almanac/editor/AlmanacInterfaceEditor.php index 865402db36..771076efc9 100644 --- a/src/applications/almanac/editor/AlmanacInterfaceEditor.php +++ b/src/applications/almanac/editor/AlmanacInterfaceEditor.php @@ -15,4 +15,22 @@ final class AlmanacInterfaceEditor return pht('%s created %s.', $author, $object); } + protected function didCatchDuplicateKeyException( + PhabricatorLiskDAO $object, + array $xactions, + Exception $ex) { + + $errors = array(); + + $errors[] = new PhabricatorApplicationTransactionValidationError( + null, + pht('Invalid'), + pht( + 'Interfaces must have a unique combination of network, device, '. + 'address, and port.'), + null); + + throw new PhabricatorApplicationTransactionValidationException($errors); + } + } diff --git a/src/applications/almanac/query/AlmanacDeviceQuery.php b/src/applications/almanac/query/AlmanacDeviceQuery.php index 29461bfbfd..0d38070e0b 100644 --- a/src/applications/almanac/query/AlmanacDeviceQuery.php +++ b/src/applications/almanac/query/AlmanacDeviceQuery.php @@ -8,6 +8,7 @@ final class AlmanacDeviceQuery private $names; private $namePrefix; private $nameSuffix; + private $isClusterDevice; public function withIDs(array $ids) { $this->ids = $ids; @@ -40,6 +41,11 @@ final class AlmanacDeviceQuery $ngrams); } + public function withIsClusterDevice($is_cluster_device) { + $this->isClusterDevice = $is_cluster_device; + return $this; + } + public function newResultObject() { return new AlmanacDevice(); } @@ -90,6 +96,13 @@ final class AlmanacDeviceQuery $this->nameSuffix); } + if ($this->isClusterDevice !== null) { + $where[] = qsprintf( + $conn, + 'device.isBoundToClusterService = %d', + (int)$this->isClusterDevice); + } + return $where; } diff --git a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php index 6e28d7e5b8..1d0a1d8475 100644 --- a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php @@ -25,6 +25,13 @@ final class AlmanacDeviceSearchEngine ->setLabel(pht('Exact Names')) ->setKey('names') ->setDescription(pht('Search for devices with specific names.')), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Cluster Device')) + ->setKey('isClusterDevice') + ->setOptions( + pht('Both Cluster and Non-cluster Devices'), + pht('Cluster Devices Only'), + pht('Non-cluster Devices Only')), ); } @@ -39,6 +46,10 @@ final class AlmanacDeviceSearchEngine $query->withNames($map['names']); } + if ($map['isClusterDevice'] !== null) { + $query->withIsClusterDevice($map['isClusterDevice']); + } + return $query; } diff --git a/src/applications/almanac/query/AlmanacNetworkQuery.php b/src/applications/almanac/query/AlmanacNetworkQuery.php index a09da0093b..13176f7de1 100644 --- a/src/applications/almanac/query/AlmanacNetworkQuery.php +++ b/src/applications/almanac/query/AlmanacNetworkQuery.php @@ -5,6 +5,7 @@ final class AlmanacNetworkQuery private $ids; private $phids; + private $names; public function withIDs(array $ids) { $this->ids = $ids; @@ -20,6 +21,11 @@ final class AlmanacNetworkQuery return new AlmanacNetwork(); } + public function withNames(array $names) { + $this->names = $names; + return $this; + } + public function withNameNgrams($ngrams) { return $this->withNgramsConstraint( new AlmanacNetworkNameNgrams(), @@ -47,6 +53,13 @@ final class AlmanacNetworkQuery $this->phids); } + if ($this->names !== null) { + $where[] = qsprintf( + $conn, + 'network.name IN (%Ls)', + $this->names); + } + return $where; } diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index 4002651d08..5c7f65ddd1 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -35,6 +35,10 @@ final class AlmanacInterface 'key_device' => array( 'columns' => array('devicePHID'), ), + 'key_unique' => array( + 'columns' => array('devicePHID', 'networkPHID', 'address', 'port'), + 'unique' => true, + ), ), ) + parent::getConfiguration(); } diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index 6a530b4cb3..6d2f23032e 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -24,8 +24,15 @@ final class AlmanacNetwork return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( - 'name' => 'text128', + 'name' => 'sort128', 'mailKey' => 'bytes20', + + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_name' => array( + 'columns' => array('name'), + 'unique' => true, + ), ), ) + parent::getConfiguration(); } diff --git a/src/applications/almanac/util/AlmanacNames.php b/src/applications/almanac/util/AlmanacNames.php index 68a387cb9b..cee3c81151 100644 --- a/src/applications/almanac/util/AlmanacNames.php +++ b/src/applications/almanac/util/AlmanacNames.php @@ -6,57 +6,58 @@ final class AlmanacNames extends Phobject { if (strlen($name) < 3) { throw new Exception( pht( - 'Almanac service, device, property and namespace names must be '. - 'at least 3 characters long.')); + 'Almanac service, device, property, network and namespace names '. + 'must be at least 3 characters long.')); } if (strlen($name) > 100) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'be more than 100 characters long.')); + 'Almanac service, device, property, network and namespace names '. + 'may not be more than 100 characters long.')); } if (!preg_match('/^[a-z0-9.-]+\z/', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may only '. - 'contain lowercase letters, numbers, hyphens, and periods.')); + 'Almanac service, device, property, network and namespace names '. + 'may only contain lowercase letters, numbers, hyphens, and '. + 'periods.')); } if (preg_match('/(^|\\.)\d+(\z|\\.)/', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'have any segments containing only digits.')); + 'Almanac service, device, network, property and namespace names '. + 'may not have any segments containing only digits.')); } if (preg_match('/\.\./', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'contain multiple consecutive periods.')); + 'Almanac service, device, property, network and namespace names '. + 'may not contain multiple consecutive periods.')); } if (preg_match('/\\.-|-\\./', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'contain hyphens adjacent to periods.')); + 'Almanac service, device, property, network and namespace names '. + 'may not contain hyphens adjacent to periods.')); } if (preg_match('/--/', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names may not '. - 'contain multiple consecutive hyphens.')); + 'Almanac service, device, property, network and namespace names '. + 'may not contain multiple consecutive hyphens.')); } if (!preg_match('/^[a-z0-9].*[a-z0-9]\z/', $name)) { throw new Exception( pht( - 'Almanac service, device, property and namespace names must begin '. - 'and end with a letter or number.')); + 'Almanac service, device, property, network and namespace names '. + 'must begin and end with a letter or number.')); } } diff --git a/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php b/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php index 140c1a9661..f049587b26 100644 --- a/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php +++ b/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php @@ -38,6 +38,37 @@ final class AlmanacNetworkNameTransaction pht('Network name is required.')); } + foreach ($xactions as $xaction) { + $name = $xaction->getNewValue(); + + $message = null; + try { + AlmanacNames::validateName($name); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $errors[] = $this->newInvalidError($message, $xaction); + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $other = id(new AlmanacNetworkQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $errors[] = $this->newInvalidError( + pht('Almanac networks must have unique names.'), + $xaction); + continue; + } + } + return $errors; } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 50094b02f4..82740459b0 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1555,13 +1555,41 @@ final class DifferentialTransactionEditor $auto_undraft = false; } - if ($object->isDraft() && $auto_undraft) { + $can_promote = false; + $can_demote = false; + + // "Draft" revisions can promote to "Review Requested" after builds pass, + // or demote to "Changes Planned" after builds fail. + if ($object->isDraft()) { + $can_promote = true; + $can_demote = true; + } + + // See PHI584. "Changes Planned" revisions which are not yet broadcasting + // can promote to "Review Requested" if builds pass. + + // This pass is presumably the result of someone restarting the builds and + // having them work this time, perhaps because the builds are not perfectly + // reliable or perhaps because someone fixed some issue with build hardware + // or some other dependency. + + // Currently, there's no legitimate way to end up in this state except + // through automatic demotion, so this behavior should not generate an + // undue level of confusion or ambiguity. Also note that these changes can + // not demote again since they've already been demoted once. + if ($object->isChangePlanned()) { + if (!$object->getShouldBroadcast()) { + $can_promote = true; + } + } + + if (($can_promote || $can_demote) && $auto_undraft) { $status = $this->loadCompletedBuildableStatus($object); $is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED); $is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED); - if ($is_passed) { + if ($is_passed && $can_promote) { // When Harbormaster moves a revision out of the draft state, we // attribute the action to the revision author since this is more // natural and more useful. @@ -1593,7 +1621,7 @@ final class DifferentialTransactionEditor // batch of transactions finishes so that Herald can fire on the new // revision state. See T13027 for discussion. $this->queueTransaction($xaction); - } else if ($is_failed) { + } else if ($is_failed && $can_demote) { // When demoting a revision, we act as "Harbormaster" instead of // the author since this feels a little more natural. $harbormaster_phid = id(new PhabricatorHarbormasterApplication()) @@ -1607,8 +1635,6 @@ final class DifferentialTransactionEditor ->setNewValue(true); $this->queueTransaction($xaction); - - // TODO: Notify the author (only) that we did this. } } diff --git a/src/applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php b/src/applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php new file mode 100644 index 0000000000..395541c9f9 --- /dev/null +++ b/src/applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php @@ -0,0 +1,77 @@ +loadAllWhere( + 'diffID IN (%Ld) AND name = %s', + mpull($objects, 'getID'), + 'local:commits'); + + $map = array(); + foreach ($properties as $property) { + $map[$property->getDiffID()] = $property->getData(); + } + + return $map; + } + + public function getAttachmentForObject($object, $data, $spec) { + $diff_id = $object->getID(); + $info = idx($data, $diff_id, array()); + + // NOTE: This should be similar to the information returned about commits + // by "diffusion.commit.search". + + $list = array(); + foreach ($info as $commit) { + $author_epoch = idx($commit, 'time'); + if ($author_epoch) { + $author_epoch = (int)$author_epoch; + } + + // TODO: Currently, we don't upload the raw author string from "arc". + // Reconstruct a plausible version of it until we begin uploading this + // information. + + $author_name = idx($commit, 'author'); + $author_email = idx($commit, 'authorEmail'); + if (strlen($author_name) && strlen($author_email)) { + $author_raw = (string)id(new PhutilEmailAddress()) + ->setDisplayName($author_name) + ->setAddress($author_email); + } else if (strlen($author_email)) { + $author_raw = $author_email; + } else { + $author_raw = $author_name; + } + + $list[] = array( + 'identifier' => $commit['commit'], + 'tree' => idx($commit, 'tree'), + 'parents' => idx($commit, 'parents', array()), + 'author' => array( + 'name' => $author_name, + 'email' => $author_email, + 'raw' => $author_raw, + 'epoch' => $author_epoch, + ), + 'message' => idx($commit, 'message'), + ); + } + + return array( + 'commits' => $list, + ); + } + +} diff --git a/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php b/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php index 99125645e7..0db158e171 100644 --- a/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php +++ b/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php @@ -6,7 +6,9 @@ final class PhabricatorDifferentialMigrateHunkWorkflow protected function didConstruct() { $this ->setName('migrate-hunk') - ->setExamples('**migrate-hunk** --id __hunk__ --to __storage__') + ->setExamples( + "**migrate-hunk** --id __hunk__ --to __storage__\n". + "**migrate-hunk** --all") ->setSynopsis(pht('Migrate storage engines for a hunk.')) ->setArguments( array( @@ -20,51 +22,93 @@ final class PhabricatorDifferentialMigrateHunkWorkflow 'param' => 'storage', 'help' => pht('Storage engine to migrate to.'), ), + array( + 'name' => 'all', + 'help' => pht('Migrate all hunks.'), + ), + array( + 'name' => 'auto', + 'help' => pht('Select storage format automatically.'), + ), + array( + 'name' => 'dry-run', + 'help' => pht('Show planned writes but do not perform them.'), + ), )); } public function execute(PhutilArgumentParser $args) { + $is_dry_run = $args->getArg('dry-run'); + $id = $args->getArg('id'); - if (!$id) { + $is_all = $args->getArg('all'); + + if ($is_all && $id) { throw new PhutilArgumentUsageException( - pht('Specify a hunk to migrate with --id.')); - } - - $storage = $args->getArg('to'); - switch ($storage) { - case DifferentialHunk::DATATYPE_TEXT: - case DifferentialHunk::DATATYPE_FILE: - break; - default: - throw new PhutilArgumentUsageException( - pht('Specify a hunk storage engine with --to.')); - } - - $hunk = $this->loadHunk($id); - $old_data = $hunk->getChanges(); - - switch ($storage) { - case DifferentialHunk::DATATYPE_TEXT: - $hunk->saveAsText(); - $this->logOkay( - pht('TEXT'), - pht('Convereted hunk to text storage.')); - break; - case DifferentialHunk::DATATYPE_FILE: - $hunk->saveAsFile(); - $this->logOkay( - pht('FILE'), - pht('Convereted hunk to file storage.')); - break; - } - - $hunk = $this->loadHunk($id); - $new_data = $hunk->getChanges(); - - if ($old_data !== $new_data) { - throw new Exception( pht( - 'Integrity check failed: new file data differs fom old data!')); + 'Options "--all" (to migrate all hunks) and "--id" (to migrate a '. + 'specific hunk) are mutually exclusive.')); + } else if (!$is_all && !$id) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a hunk to migrate with "--id", or migrate all hunks '. + 'with "--all".')); + } + + $is_auto = $args->getArg('auto'); + $storage = $args->getArg('to'); + if ($is_auto && $storage) { + throw new PhutilArgumentUsageException( + pht( + 'Options "--to" (to choose a specific storage format) and "--auto" '. + '(to select a storage format automatically) are mutually '. + 'exclusive.')); + } else if (!$is_auto && !$storage) { + throw new PhutilArgumentUsageException( + pht( + 'Use "--to" to choose a storage format, or "--auto" to select a '. + 'format automatically.')); + } + + $types = array( + DifferentialHunk::DATATYPE_TEXT, + DifferentialHunk::DATATYPE_FILE, + ); + $types = array_fuse($types); + if (strlen($storage)) { + if (!isset($types[$storage])) { + throw new PhutilArgumentUsageException( + pht( + 'Storage type "%s" is unknown. Supported types are: %s.', + $storage, + implode(', ', array_keys($types)))); + } + } + + if ($id) { + $hunk = $this->loadHunk($id); + $hunks = array($hunk); + } else { + $hunks = new LiskMigrationIterator(new DifferentialHunk()); + } + + foreach ($hunks as $hunk) { + try { + $this->migrateHunk($hunk, $storage, $is_auto, $is_dry_run); + } catch (Exception $ex) { + // If we're migrating a single hunk, just throw the exception. If + // we're migrating multiple hunks, warn but continue. + if ($id) { + throw $ex; + } + + $this->logWarn( + pht('WARN'), + pht( + 'Failed to migrate hunk %d: %s', + $hunk->getID(), + $ex->getMessage())); + } } return 0; @@ -82,5 +126,87 @@ final class PhabricatorDifferentialMigrateHunkWorkflow return $hunk; } + private function migrateHunk( + DifferentialHunk $hunk, + $type, + $is_auto, + $is_dry_run) { + + $old_type = $hunk->getDataType(); + + if ($is_auto) { + // By default, we're just going to keep hunks in the same storage + // engine. In the future, we could perhaps select large hunks stored in + // text engine and move them into file storage. + $new_type = $old_type; + } else { + $new_type = $type; + } + + // Figure out if the storage format (e.g., plain text vs compressed) + // would change if we wrote this hunk anew today. + $old_format = $hunk->getDataFormat(); + $new_format = $hunk->getAutomaticDataFormat(); + + $same_type = ($old_type === $new_type); + $same_format = ($old_format === $new_format); + + // If we aren't going to change the storage engine and aren't going to + // change the storage format, just bail out. + if ($same_type && $same_format) { + $this->logInfo( + pht('SKIP'), + pht( + 'Hunk %d is already stored in the preferred engine ("%s") '. + 'with the preferred format ("%s").', + $hunk->getID(), + $new_type, + $new_format)); + return; + } + + if ($is_dry_run) { + $this->logOkay( + pht('DRY RUN'), + pht( + 'Hunk %d would be rewritten (storage: "%s" -> "%s"; '. + 'format: "%s" -> "%s").', + $hunk->getID(), + $old_type, + $new_type, + $old_format, + $new_format)); + return; + } + + $old_data = $hunk->getChanges(); + + switch ($new_type) { + case DifferentialHunk::DATATYPE_TEXT: + $hunk->saveAsText(); + break; + case DifferentialHunk::DATATYPE_FILE: + $hunk->saveAsFile(); + break; + } + + $this->logOkay( + pht('MIGRATE'), + pht( + 'Converted hunk %d to "%s" storage (with format "%s").', + $hunk->getID(), + $new_type, + $hunk->getDataFormat())); + + $hunk = $this->loadHunk($hunk->getID()); + $new_data = $hunk->getChanges(); + + if ($old_data !== $new_data) { + throw new Exception( + pht( + 'Integrity check failed: new file data differs from old data!')); + } + } + } diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 728ec4b538..cb392c4306 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -815,7 +815,10 @@ final class DifferentialDiff } public function getConduitSearchAttachments() { - return array(); + return array( + id(new DifferentialCommitsSearchEngineAttachment()) + ->setAttachmentKey('commits'), + ); } diff --git a/src/applications/differential/storage/DifferentialHunk.php b/src/applications/differential/storage/DifferentialHunk.php index 3defb3566b..6bfb38b789 100644 --- a/src/applications/differential/storage/DifferentialHunk.php +++ b/src/applications/differential/storage/DifferentialHunk.php @@ -290,14 +290,24 @@ final class DifferentialHunk return array(self::DATAFORMAT_RAW, $data); } + public function getAutomaticDataFormat() { + // If the hunk is already stored deflated, just keep it deflated. This is + // mostly a performance improvement for "bin/differential migrate-hunk" so + // that we don't have to recompress all the stored hunks when looking for + // stray uncompressed hunks. + if ($this->dataFormat === self::DATAFORMAT_DEFLATED) { + return self::DATAFORMAT_DEFLATED; + } + + list($format) = $this->formatDataForStorage($this->getRawData()); + + return $format; + } + public function saveAsText() { $old_type = $this->getDataType(); $old_data = $this->getData(); - if ($old_type == self::DATATYPE_TEXT) { - return $this; - } - $raw_data = $this->getRawData(); $this->setDataType(self::DATATYPE_TEXT); @@ -317,10 +327,6 @@ final class DifferentialHunk $old_type = $this->getDataType(); $old_data = $this->getData(); - if ($old_type == self::DATATYPE_FILE) { - return $this; - } - $raw_data = $this->getRawData(); list($format, $data) = $this->formatDataForStorage($raw_data); diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index 4c69d76274..e53bccb892 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -229,22 +229,30 @@ final class DifferentialRevisionListView extends AphrontView { $classes = array(); $classes[] = 'differential-revision-size'; + $tip = array(); + $tip[] = pht('%s Lines', new PhutilNumber($n)); + if ($plus_count <= 1) { $classes[] = 'differential-revision-small'; + $tip[] = pht('Smaller Change'); } if ($plus_count >= 4) { $classes[] = 'differential-revision-large'; + $tip[] = pht('Larger Change'); } + $tip = phutil_implode_html(" \xC2\xB7 ", $tip); + return javelin_tag( 'span', array( 'class' => implode(' ', $classes), 'sigil' => 'has-tooltip', 'meta' => array( - 'tip' => pht('%s Lines', new PhutilNumber($n)), + 'tip' => $tip, 'align' => 'E', + 'size' => 400, ), ), $size); diff --git a/src/applications/diffusion/controller/DiffusionBlameController.php b/src/applications/diffusion/controller/DiffusionBlameController.php index 66403a8af9..85ab393ef6 100644 --- a/src/applications/diffusion/controller/DiffusionBlameController.php +++ b/src/applications/diffusion/controller/DiffusionBlameController.php @@ -80,7 +80,6 @@ final class DiffusionBlameController extends DiffusionController { $handles = $viewer->loadHandles($handle_phids); - $map = array(); $epochs = array(); foreach ($identifiers as $identifier) { @@ -106,9 +105,21 @@ final class DiffusionBlameController extends DiffusionController { ), $skip_icon); - $commit = $commits[$identifier]; + // We may not have a commit object for a given identifier if the commit + // has not imported yet. + + // At time of writing, this can also happen if a line was part of the + // initial import: blame produces a "^abc123" identifier in Git, which + // doesn't correspond to a real commit. + + $commit = idx($commits, $identifier); + + $author_phid = null; + + if ($commit) { + $author_phid = $commit->getAuthorPHID(); + } - $author_phid = $commit->getAuthorPHID(); if (!$author_phid && $revision) { $author_phid = $revision->getAuthorPHID(); } @@ -141,18 +152,22 @@ final class DiffusionBlameController extends DiffusionController { 'meta' => $author_meta, )); - $commit_link = javelin_tag( - 'a', - array( - 'href' => $commit->getURI(), - 'sigil' => 'has-tooltip', - 'meta' => array( - 'tip' => $this->renderCommitTooltip($commit, $handles), - 'align' => 'E', - 'size' => 600, + if ($commit) { + $commit_link = javelin_tag( + 'a', + array( + 'href' => $commit->getURI(), + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $this->renderCommitTooltip($commit, $handles), + 'align' => 'E', + 'size' => 600, + ), ), - ), - $commit->getLocalName()); + $commit->getLocalName()); + } else { + $commit_link = null; + } $info = array( $author_link, @@ -180,7 +195,12 @@ final class DiffusionBlameController extends DiffusionController { ); } - $epoch = $commit->getEpoch(); + if ($commit) { + $epoch = $commit->getEpoch(); + } else { + $epoch = 0; + } + $epochs[] = $epoch; $data = array( diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index 1053ef477c..54be7dd7f1 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -4,7 +4,6 @@ final class DiffusionBrowseController extends DiffusionController { private $lintCommit; private $lintMessages; - private $coverage; private $corpusButtons = array(); public function shouldAllowPublic() { @@ -182,7 +181,6 @@ final class DiffusionBrowseController extends DiffusionController { $corpus = $this->buildGitLFSCorpus($lfs_ref); } else { - $this->coverage = $drequest->loadCoverage(); $show_editor = true; $ref = id(new PhabricatorDocumentRef()) diff --git a/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php b/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php index 17abba5c4f..9615b35799 100644 --- a/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php +++ b/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php @@ -81,6 +81,11 @@ final class DiffusionDocumentRenderingEngine $ref ->setSymbolMetadata($this->getSymbolMetadata()) ->setBlameURI($blame_uri); + + $coverage = $drequest->loadCoverage(); + if (strlen($coverage)) { + $ref->addCoverage($coverage); + } } private function getSymbolMetadata() { diff --git a/src/applications/diffusion/query/blame/DiffusionGitBlameQuery.php b/src/applications/diffusion/query/blame/DiffusionGitBlameQuery.php index f98cc645a7..0eb15cd018 100644 --- a/src/applications/diffusion/query/blame/DiffusionGitBlameQuery.php +++ b/src/applications/diffusion/query/blame/DiffusionGitBlameQuery.php @@ -7,8 +7,12 @@ final class DiffusionGitBlameQuery extends DiffusionBlameQuery { $commit = $request->getCommit(); + // NOTE: The "--root" flag suppresses the addition of the "^" boundary + // commit marker. Without it, root commits render with a "^" before them, + // and one fewer character of the commit hash. + return $repository->getLocalCommandFuture( - '--no-pager blame -s -l %s -- %s', + '--no-pager blame --root -s -l %s -- %s', $commit, $path); } diff --git a/src/applications/files/document/PhabricatorDocumentRef.php b/src/applications/files/document/PhabricatorDocumentRef.php index 953ac7df5d..38e4491615 100644 --- a/src/applications/files/document/PhabricatorDocumentRef.php +++ b/src/applications/files/document/PhabricatorDocumentRef.php @@ -10,6 +10,7 @@ final class PhabricatorDocumentRef private $snippet; private $symbolMetadata = array(); private $blameURI; + private $coverage = array(); public function setFile(PhabricatorFile $file) { $this->file = $file; @@ -151,4 +152,15 @@ final class PhabricatorDocumentRef return $this->blameURI; } + public function addCoverage($coverage) { + $this->coverage[] = array( + 'data' => $coverage, + ); + return $this; + } + + public function getCoverage() { + return $this->coverage; + } + } diff --git a/src/applications/files/document/PhabricatorSourceDocumentEngine.php b/src/applications/files/document/PhabricatorSourceDocumentEngine.php index 20a1c94a95..c88d979b4e 100644 --- a/src/applications/files/document/PhabricatorSourceDocumentEngine.php +++ b/src/applications/files/document/PhabricatorSourceDocumentEngine.php @@ -57,6 +57,10 @@ final class PhabricatorSourceDocumentEngine $options['blame'] = $blame; } + if ($ref->getCoverage()) { + $options['coverage'] = $ref->getCoverage(); + } + return array( $messages, $this->newTextDocumentContent($ref, $content, $options), diff --git a/src/applications/files/document/PhabricatorTextDocumentEngine.php b/src/applications/files/document/PhabricatorTextDocumentEngine.php index 0377a24353..2fce98baf1 100644 --- a/src/applications/files/document/PhabricatorTextDocumentEngine.php +++ b/src/applications/files/document/PhabricatorTextDocumentEngine.php @@ -22,6 +22,7 @@ abstract class PhabricatorTextDocumentEngine $options, array( 'blame' => 'optional wild', + 'coverage' => 'optional list', )); if (is_array($content)) { @@ -40,6 +41,11 @@ abstract class PhabricatorTextDocumentEngine $view->setBlameMap($blame); } + $coverage = idx($options, 'coverage'); + if ($coverage !== null) { + $view->setCoverage($coverage); + } + $message = null; if ($this->encodingMessage !== null) { $message = $this->newMessage($this->encodingMessage); diff --git a/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php b/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php index 4a16f53fdf..155ed28dfa 100644 --- a/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php +++ b/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php @@ -145,6 +145,17 @@ abstract class PhabricatorDocumentRenderingEngine 'uri' => $ref->getBlameURI(), 'value' => null, ), + 'coverage' => array( + 'labels' => array( + // TODO: Modularize this properly, see T13125. + array( + 'C' => pht('Covered'), + 'U' => pht('Not Covered'), + 'N' => pht('Not Executable'), + 'X' => pht('Not Reachable'), + ), + ), + ), ); $view_button = id(new PHUIButtonView()) diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php index dc63b5fd95..7437e48f6c 100644 --- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php +++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php @@ -98,6 +98,19 @@ final class HarbormasterBuildStatus extends Phobject { ); } + public static function getIncompleteStatusConstants() { + $map = self::getBuildStatusSpecMap(); + + $constants = array(); + foreach ($map as $constant => $spec) { + if (!$spec['isComplete']) { + $constants[] = $constant; + } + } + + return $constants; + } + public static function getCompletedStatusConstants() { return array( self::STATUS_PASSED, diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index ddab677faf..843ffd4702 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -51,18 +51,7 @@ final class HarbormasterBuildActionController } if ($request->isDialogFormPost() && $can_issue) { - $editor = id(new HarbormasterBuildTransactionEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - - $xaction = id(new HarbormasterBuildTransaction()) - ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) - ->setNewValue($action); - - $editor->applyTransactions($build, array($xaction)); - + $build->sendMessage($viewer, $action); return id(new AphrontRedirectResponse())->setURI($return_uri); } diff --git a/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php b/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php index c14185116c..992dd4fad1 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php @@ -22,48 +22,39 @@ final class HarbormasterBuildStepQuery return $this; } - protected function loadPage() { - $table = new HarbormasterBuildStep(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + public function newResultObject() { + return new HarbormasterBuildStep(); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } - if ($this->ids) { + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid in (%Ls)', $this->phids); } - if ($this->buildPlanPHIDs) { + if ($this->buildPlanPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'buildPlanPHID in (%Ls)', $this->buildPlanPHIDs); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } protected function willFilterPage(array $page) { diff --git a/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php new file mode 100644 index 0000000000..d6c2a46734 --- /dev/null +++ b/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php @@ -0,0 +1,135 @@ +getIsManualBuildable()) { + // Don't abort anything if this is a manual buildable. + return; + } + + $object_phid = $buildable->getBuildablePHID(); + if (phid_get_type($object_phid) !== DifferentialDiffPHIDType::TYPECONST) { + // If this buildable isn't building a diff, bail out. For example, we + // might be building a commit. In this case, this step has no effect. + return; + } + + $diff = id(new DifferentialDiffQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->executeOne(); + if (!$diff) { + return; + } + + $revision_id = $diff->getRevisionID(); + + $revision = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withIDs(array($revision_id)) + ->executeOne(); + if (!$revision) { + return; + } + + $active_phid = $revision->getActiveDiffPHID(); + if ($active_phid !== $object_phid) { + // If we aren't building the active diff, bail out. + return; + } + + $diffs = id(new DifferentialDiffQuery()) + ->setViewer($viewer) + ->withRevisionIDs(array($revision_id)) + ->execute(); + $abort_diff_phids = array(); + foreach ($diffs as $diff) { + if ($diff->getPHID() !== $active_phid) { + $abort_diff_phids[] = $diff->getPHID(); + } + } + + if (!$abort_diff_phids) { + return; + } + + // We're fetching buildables even if they have "passed" or "failed" + // because they may still have ongoing builds. At the time of writing + // only "failed" buildables may still be ongoing, but it seems likely that + // "passed" buildables may be ongoing in the future. + + $abort_buildables = id(new HarbormasterBuildableQuery()) + ->setViewer($viewer) + ->withBuildablePHIDs($abort_diff_phids) + ->withManualBuildables(false) + ->execute(); + if (!$abort_buildables) { + return; + } + + $statuses = HarbormasterBuildStatus::getIncompleteStatusConstants(); + + $abort_builds = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withBuildablePHIDs(mpull($abort_buildables, 'getPHID')) + ->withBuildPlanPHIDs(array($plan->getPHID())) + ->withBuildStatuses($statuses) + ->execute(); + if (!$abort_builds) { + return; + } + + foreach ($abort_builds as $abort_build) { + $abort_build->sendMessage( + $viewer, + HarbormasterBuildCommand::COMMAND_ABORT); + } + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + return; + } + +} diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index 47af992630..ccf97451b7 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -308,6 +308,15 @@ abstract class HarbormasterBuildStepImplementation extends Phobject { 'enabled in configuration.')); } + public function willStartBuild( + PhabricatorUser $viewer, + HarbormasterBuildable $buildable, + HarbormasterBuild $build, + HarbormasterBuildPlan $plan, + HarbormasterBuildStep $step) { + return; + } + /* -( Automatic Targets )-------------------------------------------------- */ diff --git a/src/applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php b/src/applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php new file mode 100644 index 0000000000..f49fd0b127 --- /dev/null +++ b/src/applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php @@ -0,0 +1,20 @@ +save(); + $steps = id(new HarbormasterBuildStepQuery()) + ->setViewer($viewer) + ->withBuildPlanPHIDs(array($plan->getPHID())) + ->execute(); + + foreach ($steps as $step) { + $step->willStartBuild($viewer, $this, $build, $plan); + } + PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index ae0f6b13f3..6acbac6468 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -356,6 +356,35 @@ final class HarbormasterBuild extends HarbormasterDAO } } + public function sendMessage(PhabricatorUser $viewer, $command) { + // TODO: This should not be an editor transaction, but there are plans to + // merge BuildCommand into BuildMessage which should moot this. As this + // exists today, it can race against BuildEngine. + + // This is a bogus content source, but this whole flow should be obsolete + // soon. + $content_source = PhabricatorContentSource::newForSource( + PhabricatorConsoleContentSource::SOURCECONST); + + $editor = id(new HarbormasterBuildTransactionEditor()) + ->setActor($viewer) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $viewer_phid = $viewer->getPHID(); + if (!$viewer_phid) { + $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID(); + $editor->setActingAsPHID($acting_phid); + } + + $xaction = id(new HarbormasterBuildTransaction()) + ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) + ->setNewValue($command); + + $editor->applyTransactions($this, array($xaction)); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php index b29958882c..54c069e97d 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php @@ -100,6 +100,19 @@ final class HarbormasterBuildStep extends HarbormasterDAO return ($this->getStepAutoKey() !== null); } + public function willStartBuild( + PhabricatorUser $viewer, + HarbormasterBuildable $buildable, + HarbormasterBuild $build, + HarbormasterBuildPlan $plan) { + return $this->getStepImplementation()->willStartBuild( + $viewer, + $buildable, + $build, + $plan, + $this); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php index 3e30ead9e7..1e956a60ea 100644 --- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php +++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php @@ -6,6 +6,10 @@ final class PhabricatorNotificationPanelController public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); + $unread_count = $viewer->getUnreadNotificationCount(); + + $warning = $this->prunePhantomNotifications($unread_count); + $query = id(new PhabricatorNotificationQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) @@ -66,13 +70,12 @@ final class PhabricatorNotificationPanelController )); $content = hsprintf( - '%s%s%s', + '%s%s%s%s', $header, + $warning, $content, $connection_ui); - $unread_count = $viewer->getUnreadNotificationCount(); - $json = array( 'content' => $content, 'number' => (int)$unread_count, @@ -80,4 +83,81 @@ final class PhabricatorNotificationPanelController return id(new AphrontAjaxResponse())->setContent($json); } + + private function prunePhantomNotifications($unread_count) { + // See T8953. If you have an unread notification about an object you + // do not have permission to view, it isn't possible to clear it by + // visiting the object. Identify these notifications and mark them as + // read. + + $viewer = $this->getViewer(); + + if (!$unread_count) { + return null; + } + + $table = new PhabricatorFeedStoryNotification(); + $conn = $table->establishConnection('r'); + + $rows = queryfx_all( + $conn, + 'SELECT chronologicalKey, primaryObjectPHID FROM %T + WHERE userPHID = %s AND hasViewed = 0', + $table->getTableName(), + $viewer->getPHID()); + if (!$rows) { + return null; + } + + $map = array(); + foreach ($rows as $row) { + $map[$row['primaryObjectPHID']][] = $row['chronologicalKey']; + } + + $handles = $viewer->loadHandles(array_keys($map)); + $purge_keys = array(); + foreach ($handles as $handle) { + $phid = $handle->getPHID(); + if ($handle->isComplete()) { + continue; + } + + foreach ($map[$phid] as $chronological_key) { + $purge_keys[] = $chronological_key; + } + } + + if (!$purge_keys) { + return null; + } + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + $conn = $table->establishConnection('w'); + queryfx( + $conn, + 'UPDATE %T SET hasViewed = 1 + WHERE userPHID = %s AND chronologicalKey IN (%Ls)', + $table->getTableName(), + $viewer->getPHID(), + $purge_keys); + + PhabricatorUserCache::clearCache( + PhabricatorUserNotificationCountCacheType::KEY_COUNT, + $viewer->getPHID()); + + unset($unguarded); + + return phutil_tag( + 'div', + array( + 'class' => 'phabricator-notification phabricator-notification-warning', + ), + pht( + '%s notification(s) about objects which no longer exist or which '. + 'you can no longer see were discarded.', + phutil_count($purge_keys))); + } + + } diff --git a/src/applications/notification/query/PhabricatorNotificationQuery.php b/src/applications/notification/query/PhabricatorNotificationQuery.php index aa430a2518..b40bdee066 100644 --- a/src/applications/notification/query/PhabricatorNotificationQuery.php +++ b/src/applications/notification/query/PhabricatorNotificationQuery.php @@ -76,31 +76,31 @@ final class PhabricatorNotificationQuery return $stories; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->userPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'notif.userPHID IN (%Ls)', $this->userPHIDs); } if ($this->unread !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'notif.hasViewed = %d', (int)!$this->unread); } if ($this->keys) { $where[] = qsprintf( - $conn_r, + $conn, 'notif.chronologicalKey IN (%Ls)', $this->keys); } - return $this->formatWhereClause($where); + return $where; } protected function getResultCursor($item) { diff --git a/src/applications/phlux/storage/PhluxSchemaSpec.php b/src/applications/phlux/storage/PhluxSchemaSpec.php new file mode 100644 index 0000000000..ee8a38b6f5 --- /dev/null +++ b/src/applications/phlux/storage/PhluxSchemaSpec.php @@ -0,0 +1,10 @@ +buildEdgeSchemata(new PhluxVariable()); + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 01088522e7..98f78e63e6 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -716,6 +716,10 @@ final class PhabricatorRepositoryCommit } public function getFieldValuesForConduit() { + + // NOTE: This data should be similar to the information returned about + // commmits by "differential.diff.search" with the "commits" attachment. + return array( 'identifier' => $this->getCommitIdentifier(), ); diff --git a/src/applications/transactions/constants/PhabricatorTransactions.php b/src/applications/transactions/constants/PhabricatorTransactions.php index 4089187ca4..7a606fe65c 100644 --- a/src/applications/transactions/constants/PhabricatorTransactions.php +++ b/src/applications/transactions/constants/PhabricatorTransactions.php @@ -15,6 +15,7 @@ final class PhabricatorTransactions extends Phobject { const TYPE_CREATE = 'core:create'; const TYPE_COLUMNS = 'core:columns'; const TYPE_SUBTYPE = 'core:subtype'; + const TYPE_HISTORY = 'core:history'; const COLOR_RED = 'red'; const COLOR_ORANGE = 'orange'; diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 70644b1974..f76f66df66 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -83,6 +83,7 @@ abstract class PhabricatorApplicationTransactionEditor private $webhookMap = array(); private $transactionQueue = array(); + private $sendHistory = false; const STORAGE_ENCODING_BINARY = 'binary'; @@ -300,6 +301,7 @@ abstract class PhabricatorApplicationTransactionEditor $types = array(); $types[] = PhabricatorTransactions::TYPE_CREATE; + $types[] = PhabricatorTransactions::TYPE_HISTORY; if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) { $types[] = PhabricatorTransactions::TYPE_SUBTYPE; @@ -377,6 +379,7 @@ abstract class PhabricatorApplicationTransactionEditor switch ($type) { case PhabricatorTransactions::TYPE_CREATE: + case PhabricatorTransactions::TYPE_HISTORY: return null; case PhabricatorTransactions::TYPE_SUBTYPE: return $object->getEditEngineSubtype(); @@ -468,6 +471,7 @@ abstract class PhabricatorApplicationTransactionEditor case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_INLINESTATE: case PhabricatorTransactions::TYPE_SUBTYPE: + case PhabricatorTransactions::TYPE_HISTORY: return $xaction->getNewValue(); case PhabricatorTransactions::TYPE_SPACE: $space_phid = $xaction->getNewValue(); @@ -520,6 +524,7 @@ abstract class PhabricatorApplicationTransactionEditor switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_CREATE: + case PhabricatorTransactions::TYPE_HISTORY: return true; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); @@ -604,6 +609,7 @@ abstract class PhabricatorApplicationTransactionEditor $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->applyApplicationTransactionInternalEffects($xaction); case PhabricatorTransactions::TYPE_CREATE: + case PhabricatorTransactions::TYPE_HISTORY: case PhabricatorTransactions::TYPE_SUBTYPE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_VIEW_POLICY: @@ -665,6 +671,7 @@ abstract class PhabricatorApplicationTransactionEditor $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->applyApplicationTransactionExternalEffects($xaction); case PhabricatorTransactions::TYPE_CREATE: + case PhabricatorTransactions::TYPE_HISTORY: case PhabricatorTransactions::TYPE_SUBTYPE: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_TOKEN: @@ -800,6 +807,9 @@ abstract class PhabricatorApplicationTransactionEditor case PhabricatorTransactions::TYPE_SPACE: $this->scrambleFileSecrets($object); break; + case PhabricatorTransactions::TYPE_HISTORY: + $this->sendHistory = true; + break; } } @@ -1317,6 +1327,13 @@ abstract class PhabricatorApplicationTransactionEditor $this->publishFeedStory($object, $xactions, $mailed); } + if ($this->sendHistory) { + $history_mail = $this->buildHistoryMail($object); + if ($history_mail) { + $messages[] = $history_mail; + } + } + // NOTE: This actually sends the mail. We do this last to reduce the chance // that we send some mail, hit an exception, then send the mail again when // retrying. @@ -2557,6 +2574,25 @@ abstract class PhabricatorApplicationTransactionEditor $unexpandable = array(); } + $messages = $this->buildMailWithRecipients( + $object, + $xactions, + $email_to, + $email_cc, + $unexpandable); + + $this->runHeraldMailRules($messages); + + return $messages; + } + + private function buildMailWithRecipients( + PhabricatorLiskDAO $object, + array $xactions, + array $email_to, + array $email_cc, + array $unexpandable) { + $targets = $this->buildReplyHandler($object) ->setUnexpandablePHIDs($unexpandable) ->getMailTargets($email_to, $email_cc); @@ -2603,8 +2639,6 @@ abstract class PhabricatorApplicationTransactionEditor } } - $this->runHeraldMailRules($messages); - return $messages; } @@ -2935,34 +2969,62 @@ abstract class PhabricatorApplicationTransactionEditor $object_label = null, $object_href = null) { + // First, remove transactions which shouldn't be rendered in mail. + foreach ($xactions as $key => $xaction) { + if ($xaction->shouldHideForMail($xactions)) { + unset($xactions[$key]); + } + } + $headers = array(); $headers_html = array(); $comments = array(); $details = array(); + $seen_comment = false; foreach ($xactions as $xaction) { - if ($xaction->shouldHideForMail($xactions)) { - continue; - } - $header = $xaction->getTitleForMail(); - if ($header !== null) { - $headers[] = $header; - } + // Most mail has zero or one comments. In these cases, we render the + // "alice added a comment." transaction in the header, like a normal + // transaction. - $header_html = $xaction->getTitleForHTMLMail(); - if ($header_html !== null) { - $headers_html[] = $header_html; - } + // Some mail, like Differential undraft mail or "!history" mail, may + // have two or more comments. In these cases, we'll put the first + // "alice added a comment." transaction in the header normally, but + // move the other transactions down so they provide context above the + // actual comment. $comment = $xaction->getBodyForMail(); if ($comment !== null) { - $comments[] = $comment; + $is_comment = true; + $comments[] = array( + 'xaction' => $xaction, + 'comment' => $comment, + 'initial' => !$seen_comment, + ); + } else { + $is_comment = false; + } + + if (!$is_comment || !$seen_comment) { + $header = $xaction->getTitleForMail(); + if ($header !== null) { + $headers[] = $header; + } + + $header_html = $xaction->getTitleForHTMLMail(); + if ($header_html !== null) { + $headers_html[] = $header_html; + } } if ($xaction->hasChangeDetailsForMail()) { $details[] = $xaction; } + + if ($is_comment) { + $seen_comment = true; + } } $headers_text = implode("\n", $headers); @@ -2995,8 +3057,7 @@ abstract class PhabricatorApplicationTransactionEditor $object_label); } - $xactions_style = array( - ); + $xactions_style = array(); $header_action = phutil_tag( 'td', @@ -3023,7 +3084,25 @@ abstract class PhabricatorApplicationTransactionEditor $body->addRawHTMLSection($headers_html); - foreach ($comments as $comment) { + foreach ($comments as $spec) { + $xaction = $spec['xaction']; + $comment = $spec['comment']; + $is_initial = $spec['initial']; + + // If this is not the first comment in the mail, add the header showing + // who wrote the comment immediately above the comment. + if (!$is_initial) { + $header = $xaction->getTitleForMail(); + if ($header !== null) { + $body->addRawPlaintextSection($header); + } + + $header_html = $xaction->getTitleForHTMLMail(); + if ($header_html !== null) { + $body->addRawHTMLSection($header_html); + } + } + $body->addRemarkupSection(null, $comment); } @@ -3671,6 +3750,7 @@ abstract class PhabricatorApplicationTransactionEditor 'mailMutedPHIDs', 'webhookMap', 'silent', + 'sendHistory', ); } @@ -4328,4 +4408,32 @@ abstract class PhabricatorApplicationTransactionEditor return true; } + private function buildHistoryMail(PhabricatorLiskDAO $object) { + $viewer = $this->requireActor(); + $recipient_phid = $this->getActingAsPHID(); + + // Load every transaction so we can build a mail message with a complete + // history for the object. + $query = PhabricatorApplicationTransactionQuery::newQueryForObject($object); + $xactions = $query + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->execute(); + $xactions = array_reverse($xactions); + + $mail_messages = $this->buildMailWithRecipients( + $object, + $xactions, + array($recipient_phid), + array(), + array()); + $mail = head($mail_messages); + + // Since the user explicitly requested "!history", force delivery of this + // message regardless of their other mail settings. + $mail->setForceDelivery(true); + + return $mail; + } + } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 69d11fe753..ed88ac98db 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -545,11 +545,18 @@ abstract class PhabricatorApplicationTransaction return false; } + $xaction_type = $this->getTransactionType(); + + // Always hide requests for object history. + if ($xaction_type === PhabricatorTransactions::TYPE_HISTORY) { + return true; + } + // Hide creation transactions if the old value is empty. These are - // transactions like "alice set the task tile to: ...", which are + // transactions like "alice set the task title to: ...", which are // essentially never interesting. if ($this->getIsCreateTransaction()) { - switch ($this->getTransactionType()) { + switch ($xaction_type) { case PhabricatorTransactions::TYPE_CREATE: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index eb74f8debf..d33f042d0b 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1651,6 +1651,14 @@ final class PhabricatorUSEnglishTranslation 'Destroyed %s credentials of type "%s".', ), + '%s notification(s) about objects which no longer exist or which '. + 'you can no longer see were discarded.' => array( + 'One notification about an object which no longer exists or which '. + 'you can no longer see was discarded.', + '%s notifications about objects which no longer exist or which '. + 'you can no longer see were discarded.', + ), + ); } diff --git a/src/view/layout/PhabricatorSourceCodeView.php b/src/view/layout/PhabricatorSourceCodeView.php index 19126d1c6a..ae20fe2501 100644 --- a/src/view/layout/PhabricatorSourceCodeView.php +++ b/src/view/layout/PhabricatorSourceCodeView.php @@ -10,6 +10,7 @@ final class PhabricatorSourceCodeView extends AphrontView { private $truncatedFirstLines = false; private $symbolMetadata; private $blameMap; + private $coverage = array(); public function setLines(array $lines) { $this->lines = $lines; @@ -59,6 +60,15 @@ final class PhabricatorSourceCodeView extends AphrontView { return $this->blameMap; } + public function setCoverage(array $coverage) { + $this->coverage = $coverage; + return $this; + } + + public function getCoverage() { + return $this->coverage; + } + public function render() { $blame_map = $this->getBlameMap(); $has_blame = ($blame_map !== null); @@ -97,6 +107,19 @@ final class PhabricatorSourceCodeView extends AphrontView { $base_uri = (string)$this->uri; $wrote_anchor = false; + + $coverage = $this->getCoverage(); + $coverage_count = count($coverage); + $coverage_data = ipull($coverage, 'data'); + + // TODO: Modularize this properly, see T13125. + $coverage_map = array( + 'C' => 'background: #66bbff;', + 'U' => 'background: #dd8866;', + 'N' => 'background: #ddeeff;', + 'X' => 'background: #aa00aa;', + ); + foreach ($lines as $line) { $row_attributes = array(); if (isset($this->highlights[$line_number])) { @@ -157,6 +180,25 @@ final class PhabricatorSourceCodeView extends AphrontView { $blame_cells = null; } + $coverage_cells = array(); + foreach ($coverage as $coverage_idx => $coverage_spec) { + if (isset($coverage_spec['data'][$line_number - 1])) { + $coverage_char = $coverage_spec['data'][$line_number - 1]; + } else { + $coverage_char = null; + } + + $coverage_style = idx($coverage_map, $coverage_char, null); + + $coverage_cells[] = phutil_tag( + 'th', + array( + 'class' => 'phabricator-source-coverage', + 'style' => $coverage_style, + 'data-coverage' => $coverage_idx.'/'.$coverage_char, + )); + } + $rows[] = phutil_tag( 'tr', $row_attributes, @@ -174,7 +216,8 @@ final class PhabricatorSourceCodeView extends AphrontView { 'class' => 'phabricator-source-code', ), $line), - )); + $coverage_cells, + )); $line_number++; } diff --git a/webroot/rsrc/css/application/base/notification-menu.css b/webroot/rsrc/css/application/base/notification-menu.css index 8e2391d57c..8db2436891 100644 --- a/webroot/rsrc/css/application/base/notification-menu.css +++ b/webroot/rsrc/css/application/base/notification-menu.css @@ -68,6 +68,10 @@ color: {$lightgreytext}; } +.phabricator-notification-warning { + background: {$sh-yellowbackground}; +} + .phabricator-notification-list .phabricator-notification-unread, .phabricator-notification-menu .phabricator-notification-unread { background: {$hoverblue}; @@ -95,7 +99,7 @@ .phabricator-notification-unread .phabricator-notification-foot .phabricator-notification-status { font-size: 7px; - color: {$lightgreytext}; + color: {$lightbluetext}; position: absolute; display: inline-block; top: 6px; diff --git a/webroot/rsrc/css/layout/phabricator-source-code-view.css b/webroot/rsrc/css/layout/phabricator-source-code-view.css index 6f2864d067..9b61425d63 100644 --- a/webroot/rsrc/css/layout/phabricator-source-code-view.css +++ b/webroot/rsrc/css/layout/phabricator-source-code-view.css @@ -6,7 +6,6 @@ overflow-x: auto; overflow-y: hidden; border: 1px solid {$paste.border}; - border-radius: 3px; } .phui-oi .phabricator-source-code-container { @@ -25,6 +24,7 @@ text-align: right; border-right: 1px solid {$paste.border}; color: {$sh-yellowtext}; + white-space: nowrap; } .phabricator-source-line > a::before { @@ -47,10 +47,12 @@ th.phabricator-source-line a:hover { text-decoration: none; } +.phabricator-source-coverage-highlight .phabricator-source-code, .phabricator-source-highlight .phabricator-source-code { background: {$paste.highlight}; } +.phabricator-source-coverage-highlight .phabricator-source-line, .phabricator-source-highlight .phabricator-source-line { background: {$paste.border}; } @@ -96,7 +98,7 @@ th.phabricator-source-line a:hover { .phabricator-source-blame-info a { color: {$darkbluetext}; - text-shadow: 1px 1px rgba(0, 0, 0, 0.111); + text-shadow: 1px 1px rgba(0, 0, 0, 0.05); } .phabricator-source-blame-skip a { @@ -123,3 +125,10 @@ th.phabricator-source-line a:hover { background-size: 100% 100%; background-repeat: no-repeat; } + +th.phabricator-source-coverage { + padding: 0 8px; + border-left: 1px solid {$thinblueborder}; + background: {$lightgreybackground}; + cursor: w-resize; +} diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css index 9436d171df..6f2421ca2f 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css @@ -697,22 +697,26 @@ ul.phui-oi-list-view .phui-oi-selectable .differential-revision-size .phui-icon-view { margin: 0 1px 0 1px; - font-size: smaller; - color: {$blueborder}; + font-size: 7px; + position: relative; + top: -2px; + color: {$lightbluetext}; } .differential-revision-large { - background: {$sh-redbackground}; + background: {$sh-orangebackground}; } +/* NOTE: These are intentionally using nonstandard colors, see T13127. */ + .differential-revision-large .phui-icon-view { - color: {$red}; + color: #e5ae7e; } .differential-revision-small { - background: {$sh-greenbackground}; + background: #f2f7ff; } .differential-revision-small .phui-icon-view { - color: {$green}; + color: #6699ba; } diff --git a/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js b/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js deleted file mode 100644 index 41ff0cfcc3..0000000000 --- a/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @provides javelin-behavior-diffusion-browse-file - * @requires javelin-behavior - * javelin-dom - * javelin-util - * phabricator-tooltip - */ - -JX.behavior('diffusion-browse-file', function(config, statics) { - if (statics.installed) { - return; - } - statics.installed = true; - - var map = config.labels; - - JX.Stratcom.listen( - ['mouseover', 'mouseout'], - ['phabricator-source', 'tag:td'], - function(e) { - var target = e.getTarget(); - - // NOTE: We're using raw classnames instead of sigils and metadata here - // because these elements are unusual: there are a lot of them on the - // page, and rendering all the extra metadata to do this in a normal way - // would be needlessly expensive. This is an unusual case. - - if (!target.className.match(/cov-/)) { - return; - } - - if (e.getType() == 'mouseout') { - JX.Tooltip.hide(); - return; - } - - for (var k in map) { - if (!target.className.match(k)) { - continue; - } - - var label = map[k]; - JX.Tooltip.show(target, 300, 'E', label); - break; - } - }); -}); diff --git a/webroot/rsrc/js/application/files/behavior-document-engine.js b/webroot/rsrc/js/application/files/behavior-document-engine.js index 2fc4887408..8b957e0ea0 100644 --- a/webroot/rsrc/js/application/files/behavior-document-engine.js +++ b/webroot/rsrc/js/application/files/behavior-document-engine.js @@ -322,7 +322,7 @@ JX.behavior('document-engine', function(config, statics) { var h_max = 0.44; var h = h_min + ((h_max - h_min) * epoch_value); - var s = 0.44; + var s = 0.25; var v_min = 0.92; var v_max = 1.00; @@ -357,6 +357,57 @@ JX.behavior('document-engine', function(config, statics) { return 'rgb(' + r + ', ' + g + ', ' + b + ')'; } + function onhovercoverage(data, e) { + if (e.getType() === 'mouseout') { + redraw_coverage(data, null); + return; + } + + var target = e.getNode('tag:th'); + var coverage = target.getAttribute('data-coverage'); + if (!coverage) { + return; + } + + redraw_coverage(data, target); + } + + var coverage_row = null; + function redraw_coverage(data, node) { + if (coverage_row) { + JX.DOM.alterClass( + coverage_row, + 'phabricator-source-coverage-highlight', + false); + coverage_row = null; + } + + if (!node) { + JX.Tooltip.hide(); + return; + } + + var coverage = node.getAttribute('data-coverage'); + coverage = coverage.split('/'); + + var idx = parseInt(coverage[0], 10); + var chr = coverage[1]; + + var map = data.coverage.labels[idx]; + if (map) { + var label = map[chr]; + if (label) { + JX.Tooltip.show(node, 300, 'W', label); + + coverage_row = JX.DOM.findAbove(node, 'tr'); + JX.DOM.alterClass( + coverage_row, + 'phabricator-source-coverage-highlight', + true); + } + } + } + if (!statics.initialized) { JX.Stratcom.listen('click', 'document-engine-view-dropdown', onmenu); statics.initialized = true; @@ -374,6 +425,12 @@ JX.behavior('document-engine', function(config, statics) { blame(data); break; } + + JX.DOM.listen( + JX.$(data.viewportID), + ['mouseover', 'mouseout'], + 'tag:th', + JX.bind(null, onhovercoverage, data)); } });