<?php

/*
 * Git.php
 *
 * A PHP git library
 *
 * @package    Git.php
 * @version    0.1.4
 * @author     James Brumond
 * @copyright  Copyright 2013 James Brumond
 * @repo       http://github.com/kbjr/Git.php
 * @fork       ELMS:LN team
 */

if (__FILE__ == $_SERVER['SCRIPT_FILENAME']) {
    die('Bad load order');
}

// ------------------------------------------------------------------------

/**
 * Git Interface Class
 *
 * This class enables the creating, reading, and manipulation
 * of git repositories.
 *
 * @class  Git
 */
class Git
{
    /**
     * Git executable location
     *
     * @var string
     */
    protected static $bin = '/usr/bin/git';
    /**
     * Auto detect environment for mac
     */
    protected static function get_env_bin()
    {
        $uname = php_uname();
        // auto detect mac
        if (strpos($uname, 'Darwin') === 0) {
            return '/usr/local/bin/git';
        } else {
            return '/usr/bin/git';
        }
    }
    /**
     * Sets git executable path
     *
     * @param string $path executable location
     */
    public static function set_bin($path)
    {
        self::$bin = $path;
    }

    /**
     * Gets git executable path
     */
    public static function get_bin()
    {
        return self::$bin;
    }

    /**
     * Sets up library for use in a default Windows environment
     */
    public static function windows_mode()
    {
        self::set_bin('git');
    }

    /**
     * Create a new git repository
     *
     * Accepts a creation path, and, optionally, a source path
     *
     * @access  public
     * @param   string  repository path
     * @param   string  directory to source
     * @return  GitRepo
     */
    public static function &create($repo_path, $source = null, $remote = false, $reference = null)
    {
        return GitRepo::create_new($repo_path, $source, $remote, $reference);
    }

    /**
     * Open an existing git repository
     *
     * Accepts a repository path
     *
     * @access  public
     * @param   string  repository path
     * @return  GitRepo
     */
    public static function open($repo_path, $create_new = false)
    {
        return new GitRepo($repo_path, $create_new);
    }

    /**
     * Clones a remote repo into a directory and then returns a GitRepo object
     * for the newly created local repo
     *
     * Accepts a creation path and a remote to clone from
     *
     * @access  public
     * @param   string  repository path
     * @param   string  remote source
     * @param   string  reference path
     * @return  GitRepo
     **/
    public static function &clone_remote($repo_path, $remote, $reference = null)
    {
        return GitRepo::create_new($repo_path, $remote, true, $reference);
    }

    /**
     * Checks if a variable is an instance of GitRepo
     *
     * Accepts a variable
     *
     * @access  public
     * @param   mixed   variable
     * @return  bool
     */
    public static function is_repo($var)
    {
        return get_class($var) == 'GitRepo';
    }
    /**
     * Constructor
     *
     * Accepts a repository path
     *
     * @access  public
     * @param   string  repository path
     * @param   bool    create if not exists?
     * @return  void
     */
    public function __construct(
        $repo_path = null,
        $create_new = false,
        $_init = true
    ) {
        $this->set_bin($this->get_env_bin());
    }
}

// ------------------------------------------------------------------------

/**
 * Git Repository Interface Class
 *
 * This class enables the creating, reading, and manipulation
 * of a git repository
 *
 * @class  GitRepo
 */
class GitRepo
{
    protected $repo_path = null;
    protected $bare = false;
    protected $envopts = array();

    /**
     * Create a new git repository
     *
     * Accepts a creation path, and, optionally, a source path
     *
     * @access  public
     * @param   string  repository path
     * @param   string  directory to source
     * @param   string  reference path
     * @return  GitRepo
     */
    public static function &create_new(
        $repo_path,
        $source = null,
        $remote_source = false,
        $reference = null
    ) {
        if (
            is_dir($repo_path) &&
            file_exists($repo_path . "/.git") &&
            is_dir($repo_path . "/.git")
        ) {
            throw new Exception(
                '"' . $repo_path . '" is already a git repository'
            );
        } else {
            $repo = new self($repo_path, true, false);
            if (is_string($source)) {
                if ($remote_source) {
                    if (!is_dir($reference) || !is_dir($reference . '/.git')) {
                        throw new Exception(
                            '"' .
                                $reference .
                                '" is not a git repository. Cannot use as reference.'
                        );
                    } elseif (strlen($reference)) {
                        $reference = realpath($reference);
                        $reference = "--reference $reference";
                    }
                    $repo->clone_remote($source, $reference);
                } else {
                    $repo->clone_from($source);
                }
            } else {
                $repo->run('init');
            }
            return $repo;
        }
    }

