diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php
--- a/src/land/engine/ArcanistGitLandEngine.php
+++ b/src/land/engine/ArcanistGitLandEngine.php
@@ -1349,7 +1349,7 @@
       }
 
       $commits = phutil_split_lines($commits, false);
-      $is_first = false;
+      $is_first = true;
       foreach ($commits as $line) {
         if (!strlen($line)) {
           continue;
diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php
--- a/src/land/engine/ArcanistMercurialLandEngine.php
+++ b/src/land/engine/ArcanistMercurialLandEngine.php
@@ -406,6 +406,7 @@
       }
 
       $commits = phutil_split_lines($commits, false);
+      $is_first = true;
       foreach ($commits as $line) {
         if (!strlen($line)) {
           continue;
@@ -438,7 +439,12 @@
         }
 
         $commit = $commit_map[$hash];
-        $commit->addSymbol($symbol);
+        if ($is_first) {
+          $commit->addDirectSymbol($symbol);
+          $is_first = false;
+        }
+
+        $commit->addIndirectSymbol($symbol);
       }
     }
 
@@ -607,14 +613,12 @@
     $message = pht(
       'Holding changes locally, they have not been pushed.');
 
-    $push_command = csprintf(
-      '$ hg push -- %s %Ls',
-
-      // TODO: When a parameter contains only "safe" characters, we could
-      // relax the behavior of hgsprintf().
+    // TODO: This is only vaguely correct.
 
+    $push_command = csprintf(
+      '$ hg push --rev %s -- %s',
       hgsprintf('%s', $this->getDisplayHash($into_commit)),
-      $this->newOntoRefArguments($into_commit));
+      $this->getOntoRemote());
 
     echo tsprintf(
       "\n%!\n%s\n\n",
@@ -643,7 +647,7 @@
     }
 
     echo tsprintf(
-      "%s\n".
+      "%s\n",
       pht(
         'Local branches and bookmarks have not been changed, and are still '.
         'in the same state as before.'));
diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php
--- a/src/repository/api/ArcanistMercurialAPI.php
+++ b/src/repository/api/ArcanistMercurialAPI.php
@@ -12,6 +12,9 @@
   private $supportsRebase;
   private $supportsPhases;
 
+  private $featureResults = array();
+  private $featureFutures = array();
+
   protected function buildLocalFuture(array $argv) {
     $env = $this->getMercurialEnvironmentVariables();
 
@@ -1167,5 +1170,57 @@
       ->setRepositoryAPI($this);
   }
 
+  public function willTestMercurialFeature($feature) {
+    $this->executeMercurialFeatureTest($feature, false);
+    return $this;
+  }
+
+  public function getMercurialFeature($feature) {
+    return $this->executeMercurialFeatureTest($feature, true);
+  }
+
+  private function executeMercurialFeatureTest($feature, $resolve) {
+    if (array_key_exists($feature, $this->featureResults)) {
+      return $this->featureResults[$feature];
+    }
+
+    if (!array_key_exists($feature, $this->featureFutures)) {
+      $future = $this->newMercurialFeatureFuture($feature);
+      $future->start();
+      $this->featureFutures[$feature] = $future;
+    }
+
+    if (!$resolve) {
+      return;
+    }
+
+    $future = $this->featureFutures[$feature];
+    $result = $this->resolveMercurialFeatureFuture($feature, $future);
+    $this->featureResults[$feature] = $result;
+
+    return $result;
+  }
+
+  private function newMercurialFeatureFuture($feature) {
+    switch ($feature) {
+      case 'shelve':
+        return $this->execFutureLocal(
+          '--config extensions.shelve= shelve --help');
+      default:
+        throw new Exception(
+          pht(
+            'Unknown Mercurial feature "%s".',
+            $feature));
+    }
+  }
+
+  private function resolveMercurialFeatureFuture($feature, $future) {
+    // By default, assume the feature is a simple capability test and the
+    // capability is present if the feature resolves without an error.
+
+    list($err) = $future->resolve();
+    return !$err;
+  }
+
 
 }
diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php
--- a/src/repository/state/ArcanistMercurialLocalState.php
+++ b/src/repository/state/ArcanistMercurialLocalState.php
@@ -5,7 +5,6 @@
 
   private $localCommit;
   private $localRef;
