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');
?>