    /**
     * Constructor
     *
     * Accepts a repository path
     *
     * @access  public
     * @param   string  repository path
     * @param   bool    create if not exists?
     * @return  void
     */
    public function __construct(
        $repo_path = null,
        $create_new = false,
        $_init = true
    ) {
        if (is_string($repo_path)) {
            $this->set_repo_path($repo_path, $create_new, $_init);
        }
    }

    /**
     * Set the repository's path
     *
     * Accepts the repository path
     *
     * @access  public
     * @param   string  repository path
     * @param   bool    create if not exists?
     * @param   bool    initialize new Git repo if not exists?
     * @return  void
     */
    public function set_repo_path(
        $repo_path,
        $create_new = false,
        $_init = true
    ) {
        if (is_string($repo_path)) {
            if ($new_path = realpath($repo_path)) {
                $repo_path = $new_path;
                if (is_dir($repo_path)) {
                    // Is this a work tree?
                    if (
                        file_exists($repo_path . "/.git") &&
                        is_dir($repo_path . "/.git")
                    ) {
                        $this->repo_path = $repo_path;
                        $this->bare = false;
                        // Is this a bare repo?
                    } elseif (is_file($repo_path . "/config")) {
                        $parse_ini = parse_ini_file($repo_path . "/config");
                        if ($parse_ini['bare']) {
                            $this->repo_path = $repo_path;
                            $this->bare = true;
                        }
                    } else {
                        if ($create_new) {
                            $this->repo_path = $repo_path;
                            if ($_init) {
                                $this->run('init');
                            }
                        } else {
                            throw new Exception(
                                '"' . $repo_path . '" is not a git repository'
                            );
                        }
                    }
                } else {
                    throw new Exception(
                        '"' . $repo_path . '" is not a directory'
                    );
                }
            } else {
                if ($create_new) {
                    if ($parent = realpath(dirname($repo_path))) {
                        mkdir($repo_path);
                        $this->repo_path = $repo_path;
                        if ($_init) {
                            $this->run('init');
                        }
                    } else {
                        throw new Exception(
                            'cannot create repository in non-existent directory'
                        );
                    }
                } else {
                    throw new Exception('"' . $repo_path . '" does not exist');
                }
            }
        }
    }

    /**
     * Get the path to the git repo directory (eg. the ".git" directory)
     *
     * @access public
     * @return string
     */
    public function git_directory_path()
    {
        return $this->bare ? $this->repo_path : $this->repo_path . "/.git";
    }

