From 70bf63bc3adc0705ee3667d6bbf315643dea3f33 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Tue, 4 Dec 2018 05:50:54 -0800
Subject: [PATCH 1/8] Fix a transaction editor "continue;" inside "switch()"
 for PHP 7.3

Summary: See <https://discourse.phabricator-community.org/t/new-git-commit-processing-fails-on-php-7-3/>. This "continue" should be a "break".

Test Plan:
{F6045490}

  - Tried to assign a task to myself while I was already the owner, got an appropriate error.

Reviewers: amckinley

Reviewed By: amckinley

Differential Revision: https://secure.phabricator.com/D19844
---
 .../maniphest/editor/ManiphestTransactionEditor.php             | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 4b6c7707f6..47a6b1b4f2 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -649,7 +649,7 @@ final class ManiphestTransactionEditor
         $old_value = $object->getOwnerPHID();
         $new_value = $xaction->getNewValue();
         if ($old_value === $new_value) {
-          continue;
+          break;
         }
 
         // When a task is reassigned, move the old owner to the subscriber

From 5d54f26daca83875d31efda0ab9c52db7cd78652 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Wed, 5 Dec 2018 11:34:52 -0800
Subject: [PATCH 2/8] Support reading and querying Almanac service PHIDs via
 "diffusion.repository.search"

Summary:
Ref T13222. See PHI992. If you lose all hosts in a service cluster, you may need to get a list of affected repositories to figure out which backups to pull.

Support doing this via the API.

Test Plan: Queried by service PHID and saw service PHIDs in the call results.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13222

Differential Revision: https://secure.phabricator.com/D19848
---
 .../query/PhabricatorRepositorySearchEngine.php     | 13 +++++++++++++
 .../repository/storage/PhabricatorRepository.php    |  8 ++++++++
 2 files changed, 21 insertions(+)

diff --git a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
index 9b5b721edb..90a10e9872 100644
--- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
+++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
@@ -44,6 +44,15 @@ final class PhabricatorRepositorySearchEngine
         ->setKey('uris')
         ->setDescription(
           pht('Search for repositories by clone/checkout URI.')),
+      id(new PhabricatorPHIDsSearchField())
+        ->setLabel(pht('Services'))
+        ->setKey('almanacServicePHIDs')
+        ->setAliases(
+          array(
+            'almanacServicePHID',
+            'almanacService',
+            'almanacServices',
+          )),
     );
   }
 
@@ -80,6 +89,10 @@ final class PhabricatorRepositorySearchEngine
       $query->withURIs($map['uris']);
     }
 
+    if ($map['almanacServicePHIDs']) {
+      $query->withAlmanacServicePHIDs($map['almanacServicePHIDs']);
+    }
+
     return $query;
   }
 
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index 3b7918cd59..c961e06f27 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -2773,6 +2773,13 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
         ->setDescription(
           pht(
             'True if the repository is importing initial commits.')),
+      id(new PhabricatorConduitSearchFieldSpecification())
+        ->setKey('almanacServicePHID')
+        ->setType('phid?')
+        ->setDescription(
+          pht(
+            'The Almanac Service that hosts this repository, if the '.
+            'repository is clustered.')),
     );
   }
 
@@ -2784,6 +2791,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
       'shortName' => $this->getRepositorySlug(),
       'status' => $this->getStatus(),
       'isImporting' => (bool)$this->isImporting(),
+      'almanacServicePHID' => $this->getAlmanacServicePHID(),
     );
   }
 

From f0eefdd0b58b8f2a859637d3f1faeae50c60af09 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Thu, 6 Dec 2018 08:10:21 -0800
Subject: [PATCH 3/8] Replace the informal "array" subtype map with a more
 formal "SubtypeMap" object

Summary: Ref T13222. Ref T12588. See PHI683. To make "Create Subtask..." fancier, we need slightly more logic around subtype maps. Upgrade the plain old array into a proper object so it can have relevant methods, notably "get a list of valid child subtypes for some parent subtype".

Test Plan: Created and edited tasks, changed task subtypes. Grepped for affected symbols (`newEditEngineSubtypeMap`, `newSubtypeMap`).

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13222, T12588

