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