Merge branch 'master' into blender-tweaks
This commit is contained in:
		@@ -9,7 +9,7 @@ return array(
 | 
			
		||||
  'names' => array(
 | 
			
		||||
    'conpherence.pkg.css' => 'e68cf1fa',
 | 
			
		||||
    'conpherence.pkg.js' => '15191c65',
 | 
			
		||||
    'core.pkg.css' => '6c6885d9',
 | 
			
		||||
    'core.pkg.css' => '70cc8c80',
 | 
			
		||||
    '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' => '7821ca89',
 | 
			
		||||
    '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',
 | 
			
		||||
@@ -386,12 +386,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',
 | 
			
		||||
@@ -599,12 +598,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',
 | 
			
		||||
@@ -772,7 +770,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',
 | 
			
		||||
@@ -780,7 +778,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',
 | 
			
		||||
@@ -843,7 +841,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',
 | 
			
		||||
@@ -910,11 +908,6 @@ return array(
 | 
			
		||||
      'javelin-behavior',
 | 
			
		||||
      'javelin-uri',
 | 
			
		||||
    ),
 | 
			
		||||
    '0333c0b6' => array(
 | 
			
		||||
      'javelin-behavior',
 | 
			
		||||
      'javelin-dom',
 | 
			
		||||
      'javelin-stratcom',
 | 
			
		||||
    ),
 | 
			
		||||
    '040fce04' => array(
 | 
			
		||||
      'javelin-behavior',
 | 
			
		||||
      'javelin-request',
 | 
			
		||||
@@ -935,12 +928,6 @@ return array(
 | 
			
		||||
      'javelin-util',
 | 
			
		||||
      'javelin-magical-init',
 | 
			
		||||
    ),
 | 
			
		||||
    '054a0f0b' => array(
 | 
			
		||||
      'javelin-behavior',
 | 
			
		||||
      'javelin-dom',
 | 
			
		||||
      'javelin-util',
 | 
			
		||||
      'phabricator-tooltip',
 | 
			
		||||
    ),
 | 
			
		||||
    '065227cc' => array(
 | 
			
		||||
      'javelin-behavior',
 | 
			
		||||
      'javelin-dom',
 | 
			
		||||
@@ -2121,6 +2108,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};
 | 
			
		||||
@@ -5,9 +5,22 @@ confirm() {
 | 
			
		||||
  read -e ignored
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
GIT='git'
 | 
			
		||||
INSTALL_URI="   https://phurl.io/u/install"
 | 
			
		||||
 | 
			
		||||
failed() {
 | 
			
		||||
  echo
 | 
			
		||||
  echo
 | 
			
		||||
  echo "Installation has failed."
 | 
			
		||||
  echo "Text above this message might be useful to understanding what exactly failed."
 | 
			
		||||
  echo
 | 
			
		||||
  echo "Please follow this guide to manually complete installation:"
 | 
			
		||||
  echo
 | 
			
		||||
  echo $INSTALL_URI
 | 
			
		||||
  echo
 | 
			
		||||
  echo "We apologize for the inconvenience."
 | 
			
		||||
  exit 3
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
LTS="Ubuntu 10.04"
 | 
			
		||||
ISSUE=`cat /etc/issue`
 | 
			
		||||
if [[ ($ISSUE != Ubuntu*) && ($ISSUE != Debian*) ]];
 | 
			
		||||
then
 | 
			
		||||
@@ -15,20 +28,13 @@ then
 | 
			
		||||
  echo "to be something else. Your results may vary.";
 | 
			
		||||
  echo
 | 
			
		||||
  confirm
 | 
			
		||||
elif [[ `expr match "$ISSUE" "$LTS"` -eq ${#LTS} ]]
 | 
			
		||||
then
 | 
			
		||||
  GIT='git-core'
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
echo "PHABRICATOR UBUNTU INSTALL SCRIPT";
 | 
			
		||||
echo "This script will install Phabricator and all of its core dependencies.";
 | 
			
		||||
echo "This script will install Apache, Phabricator and its core dependencies.";
 | 
			
		||||
echo "Run it from the directory you want to install into.";
 | 
			
		||||
echo
 | 
			
		||||
 | 
			
		||||
ROOT=`pwd`
 | 
			
		||||
echo "Phabricator will be installed to: ${ROOT}.";
 | 
			
		||||
confirm
 | 
			
		||||
 | 
			
		||||
echo "Testing sudo..."
 | 
			
		||||
sudo true
 | 
			
		||||
if [ $? -ne 0 ]
 | 
			
		||||
@@ -37,31 +43,56 @@ then
 | 
			
		||||
  exit 1;
 | 
			
		||||
fi;
 | 
			
		||||
 | 
			
		||||
echo "Installing dependencies: git, apache, mysql, php...";
 | 
			
		||||
echo
 | 
			
		||||
echo 'Testing Ubuntu version...'
 | 
			
		||||
 | 
			
		||||
set +x
 | 
			
		||||
VERSION=`lsb_release -rs`
 | 
			
		||||
MAJOR=`expr match "$VERSION" '\([0-9]*\)'`
 | 
			
		||||
 | 
			
		||||
sudo apt-get -qq update
 | 
			
		||||
sudo apt-get install \
 | 
			
		||||
  $GIT mysql-server apache2 dpkg-dev \
 | 
			
		||||
  php5 php5-mysqlnd php5-gd php5-dev php5-curl php-apc php5-cli php5-json
 | 
			
		||||
 | 
			
		||||
# Enable mod_rewrite
 | 
			
		||||
sudo a2enmod rewrite
 | 
			
		||||
 | 
			
		||||
HAVEPCNTL=`php -r "echo extension_loaded('pcntl');"`
 | 
			
		||||
if [ $HAVEPCNTL != "1" ]
 | 
			
		||||
if [ "$MAJOR" -lt 16 ]
 | 
			
		||||
then
 | 
			
		||||
  echo "Installing pcntl...";
 | 
			
		||||
  echo 'This script is intented to install on modern operating systems; Your '
 | 
			
		||||
  echo 'operating system is too old for this script.'
 | 
			
		||||
  echo 'You can still install Phabricator manually - please consult the installation'
 | 
			
		||||
  echo 'guide to see how:'
 | 
			
		||||
  echo
 | 
			
		||||
  apt-get source php5
 | 
			
		||||
  PHP5=`ls -1F | grep '^php5-.*/$'`
 | 
			
		||||
  (cd $PHP5/ext/pcntl && phpize && ./configure && make && sudo make install)
 | 
			
		||||
else
 | 
			
		||||
  echo "pcntl already installed";
 | 
			
		||||
  echo $INSTALL_URI
 | 
			
		||||
  echo
 | 
			
		||||
  exit 2
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Ubuntu 16.04 LTS only has php 7.0 in their repos, so they need this extra ppa.
 | 
			
		||||
# Ubuntu 17.4 and up have official 7.2 builds.
 | 
			
		||||
if [ "$MAJOR" -eq 16 ]
 | 
			
		||||
then
 | 
			
		||||
  echo 'This version of Ubuntu requires additional resources in order to install'
 | 
			
		||||
  echo 'and run Phabricator.'
 | 
			
		||||
  echo 'We will now add a the following package repository to your system:'
 | 
			
		||||
  echo '  https://launchpad.net/~ondrej/+archive/ubuntu/php'
 | 
			
		||||
  echo
 | 
			
		||||
  echo 'This repository is generally considered safe to use.'
 | 
			
		||||
  confirm
 | 
			
		||||
 | 
			
		||||
  sudo add-apt-repository -y ppa:ondrej/php  || failed
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
ROOT=`pwd`
 | 
			
		||||
echo "Phabricator will be installed to: ${ROOT}.";
 | 
			
		||||
confirm
 | 
			
		||||
 | 
			
		||||
echo "Installing dependencies: git, apache, mysql, php...";
 | 
			
		||||
echo
 | 
			
		||||
sudo apt-get -qq update
 | 
			
		||||
sudo apt-get install \
 | 
			
		||||
  git mysql-server apache2 libapache2-mod-php \
 | 
			
		||||
  php php-mysql php-gd php-curl php-apcu php-cli php-json php-mbstring \
 | 
			
		||||
  || failed
 | 
			
		||||
 | 
			
		||||
echo "Enabling mod_rewrite in Apache..."
 | 
			
		||||
echo
 | 
			
		||||
sudo a2enmod rewrite  || failed
 | 
			
		||||
 | 
			
		||||
echo "Downloading Phabricator and dependencies..."
 | 
			
		||||
echo
 | 
			
		||||
if [ ! -e libphutil ]
 | 
			
		||||
then
 | 
			
		||||
  git clone https://github.com/phacility/libphutil.git
 | 
			
		||||
@@ -89,4 +120,4 @@ echo "Install probably worked mostly correctly. Continue with the 'Configuration
 | 
			
		||||
echo
 | 
			
		||||
echo "    https://secure.phabricator.com/book/phabricator/article/configuration_guide/";
 | 
			
		||||
echo
 | 
			
		||||
echo "You can delete any php5-* stuff that's left over in this directory if you want.";
 | 
			
		||||
echo 'Next step is "Configuring Apache webserver".'
 | 
			
		||||
 
 | 
			
		||||
@@ -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',
 | 
			
		||||
@@ -4719,6 +4722,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',
 | 
			
		||||
@@ -5737,6 +5741,7 @@ phutil_register_library_map(array(
 | 
			
		||||
    'DifferentialCommitMessageParser' => 'Phobject',
 | 
			
		||||
    'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase',
 | 
			
		||||
    'DifferentialCommitsField' => 'DifferentialCustomField',
 | 
			
		||||
    'DifferentialCommitsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
 | 
			
		||||
    'DifferentialConduitAPIMethod' => 'ConduitAPIMethod',
 | 
			
		||||
    'DifferentialConflictsCommitMessageField' => 'DifferentialCommitMessageField',
 | 
			
		||||
    'DifferentialController' => 'PhabricatorController',
 | 
			
		||||
@@ -6643,6 +6648,7 @@ phutil_register_library_map(array(
 | 
			
		||||
    'FundInitiativeTransactionType' => 'PhabricatorModularTransactionType',
 | 
			
		||||
    'FundInitiativeViewController' => 'FundController',
 | 
			
		||||
    'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec',
 | 
			
		||||
    'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
 | 
			
		||||
    'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
 | 
			
		||||
    'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
 | 
			
		||||
    'HarbormasterArtifact' => 'Phobject',
 | 
			
		||||
@@ -6778,6 +6784,7 @@ phutil_register_library_map(array(
 | 
			
		||||
    'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
 | 
			
		||||
    'HarbormasterCircleCIHookController' => 'HarbormasterController',
 | 
			
		||||
    'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
 | 
			
		||||
    'HarbormasterControlBuildStepGroup' => 'HarbormasterBuildStepGroup',
 | 
			
		||||
    'HarbormasterController' => 'PhabricatorController',
 | 
			
		||||
    'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
 | 
			
		||||
    'HarbormasterCreatePlansCapability' => 'PhabricatorPolicyCapability',
 | 
			
		||||
@@ -10701,6 +10708,7 @@ phutil_register_library_map(array(
 | 
			
		||||
    'PhluxDAO' => 'PhabricatorLiskDAO',
 | 
			
		||||
    'PhluxEditController' => 'PhluxController',
 | 
			
		||||
    'PhluxListController' => 'PhluxController',
 | 
			
		||||
    'PhluxSchemaSpec' => 'PhabricatorConfigSchemaSpec',
 | 
			
		||||
    'PhluxTransaction' => 'PhabricatorApplicationTransaction',
 | 
			
		||||
    'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
 | 
			
		||||
    'PhluxVariable' => array(
 | 
			
		||||
 
 | 
			
		||||
@@ -113,6 +113,7 @@ abstract class AphrontResponse extends Phobject {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      $cdn = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
 | 
			
		||||
      $base_uri = PhabricatorEnv::getURI('/');
 | 
			
		||||
    } catch (Exception $ex) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
@@ -124,8 +125,6 @@ abstract class AphrontResponse extends Phobject {
 | 
			
		||||
      // If an alternate file domain is not configured and the user is viewing
 | 
			
		||||
      // a Phame blog on a custom domain or some other custom site, we'll still
 | 
			
		||||
      // serve resources from the main site. Include the main site explicitly.
 | 
			
		||||
 | 
			
		||||
      $base_uri = PhabricatorEnv::getURI('/');
 | 
			
		||||
      $base_uri = $this->newContentSecurityPolicySource($base_uri);
 | 
			
		||||
 | 
			
		||||
      $default = "'self' {$base_uri}";
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -162,7 +162,7 @@ final class DifferentialRevisionAcceptTransaction
 | 
			
		||||
          'closed. Only open revisions can be accepted.'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($object->isDraft()) {
 | 
			
		||||
    if ($object->isDraft() || !$object->getShouldBroadcast()) {
 | 
			
		||||
      throw new Exception(
 | 
			
		||||
        pht('You can not accept a draft revision.'));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ final class DifferentialRevisionRejectTransaction
 | 
			
		||||
          'not own.'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($object->isDraft()) {
 | 
			
		||||
    if ($object->isDraft() || !$object->getShouldBroadcast()) {
 | 
			
		||||
      throw new Exception(
 | 
			
		||||
        pht('You can not request changes to a draft revision.'));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -2560,6 +2577,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);
 | 
			
		||||
@@ -2606,8 +2642,6 @@ abstract class PhabricatorApplicationTransactionEditor
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $this->runHeraldMailRules($messages);
 | 
			
		||||
 | 
			
		||||
    return $messages;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -2938,16 +2972,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;
 | 
			
		||||
@@ -2957,15 +3019,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);
 | 
			
		||||
@@ -2998,8 +3060,7 @@ abstract class PhabricatorApplicationTransactionEditor
 | 
			
		||||
        $object_label);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $xactions_style = array(
 | 
			
		||||
    );
 | 
			
		||||
    $xactions_style = array();
 | 
			
		||||
 | 
			
		||||
    $header_action = phutil_tag(
 | 
			
		||||
      'td',
 | 
			
		||||
@@ -3026,7 +3087,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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -3674,6 +3753,7 @@ abstract class PhabricatorApplicationTransactionEditor
 | 
			
		||||
      'mailMutedPHIDs',
 | 
			
		||||
      'webhookMap',
 | 
			
		||||
      'silent',
 | 
			
		||||
      'sendHistory',
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -4331,4 +4411,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,22 @@ 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.',
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
      'This draft revision will be sent for review once %s '.
 | 
			
		||||
      'build(s) pass: %s.' => array(
 | 
			
		||||
        'This draft revision will be sent for review once this build '.
 | 
			
		||||
        'passes: %2$s.',
 | 
			
		||||
        'This draft revision will be sent for review once these builds '.
 | 
			
		||||
        'pass: %2$s.',
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user