1
0
Fork 0
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:
epriestley 2018-04-20 14:53:24 -07:00
commit 201c56a91e
50 changed files with 1280 additions and 233 deletions

View file

@ -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',

View file

@ -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');

View file

@ -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');

View 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};

View file

@ -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(

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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.'));
}
}

View file

@ -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;
}

View file

@ -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.
}
}

View file

@ -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,
);
}
}

View file

@ -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!'));
}
}
}

View file

@ -815,7 +815,10 @@ final class DifferentialDiff
}
public function getConduitSearchAttachments() {
return array();
return array(
id(new DifferentialCommitsSearchEngineAttachment())
->setAttachmentKey('commits'),
);
}

View file

@ -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);

View file

@ -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);

View file

@ -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(

View file

@ -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())

View file

@ -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() {

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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),

View file

@ -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);

View file

@ -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())

View file

@ -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,

View file

@ -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);
}

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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 )-------------------------------------------------- */

View file

@ -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;
}
}

View file

@ -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(

View file

@ -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 )------------------------- */

View file

@ -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 )------------------------- */

View file

@ -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)));
}
}

View file

@ -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) {

View file

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

View file

@ -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(),
);

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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:

View file

@ -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.',
),
);
}

View file

@ -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++;

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
});
});

View file

@ -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));
}
});