mirror of
https://we.phorge.it/source/arcanist.git
synced 2025-04-04 08:28:20 +02:00
Summary: Ref T13395. Moves all remaining code in "libphutil/" into "arcanist/". Test Plan: Ran various arc workflows, although this probably has some remaining rough edges. Maniphest Tasks: T13395 Differential Revision: https://secure.phabricator.com/D20980
365 lines
7.8 KiB
PHP
365 lines
7.8 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Find files on disk matching criteria, like the 'find' system utility. Use of
|
|
* this class is straightforward:
|
|
*
|
|
* // Find PHP files in /tmp
|
|
* $files = id(new FileFinder('/tmp'))
|
|
* ->withType('f')
|
|
* ->withSuffix('php')
|
|
* ->find();
|
|
*
|
|
* @task create Creating a File Query
|
|
* @task config Configuring File Queries
|
|
* @task exec Executing the File Query
|
|
* @task internal Internal
|
|
*/
|
|
final class FileFinder extends Phobject {
|
|
|
|
private $root;
|
|
private $exclude = array();
|
|
private $paths = array();
|
|
private $name = array();
|
|
private $suffix = array();
|
|
private $nameGlobs = array();
|
|
private $type;
|
|
private $generateChecksums = false;
|
|
private $followSymlinks;
|
|
private $forceMode;
|
|
|
|
/**
|
|
* Create a new FileFinder.
|
|
*
|
|
* @param string Root directory to find files beneath.
|
|
* @return this
|
|
* @task create
|
|
*/
|
|
public function __construct($root) {
|
|
$this->root = rtrim($root, '/');
|
|
}
|
|
|
|
/**
|
|
* @task config
|
|
*/
|
|
public function excludePath($path) {
|
|
$this->exclude[] = $path;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @task config
|
|
*/
|
|
public function withName($name) {
|
|
$this->name[] = $name;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @task config
|
|
*/
|
|
public function withSuffix($suffix) {
|
|
$this->suffix[] = $suffix;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @task config
|
|
*/
|
|
public function withPath($path) {
|
|
$this->paths[] = $path;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @task config
|
|
*/
|
|
public function withType($type) {
|
|
$this->type = $type;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @task config
|
|
*/
|
|
public function withFollowSymlinks($follow) {
|
|
$this->followSymlinks = $follow;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @task config
|
|
*/
|
|
public function setGenerateChecksums($generate) {
|
|
$this->generateChecksums = $generate;
|
|
return $this;
|
|
}
|
|
|
|
public function getGenerateChecksums() {
|
|
return $this->generateChecksums;
|
|
}
|
|
|
|
public function withNameGlob($pattern) {
|
|
$this->nameGlobs[] = $pattern;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @task config
|
|
* @param string Either "php", "shell", or the empty string.
|
|
*/
|
|
public function setForceMode($mode) {
|
|
$this->forceMode = $mode;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @task internal
|
|
*/
|
|
public function validateFile($file) {
|
|
|
|
if ($this->name) {
|
|
$matches = false;
|
|
foreach ($this->name as $curr_name) {
|
|
if (basename($file) === $curr_name) {
|
|
$matches = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$matches) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ($this->nameGlobs) {
|
|
$name = basename($file);
|
|
|
|
$matches = false;
|
|
foreach ($this->nameGlobs as $glob) {
|
|
$glob = addcslashes($glob, '\\');
|
|
if (fnmatch($glob, $name)) {
|
|
$matches = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$matches) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ($this->suffix) {
|
|
$matches = false;
|
|
foreach ($this->suffix as $suffix) {
|
|
$suffix = addcslashes($suffix, '\\?*');
|
|
$suffix = '*.'.$suffix;
|
|
if (fnmatch($suffix, $file)) {
|
|
$matches = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$matches) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ($this->paths) {
|
|
$matches = false;
|
|
foreach ($this->paths as $path) {
|
|
if (fnmatch($path, $this->root.'/'.$file)) {
|
|
$matches = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$matches) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$fullpath = $this->root.'/'.ltrim($file, '/');
|
|
if (($this->type == 'f' && is_dir($fullpath))
|
|
|| ($this->type == 'd' && !is_dir($fullpath))) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @task internal
|
|
*/
|
|
private function getFiles($dir) {
|
|
$found = Filesystem::listDirectory($this->root.'/'.$dir, true);
|
|
$files = array();
|
|
if (strlen($dir) > 0) {
|
|
$dir = rtrim($dir, '/').'/';
|
|
}
|
|
foreach ($found as $filename) {
|
|
// Only exclude files whose names match relative to the root.
|
|
if ($dir == '') {
|
|
$matches = true;
|
|
foreach ($this->exclude as $exclude_path) {
|
|
if (fnmatch(ltrim($exclude_path, './'), $dir.$filename)) {
|
|
$matches = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!$matches) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ($this->validateFile($dir.$filename)) {
|
|
$files[] = $dir.$filename;
|
|
}
|
|
|
|
if (is_dir($this->root.'/'.$dir.$filename)) {
|
|
foreach ($this->getFiles($dir.$filename) as $file) {
|
|
$files[] = $file;
|
|
}
|
|
}
|
|
}
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* @task exec
|
|
*/
|
|
public function find() {
|
|
|
|
$files = array();
|
|
|
|
if (!is_dir($this->root) || !is_readable($this->root)) {
|
|
throw new Exception(
|
|
pht(
|
|
"Invalid %s root directory specified ('%s'). Root directory ".
|
|
"must be a directory, be readable, and be specified with an ".
|
|
"absolute path.",
|
|
__CLASS__,
|
|
$this->root));
|
|
}
|
|
|
|
if ($this->forceMode == 'shell') {
|
|
$php_mode = false;
|
|
} else if ($this->forceMode == 'php') {
|
|
$php_mode = true;
|
|
} else {
|
|
$php_mode = (phutil_is_windows() || !Filesystem::binaryExists('find'));
|
|
}
|
|
|
|
if ($php_mode) {
|
|
$files = $this->getFiles('');
|
|
} else {
|
|
$args = array();
|
|
$command = array();
|
|
|
|
$command[] = 'find';
|
|
if ($this->followSymlinks) {
|
|
$command[] = '-L';
|
|
}
|
|
$command[] = '.';
|
|
|
|
if ($this->exclude) {
|
|
$command[] = $this->generateList('path', $this->exclude).' -prune';
|
|
$command[] = '-o';
|
|
}
|
|
|
|
if ($this->type) {
|
|
$command[] = '-type %s';
|
|
$args[] = $this->type;
|
|
}
|
|
|
|
if ($this->name) {
|
|
$command[] = $this->generateList('name', $this->name, 'name');
|
|
}
|
|
|
|
if ($this->suffix) {
|
|
$command[] = $this->generateList('name', $this->suffix, 'suffix');
|
|
}
|
|
|
|
if ($this->paths) {
|
|
$command[] = $this->generateList('path', $this->paths);
|
|
}
|
|
|
|
if ($this->nameGlobs) {
|
|
$command[] = $this->generateList('name', $this->nameGlobs);
|
|
}
|
|
|
|
$command[] = '-print0';
|
|
|
|
array_unshift($args, implode(' ', $command));
|
|
list($stdout) = newv('ExecFuture', $args)
|
|
->setCWD($this->root)
|
|
->resolvex();
|
|
|
|
$stdout = trim($stdout);
|
|
if (!strlen($stdout)) {
|
|
return array();
|
|
}
|
|
|
|
$files = explode("\0", $stdout);
|
|
|
|
// On OSX/BSD, find prepends a './' to each file.
|
|
foreach ($files as $key => $file) {
|
|
// When matching directories, we can get "." back in the result set,
|
|
// but this isn't an interesting result.
|
|
if ($file == '.') {
|
|
unset($files[$key]);
|
|
continue;
|
|
}
|
|
|
|
if (substr($files[$key], 0, 2) == './') {
|
|
$files[$key] = substr($files[$key], 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$this->generateChecksums) {
|
|
return $files;
|
|
} else {
|
|
$map = array();
|
|
foreach ($files as $line) {
|
|
$fullpath = $this->root.'/'.ltrim($line, '/');
|
|
if (is_dir($fullpath)) {
|
|
$map[$line] = null;
|
|
} else {
|
|
$map[$line] = md5_file($fullpath);
|
|
}
|
|
}
|
|
return $map;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @task internal
|
|
*/
|
|
private function generateList(
|
|
$flag,
|
|
array $items,
|
|
$mode = 'glob') {
|
|
|
|
foreach ($items as $key => $item) {
|
|
// If the mode is not "glob" mode, we're going to escape glob characters
|
|
// in the pattern. Otherwise, we escape only backslashes.
|
|
if ($mode === 'glob') {
|
|
$item = addcslashes($item, '\\');
|
|
} else {
|
|
$item = addcslashes($item, '\\*?');
|
|
}
|
|
|
|
if ($mode === 'suffix') {
|
|
$item = '*.'.$item;
|
|
}
|
|
|
|
$item = (string)csprintf('%s %s', '-'.$flag, $item);
|
|
|
|
$items[$key] = $item;
|
|
}
|
|
|
|
$items = implode(' -o ', $items);
|
|
return '"(" '.$items.' ")"';
|
|
}
|
|
}
|