1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-25 16:22:42 +01:00

Fix working copy binding powers in weird edge cases

Summary:
Fixes T1277. The rules we use to figure out the root of the working copy get a bunch of edge cases wrong right now. A particularly troublesome one is when a user has a `/.arcconfig` or `/home/.arcconfig` or similar, which raises a completely useless and confusing error message (T1277).

Rewrite these rules to get all the edge cases correct and do reasonable things in the presence of stray `.arcconfig`. There are a bunch of comments, but basically the algorithm is:

  - From the top, go down one directory at a time until we find ".svn", ".git", or ".hg".
  - In Subversion, keep going down looking for ".arcconfig". In Git and Mercurial, look for ".arcconfig" only in the same directory.
  - Now that we've figured out the VCS root (where the ".vcs" directory is) and the project root (where the ".arcconfig" file is, if it exists), build an identity.

This logic was also spread across three different places. Consolidate it into one and add some logging so we can figure out what's going wrong if users run into trouble.

Test Plan:
  - Ran VCS (`arc list`) and non-VCS (`arc help`) commands in Git, Mercurial, and Subversions roots and subdirectories. Also ran them in non-VCS directories. Ran them with and without .arcconfig. All the outputs seemed completely reasonable.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1277

Differential Revision: https://secure.phabricator.com/D7686
This commit is contained in:
epriestley 2013-12-03 10:32:31 -08:00
parent 00d5eb2f21
commit 4fdb87761b
2 changed files with 179 additions and 129 deletions

View file