    /**
     * Tests if git is installed
     *
     * @access  public
     * @return  bool
     */
    public function test_git()
    {
        $descriptorspec = array(
            1 => array('pipe', 'w'),
            2 => array('pipe', 'w')
        );
        $pipes = array();
        $resource = proc_open(Git::get_bin(), $descriptorspec, $pipes);

        $stdout = stream_get_contents($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        foreach ($pipes as $pipe) {
            fclose($pipe);
        }

        $status = trim(proc_close($resource));
        return $status != 127;
    }

    /**
     * Run a command in the git repository
     *
     * Accepts a shell command to run
     *
     * @access  protected
     * @param   string  command to run
     * @return  string
     */
    protected function run_command($command, $skip_fail = true)
    {
        $descriptorspec = array(
            1 => array('pipe', 'w'),
            2 => array('pipe', 'w')
        );
        $pipes = array();
        /* Depending on the value of variables_order, $_ENV may be empty.
         * In that case, we have to explicitly set the new variables with
         * putenv, and call proc_open with env=null to inherit the reset
         * of the system.
         *
         * This is kind of crappy because we cannot easily restore just those
         * variables afterwards.
         *
         * If $_ENV is not empty, then we can just copy it and be done with it.
         */
        if (count($_ENV) === 0) {
            $env = null;
            foreach ($this->envopts as $k => $v) {
                putenv(sprintf("%s=%s", $k, $v));
            }
        } else {
            $env = array_merge($_ENV, $this->envopts);
        }
        $cwd = $this->repo_path;
        $resource = proc_open($command, $descriptorspec, $pipes, $cwd, $env);

        $stdout = stream_get_contents($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        foreach ($pipes as $pipe) {
            fclose($pipe);
        }

        $status = trim(proc_close($resource));
        if ($status && !$skip_fail) {
            throw new Exception($stderr);
        }

        return $stdout;
    }

    /**
     * Run a git command in the git repository
     *
     * Accepts a git command to run
     *
     * @access  public
     * @param   string  command to run
     * @return  string
     */
    public function run($command)
    {
        return $this->run_command(Git::get_bin() . " " . $command);
    }
    /**
     * return the current sha as a string
     */
    public function currentSHA() {
        return str_replace("\n","",$this->run("rev-parse HEAD"));
    }

    /**
     * Runs a 'git status' call
     *
     * Accept a convert to HTML bool
     *
     * @access public
     * @param bool  return string with <br />
     * @return string
     */
    public function status($html = false)
    {
        $msg = $this->run("status");
        if ($html == true) {
            $msg = str_replace("\n", "<br />", $msg);
        }
        return $msg;
    }

    /**
     * Runs a `git add` call
     *
     * Accepts a list of files to add
     *
     * @access  public
     * @param   mixed   files to add
     * @return  string
     */
    public function add($files = "*")
    {
        if (is_array($files)) {
            $files = '"' . implode('" "', $files) . '"';
        }
        return $this->run("add $files -v");
    }

    /**
     * runs a `git remote add` call
     *
     * Accepts a destination name and url of the remote repo
     */
    public function set_remote($destination, $url)
    {
        return $this->run("remote add $destination $url");
    }

    /**
     * Runs a `git rm` call
     *
     * Accepts a list of files to remove
     *
     * @access  public
     * @param   mixed    files to remove
     * @param   Boolean  use the --cached flag?
     * @return  string
     */
    public function rm($files = "*", $cached = false)
    {
        if (is_array($files)) {
            $files = '"' . implode('" "', $files) . '"';
        }
        return $this->run("rm " . ($cached ? '--cached ' : '') . $files);
    }

    /**
     * Runs a `git commit` call
     *
     * Accepts a commit message string
     *
     * @access  public
     * @param   string  commit message
     * @param   boolean  should all files be committed automatically (-a flag)
     * @return  string
     */
    public function commit($message = "", $commit_all = true)
    {
        $flags = $commit_all ? '-av' : '-v';
        return $this->run(
            "commit " . $flags . " -m " . escapeshellarg($message)
        );
    }

    /**
     * Runs a `git clone` call to clone the current repository
     * into a different directory
     *
     * Accepts a target directory
     *
     * @access  public
     * @param   string  target directory
     * @return  string
     */
    public function clone_to($target)
    {
        return $this->run("clone --local " . $this->repo_path . " $target");
    }

    /**
     * Runs a `git clone` call to clone a different repository
     * into the current repository
     *
     * Accepts a source directory
     *
     * @access  public
     * @param   string  source directory
     * @return  string
     */
    public function clone_from($source)
    {
        return $this->run("clone --local $source " . $this->repo_path);
    }

    /**
     * Runs a `git clone` call to clone a remote repository
     * into the current repository
     *
     * Accepts a source url
     *
     * @access  public
     * @param   string  source url
     * @param   string  reference path
     * @return  string
     */
    public function clone_remote($source, $reference)
    {
        return $this->run("clone $reference $source " . $this->repo_path);
    }

    /**
     * Runs a `git clean` call
     *
     * Accepts a remove directories flag
     *
     * @access  public
     * @param   bool    delete directories?
     * @param   bool    force clean?
     * @return  string
     */
    public function clean($dirs = false, $force = false)
    {
        return $this->run(
            "clean" . ($force ? " -f" : "") . ($dirs ? " -d" : "")
        );
    }

    /**
     * Runs a `git branch` call
     *
     * Accepts a name for the branch
     *
     * @access  public
     * @param   string  branch name
     * @return  string
     */
    public function create_branch($branch)
    {
        return $this->run("branch $branch");
    }

    /**
     * Runs a `git branch -[d|D]` call
     *
     * Accepts a name for the branch
     *
     * @access  public
     * @param   string  branch name
     * @return  string
     */
    public function delete_branch($branch, $force = false)
    {
        return $this->run("branch " . ($force ? '-D' : '-d') . " $branch");
    }

    /**
     * Runs a `git branch` call
     *
     * @access  public
     * @param   bool    keep asterisk mark on active branch
     * @return  array
     */
    public function list_branches($keep_asterisk = false)
    {
        $branchArray = explode("\n", $this->run("branch"));
        foreach ($branchArray as $i => &$branch) {
            $branch = trim($branch);
            if (!$keep_asterisk) {
                $branch = str_replace("* ", "", $branch);
            }
            if ($branch == "") {
                unset($branchArray[$i]);
            }
        }
        return $branchArray;
    }

    /**
     * Lists remote branches (using `git branch -r`).
     *
     * Also strips out the HEAD reference (e.g. "origin/HEAD -> origin/master").
     *
     * @access  public
     * @return  array
     */
    public function list_remote_branches()
    {
        $branchArray = explode("\n", $this->run("branch -r"));
        foreach ($branchArray as $i => &$branch) {
            $branch = trim($branch);
            if ($branch == "" || strpos($branch, 'HEAD -> ') !== false) {
                unset($branchArray[$i]);
            }
        }
        return $branchArray;
    }

    /**
     * Returns name of active branch
     *
     * @access  public
     * @param   bool    keep asterisk mark on branch name
     * @return  string
     */
    public function active_branch($keep_asterisk = false)
    {
        $branchArray = $this->list_branches(true);
        $active_branch = preg_grep("/^\*/", $branchArray);
        reset($active_branch);
        if ($keep_asterisk) {
            return current($active_branch);
        } else {
            return str_replace("* ", "", current($active_branch));
        }
    }

    /**
     * Runs a `git checkout` call
     *
     * Accepts a name for the branch
     *
     * @access  public
     * @param   string  branch name
     * @return  string
     */
    public function checkout($branch)
    {
        return $this->run("checkout $branch");
    }

    /**
     * Runs a `git merge` call
     *
     * Accepts a name for the branch to be merged
     *
     * @access  public
     * @param   string $branch
     * @return  string
     */
    public function merge($branch)
    {
        return $this->run("merge $branch --no-ff");
    }

    /**
     * Runs a `git reset` call
     *
     * Accepts a name and remote for the branch to be reset to.
     *
     * @access  public
     * @param   string $branch
     * @param   string $remote
     * @param   boolean $hard
     * @return  string
     */
    public function reset($branch = 'master', $remote = 'origin', $hard = true)
    {
        $flag = '';
        if ($hard) {
            $flag = '--hard';
        }
        return $this->run("reset $flag $remote/$branch");
    }

    /**
     * Runs a `git reset --soft HEAD~1` call
     *
     * Revert the last commit for each commit we were told to revert
     * @todo expand to support hash values for specific ones
     *
     * @access  public
     * @param   number $count
     * @return  string
     */
    public function revert($count = 1)
    {
        $counter = 0;
        // sanity check
        if ($count < 1) {
            $count = 1;
        }
        while ($counter != $count) {
            $this->run("reset --hard HEAD~1");
            $counter++;
        }
        return TRUE;
    }

    /**
     * Runs a git fetch on the current branch
     *
     * @access  public
     * @return  string
     */
    public function fetch()
    {
        return $this->run("fetch");
    }

    /**
     * Add a new tag on the current position
     *
     * Accepts the name for the tag and the message
     *
     * @param string $tag
     * @param string $message
     * @return string
     */
    public function add_tag($tag, $message = null)
    {
        if ($message === null) {
            $message = $tag;
        }
        return $this->run("tag -a $tag -m " . escapeshellarg($message));
    }

    /**
     * List all the available repository tags.
     *
     * Optionally, accept a shell wildcard pattern and return only tags matching it.
     *
     * @access	public
     * @param	string	$pattern	Shell wildcard pattern to match tags against.
     * @return	array				Available repository tags.
     */
    public function list_tags($pattern = null)
    {
        $tagArray = explode("\n", $this->run("tag -l $pattern"));
        foreach ($tagArray as $i => &$tag) {
            $tag = trim($tag);
            if ($tag == '') {
                unset($tagArray[$i]);
            }
        }

        return $tagArray;
    }

    /**
     * Push specific branch to a remote
     *
     * Accepts the name of the remote and local branch
     *
     * @param string $remote
     * @param string $branch
     * @return string
     */
    public function push($remote, $branch, $flags = '--tags')
    {
        return $this->run("push $remote $branch $flags");
    }

    /**
     * Pull specific branch from remote
     *
     * Accepts the name of the remote and local branch
     *
     * @param string $remote
     * @param string $branch
     * @return string
     */
    public function pull($remote, $branch)
    {
        return $this->run("pull $remote $branch");
    }

    /**
     * List log entries.
     *
     * @param string $format
     * @return string
     */
    public function log($format = null)
    {
        if ($format === null) {
            return $this->run('log');
        } else {
            return $this->run('log --pretty=format:"' . $format . '"');
        }
    }

    /**
     * Show commit info.
     *
     * @param string $commit
     * @param string $format
     * @return string
     */
    public function show($commit, $format = null)
    {
        if ($format === null) {
            return $this->run('show');
        } else {
            return $this->run(
                'show --pretty=format:"' . $format . '" ' . $commit
            );
        }
    }

    /**
     * Sets the project description.
     *
     * @param string $new
     */
    public function set_description($new)
    {
        $path = $this->git_directory_path();
        file_put_contents($path . "/description", $new);
    }

    /**
     * Gets the project description.
     *
     * @return string
     */
    public function get_description()
    {
        $path = $this->git_directory_path();
        return file_get_contents($path . "/description");
    }

    /**
     * Sets custom environment options for calling Git
     *
     * @param string key
     * @param string value
     */
    public function setenv($key, $value)
    {
        $this->envopts[$key] = $value;
    }
}

/* End of file */