Differential Revision: https://secure.phabricator.com/D19852
---
 src/__phutil_library_map__.php                |  2 +
 .../query/ManiphestTaskSearchEngine.php       |  2 +-
 .../maniphest/storage/ManiphestTask.php       |  2 +-
 .../storage/ManiphestTransaction.php          |  5 ++-
 .../ManiphestTaskSubtypeDatasource.php        |  2 +-
 .../maniphest/view/ManiphestTaskListView.php  |  3 --
 ...itEngineConfigurationSubtypeController.php |  3 +-
 .../PhabricatorEditEngineSubtype.php          |  2 +-
 .../PhabricatorEditEngineSubtypeMap.php       | 42 +++++++++++++++++++
 ...habricatorApplicationTransactionEditor.php |  2 +-
 ...abricatorEditEngineConfigurationEditor.php |  2 +-
 .../PhabricatorSubtypeEditEngineExtension.php |  4 +-
 12 files changed, 56 insertions(+), 15 deletions(-)
 create mode 100644 src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index e4ffb74b0f..aeba5e18ae 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2976,6 +2976,7 @@ phutil_register_library_map(array(
     'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php',
     'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php',
     'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php',
+    'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php',
     'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php',
     'PhabricatorEditEngineTokenizerCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineTokenizerCommentAction.php',
     'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php',
@@ -8729,6 +8730,7 @@ phutil_register_library_map(array(
     'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction',
     'PhabricatorEditEngineSubtype' => 'Phobject',
+    'PhabricatorEditEngineSubtypeMap' => 'Phobject',
     'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase',
     'PhabricatorEditEngineTokenizerCommentAction' => 'PhabricatorEditEngineCommentAction',
     'PhabricatorEditField' => 'Phobject',
diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
index 437362fa41..f4a1f2b344 100644
--- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
+++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
@@ -47,7 +47,7 @@ final class ManiphestTaskSearchEngine
     // Hide the "Subtypes" constraint from the web UI if the install only
     // defines one task subtype, since it isn't of any use in this case.
     $subtype_map = id(new ManiphestTask())->newEditEngineSubtypeMap();
-    $hide_subtypes = (count($subtype_map) == 1);
+    $hide_subtypes = ($subtype_map->getCount() == 1);
 
     return array(
       id(new PhabricatorOwnersSearchField())
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index a93fe58c3f..cdac563a14 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -573,7 +573,7 @@ final class ManiphestTask extends ManiphestDAO
   public function newSubtypeObject() {
     $subtype_key = $this->getEditEngineSubtype();
     $subtype_map = $this->newEditEngineSubtypeMap();
-    return idx($subtype_map, $subtype_key);
+    return $subtype_map->getSubtype($subtype_key);
   }
 
 /* -(  PhabricatorFulltextInterface  )--------------------------------------- */
diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php
index e2739c1d09..297bb970d1 100644
--- a/src/applications/maniphest/storage/ManiphestTransaction.php
+++ b/src/applications/maniphest/storage/ManiphestTransaction.php
@@ -214,11 +214,12 @@ final class ManiphestTransaction
   public function renderSubtypeName($value) {
     $object = $this->getObject();
     $map = $object->newEditEngineSubtypeMap();
-    if (!isset($map[$value])) {
+
+    if (!$map->isValidSubtype($value)) {
       return $value;
     }
 
-    return $map[$value]->getName();
+    return $map->getSubtype($value)->getName();
   }
 
 }
diff --git a/src/applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php b/src/applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php
index c061c694e4..cfa5592ccc 100644
--- a/src/applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php
+++ b/src/applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php
@@ -28,7 +28,7 @@ final class ManiphestTaskSubtypeDatasource
     $results = array();
 
     $subtype_map = id(new ManiphestTask())->newEditEngineSubtypeMap();
-    foreach ($subtype_map as $key => $subtype) {
+    foreach ($subtype_map->getSubtypes() as $key => $subtype) {
 
       $result = id(new PhabricatorTypeaheadResult())
         ->setIcon($subtype->getIcon())
diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php
index ba17b8e25d..e7128fb850 100644
--- a/src/applications/maniphest/view/ManiphestTaskListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskListView.php
@@ -56,9 +56,6 @@ final class ManiphestTaskListView extends ManiphestView {
       Javelin::initBehavior('maniphest-list-editor');
     }
 
-    $subtype_map = id(new ManiphestTask())
-      ->newEditEngineSubtypeMap();
-
     foreach ($this->tasks as $task) {
       $item = id(new PHUIObjectItemView())
         ->setUser($this->getUser())
diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php
index 92bc34c72b..9ce8d7a0e5 100644
--- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php
+++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php
@@ -61,8 +61,7 @@ Choose the object **subtype** that this form should create and edit.
 EOTEXT
       );
 
-    $map = $engine->newSubtypeMap();
-    $map = mpull($map, 'getName');
+    $map = $engine->newSubtypeMap()->getDisplayMap();
 
     $form = id(new AphrontFormView())
       ->setUser($viewer)
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
index df367955af..23fac106d7 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
@@ -182,7 +182,7 @@ final class PhabricatorEditEngineSubtype
       $map[$key] = $subtype;
     }
 
-    return $map;
+    return new PhabricatorEditEngineSubtypeMap($map);
   }
 
 }
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php
new file mode 100644
index 0000000000..cd8e0674ae
--- /dev/null
+++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php
@@ -0,0 +1,42 @@
+<?php
+
+
+final class PhabricatorEditEngineSubtypeMap
+  extends Phobject {
+
+  private $subtypes;
+
+  public function __construct(array $subtypes) {
+    assert_instances_of($subtypes, 'PhabricatorEditEngineSubtype');
+
+    $this->subtypes = $subtypes;
+  }
+
+  public function getDisplayMap() {
+    return mpull($this->subtypes, 'getName');
+  }
+
+  public function getCount() {
+    return count($this->subtypes);
+  }
+
+  public function isValidSubtype($subtype_key) {
+    return isset($this->subtypes[$subtype_key]);
+  }
+
+  public function getSubtypes() {
+    return $this->subtypes;
+  }
+
+  public function getSubtype($subtype_key) {
+    if (!$this->isValidSubtype($subtype_key)) {
+      throw new Exception(
+        pht(
+          'Subtype key "%s" does not identify a valid subtype.',
+          $subtype_key));
+    }
+
+    return $this->subtypes[$subtype_key];
+  }
+
+}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index a77b7f84a4..f354de6a1a 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -2541,7 +2541,7 @@ abstract class PhabricatorApplicationTransactionEditor
         continue;
       }
 
-      if (!isset($map[$new])) {
+      if (!$map->isValidSubtype($new)) {
         $errors[] = new PhabricatorApplicationTransactionValidationError(
           $transaction_type,
           pht('Invalid'),
diff --git a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php
index d394e87266..ccadf9b819 100644
--- a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php
+++ b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php
@@ -64,7 +64,7 @@ final class PhabricatorEditEngineConfigurationEditor
           foreach ($xactions as $xaction) {
             $new = $xaction->getNewValue();
 
-            if (isset($map[$new])) {
+            if ($map->isValidSubtype($new)) {
               continue;
             }
 
diff --git a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
index 7d32545416..d0b6d017f3 100644
--- a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
+++ b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
@@ -31,7 +31,7 @@ final class PhabricatorSubtypeEditEngineExtension
     $subtype_type = PhabricatorTransactions::TYPE_SUBTYPE;
 
     $map = $object->newEditEngineSubtypeMap();
-    $options = mpull($map, 'getName');
+    $options = $map->getDisplayMap();
 
     $subtype_field = id(new PhabricatorSelectEditField())
       ->setKey(self::EDITKEY)
@@ -45,7 +45,7 @@ final class PhabricatorSubtypeEditEngineExtension
 
     // If subtypes are configured, enable changing them from the bulk editor
     // and comment action stack.
-    if (count($map) > 1) {
+    if ($map->getCount() > 1) {
       $subtype_field
         ->setBulkEditLabel(pht('Change subtype to'))
         ->setCommentActionLabel(pht('Change Subtype'))

From b88a87c43a28a6948a4fd95e7f12a5e4ffdd22c0 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Wed, 5 Dec 2018 05:49:09 -0800
Subject: [PATCH 4/8] Address a transaction issue with some audit actions not
 applying correctly

Summary:
See <https://discourse.phabricator-community.org/t/cannot-accept-commits-in-audit/2166/>.

In D19842, I changed `PhabricatorEditField->shouldGenerateTransactionsFromComment()`.

  - Previously, it bailed on `getIsConduitOnly()`.
  - After the patch, it bails on a missing `getCommentActionLabel()`.

The old code was actually wrong, and it was previously possible to apply possibly-invalid actions in some cases (or, at least, sneak them through this layer: they would only actually apply if not validated properly).

In practice, it let a different bug through: we sometimes loaded commits without loading their audit authority, so testing whether the viewer could "Accept" the commit or not (or take some other actions like "Raise Concern") would always fail and throw an exception: "Trying to access data not attached to this object..."

Fixing the insufficiently-strict transaction generation code exposed the "authority not attached" bug, which caused some actions to fail to generate transactions.

This appeared in the UI as either an unhelpful error ("You can't post an empty comment") or an action with no effect. The unhelpful error was because we show that error if you aren't taking any //other// actions, and we wouldn't generate an "Accept" action because of the interaction of these bugs, so the code thought you were just posting an empty comment.

Test Plan: Without leaving comments, accepted and rejected commits. No more error messages, and actions took effect.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: stephan.senkbeil, hskiba

Differential Revision: https://secure.phabricator.com/D19845
---
 .../audit/editor/PhabricatorAuditEditor.php   |  4 -
 .../controller/DiffusionCommitController.php  |  2 +-
 .../editor/DiffusionCommitEditEngine.php      |  5 +-
 .../diffusion/query/DiffusionCommitQuery.php  | 92 ++++++++++++++++++-
 .../storage/PhabricatorRepositoryCommit.php   | 80 +++-------------
 5 files changed, 108 insertions(+), 75 deletions(-)

diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php
index cfa84b8bd7..3f65cc80f8 100644
--- a/src/applications/audit/editor/PhabricatorAuditEditor.php
+++ b/src/applications/audit/editor/PhabricatorAuditEditor.php
@@ -59,10 +59,6 @@ final class PhabricatorAuditEditor
 
     $this->oldAuditStatus = $object->getAuditStatus();
 
-    $object->loadAndAttachAuditAuthority(
-      $this->getActor(),
-      $this->getActingAsPHID());
-
     return parent::expandTransactions($object, $xactions);
   }
 
diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php
index 283278fcf8..5621b1fa12 100644
--- a/src/applications/diffusion/controller/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitController.php
@@ -45,6 +45,7 @@ final class DiffusionCommitController extends DiffusionController {
       ->withIdentifiers(array($commit_identifier))
       ->needCommitData(true)
       ->needAuditRequests(true)
+      ->needAuditAuthority(array($viewer))
       ->setLimit(100)
       ->needIdentities(true)
       ->execute();
@@ -111,7 +112,6 @@ final class DiffusionCommitController extends DiffusionController {
     }
 
     $audit_requests = $commit->getAudits();
-    $commit->loadAndAttachAuditAuthority($viewer);
 
     $commit_data = $commit->getCommitData();
     $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub');
diff --git a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php
index 3470d02f92..8392504def 100644
--- a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php
+++ b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php
@@ -43,9 +43,12 @@ final class DiffusionCommitEditEngine
   }
 
   protected function newObjectQuery() {
+    $viewer = $this->getViewer();
+
     return id(new DiffusionCommitQuery())
       ->needCommitData(true)
-      ->needAuditRequests(true);
+      ->needAuditRequests(true)
+      ->needAuditAuthority(array($viewer));
   }
 
   protected function getEditorURI() {
diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php
index cf8c25baf1..05072e07c7 100644
--- a/src/applications/diffusion/query/DiffusionCommitQuery.php
+++ b/src/applications/diffusion/query/DiffusionCommitQuery.php
@@ -17,6 +17,7 @@ final class DiffusionCommitQuery
   private $unreachable;
 
   private $needAuditRequests;
+  private $needAuditAuthority;
   private $auditIDs;
   private $auditorPHIDs;
   private $epochMin;
@@ -121,6 +122,12 @@ final class DiffusionCommitQuery
     return $this;
   }
 
+  public function needAuditAuthority(array $users) {
+    assert_instances_of($users, 'PhabricatorUser');
+    $this->needAuditAuthority = $users;
+    return $this;
+  }
+
   public function withAuditIDs(array $ids) {
     $this->auditIDs = $ids;
     return $this;
@@ -231,14 +238,27 @@ final class DiffusionCommitQuery
     }
 
     if (count($subqueries) > 1) {
-      foreach ($subqueries as $key => $subquery) {
-        $subqueries[$key] = '('.$subquery.')';
+      $unions = null;
+      foreach ($subqueries as $subquery) {
+        if (!$unions) {
+          $unions = qsprintf(
+            $conn,
+            '(%Q)',
+            $subquery);
+          continue;
+        }
+
+        $unions = qsprintf(
+          $conn,
+          '%Q UNION DISTINCT (%Q)',
+          $unions,
+          $subquery);
       }
 
       $query = qsprintf(
         $conn,
         '%Q %Q %Q',
-        implode(' UNION DISTINCT ', $subqueries),
+        $unions,
         $this->buildOrderClause($conn, true),
         $this->buildLimitClause($conn));
     } else {
@@ -423,6 +443,72 @@ final class DiffusionCommitQuery
         $commits);
     }
 
+    if ($this->needAuditAuthority) {
+      $authority_users = $this->needAuditAuthority;
+
+      // NOTE: This isn't very efficient since we're running two queries per
+      // user, but there's currently no way to figure out authority for
+      // multiple users in one query. Today, we only ever request authority for
+      // a single user and single commit, so this has no practical impact.
+
+      // NOTE: We're querying with the viewership of query viewer, not the
+      // actual users. If the viewer can't see a project or package, they
+      // won't be able to see who has authority on it. This is safer than
+      // showing them true authority, and should never matter today, but it
+      // also doesn't seem like a significant disclosure and might be
+      // reasonable to adjust later if it causes something weird or confusing
+      // to happen.
+
+      $authority_map = array();
+      foreach ($authority_users as $authority_user) {
+        $authority_phid = $authority_user->getPHID();
+        if (!$authority_phid) {
+          continue;
+        }
+
+        $result_phids = array();
+
+        // Users have authority over themselves.
+        $result_phids[] = $authority_phid;
+
+        // Users have authority over packages they own.
+        $owned_packages = id(new PhabricatorOwnersPackageQuery())
+          ->setViewer($viewer)
+          ->withAuthorityPHIDs(array($authority_phid))
+          ->execute();
+        foreach ($owned_packages as $package) {
+          $result_phids[] = $package->getPHID();
+        }
+
+        // Users have authority over projects they're members of.
+        $projects = id(new PhabricatorProjectQuery())
+          ->setViewer($viewer)
+          ->withMemberPHIDs(array($authority_phid))
+          ->execute();
+        foreach ($projects as $project) {
+          $result_phids[] = $project->getPHID();
+        }
+
+        $result_phids = array_fuse($result_phids);
+
+        foreach ($commits as $commit) {
+          $attach_phids = $result_phids;
+
+          // NOTE: When modifying your own commits, you act only on behalf of
+          // yourself, not your packages or projects. The idea here is that you
+          // can't accept your own commits. In the future, this might change or
+          // depend on configuration.
+          $author_phid = $commit->getAuthorPHID();
+          if ($author_phid == $authority_phid) {
+            $attach_phids = array($author_phid);
+            $attach_phids = array_fuse($attach_phids);
+          }
+
+          $commit->attachAuditAuthority($authority_user, $attach_phids);
+        }
+      }
+    }
+
     return $commits;
   }
 
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
index 5615b5231f..b5fe08b6e0 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
@@ -210,84 +210,32 @@ final class PhabricatorRepositoryCommit
     return $this->assertAttached($this->committerIdentity);
   }
 
-  public function loadAndAttachAuditAuthority(
-    PhabricatorUser $viewer,
-    $actor_phid = null) {
+  public function attachAuditAuthority(
+    PhabricatorUser $user,
+    array $authority) {
 
-    if ($actor_phid === null) {
-      $actor_phid = $viewer->getPHID();
+    $user_phid = $user->getPHID();
+    if (!$user->getPHID()) {
+      throw new Exception(
+        pht('You can not attach audit authority for a user with no PHID.'));
     }
 
-    // TODO: This method is a little weird and sketchy, but worlds better than
-    // what came before it. Eventually, this should probably live in a Query
-    // class.
-
-    // Figure out which requests the actor has authority over: these are user
-    // requests where they are the auditor, and packages and projects they are
-    // a member of.
-
-    if (!$actor_phid) {
-      $attach_key = $viewer->getCacheFragment();
-      $phids = array();
-    } else {
-      $attach_key = $actor_phid;
-      // At least currently, when modifying your own commits, you act only on
-      // behalf of yourself, not your packages/projects -- the idea being that
-      // you can't accept your own commits. This may change or depend on
-      // config.
-      $actor_is_author = ($actor_phid == $this->getAuthorPHID());
-      if ($actor_is_author) {
-        $phids = array($actor_phid);
-      } else {
-        $phids = array();
-        $phids[$actor_phid] = true;
-
-        $owned_packages = id(new PhabricatorOwnersPackageQuery())
-          ->setViewer($viewer)
-          ->withAuthorityPHIDs(array($actor_phid))
-          ->execute();
-        foreach ($owned_packages as $package) {
-          $phids[$package->getPHID()] = true;
-        }
-
-        $projects = id(new PhabricatorProjectQuery())
-          ->setViewer($viewer)
-          ->withMemberPHIDs(array($actor_phid))
-          ->execute();
-        foreach ($projects as $project) {
-          $phids[$project->getPHID()] = true;
-        }
-
-        $phids = array_keys($phids);
-      }
-    }
-
-    $this->auditAuthorityPHIDs[$attach_key] = array_fuse($phids);
+    $this->auditAuthorityPHIDs[$user_phid] = $authority;
 
     return $this;
   }
 
   public function hasAuditAuthority(
-    PhabricatorUser $viewer,
-    PhabricatorRepositoryAuditRequest $audit,
-    $actor_phid = null) {
+    PhabricatorUser $user,
+    PhabricatorRepositoryAuditRequest $audit) {
 
-    if ($actor_phid === null) {
-      $actor_phid = $viewer->getPHID();
-    }
-
-    if (!$actor_phid) {
-      $attach_key = $viewer->getCacheFragment();
-    } else {
-      $attach_key = $actor_phid;
-    }
-
-    $map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $attach_key);
-
-    if (!$actor_phid) {
+    $user_phid = $user->getPHID();
+    if (!$user_phid) {
       return false;
     }
 
+    $map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $user_phid);
+
     return isset($map[$audit->getAuditorPHID()]);
   }
 

From 1029081b28051b7fb82d5eac89c6e40d1a6e6be4 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Wed, 5 Dec 2018 06:12:08 -0800
Subject: [PATCH 5/8] Correct two straggling "%Q" + "implode(...)" callsites in
 Revision updates

Summary:
See <https://discourse.phabricator-community.org/t/error-seems-to-be-related-with-da40f8074-and-php-7-3/2102/12>. When creating or updating revisions, we do some manual query construction to update the affected path table.

Update these queries to modern `qsprintf()`.

Test Plan: Created and updated revisions affecting paths, no more logs in the webserver log.

Reviewers: amckinley

Reviewed By: amckinley

Differential Revision: https://secure.phabricator.com/D19846
---
 .../differential/editor/DifferentialTransactionEditor.php | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index 7fa4cd61e2..853b024e27 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1367,9 +1367,9 @@ final class DifferentialTransactionEditor
     foreach (array_chunk($sql, 256) as $chunk) {
       queryfx(
         $conn_w,
-        'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
+        'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %LQ',
         $table->getTableName(),
-        implode(', ', $chunk));
+        $chunk);
     }
   }
 
@@ -1444,9 +1444,9 @@ final class DifferentialTransactionEditor
     if ($sql) {
       queryfx(
         $conn_w,
-        'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
+        'INSERT INTO %T (revisionID, type, hash) VALUES %LQ',
         ArcanistDifferentialRevisionHash::TABLE_NAME,
-        implode(', ', $sql));
+        $sql);
     }
   }
 

From 1e4bdc39a11beb2fe39a53e13d35773786ef0306 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Wed, 5 Dec 2018 12:40:59 -0800
Subject: [PATCH 6/8] Add an "availaiblity" attachment for user.search

Summary: Ref T13222. See PHI990. The older `user.query` supports availability information, but it isn't currently available in a modern way. Make it available.

Test Plan: {F6048126}

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13222

Differential Revision: https://secure.phabricator.com/D19851
---
 src/__phutil_library_map__.php                |  2 +
 ...opleAvailabilitySearchEngineAttachment.php | 46 +++++++++++++++++++
 .../people/storage/PhabricatorUser.php        |  5 +-
 3 files changed, 52 insertions(+), 1 deletion(-)
 create mode 100644 src/applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index aeba5e18ae..cd260f497d 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -3761,6 +3761,7 @@ phutil_register_library_map(array(
     'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php',
     'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php',
     'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php',
+    'PhabricatorPeopleAvailabilitySearchEngineAttachment' => 'applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php',
     'PhabricatorPeopleBadgesProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php',
     'PhabricatorPeopleCommitsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php',
     'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php',
@@ -9640,6 +9641,7 @@ phutil_register_library_map(array(
     'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorPeopleApplication' => 'PhabricatorApplication',
     'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController',
+    'PhabricatorPeopleAvailabilitySearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
     'PhabricatorPeopleBadgesProfileMenuItem' => 'PhabricatorProfileMenuItem',
     'PhabricatorPeopleCommitsProfileMenuItem' => 'PhabricatorProfileMenuItem',
     'PhabricatorPeopleController' => 'PhabricatorController',
diff --git a/src/applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php b/src/applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php
new file mode 100644
index 0000000000..f61ebe7f01
--- /dev/null
+++ b/src/applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php
@@ -0,0 +1,46 @@
+<?php
+
+final class PhabricatorPeopleAvailabilitySearchEngineAttachment
+  extends PhabricatorSearchEngineAttachment {
+
+  public function getAttachmentName() {
+    return pht('User Availability');
+  }
+
+  public function getAttachmentDescription() {
+    return pht('Get availability information for users.');
+  }
+
+  public function willLoadAttachmentData($query, $spec) {
+    $query->needAvailability(true);
+  }
+
+  public function getAttachmentForObject($object, $data, $spec) {
+
+    $until = $object->getAwayUntil();
+    if ($until) {
+      $until = (int)$until;
+    } else {
+      $until = null;
+    }
+
+    $value = $object->getDisplayAvailability();
+    if ($value === null) {
+      $value = PhabricatorCalendarEventInvitee::AVAILABILITY_AVAILABLE;
+    }
+
+    $name = PhabricatorCalendarEventInvitee::getAvailabilityName($value);
+    $color = PhabricatorCalendarEventInvitee::getAvailabilityColor($value);
+
+    $event_phid = $object->getAvailabilityEventPHID();
+
+    return array(
+      'value' => $value,
+      'until' => $until,
+      'name' => $name,
+      'color' => $color,
+      'eventPHID' => $event_phid,
+    );
+  }
+
+}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index 651d1b75a6..7717fbf18c 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1466,7 +1466,10 @@ final class PhabricatorUser
   }
 
   public function getConduitSearchAttachments() {
-    return array();
+    return array(
+      id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())
+        ->setAttachmentKey('availability'),
+    );
   }
 
 

From bba4186005917d7325e9888a5ff7b989061889f9 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Wed, 5 Dec 2018 11:51:15 -0800
Subject: [PATCH 7/8] Allow "bin/repository thaw" to accept
 "--all-repositories" instead of a list of repositories

Summary:
Ref T13222. See PHI992. If you've lost an entire cluster (or have lost a device and are willing to make broad assumptions about the state the device was in) you currently have to `xargs` to thaw everything or do something else creative.

Since this workflow is broadly reasonable, provide an easier way to accomplish the goal.

Test Plan:
  - Ran with `--all-repositories`, a list of repositories, both (error) and neither (error).
  - Saw a helpful new list of affected repositories.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13222

Differential Revision: https://secure.phabricator.com/D19849
---
 ...icatorRepositoryManagementThawWorkflow.php | 66 +++++++++++++++++--
 1 file changed, 60 insertions(+), 6 deletions(-)

diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php
index 4c4eed85b9..ed2e1df3e2 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php
@@ -32,6 +32,12 @@ final class PhabricatorRepositoryManagementThawWorkflow
             'name' => 'force',
             'help' => pht('Run operations without asking for confirmation.'),
           ),
+          array(
+            'name' => 'all-repositories',
+            'help' => pht(
+              'Apply the promotion or demotion to all repositories hosted '.
+              'on the device.'),
+          ),
           array(
             'name' => 'repositories',
             'wildcard' => true,
@@ -42,12 +48,6 @@ final class PhabricatorRepositoryManagementThawWorkflow
   public function execute(PhutilArgumentParser $args) {
     $viewer = $this->getViewer();
 
-    $repositories = $this->loadRepositories($args, 'repositories');
-    if (!$repositories) {
-      throw new PhutilArgumentUsageException(
-        pht('Specify one or more repositories to thaw.'));
-    }
-
     $promote = $args->getArg('promote');
     $demote = $args->getArg('demote');
 
@@ -72,6 +72,60 @@ final class PhabricatorRepositoryManagementThawWorkflow
         pht('No device "%s" exists.', $device_name));
     }
 
+    $repository_names = $args->getArg('repositories');
+    $all_repositories = $args->getArg('all-repositories');
+    if ($repository_names && $all_repositories) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Specify a list of repositories or "--all-repositories", '.
+          'but not both.'));
+    } else if (!$repository_names && !$all_repositories) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Select repositories to affect by providing a list of repositories '.
+          'or using the "--all-repositories" flag.'));
+    }
+
+    if ($repository_names) {
+      $repositories = $this->loadRepositories($args, 'repositories');
+      if (!$repositories) {
+        throw new PhutilArgumentUsageException(
+          pht('Specify one or more repositories to thaw.'));
+      }
+    } else {
+      $repositories = array();
+
+      $services = id(new AlmanacServiceQuery())
+        ->setViewer($viewer)
+        ->withDevicePHIDs(array($device->getPHID()))
+        ->execute();
+      if ($services) {
+        $repositories = id(new PhabricatorRepositoryQuery())
+          ->setViewer($viewer)
+          ->withAlmanacServicePHIDs(mpull($services, 'getPHID'))
+          ->execute();
+      }
+
+      if (!$repositories) {
+        throw new PhutilArgumentUsageException(
+          pht('There are no repositories on the selected device.'));
+      }
+    }
+
+    $display_list = new PhutilConsoleList();
+    foreach ($repositories as $repository) {
+      $display_list->addItem(
+        pht(
+          '%s %s',
+          $repository->getMonogram(),
+          $repository->getName()));
+    }
+
+    echo tsprintf(
+      "%s\n\n%B\n",
+      pht('These repositories will be thawed:'),
+      $display_list->drawConsoleString());
+
     if ($promote) {
       $risk_message = pht(
         'Promoting a device can cause the loss of any repository data which '.

From 1a6a0181a8985371fe1470cbea24463885850668 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Wed, 5 Dec 2018 12:22:14 -0800
Subject: [PATCH 8/8] Allow "bin/repository thaw --demote" to demote an entire
 service, not just a single device

Summary: Ref T13222. See PHI992. If you lose an entire cluster, you may want to aggressively demote it out of existence. You currently need to `xargs` your way through this. Allow `--demote <service>`, which demotes all devices in a service.

Test Plan: Demoted with `--demote <device>` and `--demote <service>`. Hit the `--promote service` error.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13222

Differential Revision: https://secure.phabricator.com/D19850
---
 ...icatorRepositoryManagementThawWorkflow.php | 263 ++++++++++--------
 .../user/cluster/cluster_repositories.diviner |  12 +
 2 files changed, 164 insertions(+), 111 deletions(-)

diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php
index ed2e1df3e2..3743045c34 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php
@@ -15,10 +15,11 @@ final class PhabricatorRepositoryManagementThawWorkflow
         array(
           array(
             'name' => 'demote',
-            'param' => 'device',
+            'param' => 'device/service',
             'help' => pht(
-              'Demote a device, discarding local changes. Clears stuck '.
-              'write locks and recovers from lost leaders.'),
+              'Demote a device (or all devices in a service) discarding '.
+              'local changes. Clears stuck write locks and recovers from '.
+              'lost leaders.'),
           ),
           array(
             'name' => 'promote',
@@ -61,15 +62,53 @@ final class PhabricatorRepositoryManagementThawWorkflow
         pht('Specify either --promote or --demote, but not both.'));
     }
 
-    $device_name = nonempty($promote, $demote);
+    $target_name = nonempty($promote, $demote);
 
-    $device = id(new AlmanacDeviceQuery())
+    $devices = id(new AlmanacDeviceQuery())
       ->setViewer($viewer)
-      ->withNames(array($device_name))
-      ->executeOne();
-    if (!$device) {
-      throw new PhutilArgumentUsageException(
-        pht('No device "%s" exists.', $device_name));
+      ->withNames(array($target_name))
+      ->execute();
+    if (!$devices) {
+      $service = id(new AlmanacServiceQuery())
+        ->setViewer($viewer)
+        ->withNames(array($target_name))
+        ->executeOne();
+
+      if (!$service) {
+        throw new PhutilArgumentUsageException(
+          pht('No device or service named "%s" exists.', $target_name));
+      }
+
+      if ($promote) {
+        throw new PhutilArgumentUsageException(
+          pht(
+            'You can not "--promote" an entire service ("%s"). Only a single '.
+            'device may be promoted.',
+            $target_name));
+      }
+
+      $bindings = id(new AlmanacBindingQuery())
+        ->setViewer($viewer)
+        ->withServicePHIDs(array($service->getPHID()))
+        ->execute();
+      if (!$bindings) {
+        throw new PhutilArgumentUsageException(
+          pht(
+            'Service "%s" is not bound to any devices.',
+            $target_name));
+      }
+
+      $interfaces = id(new AlmanacInterfaceQuery())
+        ->setViewer($viewer)
+        ->withPHIDs(mpull($bindings, 'getInterfacePHID'))
+        ->execute();
+
+      $device_phids = mpull($interfaces, 'getDevicePHID');
+
+      $devices = id(new AlmanacDeviceQuery())
+        ->setViewer($viewer)
+        ->withPHIDs($device_phids)
+        ->execute();
     }
 
     $repository_names = $args->getArg('repositories');
@@ -97,7 +136,7 @@ final class PhabricatorRepositoryManagementThawWorkflow
 
       $services = id(new AlmanacServiceQuery())
         ->setViewer($viewer)
-        ->withDevicePHIDs(array($device->getPHID()))
+        ->withDevicePHIDs(mpull($devices, 'getPHID'))
         ->execute();
       if ($services) {
         $repositories = id(new PhabricatorRepositoryQuery())
@@ -108,7 +147,7 @@ final class PhabricatorRepositoryManagementThawWorkflow
 
       if (!$repositories) {
         throw new PhutilArgumentUsageException(
-          pht('There are no repositories on the selected device.'));
+          pht('There are no repositories on the selected device or service.'));
       }
     }
 
@@ -150,126 +189,128 @@ final class PhabricatorRepositoryManagementThawWorkflow
         pht('User aborted the workflow.'));
     }
 
