diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 1c58fdb4..c9ace486 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -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. * diff --git a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php index c86f1def..7c617153 100644 --- a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php +++ b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php @@ -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 )------------------------------------------------------------- */