options['dry-run'] = array('-t','--dry-run',
'action'=>'store_true',
'help'=>'Don\'t actually deploy new code. Just show the files
that would be copied');
$this->options['setup'] = array('-s','--setup',
'action'=>'store_true',
'help'=>'Deploy the setup folder. Useful for deploying for new
installations.');
$this->options['clean'] = array('-C','--clean',
'action'=>'store_true',
'help'=>'Remove files from the destination that are no longer
included in this repository');
$this->options['git'] = array('-g','--git',
'action'=>'store_true',
'help'=>'Use `git ls-files -s` as files source. Eliminates
possibility of deploying untracked files');
$this->options['force'] = array('-f', '--force',
'action'=>'store_true',
'help'=>'Deploy all files, even if they have not changed');
# super(*args);
call_user_func_array(array('parent', '__construct'), func_get_args());
}
function find_root_folder() {
# Hop up to the root folder of this repo
$start = dirname(__file__);
for (;;) {
if (is_file($start . '/main.inc.php')) break;
$start .= '/..';
}
return self::realpath($start);
}
/**
* Removes files from the deployment location that no longer exist in
* the local repository
*/
function clean($local, $destination, $root, $recurse=0, $exclude=false) {
$dryrun = $this->getOption('dry-run', false);
$verbose = $dryrun || $this->getOption('verbose');
$destination = rtrim($destination, '/') . '/';
$contents = glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT);
foreach ($contents as $i=>$file) {
$relative = str_replace($root, "", $file);
if ($this->exclude($exclude, $relative))
continue;
if (is_file($file)) {
$ltarget = $local . '/' . basename($file);
if (is_file($ltarget))
continue;
if ($verbose)
$this->stdout->write("(delete): $file\n");
if (!$dryrun)
unlink($file);
unset($contents[$i]);
}
elseif (in_array(basename($file), array('.','..'))) {
// Doesn't indicate that the folder has contents
unset($contents[$i]);
}
}
if ($recurse) {
$folders = glob(dirname($destination).'/'.basename($destination).'/*',
GLOB_BRACE|GLOB_ONLYDIR|GLOB_NOSORT);
foreach ($folders as $dir) {
if (in_array(basename($dir), array('.','..')))
continue;
$relative = str_replace($root, "", $dir);
if ($this->exclude($exclude, "$relative/"))
continue;
$this->clean(
$local.'/'.basename($dir),
$destination.basename($dir),
$root, $recurse - 1, $exclude);
}
}
if (!$contents || !glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT)) {
if ($verbose)
$this->stdout->write("(delete-folder): $destination\n");
if (!$dryrun)
rmdir($destination);
}
}
function writeManifest($root) {
$lines = array();
foreach ($this->manifest as $F=>$H)
$lines[] = "$H $F";
return file_put_contents($this->include_path.'/.MANIFEST', implode("\n", $lines));
}
function hashContents($file) {
$md5 = md5($file);
$sha1 = sha1($file);
return substr($md5, -20) . substr($sha1, -20);
}
function getEditedContents($src) {
static $short = false;
static $version = false;
if (substr($src, -4) != '.php')
return false;
if (!$short) {
$hash = exec('git rev-parse HEAD');
$short = substr($hash, 0, 7);
}
if (!$version)
$version = exec('git describe');
if (!$short || !$version)
return false;
$source = file_get_contents($src);
$original = crc32($source);
$source = preg_replace('::',
'',
$source);
$source = preg_replace(':]*)/?>:', # ',
$source);
// Set THIS_VERSION
$source = preg_replace("/^(\s*)define\s*\(\s*'THIS_VERSION'.*$/m",
"$1define('THIS_VERSION', '".$version."'); // Set by installer",
$source);
// Set GIT_VERSION
$source = preg_replace("/^(\s*)define\s*\(\s*'GIT_VERSION'.*$/m",
"$1define('GIT_VERSION', '".$short."'); // Set by installer",
$source);
// Disable error display
$source = preg_replace("/^(\s*)ini_set\s*\(\s*'(display_errors|display_startup_errors)'.*$/m",
"$1ini_set('$2', '0'); // Set by installer",
$source);
// return FALSE if the edited contents do not differ from the
// original contents
return $original != crc32($source) ? $source : false;
}
function isChanged($source, $hash=false) {
$local = str_replace($this->source.'/', '', $source);
$hash = $hash ?: $this->hashFile($source);
list($shash, $flag) = explode(':', $this->readManifest($local));
return ($flag === 'rewrite') ? $flag : $shash != $hash;
}
function copyFile($source, $dest, $hash=false, $mode=0644, $contents=false) {
$contents = $contents ?: $this->getEditedContents($source);
if ($contents === false)
// Regular file
return parent::copyFile($source, $dest, $hash, $mode);
if (!file_put_contents($dest, $contents))
$this->fail($dest.": Unable to apply rewrite rules");
$this->updateManifest($source, "$hash:rewrite");
return chmod($dest, $mode);
}
function unpackage($folder, $destination, $recurse=0, $exclude=false) {
$use_git = $this->getOption('git', false);
if (!$use_git)
return parent::unpackage($folder, $destination, $recurse, $exclude);
// Attempt to read from git using `git ls-files` for deployment
if (substr($destination, -1) !== '/')
$destination .= '/';
$source = $this->source;
if (substr($source, -1) != '/')
$source .= '/';
$local = str_replace(array($source, '{,.}*'), array('',''), $folder);
$pipes = array();
$patterns = array();
foreach ((array) $exclude as $x) {
$patterns[] = str_replace($source, '', $x);
}
$X = implode(' --exclude-per-directory=', $patterns);
chdir($source.$local);
if (!($files = proc_open(
"git ls-files -zs --exclude-standard --exclude-per-directory=$X -- .",
array(1 => array('pipe', 'w')),
$pipes
))) {
return parent::unpackage($folder, $destination, $recurse, $exclude);
}
$dryrun = $this->getOption('dry-run', false);
$verbose = $this->getOption('verbose') || $dryrun;
$force = $this->getOption('force');
while ($line = stream_get_line($pipes[1], 255, "\x00")) {
list($mode, $hash, , $path, $pathx, $pathy, $pathz) = preg_split('/\s+/', $line);
if (isset($pathx))
$path = "$path $pathx";
if (isset($pathy))
$path = "$path $pathy";
if (isset($pathz))
$path = "$path $pathz";
$src = $source.$local.$path;
if ($this->exclude($exclude, $src))
continue;
if (!$force && false === ($flag = $this->isChanged($src, $hash)))
continue;
$dst = $destination.$path;
if ($verbose) {
$msg = $dst;
if (is_string($flag))
$msg = "$msg ({$flag})";
$this->stdout->write("$msg\n");
}
if ($dryrun)
continue;
if (!is_dir(dirname($dst)))
mkdir(dirname($dst), 0755, true);
$this->copyFile($src, $dst, $hash, octdec($mode));
}
}
function run($args, $options) {
$this->destination = $args['install-path'];
if (!is_dir($this->destination))
if (!@mkdir($this->destination, 0751, true))
die("Destination path does not exist and cannot be created");
$this->destination = self::realpath($this->destination).'/';
# Determine if this is an upgrade, and if so, where the include/
# folder is currently located
$upgrade = file_exists("{$this->destination}/main.inc.php");
# Get the current value of the INCLUDE_DIR before overwriting
# bootstrap.php
$include = ($upgrade) ? $this->get_include_dir()
: ($options['include'] ? $options['include']
: rtrim($this->destination, '/')."/include");
$this->include_path = $include = rtrim($include, '/').'/';
# Locate the upload folder
$root = $this->source = $this->find_root_folder();
$rootPattern = str_replace("\\","\\\\", $root); //need for windows case
# Prime the manifest system
$this->readManifest($this->destination.'/.MANIFEST');
$exclusions = array("$rootPattern/include/*", "$rootPattern/.git*",
"*.sw[a-z]","*.md", "*.txt");
if (!$options['setup'])
$exclusions[] = "$rootPattern/setup/*";
# Unpack everything but the include/ folder
$this->unpackage("$root/{,.}*", $this->destination, -1,
$exclusions);
# Unpack the include folder
$this->unpackage("$root/include/{,.}*", $include, -1,
array("*/include/ost-config.php", "*.sw[a-z]"));
if (!$options['dry-run']) {
if ($include != "{$this->destination}/include/")
$this->change_include_dir($include);
}
if ($options['clean']) {
// Clean everything but include folder first
$local_include = str_replace($this->destination, "", $include);
$this->clean($root, $this->destination, $this->destination, -1,
array($local_include, "setup/"));
$this->clean("$root/include", $include, $include, -1,
array("ost-config.php","settings.php","plugins/",
"*/.htaccess", ".MANIFEST"));
}
if (!$options['dry-run'])
$this->writeManifest($this->destination);
}
}
Module::register('deploy', 'Deployment');
?>