-    foreach ($repositories as $repository) {
-      $repository_phid = $repository->getPHID();
+    foreach ($devices as $device) {
+      foreach ($repositories as $repository) {
+        $repository_phid = $repository->getPHID();
 
-      $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock(
-        $repository_phid);
+        $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock(
+          $repository_phid);
 
-      echo tsprintf(
-        "%s\n",
-        pht(
-          'Waiting to acquire write lock for "%s"...',
-          $repository->getDisplayName()));
+        echo tsprintf(
+          "%s\n",
+          pht(
+            'Waiting to acquire write lock for "%s"...',
+            $repository->getDisplayName()));
 
-      $write_lock->lock(phutil_units('5 minutes in seconds'));
-      try {
+        $write_lock->lock(phutil_units('5 minutes in seconds'));
+        try {
 
-        $service = $repository->loadAlmanacService();
-        if (!$service) {
-          throw new PhutilArgumentUsageException(
-            pht(
-              'Repository "%s" is not a cluster repository: it is not '.
-              'bound to an Almanac service.',
-              $repository->getDisplayName()));
-        }
-
-        if ($promote) {
-          // You can only promote active devices. (You may demote active or
-          // inactive devices.)
-          $bindings = $service->getActiveBindings();
-          $bindings = mpull($bindings, null, 'getDevicePHID');
-          if (empty($bindings[$device->getPHID()])) {
+          $service = $repository->loadAlmanacService();
+          if (!$service) {
             throw new PhutilArgumentUsageException(
               pht(
-                'Repository "%s" has no active binding to device "%s". Only '.
-                'actively bound devices can be promoted.',
-                $repository->getDisplayName(),
-                $device->getName()));
+                'Repository "%s" is not a cluster repository: it is not '.
+                'bound to an Almanac service.',
+                $repository->getDisplayName()));
           }
 
-          $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
-            $repository->getPHID());
-          $versions = mpull($versions, null, 'getDevicePHID');
-
-          // Before we promote, make sure there are no outstanding versions on
-          // devices with inactive bindings. If there are, you need to demote
-          // these first.
-          $inactive = array();
-          foreach ($versions as $device_phid => $version) {
-            if (isset($bindings[$device_phid])) {
-              continue;
+          if ($promote) {
+            // You can only promote active devices. (You may demote active or
+            // inactive devices.)
+            $bindings = $service->getActiveBindings();
+            $bindings = mpull($bindings, null, 'getDevicePHID');
+            if (empty($bindings[$device->getPHID()])) {
+              throw new PhutilArgumentUsageException(
+                pht(
+                  'Repository "%s" has no active binding to device "%s". '.
+                  'Only actively bound devices can be promoted.',
+                  $repository->getDisplayName(),
+                  $device->getName()));
             }
-            $inactive[$device_phid] = $version;
-          }
 
-          if ($inactive) {
-            $handles = $viewer->loadHandles(array_keys($inactive));
+            $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
+              $repository->getPHID());
+            $versions = mpull($versions, null, 'getDevicePHID');
 
-            $handle_list = iterator_to_array($handles);
-            $handle_list = mpull($handle_list, 'getName');
-            $handle_list = implode(', ', $handle_list);
+            // Before we promote, make sure there are no outstanding versions
+            // on devices with inactive bindings. If there are, you need to
+            // demote these first.
+            $inactive = array();
+            foreach ($versions as $device_phid => $version) {
+              if (isset($bindings[$device_phid])) {
+                continue;
+              }
+              $inactive[$device_phid] = $version;
+            }
 
-            throw new PhutilArgumentUsageException(
+            if ($inactive) {
+              $handles = $viewer->loadHandles(array_keys($inactive));
+
+              $handle_list = iterator_to_array($handles);
+              $handle_list = mpull($handle_list, 'getName');
+              $handle_list = implode(', ', $handle_list);
+
+              throw new PhutilArgumentUsageException(
+                pht(
+                  'Repository "%s" has versions on inactive devices. Demote '.
+                  '(or reactivate) these devices before promoting a new '.
+                  'leader: %s.',
+                  $repository->getDisplayName(),
+                  $handle_list));
+            }
+
+            // Now, make sure there are no outstanding versions on devices with
+            // active bindings. These also need to be demoted (or promoting is
+            // a mistake or already happened).
+            $active = array_select_keys($versions, array_keys($bindings));
+            if ($active) {
+              $handles = $viewer->loadHandles(array_keys($active));
+
+              $handle_list = iterator_to_array($handles);
+              $handle_list = mpull($handle_list, 'getName');
+              $handle_list = implode(', ', $handle_list);
+
+              throw new PhutilArgumentUsageException(
+                pht(
+                  'Unable to promote "%s" for repository "%s" because this '.
+                  'cluster already has one or more unambiguous leaders: %s.',
+                  $device->getName(),
+                  $repository->getDisplayName(),
+                  $handle_list));
+            }
+
+            PhabricatorRepositoryWorkingCopyVersion::updateVersion(
+              $repository->getPHID(),
+              $device->getPHID(),
+              0);
+
+            echo tsprintf(
+              "%s\n",
               pht(
-                'Repository "%s" has versions on inactive devices. Demote '.
-                '(or reactivate) these devices before promoting a new '.
-                'leader: %s.',
-                $repository->getDisplayName(),
-                $handle_list));
-          }
-
-          // Now, make sure there are no outstanding versions on devices with
-          // active bindings. These also need to be demoted (or promoting is a
-          // mistake or already happened).
-          $active = array_select_keys($versions, array_keys($bindings));
-          if ($active) {
-            $handles = $viewer->loadHandles(array_keys($active));
-
-            $handle_list = iterator_to_array($handles);
-            $handle_list = mpull($handle_list, 'getName');
-            $handle_list = implode(', ', $handle_list);
-
-            throw new PhutilArgumentUsageException(
-              pht(
-                'Unable to promote "%s" for repository "%s" because this '.
-                'cluster already has one or more unambiguous leaders: %s.',
+                'Promoted "%s" to become a leader for "%s".',
                 $device->getName(),
-                $repository->getDisplayName(),
-                $handle_list));
+                $repository->getDisplayName()));
           }
 
-          PhabricatorRepositoryWorkingCopyVersion::updateVersion(
-            $repository->getPHID(),
-            $device->getPHID(),
-            0);
+          if ($demote) {
+            PhabricatorRepositoryWorkingCopyVersion::demoteDevice(
+              $repository->getPHID(),
+              $device->getPHID());
 
-          echo tsprintf(
-            "%s\n",
-            pht(
-              'Promoted "%s" to become a leader for "%s".',
-              $device->getName(),
-              $repository->getDisplayName()));
+            echo tsprintf(
+              "%s\n",
+              pht(
+                'Demoted "%s" from leadership of repository "%s".',
+                $device->getName(),
+                $repository->getDisplayName()));
+          }
+        } catch (Exception $ex) {
+          $write_lock->unlock();
+          throw $ex;
         }
 
-        if ($demote) {
-          PhabricatorRepositoryWorkingCopyVersion::demoteDevice(
-            $repository->getPHID(),
-            $device->getPHID());
-
-          echo tsprintf(
-            "%s\n",
-            pht(
-              'Demoted "%s" from leadership of repository "%s".',
-              $device->getName(),
-              $repository->getDisplayName()));
-        }
-      } catch (Exception $ex) {
         $write_lock->unlock();
-        throw $ex;
       }
-
-      $write_lock->unlock();
     }
 
     return 0;
diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner
index cdf2641933..13307d0b79 100644
--- a/src/docs/user/cluster/cluster_repositories.diviner
+++ b/src/docs/user/cluster/cluster_repositories.diviner
@@ -433,6 +433,18 @@ If you do this, **you will lose unreplicated data**. You will discard any
 changes on the affected leaders which have not replicated to other devices
 in the cluster.
 
+If you have lost an entire cluster and replaced it with new devices that you
+have restored from backups, you can aggressively wipe all memory of the old
+devices by using `--demote <service>` and `--all-repositories`. **This is
+dangerous and discards all unreplicated data in any repository on any device.**
+
+```
+phabricator/ $ ./bin/repository thaw --demote repo.corp.net --all-repositories
+```
+
+After you do this, continue below to promote a leader and restore the cluster
+to service.
+
 
 Ambiguous Leaders
 =================