<?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 false if no branch is set
     * 
     * @param boolean  $bRefresh  optional: refresh data; default: use cache
     * @return bool|array
     */
    public function getRepoRevision(bool $bRefresh = false): bool|array
    {
        $this->log(__FUNCTION__ . "($bRefresh) start");
        if (!$this->_sCurrentBranch) {
            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"] ?? '';
    }

}