<?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); 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"); 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"); $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 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 * @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); } 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; $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 ); } 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 . '" ; '; $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"]; } }