public static function newAPIFromWorkingCopyIdentity(ArcanistWorkingCopyIdentity $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->workingCopyIdentity = $working_copy; 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->workingCopyIdentity = $working_copy; return $api; } // check if we're in an svn working copy foreach (Filesystem::walkToRoot($root) as $dir) { if (Filesystem::pathExists($dir . '/.svn')) { $api = new ArcanistSubversionAPI($root); $api->workingCopyIdentity = $working_copy; return $api; } } throw new ArcanistUsageException("The current working directory is not part of a working copy for a " . "supported version control system (svn, git or mercurial)."); }
public function testWalkToRoot() { $test_cases = array(array(dirname(__FILE__) . '/data/include_dir.txt/subdir.txt/test', dirname(__FILE__), array(dirname(__FILE__) . '/data/include_dir.txt/subdir.txt/test', dirname(__FILE__) . '/data/include_dir.txt/subdir.txt', dirname(__FILE__) . '/data/include_dir.txt', dirname(__FILE__) . '/data', dirname(__FILE__))), array(dirname(__FILE__) . '/data/include_dir.txt/subdir.txt', dirname(__FILE__), array(dirname(__FILE__) . '/data/include_dir.txt/subdir.txt', dirname(__FILE__) . '/data/include_dir.txt', dirname(__FILE__) . '/data', dirname(__FILE__))), 'root and path are identical' => array(dirname(__FILE__), dirname(__FILE__), array(dirname(__FILE__))), 'root is not an ancestor of path' => array(dirname(__FILE__), dirname(__FILE__) . '/data/include_dir.txt/subdir.txt', array()), 'fictional paths work' => array('/x/y/z', '/', array('/x/y/z', '/x/y', '/x', '/'))); foreach ($test_cases as $test_case) { list($path, $root, $expected) = $test_case; $this->assertEqual($expected, Filesystem::walkToRoot($path, $root)); } }
function phutil_get_library_root_for_path($path) { foreach (Filesystem::walkToRoot($path) as $dir) { if (@file_exists($dir . '/__phutil_library_init__.php')) { return $dir; } } return null; }
public function run() { $argv = $this->getArgument('argv'); if (count($argv) > 1) { throw new ArcanistUsageException("Provide only one path to 'arc liberate'. The path should be a " . "directory where you want to create or update a libphutil library."); } else { if (count($argv) == 0) { $path = getcwd(); } else { $path = reset($argv); } } $is_remap = $this->getArgument('remap'); $is_verify = $this->getArgument('verify'); $path = Filesystem::resolvePath($path); if (Filesystem::pathExists($path) && is_dir($path)) { $init = id(new FileFinder($path))->withPath('*/__phutil_library_init__.php')->find(); } else { $init = null; } if ($init) { if (count($init) > 1) { throw new ArcanistUsageException("Specified directory contains more than one libphutil library. Use " . "a more specific path."); } $path = Filesystem::resolvePath(dirname(reset($init)), $path); } else { $found = false; foreach (Filesystem::walkToRoot($path) as $dir) { if (Filesystem::pathExists($dir . '/__phutil_library_init__.php')) { $path = $dir; $found = true; break; } } if (!$found) { echo "No library currently exists at that path...\n"; $this->liberateCreateDirectory($path); $this->liberateCreateLibrary($path); return; } } $version = $this->getLibraryFormatVersion($path); switch ($version) { case 1: if ($this->getArgument('upgrade')) { return $this->upgradeLibrary($path); } throw new ArcanistUsageException("This library is using libphutil v1, which is no longer supported. " . "Run 'arc liberate --upgrade' to upgrade to v2."); case 2: if ($this->getArgument('upgrade')) { throw new ArcanistUsageException("Can't upgrade a v2 library!"); } return $this->liberateVersion2($path); default: throw new ArcanistUsageException("Unknown library version '{$version}'!"); } }
public function getMetadataPath() { static $svn_dir = null; if ($svn_dir === null) { // from svn 1.7, subversion keeps a single .svn directly under // the working copy root. However, we allow .arcconfigs that // aren't at the working copy root. foreach (Filesystem::walkToRoot($this->getPath()) as $parent) { $possible_svn_dir = Filesystem::resolvePath('.svn', $parent); if (Filesystem::pathExists($possible_svn_dir)) { $svn_dir = $possible_svn_dir; break; } } } return $svn_dir; }
public static function newFromPath($path) { $project_id = null; $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; } return new ArcanistWorkingCopyIdentity($project_root, $config); }
/** * 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 .arcconfig from "%s".', $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 .arcconfig in any of these ' . 'locations: %s.', implode(', ', $looked_in))); } else { $console->writeLog("%s\n", pht('Working Copy: No candidate locations for .arcconfig from ' . 'this working directory.')); } $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; }
/** * Returns the paths in which we should look for tests to execute. * * @return list<string> A list of paths in which to search for test cases. */ public function getTestPaths() { $root = $this->getWorkingCopy()->getProjectRoot(); $paths = array(); foreach ($this->getPaths() as $path) { $library_root = phutil_get_library_root_for_path($path); if (!$library_root) { continue; } $library_name = phutil_get_library_name_for_root($library_root); if (!$library_name) { throw new Exception(pht("Attempting to run unit tests on a libphutil library which has " . "not been loaded, at:\n\n" . " %s\n\n" . "This probably means one of two things:\n\n" . " - You may need to add this library to %s.\n" . " - You may be running tests on a copy of libphutil or " . "arcanist using a different copy of libphutil or arcanist. " . "This operation is not supported.\n", $library_root, '.arcconfig.')); } $path = Filesystem::resolvePath($path, $root); $library_path = Filesystem::readablePath($path, $library_root); if (!Filesystem::isDescendant($path, $library_root)) { // We have encountered some kind of symlink maze -- for instance, $path // is some symlink living outside the library that links into some file // inside the library. Just ignore these cases, since the affected file // does not actually lie within the library. continue; } if (is_file($path) && preg_match('@(?:^|/)__tests__/@', $path)) { $paths[$library_name . ':' . $library_path] = array('library' => $library_name, 'path' => $library_path); continue; } foreach (Filesystem::walkToRoot($path, $library_root) as $subpath) { if ($subpath == $library_root) { $paths[$library_name . ':.'] = array('library' => $library_name, 'path' => '__tests__/'); } else { $library_subpath = Filesystem::readablePath($subpath, $library_root); $paths[$library_name . ':' . $library_subpath] = array('library' => $library_name, 'path' => $library_subpath . '/__tests__/'); } } } return $paths; }
/** * Get places to look for PHP Unit tests that cover a given file. For some * file "/a/b/c/X.php", we look in the same directory: * * /a/b/c/ * * We then look in all parent directories for a directory named "tests/" * (or "Tests/"): * * /a/b/c/tests/ * /a/b/tests/ * /a/tests/ * /tests/ * * We also try to replace each directory component with "tests/": * * /a/b/tests/ * /a/tests/c/ * /tests/b/c/ * * We also try to add "tests/" at each directory level: * * /a/b/c/tests/ * /a/b/tests/c/ * /a/tests/b/c/ * /tests/a/b/c/ * * This finds tests with a layout like: * * docs/ * src/ * tests/ * * ...or similar. This list will be further pruned by the caller; it is * intentionally filesystem-agnostic to be unit testable. * * @param string PHP file to locate test cases for. * @return list<string> List of directories to search for tests in. */ public static function getSearchLocationsForTests($path) { $file = basename($path); $dir = dirname($path); $test_dir_names = array('tests', 'Tests'); $try_directories = array(); // Try in the current directory. $try_directories[] = array($dir); // Try in a tests/ directory anywhere in the ancestry. foreach (Filesystem::walkToRoot($dir) as $parent_dir) { if ($parent_dir == '/') { // We'll restore this later. $parent_dir = ''; } foreach ($test_dir_names as $test_dir_name) { $try_directories[] = array($parent_dir, $test_dir_name); } } // Try replacing each directory component with 'tests/'. $parts = trim($dir, DIRECTORY_SEPARATOR); $parts = explode(DIRECTORY_SEPARATOR, $parts); foreach (array_reverse(array_keys($parts)) as $key) { foreach ($test_dir_names as $test_dir_name) { $try = $parts; $try[$key] = $test_dir_name; array_unshift($try, ''); $try_directories[] = $try; } } // Try adding 'tests/' at each level. foreach (array_reverse(array_keys($parts)) as $key) { foreach ($test_dir_names as $test_dir_name) { $try = $parts; $try[$key] = $test_dir_name . DIRECTORY_SEPARATOR . $try[$key]; array_unshift($try, ''); $try_directories[] = $try; } } $results = array(); foreach ($try_directories as $parts) { $results[implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR] = true; } return array_keys($results); }
/** * Some nasty guessing here. * * Walk up to the project root trying to find * [Tt]ests directory and replicate the structure there. * * Assume that the class path is * /www/project/module/package/subpackage/FooBar.php * and a project root is /www/project it will look for it by these paths: * /www/project/module/package/subpackage/[Tt]ests/FooBarTest.php * /www/project/module/package/[Tt]ests/subpackage/FooBarTest.php * /www/project/module/[Tt]ests/package/subpackage/FooBarTest.php * /www/project/Tt]ests/module/package/subpackage/FooBarTest.php * * TODO: Add support for finding tests based on PSR-1 naming conventions: * /www/project/src/Something/Foo/Bar.php tests should be detected in * /www/project/tests/Something/Foo/BarTest.php * * TODO: Add support for finding tests in testsuite folders from * phpunit.xml configuration. * * @param string $path * * @return string|boolean */ private function findTestFile($path) { $expected_file = substr(basename($path), 0, -4) . 'Test.php'; $expected_dir = null; $dirname = dirname($path); foreach (Filesystem::walkToRoot($dirname) as $dir) { $expected_dir = DIRECTORY_SEPARATOR . substr($dirname, strlen($dir) + 1) . $expected_dir; $look_for = $dir . DIRECTORY_SEPARATOR . '%s' . $expected_dir . $expected_file; if (Filesystem::pathExists(sprintf($look_for, 'Tests'))) { return sprintf($look_for, 'Tests'); } else { if (Filesystem::pathExists(sprintf($look_for, 'Tests'))) { return sprintf($look_for, 'Tests'); } } if ($dir == $this->projectRoot) { break; } } return false; }
protected function executeChecks() { // NOTE: We've already appended `environment.append-paths`, so we don't // need to explicitly check for it. $path = getenv('PATH'); if (!$path) { $summary = pht('The environmental variable %s is empty. Phabricator will not ' . 'be able to execute some commands.', '$PATH'); $message = pht("The environmental variable %s is empty. Phabricator needs to execute " . "some system commands, like `%s`, `%s`, `%s`, and `%s`. To execute " . "these commands, the binaries must be available in the webserver's " . "%s. You can set additional paths in Phabricator configuration.", '$PATH', 'svn', 'git', 'hg', 'diff', '$PATH'); $this->newIssue('config.environment.append-paths')->setName(pht('%s Not Set', '$PATH'))->setSummary($summary)->setMessage($message)->addPhabricatorConfig('environment.append-paths'); // Bail on checks below. return; } // Users are remarkably industrious at misconfiguring software. Try to // catch mistaken configuration of PATH. $path_parts = explode(PATH_SEPARATOR, $path); $bad_paths = array(); foreach ($path_parts as $path_part) { if (!strlen($path_part)) { continue; } $message = null; $not_exists = false; foreach (Filesystem::walkToRoot($path_part) as $part) { if (!Filesystem::pathExists($part)) { $not_exists = $part; // Walk up so we can tell if this is a readability issue or not. continue; } else { if (!is_dir(Filesystem::resolvePath($part))) { $message = pht("The PATH component '%s' (which resolves as the absolute path " . "'%s') is not usable because '%s' is not a directory.", $path_part, Filesystem::resolvePath($path_part), $part); } else { if (!is_readable($part)) { $message = pht("The PATH component '%s' (which resolves as the absolute path " . "'%s') is not usable because '%s' is not readable.", $path_part, Filesystem::resolvePath($path_part), $part); } else { if ($not_exists) { $message = pht("The PATH component '%s' (which resolves as the absolute path " . "'%s') is not usable because '%s' does not exist.", $path_part, Filesystem::resolvePath($path_part), $not_exists); } else { // Everything seems good. break; } } } } if ($message !== null) { break; } } if ($message === null) { if (!phutil_is_windows() && !@file_exists($path_part . '/.')) { $message = pht("The PATH component '%s' (which resolves as the absolute path " . "'%s') is not usable because it is not traversable (its '%s' " . "permission bit is not set).", $path_part, Filesystem::resolvePath($path_part), '+x'); } } if ($message !== null) { $bad_paths[$path_part] = $message; } } if ($bad_paths) { foreach ($bad_paths as $path_part => $message) { $digest = substr(PhabricatorHash::digest($path_part), 0, 8); $this->newIssue('config.PATH.' . $digest)->setName(pht('%s Component Unusable', '$PATH'))->setSummary(pht('A component of the configured PATH can not be used by ' . 'the webserver: %s', $path_part))->setMessage(pht("The configured PATH includes a component which is not usable. " . "Phabricator will be unable to find or execute binaries located " . "here:" . "\n\n" . "%s" . "\n\n" . "The user that the webserver runs as must be able to read all " . "the directories in PATH in order to make use of them.", $message))->addPhabricatorConfig('environment.append-paths'); } } }
protected function __construct($root, array $config) { $this->projectRoot = $root; $this->projectConfig = $config; $this->localConfig = array(); $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; $local_path = Filesystem::resolvePath('arc/config', $meta_path); if (Filesystem::pathExists($local_path)) { $file = Filesystem::readFile($local_path); if ($file) { $this->localConfig = json_decode($file, true); } } 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) { $local_path = Filesystem::resolvePath('.svn/arc/config', $parent_path); if (Filesystem::pathExists($local_path)) { $file = Filesystem::readFile($local_path); if ($file) { $this->localConfig = json_decode($file, true); } } } } }
public function run() { $argv = $this->getArgument('argv'); if (count($argv) > 1) { throw new ArcanistUsageException("Provide only one path to 'arc liberate'. The path should be a " . "directory where you want to create or update a libphutil library."); } else { if (count($argv) == 0) { $path = getcwd(); } else { $path = reset($argv); } } $is_remap = $this->getArgument('remap'); $is_verify = $this->getArgument('verify'); $path = Filesystem::resolvePath($path); if (Filesystem::pathExists($path) && is_dir($path)) { $init = id(new FileFinder($path))->withPath('*/__phutil_library_init__.php')->find(); } else { $init = null; } if ($init) { if (count($init) > 1) { throw new ArcanistUsageException("Specified directory contains more than one libphutil library. Use " . "a more specific path."); } $path = Filesystem::resolvePath(dirname(reset($init)), $path); } else { $found = false; foreach (Filesystem::walkToRoot($path) as $dir) { if (Filesystem::pathExists($dir . '/__phutil_library_init__.php')) { $path = $dir; break; } } if (!$found) { echo "No library currently exists at that path...\n"; $this->liberateCreateDirectory($path); $this->liberateCreateLibrary($path); } } if ($this->getArgument('remap')) { return $this->liberateRunRemap($path); } if ($this->getArgument('verify')) { return $this->liberateRunVerify($path); } $readable = Filesystem::readablePath($path); echo "Using library root at '{$readable}'...\n"; $this->checkForLooseFiles($path); if ($this->getArgument('all')) { echo "Dropping module cache...\n"; Filesystem::remove($path . '/.phutil_module_cache'); } echo "Mapping library...\n"; // Force a rebuild of the library map before running lint. The remap // operation will load the map before regenerating it, so if a class has // been renamed (say, from OldClass to NewClass) this rebuild will // cause the initial remap to see NewClass and correctly remove includes // caused by use of OldClass. $this->liberateGetChangedPaths($path); $arc_bin = $this->getScriptPath('bin/arc'); do { $future = new ExecFuture('%s liberate --remap -- %s', $arc_bin, $path); $wrote = $future->resolveJSON(); foreach ($wrote as $wrote_path) { echo "Updated '{$wrote_path}'...\n"; } } while ($wrote); echo "Verifying library...\n"; $err = phutil_passthru('%s liberate --verify -- %s', $arc_bin, $path); $do_update = !$err || $this->getArgument('force-update'); if ($do_update) { echo "Finalizing library map...\n"; execx('%s %s', $this->getPhutilMapperLocation(), $path); } if ($err) { if ($do_update) { echo phutil_console_format("<bg:yellow>** WARNING **</bg> Library update forced, but lint " . "failures remain.\n"); } else { echo phutil_console_format("<bg:red>** UNRESOLVED LINT ERRORS **</bg> This library has " . "unresolved lint failures. The library map was not updated. Use " . "--force-update to force an update.\n"); } } else { echo phutil_console_format("<bg:green>** OKAY **</bg> Library updated.\n"); } return $err; }