Skip to content
Snippets Groups Projects
Select Git revision
  • 8d9a2c9b96bb5fc92a5722d5c28548a2eddb3fd3
  • master default protected
2 results

rest-api-client.sh.md

Blame
  • vcs.git.class.php 19.08 KiB
    <?php
    
    require_once("vcs.interface.php");
    require_once 'cache.class.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);
                exec($sGitCmd);
            }
            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");
            $oCache = new AhCache($this->_getNameOfCacheModule());
            ob_start();
            $oCache->cleanup((int)$iAge);
            $sOut = ob_get_contents();
            ob_end_clean();
            return $sOut;
        }
    
        /**
         * helper: cache hash with all branches
         * @return boolean
         */
        private function _cacheRemoteBranches() {
            $iTtl=300;
            $oCache = new AhCache($this->_getNameOfCacheModule(), "RemoteBranches");
            // $this->log(__FUNCTION__." <pre>".print_r($this->_aRemoteBranches, 1)."</pre>");
            $oCache->write($this->_aRemoteBranches, $iTtl);
            return true;
        }
        
        private function _extend_brancharray($a){
            
        }
        
        /**
         * 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;
            }
    
            $oCache = new AhCache($this->_getNameOfCacheModule(), "RemoteBranches");
            $aOutput=false; 
            $iRc=false;
    
            // 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 with rc=$iRc ", ($iRc==0 ? 'info':'error'));
                if ($iRc == 0) {
    
                    // use cache that getCommitmessageByBranch can access it
                    $this->_aRemoteBranches = $oCache->read();
                    $this->log(__FUNCTION__ . ' data from cache: <pre>'.print_r($this->_aRemoteBranches, 1).'</pre>');
                    /**
                     * $aOutput = Array
                     * (
                     *     [0] => cd7b238a75fe3df3f53ca3c258078d6736cc5bec	refs/heads/foreman-integration
                     *     [1] => 68cef2c74b58db8e13413c1c54104e060d8ffbb3	refs/heads/master
                     *     [2] => cae3a1eaee180b05fff883d1cfb36d09778dec2c	refs/heads/task-1726-gitssh-extensions
                     * )
                     */
    
                    foreach ($aOutput as $sBranchLine) {
                        $this->log(__FUNCTION__ . ' loop over output of git ls-remote <pre>'.print_r($sBranchLine, 1).'</pre>');
                        $aTmp = explode("\t", $sBranchLine);
    
                        $aBranch = explode("/", $aTmp[1]);
                        $sBranch = array_pop($aBranch); // remove "refs"
    
                        // skip dereferences
                        // http://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name
                        if (!preg_match('/\^\{\}$/', $sBranch)) {
                            $sRevision = $aTmp[0];
                            $sType = array_pop($aBranch);
                            $sName = ($sType == "heads") ? "origin/" . $sBranch : $sBranch;
                            
                            $sBranchKey = $sName;
                            $this->log(__FUNCTION__ . ' $sBranchKey = '.$sBranchKey);
    
                            $sMessage = $this->getCommitmessageByBranch($sName, $sRevision);
                            $aReturn[$sBranchKey] = array(
                                'debug'=> $aTmp,
                                'revision' => $sRevision,
                                'name' => $sName,
                                'label' => $sType . ': ' . $sBranch,
                                'type' => $sType,
                                'message' => $sMessage
                            );
                        }
                    }
                    $this->_aRemoteBranches = $aReturn;
                    $this->_cacheRemoteBranches();
                }
            } 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
         * @param boolean  $bRefresh  optional: refresh data; default: use cache
         * @return array
         */
        public function getRepoRevision($bRefresh=false) {
            $this->log(__FUNCTION__."($bRefresh) start");
            $sMessage = $this->getCommitmessageByBranch(false, $bRefresh ? 'dummy_to_force_refresh' : false);
            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: revision to verify if it is the newsest
         * @return string
         */
        public function getCommitmessageByBranch($sBranch = false, $sVerifyRevision = false) {
            $this->log(__FUNCTION__."($sBranch, $sVerifyRevision) 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
                $this->log(__FUNCTION__." return cached data");
                return $this->_aRemoteBranches[$sBranch]['message'];
            }
            // ok, then I need to read it
            $this->log(__FUNCTION__." return fresh data");
            if ($this->_sCurrentBranch != $sBranch) {
                $sSaveBranch = $this->_sCurrentBranch;
                $this->setCurrentBranch($sBranch);
                $a = $this->getRevision(false);
                $this->setCurrentBranch($sSaveBranch);
            } else {
                $a = $this->getRevision(false);
            }
            // merge with cached info ... to add type and label
            if(isset($this->_aRemoteBranches[$a['branch']])){
                $this->_aRemoteBranches[$a['branch']]=array_merge($this->_aRemoteBranches[$a['branch']], $a);
            } else {
                $this->_aRemoteBranches[$a['branch']]=$a;
            }
            // store in cache
            $this->_cacheRemoteBranches();
            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();
            $aOutput=array();
            $iRc=false;
            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);
            exec($sGitCmd, $aOutput, $iRc);
            $sLoginfo= implode("\n", $aOutput);
            $this->log(__FUNCTION__." end with rc=$iRc <pre>$sLoginfo</pre>", ($iRc==0 ? 'info':'error'));
            
            /*
             * 
             * 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;
            $aRev=array();
            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 // ."\n". microtime(true),
                );
            } else {
                if (!$sLoginfo) {
                    $sLoginfo = $sGitCmd;
                }
                // echo "DEBUG: error on reading git revision<br>";
                $aReturn = array(
                    "error" => '<pre>' . $sLoginfo . '<hr>' . $sGitCmd . '</pre>'
                );
            }
            // $this->log(__FUNCTION__ . ' return is <pre>'.print_r($aReturn, 1).'</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 . '" ; ';
            $sReturn=false;
            $iRc=false;
            
            // 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"];
        }
    
    }