#!/usr/bin/env php
<?php
# SVN自动合并工具
#
# 支持的功能
#
# * 合并版本
# * 将一个目录文件全部合并到一个带svn版本控制的目录，多余的文件会执行svn delete命令清除Index
#
# 通常我们会在working版本(或dev版本)中开发代码，然后把修改的内容合并到trunk中，这个工具就是自动合并指定版本到trunk目录用的
#
# 可自动将开发版本文件合并到trunk文件中
# 可查看未合并的log信息
#
# 使用方法
# svn-tools merge -v 1033:1036              把1034,1035,1036版本合并进trunk（注意不含1033版本）
# svn-tools merge -v 1055                   把1055版本合并进trunk
# svn-tools log                             查看没有合并的版本记录
# svn-tools sync-dir -f ~/Sites/test1/ -t ~/Sites/test2/    将test1目录同步到test2目录
#
# svn-tools sync-dir 的其它参数
# * -a 或 --all 将完整同步到目标目录，如果某个文件或文件夹在from目录中不存在但在目标文件中存在，则会用svn delete命令删除掉目标文件中多余的文件

$args = $_SERVER['argv'];

if (count($args)==1)
{
    Svn_Tools::usage();
}

$obj = new Svn_Tools();
if ($args[1]=='merge')
{
    $obj->action_merge();
}
else if ($args[1]=='log')
{
    $obj->analyze_merge_log();
}
else if ($args[1]=='sync-dir')
{
    $obj->sync_dir();
}
else
{
    Svn_Tools::usage();
}


class Svn_Tools
{
    /**
     * 配置
     *
     * @var array
     */
    protected static $config = array();

//    const SVN_LOG_REVISION_PATTERN = '#^r(\d+)\s+\|\s+(\w+)\s+\|#';

    /**
     * 合并日志格式
     *
     * @var string
     */
    protected static $SVN_LOG_TOP_MERGED_PATTERN = '#^merge\s+working\((\d+)\)\s*->\s*trunk$#';

    /**
     * 已全部合并日志格式
     *
     * @var string
     */
    protected static $SVN_LOG_MERGED_PATTERN = '#^merge\s+working:(\d+):(\d+)\s*->\s*trunk$#';

    /**
     * SVN 日志缓存目录
     *
     * @var string
     */
    protected static $SVN_LOG_CACHEFILE;

    /**
     * SVN 合并TRUNK路径，支持本地目录
     *
     * @var string
     */
    protected static $SVNMERGE_TRUNK_DIR;

    /**
     * SVN 工作路径，支持本地目录
     *
     * @var string
     */
    protected static $SVNMERGE_WORKING_URL;

    public function __construct()
    {
        self::$config = parse_ini_file('svn-tools.ini');

        if (!self::$config)
        {
            self::$config = array();
        }

        if (!isset(self::$config['verbose']))self::$config['verbose'] = 0;
        if (!isset(self::$config['path']))self::$config['path'] = '/';
    }

