3167 lines
116 KiB
PHP
3167 lines
116 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
/**
|
||
* The model file of repo module of ZenTaoPMS.
|
||
*
|
||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
|
||
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||
* @author Yanyi Cao <caoyanyi@cnezsoft.com>
|
||
* @package repo
|
||
* @property repoTao $repoTao
|
||
* @link https://www.zentao.net
|
||
*/
|
||
class repoModel extends model
|
||
{
|
||
/**
|
||
* 检查代码库的权限。
|
||
* Check repo priv.
|
||
*
|
||
* @param object $repo
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function checkPriv(object $repo): bool
|
||
{
|
||
$account = $this->app->user->account;
|
||
$acl = !empty($repo->acl->acl) ? $repo->acl->acl : 'custom';
|
||
if(empty($repo->acl)) $repo->acl = new stdclass();
|
||
if(empty($repo->acl->users)) $repo->acl->users = array();
|
||
if(empty($repo->acl->groups)) $repo->acl->groups = array();
|
||
|
||
if(strpos(",{$this->app->company->admins},", ",$account,") !== false || $acl == 'open') return true;
|
||
if($acl == 'custom' && empty(array_filter($repo->acl->groups)) && empty(array_filter($repo->acl->users))) return true;
|
||
|
||
if($acl == 'private')
|
||
{
|
||
$userProducts = explode(',', $this->app->user->view->products);
|
||
$repoProducts = explode(',', $repo->product);
|
||
if(array_intersect($userProducts, $repoProducts)) return true;
|
||
}
|
||
|
||
if(!empty($repo->acl->groups))
|
||
{
|
||
foreach($this->app->user->groups as $group)
|
||
{
|
||
if(in_array($group, $repo->acl->groups)) return true;
|
||
}
|
||
}
|
||
if(!empty($repo->acl->users) and in_array($account, $repo->acl->users)) return true;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 设置菜单链接信息。
|
||
* Set menu.
|
||
*
|
||
* @param array $repos
|
||
* @param int $repoID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function setMenu(array $repos, int $repoID = 0)
|
||
{
|
||
if(empty($repoID)) $repoID = $this->session->repoID ? $this->session->repoID : key($repos);
|
||
if(!isset($repos[$repoID])) $repoID = key($repos);
|
||
|
||
$repoID = (int)$repoID;
|
||
|
||
/* Check the privilege. */
|
||
if($repoID)
|
||
{
|
||
$repo = $this->getByID($repoID);
|
||
if(!$repo || !$this->checkPriv($repo)) $repoID = 0;
|
||
if(!$repo || !in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) unset($this->lang->devops->menu->mr);
|
||
if(!$repo || !in_array($repo->SCM, $this->config->repo->notSyncSCM))
|
||
{
|
||
unset($this->lang->devops->menu->tag);
|
||
unset($this->lang->devops->menu->branch);
|
||
}
|
||
}
|
||
|
||
if(!in_array($this->app->methodName, $this->config->repo->notSetMenuVars)) common::setMenuVars('devops', $repoID);
|
||
if(!session_id()) session_start();
|
||
$this->session->set('repoID', $repoID);
|
||
session_write_close();
|
||
}
|
||
|
||
/**
|
||
* 获取代码库列表。
|
||
* Get repo list.
|
||
*
|
||
* @param int $projectID
|
||
* @param string $SCM Subversion|Git|Gitlab
|
||
* @param string $orderBy
|
||
* @param object $pager
|
||
* @param bool $getCodePath
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getList(int $projectID = 0, string $SCM = '', string $orderBy = 'id_desc', ?object $pager = null, bool $getCodePath = false, bool $lastSubmitTime = false, string $type = '', int $param = 0): array
|
||
{
|
||
$repoQuery = $type == 'bySearch' ? $this->repoTao->processSearchQuery($param) : '';
|
||
$repos = $this->getListByCondition($repoQuery, $SCM, $orderBy, $pager);
|
||
|
||
/* Get products. */
|
||
$productIdList = $this->loadModel('product')->getProductIDByProject($projectID, false);
|
||
foreach($repos as $i => $repo)
|
||
{
|
||
$repo->acl = json_decode($repo->acl);
|
||
$repo->codePath = $repo->path;
|
||
if(!$this->checkPriv($repo))
|
||
{
|
||
unset($repos[$i]);
|
||
}
|
||
elseif($projectID)
|
||
{
|
||
$hasPriv = false;
|
||
foreach(explode(',', $repo->product) as $productID)
|
||
{
|
||
if(isset($productIdList[$productID])) $hasPriv = true;
|
||
}
|
||
|
||
if(!$hasPriv) unset($repos[$i]);
|
||
}
|
||
|
||
if($lastSubmitTime) $repo->lastSubmitTime = $repo->lastCommit ? $repo->lastCommit : $this->repoTao->getLastRevision($repo->id);
|
||
|
||
if(in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) $repo = $this->processGitService($repo, $getCodePath);
|
||
}
|
||
|
||
return $repos;
|
||
}
|
||
|
||
/**
|
||
* 根据SCM和权限获取代码库列表。
|
||
* Get list by SCM.
|
||
*
|
||
* @param string $scm
|
||
* @param string $type all|haspriv
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getListBySCM(string $scm, string $type = 'all')
|
||
{
|
||
$repos = $this->dao->select('*,acl')->from(TABLE_REPO)->where('deleted')->eq('0')
|
||
->andWhere('SCM')->in($scm)
|
||
->andWhere('synced')->eq(1)
|
||
->fetchAll('id', false);
|
||
|
||
foreach($repos as $i => $repo)
|
||
{
|
||
if($repo->encrypt == 'base64') $repo->password = base64_decode($repo->password);
|
||
$repo->acl = json_decode($repo->acl);
|
||
$repo->codePath = $repo->path;
|
||
if($type == 'haspriv' and !$this->checkPriv($repo)) unset($repos[$i]);
|
||
if(in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) $repo = $this->processGitService($repo);
|
||
}
|
||
|
||
return $repos;
|
||
}
|
||
|
||
/**
|
||
* 创建版本库。
|
||
* Create a repo.
|
||
*
|
||
* @param object $repo
|
||
* @param bool $isPipelineServer
|
||
* @access public
|
||
* @return int|false
|
||
*/
|
||
public function create(object $repo, bool $isPipelineServer): int|false
|
||
{
|
||
$this->dao->insert(TABLE_REPO)->data($repo, 'serviceToken')
|
||
->batchCheck($this->config->repo->create->requiredFields, 'notempty')
|
||
->batchCheckIF(!in_array($repo->SCM, $this->config->repo->notSyncSCM), 'path,client', 'notempty')
|
||
->batchCheckIF($isPipelineServer, 'serviceHost,serviceProject', 'notempty')
|
||
->batchCheckIF($repo->SCM == 'Subversion', $this->config->repo->svn->requiredFields, 'notempty')
|
||
->check('name', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($repo->SCM))
|
||
->checkIF(!$isPipelineServer, 'path', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($repo->SCM) . " and `serviceHost` = " . $this->dao->sqlobj->quote($repo->serviceHost))
|
||
->autoCheck()
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
$repoID = $this->dao->lastInsertID();
|
||
|
||
$repo = $this->getByID($repoID);
|
||
if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
|
||
{
|
||
$token = uniqid();
|
||
$res = $this->loadModel($repo->SCM)->addPushWebhook($repo, $token);
|
||
if($res !== true)
|
||
{
|
||
$this->dao->delete()->from(TABLE_REPO)->where('id')->eq($repoID)->exec();
|
||
dao::$errors['webhook'][] = isset($res['message']) ? $res['message'] : $this->lang->gitlab->failCreateWebhook;
|
||
return false;
|
||
}
|
||
else
|
||
{
|
||
$this->dao->update(TABLE_REPO)->set('password')->eq($token)->where('id')->eq($repoID)->exec();
|
||
}
|
||
}
|
||
$this->rmClientVersionFile();
|
||
|
||
return $repoID;
|
||
}
|
||
|
||
/**
|
||
* 创建远程版本库。
|
||
* Create a repo.
|
||
*
|
||
* @param object $repo
|
||
* @access public
|
||
* @return int|false
|
||
*/
|
||
public function createRepo(object $repo): int|false
|
||
{
|
||
$check = $this->checkName($repo->name);
|
||
if(!$check)
|
||
{
|
||
dao::$errors['name'] = $this->lang->repo->error->repoNameInvalid;
|
||
return false;
|
||
}
|
||
|
||
$response = $this->createGitlabRepo($repo, $repo->namespace);
|
||
|
||
$this->loadModel('gitlab');
|
||
if(!empty($response->id))
|
||
{
|
||
$repo->path = $response->path;
|
||
$repo->serviceProject = $response->serviceProject;
|
||
$repo->extra = $response->extra;
|
||
$repo->SCM = 'Gitlab';
|
||
|
||
unset($repo->namespace);
|
||
$repoID = $this->create($repo, false);
|
||
if(dao::isError())
|
||
{
|
||
$this->gitlab->apiDeleteProject($repo->serviceHost, $response->id);
|
||
return false;
|
||
}
|
||
return $repoID;
|
||
}
|
||
|
||
return $this->gitlab->apiErrorHandling($response);
|
||
}
|
||
|
||
/**
|
||
* 创建gitlab远程版本库。
|
||
* Create gitlab repo.
|
||
*
|
||
* @param object $repo
|
||
* @param int $namespace
|
||
* @access public
|
||
* @return object|false
|
||
*/
|
||
public function createGitlabRepo(object $repo, string $namespace): object|false
|
||
{
|
||
$project = new stdclass();
|
||
$project->name = $repo->name;
|
||
$project->path = $repo->name;
|
||
$project->description = $repo->desc;
|
||
$project->namespace_id = (int)$namespace;
|
||
$project->initialize_with_readme = true;
|
||
|
||
$response = $this->loadModel('gitlab')->apiCreateProject($repo->serviceHost, $project);
|
||
|
||
if(empty($response->id)) return $response;
|
||
|
||
$result = new stdclass();
|
||
$result->id = $response->id;
|
||
$result->path = $response->web_url;
|
||
$result->serviceProject = $response->id;
|
||
$result->extra = $response->id;
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 批量创建版本库。
|
||
* Batch create repos.
|
||
*
|
||
* @param array $repos
|
||
* @param int $serviceHost
|
||
* @param string $scm
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function batchCreate(array $repos, int $serviceHost, string $scm): bool
|
||
{
|
||
$this->loadModel('instance');
|
||
foreach($repos as $index => $repo)
|
||
{
|
||
if(empty($repo->product)) continue;
|
||
if(empty($repo->name))
|
||
{
|
||
dao::$errors["name[$index]"] = sprintf($this->lang->error->notempty, $this->lang->repo->name);
|
||
return false;
|
||
}
|
||
|
||
$repo->serviceHost = $serviceHost;
|
||
$repo->SCM = $scm;
|
||
|
||
$this->dao->insert(TABLE_REPO)->data($repo)
|
||
->batchCheck($this->config->repo->create->requiredFields, 'notempty')
|
||
->check('serviceHost,serviceProject', 'notempty')
|
||
->check('name', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($repo->SCM))
|
||
->check('serviceProject', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($repo->SCM) . " and `serviceHost` = " . $this->dao->sqlobj->quote($repo->serviceHost))
|
||
->autoCheck()
|
||
->exec();
|
||
|
||
if(dao::isError()) return false;
|
||
|
||
$repoID = $this->dao->lastInsertID();
|
||
|
||
if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
|
||
{
|
||
/* Add webhook. */
|
||
$repo = $this->getByID($repoID);
|
||
$this->loadModel($repo->SCM)->addPushWebhook($repo);
|
||
$this->{$repo->SCM}->updateCodePath($repo->serviceHost, (int)$repo->serviceProject, (int)$repo->id);
|
||
}
|
||
|
||
$this->loadModel('action')->create('repo', $repoID, 'created');
|
||
if(method_exists($this->instance, 'saveWaitSyncData')) $this->instance->saveWaitSyncData('repo', $repoID, 'add', false);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 更新版本库。
|
||
* Update a repo.
|
||
*
|
||
* @param object $data
|
||
* @param object $repo
|
||
* @param bool $isPipelineServer
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function update(object $data, object $repo, bool $isPipelineServer): bool
|
||
{
|
||
if(($repo->serviceHost != $data->serviceHost || $repo->serviceProject != $data->serviceProject) && $data->SCM == 'Gitlab')
|
||
{
|
||
$repo->gitService = $data->serviceHost;
|
||
$repo->project = $data->serviceProject;
|
||
|
||
$token = uniqid();
|
||
$res = $this->loadModel('gitlab')->addPushWebhook($repo, $token);
|
||
if($res !== true)
|
||
{
|
||
dao::$errors['webhook'][] = isset($res['message']) ? $res['message'] : $this->lang->gitlab->failCreateWebhook;
|
||
return false;
|
||
}
|
||
else
|
||
{
|
||
$data->password = $token;
|
||
}
|
||
}
|
||
|
||
if($data->SCM == 'Subversion' && $data->path != $repo->path)
|
||
{
|
||
$data->synced = 0;
|
||
$data->lastSync = null;
|
||
$data->lastCommit = null;
|
||
}
|
||
|
||
if($data->encrypt == 'base64') $data->password = base64_encode((string)$data->password);
|
||
$this->dao->update(TABLE_REPO)->data($data, 'serviceToken')
|
||
->batchCheck($this->config->repo->edit->requiredFields, 'notempty')
|
||
->batchCheckIF($data->SCM != 'Gitlab', 'path,client', 'notempty')
|
||
->batchCheckIF($isPipelineServer, 'serviceHost,serviceProject', 'notempty')
|
||
->batchCheckIF($data->SCM == 'Subversion', $this->config->repo->svn->requiredFields, 'notempty')
|
||
->check('name', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($data->SCM) . " and `id` != $repo->id")
|
||
->checkIF(!$isPipelineServer, 'path', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($data->SCM) . " and `serviceHost` = " . $this->dao->sqlobj->quote($data->serviceHost) . " and `id` != $repo->id")
|
||
->autoCheck()
|
||
->where('id')->eq($repo->id)->exec();
|
||
|
||
$this->rmClientVersionFile();
|
||
|
||
if(in_array($data->SCM, $this->config->repo->notSyncSCM))
|
||
{
|
||
$this->loadModel($data->SCM)->updateCodePath($data->serviceHost, (int)$data->serviceProject, $repo->id);
|
||
$data->path = $this->getByID($repo->id)->path;
|
||
$this->updateCommitDate($repo->id);
|
||
}
|
||
|
||
if(($repo->serviceHost != $data->serviceHost || $repo->serviceProject != $data->serviceProject || $repo->SCM == 'Subversion') && $repo->path != $data->path)
|
||
{
|
||
$this->repoTao->deleteInfoByID($repo->id);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 代码提交关联任务、需求、Bug。
|
||
* Link commit to story, bug, task.
|
||
*
|
||
* @param int $repoID
|
||
* @param string $revision
|
||
* @param string $type
|
||
* @param string $from repo|commit
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function link(int $repoID, string $revision, string $type = 'story', string $from = 'repo'): bool
|
||
{
|
||
$this->loadModel('action');
|
||
if($type == 'story') $links = $objects['stories'] = $this->post->stories;
|
||
if($type == 'bug') $links = $objects['bugs'] = $this->post->bugs;
|
||
if($type == 'task') $links = $objects['task'] = $this->post->tasks;
|
||
|
||
$revisionInfo = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($revision)->fetch();
|
||
if(empty($revisionInfo))
|
||
{
|
||
$repo = $this->getByID($repoID);
|
||
if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
|
||
{
|
||
$scm = $this->app->loadClass('scm');
|
||
$scm->setEngine($repo);
|
||
$logs = $scm->getCommits($revision, 1);
|
||
$this->saveCommit($repoID, $logs, 0);
|
||
}
|
||
else
|
||
{
|
||
$this->updateCommit($repoID);
|
||
}
|
||
}
|
||
|
||
$revisionInfo = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($revision)->fetch();
|
||
if(empty($revisionInfo))
|
||
{
|
||
dao::$errors = $this->lang->fail;
|
||
return false;
|
||
}
|
||
|
||
$revisionID = $revisionInfo->id;
|
||
$committer = $this->dao->select('account')->from(TABLE_USER)->where('commiter')->eq($revisionInfo->committer)->fetch('account');
|
||
if(empty($committer)) $committer = $revisionInfo->committer;
|
||
if($from == 'repo') $committer = $this->app->user->account;
|
||
foreach($links as $linkID)
|
||
{
|
||
$relation = new stdclass;
|
||
$relation->AType = 'revision';
|
||
$relation->AID = $revisionID;
|
||
$relation->relation = 'commit';
|
||
$relation->BType = $type;
|
||
$relation->BID = $linkID;
|
||
$relation->product = 0;
|
||
|
||
/* record module related information. */
|
||
$this->loadModel($type)->updateLinkedCommits((int)$linkID, $repoID, [$revisionID]);
|
||
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
|
||
|
||
$this->action->create($type, (int)$linkID, 'linked2revision', '', substr($revisionInfo->revision, 0, 10), $committer);
|
||
}
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 删除一个版本库。
|
||
* Delete a repo.
|
||
*
|
||
* @param int $repoID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function deleteRepo(int $repoID): bool
|
||
{
|
||
$this->repoTao->deleteInfoByID($repoID);
|
||
$this->dao->delete()->from(TABLE_REPO)->where('id')->eq($repoID)->exec();
|
||
if(dao::isError()) return false;
|
||
|
||
$this->loadModel('action')->create('repo', $repoID, 'deleted', '');
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 取消代码提交关联的任务、需求、Bug。
|
||
* Unlink object and commit revision.
|
||
*
|
||
* @param int $repoID
|
||
* @param string $revision
|
||
* @param string $objectType story|bug|task
|
||
* @param int $objectID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function unlink(int $repoID, string $revision, string $objectType, int $objectID): bool
|
||
{
|
||
$revisionID = $this->dao->select('id')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($revision)->fetch('id');
|
||
$this->dao->delete()->from(TABLE_RELATION)
|
||
->where('AID')->eq($revisionID)
|
||
->andWhere('AType')->eq('revision')
|
||
->andWhere('relation')->eq('commit')
|
||
->andWhere('BType')->eq($objectType)
|
||
->andWhere('BID')->eq($objectID)->exec();
|
||
|
||
$this->dao->delete()->from(TABLE_RELATION)
|
||
->where('AType')->eq($objectType)
|
||
->andWhere('AID')->eq($objectID)
|
||
->andWhere('BType')->eq('commit')
|
||
->andWhere('BID')->eq($revisionID)
|
||
->andWhere('relation')->eq('completedin')->exec();
|
||
|
||
$this->dao->delete()->from(TABLE_RELATION)
|
||
->where('AType')->eq('commit')
|
||
->andWhere('AID')->eq($revisionID)
|
||
->andWhere('BType')->eq('story')
|
||
->andWhere('BID')->eq($objectID)
|
||
->andWhere('relation')->eq('completedfrom')->exec();
|
||
|
||
if(!dao::isError()) $this->loadModel('action')->create($objectType, $objectID, 'unlinkedfromrevision', '', substr($revision, 0, 10));
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 设置代码库id。
|
||
* Save repo state.
|
||
*
|
||
* @param int $repoID
|
||
* @param int $objectID
|
||
* @access public
|
||
* @return int
|
||
*/
|
||
public function saveState(int $repoID = 0, int $objectID = 0): int
|
||
{
|
||
if(session_id()) session_write_close();
|
||
|
||
if(!defined('RUN_MODE') || RUN_MODE != 'test') session_start();
|
||
if($repoID > 0) $this->session->set('repoID', (int)$repoID);
|
||
|
||
$repos = $this->getRepoPairs($this->app->tab, $objectID);
|
||
if($repoID == 0 && $this->session->repoID == '') $this->session->set('repoID', key($repos));
|
||
|
||
if(!isset($repos[$this->session->repoID])) $this->session->set('repoID', key($repos));
|
||
|
||
$repoID = (int)$this->session->repoID;
|
||
session_write_close();
|
||
|
||
return $repoID;
|
||
}
|
||
|
||
/**
|
||
* 获取代码库列表键值对。
|
||
* Get repo pairs.
|
||
*
|
||
* @param string $type project|execution|repo
|
||
* @param int $projectID
|
||
* @param bool $showScm
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRepoPairs(string $type, int $projectID = 0, bool $showScm = true): array
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getRepoPairs();
|
||
|
||
$repos = $this->dao->select('*,acl')->from(TABLE_REPO)
|
||
->where('deleted')->eq(0)
|
||
->fetchAll('id', false);
|
||
|
||
/* Get products. */
|
||
$productIdList = ($type == 'project' or $type == 'execution') ? $this->loadModel('product')->getProductIDByProject($projectID, false) : array();
|
||
|
||
$repoPairs = array();
|
||
foreach($repos as $repo)
|
||
{
|
||
$repo->acl = json_decode($repo->acl);
|
||
$scm = '';
|
||
if($showScm) $scm = $repo->SCM == 'Subversion' ? '[svn] ' : '[' . strtolower($repo->SCM) . '] ';
|
||
if($this->checkPriv($repo))
|
||
{
|
||
if(($type == 'project' or $type == 'execution') and $projectID)
|
||
{
|
||
foreach($productIdList as $productID)
|
||
{
|
||
if(strpos(",$repo->product,", ",$productID,") !== false) $repoPairs[$repo->id] = $scm . $repo->name;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
$repoPairs[$repo->id] = $scm . $repo->name;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $repoPairs;
|
||
}
|
||
|
||
/**
|
||
* 根据应用获取代码库分组。
|
||
* Get repos group by repo type.
|
||
*
|
||
* @param string $type
|
||
* @param int $projectID
|
||
* @param array $scmList
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRepoGroup(string $type, int $projectID = 0, array $scmList = array()): array
|
||
{
|
||
$repos = $this->getList(0, implode(',', $scmList));
|
||
$productIds = $productItems = array();
|
||
if($projectID)
|
||
{
|
||
$productIds = $this->loadModel('product')->getProductIDByProject($projectID, false);
|
||
}
|
||
else
|
||
{
|
||
foreach($repos as $repo) $productIds = array_merge($productIds, explode(',', $repo->product));
|
||
}
|
||
|
||
$products = $this->loadModel('product')->getByIdList(array_unique($productIds));
|
||
foreach($products as $productID => $product)
|
||
{
|
||
$productItem = array();
|
||
$productItem['pid'] = $productID;
|
||
$productItem['type'] = $product->shadow ? $this->lang->project->common : 'product';
|
||
$productItem['text'] = $product->name;
|
||
$productItem['items'] = array();
|
||
|
||
$productItems[$productID] = $productItem;
|
||
}
|
||
|
||
/* Get project products. */
|
||
$projectProductIds = in_array($type, array('project', 'execution')) ? $this->loadModel('product')->getProductIDByProject($projectID, false) : array();
|
||
|
||
/* Get repo data for dropmenu. */
|
||
$repoPairs = array();
|
||
foreach($repos as $repo)
|
||
{
|
||
$repo->acl = is_string($repo->acl) ? json_decode($repo->acl) : $repo->acl;
|
||
if($this->checkPriv($repo))
|
||
{
|
||
$repoItem = array();
|
||
$repoItem['id'] = $repo->id;
|
||
$repoItem['text'] = $repo->name;
|
||
$repoItem['keys'] = zget(common::convert2Pinyin(array($repo->name)), $repo->name, '');
|
||
$repoItem['data-app'] = $this->app->tab;
|
||
|
||
$repoProducts = explode(',', $repo->product);
|
||
foreach($repoProducts as $productID)
|
||
{
|
||
if(in_array($type, array('project', 'execution')) && $projectID && !in_array($productID, $projectProductIds)) continue;
|
||
|
||
if(strpos(",$repo->product,", ",$productID,") !== false)
|
||
{
|
||
if(!isset($repoPairs[$productID])) $repoPairs[$productID] = $productItems[$productID];
|
||
$repoPairs[$productID]['items'][] = $repoItem;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return $repoPairs;
|
||
}
|
||
|
||
/**
|
||
* 根据ID获取代码库。
|
||
* Get repo by id.
|
||
*
|
||
* @param int $repoID
|
||
* @access public
|
||
* @return object|false
|
||
*/
|
||
public function getByID(int $repoID): object|false
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getRepo();
|
||
|
||
if(empty($repoID)) return false;
|
||
$repo = $this->dao->select('*')->from(TABLE_REPO)->where('id')->eq($repoID)->fetch();
|
||
if(!$repo) return false;
|
||
|
||
/* Update repo table for old version. */
|
||
if(empty($repo->serviceHost) && in_array($repo->SCM, $this->config->repo->gitServiceTypeList))
|
||
{
|
||
$repo->serviceHost = $repo->client;
|
||
$repo->serviceProject = $repo->extra;
|
||
$this->dao->update(TABLE_REPO)->data(array('serviceHost' => $repo->serviceHost, 'serviceProject' => $repo->serviceProject))->where('id')->eq($repoID)->exec();
|
||
|
||
/* Add webhook. */
|
||
if($repo->SCM == 'Gitlab') $this->loadModel('gitlab')->updateCodePath((int)$repo->serviceHost, (int)$repo->serviceProject, $repo->id);
|
||
}
|
||
|
||
if($repo->encrypt == 'base64') $repo->password = base64_decode($repo->password);
|
||
$repo->codePath = $repo->path;
|
||
if(in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) $repo = $this->processGitService($repo);
|
||
$repo->acl = json_decode($repo->acl);
|
||
if(empty($repo->acl)) $repo->acl = new stdclass();
|
||
if(empty($repo->acl->acl)) $repo->acl->acl = 'custom';
|
||
|
||
$repo->serviceHost = (int)$repo->serviceHost;
|
||
$repo->gitService = $repo->serviceHost;
|
||
$repo->serviceProject = $repo->SCM == 'Gitlab' ? (int)$repo->serviceProject : $repo->serviceProject;
|
||
return $repo;
|
||
}
|
||
|
||
/**
|
||
* 根据URL获取代码库。
|
||
* Get repo by url.
|
||
*
|
||
* @param string $url
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRepoByUrl(string $url): array
|
||
{
|
||
if(empty($url)) return array('result' => 'fail', 'message' => 'Url is empty.');
|
||
|
||
$matches = $this->repoTao->getMatchedReposByUrl($url);
|
||
if(empty($matches)) return array('result' => 'fail', 'message' => 'No matched gitlab.');
|
||
|
||
$conditions = array();
|
||
foreach($matches as $matched) $conditions[] = "(`serviceHost`='{$matched['gitlab']}' and `serviceProject`='{$matched['project']}')";
|
||
|
||
$matchedRepos = $this->getListByCondition('(' . implode(' OR ', $conditions). ')', 'Gitlab');
|
||
if(empty($matchedRepos)) return array('result' => 'fail', 'message' => 'No matched gitlab.');
|
||
|
||
$matchedRepo = new stdclass();
|
||
foreach($matchedRepos as $repo)
|
||
{
|
||
if(!empty($repo->preMerge))
|
||
{
|
||
$matchedRepo = $repo;
|
||
break;
|
||
}
|
||
}
|
||
if(empty($matchedRepo)) return array('result' => 'fail', 'message' => 'Matched gitlab is not open pre merge.');
|
||
if(empty($matchedRepo->job)) return array('result' => 'fail', 'message' => 'No linked job.');
|
||
|
||
$job = $this->dao->select('*')->from(TABLE_JOB)->where('id')->eq($matchedRepo->job)->andWhere('deleted')->eq(0)->fetch();
|
||
if(empty($job)) return array('result' => 'fail', 'message' => 'Linked job is not exists.');
|
||
|
||
$matchedRepo->job = $job;
|
||
return array('result' => 'success', 'data' => $matchedRepo);
|
||
}
|
||
|
||
/**
|
||
* 根据URL获取代码库列表。
|
||
* Get repo list by url.
|
||
*
|
||
* @param string $url
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRepoListByUrl(string $url = ''): array
|
||
{
|
||
if(empty($url)) return array('status' => 'fail', 'message' => 'Url is empty.');
|
||
|
||
$matches = $this->repoTao->getMatchedReposByUrl($url);
|
||
if(empty($matches)) return array('status' => 'fail', 'message' => 'No matched gitlab.');
|
||
|
||
$conditions = array();
|
||
foreach($matches as $matched) $conditions[] = "(`serviceHost`='{$matched['gitlab']}' and `serviceProject`='{$matched['project']}')";
|
||
|
||
$matchedRepos = $this->getListByCondition('(' . implode(' OR ', $conditions). ')', 'Gitlab');
|
||
foreach($matchedRepos as $key => $repo)
|
||
{
|
||
if(!$this->checkPriv($repo)) unset($matchedRepos[$key]);
|
||
}
|
||
if(empty($matchedRepos)) return array('status' => 'fail', 'message' => 'No matched gitlab.');
|
||
|
||
return array('status' => 'success', 'repos' => $matchedRepos);
|
||
}
|
||
|
||
/**
|
||
* 根据ID列表获取代码库列表。
|
||
* Get by id list.
|
||
*
|
||
* @param array $idList
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getByIdList(array $idList): array
|
||
{
|
||
$repos = $this->dao->select('*')->from(TABLE_REPO)->where('deleted')->eq(0)->andWhere('id')->in($idList)->fetchAll('id', false);
|
||
foreach($repos as $repo)
|
||
{
|
||
if($repo->encrypt == 'base64') $repo->password = base64_decode($repo->password);
|
||
$repo->acl = json_decode($repo->acl);
|
||
}
|
||
|
||
return $repos;
|
||
}
|
||
|
||
/**
|
||
* 获取代码库的分支列表。
|
||
* Get git branches.
|
||
*
|
||
* @param object $repo
|
||
* @param bool $printLabel
|
||
* @param string $source select current repo's branches from scm or database.
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getBranches(object $repo, bool $printLabel = false, string $source = 'scm'): array
|
||
{
|
||
if($source == 'database')
|
||
{
|
||
$branches = $this->dao->select('branch')->from(TABLE_REPOBRANCH)
|
||
->where('repo')->eq($repo->id)
|
||
->fetchPairs();
|
||
}
|
||
else
|
||
{
|
||
$this->scm = $this->app->loadClass('scm');
|
||
$this->scm->setEngine($repo);
|
||
$branches = $this->scm->branch();
|
||
}
|
||
|
||
if($printLabel)
|
||
{
|
||
foreach($branches as &$branch) $branch = 'Branch::' . $branch;
|
||
}
|
||
|
||
return $branches;
|
||
}
|
||
|
||
/**
|
||
* 根据提交ID获取提交信息。
|
||
* Get commit by id.
|
||
*
|
||
* @param array $revisions
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function getCommitsByRevisions(array $revisions): array
|
||
{
|
||
return $this->dao->select('id')->from(TABLE_REPOHISTORY)->where('revision')->in($revisions)->fetchPairs('id');
|
||
}
|
||
|
||
/**
|
||
* 获取代码库的提交列表。
|
||
* Get commits.
|
||
*
|
||
* @param object $repo
|
||
* @param string $entry
|
||
* @param string $revision
|
||
* @param string $type
|
||
* @param object $pager
|
||
* @param string $begin
|
||
* @param string $end
|
||
* @param object|string $query
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getCommits(object $repo, string $entry, string $revision = 'HEAD', string $type = 'dir', ?object $pager = null, string $begin = '', string $end = '', mixed $query = null): array
|
||
{
|
||
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getCommits();
|
||
|
||
if(!isset($repo->id)) return array();
|
||
if(in_array($repo->SCM, $this->config->repo->notSyncSCM)) return $this->loadModel('gitlab')->getCommits($repo, $entry, $pager, $begin, $end, $query);
|
||
|
||
$entry = ltrim($entry, '/');
|
||
$entry = empty($entry) ? '' : '/' . $entry;
|
||
$revisionTime = $this->repoTao->getLatestCommitTime($repo->id, $revision, $repo->SCM == 'Subversion' ? '' : (string)$this->cookie->repoBranch);
|
||
$hasBranch = $repo->SCM != 'Subversion' && $this->cookie->repoBranch;
|
||
$historyIdList = array();
|
||
if($entry != '/' && !empty($entry))
|
||
{
|
||
$historyIdList = $this->dao->select('DISTINCT t2.id,t2.`time`')->from(TABLE_REPOFILES)->alias('t1')
|
||
->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.revision=t2.id')
|
||
->beginIF($hasBranch)->leftJoin(TABLE_REPOBRANCH)->alias('t3')->on('t2.id=t3.revision')->fi()
|
||
->where('t1.repo')->eq($repo->id)
|
||
->beginIF($begin)->andWhere('t2.`time`')->ge($begin)->fi()
|
||
->beginIF($end)->andWhere('t2.`time`')->le($end)->fi()
|
||
->beginIF($revisionTime)->andWhere('t2.`time`')->le($revisionTime)->fi()
|
||
->andWhere('left(t2.`comment`, 12)')->ne('Merge branch')
|
||
->beginIF($hasBranch)->andWhere('t3.branch')->eq($this->cookie->repoBranch)->fi()
|
||
->beginIF($type == 'dir')
|
||
->andWhere('t1.parent', true)->like(rtrim($entry, '/') . "/%")
|
||
->orWhere('t1.parent')->eq(rtrim($entry, '/'))
|
||
->markRight(1)
|
||
->fi()
|
||
->beginIF($type == 'file')->andWhere('t1.path')->eq("$entry")->fi()
|
||
->orderBy('t2.`time` desc')
|
||
->page($pager, 't2.id')
|
||
->fetchPairs('id', 'id');
|
||
}
|
||
|
||
$comments = $this->dao->select('DISTINCT t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
|
||
->beginIF($hasBranch)->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')->fi()
|
||
->where('t1.repo')->eq($repo->id)
|
||
->andWhere('left(t1.`comment`, 12)')->ne('Merge branch')
|
||
->beginIF($revisionTime)->andWhere('t1.`time`')->le($revisionTime)->fi()
|
||
->beginIF($begin)->andWhere('t1.`time`')->ge($begin)->fi()
|
||
->beginIF($end)->andWhere('t1.`time`')->le($end)->fi()
|
||
->beginIF($query)->andWhere($query)->fi()
|
||
->beginIF($hasBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
|
||
->beginIF($entry != '/' && !empty($entry))->andWhere('t1.id')->in($historyIdList)->fi()
|
||
->beginIF($begin)->andWhere('t1.time')->ge($begin)->fi()
|
||
->beginIF($end)->andWhere('t1.time')->le($end)->fi()
|
||
->orderBy('time desc');
|
||
if($entry == '/' || empty($entry)) $comments->page($pager, 't1.id');
|
||
$comments = $comments->fetchAll('revision');
|
||
|
||
foreach($comments as $repoComment)
|
||
{
|
||
$repoComment->originalComment = $repoComment->comment;
|
||
$repoComment->comment = $this->replaceCommentLink($repoComment->comment);
|
||
}
|
||
|
||
return $comments;
|
||
}
|
||
|
||
/**
|
||
* 获取最后一次提交的信息。
|
||
* Get latest commit.
|
||
*
|
||
* @param int $repoID
|
||
* @param bool $checkCount
|
||
* @access public
|
||
* @return object|false
|
||
*/
|
||
public function getLatestCommit(int $repoID, bool $checkCount = true): object|false
|
||
{
|
||
$repo = $this->fetchByID($repoID);
|
||
$branchID = (string)$this->cookie->repoBranch;
|
||
$lastComment = $this->dao->select('t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
|
||
->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
|
||
->where('t1.repo')->eq($repoID)
|
||
->beginIF($repo->SCM != 'Subversion' && $branchID)->andWhere('t2.branch')->eq($branchID)->fi()
|
||
->beginIF($repo->SCM == 'Subversion')->andWhere('t1.time')->ne('1970-01-01 08:00:00')->fi()
|
||
->orderBy('t1.`time` desc')
|
||
->fetch();
|
||
if(empty($lastComment)) return false;
|
||
|
||
$lastComment->svnRevision = intval($lastComment->revision);
|
||
if(!$checkCount) return $lastComment;
|
||
|
||
$count = $this->dao->select('count(DISTINCT t1.id) as count')->from(TABLE_REPOHISTORY)->alias('t1')
|
||
->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
|
||
->where('t1.repo')->eq($repoID)
|
||
->beginIF($repo->SCM != 'Subversion' && $branchID)->andWhere('t2.branch')->eq($branchID)->fi()
|
||
->fetch('count');
|
||
|
||
if($repo->SCM == 'Git' && $lastComment->commit != $count)
|
||
{
|
||
$this->fixCommit($repo->id);
|
||
$lastComment->commit = $count;
|
||
}
|
||
|
||
return $lastComment;
|
||
}
|
||
|
||
/**
|
||
* 从数据库中获取提交记录。
|
||
* Get revisions from db.
|
||
*
|
||
* @param int $repoID
|
||
* @param int $limit
|
||
* @param string $maxRevision
|
||
* @param string $minRevision
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRevisionsFromDB(int $repoID, int $limit = 0, string $maxRevision = '', string $minRevision = ''): array
|
||
{
|
||
$revisions = $this->dao->select('DISTINCT t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
|
||
->beginIF($this->cookie->repoBranch)->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')->fi()
|
||
->where('t1.repo')->eq($repoID)
|
||
->beginIF(!empty($maxRevision))->andWhere('t1.revision')->le($maxRevision)->fi()
|
||
->beginIF(!empty($minRevision))->andWhere('t1.revision')->ge($minRevision)->fi()
|
||
->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
|
||
->orderBy('t1.revision desc')
|
||
->beginIF(!empty($limit))->limit($limit)->fi()
|
||
->fetchAll('revision');
|
||
|
||
$commiters = $this->loadModel('user')->getCommiters();
|
||
foreach($revisions as $revision)
|
||
{
|
||
$revision->comment = $this->replaceCommentLink($revision->comment);
|
||
$revision->committer = isset($commiters[$revision->committer]) ? $commiters[$revision->committer] : $revision->committer;
|
||
}
|
||
return $revisions;
|
||
}
|
||
|
||
/**
|
||
* 获取代码提交记录。
|
||
* Get history.
|
||
*
|
||
* @param int $repoID
|
||
* @param array $revisions
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getHistory(int $repoID, array $revisions): array
|
||
{
|
||
return $this->dao->select('DISTINCT t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
|
||
->beginIF($this->cookie->repoBranch)->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')->fi()
|
||
->where('t1.repo')->eq($repoID)
|
||
->andWhere('t1.revision')->in($revisions)
|
||
->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
|
||
->fetchAll('revision');
|
||
}
|
||
|
||
/**
|
||
* 查询提交记录的名称。
|
||
* Get git revisionName.
|
||
*
|
||
* @param string $revision
|
||
* @param int $commit
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function getGitRevisionName(string $revision, int $commit): string
|
||
{
|
||
if(empty($commit)) return substr($revision, 0, 10);
|
||
return substr($revision, 0, 10) . '<span title="' . sprintf($this->lang->repo->commitTitle, $commit) . '"> (' . $commit . ') </span>';
|
||
}
|
||
|
||
/**
|
||
* 获取缓存文件位置。
|
||
* Get cache file.
|
||
*
|
||
* @param int $repoID
|
||
* @param string $path
|
||
* @param int $revision
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function getCacheFile(int $repoID, string $path, string $revision): string
|
||
{
|
||
$cachePath = $this->app->getCacheRoot() . '/repo/' . $repoID;
|
||
if(!is_dir($cachePath)) mkdir($cachePath, 0777, true);
|
||
if(!is_writable($cachePath)) return false;
|
||
return $cachePath . '/' . md5("{$this->cookie->repoBranch}-$path-$revision");
|
||
}
|
||
|
||
/**
|
||
* 查询代码库关联的产品列表。
|
||
* Get products by repoID.
|
||
*
|
||
* @param int $repoID
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getProductsByRepo(int $repoID): array
|
||
{
|
||
$repo = $this->getByID($repoID);
|
||
if(empty($repo->id)) return array();
|
||
|
||
return $this->dao->select('id,name')->from(TABLE_PRODUCT)
|
||
->where('id')->in($repo->product)
|
||
->andWhere('deleted')->eq(0)
|
||
->fetchPairs();
|
||
}
|
||
|
||
/**
|
||
* 保存代码提交信息并返回保存数量。
|
||
* Save commit.
|
||
*
|
||
* @param int $repoID
|
||
* @param array $logs
|
||
* @param int $version
|
||
* @param string $branch
|
||
* @access public
|
||
* @return int
|
||
*/
|
||
public function saveCommit(int $repoID, array $logs, int $version, string $branch = ''): int
|
||
{
|
||
$count = 0;
|
||
if(empty($logs)) return $count;
|
||
|
||
foreach($logs['commits'] as $i => $commit)
|
||
{
|
||
$existsRevision = $this->dao->select('id,revision')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($commit->revision)->fetch();
|
||
if($existsRevision)
|
||
{
|
||
if($branch) $this->dao->replace(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($existsRevision->id)->set('branch')->eq($branch)->exec();
|
||
continue;
|
||
}
|
||
|
||
$commit->repo = $repoID;
|
||
$commit->commit = $version;
|
||
$commit->comment = htmlSpecialString($commit->comment);
|
||
$this->dao->insert(TABLE_REPOHISTORY)->data($commit)->exec();
|
||
if(!dao::isError())
|
||
{
|
||
$commitID = $this->dao->lastInsertID();
|
||
if($branch) $this->dao->replace(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($commitID)->set('branch')->eq($branch)->exec();
|
||
if(!empty($logs['files']))
|
||
{
|
||
foreach($logs['files'][$i] as $file)
|
||
{
|
||
$parentPath = dirname($file->path);
|
||
|
||
$copyfromPath = !empty($file->copyfromPath) ? $file->copyfromPath : '';
|
||
$copyfromRev = !empty($file->copyfromRev) ? $file->copyfromRev : '';
|
||
unset($file->copyfromPath);
|
||
unset($file->copyfromRev);
|
||
|
||
$file->parent = $parentPath == '\\' ? '/' : $parentPath;
|
||
$file->revision = $commitID;
|
||
$file->repo = $repoID;
|
||
$this->dao->insert(TABLE_REPOFILES)->data($file)->exec();
|
||
|
||
if($file->action == 'R' && !empty($file->oldPath))
|
||
{
|
||
$file->path = $file->oldPath;
|
||
$file->parent = dirname($file->path);
|
||
$file->oldPath = '';
|
||
$file->action = 'D';
|
||
$this->dao->insert(TABLE_REPOFILES)->data($file)->exec();
|
||
}
|
||
|
||
if(!empty($copyfromPath) && !empty($copyfromRev)) $this->repoTao->copySvnDir($repoID, $copyfromPath, $copyfromRev, $file->path);
|
||
}
|
||
}
|
||
$revisionPairs[$commit->revision] = $commit->revision;
|
||
$version ++;
|
||
$count ++;
|
||
}
|
||
|
||
dao::$errors = array();
|
||
}
|
||
return $count;
|
||
}
|
||
|
||
/**
|
||
* 保存单个提交信息。
|
||
* Save One Commit.
|
||
*
|
||
* @param int $repoID
|
||
* @param object $commit
|
||
* @param int $version
|
||
* @param string $branch
|
||
* @access public
|
||
* @return int
|
||
*/
|
||
public function saveOneCommit(int $repoID, object $commit, int $version, string $branch = ''): int
|
||
{
|
||
$existsRevision = $this->dao->select('id,revision')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($commit->revision)->fetch();
|
||
if($existsRevision)
|
||
{
|
||
if($branch) $this->dao->replace(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($existsRevision->id)->set('branch')->eq($branch)->exec();
|
||
return $version;
|
||
}
|
||
|
||
$history = new stdclass();
|
||
$history->repo = $repoID;
|
||
$history->commit = $version;
|
||
$history->revision = $commit->revision;
|
||
$history->comment = htmlSpecialString($commit->comment);
|
||
$history->committer = $commit->committer;
|
||
$history->time = $commit->time;
|
||
$this->dao->insert(TABLE_REPOHISTORY)->data($history)->exec();
|
||
if(!dao::isError())
|
||
{
|
||
$commitID = $this->dao->lastInsertID();
|
||
if($branch) $this->dao->replace(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($commitID)->set('branch')->eq($branch)->exec();
|
||
foreach($commit->change as $file => $info)
|
||
{
|
||
$parentPath = dirname($file);
|
||
|
||
$copyfromPath = !empty($info['copyfrom-path']) ? $info['copyfrom-path'] : '';
|
||
$copyfromRev = !empty($info['copyfrom-rev']) ? $info['copyfrom-rev']: '';
|
||
|
||
$repoFile = new stdclass();
|
||
$repoFile->repo = $repoID;
|
||
$repoFile->revision = $commitID;
|
||
$repoFile->path = $file;
|
||
$repoFile->parent = $parentPath == '\\' ? '/' : $parentPath;
|
||
$repoFile->type = $info['kind'];
|
||
$repoFile->action = $info['action'];
|
||
$repoFile->oldPath = empty($info['oldPath']) ? '' : $info['oldPath'];
|
||
$this->dao->insert(TABLE_REPOFILES)->data($repoFile)->exec();
|
||
|
||
if($repoFile->oldPath and $repoFile->action == 'R')
|
||
{
|
||
$parentPath = dirname($repoFile->oldPath);
|
||
|
||
$repoFile->path = $repoFile->oldPath;
|
||
$repoFile->parent = $parentPath == '\\' ? '/' : $parentPath;
|
||
$repoFile->type = $info['kind'];
|
||
$repoFile->action = 'D';
|
||
$repoFile->oldPath = '';
|
||
$this->dao->insert(TABLE_REPOFILES)->data($repoFile)->exec();
|
||
}
|
||
|
||
if(!empty($copyfromPath) && !empty($copyfromRev)) $this->repoTao->copySvnDir($repoID, $copyfromPath, $copyfromRev, $repoFile->path);
|
||
}
|
||
|
||
$version ++;
|
||
}
|
||
|
||
dao::$errors = array();
|
||
return $version;
|
||
}
|
||
|
||
/**
|
||
* 保存已存在的分支日志。
|
||
* Save exists log branch.
|
||
*
|
||
* @param int $repoID
|
||
* @param string $branch
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function saveExistCommits4Branch(int $repoID, string $branch): bool
|
||
{
|
||
$lastBranchLog = $this->dao->select('t1.time')->from(TABLE_REPOHISTORY)->alias('t1')
|
||
->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
|
||
->where('t1.repo')->eq($repoID)
|
||
->andWhere('t2.branch')->eq($branch)
|
||
->orderBy('time')
|
||
->limit(1)
|
||
->fetch();
|
||
if(empty($lastBranchLog)) return false;
|
||
|
||
$stmt = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('time')->lt($lastBranchLog->time)->query();
|
||
while($log = $stmt->fetch())
|
||
{
|
||
$this->dao->REPLACE(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($log->id)->set('branch')->eq($branch)->exec();
|
||
}
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 更新代码库的提交次数。
|
||
* Update commit count.
|
||
*
|
||
* @param int $repoID
|
||
* @param int $count
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function updateCommitCount(int $repoID, int $count): bool
|
||
{
|
||
$this->dao->update(TABLE_REPO)->set('commits')->eq($count)->where('id')->eq($repoID)->exec();
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 获取未同步的提交。
|
||
* Get unsync commits.
|
||
*
|
||
* @param object $repo
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getUnsyncedCommits(object $repo): array
|
||
{
|
||
$repoID = $repo->id;
|
||
$lastInDB = $this->getLatestCommit($repoID);
|
||
|
||
$scm = $this->app->loadClass('scm');
|
||
$scm->setEngine($repo);
|
||
|
||
$logs = $scm->log('', $lastInDB ? $lastInDB->revision : 0);
|
||
if(empty($logs)) return array();
|
||
|
||
/* Process logs. */
|
||
$logs = array_reverse($logs, true);
|
||
foreach($logs as $i => $log)
|
||
{
|
||
if(isset($lastInDB->revision) and $lastInDB->revision == $log->revision)
|
||
{
|
||
unset($logs[$i]);
|
||
continue;
|
||
}
|
||
|
||
$log->author = $log->committer;
|
||
$log->msg = $log->comment;
|
||
$log->date = $log->time;
|
||
|
||
/* Process files. */
|
||
$log->files = array();
|
||
foreach($log->change as $file => $info) $log->files[$info['action']][] = $file;
|
||
}
|
||
|
||
return $logs;
|
||
}
|
||
|
||
/**
|
||
* 生成链接。
|
||
* Create link for repo.
|
||
*
|
||
* @param string $method
|
||
* @param string $params
|
||
* @param string $viewType
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function createLink(string $method, string $params = '', string $viewType = '')
|
||
{
|
||
if($this->config->requestType == 'GET') return helper::createLink('repo', $method, $params, $viewType);
|
||
|
||
$parsedParams = array();
|
||
parse_str($params, $parsedParams);
|
||
|
||
$pathParams = '';
|
||
$pathKey = 'path';
|
||
if(isset($parsedParams['entry'])) $pathKey = 'entry';
|
||
if(isset($parsedParams['file'])) $pathKey = 'file';
|
||
if(isset($parsedParams['root'])) $pathKey = 'root';
|
||
if(isset($parsedParams[$pathKey]))
|
||
{
|
||
$pathParams = 'repoPath=' . $parsedParams[$pathKey];
|
||
$parsedParams[$pathKey] = '';
|
||
}
|
||
|
||
$params = http_build_query($parsedParams);
|
||
$link = helper::createLink('repo', $method, $params, $viewType);
|
||
if(empty($pathParams)) return $link;
|
||
|
||
$link .= strpos($link, '?') === false ? '?' : '&';
|
||
$link .= $pathParams;
|
||
return $link;
|
||
}
|
||
|
||
/**
|
||
* 更新代码库的同步状态。
|
||
* Mark synced status.
|
||
*
|
||
* @param int $repoID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function markSynced(int $repoID): bool
|
||
{
|
||
$this->fixCommit($repoID);
|
||
$this->dao->update(TABLE_REPO)->set('synced')->eq(1)->where('id')->eq($repoID)->exec();
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 更新提交记录的排序。
|
||
* Fix commit.
|
||
*
|
||
* @param int $repoID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function fixCommit(int $repoID): bool
|
||
{
|
||
$historyList = $this->dao->select('DISTINCT t1.id,t1.`time`')->from(TABLE_REPOHISTORY)->alias('t1')
|
||
->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
|
||
->where('t1.repo')->eq($repoID)
|
||
->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
|
||
->orderBy('time')
|
||
->query();
|
||
|
||
foreach($historyList as $i => $repoHistory)
|
||
{
|
||
$i++;
|
||
|
||
$this->dao->update(TABLE_REPOHISTORY)->set('`commit`')->eq($i)->where('id')->eq($repoHistory->id)->exec();
|
||
}
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 转义代码库文件路径。
|
||
* Encode repo path.
|
||
*
|
||
* @param string $path
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function encodePath(string $path = ''): string
|
||
{
|
||
if(empty($path)) return $path;
|
||
return helper::safe64Encode(urlencode($path));
|
||
}
|
||
|
||
/**
|
||
* 解析代码库文件路径。
|
||
* Decode repo path.
|
||
*
|
||
* @param string $path
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function decodePath(string $path = ''): string
|
||
{
|
||
if(empty($path)) return $path;
|
||
return trim(urldecode(helper::safe64Decode($path)), '/');
|
||
}
|
||
|
||
/**
|
||
* 删除客户端代码工具生成的版本文件。
|
||
* remove client version file.
|
||
*
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function rmClientVersionFile()
|
||
{
|
||
$clientVersionFile = $this->session->clientVersionFile;
|
||
if($clientVersionFile)
|
||
{
|
||
if(!session_id()) session_start();
|
||
$this->session->set('clientVersionFile', '');
|
||
session_write_close();
|
||
|
||
if(file_exists($clientVersionFile)) @unlink($clientVersionFile);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 替换提交记录中的链接。
|
||
* Replace comment link.
|
||
*
|
||
* @param string $comment
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function replaceCommentLink(string $comment): string
|
||
{
|
||
$rules = $this->processRules();
|
||
$storyReg = '/' . $rules['storyReg'] . '/i';
|
||
$taskReg = '/' . $rules['taskReg'] . '/i';
|
||
$bugReg = '/' . $rules['bugReg'] . '/i';
|
||
if(preg_match_all($taskReg, $comment, $result))
|
||
{
|
||
$taskLinks = $this->addLink($result, 'task');
|
||
foreach($taskLinks as $search => $replace) $comment = str_replace($search, $replace, $comment);
|
||
}
|
||
if(preg_match_all($bugReg, $comment, $result))
|
||
{
|
||
$bugLinks = $this->addLink($result, 'bug');
|
||
foreach($bugLinks as $search => $replace) $comment = str_replace($search, $replace, $comment);
|
||
}
|
||
if(preg_match_all($storyReg, $comment, $result))
|
||
{
|
||
$storyLinks = $this->addLink($result, 'story');
|
||
foreach($storyLinks as $search => $replace) $comment = str_replace($search, $replace, $comment);
|
||
}
|
||
return $comment;
|
||
}
|
||
|
||
/**
|
||
* 解析提交记录中的链接。
|
||
* Add link.
|
||
*
|
||
* @param array $matches
|
||
* @param string $method
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function addLink(array $matches, string $method): array
|
||
{
|
||
if(empty($matches)) return array();
|
||
|
||
$replaceLines = array();
|
||
foreach($matches[3] as $i => $idList)
|
||
{
|
||
$links = $matches[2][$i] . ' ' . $matches[4][$i];
|
||
preg_match_all('/\d+/', $idList, $idMatches);
|
||
foreach($idMatches[0] as $id)
|
||
{
|
||
$links .= html::a(helper::createLink($method, 'view', "id=$id"), $id) . $matches[6][$i];
|
||
}
|
||
|
||
$replaceLines[$matches[0][$i]] = rtrim($links, $matches[6][$i]);
|
||
}
|
||
return $replaceLines;
|
||
}
|
||
|
||
/**
|
||
* 解析git和svn的注释,从中提取对象id列表。
|
||
* Parse the comment of git and svn, extract object id list from it.
|
||
*
|
||
* @param string $comment
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function parseComment(string $comment): array
|
||
{
|
||
$rules = $this->processRules();
|
||
$stories = array();
|
||
$actions = array();
|
||
$designs = array();
|
||
|
||
$tasks = $this->repoTao->parseTaskComment($comment, $rules, $actions);
|
||
$bugs = $this->repoTao->parseBugComment($comment, $rules, $actions);
|
||
|
||
preg_match_all("/{$rules['taskReg']}/i", $comment, $matches);
|
||
if($matches[0])
|
||
{
|
||
foreach($matches[3] as $idList)
|
||
{
|
||
preg_match_all('/\d+/', $idList, $idMatches);
|
||
foreach($idMatches[0] as $id) $tasks[$id] = $id;
|
||
}
|
||
}
|
||
|
||
preg_match_all("/{$rules['bugReg']}/i", $comment, $matches);
|
||
if($matches[0])
|
||
{
|
||
foreach($matches[3] as $idList)
|
||
{
|
||
preg_match_all('/\d+/', $idList, $idMatches);
|
||
foreach($idMatches[0] as $id) $bugs[$id] = $id;
|
||
}
|
||
}
|
||
|
||
preg_match_all("/{$rules['storyReg']}/i", $comment, $matches);
|
||
if($matches[0])
|
||
{
|
||
foreach($matches[3] as $idList)
|
||
{
|
||
preg_match_all('/\d+/', $idList, $idMatches);
|
||
foreach($idMatches[0] as $id) $stories[$id] = $id;
|
||
}
|
||
}
|
||
|
||
preg_match_all("/{$rules['designReg']}/i", $comment, $matches);
|
||
if($matches[0])
|
||
{
|
||
$designs = implode(' ', $matches[1]);
|
||
if($designs) $designs = array_unique(explode(' ', str_replace(',', ' ', $designs)));
|
||
}
|
||
|
||
return array('stories' => $stories, 'tasks' => $tasks, 'bugs' => $bugs, 'actions' => $actions, 'designs' => $designs);
|
||
}
|
||
|
||
/**
|
||
* 转码提交注释信息。
|
||
* Convert encoding of comment.
|
||
*
|
||
* @param string $comment
|
||
* @param string $encodings
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function iconvComment(string $comment, string $encodings): string
|
||
{
|
||
/* Get encodings. */
|
||
if($encodings == '') return $comment;
|
||
|
||
/* Try convert. */
|
||
$encodings = explode(',', $encodings);
|
||
foreach($encodings as $encoding)
|
||
{
|
||
if($encoding == 'utf-8') continue;
|
||
|
||
$result = helper::convertEncoding($comment, $encoding);
|
||
if($result) return $result;
|
||
}
|
||
|
||
return $comment;
|
||
}
|
||
|
||
/**
|
||
* 解析提交指令规则。
|
||
* Process rules to REG.
|
||
*
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function processRules(): array
|
||
{
|
||
if(is_string($this->config->repo->rules)) $this->config->repo->rules = json_decode($this->config->repo->rules, true);
|
||
$rules = $this->config->repo->rules;
|
||
|
||
$idMarks = str_replace(';', '|', preg_replace('/([^;])/', '\\\\\1', trim($rules['id']['mark'], ';')));
|
||
$idSplits = str_replace(';', '|', preg_replace('/([^;])/', '\\\\\1', trim($rules['id']['split'], ';')));
|
||
$costs = str_replace(';', '|', trim($rules['task']['consumed'], ';'));
|
||
$costMarks = str_replace(';', '|', preg_replace('/([^;])/', '\\\\\1', trim($rules['mark']['consumed'], ';')));
|
||
$lefts = str_replace(';', '|', trim($rules['task']['left'], ';'));
|
||
$leftMarks = str_replace(';', '|', preg_replace('/([^;])/', '\\\\\1', trim($rules['mark']['left'], ';')));
|
||
$storyModule = str_replace(';', '|', trim($rules['module']['story'], ';'));
|
||
$taskModule = str_replace(';', '|', trim($rules['module']['task'], ';'));
|
||
$bugModule = str_replace(';', '|', trim($rules['module']['bug'], ';'));
|
||
$costUnit = str_replace(';', '|', trim($rules['unit']['consumed'], ';'));
|
||
$leftUnit = str_replace(';', '|', trim($rules['unit']['left'], ';'));
|
||
$startAction = str_replace(';', '|', trim($rules['task']['start'], ';'));
|
||
$finishAction = str_replace(';', '|', trim($rules['task']['finish'], ';'));
|
||
$effortAction = str_replace(';', '|', trim($rules['task']['logEfforts'], ';'));
|
||
$resolveAction = str_replace(';', '|', trim($rules['bug']['resolve'], ';'));
|
||
|
||
$storyReg = "(($storyModule) *(({$idMarks})[0-9]+(({$idSplits})[0-9]+)*))";
|
||
$taskReg = "(($taskModule) *(({$idMarks})[0-9]+(({$idSplits})[0-9]+)*))";
|
||
$bugReg = "(($bugModule) *(({$idMarks})[0-9]+(({$idSplits})[0-9]+)*))";
|
||
$costReg = "($costs) *(($costMarks)([0-9]+(\.?[0-9]+)?)($costUnit))";
|
||
$leftReg = "($lefts) *(($leftMarks)([0-9]+(\.?[0-9]+)?)($leftUnit))";
|
||
|
||
$startTaskReg = "({$startAction}) *{$taskReg}.*$costReg.*$leftReg";
|
||
$effortTaskReg = "({$effortAction}) *{$taskReg}.*$costReg.*$leftReg";
|
||
$finishTaskReg = "({$finishAction}) *{$taskReg}.*$costReg";
|
||
$resolveBugReg = "({$resolveAction}) *{$bugReg}";
|
||
|
||
$reg = array();
|
||
$reg['storyReg'] = $storyReg;
|
||
$reg['taskReg'] = $taskReg;
|
||
$reg['bugReg'] = $bugReg;
|
||
$reg['costReg'] = $costReg;
|
||
$reg['leftReg'] = $leftReg;
|
||
$reg['startTaskReg'] = $startTaskReg;
|
||
$reg['effortTaskReg'] = $effortTaskReg;
|
||
$reg['finishTaskReg'] = $finishTaskReg;
|
||
$reg['resolveBugReg'] = $resolveBugReg;
|
||
$reg['designReg'] = 'design(?:\s){0,}(?:#|:|:){0,}([0-9, ]{1,})';
|
||
return $reg;
|
||
}
|
||
|
||
/**
|
||
* 保存提交信息到系统。
|
||
* Save action to pms.
|
||
*
|
||
* @param array $objects
|
||
* @param object $log
|
||
* @param string $repoRoot
|
||
* @param string $encodings
|
||
* @param string $scm
|
||
* @param array $gitlabAccountPairs
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function saveAction2PMS(array $objects, object $log, string $repoRoot = '', string $encodings = 'utf-8', string $scm = 'svn', array $gitlabAccountPairs = array()): bool
|
||
{
|
||
$committers = $this->loadModel('user')->getCommiters('account');
|
||
$log->author = zget($gitlabAccountPairs, $log->author, zget($committers, $log->author));
|
||
|
||
if(isset($this->app->user))
|
||
{
|
||
$account = $this->app->user->account;
|
||
$this->app->user->account = $log->author;
|
||
}
|
||
|
||
$action = new stdclass();
|
||
$action->actor = $log->author;
|
||
$action->date = $log->date;
|
||
$action->extra = $scm == 'svn' ? $log->revision : substr($log->revision, 0, 10);
|
||
$action->comment = $this->lang->repo->revisionA . ': #' . $action->extra . "<br />" . htmlSpecialString($this->iconvComment($log->msg, $encodings));
|
||
|
||
$this->loadModel('action');
|
||
$actions = $objects['actions'];
|
||
$changes = $this->createActionChanges($log, $repoRoot, $scm);
|
||
if(isset($actions['task']))
|
||
{
|
||
$this->loadModel('task');
|
||
$productsAndExecutions = $this->getTaskProductsAndExecutions($objects['tasks']);
|
||
foreach($actions['task'] as $taskID => $taskActions)
|
||
{
|
||
$task = $this->task->getById($taskID);
|
||
if(empty($task)) continue;
|
||
|
||
$action->objectType = 'task';
|
||
$action->objectID = $taskID;
|
||
$action->product = $productsAndExecutions[$taskID]['product'];
|
||
$action->execution = $productsAndExecutions[$taskID]['execution'];
|
||
|
||
$this->setTaskByCommit($task, $taskActions, $action, $changes, $scm);
|
||
unset($objects['tasks'][$taskID]);
|
||
dao::$errors = array();
|
||
}
|
||
}
|
||
|
||
if(isset($actions['bug'])) $objects['bugs'] = $this->setBugStatusByCommit($objects['bugs'], $actions, $action, $changes);
|
||
|
||
$action->action = $scm == 'svn' ? 'svncommited' : 'gitcommited';
|
||
$this->saveObjectToPms($objects, $action, $changes);
|
||
|
||
if(isset($this->app->user)) $this->app->user->account = $account;
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 保存commit触发的操作日志信息。
|
||
* Save an action to pms.
|
||
*
|
||
* @param object $action
|
||
* @param array $changes
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function saveRecord(object $action, array $changes): bool
|
||
{
|
||
/* Remove sql error. */
|
||
dao::getError();
|
||
|
||
$record = $this->dao->select('*')->from(TABLE_ACTION)
|
||
->where('objectType')->eq($action->objectType)
|
||
->andWhere('objectID')->eq($action->objectID)
|
||
->andWhere('extra')->eq($action->extra)
|
||
->andWhere('action')->eq($action->action)
|
||
->beginIf(!empty($action->comment))->andWhere('comment')->eq(zget($action, 'comment', ''))->fi()
|
||
->fetch();
|
||
if($record)
|
||
{
|
||
$this->dao->update(TABLE_ACTION)->data($action)->where('id')->eq($record->id)->exec();
|
||
if($changes)
|
||
{
|
||
$historyIdList = $this->dao->findByAction($record->id)->from(TABLE_HISTORY)->fetchPairs('id', 'id');
|
||
if($historyIdList) $this->dao->delete()->from(TABLE_HISTORY)->where('id')->in($historyIdList)->exec();
|
||
$this->loadModel('action')->logHistory($record->id, $changes);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
$this->dao->insert(TABLE_ACTION)->data($action)->autoCheck()->exec();
|
||
if($changes)
|
||
{
|
||
$actionID = $this->dao->lastInsertID();
|
||
$this->loadModel('action')->logHistory($actionID, $changes);
|
||
}
|
||
}
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 从日志中为设置变更信息。
|
||
* Create changes for action from a log.
|
||
*
|
||
* @param object $log
|
||
* @param string $repoRoot
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function createActionChanges(object $log, string $repoRoot, string $scm = 'svn'): array
|
||
{
|
||
if(empty($log->files)) return array();
|
||
|
||
$oldSelf = $this->server->PHP_SELF;
|
||
$this->server->set('PHP_SELF', $this->config->webRoot, '', false, true);
|
||
|
||
$diff = '';
|
||
foreach($log->files as $action => $actionFiles)
|
||
{
|
||
foreach($actionFiles as $file)
|
||
{
|
||
$catLink = trim(html::a($this->buildURL('cat', $repoRoot . $file, (string) $log->revision, $scm), 'view', '', "data-toggle='modal' data-size='{\"width\": 800, \"height\": 500}'"));
|
||
$diffLink = trim(html::a($this->buildURL('diff', $repoRoot . $file, (string) $log->revision, $scm), 'diff', '', "data-toggle='modal' data-size='{\"width\": 800, \"height\": 500}'"));
|
||
|
||
$catLink = str_replace('+', '%2B', $catLink);
|
||
$diffLink = str_replace('+', '%2B', $diffLink);
|
||
|
||
$diff .= $action . " " . $file . " $catLink ";
|
||
$diff .= $action == 'M' ? "$diffLink\n" : "\n" ;
|
||
}
|
||
}
|
||
|
||
$this->server->set('PHP_SELF', $oldSelf);
|
||
|
||
$change = new stdclass();
|
||
$change->field = $scm == 'svn' ? 'subversion' : 'git';
|
||
$change->old = '';
|
||
$change->new = '';
|
||
$change->diff = trim($diff);
|
||
return array($change);
|
||
}
|
||
|
||
/**
|
||
* 根据任务列表获取产品和执行。
|
||
* Get products and executions of tasks.
|
||
*
|
||
* @param array $tasks
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getTaskProductsAndExecutions(array $tasks): array
|
||
{
|
||
$records = array();
|
||
$products = $this->dao->select('t1.id,t1.execution,t2.product')->from(TABLE_TASK)->alias('t1')
|
||
->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.execution = t2.project')
|
||
->where('t1.id')->in($tasks)
|
||
->fetchGroup('id','product');
|
||
|
||
$executions = $this->dao->select('id, execution')->from(TABLE_TASK)->where('id')->in($tasks)->fetchPairs();
|
||
foreach($executions as $taskID => $executionID)
|
||
{
|
||
$record = array();
|
||
$record['execution'] = $executionID;
|
||
$record['product'] = isset($products[$taskID]) ? "," . implode(',', array_keys($products[$taskID])) . "," : ",0,";
|
||
$records[$taskID] = $record;
|
||
}
|
||
return $records;
|
||
}
|
||
|
||
/**
|
||
* 根据bug列表获取产品和执行。
|
||
* Get products and executions of bugs.
|
||
*
|
||
* @param array $bugs
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getBugProductsAndExecutions($bugs)
|
||
{
|
||
$records = $this->dao->select('id, execution, product')->from(TABLE_BUG)->where('id')->in($bugs)->fetchAll('id');
|
||
foreach($records as $record) $record->product = ",{$record->product},";
|
||
return $records;
|
||
}
|
||
|
||
/**
|
||
* 构造git和svn的展示链接。
|
||
* Build url for git and svn.
|
||
*
|
||
* @param string $methodName
|
||
* @param string $url
|
||
* @param string $revision
|
||
* @access public
|
||
* @return string
|
||
*/
|
||
public function buildURL(string $methodName, string $url, string $revision, string $scm = 'svn'): string
|
||
{
|
||
$buildedURL = helper::createLink($scm, $methodName, "url=&revision=$revision", 'html');
|
||
$buildedURL .= strpos($buildedURL, '?') === false ? '?' : '&';
|
||
$buildedURL .= 'repoUrl=' . helper::safe64Encode($url);
|
||
|
||
return $buildedURL;
|
||
}
|
||
|
||
/**
|
||
* 处理代码库信息,增加代码路径和api路径。
|
||
* Process git service, add code path and api path.
|
||
*
|
||
* @param object $repo
|
||
* @param bool $getCodePath
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function processGitService(object $repo, bool $getCodePath = false): object
|
||
{
|
||
$service = $this->loadModel('pipeline')->getByID((int)$repo->serviceHost);
|
||
if(!$service) return $repo;
|
||
|
||
if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
|
||
{
|
||
if($getCodePath)
|
||
{
|
||
if($repo->SCM == 'Gitlab') $repo->serviceProject = (int)$repo->serviceProject;
|
||
$project = $this->loadModel($repo->SCM)->apiGetSingleProject((int)$repo->serviceHost, $repo->serviceProject);
|
||
if(isset($project->web_url) && $repo->path != $project->web_url)
|
||
{
|
||
$repo->path = $project->web_url;
|
||
$this->dao->update(TABLE_REPO)->set('path')->eq($repo->path)->where('id')->eq($repo->id)->exec();
|
||
}
|
||
}
|
||
|
||
$repo->path = (!$repo->path && $service) ? sprintf($this->config->repo->{$service->type}->apiPath, $service->url, $repo->serviceProject) : $repo->path;
|
||
$repo->apiPath = sprintf($this->config->repo->{$service->type}->apiPath, $service->url, $repo->serviceProject);
|
||
$repo->client = $service ? $service->url : '';
|
||
$repo->password = $service ? $service->token : '';
|
||
$repo->codePath = isset($project->web_url) ? $project->web_url : $repo->path;
|
||
}
|
||
else
|
||
{
|
||
if(!is_dir($repo->path) && !is_writable(dirname($repo->path)))
|
||
{
|
||
$path = $this->app->getAppRoot() . "www/data/repo/{$repo->name}_{$repo->SCM}";
|
||
$repo->path = $path;
|
||
|
||
$this->dao->update(TABLE_REPO)->set('path')->eq($repo->path)->where('id')->eq($repo->id)->exec();
|
||
}
|
||
|
||
$repo->codePath = $service ? "{$service->url}/{$repo->serviceProject}" : $repo->path;
|
||
}
|
||
|
||
$repo->gitService = (int)$repo->serviceHost;
|
||
return $repo;
|
||
}
|
||
|
||
/**
|
||
* 处理webhook请求。
|
||
* Handle received GitLab webhook.
|
||
*
|
||
* @param string $event
|
||
* @param object $data
|
||
* @param object $repotime
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function handleWebhook(string $event, object $data, object $repo): bool
|
||
{
|
||
if(!in_array($event, array('Push Hook', 'Merge Request Hook', 'branch_updated'))) return false;
|
||
if(empty($data->commits)) return false;
|
||
|
||
/* Update code commit history. */
|
||
$commentGroup = $this->loadModel('job')->getTriggerGroup('commit', array($repo->id));
|
||
if(!in_array($repo->SCM, $this->config->repo->notSyncSCM)) return $this->loadModel('git')->updateCommit($repo, $commentGroup, false);
|
||
|
||
$scm = $this->app->loadClass('scm');
|
||
$scm->setEngine($repo);
|
||
|
||
$jobs = zget($commentGroup, $repo->id, array());
|
||
|
||
$accountPairs = array();
|
||
$userList = $this->loadModel($repo->SCM)->apiGetUsers($repo->gitService);
|
||
$accountIDPairs = $this->loadModel('pipeline')->getUserBindedPairs($repo->gitService, strtolower($repo->SCM), 'openID,account');
|
||
foreach($userList as $gitlabUser) $accountPairs[$gitlabUser->realname] = zget($accountIDPairs, $gitlabUser->id, '');
|
||
|
||
foreach($data->commits as $commit)
|
||
{
|
||
$time = zget($commit, 'timestamp', '');
|
||
if(isset($commit->author->when)) $time = $commit->author->when;
|
||
|
||
$log = new stdclass();
|
||
$log->revision = isset($commit->id) ? $commit->id : $commit->sha;
|
||
$log->msg = $commit->message;
|
||
$log->author = isset($commit->author->identity->name) ? $commit->author->identity->name : $commit->author->name;
|
||
$log->date = date("Y-m-d H:i:s", strtotime($time));
|
||
$log->files = array();
|
||
$log->repo = $repo;
|
||
|
||
if(!isset($commit->added))
|
||
{
|
||
$diffs = $scm->engine->getFilesByCommit($log->revision);
|
||
if(!empty($diffs))
|
||
{
|
||
foreach($diffs as $diff) $log->files[$diff->action][] = $diff->path;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
foreach($commit->added as $file) $log->files['A'][] = $file;
|
||
foreach($commit->removed as $file) $log->files['D'][] = $file;
|
||
foreach($commit->modified as $file) $log->files['M'][] = $file;
|
||
}
|
||
|
||
$objects = $this->parseComment($log->msg);
|
||
$this->saveAction2PMS($objects, $log, $repo->path, $repo->encoding, 'git', $accountPairs);
|
||
|
||
foreach($jobs as $job)
|
||
{
|
||
foreach(explode(',', $job->comment) as $comment)
|
||
{
|
||
if(strpos($log->msg, $comment) !== false)
|
||
{
|
||
$this->loadModel('job')->exec($job->id, array(), 'commit');
|
||
continue 2;
|
||
}
|
||
}
|
||
}
|
||
if(!empty($objects['stories']) || !empty($objects['tasks']) || !empty($objects['bugs']))
|
||
{
|
||
$historyLog = new stdclass();
|
||
$historyLog->committer = $log->author;
|
||
$historyLog->revision = $log->revision;
|
||
$historyLog->comment = $commit->message;
|
||
$historyLog->time = date("Y-m-d H:i:s", strtotime($time));
|
||
$this->saveCommit($repo->id, array('commits' => [$historyLog]), 0);
|
||
$revisions = $this->dao->select('id')->from(TABLE_REPOHISTORY)
|
||
->where('revision')->in($log->revision)
|
||
->andWhere('repo')->eq($repo->id)
|
||
->fetchPairs('id');
|
||
foreach (array('stories' => 'story', 'tasks' => 'task', 'bugs' => 'bug') as $objectType=>$modelType)
|
||
{
|
||
if(!empty($objects[$objectType]))
|
||
{
|
||
foreach($objects[$objectType] as $modelID)
|
||
{
|
||
$this->loadModel($modelType)->updateLinkedCommits((int)$modelID, $repo->id, $revisions);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* Get execution pairs.
|
||
*
|
||
* @param int $product
|
||
* @param int $branch
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getExecutionPairs(int $product, int $branch = 0): array
|
||
{
|
||
$pairs = array();
|
||
$executions = $this->loadModel('execution')->getList(0, 'all', 'undone', 0, $product, $branch);
|
||
$parents = $this->dao->select('distinct parent,parent')->from(TABLE_EXECUTION)->where('type')->eq('stage')->andWhere('grade')->gt(1)->andWhere('deleted')->eq(0)->fetchPairs();
|
||
foreach($executions as $execution)
|
||
{
|
||
if(!empty($parents[$execution->id]) or ($execution->type == 'stage' and in_array($execution->attribute, array('request', 'design', 'review')))) continue;
|
||
|
||
if($execution->type == 'stage' and $execution->grade > 1)
|
||
{
|
||
$parentExecutions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(trim($execution->path, ','))->andWhere('type')->in('stage,kanban,sprint')->orderBy('grade')->fetchPairs();
|
||
$execution->name = implode('/', $parentExecutions);
|
||
}
|
||
$pairs[$execution->id] = $execution->name;
|
||
}
|
||
return $pairs;
|
||
}
|
||
|
||
/**
|
||
* 获取代码库的clone地址。
|
||
* Get clone url.
|
||
*
|
||
* @param object $repo
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function getCloneUrl(object $repo): object
|
||
{
|
||
if(empty($repo->id)) return new stdclass();
|
||
|
||
$url = new stdClass();
|
||
if($repo->SCM == 'Subversion')
|
||
{
|
||
$url->svn = $repo->path;
|
||
}
|
||
elseif($repo->SCM == 'Gitlab')
|
||
{
|
||
$project = $this->loadModel('gitlab')->apiGetSingleProject($repo->gitService, (int)$repo->serviceProject);
|
||
if(isset($project->id))
|
||
{
|
||
$url->http = $project->http_url_to_repo ?? '';
|
||
$url->ssh = $project->ssh_url_to_repo ?? '';
|
||
}
|
||
}
|
||
elseif($repo->SCM == 'Gitea')
|
||
{
|
||
$project = $this->loadModel('gitea')->apiGetSingleProject($repo->gitService, (string)$repo->serviceProject);
|
||
if(isset($project->id))
|
||
{
|
||
$url->http = $project->clone_url;
|
||
$url->ssh = $project->ssh_url;
|
||
}
|
||
}
|
||
elseif($repo->SCM == 'Gogs')
|
||
{
|
||
$project = $this->loadModel('gogs')->apiGetSingleProject($repo->gitService, (string)$repo->serviceProject);
|
||
if(isset($project->id))
|
||
{
|
||
$url->http = $project->clone_url;
|
||
$url->ssh = $project->ssh_url;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
$this->scm = $this->app->loadClass('scm');
|
||
$this->scm->setEngine($repo);
|
||
$url = $this->scm->getCloneUrl();
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* 获取代码文件的提交信息。
|
||
* Get file commits.
|
||
*
|
||
* @param object $repo
|
||
* @param string $branch
|
||
* @param string $parent
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getFileCommits(object $repo, string $branch, string $parent = ''): array
|
||
{
|
||
/* Get file commits by repo. */
|
||
if($repo->SCM != 'Subversion' && empty($branch)) $branch = $this->cookie->repoBranch;
|
||
|
||
$parent = '/' . ltrim($parent, '/');
|
||
if($repo->prefix) $parent = rtrim($repo->prefix . $parent, '/');
|
||
|
||
$fileCommits = $this->dao->select('t1.id,t1.path,t1.type,t1.action,t1.oldPath,t1.parent,t2.revision,t2.comment,t2.committer,t2.time')->from(TABLE_REPOFILES)->alias('t1')
|
||
->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.revision=t2.id')
|
||
->beginIF($repo->SCM != 'Subversion' && $branch)->leftJoin(TABLE_REPOBRANCH)->alias('t3')->on('t2.id=t3.revision')->fi()
|
||
->where('t1.repo')->eq($repo->id)
|
||
->andWhere('left(t2.`comment`, 12)')->ne('Merge branch')
|
||
->beginIF($repo->SCM != 'Subversion' && $branch)->andWhere('t3.branch')->eq($branch)->fi()
|
||
->beginIF($repo->SCM == 'Subversion')->andWhere('t1.parent')->eq("$parent")->fi()
|
||
->beginIF($repo->SCM != 'Subversion')->andWhere('t1.parent')->like("$parent%")->fi()
|
||
->orderBy('t2.`time` asc')
|
||
->fetchAll('path');
|
||
|
||
$files = $folders = $fileSort = $dirSort = array();
|
||
$existsFiles = array();
|
||
foreach($fileCommits as $fileCommit)
|
||
{
|
||
if($fileCommit->action != 'D' && strpos($fileCommit->path, $parent) === 0) $existsFiles[$fileCommit->path] = true;
|
||
if($fileCommit->action == 'R' && isset($existsFiles[$fileCommit->oldPath])) unset($existsFiles[$fileCommit->oldPath]);
|
||
}
|
||
|
||
foreach($fileCommits as $fileCommit)
|
||
{
|
||
/* Filter by parent. */
|
||
if(!isset($existsFiles[$fileCommit->path])) continue;
|
||
|
||
$pathList = explode('/', ltrim($fileCommit->path, '/'));
|
||
$file = new stdclass();
|
||
$file->revision = $fileCommit->revision;
|
||
$file->comment = $fileCommit->comment;
|
||
$file->account = $fileCommit->committer;
|
||
$file->date = $fileCommit->time;
|
||
$file->kind = 'file';
|
||
$file->name = end($pathList);
|
||
|
||
if($fileCommit->parent == $parent && $fileCommit->type == 'file')
|
||
{
|
||
$files[] = $file;
|
||
$fileSort[] = $file->name;
|
||
}
|
||
else
|
||
{
|
||
$childPath = explode('/', ltrim(substr($fileCommit->path, strlen($parent)), '/'));
|
||
$fileName = $fileCommit->type == 'dir' ? end($pathList) : $childPath[0];
|
||
if(in_array($fileName, $dirSort)) continue;
|
||
|
||
$file->name = $fileName;
|
||
$file->kind = 'dir';
|
||
$folders[] = $file;
|
||
$dirSort[] = $fileName;
|
||
}
|
||
}
|
||
array_multisort($fileSort, SORT_ASC, $files);
|
||
array_multisort($dirSort, SORT_ASC, $folders);
|
||
|
||
return array_merge($folders, $files);
|
||
}
|
||
|
||
/**
|
||
* 获取目录树。
|
||
* Get html for file tree.
|
||
*
|
||
* @param object $repo
|
||
* @param string $branch
|
||
* @param array $diffs
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getFileTree(object $repo, string $branch = '', ?array $diffs = null): array
|
||
{
|
||
set_time_limit(0);
|
||
$allFiles = array();
|
||
if(is_null($diffs))
|
||
{
|
||
if($repo->SCM != 'Subversion' && empty($branch)) $branch = $this->cookie->repoBranch;
|
||
$files = $this->dao->select('t1.path,t2.time,t1.action')->from(TABLE_REPOFILES)->alias('t1')
|
||
->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.revision=t2.id')
|
||
->leftJoin(TABLE_REPOBRANCH)->alias('t3')->on('t2.id=t3.revision')
|
||
->where('t1.repo')->eq($repo->id)
|
||
->andWhere('t1.type')->eq('file')
|
||
->andWhere('left(t2.`comment`, 12)')->ne('Merge branch')
|
||
->beginIF($repo->SCM != 'Subversion' && $branch)->andWhere('t3.branch')->eq($branch)->fi()
|
||
->orderBy('t2.`time` asc')
|
||
->fetchAll('path');
|
||
|
||
$removeDirs = array();
|
||
if($repo->SCM == 'Subversion')
|
||
{
|
||
$removeDirs = $this->dao->select('t2.time,t1.path')->from(TABLE_REPOFILES)->alias('t1')
|
||
->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.revision=t2.id')
|
||
->where('t1.repo')->eq($repo->id)
|
||
->andWhere('t1.type')->eq('dir')
|
||
->andWhere('t1.action')->eq('D')
|
||
->fetchPairs();
|
||
}
|
||
|
||
foreach($files as $file)
|
||
{
|
||
foreach($removeDirs as $removeTime => $dir)
|
||
{
|
||
if(strpos($file->path, $dir . '/') === 0 and $file->time <= $removeTime)
|
||
{
|
||
$file->action = 'D';
|
||
break;
|
||
}
|
||
}
|
||
|
||
if($file->action != 'D') $allFiles[] = $file->path;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
foreach($diffs as $diff) $allFiles[] = $diff->fileName;
|
||
}
|
||
|
||
return $this->repoTao->buildFileTree($allFiles);
|
||
}
|
||
|
||
/**
|
||
* 根据提交获取关联信息。
|
||
* Get relation by commit.
|
||
*
|
||
* @param int $repoID
|
||
* @param string $commit
|
||
* @param string $type story|bug|task
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getRelationByCommit(int $repoID, string $commit, string $type = ''): array
|
||
{
|
||
$relationList = $this->dao->select('t1.BID as id, t1.BType as type')->from(TABLE_RELATION)->alias('t1')
|
||
->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.AID = t2.id')
|
||
->where('t2.revision')->eq($commit)
|
||
->andWhere('t2.repo')->eq($repoID)
|
||
->andWhere('t1.AType')->eq('revision')
|
||
->beginIF($type)->andWhere('t1.BType')->eq($type)->fi()
|
||
->fetchGroup('type', 'id');
|
||
|
||
$stories = empty($relationList['story']) ? array() : $this->loadModel('story')->getByList(array_keys($relationList['story']));
|
||
$bugs = empty($relationList['bug']) ? array() : $this->loadModel('bug')->getByIdList(array_keys($relationList['bug']));
|
||
$tasks = empty($relationList['task']) ? array() : $this->loadModel('task')->getByIdList(array_keys($relationList['task']));
|
||
|
||
$index = 0;
|
||
$titleList = array();
|
||
foreach($relationList as $objectType => $objects)
|
||
{
|
||
foreach($objects as $object)
|
||
{
|
||
$titleList[$index] = array(
|
||
'id' => $object->id,
|
||
'type' => $objectType,
|
||
'title' => "#$object->id "
|
||
);
|
||
if($objectType == 'story')
|
||
{
|
||
$story = zget($stories, $object->id, array());
|
||
$titleList[$index]['title'] .= zget($story, 'title', '');
|
||
}
|
||
elseif($objectType == 'bug')
|
||
{
|
||
$bug = zget($bugs, $object->id, array());
|
||
$titleList[$index]['title'] .= zget($bug, 'title', '');
|
||
}
|
||
elseif($objectType == 'task')
|
||
{
|
||
$task = zget($tasks, $object->id, array());
|
||
$titleList[$index]['title'] .= zget($task, 'name', '');
|
||
}
|
||
|
||
$index ++;
|
||
}
|
||
}
|
||
|
||
return $type ? $titleList : array_values($titleList);
|
||
}
|
||
|
||
/**
|
||
* 根据关联对象获取提交。
|
||
* Get relation commit.
|
||
*
|
||
* @param int $objectID
|
||
* @param string $objectType story|bug|task
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getCommitsByObject(int $objectID, string $objectType): array
|
||
{
|
||
return $this->dao->select('t2.*')->from(TABLE_RELATION)->alias('t1')
|
||
->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.AID = t2.id')
|
||
->where('t1.BID')->eq($objectID)
|
||
->andWhere('t1.BType')->eq($objectType)
|
||
->andWhere('t1.AType')->eq('revision')
|
||
->andWhere('t1.relation')->eq('commit')
|
||
->fetchAll('', false);
|
||
}
|
||
|
||
/*
|
||
* 移除没有权限的项目。
|
||
* Remove projects without privileges.
|
||
*
|
||
* @param array $productIDList
|
||
* @param array $projectIDList
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function filterProject(array $productIDList, array $projectIDList = array()): array
|
||
{
|
||
/* Get all projects that can be accessed. */
|
||
$accessProjects = array();
|
||
foreach($productIDList as $productID)
|
||
{
|
||
$projects = $this->loadModel('product')->getProjectPairsByProduct((int)$productID);
|
||
$accessProjects = $accessProjects + $projects;
|
||
}
|
||
|
||
/* Get linked projects. */
|
||
$linkedProjects = $this->dao->select('id,name')->from(TABLE_PROJECT)->where('id')->in($projectIDList)->fetchPairs('id', 'name');
|
||
return $accessProjects + $linkedProjects; // Merge projects can be accessed and exists.
|
||
}
|
||
|
||
/**
|
||
* 更新代码提交历史。
|
||
* Update commit history.
|
||
*
|
||
* @param int $repoID
|
||
* @param int $objectID
|
||
* @param string $branchID
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function updateCommit(int $repoID, int $objectID = 0, string $branchID = ''): bool
|
||
{
|
||
$repo = $this->getByID($repoID);
|
||
if($repo->SCM == 'Gitlab') return true;
|
||
|
||
/* Update code commit history. */
|
||
$commentGroup = $this->loadModel('job')->getTriggerGroup('commit', array($repoID));
|
||
if(in_array($repo->SCM, $this->config->repo->gitTypeList))
|
||
{
|
||
$branch = $this->cookie->repoBranch;
|
||
if($branchID)
|
||
{
|
||
$currentBranches = $this->getBranches($repo, false, 'database');
|
||
if(!in_array($branch, $currentBranches))
|
||
{
|
||
$link = $this->createLink('showSyncCommit', "repoID=$repoID&objectID=$objectID&branch=$branchID", '', false);
|
||
return $this->app->control->locate($link);
|
||
}
|
||
}
|
||
$this->loadModel('git')->updateCommit($repo, $commentGroup, false);
|
||
$_COOKIE['repoBranch'] = $branch;
|
||
}
|
||
|
||
if($repo->SCM == 'Subversion') $this->loadModel('svn')->updateCommit($repo, $commentGroup, false);
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 移除已经删除的分支。
|
||
* Delete the deleted branch.
|
||
*
|
||
* @param int $repoID
|
||
* @param array $latestBranches
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function checkDeletedBranches(int $repoID, array $latestBranches): bool
|
||
{
|
||
if(empty($latestBranches)) return false;
|
||
|
||
$currentBranches = $this->dao->select('branch')->from(TABLE_REPOBRANCH)->where('repo')->eq($repoID)->groupBy('branch')->fetchPairs('branch');
|
||
$deletedBranches = array_diff($currentBranches, $latestBranches);
|
||
foreach($deletedBranches as $deletedBranch)
|
||
{
|
||
if($deletedBranch == 'master') continue;
|
||
|
||
$revisionIds = $this->dao->select('revision')->from(TABLE_REPOBRANCH)->where('repo')->eq($repoID)->andWhere('branch')->eq($deletedBranch)->fetchPairs('revision');
|
||
$branchRevisionIds = $this->dao->select('revision,count(branch) as count')->from(TABLE_REPOBRANCH)->where('revision')->in($revisionIds)->groupBy('revision')->having('count')->eq(1)->fetchPairs('revision', 'revision');
|
||
$fileIds = $this->dao->select('id')->from(TABLE_REPOFILES)->where('revision')->in($branchRevisionIds)->fetchPairs('id');
|
||
|
||
$this->dao->delete()->from(TABLE_REPOHISTORY)->where('id')->in($branchRevisionIds)->exec();
|
||
$this->dao->delete()->from(TABLE_REPOFILES)->where('id')->in($fileIds)->exec();
|
||
$this->dao->delete()->from(TABLE_REPOBRANCH)->where('repo')->eq($repoID)->andWhere('branch')->eq($deletedBranch)->exec();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 判断按钮是否可点击。
|
||
* Judge an action is clickable or not.
|
||
*
|
||
* @param object $repo
|
||
* @param string $action
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public static function isClickable(object $repo, string $action): bool
|
||
{
|
||
$action = strtolower($action);
|
||
|
||
if($action == 'execjob') return common::hasPriv('sonarqube', $action) && !$repo->exec;
|
||
if($action == 'reportview') return common::hasPriv('sonarqube', $action) && !$repo->report;
|
||
if(!commonModel::hasPriv('repo', $action)) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取gitlab项目列表。
|
||
* Get gitlab projects.
|
||
*
|
||
* @param int $gitlabID
|
||
* @param string $projectFilter
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getGitlabProjects(int $gitlabID, string $projectFilter = ''): array
|
||
{
|
||
if($this->app->user->admin || ($projectFilter == 'ALL' && common::hasPriv('repo', 'create')))
|
||
{
|
||
$projects = $this->loadModel('gitlab')->apiGetProjects($gitlabID, 'true', 0, 0, false);
|
||
}
|
||
else
|
||
{
|
||
$gitlabUser = $this->loadModel('pipeline')->getOpenIdByAccount($gitlabID, 'gitlab', $this->app->user->account);
|
||
if(!$gitlabUser) return array();
|
||
|
||
$projects = $this->loadModel('gitlab')->apiGetProjects($gitlabID, $projectFilter ? 'false' : 'true');
|
||
$groupIDList = array(0 => 0);
|
||
$groups = $this->gitlab->apiGetGroups($gitlabID, 'name_asc', 'developer');
|
||
foreach($groups as $group) $groupIDList[] = $group->id;
|
||
if($projectFilter == 'IS_DEVELOPER')
|
||
{
|
||
foreach($projects as $key => $project)
|
||
{
|
||
if(!$this->gitlab->checkUserAccess($gitlabID, 0, $project, $groupIDList, 'developer')) unset($projects[$key]);
|
||
}
|
||
}
|
||
}
|
||
|
||
$importedProjects = $this->getImportedProjects($gitlabID);
|
||
$projects = array_filter($projects, function($project) use ($importedProjects) { return !in_array($project->id, $importedProjects); });
|
||
|
||
return $projects;
|
||
}
|
||
|
||
/**
|
||
* Get repo groups.
|
||
*
|
||
* @param int $serverID
|
||
* @param int $groupID
|
||
* @access public
|
||
* @return string|array|false
|
||
*/
|
||
public function getGroups(int $serverID, int|string $groupID = 0): string|array|false
|
||
{
|
||
$server = $this->loadModel('pipeline')->getByID($serverID);
|
||
if(empty($server->type)) return false;
|
||
|
||
$getGroupFunc = 'get' . $server->type . 'Groups';
|
||
$groups = $this->$getGroupFunc($serverID);
|
||
|
||
if($groupID !== 0)
|
||
{
|
||
foreach($groups as $group)
|
||
{
|
||
if($group['value'] == $groupID) return $group['text'];
|
||
}
|
||
return false;
|
||
}
|
||
|
||
return $groups;
|
||
}
|
||
|
||
/**
|
||
* Get gitlab groups.
|
||
*
|
||
* @param int $gitlabID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function getGitlabGroups(int $gitlabID): array
|
||
{
|
||
$groups = $this->loadModel('gitlab')->apiGetGroups($gitlabID, 'name_asc');
|
||
$options = array();
|
||
foreach($groups as $group)
|
||
{
|
||
$options[] = array('text' => $group->name, 'value' => $group->id);
|
||
}
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* Get gitea groups.
|
||
*
|
||
* @param int $giteaID
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getGiteaGroups(int $giteaID): array
|
||
{
|
||
$groups = $this->loadModel('gitea')->apiGetGroups($giteaID);
|
||
$options = array();
|
||
foreach($groups as $group)
|
||
{
|
||
$options[] = array('text' => $group->username, 'value' => $group->id);
|
||
}
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* 更新版本库最后提交时间。
|
||
* Update repo last commited date.
|
||
*
|
||
* @param int $repoID
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function updateCommitDate(int $repoID): void
|
||
{
|
||
$repo = $this->getByID($repoID);
|
||
if(empty($repo->id)) return;
|
||
|
||
if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
|
||
{
|
||
$scm = $this->app->loadClass('scm');
|
||
$scm->setEngine($repo);
|
||
$commits = $scm->engine->getCommitsByPath('', '', 'HEAD', 1, 1);
|
||
if(empty($commits)) return;
|
||
|
||
$commitDate = $repo->SCM == 'Gitlab' ? $commits[0]->committed_date : $commits[0]->author->when;
|
||
if(!empty($commitDate))
|
||
{
|
||
$lastCommitDate = date('Y-m-d H:i:s', strtotime($commitDate));
|
||
$this->dao->update(TABLE_REPO)->set('lastCommit')->eq($lastCommitDate)->where('id')->eq($repoID)->exec();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查gitea连接。
|
||
* Check gitea connection.
|
||
*
|
||
* @param string $scm
|
||
* @param string $name
|
||
* @param int|string $serviceHost
|
||
* @param int|string $serviceProject
|
||
* @access public
|
||
* @return string|false
|
||
*/
|
||
public function checkGiteaConnection(string $scm, string $name, int|string $serviceHost, int|string $serviceProject): string|false
|
||
{
|
||
if($name != '' and $serviceProject != '')
|
||
{
|
||
$module = strtolower($scm);
|
||
$project = $this->loadModel($module)->apiGetSingleProject($serviceHost, $serviceProject);
|
||
if(isset($project->tokenCloneUrl))
|
||
{
|
||
$path = $this->app->getAppRoot() . 'www/data/repo/' . $name . '_' . $module;
|
||
if(!realpath($path))
|
||
{
|
||
$cmd = 'git clone --progress -v "' . $project->tokenCloneUrl . '" "' . $path . '" > "' . $this->app->getTmpRoot() . "log/clone.progress.$module.{$name}.log\" 2>&1 &";
|
||
if(PHP_OS == 'WINNT') $cmd = "start /b $cmd";
|
||
exec($cmd);
|
||
}
|
||
return $path;
|
||
}
|
||
else
|
||
{
|
||
dao::$errors['serviceProject'] = $this->lang->repo->error->noCloneAddr;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 根据路径获取gitlab文件列表。
|
||
* Get gitlab files by path.
|
||
*
|
||
* @param object $repo
|
||
* @param string $path
|
||
* @param string $branch
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getGitlabFilesByPath(object $repo, string $path = '', string $branch = ''): array
|
||
{
|
||
$fileList = $this->getTreeByGraphql($repo, $path, $branch, 'blobs');
|
||
$folderList = $this->getTreeByGraphql($repo, $path, $branch, 'trees');
|
||
if(empty($fileList) && empty($folderList)) return array();
|
||
|
||
$files = array();
|
||
$folders = array();
|
||
$fileSort = $dirSort = array(); // Use it to sort array.
|
||
|
||
foreach($fileList as $file)
|
||
{
|
||
if(in_array($file->name, $fileSort)) continue;
|
||
$base64Name = $this->encodePath($file->path);
|
||
|
||
$fileInfo = new stdclass();
|
||
$fileInfo->id = $base64Name;
|
||
$fileInfo->name = $file->name;
|
||
$fileInfo->text = $file->name;
|
||
$fileInfo->path = $file->path;
|
||
$fileInfo->key = $base64Name;
|
||
$fileInfo->kind = 'file';
|
||
|
||
$files[] = $fileInfo;
|
||
$fileSort[] = $file->name;
|
||
}
|
||
|
||
foreach($folderList as $dir)
|
||
{
|
||
if(in_array($dir->name, $dirSort)) continue;
|
||
$base64Name = $this->encodePath($dir->path);
|
||
|
||
$folder = new stdclass();
|
||
$folder->id = $base64Name;
|
||
$folder->name = $dir->name;
|
||
$folder->text = $dir->name;
|
||
$folder->path = $dir->path;
|
||
$folder->key = $base64Name;
|
||
$folder->kind = 'dir';
|
||
$folder->items = array('url' => helper::createLink('repo', 'ajaxGetFiles', "repoID={$repo->id}&branch={$branch}&path=" . helper::safe64Encode($dir->path)));
|
||
|
||
$folders[] = $folder;
|
||
$dirSort[] = $dir->name;
|
||
}
|
||
array_multisort($fileSort, SORT_ASC, $files);
|
||
array_multisort($dirSort, SORT_ASC, $folders);
|
||
|
||
return array_merge($folders, $files);
|
||
}
|
||
|
||
/**
|
||
* 通过Graphql获取GitLab文件列表。
|
||
* Get GitLab files by Graphql.
|
||
*
|
||
* @param object $repo
|
||
* @param string $path
|
||
* @param string $branch
|
||
* @param string $type
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getTreeByGraphql(object $repo, string $path = '', string $branch = '', string $type = 'blobs'): array
|
||
{
|
||
if(!$branch) $branch = $this->cookie->branch;
|
||
|
||
$this->loadModel('gitlab');
|
||
$fileList = array();
|
||
$endCursor = '';
|
||
$hasNextPage = true;
|
||
while($hasNextPage)
|
||
{
|
||
$query = 'query { project(fullPath: "%s") {repository {tree(path: "' . trim($path, '/') . '", ref: "' . $branch . '") {' . $type . '(after: "' . $endCursor . '") {pageInfo {endCursor hasNextPage} nodes {sha name path}}}}}}';
|
||
$response = $this->gitlab->apiGetByGraphql($repo, $query);
|
||
|
||
if(!$endCursor && !isset($response->data->project->repository)) return array();
|
||
if(empty($response->data->project->repository->tree)) break;
|
||
|
||
$fileList = array_merge($fileList, $response->data->project->repository->tree->{$type}->nodes);
|
||
$hasNextPage = $response->data->project->repository->tree->{$type}->pageInfo->hasNextPage;
|
||
$endCursor = $response->data->project->repository->tree->{$type}->pageInfo->endCursor;
|
||
}
|
||
return $fileList;
|
||
}
|
||
|
||
/**
|
||
* 查询提交记录的版本号。
|
||
* Get history revision.
|
||
*
|
||
* @param int $repoID
|
||
* @param string $revision
|
||
* @param bool $withCommit
|
||
* @param string $condition
|
||
* @access public
|
||
* @return string|object|false
|
||
*/
|
||
public function getHistoryRevision(int $repoID, string $revision, bool $withCommit = false, string $condition = 'eq'): string|object|false
|
||
{
|
||
$field = $withCommit ? 'revision, commit' : 'revision';
|
||
return $this->dao->select($field)->from(TABLE_REPOHISTORY)
|
||
->where('repo')->eq($repoID)
|
||
->beginIF($condition != 'lt')->andWhere('revision')->eq($revision)->fi()
|
||
->beginIF($condition == 'lt')->andWhere('revision')->lt($revision)->fi()
|
||
->orderBy('id desc')
|
||
->fetch($withCommit ? '' : 'revision');
|
||
}
|
||
|
||
/**
|
||
* 通过指令开始任务。
|
||
* Start task by commit.
|
||
*
|
||
* @param object $task
|
||
* @param array $params
|
||
* @param object $action
|
||
* @param array $changes
|
||
* @access private
|
||
* @return bool
|
||
*/
|
||
private function startTask(object $task, array $params, object $action, array $changes): bool
|
||
{
|
||
$now = helper::now();
|
||
$newTask = new stdclass();
|
||
$newTask->id = $task->id;
|
||
$newTask->status = 'doing';
|
||
$newTask->left = $params['left'];
|
||
$newTask->consumed = $params['consumed'] + $task->consumed;
|
||
$newTask->realStarted = $now;
|
||
$newTask->lastEditedBy = $this->app->user->account;
|
||
$newTask->lastEditedDate = $now;
|
||
if($newTask->left == 0 && empty($task->team))
|
||
{
|
||
$newTask->status = 'done';
|
||
$newTask->finishedBy = $this->app->user->account;
|
||
$newTask->finishedDate = $now;
|
||
$newTask->assignedTo = $task->openedBy;
|
||
}
|
||
|
||
$this->loadModel('task');
|
||
$currentTeam = !empty($task->team) ? $this->task->getTeamByAccount($task->team) : array();
|
||
$effort = new stdclass();
|
||
$effort->date = helper::today();
|
||
$effort->task = $newTask->id;
|
||
$effort->consumed = zget($newTask, 'consumed', 0);
|
||
$effort->left = zget($newTask, 'left', 0);
|
||
$effort->account = $this->app->user->account;
|
||
$effort->work = $this->lang->action->label->started . $this->lang->task->task . " : " . $task->name;
|
||
$effort->consumed = !empty($task->team) && $currentTeam ? $effort->consumed - $currentTeam->consumed : $effort->consumed - $task->consumed;
|
||
if($effort->consumed > 0) $effortID = $this->task->addTaskEffort($effort);
|
||
if($task->mode == 'linear' && !empty($effortID)) $this->task->updateEffortOrder($effortID, $currentTeam->order);
|
||
|
||
$taskChanges = $this->task->start($task, $newTask);
|
||
if($taskChanges)
|
||
{
|
||
$taskChanges = array_merge($taskChanges, $changes);
|
||
$action->action = $newTask->left == 0 ? 'finished' : 'started';
|
||
$this->saveRecord($action, $taskChanges);
|
||
|
||
$this->task->afterStart($task, array(), (float)$newTask->left, array());
|
||
}
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 根据指令完成任务。
|
||
* Finish task by commit.
|
||
*
|
||
* @param object $task
|
||
* @param array $params
|
||
* @param object $action
|
||
* @param array $changes
|
||
* @access private
|
||
* @return bool
|
||
*/
|
||
private function finishTask(object $task, array $params, object $action, array $changes): bool
|
||
{
|
||
$now = helper::now();
|
||
$newTask = new stdclass();
|
||
$newTask->status = 'done';
|
||
$newTask->left = zget($params, 'left', 0);
|
||
$newTask->consumed = $params['consumed'] + $task->consumed;
|
||
$newTask->assignedTo = $task->openedBy;
|
||
$newTask->realStarted = $task->realStarted ? $task->realStarted : $now;
|
||
$newTask->finishedDate = $now;
|
||
$newTask->lastEditedDate = $now;
|
||
$newTask->assignedDate = $now;
|
||
$newTask->finishedBy = $this->app->user->account;
|
||
$newTask->lastEditedBy = $this->app->user->account;
|
||
|
||
$this->loadModel('task');
|
||
if(empty($task->team))
|
||
{
|
||
$consumed = $params['consumed'];
|
||
}
|
||
else
|
||
{
|
||
$currentTeam = $this->task->getTeamByAccount($task->team);
|
||
$consumed = $currentTeam ? $task->consumed - $currentTeam->consumed : $newTask->consumed;
|
||
}
|
||
|
||
$effort = new stdclass();
|
||
$effort->date = helper::today();
|
||
$effort->task = $task->id;
|
||
$effort->left = 0;
|
||
$effort->account = $this->app->user->account;
|
||
$effort->consumed = $consumed > 0 ? $consumed : 0;
|
||
$effort->work = $this->lang->action->label->finished . $this->lang->task->task . " : " . $task->name;
|
||
if($effort->consumed > 0) $effortID = $this->task->addTaskEffort($effort);
|
||
if($task->mode == 'linear' && !empty($effortID)) $this->task->updateEffortOrder($effortID, $currentTeam->order);
|
||
|
||
$taskChanges = $this->task->finish($task, $newTask);
|
||
if($taskChanges)
|
||
{
|
||
$taskChanges = array_merge($taskChanges, $changes);
|
||
$action->action = 'finished';
|
||
$this->saveRecord($action, $taskChanges);
|
||
|
||
$this->task->afterStart($task, array(), 0, array());
|
||
}
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 根据提交信息设置任务信息。
|
||
* Set task by commit.
|
||
*
|
||
* @param object $task
|
||
* @param array $taskActions
|
||
* @param object $action
|
||
* @param array $changes
|
||
* @param string $scm
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function setTaskByCommit(object $task, array $taskActions, object $action, array $changes, string $scm): bool
|
||
{
|
||
foreach($taskActions as $taskAction => $params)
|
||
{
|
||
if($taskAction == 'start' && $task->status == 'wait')
|
||
{
|
||
$this->startTask($task, $params, $action, $changes);
|
||
dao::$errors = array();
|
||
}
|
||
elseif($taskAction == 'effort' && in_array($task->status, array('wait', 'pause', 'doing')))
|
||
{
|
||
$action->action = $scm == 'svn' ? 'svncommited' : 'gitcommited';
|
||
$this->saveEffortForCommit($task->id, $params, $action, $changes);
|
||
}
|
||
elseif($taskAction == 'finish' and in_array($task->status, array('wait', 'pause', 'doing')))
|
||
{
|
||
$this->finishTask($task, $params, $action, $changes);
|
||
}
|
||
}
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 根据提交信息设置工时。
|
||
* Set effort by commit message.
|
||
*
|
||
* @param int $taskID
|
||
* @param array $params
|
||
* @param object $action
|
||
* @param array $changes
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function saveEffortForCommit(int $taskID, array $params, object $action, array $changes): bool
|
||
{
|
||
unset($_POST['consumed']);
|
||
unset($_POST['left']);
|
||
|
||
$_POST['date'][1] = date('Y-m-d');
|
||
$_POST['consumed'][1] = $params['consumed'];
|
||
$_POST['left'][1] = $params['left'];
|
||
$_POST['work'][1] = str_replace('<br />', "\n", $action->comment);
|
||
|
||
$this->loadModel('task');
|
||
$workhour = form::batchData($this->config->task->form->recordWorkhour)->get();
|
||
$this->task->recordWorkhour($taskID, $workhour);
|
||
|
||
$this->saveRecord($action, $changes);
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 根据提交信息设置Bug状态。
|
||
* Set bug status by commit.
|
||
*
|
||
* @param array $bugs
|
||
* @param array $actions
|
||
* @param object $action
|
||
* @param array $changes
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function setBugStatusByCommit(array $bugs, array $actions, object $action, array $changes): array
|
||
{
|
||
global $app;
|
||
$productsAndExecutions = $this->loadModel('bug')->getByIdList($bugs);
|
||
foreach($actions['bug'] as $bugID => $bugActions)
|
||
{
|
||
$app->rawModule = 'bug';
|
||
$bug = $this->bug->getByID($bugID);
|
||
if(empty($bug)) continue;
|
||
|
||
$action->objectType = 'bug';
|
||
$action->objectID = $bugID;
|
||
$action->product = ",{$productsAndExecutions[$bugID]->product},";
|
||
$action->execution = $productsAndExecutions[$bugID]->execution;
|
||
foreach($bugActions as $bugAction => $params)
|
||
{
|
||
$_POST = array();
|
||
if($bugAction == 'resolve' && $bug->status == 'active')
|
||
{
|
||
$app->rawMethod = 'resolve';
|
||
$this->post->set('resolvedBuild', 'trunk');
|
||
$this->post->set('resolution', 'fixed');
|
||
|
||
$newBug = form::data($this->config->bug->form->resolve)
|
||
->setDefault('assignedTo', $bug->openedBy)
|
||
->add('id', $bug->id)
|
||
->add('execution', $bug->execution)
|
||
->get();
|
||
|
||
$changes = array();
|
||
$result = $this->bug->resolve($newBug);
|
||
if($result)
|
||
{
|
||
$newBug = $this->bug->getByID($bugID);
|
||
$changes = common::createChanges($bug, $newBug);
|
||
}
|
||
|
||
foreach($changes as $change) $changes[] = $change;
|
||
if($changes)
|
||
{
|
||
$action->action = 'resolved';
|
||
$action->extra = 'fixed';
|
||
$this->saveRecord($action, $changes);
|
||
}
|
||
}
|
||
}
|
||
|
||
dao::$errors = array();
|
||
unset($bugs[$bugID]);
|
||
}
|
||
|
||
return $bugs;
|
||
}
|
||
|
||
/**
|
||
* 保存提交信息关联的日志。
|
||
* Save commit linkage log.
|
||
*
|
||
* @param array $objects
|
||
* @param object $action
|
||
* @param array $changes
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function saveObjectToPms(array $objects, object $action, array $changes): bool
|
||
{
|
||
$singular = array('stories' => 'story', 'tasks' => 'task', 'bugs' => 'bug', 'designs' => 'design');
|
||
foreach(array_keys($objects) as $objectType)
|
||
{
|
||
if($objectType == 'actions') continue;
|
||
|
||
if($objects[$objectType])
|
||
{
|
||
$objectList = array();
|
||
if($objectType == 'stories')
|
||
{
|
||
$objectList = $this->loadModel('story')->getByList($objects[$objectType]);
|
||
}
|
||
elseif($objectType == 'bugs')
|
||
{
|
||
$objectList = $this->getBugProductsAndExecutions($objects[$objectType]);
|
||
}
|
||
elseif($objectType != 'designs')
|
||
{
|
||
$objectList = $this->getTaskProductsAndExecutions($objects[$objectType]);
|
||
}
|
||
|
||
foreach($objects[$objectType] as $objectID)
|
||
{
|
||
$objectID = (int)$objectID;
|
||
if(!isset($objectList[$objectID])) continue;
|
||
|
||
$action->objectType = $singular[$objectType];
|
||
$action->objectID = $objectID;
|
||
|
||
if($objectType != 'designs')
|
||
{
|
||
$action->product = in_array($objectType, array('stories', 'bugs')) ? $objectList[$objectID]->product : $objectList[$objectID]['product'];
|
||
$action->execution = in_array($objectType, array('stories', 'bugs')) ? 0 : $objectList[$objectID]['execution'];
|
||
}
|
||
|
||
$this->saveRecord($action, $changes);
|
||
}
|
||
}
|
||
}
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 获取并列展示的对比信息。
|
||
* Get appose diff.
|
||
*
|
||
* @param array $diffs
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getApposeDiff(array $diffs): array
|
||
{
|
||
foreach($diffs as $diffFile)
|
||
{
|
||
if(empty($diffFile->contents)) continue;
|
||
foreach($diffFile->contents as $content)
|
||
{
|
||
$old = array();
|
||
$new = array();
|
||
foreach($content->lines as $line)
|
||
{
|
||
if($line->type != 'new') $old[$line->oldlc] = $line->line;
|
||
if($line->type != 'old') $new[$line->newlc] = $line->line;
|
||
}
|
||
$content->old = $old;
|
||
$content->new = $new;
|
||
}
|
||
}
|
||
return $diffs;
|
||
}
|
||
|
||
/**
|
||
* 根据条件获取版本库列表。
|
||
* Get repo list by condition.
|
||
*
|
||
* @param string $repoQuery
|
||
* @param string $SCM
|
||
* @param string $orderBy
|
||
* @param object $pager
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getListByCondition(string $repoQuery, string $SCM, string $orderBy = 'id_desc', ?object $pager = null): array
|
||
{
|
||
return $this->dao->select('*')->from(TABLE_REPO)
|
||
->where('deleted')->eq('0')
|
||
->beginIF(!empty($repoQuery))->andWhere($repoQuery)->fi()
|
||
->beginIF($SCM)->andWhere('SCM')->in($SCM)->fi()
|
||
->orderBy($orderBy)
|
||
->page($pager)
|
||
->fetchAll('id', false);
|
||
}
|
||
|
||
/*
|
||
* 保存对象和分支的关联关系。
|
||
* Save object and branch relation.
|
||
*
|
||
* @param int $repoID
|
||
* @param string $branch
|
||
* @param int $objectID
|
||
* @param string $objectType
|
||
* @param string $relation
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function saveRelation(int $repoID, string $branch, int $objectID, string $objectType, string $relation = 'linkrepobranch'): bool
|
||
{
|
||
$relate = new stdclass();
|
||
$relate->product = 0;
|
||
$relate->AType = $objectType;
|
||
$relate->AID = $objectID;
|
||
$relate->BType = $branch;
|
||
$relate->BID = $repoID;
|
||
$relate->relation = $relation;
|
||
$this->dao->replace(TABLE_RELATION)->data($relate)->exec();
|
||
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 获取对象关联的代码分支。
|
||
* Get linked branch of object.
|
||
*
|
||
* @param int $objectID
|
||
* @param string $objectType
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getLinkedBranch(int $objectID = 0, string $objectType = '', int $repoID = 0): array
|
||
{
|
||
return $this->dao->select('BID,BType,AType')->from(TABLE_RELATION)
|
||
->where('relation')->eq('linkrepobranch')
|
||
->beginIF($objectType)->andWhere('AType')->eq($objectType)->fi()
|
||
->beginIF($repoID)->andWhere('BID')->eq($repoID)->fi()
|
||
->beginIF($objectID)->andWhere('AID')->eq($objectID)->fi()
|
||
->fetchAll();
|
||
}
|
||
|
||
/**
|
||
* 移除对象关联的代码分支。
|
||
* Get linked branch of object.
|
||
*
|
||
* @param int $objectID
|
||
* @param string $objectType
|
||
* @param int $repoID
|
||
* @param string $branch
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function unlinkObjectBranch(int $objectID, string $objectType, int $repoID, string $branch): bool
|
||
{
|
||
$this->dao->delete()->from(TABLE_RELATION)
|
||
->where('AType')->eq($objectType)
|
||
->andWhere('relation')->eq('linkrepobranch')
|
||
->andWhere('AID')->eq($objectID)
|
||
->andWhere('BID')->eq($repoID)
|
||
->andWhere('BType')->eq($branch)
|
||
->exec();
|
||
return !dao::isError();
|
||
}
|
||
|
||
/**
|
||
* 通过产品ID和代码库类型获取代码库列表。
|
||
* Get repo list by product id.
|
||
*
|
||
* @param int $productID
|
||
* @param string $scm
|
||
* @param int $limit
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getListByProduct(int $productID, string $scm = '', int $limit = 0): array
|
||
{
|
||
return $this->dao->select('*')->from(TABLE_REPO)
|
||
->where('deleted')->eq('0')
|
||
->andWhere("FIND_IN_SET({$productID}, `product`)")
|
||
->beginIF($scm)->andWhere('SCM')->in($scm)->fi()
|
||
->beginIF($limit)->limit($limit)->fi()
|
||
->fetchAll('id');
|
||
}
|
||
|
||
/**
|
||
* 获取代码库服务器已经导入的项目/代码库。
|
||
* Get the imported projects/repositories by service host id.
|
||
*
|
||
* @param int $hostID
|
||
* @return array
|
||
*/
|
||
public function getImportedProjects(int $hostID)
|
||
{
|
||
$importedProjects = $this->dao->select('serviceProject')->from(TABLE_REPO)
|
||
->where('serviceHost')->eq($hostID)
|
||
->andWhere('deleted')->eq('0')
|
||
->fetchAll('serviceProject');
|
||
|
||
if(dao::isError()) return array();
|
||
|
||
return array_keys($importedProjects);
|
||
}
|
||
|
||
/**
|
||
* 隐藏DevOps菜单,执行和项目模块使用。
|
||
* Hide DevOps menu.
|
||
*
|
||
* @param int $objectID
|
||
* @access public
|
||
* @return int
|
||
*/
|
||
public function setHideMenu(int $objectID): int
|
||
{
|
||
$menuGroup = $this->app->tab == 'project' ? array('project', 'waterfall') : array('execution');
|
||
$repoPairs = $this->loadModel('repo')->getRepoPairs($this->app->tab, $objectID);
|
||
|
||
$showMR = false;
|
||
$showTag = false;
|
||
$showBranch = false;
|
||
$showCommit = false;
|
||
$hasTagSCM = array_map('strtolower', $this->config->repo->notSyncSCM);
|
||
foreach($repoPairs as $repoID => $repoName)
|
||
{
|
||
preg_match('/^\[(\w+)\]/', $repoName, $matches);
|
||
|
||
$result = isset($matches[1]) ? $matches[1] : '';
|
||
if($repoID == $this->session->repoID && in_array($result, $hasTagSCM))
|
||
{
|
||
$showTag = true;
|
||
$showBranch = true;
|
||
}
|
||
if(in_array($result, $this->config->repo->gitServiceList)) $showMR = true;
|
||
}
|
||
|
||
$showMR = $showMR && common::hasPriv('mr', 'browse');
|
||
$showTag = $showTag && common::hasPriv('repo', 'browsetag');
|
||
$showBranch = $showBranch && common::hasPriv('repo', 'browsebranch');
|
||
$showReview = $repoPairs && common::hasPriv('repo', 'review');
|
||
$showCommit = $repoPairs && common::hasPriv('repo', 'log');
|
||
foreach($menuGroup as $module)
|
||
{
|
||
if(!isset($this->lang->{$module}->menu->devops['subMenu'])) continue;
|
||
|
||
if(!$showMR) unset($this->lang->{$module}->menu->devops['subMenu']->mr);
|
||
if(!$showTag) unset($this->lang->{$module}->menu->devops['subMenu']->tag);
|
||
if(!$showBranch) unset($this->lang->{$module}->menu->devops['subMenu']->branch);
|
||
if(!$showReview) unset($this->lang->{$module}->menu->devops['subMenu']->review);
|
||
if(!$showCommit) unset($this->lang->{$module}->menu->devops['subMenu']->commit);
|
||
if(count((array)$this->lang->{$module}->menu->devops['subMenu']) < 2) unset($this->lang->{$module}->menu->devops['subMenu']);
|
||
}
|
||
return $objectID;
|
||
}
|
||
|
||
/**
|
||
* Check repo name.
|
||
*
|
||
* @param string $name
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function checkName(string $name)
|
||
{
|
||
$pattern = "/^[a-z_]{1}[a-z0-9_\-\.]*$/i";
|
||
return preg_match($pattern, $name);
|
||
}
|
||
}
|