Changeset View
Changeset View
Standalone View
Standalone View
src/repository/api/ArcanistMercurialAPI.php
<?php | <?php | ||||
/** | /** | ||||
* Interfaces with the Mercurial working copies. | * Interfaces with the Mercurial working copies. | ||||
*/ | */ | ||||
final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { | final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { | ||||
/** | |||||
* Mercurial deceptively indicates that the default encoding is UTF-8 however | |||||
* however the actual default appears to be "something else", at least on | |||||
* Windows systems. Force all mercurial commands to use UTF-8 encoding. | |||||
*/ | |||||
const ROOT_HG_COMMAND = 'hg --encoding utf-8 '; | |||||
private $branch; | private $branch; | ||||
private $localCommitInfo; | private $localCommitInfo; | ||||
private $rawDiffCache = array(); | private $rawDiffCache = array(); | ||||
private $featureResults = array(); | private $featureResults = array(); | ||||
private $featureFutures = array(); | private $featureFutures = array(); | ||||
protected function buildLocalFuture(array $argv) { | protected function buildLocalFuture(array $argv) { | ||||
$env = $this->getMercurialEnvironmentVariables(); | $argv[0] = self::ROOT_HG_COMMAND.$argv[0]; | ||||
$argv[0] = 'hg '.$argv[0]; | return $this->newConfiguredFuture(newv('ExecFuture', $argv)); | ||||
} | |||||
$future = newv('ExecFuture', $argv) | public function newPassthru($pattern /* , ... */) { | ||||
->setEnv($env) | $args = func_get_args(); | ||||
->setCWD($this->getPath()); | $args[0] = self::ROOT_HG_COMMAND.$args[0]; | ||||
return $future; | return $this->newConfiguredFuture(newv('PhutilExecPassthru', $args)); | ||||
} | } | ||||
public function newPassthru($pattern /* , ... */) { | private function newConfiguredFuture(PhutilExecutableFuture $future) { | ||||
$args = func_get_args(); | $args = func_get_args(); | ||||
$env = $this->getMercurialEnvironmentVariables(); | $env = $this->getMercurialEnvironmentVariables(); | ||||
$args[0] = 'hg '.$args[0]; | return $future | ||||
return newv('PhutilExecPassthru', $args) | |||||
->setEnv($env) | ->setEnv($env) | ||||
->setCWD($this->getPath()); | ->setCWD($this->getPath()); | ||||
} | } | ||||
public function getSourceControlSystemName() { | public function getSourceControlSystemName() { | ||||
return 'hg'; | return 'hg'; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 400 Lines • ▼ Show 20 Lines | private function getFileDataAtRevision($path, $revision) { | ||||
if ($err) { | if ($err) { | ||||
// Assume this is "no file at revision", i.e. a deleted or added file. | // Assume this is "no file at revision", i.e. a deleted or added file. | ||||
return null; | return null; | ||||
} else { | } else { | ||||
return $stdout; | return $stdout; | ||||
} | } | ||||
} | } | ||||
protected function newCurrentCommitSymbol() { | |||||
return $this->getWorkingCopyRevision(); | |||||
} | |||||
public function getWorkingCopyRevision() { | public function getWorkingCopyRevision() { | ||||
return '.'; | return '.'; | ||||
} | } | ||||
public function isHistoryDefaultImmutable() { | public function isHistoryDefaultImmutable() { | ||||
return true; | return true; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 191 Lines • ▼ Show 20 Lines | $this->execxLocal( | ||||
'addremove -- %Ls', | 'addremove -- %Ls', | ||||
$paths); | $paths); | ||||
$this->reloadWorkingCopy(); | $this->reloadWorkingCopy(); | ||||
} | } | ||||
public function doCommit($message) { | public function doCommit($message) { | ||||
$tmp_file = new TempFile(); | $tmp_file = new TempFile(); | ||||
Filesystem::writeFile($tmp_file, $message); | Filesystem::writeFile($tmp_file, $message); | ||||
$this->execxLocal('commit -l %s', $tmp_file); | $this->execxLocal('commit --logfile %s', $tmp_file); | ||||
$this->reloadWorkingCopy(); | $this->reloadWorkingCopy(); | ||||
} | } | ||||
public function amendCommit($message = null) { | public function amendCommit($message = null) { | ||||
if ($message === null) { | $path_statuses = $this->buildUncommittedStatus(); | ||||
$existing_message = $this->getCommitMessage( | |||||
$this->getWorkingCopyRevision()); | |||||
if ($message === null || $message == $existing_message) { | |||||
if (empty($path_statuses)) { | |||||
// If there are no changes to the working directory and the message is | |||||
// not being changed then there's nothing to amend. Notably Mercurial | |||||
// will return an error code if trying to amend a commit with no change | |||||
// to the commit metadata or file changes. | |||||
return; | |||||
} | |||||
$message = $this->getCommitMessage('.'); | $message = $this->getCommitMessage('.'); | ||||
} | } | ||||
$tmp_file = new TempFile(); | $tmp_file = new TempFile(); | ||||
Filesystem::writeFile($tmp_file, $message); | Filesystem::writeFile($tmp_file, $message); | ||||
if ($this->getMercurialFeature('evolve')) { | |||||
$this->execxLocal('amend --logfile %s --', $tmp_file); | |||||
try { | try { | ||||
$this->execxLocal( | $this->execxLocal('evolve --all --'); | ||||
'commit --amend -l %s', | |||||
$tmp_file); | |||||
} catch (CommandException $ex) { | } catch (CommandException $ex) { | ||||
if (preg_match('/nothing changed/', $ex->getStdout())) { | $this->execxLocal('evolve --abort --'); | ||||
// NOTE: Mercurial considers it an error to make a no-op amend. Although | |||||
// we generally defer to the underlying VCS to dictate behavior, this | |||||
// one seems a little goofy, and we use amend as part of various | |||||
// workflows under the assumption that no-op amends are fine. If this | |||||
// amend failed because it's a no-op, just continue. | |||||
} else { | |||||
throw $ex; | throw $ex; | ||||
} | } | ||||
$this->reloadWorkingCopy(); | |||||
return; | |||||
} | |||||
// Get the child nodes of the current changeset. | |||||
list($children) = $this->execxLocal( | |||||
'log --template %s --rev %s --', | |||||
'{node} ', | |||||
'children(.)'); | |||||
$child_nodes = array_filter(explode(' ', $children)); | |||||
// For a head commit we can simply use `commit --amend` for both new commit | |||||
// message and amending changes from the working directory. | |||||
if (empty($child_nodes)) { | |||||
$this->execxLocal('commit --amend --logfile %s --', $tmp_file); | |||||
} else { | |||||
$this->amendNonHeadCommit($child_nodes, $tmp_file); | |||||
} | } | ||||
$this->reloadWorkingCopy(); | $this->reloadWorkingCopy(); | ||||
} | } | ||||
/** | |||||
* Amends a non-head commit with a new message and file changes. This | |||||
* strategy is for Mercurial repositories without the evolve extension. | |||||
* | |||||
* 1. Run 'arc-amend' which uses Mercurial internals to amend the current | |||||
* commit with updated message/file-changes. It results in a new commit | |||||
* from the right parent | |||||
* 2. For each branch from the original commit, rebase onto the new commit, | |||||
* removing the original branch. Note that there is potential for this to | |||||
* cause a conflict but this is something the user has to address. | |||||
* 3. Strip the original commit. | |||||
* | |||||
* @param array The list of child changesets off the original commit. | |||||
* @param file The file containing the new commit message. | |||||
*/ | |||||
private function amendNonHeadCommit($child_nodes, $tmp_file) { | |||||
list($current) = $this->execxLocal( | |||||
'log --template %s --rev . --', | |||||
'{node}'); | |||||
$this->execxLocalWithExtension( | |||||
'arc-hg', | |||||
'arc-amend --logfile %s', | |||||
$tmp_file); | |||||
list($new_commit) = $this->execxLocal( | |||||
'log --rev tip --template %s --', | |||||
'{node}'); | |||||
try { | |||||
$rebase_args = array( | |||||
'--dest', | |||||
$new_commit, | |||||
); | |||||
foreach ($child_nodes as $child) { | |||||
$rebase_args[] = '--source'; | |||||
$rebase_args[] = $child; | |||||
} | |||||
$this->execxLocalWithExtension( | |||||
'rebase', | |||||
'rebase %Ls --', | |||||
$rebase_args); | |||||
} catch (CommandException $ex) { | |||||
$this->execxLocalWithExtension( | |||||
'rebase', | |||||
'rebase --abort --'); | |||||
throw $ex; | |||||
} | |||||
$this->execxLocalWithExtension( | |||||
'strip', | |||||
'strip --rev %s --', | |||||
$current); | |||||
} | |||||
public function getCommitSummary($commit) { | public function getCommitSummary($commit) { | ||||
if ($commit == 'null') { | if ($commit == 'null') { | ||||
return pht('(The Empty Void)'); | return pht('(The Empty Void)'); | ||||
} | } | ||||
list($summary) = $this->execxLocal( | list($summary) = $this->execxLocal( | ||||
'log --template {desc} --limit 1 --rev %s', | 'log --template {desc} --limit 1 --rev %s', | ||||
$commit); | $commit); | ||||
▲ Show 20 Lines • Show All 255 Lines • ▼ Show 20 Lines | public function willTestMercurialFeature($feature) { | ||||
$this->executeMercurialFeatureTest($feature, false); | $this->executeMercurialFeatureTest($feature, false); | ||||
return $this; | return $this; | ||||
} | } | ||||
public function getMercurialFeature($feature) { | public function getMercurialFeature($feature) { | ||||
return $this->executeMercurialFeatureTest($feature, true); | return $this->executeMercurialFeatureTest($feature, true); | ||||
} | } | ||||
/** | |||||
* Returns the necessary flag for using a Mercurial extension. This will | |||||
* enable Mercurial built-in extensions and the "arc-hg" extension that is | |||||
* included with Arcanist. This will not enable other extensions, e.g. | |||||
* "evolve". | |||||
* | |||||
* @param string The name of the extension to enable. | |||||
* @return string A new command pattern that includes the necessary flags to | |||||
* enable the specified extension. | |||||
*/ | |||||
private function getMercurialExtensionFlag($extension) { | |||||
switch ($extension) { | |||||
case 'arc-hg': | |||||
$path = phutil_get_library_root('arcanist'); | |||||
$path = dirname($path); | |||||
$path = $path.'/support/hg/arc-hg.py'; | |||||
$ext_config = 'extensions.arc-hg='.$path; | |||||
break; | |||||
case 'rebase': | |||||
$ext_config = 'extensions.rebase='; | |||||
break; | |||||
case 'shelve': | |||||
$ext_config = 'extensions.shelve='; | |||||
break; | |||||
case 'strip': | |||||
$ext_config = 'extensions.strip='; | |||||
break; | |||||
default: | |||||
throw new Exception( | |||||
pht('Unknown Mercurial Extension: "%s".', $extension)); | |||||
} | |||||
return csprintf('--config %s', $ext_config); | |||||
} | |||||
/** | |||||
* Produces the arguments that should be passed to Mercurial command | |||||
* execution that enables a desired extension. | |||||
* | |||||
* @param string The name of the extension to enable. | |||||
* @param string The command pattern that will be run with the extension | |||||
* enabled. | |||||
* @param array Parameters for the command pattern argument. | |||||
* @return array An array where the first item is a Mercurial command | |||||
* pattern that includes the necessary flag for enabling the | |||||
* desired extension, and all remaining items are parameters | |||||
* to that command pattern. | |||||
*/ | |||||
private function buildMercurialExtensionCommand( | |||||
$extension, | |||||
$pattern /* , ... */) { | |||||
$args = func_get_args(); | |||||
$pattern_args = array_slice($args, 2); | |||||
$ext_flag = $this->getMercurialExtensionFlag($extension); | |||||
$full_cmd = $ext_flag.' '.$pattern; | |||||
$args = array_merge( | |||||
array($full_cmd), | |||||
$pattern_args); | |||||
return $args; | |||||
} | |||||
public function execxLocalWithExtension( | |||||
$extension, | |||||
$pattern /* , ... */) { | |||||
$args = func_get_args(); | |||||
$extended_args = call_user_func_array( | |||||
array($this, 'buildMercurialExtensionCommand'), | |||||
$args); | |||||
return call_user_func_array( | |||||
array($this, 'execxLocal'), | |||||
$extended_args); | |||||
} | |||||
public function execFutureLocalWithExtension( | |||||
$extension, | |||||
$pattern /* , ... */) { | |||||
$args = func_get_args(); | |||||
$extended_args = call_user_func_array( | |||||
array($this, 'buildMercurialExtensionCommand'), | |||||
$args); | |||||
return call_user_func_array( | |||||
array($this, 'execFutureLocal'), | |||||
$extended_args); | |||||
} | |||||
public function execPassthruWithExtension( | |||||
$extension, | |||||
$pattern /* , ... */) { | |||||
$args = func_get_args(); | |||||
$extended_args = call_user_func_array( | |||||
array($this, 'buildMercurialExtensionCommand'), | |||||
$args); | |||||
return call_user_func_array( | |||||
array($this, 'execPassthru'), | |||||
$extended_args); | |||||
} | |||||
public function execManualLocalWithExtension( | |||||
$extension, | |||||
$pattern /* , ... */) { | |||||
$args = func_get_args(); | |||||
$extended_args = call_user_func_array( | |||||
array($this, 'buildMercurialExtensionCommand'), | |||||
$args); | |||||
return call_user_func_array( | |||||
array($this, 'execManualLocal'), | |||||
$extended_args); | |||||
} | |||||
private function executeMercurialFeatureTest($feature, $resolve) { | private function executeMercurialFeatureTest($feature, $resolve) { | ||||
if (array_key_exists($feature, $this->featureResults)) { | if (array_key_exists($feature, $this->featureResults)) { | ||||
return $this->featureResults[$feature]; | return $this->featureResults[$feature]; | ||||
} | } | ||||
if (!array_key_exists($feature, $this->featureFutures)) { | if (!array_key_exists($feature, $this->featureFutures)) { | ||||
$future = $this->newMercurialFeatureFuture($feature); | $future = $this->newMercurialFeatureFuture($feature); | ||||
$future->start(); | $future->start(); | ||||
Show All 9 Lines | private function executeMercurialFeatureTest($feature, $resolve) { | ||||
$this->featureResults[$feature] = $result; | $this->featureResults[$feature] = $result; | ||||
return $result; | return $result; | ||||
} | } | ||||
private function newMercurialFeatureFuture($feature) { | private function newMercurialFeatureFuture($feature) { | ||||
switch ($feature) { | switch ($feature) { | ||||
case 'shelve': | case 'shelve': | ||||
return $this->execFutureLocal( | return $this->execFutureLocalWithExtension( | ||||
'--config extensions.shelve= shelve --help --'); | 'shelve', | ||||
'shelve --help --'); | |||||
case 'evolve': | case 'evolve': | ||||
return $this->execFutureLocal('prune --help --'); | return $this->execFutureLocal('prune --help --'); | ||||
default: | default: | ||||
throw new Exception( | throw new Exception( | ||||
pht( | pht( | ||||
'Unknown Mercurial feature "%s".', | 'Unknown Mercurial feature "%s".', | ||||
$feature)); | $feature)); | ||||
} | } | ||||
Show All 17 Lines | |||||
protected function newMarkerRefQueryTemplate() { | protected function newMarkerRefQueryTemplate() { | ||||
return new ArcanistMercurialRepositoryMarkerQuery(); | return new ArcanistMercurialRepositoryMarkerQuery(); | ||||
} | } | ||||
protected function newRemoteRefQueryTemplate() { | protected function newRemoteRefQueryTemplate() { | ||||
return new ArcanistMercurialRepositoryRemoteQuery(); | return new ArcanistMercurialRepositoryRemoteQuery(); | ||||
} | } | ||||
public function getMercurialExtensionArguments() { | |||||
$path = phutil_get_library_root('arcanist'); | |||||
$path = dirname($path); | |||||
$path = $path.'/support/hg/arc-hg.py'; | |||||
return array( | |||||
'--config', | |||||
'extensions.arc-hg='.$path, | |||||
); | |||||
} | |||||
protected function newNormalizedURI($uri) { | protected function newNormalizedURI($uri) { | ||||
return new ArcanistRepositoryURINormalizer( | return new ArcanistRepositoryURINormalizer( | ||||
ArcanistRepositoryURINormalizer::TYPE_MERCURIAL, | ArcanistRepositoryURINormalizer::TYPE_MERCURIAL, | ||||
$uri); | $uri); | ||||
} | } | ||||
protected function newCommitGraphQueryTemplate() { | protected function newCommitGraphQueryTemplate() { | ||||
return new ArcanistMercurialCommitGraphQuery(); | return new ArcanistMercurialCommitGraphQuery(); | ||||
Show All 23 Lines |
Content licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA) unless otherwise noted; code licensed under Apache 2.0 or other open source licenses. · CC BY-SA 4.0 · Apache 2.0