mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-10 23:01:04 +01:00
(stable) Promote 2018 Week 16
This commit is contained in:
commit
201c56a91e
50 changed files with 1280 additions and 233 deletions
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
$interface_table = new AlmanacInterface();
|
||||
$binding_table = new AlmanacBinding();
|
||||
$interface_conn = $interface_table->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');
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
$table = new AlmanacNetwork();
|
||||
$conn = $table->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');
|
16
resources/sql/autopatches/20180419.phlux.edges.sql
Normal file
16
resources/sql/autopatches/20180419.phlux.edges.sql
Normal file
|
@ -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};
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
final class DifferentialCommitsSearchEngineAttachment
|
||||
extends PhabricatorSearchEngineAttachment {
|
||||
|
||||
public function getAttachmentName() {
|
||||
return pht('Diff Commits');
|
||||
}
|
||||
|
||||
public function getAttachmentDescription() {
|
||||
return pht('Get the local commits (if any) for each diff.');
|
||||
}
|
||||
|
||||
public function loadAttachmentData(array $objects, $spec) {
|
||||
$properties = id(new DifferentialDiffProperty())->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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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!'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -815,7 +815,10 @@ final class DifferentialDiff
|
|||
}
|
||||
|
||||
public function getConduitSearchAttachments() {
|
||||
return array();
|
||||
return array(
|
||||
id(new DifferentialCommitsSearchEngineAttachment())
|
||||
->setAttachmentKey('commits'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
if (!$author_phid && $revision) {
|
||||
$author_phid = $revision->getAuthorPHID();
|
||||
}
|
||||
|
@ -141,6 +152,7 @@ final class DiffusionBlameController extends DiffusionController {
|
|||
'meta' => $author_meta,
|
||||
));
|
||||
|
||||
if ($commit) {
|
||||
$commit_link = javelin_tag(
|
||||
'a',
|
||||
array(
|
||||
|
@ -153,6 +165,9 @@ final class DiffusionBlameController extends DiffusionController {
|
|||
),
|
||||
),
|
||||
$commit->getLocalName());
|
||||
} else {
|
||||
$commit_link = null;
|
||||
}
|
||||
|
||||
$info = array(
|
||||
$author_link,
|
||||
|
@ -180,7 +195,12 @@ final class DiffusionBlameController extends DiffusionController {
|
|||
);
|
||||
}
|
||||
|
||||
if ($commit) {
|
||||
$epoch = $commit->getEpoch();
|
||||
} else {
|
||||
$epoch = 0;
|
||||
}
|
||||
|
||||
$epochs[] = $epoch;
|
||||
|
||||
$data = array(
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -22,6 +22,7 @@ abstract class PhabricatorTextDocumentEngine
|
|||
$options,
|
||||
array(
|
||||
'blame' => 'optional wild',
|
||||
'coverage' => 'optional list<wild>',
|
||||
));
|
||||
|
||||
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);
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
final class HarbormasterAbortOlderBuildsBuildStepImplementation
|
||||
extends HarbormasterBuildStepImplementation {
|
||||
|
||||
public function getName() {
|
||||
return pht('Abort Older Builds');
|
||||
}
|
||||
|
||||
public function getGenericDescription() {
|
||||
return pht(
|
||||
'When building a revision, abort copies of this build plan which are '.
|
||||
'currently running against older diffs.');
|
||||
}
|
||||
|
||||
public function getBuildStepGroupKey() {
|
||||
return HarbormasterControlBuildStepGroup::GROUPKEY;
|
||||
}
|
||||
|
||||
public function getEditInstructions() {
|
||||
return pht(<<<EOTEXT
|
||||
When run against a revision, this build step will abort any older copies of
|
||||
the same build plan which are currently running against older diffs.
|
||||
|
||||
There are some nuances to the behavior:
|
||||
|
||||
- if this build step is triggered manually, it won't abort anything;
|
||||
- this build step won't abort manual builds;
|
||||
- this build step won't abort anything if the diff it is building isn't
|
||||
the active diff when it runs.
|
||||
|
||||
Build results on outdated diffs often aren't very important, so this may
|
||||
reduce build queue load without any substantial cost.
|
||||
EOTEXT
|
||||
);
|
||||
}
|
||||
|
||||
public function willStartBuild(
|
||||
PhabricatorUser $viewer,
|
||||
HarbormasterBuildable $buildable,
|
||||
HarbormasterBuild $build,
|
||||
HarbormasterBuildPlan $plan,
|
||||
HarbormasterBuildStep $step) {
|
||||
|
||||
if ($buildable->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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 )-------------------------------------------------- */
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
final class HarbormasterControlBuildStepGroup
|
||||
extends HarbormasterBuildStepGroup {
|
||||
|
||||
const GROUPKEY = 'harbormaster.control';
|
||||
|
||||
public function getGroupName() {
|
||||
return pht('Flow Control');
|
||||
}
|
||||
|
||||
public function getGroupOrder() {
|
||||
return 5000;
|
||||
}
|
||||
|
||||
public function shouldShowIfEmpty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -141,6 +141,15 @@ final class HarbormasterBuildable
|
|||
|
||||
$build->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(
|
||||
|
|
|
@ -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 )------------------------- */
|
||||
|
||||
|
|
|
@ -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 )------------------------- */
|
||||
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
10
src/applications/phlux/storage/PhluxSchemaSpec.php
Normal file
10
src/applications/phlux/storage/PhluxSchemaSpec.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhluxSchemaSpec
|
||||
extends PhabricatorConfigSchemaSpec {
|
||||
|
||||
public function buildSchemata() {
|
||||
$this->buildEdgeSchemata(new PhluxVariable());
|
||||
}
|
||||
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,16 +2969,44 @@ 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;
|
||||
|
||||
// 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.
|
||||
|
||||
// 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) {
|
||||
$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;
|
||||
|
@ -2954,15 +3016,15 @@ abstract class PhabricatorApplicationTransactionEditor
|
|||
if ($header_html !== null) {
|
||||
$headers_html[] = $header_html;
|
||||
}
|
||||
|
||||
$comment = $xaction->getBodyForMail();
|
||||
if ($comment !== null) {
|
||||
$comments[] = $comment;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.',
|
||||
),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,6 +216,7 @@ final class PhabricatorSourceCodeView extends AphrontView {
|
|||
'class' => 'phabricator-source-code',
|
||||
),
|
||||
$line),
|
||||
$coverage_cells,
|
||||
));
|
||||
|
||||
$line_number++;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue