Select Git revision
check_docker_stats
-
Hahn Axel (hahn) authoredHahn Axel (hahn) authored
vcs.git.class.php 25.09 KiB
<?php
require_once("vcs.interface.php");
require_once __DIR__ . '/../../vendor/axelhahn/ahcache/cache.class.php';
/**
* version control system :: GIT
* implements vcs interface
*
*
* @author hahn
*
* Axel: <axel.hahn@unibe.ch>
* (...)
* 2024-08-28 Axel php8 only; added variable types; short array syntax
*/
class vcs implements iVcs
{
// class vcs {
/**
* configuration
* @var array
*/
private array $_aCfg = [];
/**
* temp dir to fetch repo version and ommit message; its value will be
* generated in set_config()
* @var string
*/
private string $_sTempDir = ''; //
/**
* filename of ssh key file with complete path
* @var string
*/
private string $_sKeyfile = '';
/**
* filename of ssh wrapper script with complete path
* @var string
*/
private string $_sWrapper = '';
/**
* flat array with remote branch names
* @var array
*/
private array $_aRemoteBranches = [];
/**
* name of the default remote branch to access
* @var string
*/
private string $_sCurrentBranch = '';
/**
* Constructor
* @param array $aRepoConfig
*/
public function __construct(array $aRepoConfig = [])
{
$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(string $sMessage, string $sLevel = "info"): bool
{
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(array $aRepoConfig = []): bool
{
// checks
// foreach (["type", "url"] as $key) {
foreach (["type"] as $key) {
if (!isset($aRepoConfig[$key])) {
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();
$this->_aCfg = $aRepoConfig;
return true;
}
/**
* Get directory für current branch of a project below tempdir
* If it does not exist yet it will be created and the repository will be initialized.
*
* @return string
*/
private function _setTempdir(): string
{
$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 ? $this->_sCurrentBranch : '__no-branch__')) . '/';
if (!is_dir($this->_sTempDir . ".git")) {
$this->log(__FUNCTION__ . " does not exist yet: " . $this->_sTempDir . ".git");
$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);
$this->log(__FUNCTION__ . " start command <code>$sGitCmd</code>");
exec($sGitCmd, $aOutput, $iRc);
$this->log(__FUNCTION__ . " command ended with rc=$iRc " . '<pre>' . implode("\n", $aOutput) . '</pre>', ($iRc == 0 ? 'info' : 'error'));
}
return $this->_sTempDir;
}
/**
* Set the current branch
*
* @param string $sBranchname name of the branch
* @return void
*/
public function setCurrentBranch($sBranchname): void
{
$this->_sCurrentBranch = $sBranchname;
$this->_setTempdir();
}
/**
* helper: dump values
* @return boolean
*/
public function dump(): bool
{
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.
* This method works on linux only
*
* @param string $sWorkDir path of the build directory to cleanup the git meta data from
* @return bool
*/
public function cleanupWorkdir(string $sWorkDir): bool
{
if (!is_dir($sWorkDir)) {
return false;
}
shell_exec('rm -rf "' . $sWorkDir . '/.git"');
@unlink($sWorkDir . "/.gitignore");
return true;
}
/**
* Get the current branch
*
* @param bool $bReturnMasterIfEmpty flag: if there is no current branch then detect a master branch
* @return string
*/
public function getCurrentBranch(bool $bReturnMasterIfEmpty = false): string
{
if (!$this->_sCurrentBranch) {
if ($bReturnMasterIfEmpty) {
$this->_sCurrentBranch = $this->_getMasterbranchname();
}
}
return $this->_sCurrentBranch;
}
/**
* Detect an existing master branch ... and return one of 'origin/main' | 'origin/master'
*
* @return string
*/
protected function _getMasterbranchname(): string
{
$sMasterBranch = '';
$aMasternames = ['origin/main', 'origin/master'];
$aAllBranches = $this->getRemoteBranches();
if (count($aAllBranches)) {
foreach ($aMasternames as $sBranchToTest) {
if (isset($aAllBranches[$sBranchToTest])) {
$sMasterBranch = $sBranchToTest;
break;
}
}
}
return $sMasterBranch;
}
/**
* Get the build type, i.e. git|svn|cvs|
* @return string
*/
public function getBuildType(): string
{
return $this->_aCfg["type"] ?? '';
}
/**
* Get a nice name for a cache module based on repo url
*
* @return string
*/
private function _getNameOfCacheModule(): string
{
return preg_replace('/([^0-9a-z])/i', "", $this->getUrl());
}
/**
* Cleanup cache data for this project (revisions, list of branches+tags)
*
* @param int $iAge max age in sec; older items will be deleted
* @return bool
*/
public function cleanupCache(int $iAge): string
{
$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: store hash with all branches in cache
* It saves 1.5 sec for reading 300 branches
*
* @return boolean
*/
private function _cacheRemoteBranches(): bool
{
$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;
}
/**
* Read remote repository and get an array with names and revisions of
* all branches and tags
* per branch you get an array element with the keys revision, name, type
* It returns false if there is no git url
*
* @param bool $bIgnoreCache flag to overrde cache
* @return array
*/
private function _fetchRemoteBranches($bIgnoreCache = false): bool|array
{
$this->log(__FUNCTION__ . "(bIgnoreCache = " . ($bIgnoreCache ? 'true' : 'false') . ") start");
$aReturn = [];
$sGitUrl = $this->getUrl();
if (!$sGitUrl) {
return false;
}
$oCache = new AhCache($this->_getNameOfCacheModule(), "RemoteBranches");
$aOutput = [];
$iRc = false;
// list of cached branch keys
if ($oCache->isExpired() || $bIgnoreCache) {
// workdir is on level of set project ... going 1 level up means to leave the dir with the current branch
$sWorkdir = dirname($this->_sTempDir) . '/fetchRemoteBranches/';
$this->log(__FUNCTION__ . " - sWorkdir = $sWorkdir");
$sGitCmd = 'export GIT_SSH="' . $this->_sWrapper . '" ; export PKEY="' . $this->_sKeyfile . '" ; ';
if (is_dir($sWorkdir . ".git")) {
// if a subdir .git exists:
// Verify if git remote -v contains the current git url
// If not, we delete it
$sPreCmd = 'cd "' . $sWorkdir . '" 2>&1 && git remote -v 2>&1 | grep -F "' . $sGitUrl . '" >/dev/null || ( echo "DELETING .git dir..."; rm -rf .git && rc=$?; echo "rc=$rc"; sleep 1; exit $rc) ';
$this->log(__FUNCTION__ . " - start PRE command <code>$sPreCmd</code>");
exec($sPreCmd, $aPreLines, $iRc);
if (!$iRc == 0) {
$this->log(__FUNCTION__ . " <code>" . print_r($aPreLines, 1) . "</code> rc=$iRc");
}
}
if (!is_dir($sWorkdir . ".git")) {
$sGitCmd .= 'mkdir -p "' . $sWorkdir . '" && cd "' . $sWorkdir . '" && ';
$sGitCmd .= 'git init >/dev/null && ';
$sGitCmd .= 'git remote add origin "' . $sGitUrl . '" 2>&1 && ';
} else {
$sGitCmd .= 'cd "' . $sWorkdir . '" 2>&1 && ';
}
$sGitCmd .= 'git ls-remote --heads --tags origin 2>&1 ;';
$this->log(__FUNCTION__ . " - start command <code>$sGitCmd</code>");
exec($sGitCmd, $aOutput, $iRc);
$this->log(__FUNCTION__ . " - command ended with rc=$iRc " . '<pre>' . implode("\n", $aOutput) . '</pre>', ($iRc == 0 ? 'info' : 'error'));
if ($iRc == 0) {
$this->log(__FUNCTION__ . ' start reading all branches');
// $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);
$sBranchPath = preg_replace('#^refs/#', '', $aTmp[1]);
$sBranch = preg_replace('#^[^/]*/#', '', $sBranchPath);
// 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 = preg_replace('#/.*$#', '', $sBranchPath);
$sName = ($sType == "heads") ? "origin/" . $sBranch : $sBranch;
$sBranchKey = $sName;
// $this->log(__FUNCTION__ . ' $sBranchKey = '.$sBranchKey);
// $sMessage = $this->getCommitmessageByBranch($sName, $sRevision);
$aReturn[$sBranchKey] = [
// 'debug'=> $aTmp,
'revision' => $sRevision,
'name' => $sName,
'shortname' => $sBranch,
'label' => $sType . ': ' . $sBranch,
'type' => $sType,
// 'message' => $sMessage
];
}
}
$this->_aRemoteBranches = $aReturn;
$this->log(__FUNCTION__ . ' ' . count($aReturn) . ' branches: <pre>' . print_r($this->_aRemoteBranches, 1) . '</pre>');
$this->_cacheRemoteBranches();
} else {
// $this->_aRemoteBranches = $oCache->read();
$this->log(__FUNCTION__ . " - No git access? --> deleting cache of former fetched branches...");
$oCache->delete();
$this->_aRemoteBranches = [];
}
} else {
// use cache that getCommitmessageByBranch can access it
$this->_aRemoteBranches = $oCache->read();
}
return $this->_aRemoteBranches;
}
/**
* Get a flat array with names of all remote branches
* @param bool $bIgnoreCache flag: ignore caching; default: use cache
* @return array
*/
public function getRemoteBranches(bool $bIgnoreCache = false): array
{
$this->log(__FUNCTION__ . "($bIgnoreCache) start");
if (!$this->_aRemoteBranches || $bIgnoreCache) {
$this->log(__FUNCTION__ . "($bIgnoreCache) --> fetching fresh data");
$this->_fetchRemoteBranches($bIgnoreCache);
} else {
$this->log(__FUNCTION__ . "($bIgnoreCache) --> returning cached data");
}
return $this->_aRemoteBranches;
}
/**
* Get current revision and commit message from remote repository
* @see $this::getRevision
* It returns a key "error" if no branch is set
*
* @param boolean $bRefresh optional: refresh data; default: use cache
* @return array
*/
public function getRepoRevision(bool $bRefresh = false): array
{
$this->log(__FUNCTION__ . "($bRefresh) start");
if (!$this->_sCurrentBranch) {
return [
"error" => "Error: Current git branch was not detected.",
];
// return false;
}
$sMessage = $this->getCommitmessageByBranch(false, $bRefresh ? 'dummy_to_force_refresh' : false);
if ($sMessage) {
$aReturn = [
'branch' => $this->_sCurrentBranch,
'shortname' => $this->_aRemoteBranches[$this->_sCurrentBranch]['shortname'],
'revision' => $this->_aRemoteBranches[$this->_sCurrentBranch]['revision'],
'type' => $this->_aRemoteBranches[$this->_sCurrentBranch]['type'],
'message' => $sMessage,
'_data' => $this->_aRemoteBranches[$this->_sCurrentBranch],
];
} else {
$aReturn = $this->getRevision(false);
}
return $aReturn;
}
/**
* Get a commit message of a given branch.
* It reurns if no branch was found in meta infos
*
* @param string $sBranch name of a branch
* @param string $sVerifyRevision optional: revision to verify if it is the newsest
* @return string
*/
public function getCommitmessageByBranch(string $sBranch = '', string $sVerifyRevision = ''): bool|string
{
$this->log(__FUNCTION__ . "($sBranch, $sVerifyRevision) start");
if (!$sBranch) {
$sBranch = $this->_sCurrentBranch;
}
// try to get infos from the cache
if (
is_array($this->_aRemoteBranches)
&& (
isset($this->_aRemoteBranches[$sBranch]) && $sVerifyRevision && $this->_aRemoteBranches[$sBranch]['revision'] == $sVerifyRevision
||
isset($this->_aRemoteBranches[$sBranch]) && !$sVerifyRevision
)
&& isset($this->_aRemoteBranches[$sBranch]['message'])
) {
// 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);
}
if (!isset($a['branch'])) {
return 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:
* [
* "branch" => $sRevision,
* "revision" => $sRevision,
* "message" => $sCommitmessage
* ];
*
* on error:
* [
* "error" => $sErrormessage,
* ];
*
* @param string $sWorkDir optional: local directory with initialized git repo
* @return bool|array
*/
public function getRevision(string $sWorkDir = ''): bool|array
{
$this->log(__FUNCTION__ . " start");
$aReturn = [];
$aOutput = [];
$iRc = false;
if (!$this->getUrl()) {
return false;
}
$sGitCmd = 'export GIT_SSH="' . $this->_sWrapper . '" ; export PKEY="' . $this->_sKeyfile . '" ; ';
// Luki:
// git clone -b <branch_or_tag> --single-branch <repo_url> --depth 1 --bare <dir>
/*
$sWorkDir=$sWorkDir ? $sWorkDir : $this->_sTempDir;
$sWorkDir='/dev/shm/abc';
$sGitCmd.='git clone -b '.$this->_sCurrentBranch.' --single-branch '.$this->getUrl().' --depth 1 --bare "' . $sWorkDir . '" 2>&1; rm -rf "' . $sWorkDir . '"';
*/
if ($sWorkDir) {
$sGitCmd .= 'cd "' . $sWorkDir . '" && ';
} else {
if (!file_exists($this->_sTempDir . ".git")) {
$this->log(__FUNCTION__ . " does not exist yet: " . $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
// #7706 add --force
$sGitCmd .= ' ( '
// . 'git fetch --update-head-ok --tags --depth 1 2>&1 ; ' // 1.5 s
. 'git fetch --update-head-ok --tags --depth 1 --force 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
// $sGitCmd.='git log -1 2>&1 ; '; // 0.0 s
$this->log(__FUNCTION__ . " start command <code>$sGitCmd</code>");
// $sLoginfo = shell_exec($sGitCmd);
exec($sGitCmd, $aOutput, $iRc);
$this->log(__FUNCTION__ . " command ended with rc=$iRc " . '<pre>' . implode("\n", $aOutput) . '</pre>', ($iRc == 0 ? 'info' : 'error'));
$sLoginfo = implode("\n", $aOutput);
/*
*
* 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 = [];
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 = [
"branch" => $this->_sCurrentBranch,
"revision" => $sRevision,
"message" => $sCommitMsg // ."\n". microtime(true),
];
} else {
if (!$sLoginfo) {
$sLoginfo = $sGitCmd;
}
// echo "DEBUG: error on reading git revision<br>";
$aReturn = [
"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
* It returns false if the workdir was not found or the url for git repo is not set
* Otherwise it retunrs the output of git clone + git checkout
*
* @param string $sWorkDir working dir where to check out source url
* @return bool|string
*/
public function getSources(string $sWorkDir): bool|string
{
$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 . '" ; ';
$aOutput = [];
$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 <code>$sGitCmd</code>");
// $sReturn = shell_exec($sGitCmd);
exec($sGitCmd, $aOutput, $iRc);
$this->log(__FUNCTION__ . " command ended with rc=$iRc " . '<pre>' . implode("\n", $aOutput) . '</pre>', ($iRc == 0 ? 'info' : 'error'));
return implode("\n", $aOutput) . "\nrc=$iRc";
}
/**
* Get url to vcs sources
*
* @return string
*/
public function getUrl(): string
{
$this->log(__FUNCTION__ . " --> " . $this->_aCfg["url"]);
return $this->_aCfg["url"] ?? '';
}
/**
* Get url to view sources in webrowser to generate an infolink
*
* @return string
*/
public function getWebGuiUrl(): string
{
$this->log(__FUNCTION__ . " start");
return $this->_aCfg["webaccess"] ?? '';
}
}