@ -63,48 +63,31 @@ abstract class ArcanistRepositoryAPI {
if (!$working_copy) {
throw new Exception(
"Trying to create a RepositoryApi without a working copy");
pht(
"Trying to create a RepositoryAPI without a working copy!"));
}
$root = $working_copy->getProjectRoot();
if (!$root) {
throw new ArcanistUsageException(
"There is no readable '.arcconfig' file in the working directory or ".
"any parent directory. Create an '.arcconfig' file to configure arc.");
}
if (Filesystem::pathExists($root.'/.hg')) {
$api = new ArcanistMercurialAPI($root);
$api->configurationManager = $configuration_manager;
return $api;
}
$git_root = self::discoverGitBaseDirectory($root);
if ($git_root) {
if (!Filesystem::pathsAreEquivalent($root, $git_root)) {
throw new ArcanistUsageException(
"'.arcconfig' file is located at '{$root}', but working copy root ".
"is '{$git_root}'. Move '.arcconfig' file to the working copy root.");
}
$api = new ArcanistGitAPI($root);
$api->configurationManager = $configuration_manager;
return $api;
}
// check if we're in an svn working copy
foreach (Filesystem::walkToRoot($root) as $dir) {
if (Filesystem::pathExists($dir . '/.svn')) {
switch ($working_copy->getVCSType()) {
case 'svn':
$api = new ArcanistSubversionAPI($root);
$api->configurationManager = $configuration_manager;
return $api;
}
break;
case 'hg':
$api = new ArcanistMercurialAPI($root);
break;
case 'git':
$api = new ArcanistGitAPI($root);
break;
default:
throw new Exception(
pht(
"The current working directory is not part of a working copy for ".
"a supported version control system (Git, Subversion or ".
"Mercurial)."));
}
throw new ArcanistUsageException(
"The current working directory is not part of a working copy for a ".
"supported version control system (svn, git or mercurial).");
$api->configurationManager = $configuration_manager;
return $api;
}
public function __construct($path) {
@ -288,25 +271,6 @@ abstract class ArcanistRepositoryAPI {
}
private static function discoverGitBaseDirectory($root) {
try {
// NOTE: This awkward construction is to make sure things work on Windows.
$future = new ExecFuture('git rev-parse --show-cdup');
$future->setCWD($root);
list($stdout) = $future->resolvex();
return Filesystem::resolvePath(rtrim($stdout, "\n"), $root);
} catch (CommandException $ex) {
// This might be because the $root isn't a Git working copy, or the user
// might not have Git installed at all so the `git` command fails. Assume
// that users trying to work with git working copies will have a working
// `git` binary.
return null;
}
}
/**
* Fetches the original file data for each path provided.
*

View file

@ -10,47 +10,165 @@
*/
final class ArcanistWorkingCopyIdentity {
protected $localConfig;
protected $projectConfig;
protected $projectRoot;
protected $localMetaDir;
private $projectConfig;
private $projectRoot;
private $localConfig = array();
private $localMetaDir;
private $vcsType;
private $vcsRoot;
public static function newDummyWorkingCopy() {
return new ArcanistWorkingCopyIdentity('/', array());
return self::newFromPathWithConfig('/', array());
}
public static function newFromPath($path) {
$project_id = null;
return self::newFromPathWithConfig($path, null);
}
/**
* Locate all the information we need about a directory which we presume
* to be a working copy. Particularly, we want to discover:
*
* - Is the directory inside a working copy (hg, git, svn)?
* - If so, what is the root of the working copy?
* - Is there a `.arcconfig` file?
*
* This is complicated, mostly because Subversion has special rules. In
* particular:
*
* - Until 1.7, Subversion put a `.svn/` directory inside //every//
* directory in a working copy. After 1.7, it //only// puts one at the
* root.
* - We allow `.arcconfig` to appear anywhere in a Subversion working copy,
* and use the one closest to the directory.
* - Although we may use a `.arcconfig` from a subdirectory, we store
* metadata in the root's `.svn/`, because it's the only one guaranteed
* to exist.
*
* Users also do these kinds of things in the wild:
*
* - Put working copies inside other working copies.
* - Put working copies inside `.git/` directories.
* - Create `.arcconfig` files at `/.arcconfig`, `/home/.arcconfig`, etc.
*
* This method attempts to be robust against all sorts of possible
* misconfiguration.
*
* @param string Path to load information for, usually the current working
* directory (unless running unit tests).
* @param map|null Pass `null` to locate and load a `.arcconfig` file if one
* exists. Pass a map to use it to set configuration.
* @return ArcanistWorkingCopyIdentity Constructed working copy identity.
*/
private static function newFromPathWithConfig($path, $config) {
$project_root = null;
$config = array();
foreach (Filesystem::walkToRoot($path) as $dir) {
$config_file = $dir.'/.arcconfig';
if (!Filesystem::pathExists($config_file)) {
continue;
}
$proj_raw = Filesystem::readFile($config_file);
$config = self::parseRawConfigFile($proj_raw, $config_file);
$project_root = $dir;
break;
}
$vcs_root = null;
$vcs_type = null;
if (!$project_root) {
foreach (Filesystem::walkToRoot($path) as $dir) {
$try = array(
$dir.'/.svn',
$dir.'/.hg',
$dir.'/.git',
);
foreach ($try as $trydir) {
if (Filesystem::pathExists($trydir)) {
$project_root = $dir;
break 2;
}
// First, find the outermost directory which is a Git, Mercurial or
// Subversion repository, if one exists. We go from the top because this
// makes it easier to identify the root of old SVN working copies (which
// have a ".svn/" directory inside every directory in the working copy) and
// gives us the right result if you have a Git repository inside a
// Subversion repository or something equally ridiculous.
$paths = Filesystem::walkToRoot($path);
$config_paths = array();
$paths = array_reverse($paths);
foreach ($paths as $path_key => $parent_path) {
$try = array(
'git' => $parent_path.'/.git',
'hg' => $parent_path.'/.hg',
'svn' => $parent_path.'/.svn',
);
foreach ($try as $vcs => $try_dir) {
if (!Filesystem::pathExists($try_dir)) {
continue;
}
// NOTE: We're distinguishing between the `$project_root` and the
// `$vcs_root` because they may not be the same in Subversion. Normally,
// they are identical. However, in Subversion, the `$vcs_root` is the
// base directory of the working copy (the directory which has the
// `.svn/` directory, after SVN 1.7), while the `$project_root` might
// be any subdirectory of the `$vcs_root`: it's the the directory
// closest to the current directory which contains a `.arcconfig`.
$project_root = $parent_path;
$vcs_root = $parent_path;
$vcs_type = $vcs;
if ($vcs == 'svn') {
// For Subversion, we'll look for a ".arcconfig" file here or in
// any subdirectory, starting at the deepest subdirectory.
$config_paths = array_slice($paths, $path_key);
$config_paths = array_reverse($config_paths);
} else {
// For Git and Mercurial, we'll only look for ".arcconfig" right here.
$config_paths = array($parent_path);
}
break;
}
}
return new ArcanistWorkingCopyIdentity($project_root, $config);
$console = PhutilConsole::getConsole();
foreach ($config_paths as $config_path) {
$config_file = $config_path.'/.arcconfig';
if (Filesystem::pathExists($config_file)) {
// We always need to examine the filesystem to look for `.arcconfig`
// so we can set the project root correctly. We might or might not
// actually read the file: if the caller passed in configuration data,
// we'll ignore the actual file contents.
$project_root = $config_path;
if ($config === null) {
$console->writeLog(
"%s\n",
pht('Working Copy: Reading .arcconfig from "%s".', $config_file));
$config_data = Filesystem::readFile($config_file);
$config = self::parseRawConfigFile($config_data, $config_file);
}
break;
}
}
if ($config === null) {
// We didn't find a ".arcconfig" anywhere, so just use an empty array.
$config = array();
}
if ($project_root === null) {
// We aren't in a working directory at all. This is fine if we're
// running a command like "arc help". If we're running something that
// requires a working directory, an exception will be raised a little
// later on.
$console->writeLog(
"%s\n",
pht('Working Copy: Path "%s" is not in any working copy.', $path));
return new ArcanistWorkingCopyIdentity($path, $config);
}
$console->writeLog(
"%s\n",
pht(
'Working Copy: Path "%s" is part of `%s` working copy "%s".',
$path,
$vcs_type,
$vcs_root));
$console->writeLog(
"%s\n",
pht(
'Working Copy: Project root is at "%s".',
$project_root));
$identity = new ArcanistWorkingCopyIdentity($project_root, $config);
$identity->localMetaDir = $vcs_root.'/.'.$vcs_type;
$identity->localConfig = $identity->readLocalArcConfig();
$identity->vcsType = $vcs_type;
$identity->vcsRoot = $vcs_root;
return $identity;
}
public static function newFromRootAndConfigFile(
@ -64,7 +182,7 @@ final class ArcanistWorkingCopyIdentity {
$config = self::parseRawConfigFile($config_raw, $from_where);
}
return new ArcanistWorkingCopyIdentity($root, $config);
return self::newFromPathWithConfig($root, $config);
}
private static function parseRawConfigFile($raw_config, $from_where) {
@ -89,49 +207,9 @@ final class ArcanistWorkingCopyIdentity {
return $proj;
}
protected function __construct($root, array $config) {
$this->projectRoot = $root;
$this->projectConfig = $config;
$this->localConfig = array();
$this->localMetaDir = null;
$vc_dirs = array(
'.git',
'.hg',
'.svn',
);
$found_meta_dir = false;
foreach ($vc_dirs as $dir) {
$meta_path = Filesystem::resolvePath(
$dir,
$this->projectRoot);
if (Filesystem::pathExists($meta_path)) {
$found_meta_dir = true;
$this->localMetaDir = $meta_path;
$local_path = Filesystem::resolvePath(
'arc/config',
$meta_path);
$this->localConfig = $this->readLocalArcConfig();
break;
}
}
if (!$found_meta_dir) {
// Try for a single higher-level .svn directory as used by svn 1.7+
foreach (Filesystem::walkToRoot($this->projectRoot) as $parent_path) {
$meta_path = Filesystem::resolvePath(
'.svn',
$parent_path);
$local_path = Filesystem::resolvePath(
'.svn/arc/config',
$parent_path);
if (Filesystem::pathExists($local_path)) {
$this->localMetaDir = $meta_path;
$this->localConfig = $this->readLocalArcConfig();
}
}
}
private function __construct($root, array $config) {
$this->projectRoot = $root;
$this->projectConfig = $config;
}
public function getProjectID() {
@ -146,6 +224,14 @@ final class ArcanistWorkingCopyIdentity {
return $this->projectRoot.'/'.$to_file;
}
public function getVCSType() {
return $this->vcsType;
}
public function getVCSRoot() {
return $this->vcsRoot;
}
/* -( Config )------------------------------------------------------------- */