mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-12-29 00:40:57 +01:00
[Wilds] Rewrite WorkingCopyIdentity in a more modern/modular way
Summary: Ref T13098. Currently, `WorkingCopyIdentity` has rules for finding `.git/.svn/.hg` directories and `.arcconfig` files. It also has a ton of logic for reading and writing config files. Generally: - Rename `WorkingCopyIdentity` to `WorkingCopy`. - Make it an abstract base with `Git`, `Mercurial` and `Subversion` subclasses, using the standard module/extension pattern. - Throw out almost all of the config file logic. This logic is going to move into Config classes. It is not unique to working copies. - Working copies retain the actual bit of this they need: knowing where stuff should go on disk. This doesn't run yet, but sure looks a lot cleaner! Test Plan: Doesn't run yet, since Config isn't working yet. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13098 Differential Revision: https://secure.phabricator.com/D19689
This commit is contained in:
parent
9a94aa216b
commit
d936257018
6 changed files with 196 additions and 349 deletions
22
src/workingcopy/ArcanistGitWorkingCopy.php
Normal file
22
src/workingcopy/ArcanistGitWorkingCopy.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistGitWorkingCopy
|
||||
extends ArcanistWorkingCopy {
|
||||
|
||||
public function getMetadataDirectory() {
|
||||
return $this->getPath('.git');
|
||||
}
|
||||
|
||||
protected function newWorkingCopyFromDirectories(
|
||||
$working_directory,
|
||||
$ancestor_directory) {
|
||||
|
||||
if (!Filesystem::pathExits($ancestor_directory.'/.git')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self();
|
||||
}
|
||||
|
||||
}
|
||||
|
22
src/workingcopy/ArcanistMercurialWorkingCopy.php
Normal file
22
src/workingcopy/ArcanistMercurialWorkingCopy.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistMercurialWorkingCopy
|
||||
extends ArcanistWorkingCopy {
|
||||
|
||||
public function getMetadataDirectory() {
|
||||
return $this->getPath('.hg');
|
||||
}
|
||||
|
||||
protected function newWorkingCopyFromDirectories(
|
||||
$working_directory,
|
||||
$ancestor_directory) {
|
||||
|
||||
if (!Filesystem::pathExits($ancestor_directory.'/.hg')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self();
|
||||
}
|
||||
|
||||
}
|
||||
|
51
src/workingcopy/ArcanistSubversionWorkingCopy.php
Normal file
51
src/workingcopy/ArcanistSubversionWorkingCopy.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistSubversionWorkingCopy
|
||||
extends ArcanistWorkingCopy {
|
||||
|
||||
public function getProjectConfigurationFilePath() {
|
||||
// In Subversion, we allow ".arcconfig" to appear at any level of the
|
||||
// filesystem between the working directory and the working copy root.
|
||||
|
||||
// We allow this because Subversion repositories are hierarchical and
|
||||
// may have a "projects/xyz/" directory which is meaningfully an entirely
|
||||
// different project from "projects/abc/".
|
||||
|
||||
// You can checkout "projects/" and have the ".svn/" directory appear
|
||||
// there, then change into "abc/" and expect "arc" to operate within the
|
||||
// context of the "abc/" project.
|
||||
|
||||
$paths = Filesystem::walkToRoot($this->getWorkingDirectory());
|
||||
$root = $this->getPath();
|
||||
foreach ($paths as $path) {
|
||||
if (!Filesystem::isDescendant($path, $root)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$candidate = $path.'/.arcconfig';
|
||||
if (Filesystem::pathExists($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return parent::getProjectConfigurationFilePath();
|
||||
}
|
||||
|
||||
public function getMetadataDirectory() {
|
||||
return $this->getPath('.svn');
|
||||
}
|
||||
|
||||
protected function newWorkingCopyFromDirectories(
|
||||
$working_directory,
|
||||
$ancestor_directory) {
|
||||
|
||||
if (!Filesystem::pathExits($ancestor_directory.'/.svn')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return id(new self());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
95
src/workingcopy/ArcanistWorkingCopy.php
Normal file
95
src/workingcopy/ArcanistWorkingCopy.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
abstract class ArcanistWorkingCopy
|
||||
extends Phobject {
|
||||
|
||||
private $path;
|
||||
private $workingDirectory;
|
||||
|
||||
public static function newFromWorkingDirectory($path) {
|
||||
$working_types = id(new PhutilClassMapQuery())
|
||||
->setParentClass(__CLASS__)
|
||||
->execute();
|
||||
|
||||
// Find the outermost directory which is under version control. We go from
|
||||
// the top because:
|
||||
//
|
||||
// - This gives us a more reasonable behavior if you embed one repository
|
||||
// inside another repository.
|
||||
// - This handles old Subversion working copies correctly. Before
|
||||
// SVN 1.7, Subversion put a ".svn/" directory in every subdirectory.
|
||||
|
||||
$paths = Filesystem::walkToRoot($path);
|
||||
$paths = array_reverse($paths);
|
||||
foreach ($paths as $path_key => $ancestor_path) {
|
||||
foreach ($working_types as $working_type) {
|
||||
|
||||
$working_copy = $working_type->newWorkingCopyFromDirectories(
|
||||
$path,
|
||||
$ancestor_path);
|
||||
if (!$working_copy) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$working_copy->path = $ancestor_path;
|
||||
$working_copy->workingDirectory = $path;
|
||||
|
||||
return $working_copy;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
abstract protected function newWorkingCopyFromDirectories(
|
||||
$working_directory,
|
||||
$ancestor_directory);
|
||||
|
||||
final public function getPath($to_file = null) {
|
||||
return Filesystem::concatenatePaths(
|
||||
array(
|
||||
$this->path,
|
||||
$to_file,
|
||||
));
|
||||
}
|
||||
|
||||
final public function getWorkingDirectory() {
|
||||
return $this->workingDirectory;
|
||||
}
|
||||
|
||||
public function getProjectConfigurationFilePath() {
|
||||
return $this->getPath('.arcconfig');
|
||||
}
|
||||
|
||||
public function getLocalConfigurationFilePath() {
|
||||
if ($this->hasMetadataDirectory()) {
|
||||
return $this->getMetadataPath('arc/config');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getMetadataDirectory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
final public function hasMetadataDirectory() {
|
||||
return ($this->getMetadataDirectory() !== null);
|
||||
}
|
||||
|
||||
final public function getMetadataPath($to_file = null) {
|
||||
if (!$this->hasMetadataDirectory()) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This working copy has no metadata directory, so you can not '.
|
||||
'resolve metadata paths within it.'));
|
||||
}
|
||||
|
||||
return Filesystem::concatenatePaths(
|
||||
array(
|
||||
$this->getMetadataDirectory(),
|
||||
$to_file,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,342 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Interfaces with basic information about the working copy.
|
||||
*
|
||||
* @task config
|
||||
*/
|
||||
final class ArcanistWorkingCopyIdentity extends Phobject {
|
||||
|
||||
private $projectConfig;
|
||||
private $projectRoot;
|
||||
private $localConfig = array();
|
||||
private $localMetaDir;
|
||||
private $vcsType;
|
||||
private $vcsRoot;
|
||||
|
||||
public static function newDummyWorkingCopy() {
|
||||
return self::newFromPathWithConfig('/', array());
|
||||
}
|
||||
|
||||
public static function newFromPath($path) {
|
||||
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;
|
||||
$vcs_root = null;
|
||||
$vcs_type = null;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
$console = PhutilConsole::getConsole();
|
||||
|
||||
$looked_in = array();
|
||||
foreach ($config_paths as $config_path) {
|
||||
$config_file = $config_path.'/.arcconfig';
|
||||
$looked_in[] = $config_file;
|
||||
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 %s from "%s".',
|
||||
'.arcconfig',
|
||||
$config_file));
|
||||
$config_data = Filesystem::readFile($config_file);
|
||||
$config = self::parseRawConfigFile($config_data, $config_file);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($config === null) {
|
||||
if ($looked_in) {
|
||||
$console->writeLog(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Working Copy: Unable to find %s in any of these locations: %s.',
|
||||
'.arcconfig',
|
||||
implode(', ', $looked_in)));
|
||||
} else {
|
||||
$console->writeLog(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Working Copy: No candidate locations for %s from '.
|
||||
'this working directory.',
|
||||
'.arcconfig'));
|
||||
}
|
||||
$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(
|
||||
$root,
|
||||
$config_raw,
|
||||
$from_where) {
|
||||
|
||||
if ($config_raw === null) {
|
||||
$config = array();
|
||||
} else {
|
||||
$config = self::parseRawConfigFile($config_raw, $from_where);
|
||||
}
|
||||
|
||||
return self::newFromPathWithConfig($root, $config);
|
||||
}
|
||||
|
||||
private static function parseRawConfigFile($raw_config, $from_where) {
|
||||
try {
|
||||
return phutil_json_decode($raw_config);
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new PhutilProxyException(
|
||||
pht("Unable to parse '%s' file '%s'.", '.arcconfig', $from_where),
|
||||
$ex);
|
||||
}
|
||||
}
|
||||
|
||||
private function __construct($root, array $config) {
|
||||
$this->projectRoot = $root;
|
||||
$this->projectConfig = $config;
|
||||
}
|
||||
|
||||
public function getProjectRoot() {
|
||||
return $this->projectRoot;
|
||||
}
|
||||
|
||||
public function getProjectPath($to_file) {
|
||||
return $this->projectRoot.'/'.$to_file;
|
||||
}
|
||||
|
||||
public function getVCSType() {
|
||||
return $this->vcsType;
|
||||
}
|
||||
|
||||
public function getVCSRoot() {
|
||||
return $this->vcsRoot;
|
||||
}
|
||||
|
||||
|
||||
/* -( Config )------------------------------------------------------------- */
|
||||
|
||||
public function readProjectConfig() {
|
||||
return $this->projectConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a configuration directive from project configuration. This reads ONLY
|
||||
* permanent project configuration (i.e., ".arcconfig"), not other
|
||||
* configuration sources. See @{method:getConfigFromAnySource} to read from
|
||||
* user configuration.
|
||||
*
|
||||
* @param key Key to read.
|
||||
* @param wild Default value if key is not found.
|
||||
* @return wild Value, or default value if not found.
|
||||
*
|
||||
* @task config
|
||||
*/
|
||||
public function getProjectConfig($key, $default = null) {
|
||||
$settings = new ArcanistSettings();
|
||||
|
||||
$pval = idx($this->projectConfig, $key);
|
||||
|
||||
// Test for older names in the per-project config only, since
|
||||
// they've only been used there.
|
||||
if ($pval === null) {
|
||||
$legacy = $settings->getLegacyName($key);
|
||||
if ($legacy) {
|
||||
$pval = $this->getProjectConfig($legacy);
|
||||
}
|
||||
}
|
||||
|
||||
if ($pval === null) {
|
||||
$pval = $default;
|
||||
} else {
|
||||
$pval = $settings->willReadValue($key, $pval);
|
||||
}
|
||||
|
||||
return $pval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a configuration directive from local configuration. This
|
||||
* reads ONLY the per-working copy configuration,
|
||||
* i.e. .(git|hg|svn)/arc/config, and not other configuration
|
||||
* sources. See @{method:getConfigFromAnySource} to read from any
|
||||
* config source or @{method:getProjectConfig} to read permanent
|
||||
* project-level config.
|
||||
*
|
||||
* @task config
|
||||
*/
|
||||
public function getLocalConfig($key, $default = null) {
|
||||
return idx($this->localConfig, $key, $default);
|
||||
}
|
||||
|
||||
public function readLocalArcConfig() {
|
||||
if (strlen($this->localMetaDir)) {
|
||||
$local_path = Filesystem::resolvePath('arc/config', $this->localMetaDir);
|
||||
|
||||
$console = PhutilConsole::getConsole();
|
||||
|
||||
if (Filesystem::pathExists($local_path)) {
|
||||
$console->writeLog(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Config: Reading local configuration file "%s"...',
|
||||
$local_path));
|
||||
|
||||
try {
|
||||
$json = Filesystem::readFile($local_path);
|
||||
return phutil_json_decode($json);
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new PhutilProxyException(
|
||||
pht("Failed to parse '%s' as JSON.", $local_path),
|
||||
$ex);
|
||||
}
|
||||
} else {
|
||||
$console->writeLog(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Config: Did not find local configuration at "%s".',
|
||||
$local_path));
|
||||
}
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
public function writeLocalArcConfig(array $config) {
|
||||
$json_encoder = new PhutilJSON();
|
||||
$json = $json_encoder->encodeFormatted($config);
|
||||
|
||||
$dir = $this->localMetaDir;
|
||||
if (!strlen($dir)) {
|
||||
throw new Exception(pht('No working copy to write config into!'));
|
||||
}
|
||||
|
||||
$local_dir = $dir.DIRECTORY_SEPARATOR.'arc';
|
||||
if (!Filesystem::pathExists($local_dir)) {
|
||||
Filesystem::createDirectory($local_dir, 0755);
|
||||
}
|
||||
|
||||
$config_file = $local_dir.DIRECTORY_SEPARATOR.'config';
|
||||
Filesystem::writeFile($config_file, $json);
|
||||
}
|
||||
|
||||
}
|
|
@ -202,15 +202,14 @@ final class ArcanistRuntime {
|
|||
}
|
||||
|
||||
private function loadConfiguration(PhutilArgumentParser $args) {
|
||||
$configuration_manager = new ArcanistConfigurationManager();
|
||||
$engine = new ArcanistConfigurationEngine();
|
||||
|
||||
$cwd = getcwd();
|
||||
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($cwd);
|
||||
$configuration_manager->setWorkingCopyIdentity($working_copy);
|
||||
$working_copy = ArcanistWorkingCopyIdentity::newFromPath(getcwd());
|
||||
if ($working_copy) {
|
||||
$engine->setWorkingCopy($working_copy);
|
||||
}
|
||||
|
||||
$configuration_manager->applyRuntimeArcConfig($args);
|
||||
|
||||
return $configuration_manager;
|
||||
return $engine->newConfigurationSourceList();
|
||||
}
|
||||
|
||||
private function loadLibraries(
|
||||
|
|
Loading…
Reference in a new issue