    public static function usage($msg = null)
    {
        if ( isset($msg) )
        {
            printf("\x1b[33m%s\x1b[39m\n\n", $msg);
        }

        $name = basename(__FILE__);
        printf("Usage: %s [OPTIONS]

Avaliable options:
    -v, --verbose           verbose mode
    -h, --help              show this help info
    -p, --path PATH         show merge info of PATH only
        --trunk-url=<URL>
        --working-url=<URL>

Examples:
    %s merge -v 1033:1036          \x1b[34m(把1034,1035,1036版本合并进trunk ，注意不包含1033)\x1b[39m
    %s merge -v 1055               \x1b[34m(把1055版本合并进trunk)\x1b[39m
    %s merge --trunk-url=<TRUNK-URL>  --working-url=<WORKING-URL> -v
    %s merge --trunk-url=<TRUNK-URL>  --working-url=<WORKING-URL> -v -p wwwroot
    %s sync-dir -f ~/Sites/test1/ -t ~/Sites/test2/         \x1b[34m(把test1目录文件合并到test2目录，不删除目标目录中多余文件)\x1b[39m
    %s sync-dir -f ~/Sites/test1/ -t ~/Sites/test2/ --all   \x1b[34m(把test1目录文件合并到test2目录，删除目标目录中多余文件)\x1b[39m
", $name, $name , $name, $name , $name , $name , $name);
        exit();
    }


    /**
     * 合并版本
     * Usage:-c REV [-c REV] -m MSG [--no-comment] [--dry-run]
     * -c 版本 -m 提交内容 --no-comment不自动提交 --dry-run 只输出命令
     *
     */
    public function action_merge()
    {
        self::$SVNMERGE_TRUNK_DIR = self::$config['trunk-dir'];
        self::$SVNMERGE_WORKING_URL = self::$config['working-url'];

        if ( !is_dir(self::$SVNMERGE_TRUNK_DIR) )
        {
            echo "trunk-dir 错误，请配置svn-tools.ini中trunk-dir参数\n";
            exit;
        }


        $argv = $_SERVER['argv'];
        unset($argv[0]);
        unset($argv[1]);

        $shortopts = "c:m:";
        $longopts = array
        (
            'dry-run',
            'no-comment',
        );
        $opts = self::getopt($shortopts, $longopts, $argv);

        if ( ! $opts )
        {
            echo "Usage:-c REV [-c REV] -m MSG [--no-comment] [--dry-run]\n";
        }

        $revisions = array();
        $message = '';
        $GLOBALS['dry-run'] = false;
        foreach ( $opts as $opt => $val )
        {
            switch ( $opt )
            {
                case 'c' :
                    # all to array
                    if ( is_string($val) )
                    {
                        $val = array($val);
                    }
                    $revisions = array_map(array($this,'revision_converter'), $val);
                    break;
                case 'm' :
                    $message = $val;
                    break;
                case 'dry-run' :
                    $GLOBALS['dry-run'] = true;
                    break;
                case 'no-comment' :
                    break;
                default :
                    printf("Unknown option: %s.\n", $opt);
                    break;
            }
        }

        if ( empty($revisions) )
        {
            printf("Usage: %s -c REV [-c REV] -m MSG [--dry-run]\n", basename(__FILE__));
            exit(0);
        }

        chdir(self::$SVNMERGE_TRUNK_DIR);

        # collate
        sort($revisions);

        # up first
        self::do_a_up();

        # merge one by one
        foreach ( $revisions as $revision )
        {
            self::do_a_merge($revision,$message);
        }

        if ( !isset($opts['no-comment']) )
        {
            # commit
            self::do_a_commit($revisions, $message);
        }

    }

    /**
     * 查看合并情况，并显示未合并版本
     *
     * * Top Merge:
     * 	Pattern: merge working($a) -> trunk
     * 	Desc: $a and revisions under $a from working all merged into trunk.
     * * Common Merge:
     * 	Pattern: merge working:$b:$c -> trunk
     * 	Desc: Revisions from $b to $c from working all merged into trunk.
     *
     */
    public function analyze_merge_log()
    {
        self::$SVN_LOG_CACHEFILE = $_SERVER['HOME'] . '/.svnlogcache.dat';

        /*
         * TODO 暂不支持
         *
        if ( isset(self::$config['svn_log_top_merged_pattern']) && self::$config['svn_log_top_merged_pattern'] )
        {
            self::$SVN_LOG_TOP_MERGED_PATTERN = self::$config['svn_log_top_merged_pattern'];
        }

        if ( isset(self::$config['svn_log_merged_pattern']) && self::$config['svn_log_merged_pattern'] )
        {
            self::$SVN_LOG_TOP_MERGED_PATTERN = self::$config['svn_log_merged_pattern'];
        }
        */


        $argv = $_SERVER['argv'];
        unset($argv[0]);
        unset($argv[1]);

        $s  = '';
        $s .= 'p:';     //-p ...
        $s .= 'v';      //-a
        $s .= 'h';      //-h

        $longopts  = array
        (
            'path:',           //--path=...
            'trunk-url:',      //--trunk-url=...
            'working-url:',    //--working-url=...
            'verbose',         //--verbose
            'help',            //--help
        );
        $options = self::getopt($s, $longopts, $argv);

        if (is_array($opts))foreach ($opts as $opt => $val)
        {
            switch ($opt)
            {
                case 'verbose' :
                case 'v' :
                    self::$config['verbose'] = true;
                    break;
                case 'path' :
                case 'p' :
                    if ( $val[0] != '/' )
                    {
                        $val = '/' . $val;
                    }
                    self::$config['path'] = $val;
                    break;
                case 'trunk-url' :
                    self::$config['trunk-url'] = $val;
                    break;
                case 'working-url' :
                    self::$config['working-url'] = $val;
                    break;
                case 'help' :
                case 'h' :
                    self::usage();
                    break;
                default :
                    printf("Unknown option: %s.\n", $opt);
                    break;
            }
        }

        // check
        if ( !isset(self::$config['trunk-url']) || !self::$config['trunk-url'] )
        {
            self::usage('Please specify trunk url with --trunk-url=<URL> option.');
        }
        if ( !isset(self::$config['working-url']) || !self::$config['working-url'] )
        {
            self::usage('Please specify trunk url with --trunk-url=<URL> option.');
        }

        // run
        while ( true )
        {
            system('clear');

            $working_revisions = array();
            $working_logs = array();
            $merge_logs = array();
            $merge_top_revisions = array();

            // get all working revisions
            $working_logs = self::fetch_logs(self::$config['working-url'] . self::$config['path']);
            $working_revisions = array_keys($working_logs);
            sort($working_revisions, SORT_NUMERIC);
            if ( empty($working_revisions) )
            {
                printf("no logs for %s\n", self::$config['path']);
                die();
            }

            /*
             * get merge logs of trunk
             *
             * @desc
             * 	 each merge log has two revisions: 'from' and 'to'
             */
            $trunk_logs = self::fetch_logs(self::$config['trunk-url']);
            if ( is_array($trunk_logs) ) foreach ( $trunk_logs as $trunk_log )
            {
                $msg = $trunk_log['msg'];
                foreach ( explode("\n", $msg) as $line )
                {
                    if ( preg_match(self::$SVN_LOG_MERGED_PATTERN, $line, $matches) )
                    {

                        $merge_logs[] = array('from' => (int)$matches[1], 'to' => (int)$matches[2]);
                    }
                    else if ( preg_match(self::$SVN_LOG_TOP_MERGED_PATTERN, $line, $matches) )
                    {
                        $merge_top_revisions[] = (int)$matches[1];
                    }
                    else
                    {
                        $line . "\n";
                    }
                }
            }

            self::analyzer($working_revisions, $merge_logs, $merge_top_revisions, $working_logs);

            if ( !posix_isatty(STDOUT) )
            {
                break;
            }
            else
            {
                printf("\n");
                printf("Press any key to refresh (CTRL-D to exit):\n");
                $c = fgetc(STDIN);
                if ( $c === false )
                {
                    break;
                }
                else
                {
                    continue;
                }
            }
        }
    }

    protected static function analyzer($working_revisions, $merge_logs, $merge_top_revisions, $working_logs)
    {
        // get top merged revision (0 is initial revision)
        $top_merged_revision = count($merge_top_revisions) ? max($merge_top_revisions) : 1;

        /**
         * Collates merge logs
         *
         * @desc
         * collate all logs before $top_merged_revision into one log that is "0:$top_merged_revision"
         * @see
         * A -> B is merged means that:
         * if  (A < B)
         * A+1, A+2, A+3, ... , B-2, B-1, B revisions are merged.
         * else if (A > B)
         * A, A-1,  A-2, ..., B+2, B+1 revisions are unmerged.
         * else
         * nothing is done.
         * fi
         */
        foreach ( $merge_logs as $key => $merge_log )
        {
            if ( $merge_log['from'] < $top_merged_revision || $merge_log['to'] < $top_merged_revision ) unset($merge_logs[$key]);
        }
        $merge_logs[] = array
        (
            'from' => 0,
            'to'   => $top_merged_revision
        );

        /**
         * Calculate merged revision
         */
        $merged_revisions = array();
        foreach ( $merge_logs as $merge_log )
        {
            $from = $merge_log['from'];
            $to = $merge_log['to'];
            if ( $from < $to )
            {
                $merged_revisions = array_merge($merged_revisions, range($from + 1, $to));
            }
            else if ( $from > $to )
            {
                $merged_revisions = array_diff($merged_revisions, range($to + 1, $from));
            }
            else
            {
                // do nothing
                continue;
            }
        }
        # sort
        sort($merged_revisions, SORT_NUMERIC);

        // output report
        $min_revision = min($working_revisions);
        $max_revision = max($working_revisions);
        printf("### Merge Report For %s ###\n", self::$config['path']);
        printf("Min working revision: %d \n", $min_revision);
        printf("Max working revision: %d \n", $max_revision);
        printf("Top-Merged revision:%d \n", $top_merged_revision);

        // loop
        for( $i = $min_revision; $i <= $max_revision; $i ++ )
        {
            // skip non-working revisions
            if ( ! self::my_in_array($i, $working_revisions) ) continue;

            $from = $to = $i;

            // check if current revision is merged or not
            if ( ! self::my_in_array($i, $merged_revisions, true) )
            {
                // if not merged, then the backward first merged is [from]
                do
                {
                    $from --;
                    if ( self::my_in_array($from, $merged_revisions, true) && $from >= $min_revision )
                    {
                        break;
                    }
                }
                while ( true );

                // and the forward last not merged revision is [to]
                while ( true )
                {
                    if ( ! self::my_in_array($to + 1, $merged_revisions, true) && ($to + 1) <= $max_revision )
                    {
                        $to ++;
                    }
                    else
                    {
                        break;
                    }
                }
                printf("  %d:%d not merged\n", $from, $to);
                if ( self::$config['verbose'] )
                {
                    foreach ( range($from + 1, $to) as $non_merged )
                    {
                        if ( isset($working_logs[$non_merged]) )
                        {
                            printf("    %d  %s\n", $non_merged, $working_logs[$non_merged]['author']);
                        }
                    }
                }
            }
            else
            {
                // merged
            }

            $i = $to;
        }
    }

    /**
     * fetch logs from svn path (url, etc)
     *
     * @param string svn path
     * @return array logs, sort by revision asc
     */
    protected static function fetch_logs($path)
    {
        static $logs = array(); /* path => logs */

        // init from cache
        if ( empty($logs) && file_exists(self::$SVN_LOG_CACHEFILE) )
        {
            $logs = @unserialize(file_get_contents(self::$SVN_LOG_CACHEFILE));
        }

        // read diff
        $args = array();
        $args[] = $path;
        if ( ! empty($logs[$path]) )
        {
            $last_revision = max(array_keys($logs[$path]));
            $args[] = '-r';
            $args[] = sprintf('%d:HEAD', $last_revision);
        }
        $args[] = '--xml';
        $cmd = 'svn log ' . implode(' ', array_map('escapeshellarg', $args));

        $descriptorspec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'));
        $process = proc_open($cmd, $descriptorspec, $pipes);
        if ( ! is_resource($process) )
        {
            die('failed to create process');
        }
        $xmldata = stream_get_contents($pipes[1]);
        proc_close($process);

        $objDOM = new DOMDocument();
        $objDOM->loadXML($xmldata);
        $objDOMXPath = new DOMXPath($objDOM);
        $expr = "/log/logentry";
        $nodeList = $objDOMXPath->query($expr);
        for( $i = 0; $i < $nodeList->length; $i ++ )
        {
            $node = $nodeList->item($i);
            $logs[$path][(int)$node->getAttribute('revision')] = array('msg' => $node->getElementsByTagName('msg')->item(0)->textContent, 'author' => $node->getElementsByTagName('author')->item(0)->textContent);
        }

        ksort($logs[$path]);

        // update cache
        file_put_contents(self::$SVN_LOG_CACHEFILE, serialize($logs));

        return $logs[$path];
    }

    /**
     * Checks if a value exists in ascending sorted array using binary search
     *
     * @param mixed needle to search
     * @param array array in asc sort (from lowest to highest)
     * @param boolean using strict comparions with or not
     * @return boolean true if needle is found in array, false otherwise
     */
    protected static function my_in_array($needle, $haystack, $strict = false)
    {
        $top = count($haystack) - 1;
        $btm = 0;

        while ( $top >= $btm )
        {
            $p = floor(($top + $btm) / 2);
            if ( $haystack[$p] < $needle )
            {
                $btm = $p + 1;
            }
            else if ( $haystack[$p] > $needle )
            {
                $top = $p - 1;
            }
            else
            {
                if ( $strict )
                {
                    return $needle === $haystack[$p];
                }
                else
                {
                    return $needle == $haystack[$p];
                }
            }
        }

        return false;
    }

    # process
    protected static function revision_converter($val)
    {
        if ( substr($val, 0, 1) === 'r' )
        {
            # rxxxx format
            $revision = intval(substr($val, 1));
        }
        else
        {
            $revision = intval($val);
        }
        return $revision;
    }

    protected static function do_a_up()
    {
        // update
        $cmd = 'svn up';
        self::exec_cmd($cmd);
    }

    protected static function do_a_merge($revision, &$message )
    {
        if ( ! is_int($revision) )
        {
            throw new Exception();
        }

        # merge
        $cmd_tpl = 'svn merge \'%s\' --ignore-ancestry -c %d';
        $cmd = sprintf($cmd_tpl, self::$SVNMERGE_WORKING_URL, $revision);

        self::exec_cmd($cmd);


        # 获取原始版本log
        $cmd_tpl = 'svn log \'%s\' -c %d';
        $cmd = sprintf($cmd_tpl, self::$SVNMERGE_WORKING_URL, $revision);

        exec($cmd, $logs);
        if ( $logs && count($logs)>=4 )
        {
            $message .= "\n".'revision:'.$revision.' >>>>>>'."\n".'    ';
            for( $i=3;$i<=count($logs)-2;$i++ )
            {
                $message .= $logs[$i] . '。';
            }
        }
    }

    protected static function do_a_commit($revisions, $message)
    {
        $message .= "\n";
        foreach ( $revisions as $revision )
        {
            $message .= sprintf("merge working:%d:%d -> trunk\n", $revision - 1, $revision);
        }
        $cmd_tpl = 'svn ci -m %s';
        $cmd = sprintf($cmd_tpl, var_export(trim($message),true));

        self::exec_cmd($cmd);
    }

    protected static function exec_cmd($cmd)
    {
        if ( $GLOBALS['dry-run'] )
        {
            printf("Cmd: %s\n", $cmd);
        }
        else
        {
            system($cmd, $exit_code);
            if ( $exit_code !== 0 )
            {
                throw new Exception();
            }
        }
    }

    /**
     * 获取shell命令下参数
     *
     * 与getopt()相似，window下支持
     *
     * @param string $options
     * @param array $global_options
     * @return array
     */
    protected static function getopt($options, array $global_options = null, $argv = null)
    {
        if (null===$argv)
        {
            $argv = array_slice($_SERVER['argv'], 1);
        }
        $argv = array_values($argv);


        $len = strlen($options);
        $my_options = array();

        $sl = 0;
        for($i=$len-1; $i>=0; $i--)
        {
            $key = $options[$i];
            if ($key==':')
            {
                $sl++;
                continue;
            }

            # 只接受a-zA-Z0-9
            if (preg_match('#[^a-zA-Z0-9]+#', $key))continue;

            if ($sl===0)
            {
                $my_options[$key] = 1;
            }
            elseif ($sl===1)
            {
                $my_options[$key.':'] = 1;
            }
            else
            {
                $my_options[$key.'::'] = 1;
            }

            $sl = 0;
        }

        $my_global_options = array();
        foreach($global_options as $item)
        {
            $my_global_options[$item] = 1;
        }

        $rs = array();

        foreach($argv as $k=>$item)
        {
            if (preg_match('#^\-(\-)?([a-z0-9\-]+)=(.*)$#i', $item, $m))
            {
                $key   = $m[2];
                $value = $m[3];
                if ($m[1]=='-')
                {
                    if (!isset($my_global_options[$key.'::']))
                    {
                        continue;
                    }
                }
                else
                {
                    if (!isset($my_options[$key.'::']))
                    {
                        continue;
                    }
                }
            }
            elseif (preg_match('#^\-(\-)?([a-z0-9\-]+)$#i', $item, $m))
            {
                $key  = $m[2];
                if ($m[1]=='-')
                {
                    if (isset($my_global_options[$key]))
                    {
                        $value = false;
                    }
                    elseif (isset($my_global_options[$key.':']))
                    {
                        $value = $argv[$k+1];
                    }
                    else
                    {
                        continue;
                    }
                }
                else
                {
                    if (isset($my_options[$key]))
                    {
                        $value = false;
                    }
                    elseif (isset($my_options[$key.':']))
                    {
                        $value = $argv[$k+1];
                    }
                    elseif (isset($my_options[$key.'::']))
                    {
                        $value = false;
                    }
                    else
                    {
                        continue;
                    }
                }
            }
            else
            {
                continue;
            }

            if (isset($rs[$key]))
            {
                $rs[$key] = (array)$rs[$key];
                $rs[$key][] = $value;
            }
            else
            {
                # 赋值
                $rs[$key] = $value;
            }
        }

        return $rs;
    }


    /**
     * 同步目录
     */
    public function sync_dir()
    {
        $argv = $_SERVER['argv'];
        unset($argv[0]);
        unset($argv[1]);

        $s  = 'f:';     //-f ...
        $s .= 't:';     //-t ...
        $s .= 'a';      //-a

        $longopts  = array
        (
            'from:',    //--from=...
            'to:',      //--to=...
            'help',
            't::',
            'all',      //--all
        );
        $options = self::getopt($s, $longopts, $argv);

        $from = null;
        $to   = null;
        $this->is_all  = false;

        foreach ($options as $key=>$value)
        {
            switch ($key)
            {
                case 'f':
                case 'from':
                    $from = $value;
                    break;
                case 't';
                case 'to';
                    $to = $value;
                    break;
                case 'a':
                case 'all':
                    $this->is_all = true;
                    break;
                default:
                    break;
            }
        }

        if (!$from)
        {
            echo "缺少参数，需要-f或--f参数\n";
            exit;
        }
        if (!$to)
        {
            echo "缺少参数，需要-t或--to参数\n";
            exit;
        }


        if (!is_dir($from))
        {
            echo "指定的from目录不存在\n";
            exit;
        }

        if (!is_dir($to))
        {
            echo "指定的to目录不存在，是否创建？(yes|no)\n";
            $stdin = trim(strtolower(fgets(STDIN)));
            if ($stdin=='yes'|| $stdin=='y')
            {
                $r = $this->create_dir($to);
                if ($r)
                {
                    echo "创建目录{$to}成功\n";
                }
                else
                {
                    echo "目录创建失败,操作终止\n";
                    exit;
                }
            }
            else
            {
                echo "停止.\n";
                exit;
            }
        }

        $from = realpath($from) . '/';
        $to   = realpath($to) . '/';

        $this->glob_dir($from, $to);

        echo "\nall done.\n";
    }

    private function glob_dir($from, $to)
    {
        $all_to_exist_files = array();

        # 目标目录存在
        if (is_dir($to))
        {
            $handle = opendir($to);
            while (($file_name = readdir($handle))!==false)
            {
                if ($file_name=='.'||$file_name=='..')continue;
                $file = $to . $file_name;

                $file_name = basename($file);
                if ($file_name=='.svn'||$file_name=='.git')continue;

                $all_to_exist_files[$file_name] = $file;
            }

            closedir($handle);
            unset($handle);
        }
        else
        {
            # 创建一个目录
            $this->create_dir($to);
        }

        $handle = opendir($from);
        while (($file_name = readdir($handle))!==false)
        {
            if ($file_name=='.'||$file_name=='..')continue;

            $file = $from . $file_name;

            if ($file_name=='.svn'||$file_name=='.git')continue;

            # 移除目标目录中文件列表
            if (isset($all_to_exist_files[$file_name]))unset($all_to_exist_files[$file_name]);

            if (is_dir($file))
            {
                # 递归的处理目录
                $this->glob_dir($from.$file_name.'/', $to.$file_name.'/');
                continue;
            }

            $to_file = $to . $file_name;

            if (is_file($to_file))
            {
                if (!$this->is_all && md5_file($to_file)==md5_file($file))
                {
                    # 不是完整复制，且md5一样就不复制
                    continue;
                }

                if (@copy($file, $to_file))
                {
                    echo "C         $to_file\n";
                }
                else
                {
                    echo "C  fail   $to_file\n";
                }
            }
            else
            {
                if (substr($file_name, 0, 1)=='.')
                {
                    if ($this->is_all)
                    {
                        # 隐藏的文件
                        if (@copy($file, $to_file))
                        {
                            echo "C         $to_file\n";
                        }
                        else
                        {
                            echo "C  fail   $to_file\n";
                        }
                    }
                }
                else
                {
                    if (@copy($file, $to_file))
                    {
                        echo "C         $to_file\n";
                    }
                    else
                    {
                        echo "C  fail   $to_file\n";
                    }
                }
            }
        }
        closedir($handle);
        unset($handle);



        if ($all_to_exist_files)
        {
            # 这些文件或目录都是不存在的了，需要删除
            if (is_dir($to.'.svn/'))
            {
                # 通过SVN来删除
                foreach($all_to_exist_files as $file)
                {
                    $cmd = "svn delete $file --force";
                    echo shell_exec($cmd);

                    # 通过SVN还没有删除，可能本身就不在版本控制中
                    if (is_file($file))
                    {
                        if (@unlink($file))
                        {
                            echo "D         $to_file\n";
                        }
                        else
                        {
                            echo "D  fail   $to_file\n";
                        }
                    }
                    elseif (is_dir($file))
                    {
                        $this->remove_dir($file);
                    }
                }
            }
            else
            {
                # 直接删除
                foreach($all_to_exist_files as $file)
                {
                    if (is_dir($file))
                    {
                        $this->remove_dir($file);
                    }
                    else
                    {
                        if (@unlink($file))
                        {
                            echo "D         $to_file\n";
                        }
                        else
                        {
                            echo "D  fail   $to_file\n";
                        }
                    }
                }
            }
        }
    }


    private function remove_dir($dir)
    {
        if (!is_dir($dir))
        {
            return true;
        }

        $realpath = realpath($dir);

        if (!$realpath || $realpath == '/')
        {
            return true;
        }

        $handle = opendir($realpath);
        while (($file = readdir($handle)) !== false)
        {
            if ( $file != '.' && $file != '..' )
            {
                $tmp_dir = $dir . '/' . $file;
                is_dir($tmp_dir)?$this->remove_dir($tmp_dir):@unlink($tmp_dir);
            }
        }
        closedir($handle);

        if (@rmdir($dir))
        {
            echo "D         $dir\n";
            return true;
        }
        else
        {
            echo "D  fail   $dir\n";
            return false;
        }
    }

    private function create_dir($dir)
    {
        if (is_dir($dir))return true;

        $temp = explode('/', str_replace('\\', '/', $dir) );
        $cur_dir = '';

        for($i = 0; $i < count($temp); $i++)
        {
            $cur_dir .= $temp[$i] . '/';
            if (!@is_dir($cur_dir))
            {
                if (@mkdir($cur_dir, 0755))
                {
                }
                else
                {
                    return false;
                }
            }
        }

        return true;
    }
}