432 lines
16 KiB
PHP
432 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
/**
|
|
* The zen file of repo module of ZenTaoPMS.
|
|
*
|
|
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
|
|
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
|
* @author Zeng Gang<zenggang@easycorp.ltd>
|
|
* @package repo
|
|
* @link https://www.zentao.net
|
|
*/
|
|
class repoZen extends repo
|
|
{
|
|
/**
|
|
* 准备创建版本库的数据。
|
|
* Prepare create repo data.
|
|
*
|
|
* @param form $formData
|
|
* @param bool $isPipelineServer
|
|
* @access protected
|
|
* @return object|false
|
|
*/
|
|
protected function prepareCreate(form $formData, bool $isPipelineServer): object|false
|
|
{
|
|
if($this->config->inContainer || $this->config->inQuickon)
|
|
{
|
|
$formData->data->client = $_POST['client'] = $this->post->SCM == 'Subversion' ? 'svn' : 'git';
|
|
}
|
|
else
|
|
{
|
|
if(!$this->checkClient()) return false;
|
|
}
|
|
if(!$this->checkConnection()) return false;
|
|
|
|
$repo = $formData
|
|
->setIf($isPipelineServer, 'password', $this->post->serviceToken)
|
|
->setIf($this->post->SCM == 'Gitlab', 'path', '')
|
|
->setIf($this->post->SCM == 'Gitlab', 'client', '')
|
|
->setIf($this->post->SCM == 'Gitlab', 'extra', $this->post->serviceProject)
|
|
->setIf($isPipelineServer, 'prefix', '')
|
|
->setIf($this->post->SCM == 'Git', 'account', '')
|
|
->setIf($this->post->SCM == 'Git', 'password', '')
|
|
->setIf(in_array($this->post->SCM, array('Gitea', 'Gogs')), 'path', $_POST['path'])
|
|
->setIf($this->post->encrypt == 'base64', 'password', base64_encode($this->post->password))
|
|
->skipSpecial('path,client,account,password,desc')
|
|
->setDefault('product', '')->join('product', ',')
|
|
->setDefault('projects', '')->join('projects', ',')
|
|
->get();
|
|
|
|
$acl = $this->post->acl;
|
|
if($acl['acl'] == 'custom')
|
|
{
|
|
$aclGroups = array_filter($acl['groups']);
|
|
$aclUsers = array_filter($acl['users']);
|
|
if(empty($aclGroups) && empty($aclUsers))
|
|
{
|
|
$this->app->loadLang('product');
|
|
dao::$errors['acl'] = sprintf($this->lang->error->notempty, $this->lang->product->whitelist);
|
|
return false;
|
|
}
|
|
}
|
|
$repo->acl = json_encode($acl);
|
|
|
|
if($repo->SCM == 'Subversion')
|
|
{
|
|
$scm = $this->app->loadClass('scm');
|
|
$scm->setEngine($repo);
|
|
$info = $scm->info('');
|
|
$infoRoot = urldecode($info->root);
|
|
$repo->prefix = empty($infoRoot) ? '' : trim(str_ireplace($infoRoot, '', str_replace('\\', '/', $repo->path)), '/');
|
|
if($repo->prefix) $repo->prefix = '/' . $repo->prefix;
|
|
}
|
|
return $repo;
|
|
}
|
|
|
|
/**
|
|
* 检查svn、git客户端。
|
|
* Check svn/git client.
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
protected function checkClient(): bool
|
|
{
|
|
if($this->post->SCM == 'Gitlab') return true;
|
|
if(!$this->config->features->checkClient) return true;
|
|
|
|
if(!$this->post->client)
|
|
{
|
|
dao::$errors['client'] = sprintf($this->lang->error->notempty, $this->lang->repo->client);
|
|
return false;
|
|
}
|
|
|
|
if(strpos($this->post->client, ' '))
|
|
{
|
|
dao::$errors['client'] = $this->lang->repo->error->clientPath;
|
|
return false;
|
|
}
|
|
|
|
$clientVersionFile = $this->session->clientVersionFile;
|
|
if(empty($clientVersionFile))
|
|
{
|
|
$clientVersionFile = $this->app->getLogRoot() . uniqid('version_') . '.log';
|
|
|
|
session_start();
|
|
$this->session->set('clientVersionFile', $clientVersionFile);
|
|
session_write_close();
|
|
}
|
|
|
|
if(file_exists($clientVersionFile)) return true;
|
|
|
|
$cmd = $this->post->client . " --version > $clientVersionFile";
|
|
dao::$errors['client'] = sprintf($this->lang->repo->error->safe, $clientVersionFile, $cmd);
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* 检查连接。
|
|
* Check connection
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
protected function checkConnection(): bool
|
|
{
|
|
if(empty($_POST)) return false;
|
|
|
|
$scm = $this->post->SCM;
|
|
$client = $this->post->client;
|
|
$account = $this->post->account;
|
|
$password = $this->post->password;
|
|
$encoding = strtoupper($this->post->encoding);
|
|
$path = $this->post->path;
|
|
if($encoding != 'UTF8' and $encoding != 'UTF-8') $path = helper::convertEncoding($path, 'utf-8', $encoding);
|
|
|
|
if($scm == 'Subversion')
|
|
{
|
|
/* Get svn version. */
|
|
$versionCommand = "$client --version --quiet 2>&1";
|
|
exec($versionCommand, $versionOutput, $versionResult);
|
|
if($versionResult)
|
|
{
|
|
$message = sprintf($this->lang->repo->error->output, $versionCommand, $versionResult, implode("\n", $versionOutput));
|
|
dao::$errors['client'] = $this->lang->repo->error->cmd . "\n" . $message;
|
|
return false;
|
|
}
|
|
$svnVersion = end($versionOutput);
|
|
|
|
$path = '"' . str_replace(array('%3A', '%2F', '+'), array(':', '/', ' '), urlencode($path)) . '"';
|
|
if(stripos($path, 'https://') === 1 or stripos($path, 'svn://') === 1)
|
|
{
|
|
if(version_compare($svnVersion, '1.6', '<'))
|
|
{
|
|
dao::$errors['client'] = $this->lang->repo->error->version;
|
|
return false;
|
|
}
|
|
|
|
$command = "$client info --username $account --password $password --non-interactive --trust-server-cert-failures=cn-mismatch --trust-server-cert --no-auth-cache $path 2>&1";
|
|
if(version_compare($svnVersion, '1.9', '<')) $command = "$client info --username $account --password $password --non-interactive --trust-server-cert --no-auth-cache $path 2>&1";
|
|
}
|
|
elseif(stripos($path, 'file://') === 1)
|
|
{
|
|
$command = "$client info --non-interactive --no-auth-cache $path 2>&1";
|
|
}
|
|
else
|
|
{
|
|
$command = "$client info --username $account --password $password --non-interactive --no-auth-cache $path 2>&1";
|
|
}
|
|
|
|
exec($command, $output, $result);
|
|
if($result)
|
|
{
|
|
$message = sprintf($this->lang->repo->error->output, $command, $result, implode("\n", $output));
|
|
if(stripos($message, 'Expected FS format between') !== false and strpos($message, 'found format') !== false)
|
|
{
|
|
dao::$errors['client'] = $this->lang->repo->error->clientVersion;
|
|
return false;
|
|
}
|
|
if(preg_match('/[^\:\/A-Za-z0-9_\-\'\"\.]/', $path))
|
|
{
|
|
dao::$errors['encoding'] = $this->lang->repo->error->encoding . "\n" . $message;
|
|
return false;
|
|
}
|
|
|
|
dao::$errors['submit'] = $this->lang->repo->error->connect . "\n" . $message;
|
|
return false;
|
|
}
|
|
}
|
|
elseif(in_array($scm, array('Gitea', 'Gogs')))
|
|
{
|
|
if($this->post->name != '' and $this->post->serviceProject != '')
|
|
{
|
|
$module = strtolower($scm);
|
|
$project = $this->loadModel($module)->apiGetSingleProject($this->post->serviceHost, $this->post->serviceProject);
|
|
if(isset($project->tokenCloneUrl))
|
|
{
|
|
$path = $this->app->getAppRoot() . 'www/data/repo/' . $this->post->name . '_' . $module;
|
|
if(!realpath($path))
|
|
{
|
|
$cmd = 'git clone --progress -v "' . $project->tokenCloneUrl . '" "' . $path . '" > "' . $this->app->getTmpRoot() . "log/clone.progress.$module.{$this->post->name}.log\" 2>&1 &";
|
|
if(PHP_OS == 'WINNT') $cmd = "start /b $cmd";
|
|
exec($cmd);
|
|
}
|
|
$_POST['path'] = $path;
|
|
}
|
|
else
|
|
{
|
|
dao::$errors['serviceProject'] = $this->lang->repo->error->noCloneAddr;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
elseif($scm == 'Git')
|
|
{
|
|
if(!is_dir($path))
|
|
{
|
|
dao::$errors['path'] = sprintf($this->lang->repo->error->noFile, $path);
|
|
return false;
|
|
}
|
|
|
|
if(!chdir($path))
|
|
{
|
|
if(!is_executable($path))
|
|
{
|
|
dao::$errors['path'] = sprintf($this->lang->repo->error->noPriv, $path);
|
|
return false;
|
|
}
|
|
dao::$errors['path'] = $this->lang->repo->error->path;
|
|
return false;
|
|
}
|
|
|
|
$command = "$client tag 2>&1";
|
|
exec($command, $output, $result);
|
|
if($result)
|
|
{
|
|
dao::$errors['submit'] = $this->lang->repo->error->connect . "\n" . sprintf($this->lang->repo->error->output, $command, $result, implode("\n", $output));
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 构建创建版本库页面数据。
|
|
* Build form fields for create repo.
|
|
*
|
|
* @param int $objectID
|
|
* @access protected
|
|
* @return void
|
|
*/
|
|
protected function buildCreateForm(int $objectID): void
|
|
{
|
|
$repoID = $this->repo->saveState(0, $objectID);
|
|
$this->commonAction($repoID, $objectID);
|
|
|
|
$this->app->loadLang('action');
|
|
|
|
if($this->app->tab == 'project' or $this->app->tab == 'execution')
|
|
{
|
|
$products = $this->loadModel('product')->getProductPairsByProject($objectID);
|
|
}
|
|
else
|
|
{
|
|
$products = $this->loadModel('product')->getPairs('', 0, '', 'all');
|
|
}
|
|
|
|
$this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->create;
|
|
$this->view->groups = $this->loadModel('group')->getPairs();
|
|
$this->view->users = $this->loadModel('user')->getPairs('noletter|noempty|nodeleted|noclosed');
|
|
$this->view->products = $products;
|
|
$this->view->projects = $this->loadModel('product')->getProjectPairsByProductIDList(array_keys($products));
|
|
$this->view->relatedProjects = ($this->app->tab == 'project' or $this->app->tab == 'execution') ? array($objectID) : array();
|
|
$this->view->serviceHosts = $this->loadModel('gitlab')->getPairs();
|
|
$this->view->objectID = $objectID;
|
|
|
|
$this->display();
|
|
}
|
|
|
|
/**
|
|
* 准备批量创建版本库的数据。
|
|
* Prepare batch create repo data.
|
|
*
|
|
* @access protected
|
|
* @return array|false
|
|
*/
|
|
protected function prepareBatchCreate(): array|false
|
|
{
|
|
$this->app->loadLang('testcase');
|
|
|
|
$data = array();
|
|
foreach($_POST as $key => $vals)
|
|
{
|
|
if(strpos($key, 'serviceProject') === 0)
|
|
{
|
|
foreach($vals as $i => $project)
|
|
{
|
|
$products = array_filter($this->post->product[$i]);
|
|
if(empty($products)) dao::$errors['product'][] = sprintf($this->lang->testcase->whichLine . $this->lang->error->notempty, $i, $this->lang->repo->product);
|
|
if($this->post->name[$i] == '') dao::$errors['name'][] = sprintf($this->lang->testcase->whichLine . $this->lang->error->notempty, $i, $this->lang->repo->name);
|
|
if(dao::isError()) continue;
|
|
|
|
$data[] = array('serviceProject' => $project, 'product' => implode(',', $this->post->product[$i]), 'name' => $this->post->name[$i], 'projects' => empty($_POST['projects'][$i]) ? '' : implode(',', $this->post->projects[$i]));
|
|
}
|
|
}
|
|
}
|
|
if(dao::isError()) return false;
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* 获取gitlab还没存在禅道的项目列表。
|
|
* Get gitlab not exist repos.
|
|
*
|
|
* @param object $gitlab
|
|
* @access protected
|
|
* @return array
|
|
*/
|
|
protected function getGitlabNotExistRepos(object $gitlab): array
|
|
{
|
|
$repoList = array();
|
|
if(!empty($gitlab))
|
|
{
|
|
$repoList = $this->loadModel('gitlab')->apiGetProjects($gitlab->id);
|
|
$existRepoList = $this->dao->select('serviceProject,name')->from(TABLE_REPO)
|
|
->where('SCM')->eq(ucfirst($gitlab->type))
|
|
->andWhere('serviceHost')->eq($gitlab->id)
|
|
->fetchPairs();
|
|
foreach($repoList as $key => $repo)
|
|
{
|
|
if(isset($existRepoList[$repo->id])) unset($repoList[$key]);
|
|
}
|
|
}
|
|
return $repoList;
|
|
}
|
|
|
|
/**
|
|
* 获取版本库文件列表信息。
|
|
* Get repo files info.
|
|
*
|
|
* @param object $repo
|
|
* @param string $path
|
|
* @param string $branchID
|
|
* @param int $refresh
|
|
* @param string $revision
|
|
* @param object $lastRevision
|
|
* @access protected
|
|
* @return array
|
|
*/
|
|
protected function getFilesInfo(object $repo, string $path, string $branchID, int $refresh, string $revision, object $lastRevision): array
|
|
{
|
|
$cacheFile = $this->repo->getCacheFile($repo->id, $path, $branchID);
|
|
$cacheRefreshTime = isset($lastRevision->time) ? date('Y-m-d H:i', strtotime($lastRevision->time)) : date('Y-m-d H:i');
|
|
$this->scm->setEngine($repo);
|
|
if($refresh or !$cacheFile or !file_exists($cacheFile) or filemtime($cacheFile) < strtotime($cacheRefreshTime))
|
|
{
|
|
if($repo->SCM == 'Gitlab')
|
|
{
|
|
$infos = $this->repo->getFileList($repo, $branchID, $path);
|
|
}
|
|
else
|
|
{
|
|
$infos = $this->scm->ls($path, $revision);
|
|
$revisionList = array_column($infos, 'revision', 'revision');
|
|
$comments = $this->repo->getHistory($repo->id, $revisionList);
|
|
foreach($infos as $info)
|
|
{
|
|
if(isset($comments[$info->revision]))
|
|
{
|
|
$comment = $comments[$info->revision];
|
|
$info->comment = $comment->comment;
|
|
}
|
|
}
|
|
}
|
|
if($cacheFile) file_put_contents($cacheFile, serialize($infos), LOCK_EX);
|
|
}
|
|
else
|
|
{
|
|
$infos = unserialize(file_get_contents($cacheFile));
|
|
}
|
|
|
|
foreach($infos as $info)
|
|
{
|
|
$info->originalComment = $info->comment;
|
|
$info->comment = $this->repo->replaceCommentLink($info->comment);
|
|
}
|
|
|
|
return $infos;
|
|
}
|
|
|
|
/**
|
|
* 获取分支与tag下拉菜单组件配置。
|
|
* Get items of branch and tags menu.
|
|
*
|
|
* @param object $repo
|
|
* @param string $branchID
|
|
* @access protected
|
|
* @return array
|
|
*/
|
|
protected function getBranchAndTagItems(object $repo, string $branchID): array
|
|
{
|
|
/* Set branch or tag for git. */
|
|
$branches = $tags = array();
|
|
if(!in_array($repo->SCM, $this->config->repo->gitTypeList)) return array();
|
|
|
|
$scm = $this->app->loadClass('scm');
|
|
$scm->setEngine($repo);
|
|
$branches = $scm->branch();
|
|
$initTags = $scm->tags('');
|
|
foreach($initTags as $tag) $tags[$tag] = $tag;
|
|
|
|
$selected = '';
|
|
$branchMenus = array();
|
|
$tagMenus = array();
|
|
foreach($branches as $branchName)
|
|
{
|
|
$selected = ($branchName == $branchID) ? $branchName : $selected;
|
|
$branchMenus[] = array('text' => $branchName, 'id' => $branchName, 'keys' => zget(common::convert2Pinyin(array($branchName)), $branchName, ''), 'url' => 'javascript:;');
|
|
}
|
|
foreach($tags as $tagName)
|
|
{
|
|
$selected = ($tagName == $branchID) ? $tagName : $selected;
|
|
$tagMenus[] = array('text' => $tagName, 'id' => $tagName, 'keys' => zget(common::convert2Pinyin(array($tagName)), $tagName, ''), 'url' => 'javascript:;');
|
|
}
|
|
|
|
return array('branchMenus' => $branchMenus, 'tagMenus' => $tagMenus, 'selected' => $selected);
|
|
}
|
|
}
|
|
|