-  private $localPath;
 
   public function getLocalRef() {
     return $this->localRef;
@@ -17,6 +16,7 @@
 
   protected function executeSaveLocalState() {
     $api = $this->getRepositoryAPI();
+
     // TODO: Fix this.
   }
 
@@ -36,13 +36,16 @@
   }
 
   protected function canStashChanges() {
-    // Depends on stash extension.
-    return false;
+    $api = $this->getRepositoryAPI();
+    return $api->getMercurialFeature('shelve');
   }
 
   protected function getIgnoreHints() {
-    // TODO: Provide this.
-    return array();
+    return array(
+      pht(
+        'To configure Mercurial to ignore certain files in the working '.
+        'copy, add them to ".hgignore".'),
+    );
   }
 
   protected function newRestoreCommandsForDisplay() {
@@ -51,15 +54,43 @@
   }
 
   protected function saveStash() {
-    return null;
+    $api = $this->getRepositoryAPI();
+    $log = $this->getWorkflow()->getLogEngine();
+
+    $stash_ref = sprintf(
+      'arc-%s',
+      Filesystem::readRandomCharacters(12));
+
+    $api->execxLocal(
+      '--config extensions.shelve= shelve --unknown --name %s --',
+      $stash_ref);
+
+    $log->writeStatus(
+      pht('SHELVE'),
+      pht('Shelving uncommitted changes from working copy.'));
+
+    return $stash_ref;
   }
 
   protected function restoreStash($stash_ref) {
-    return null;
+    $api = $this->getRepositoryAPI();
+    $log = $this->getWorkflow()->getLogEngine();
+
+    $log->writeStatus(
+      pht('UNSHELVE'),
+      pht('Restoring uncommitted changes to working copy.'));
+
+    $api->execxLocal(
+      '--config extensions.shelve= unshelve --keep --name %s --',
+      $stash_ref);
   }
 
   protected function discardStash($stash_ref) {
-    return null;
+    $api = $this->getRepositoryAPI();
+
+    $api->execxLocal(
+      '--config extensions.shelve= shelve --delete %s --',
+      $stash_ref);
   }
 
 }
diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php
--- a/src/repository/state/ArcanistRepositoryLocalState.php
+++ b/src/repository/state/ArcanistRepositoryLocalState.php
@@ -161,9 +161,8 @@
     $this->shouldRestore = false;
 
     $this->executeRestoreLocalState();
-    if ($this->stashRef !== null) {
-      $this->restoreStash($this->stashRef);
-    }
+    $this->applyStash();
+    $this->executeDiscardLocalState();
 
     return $this;
   }
@@ -171,12 +170,8 @@
   final public function discardLocalState() {
     $this->shouldRestore = false;
 
+    $this->applyStash();
     $this->executeDiscardLocalState();
-    if ($this->stashRef !== null) {
-      $this->restoreStash($this->stashRef);
-      $this->discardStash($this->stashRef);
-      $this->stashRef = null;
-    }
 
     return $this;
   }
@@ -184,9 +179,9 @@
   final public function __destruct() {
     if ($this->shouldRestore) {
       $this->restoreLocalState();
+    } else {
+      $this->discardLocalState();
     }
-
-    $this->discardLocalState();
   }
 
   final public function getRestoreCommandsForDisplay() {
@@ -209,6 +204,17 @@
     throw new PhutilMethodNotImplementedException();
   }
 
+  private function applyStash() {
+    if ($this->stashRef === null) {
+      return;
+    }
+    $stash_ref = $this->stashRef;
+    $this->stashRef = null;
+
+    $this->restoreStash($stash_ref);
+    $this->discardStash($stash_ref);
+  }
+
   abstract protected function executeSaveLocalState();
   abstract protected function executeRestoreLocalState();
   abstract protected function executeDiscardLocalState();
diff --git a/src/xsprintf/hgsprintf.php b/src/xsprintf/hgsprintf.php
--- a/src/xsprintf/hgsprintf.php
+++ b/src/xsprintf/hgsprintf.php
@@ -22,7 +22,14 @@
 
   switch ($type) {
     case 's':
-      $value = "'".addcslashes($value, "'\\")."'";
+      // If this is symbol only has "safe" alphanumeric latin characters,
+      // and is at least one character long, we can let it through without
+      // escaping it. This tends to produce more readable commands.
+      if (preg_match('(^[a-zA-Z0-9]+\z)', $value)) {
+        $value = $value;
+      } else {
+        $value = "'".addcslashes($value, "'\\")."'";
+      }
       break;
     case 'R':
       $type = 's';