<?php

require_once("vcs.interface.php");

/**
 * version control system :: GIT
 * implements vcs interface
 * 
 *
 * @author hahn
 */
class vcs implements iVcs {
// class vcs {

    /**
     * configuration
     * @var array 
     */
    private $_aCfg = array();

    /**
     * temp dir to fetch repo version and ommit message; its value will be
     * generated in set_config()
     * @var string
     */
    private $_sTempDir = false;  // 

    /**
     * filename of ssh key file with complete path
     * @var string
     */
    private $_sKeyfile = false;

    /**
     * filename of ssh wrapper script with complete path
     * @var string
     */
    private $_sWrapper = false;

    /**
     * flat array with remote branch names
     * @var array
     */
    private $_aRemoteBranches = array();

    /**
     * name of the default remote branch to access
     * @var type 
     */
    private $_sCurrentBranch = "origin/master";

    /**
     * constructor
     * @param array $aRepoConfig
     */
    public function __construct($aRepoConfig = array()) {
        $this->setConfig($aRepoConfig);
        $this->getRemoteBranches(); // to fill the cache
    }
    
    /**
     * add a log messsage
     * @global object $oLog
     * @param  string $sMessage  messeage text
     * @param  string $sLevel    warnlevel of the given message
     * @return bool
     */
    private function log($sMessage,$sLevel="info"){
        global $oCLog;
        return $oCLog->add(basename(__FILE__)." class ".__CLASS__." - ".$sMessage,$sLevel);
    }

    /**
     * set a config and update internal (private) variables
     * @param array $aRepoConfig
     * @return boolean
     */
    public function setConfig($aRepoConfig = array()) {
        // checks
        // foreach (array("type", "url") as $key) {
        foreach (array("type") as $key) {
            if (!array_key_exists($key, $aRepoConfig)) {
                die("ERROR: key $key does not exist in config <pre>" . print_r($aRepoConfig, true) . "</pre>");
            }
            if (!$aRepoConfig[$key]) {
                die("ERROR: key $key in config exists but is empty<pre>" . print_r($aRepoConfig, true) . "</pre>");
            }
        }
        if ($aRepoConfig["type"] !== "git") {
            die("ERROR: type is not git<pre>" . print_r($aRepoConfig, true) . "</pre>");
        }

        // set config array
        $this->_aCfg = $aRepoConfig;

        // define temp dir
        $this->_sKeyfile = $this->_aCfg["dataDir"] . "/sshkeys/" . $this->_aCfg["auth"];
        $this->_sWrapper = $this->_aCfg["appRootDir"] . "/shellscripts/gitsshwrapper.sh";
        $this->_setTempdir();

        return $this->_aCfg = $aRepoConfig;
    }

    private function _setTempdir() {
        $this->_sTempDir = $this->_aCfg["url"];
        $this->_sTempDir = preg_replace('/[\@\.\:\/]/', '_', $this->_sTempDir);
        $this->_sTempDir = $this->_aCfg["tmpDir"] . '/checkout_vcsgit_' . $this->_sTempDir . '/';
        $this->_sTempDir .= preg_replace('/[\@\.\:\/]/', '_', $this->_sCurrentBranch) . '/';

        if (!file_exists($this->_sTempDir . ".git") || true) {
            $sGitCmd = 'export GIT_SSH="' . $this->_sWrapper . '" ; export PKEY="' . $this->_sKeyfile . '" ; ';
            $sGitCmd.='mkdir -p "' . $this->_sTempDir . '" && cd "' . $this->_sTempDir . '" && ';
            $sGitCmd.='git init >/dev/null && ';
            $sGitCmd.='git remote add origin "' . $this->getUrl() . '" 2>&1 ';
            // $sGitCmd='time ('.$sGitCmd.')';
            exec($sGitCmd, $aOutput, $iRc);
        }
        return $this->_sTempDir;
    }

    /**
     * set the current branch
     * @param string $sBranchname  name of the branch
     */
    public function setCurrentBranch($sBranchname) {
        $this->_sCurrentBranch = $sBranchname;
        $this->_setTempdir();
        return $this->_sCurrentBranch;
    }

    /**
     * helper: dump values
     * @return boolean
     */
    public function dump() {
        echo "<h3>Dump class " . __CLASS__ . "</h3>";
        echo "config array: <pre>" . print_r($this->_aCfg, true) . "</pre>";
        echo "temp dir to read revision: " . $this->_sTempDir . "<br>";
        return true;
    }

    /**
     * cleanup unneeded files and directories in a checked out directory
     * and remove all vcs specific files and directories
     * @return bool
     */
    public function cleanupWorkdir($sWorkDir) {
        if (!is_dir($sWorkDir)) {
            return false;
        }
        shell_exec('rm -rf "' . $sWorkDir . '/.git"');
        @unlink($sWorkDir . "/.gitignore");
        return true;
    }

    /**
     * get the current branch
     * @param string $sBranchname  name of the branch
     */
    public function getCurrentBranch() {
        return $this->_sCurrentBranch;
    }

    /**
     * return the build type, i.e. git|svn|cvs|
     * @return string
     */
    public function getBuildType() {
        return $this->_aCfg["type"];
    }

    /**
     * get a nice name for a cache module based on repo url
     * @return type
     */
    private function _getNameOfCacheModule() {
        return preg_replace('/([^0-9a-z])/i', "", $this->getUrl());
    }

    /**
     * cleanup cache data for this project (revisions, list of branches+tags)
     * @return bool
     */
    public function cleanupCache($iAge) {
        $this->log(__FUNCTION__." start");
        require_once 'cache.class.php';
        $oCache = new AhCache($this->_getNameOfCacheModule());
        ob_start();
        $oCache->cleanup((int)$iAge);
        $sOut = ob_get_contents();
        ob_end_clean();
        return $sOut;
    }

    /**
     * read remote repository and get an array with names and revisions of 
     * all branches and tags
     * pre branch you get an array element with the keys revision, name, type
     * @param bool $bForceNoCache  flag to overrde cache
     * @return array
     */
    private function _fetchRemoteBranches($bForceNoCache = false) {
        $this->log(__FUNCTION__." start");
        $aReturn = array();

        if (!$this->getUrl()) {
            return false;
        }

        $iTtl = 300; // cache for 5 min
        require_once 'cache.class.php';
        $oCache = new AhCache($this->_getNameOfCacheModule(), "RemoteBranches");

        // list of cached branch keys
        if ($oCache->isExpired() || $bForceNoCache) {
            $sWorkdir = dirname($this->_sTempDir) . '/fetchRemoteBranches/';
            $sGitCmd = 'export GIT_SSH="' . $this->_sWrapper . '" ; export PKEY="' . $this->_sKeyfile . '" ; ';
            if (!file_exists($sWorkdir . ".git")) {
                $sGitCmd.='mkdir -p "' . $sWorkdir . '" && cd "' . $sWorkdir . '" && ';
                $sGitCmd.='git init >/dev/null && ';
                $sGitCmd.='git remote add origin "' . $this->getUrl() . '" 2>&1 && ';
            } else {
                $sGitCmd.='cd "' . $sWorkdir . '" 2>&1 && ';
            }
            $sGitCmd.='git ls-remote --heads --tags origin 2>&1 ;';
            $this->log(__FUNCTION__." start command $sGitCmd");
            exec($sGitCmd, $aOutput, $iRc);
            $this->log(__FUNCTION__." end command $sGitCmd");
            if ($iRc == 0) {

                // use cache that getCommitmessageByBranch can access it
                $this->_aRemoteBranches = $oCache->read();

                foreach ($aOutput as $sBranchLine) {
                    $aTmp = explode("\t", $sBranchLine);

                    $aBranch = explode("/", $aTmp[1]);
                    $sBranch = array_pop($aBranch);
                    $sRevision = $aTmp[0];

                    // skip dereferences
                    // http://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name
                    if (!preg_match('/\^\{\}$/', $sBranch)) {
                        $sType = array_pop($aBranch);
                        $sName = ($sType == "heads") ? "origin/" . $sBranch : $sBranch;
                        $sBranchKey = $sName;

                        $sMessage = $this->getCommitmessageByBranch($sName, $sRevision);
                        $aReturn[$sBranchKey] = array(
                            // 'debug'=> $aTmp,
                            'revision' => $sRevision,
                            'name' => $sName,
                            'label' => $sType . ': ' . $sBranch,
                            'type' => $sType,
                            'message' => $sMessage
                        );
                    }
                }
                $this->_aRemoteBranches = $aReturn;
                $oCache->write($aReturn, $iTtl);
            }
        } else {
            $this->_aRemoteBranches = $oCache->read();
        }
        return $this->_aRemoteBranches;
    }

    /**
     * get a flat array with names of all remote branches
     * @return array
     */
    public function getRemoteBranches() {
        $this->log(__FUNCTION__." start");
        if (!$this->_aRemoteBranches) {
            $this->_fetchRemoteBranches();
        }
        return $this->_aRemoteBranches;
    }

    /**
     * get current revision and commit message from remote repository
     * @see $this::getRevision
     * @return array
     */
    public function getRepoRevision() {
        $this->log(__FUNCTION__." start");
        $sMessage = $this->getCommitmessageByBranch();
        if ($sMessage) {
            $aReturn = array(
                'branch' => $this->_sCurrentBranch,
                'revision' => $this->_aRemoteBranches[$this->_sCurrentBranch]['revision'],
                'message' => $sMessage,
            );
        } else {
            $aReturn = $this->getRevision(false);
        }
        return $aReturn;
    }

    /**
     * get a commit message of a given branch
     * @param string  $sBranch          name of a branch
     * @param string  $sVerifyRevision  optional: verify if this revision is the newsest
     * @return string
     */
    public function getCommitmessageByBranch($sBranch = false, $sVerifyRevision = false) {
        $this->log(__FUNCTION__." start");
        if (!$sBranch) {
            $sBranch = $this->_sCurrentBranch;
        }
        // try to get infos from the cache
        if (
                is_array($this->_aRemoteBranches)
                && (
                   array_key_exists($sBranch, $this->_aRemoteBranches) && $sVerifyRevision && $this->_aRemoteBranches[$sBranch]['revision'] == $sVerifyRevision
                   ||
                   array_key_exists($sBranch, $this->_aRemoteBranches) && !$sVerifyRevision
                )
        ) {
            // it is up to date - doing nothing
            return $this->_aRemoteBranches[$sBranch]['message'];
        }
        // ok, then I need to read it
        if ($this->_sCurrentBranch != $sBranch) {
            $sSaveBranch = $this->_sCurrentBranch;
            $this->setCurrentBranch($sBranch);
            $a = $this->getRevision(false);
            $this->setCurrentBranch($sSaveBranch);
        } else {
            $a = $this->getRevision(false);
        }
        return $a['message'];
    }

    /**
     * get current revision and commit message from an existing directory or a
     * remote repository
     * the return will fill $this->_aData["phases"]["source"] in project class
     * (array keys are revision, message or error)
     *    if ok:
     *       array(
     *          "branch" => $sRevision,
     *          "revision" => $sRevision,
     *          "message" => $sCommitmessage
     *      );
     *   
     *  on error:
     *      array(
     *          "error" => $sErrormessage,
     *      );
     * @param string  $sWorkDir  optional: local directory with initialized git repo
     * @return array
     */
    public function getRevision($sWorkDir = false) {
        $this->log(__FUNCTION__." start");
        $aReturn = array();
        if (!$this->getUrl()) {
            return false;
        }

        $sGitCmd = 'export GIT_SSH="' . $this->_sWrapper . '" ; export PKEY="' . $this->_sKeyfile . '" ; ';
        if ($sWorkDir) {
            $sGitCmd.='cd "' . $sWorkDir . '" && ';
        } else {
            if (!file_exists($this->_sTempDir . ".git")) {
                $sGitCmd.='mkdir -p "' . $this->_sTempDir . '" && cd "' . $this->_sTempDir . '" && ';
                $sGitCmd.='git init >/dev/null 2>&1 && ';
                $sGitCmd.='git remote add origin "' . $this->getUrl() . '" 2>&1 && ';
            } else {
                $sGitCmd.='cd "' . $this->_sTempDir . '" && ';
            }

            // TODO: git 1.9 does needs only the line with --tags
            $sGitCmd.=' ( '
                    . 'git fetch --update-head-ok --tags --depth 1 2>&1 ; ' // 1.5 s
                    . 'git fetch --update-head-ok --depth 1 2>&1 '          // 1.5 s
                    . ') && ';
        }

        $sGitCmd.='git log -1 "' . $this->_sCurrentBranch . '" 2>&1 ; '; // 0.0 s
        $this->log(__FUNCTION__." start command $sGitCmd");
        $sLoginfo = shell_exec($sGitCmd);
        $this->log(__FUNCTION__." end command $sGitCmd");
        /*
         * 
         * example output:
          From gitlab.iml.unibe.ch:admins/imldeployment
           * [new branch]      master     -> origin/master
          commit 0b0dbe0dee80ca71ff43a54641d616c131e6fd8a
          Author: Axel Hahn
          Date:   Fri Dec 12 16:35:38 2014 +0100

             - added: skip dereferenced tags in git (tags ending ^{} )
             - added: modal infobox in build page if you switch the branch
             - added: git uses a cache for taglist and revision infos (ttl is 5 min)
         * 
         */

        // parse revision
        $sRevision = false;
        if (preg_match('#commit\ (.*)#', $sLoginfo, $aRev)) {
            $sRevision = $aRev[1];
        }


        if ($sRevision) {
            $sCommitMsg = $sLoginfo;
            $sCommitMsg = preg_replace('/From\ .*\n/', '', $sCommitMsg);
            $sCommitMsg = preg_replace('/\ \*.*\n/', '', $sCommitMsg);
            $sCommitMsg = preg_replace('/commit.*\n/', '', $sCommitMsg);
            // keep these to see them in the output:
            // $sCommitMsg=preg_replace('/Author:\ .*\n/', '', $sCommitMsg);
            // $sCommitMsg=preg_replace('/Date:\ .*\n/', '', $sCommitMsg);

            $aReturn = array(
                "branch" => $this->_sCurrentBranch,
                "revision" => $sRevision,
                "message" => $sCommitMsg
            );
        } else {
            if (!$sLoginfo) {
                $sLoginfo = $sGitCmd;
            }
            // echo "DEBUG: error on reading git revision<br>";
            $aReturn = array(
                "error" => '<pre>' . $sLoginfo . '<hr>' . $sGitCmd . '</pre>'
            );
        }
        return $aReturn;
    }

    /**
     * get sources from vsc and check them out in given directory
     * @return bool
     */
    public function getSources($sWorkDir) {
        $this->log(__FUNCTION__." start");
        if (!$sWorkDir || !is_dir($sWorkDir)) {
            return false;
        }
        if (!$this->getUrl()) {
            return false;
        }
        $sBranchname = str_replace("origin/", "", $this->_sCurrentBranch);

        $sGitCmd = 'export GIT_SSH="' . $this->_sWrapper . '" ; export PKEY="' . $this->_sKeyfile . '" ; ';
        
        // this does not checkout tags in git v1.7 - only branches:
        // $sGitCmd .= 'echo git clone --depth 1 --recursive --branch "' . $sBranchname . '" "' . $this->getUrl() . '" "' . $sWorkDir . '" ; ';
        // $sGitCmd .= '     git clone --depth 1 --recursive --branch "' . $sBranchname . '" "' . $this->getUrl() . '" "' . $sWorkDir . '" 2>&1; ';
        // 
        $sGitCmd .= 'echo git clone "' . $this->getUrl() . '" "'.$sWorkDir.'" 2>&1 \&\& cd  "'.$sWorkDir.'" \&\& git checkout "' . $sBranchname . '" ; ';
        $sGitCmd .= '     git clone "' . $this->getUrl() . '" "'.$sWorkDir.'" 2>&1 &&   cd  "'.$sWorkDir.'" &&   git checkout "' . $sBranchname . '" 2>&1 ';
        $this->log(__FUNCTION__." start command $sGitCmd");
        // $sReturn = shell_exec($sGitCmd);
        exec($sGitCmd, $sReturn, $iRc);
        $this->log(__FUNCTION__." end command $sGitCmd");
        return implode("\n", $sReturn). "\nrc=$iRc";
    }

    /**
     * return url to vcs sources
     */
    public function getUrl() {
        $this->log(__FUNCTION__." start");
        return $this->_aCfg["url"];
    }

    /**
     * return url to view sources in webrowser to generate an infolink
     */
    public function getWebGuiUrl() {
        $this->log(__FUNCTION__." start");
        return $this->_aCfg["webaccess"];
    }

}
