znt/module/mr/model.php

1335 lines
50 KiB
PHP

<?php
declare(strict_types=1);
/**
* The model file of mr 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 dingguodong <dingguodong@easycorp.ltd>
* @package mr
* @link https://www.zentao.net
* @property gitlabModel $gitlab
*/
class mrModel extends model
{
public $moduleName = 'mr';
public function __construct($appName = '')
{
parent::__construct($appName);
if($this->app->rawModule == 'pullreq') $this->moduleName = 'pullreq';
}
/**
* 获取合并请求列表.
* Get MR list of gitlab project.
*
* @param string $mode
* @param string $param
* @param string $orderBy
* @param array $filterProjects
* @param int $repoID
* @param int $objectID
* @param object $pager
* @access public
* @return array
*/
public function getList(string $mode = 'all', string $param = 'all', string $orderBy = 'id_desc', array $filterProjects = array(), int $repoID = 0, int $objectID = 0, ?object $pager = null): array
{
$filterProjectSql = '';
if(!$this->app->user->admin && !empty($filterProjects))
{
foreach($filterProjects as $hostID => $projectID)
{
$filterProjectSql .= "(hostID = {$hostID} AND sourceProject = '{$projectID}') OR ";
}
if($filterProjectSql) $filterProjectSql = '(' . substr($filterProjectSql, 0, -3) . ')'; // Remove last or.
}
if($this->app->tab == 'project')
{
$executionIdList = $this->loadModel('execution')->fetchExecutionList($objectID, 'all');
if(!empty($executionIdList)) $objectID = array_merge(array_keys($executionIdList), array($objectID));
}
return $this->dao->select('*')->from(TABLE_MR)
->where('deleted')->eq('0')
->beginIF($mode == 'status' && $param != 'all')->andWhere('status')->eq($param)->fi()
->beginIF($mode == 'assignee' && $param != 'all')->andWhere('assignee')->eq($param)->fi()
->beginIF($mode == 'creator' && $param != 'all')->andWhere('createdBy')->eq($param)->fi()
->beginIF($filterProjectSql)->andWhere($filterProjectSql)->fi()
->beginIF($repoID)->andWhere('repoID')->eq($repoID)->fi()
->beginIF($this->moduleName == 'mr')->andWhere('isFlow')->eq('0')->fi()
->beginIF($this->moduleName == 'pullreq')->andWhere('isFlow')->eq('1')->fi()
->beginIF($objectID && $this->moduleName == 'mr')->andWhere('executionID')->in($objectID)->fi()
->orderBy($orderBy)
->page($pager)
->fetchAll('id');
}
/**
* 根据代码库ID获取合并请求列表.
* Get MR list by repoID.
*
* @access public
* @return array
*/
public function getPairs(int $repoID): array
{
return $this->dao->select('id,title')
->from(TABLE_MR)
->where('deleted')->eq('0')
->andWhere('repoID')->eq($repoID)
->orderBy('id')
->fetchPairs('id', 'title');
}
/**
* 获取Gitea服务器的项目.
* Get gitea projects.
*
* @param int $hostID
* @access public
* @return array
*/
public function getGiteaProjects(int $hostID = 0): array
{
$projects = $this->loadModel('gitea')->apiGetProjects($hostID);
return array($hostID => helper::arrayColumn($projects, null, 'full_name'));
}
/**
* 获取Gogs服务器的项目.
* Get gogs projects.
*
* @param int $hostID
* @access public
* @return array
*/
public function getGogsProjects(int $hostID = 0): array
{
$projects = $this->loadModel('gogs')->apiGetProjects($hostID);
return array($hostID => helper::arrayColumn($projects, null, 'full_name'));
}
/**
* 获取Gitlab服务器的项目.
* Get gitlab projects.
*
* @param int $hostID
* @param array $projectIdList
* @access public
* @return array
*/
public function getGitlabProjects(int $hostID = 0, array $projectIdList = array()): array
{
if(!$this->app->user->admin)
{
$gitlabUsers = $this->loadModel('pipeline')->getProviderPairsByAccount('gitlab');
if(!isset($gitlabUsers[$hostID])) return array();
}
$minProject = $maxProject = 0;
/* Mysql string to int. */
$MR = $this->dao->select('min(CAST(sourceProject AS DECIMAL)) as minSource, MAX(CAST(sourceProject AS DECIMAL)) as maxSource,MIN(CAST(targetProject AS DECIMAL)) as minTarget,MAX(CAST(targetProject AS DECIMAL)) as maxTarget')->from(TABLE_MR)
->where('deleted')->eq('0')
->andWhere('hostID')->eq($hostID)
->beginIF($projectIdList)->andWhere('sourceProject', true)->in($projectIdList)
->orWhere('targetProject')->in($projectIdList)
->markRight(1)
->fi()
->fetch();
if(empty($MR->minSource) && empty($MR->minTarget)) return array();
$minProject = min($MR->minSource, $MR->minTarget);
$maxProject = max($MR->maxSource, $MR->maxTarget);
/* If not an administrator, need to obtain group member information. */
$groupIDList = array(0 => 0);
$this->loadModel('gitlab');
if(!$this->app->user->admin)
{
$groups = $this->gitlab->apiGetGroups($hostID, 'name_asc', 'reporter');
foreach($groups as $group) $groupIDList[] = $group->id;
}
$allProjectPairs = array();
$allProjects = $this->gitlab->apiGetProjects($hostID, 'false', (int)$minProject, (int)$maxProject);
foreach($allProjects as $project)
{
if($projectIdList && !in_array($project->id, $projectIdList)) continue;
if(!$this->gitlab->checkUserAccess($hostID, 0, $project, $groupIDList, 'reporter')) continue;
$project->isDeveloper = $this->gitlab->checkUserAccess($hostID, 0, $project, $groupIDList, 'developer');
$allProjectPairs[$hostID][$project->id] = $project;
}
return $allProjectPairs;
}
/**
* 创建合并请求。
* Create MR function.
*
* @param object $MR
* @access public
* @return array
*/
public function create(object $MR): array
{
$result = $this->checkSameOpened($MR->hostID, $MR->sourceProject, $MR->sourceBranch, $MR->targetProject, $MR->targetBranch);
if($result['result'] == 'fail') return $result;
$MRID = $this->insertMr($MR);
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
$this->loadModel('action')->create($this->moduleName, $MRID, 'opened');
if($MR->needCI && $MR->jobID) $this->execJob($MRID, (int)$MR->jobID);
$rawMR = $this->apiCreateMR($MR->hostID, $MR);
/**
* Another open merge request already exists for this source branch.
* The type of variable `$rawMR->message` is array.
*/
if(isset($rawMR->message) and !isset($rawMR->iid))
{
$this->dao->delete()->from(TABLE_MR)->where('id')->eq($MRID)->exec();
$errorMessage = $this->convertApiError($rawMR->message);
return array('result' => 'fail', 'message' => sprintf($this->lang->mr->apiError->createMR, $errorMessage));
}
/* Create MR failed. */
if(!isset($rawMR->iid))
{
$this->dao->delete()->from(TABLE_MR)->where('id')->eq($MRID)->exec();
return array('result' => 'fail', 'message' => $this->lang->mr->createFailedFromAPI);
}
/* Create a todo item for this MR. */
if(empty($MR->jobID)) $this->apiCreateMRTodo($MR->hostID, $MR->targetProject, $rawMR->iid);
$newMR = new stdclass();
$newMR->mriid = $rawMR->iid;
$newMR->status = $rawMR->state == 'open' ? 'opened' : $rawMR->state;
$newMR->mergeStatus = $rawMR->merge_status;
$this->dao->update(TABLE_MR)->data($newMR)->where('id')->eq($MRID)->autoCheck()->exec();
/* Link stories,bugs and tasks. */
$MR->id = $MRID;
$MR->mriid = $newMR->mriid;
$this->linkObjects($MR);
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
$linkParams = $this->app->tab == 'execution' || $this->app->tab == 'project' ? "repoID=0&mode=status&param=opened&objectID={$MR->executionID}" : "repoID={$MR->repoID}";
return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => helper::createLink($this->moduleName, 'browse', $linkParams));
}
/**
* 通过API创建合并请求。
* Create MR function by api.
*
* @access public
* @return bool|int
*/
public function apiCreate(): int|false
{
$postData = fixer::input('post')->get();
$repo = $this->dao->findByID($postData->repoID)->from(TABLE_REPO)->fetch();
if(empty($repo))
{
dao::$errors[] = 'No matched gitlab.';
return false;
}
/* Process and insert mr data. */
$MR = new stdClass();
$MR->hostID = (int)$repo->serviceHost;
$MR->sourceProject = $repo->serviceProject;
$MR->sourceBranch = $postData->sourceBranch;
$MR->targetProject = $repo->serviceProject;
$MR->targetBranch = $postData->targetBranch;
$MR->diffs = $postData->diffs;
$MR->title = $this->lang->mr->common . ' ' . $postData->sourceBranch . $this->lang->mr->to . $postData->targetBranch ;
$MR->repoID = $repo->id;
$MR->jobID = $postData->jobID;
$MR->status = 'opened';
$MR->synced = '0';
$MR->needCI = '1';
$MR->hasNoConflict = $postData->mergeStatus ? '0' : '1';
$MR->mergeStatus = $postData->mergeStatus ? 'can_be_merged' : 'cannot_be_merged';
$MR->createdBy = $this->app->user->account;
$MR->createdDate = date('Y-m-d H:i:s');
if($MR->sourceProject == $MR->targetProject && $MR->sourceBranch == $MR->targetBranch)
{
dao::$errors[] = $this->lang->mr->errorLang[1];
return false;
}
$result = $this->checkSameOpened($MR->hostID, (string)$MR->sourceProject, $MR->sourceBranch, (string)$MR->targetProject, $MR->targetBranch);
if($result['result'] == 'fail')
{
dao::$errors[] = $result['message'];
return false;
}
$MRID = $this->insertMr($MR);
if(dao::isError()) return false;
$this->loadModel('action')->create($this->moduleName, $MRID, 'opened');
/* Exec Job */
if($MR->hasNoConflict == '0' && $MR->mergeStatus == 'can_be_merged' && $MR->jobID) $this->execJob($MRID, (int)$MR->jobID);
return $MRID;
}
/**
* 更新合并请求。
* Edit MR function.
*
* @param int $MRID
* @param object $MR
* @access public
* @return array
*/
public function update(int $MRID, object $MR): array
{
$oldMR = $this->fetchByID($MRID);
$diff = array_diff_assoc((array)$oldMR, (array)$MR);
if(!$oldMR) return array('result' => 'fail', 'message' => $this->lang->mr->notFound);
/* The source and target branches cannot be the same. */
if($oldMR->sourceBranch == $MR->targetBranch ) return array('result' => 'fail', 'message' => $this->lang->mr->errorLang[1]);
$this->dao->update(TABLE_MR)->data($MR)->checkIF($MR->needCI, 'jobID', 'notempty');
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
/* Exec Job */
$needExecJob = isset($diff['targetBranch']) || isset($diff['jobID']) ? true : false;
if($needExecJob && isset($MR->jobID) && $MR->jobID)
{
$this->execJob($MRID, (int)$MR->jobID);
if(dao::isError()) return array('result' => 'fail', 'message' => implode("\n", dao::getError()));
}
/* Known issue: `reviewer_ids` takes no effect. */
$rawMR = $this->apiUpdateMR($oldMR, $MR);
if(!isset($rawMR->id) and isset($rawMR->message))
{
$errorMessage = $this->convertApiError($rawMR->message);
return array('result' => 'fail', 'message' => $errorMessage);
}
/* Update MR in Zentao database. */
$this->dao->update(TABLE_MR)->data($MR, $this->config->mr->edit->skippedFields)
->where('id')->eq($MRID)
->batchCheck($this->config->mr->edit->requiredFields, 'notempty')
->autoCheck()
->exec();
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
$MR = $this->fetchByID($MRID);
$this->linkObjects($MR);
$actionID = $this->loadModel('action')->create($this->moduleName, $MRID, 'edited');
$changes = common::createChanges($oldMR, $MR);
if(!empty($changes))
{
foreach($changes as &$change) if($change['field'] == 'assignee') $change['field'] = 'reviewer';
$this->action->logHistory($actionID, $changes);
}
$this->createMRLinkedAction($MRID, 'edit' . $this->moduleName, $MR->editedDate);
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
if($this->session->{$this->app->tab}) $MR->executionID = $this->session->{$this->app->tab};
$linkParams = $this->app->tab == 'execution' || $this->app->tab == 'project' ? "repoID=0&mode=status&param=opened&objectID={$MR->executionID}" : "repoID={$MR->repoID}";
return array('result' => 'success', 'message' => $needExecJob ? $this->lang->mr->triggeredCI : $this->lang->saveSuccess, 'load' => helper::createLink($this->moduleName, 'browse', $linkParams));
}
/**
* 更新合并请求关联信息。
* Update MR linked info.
*
* @param int $MRID
* @param string $action createmr|editmr|removemr
* @param string $actionDate
* @access public
* @return bool
*/
public function createMRLinkedAction(int $MRID, string $action, string $actionDate = ''): bool
{
if(empty($actionDate)) $actionDate = helper::now();
$MRAction = $actionDate . '::' . $this->app->user->account . '::' . helper::createLink($this->moduleName, 'view', "mr={$MRID}");
$this->loadModel('action');
foreach(array('story', 'task', 'bug') as $objectType)
{
$linkedObjects = $this->mrTao->getLinkedObjectPairs($MRID, $objectType);
foreach($linkedObjects as $objectID) $this->action->create($objectType, $objectID, $action, '', $MRAction);
}
return !dao::isError();
}
/**
* 通过API同步合并请求。
* Sync MR from GitLab API to Zentao database.
*
* @param object $MR
* @access public
* @return object|false
*/
public function apiSyncMR(object $MR): object|false
{
$rawMR = $this->apiGetSingleMR($MR->repoID, $MR->mriid);
/* Sync MR in ZenTao database whatever status of MR in GitLab. */
if(isset($rawMR->iid))
{
$newMR = new stdclass();
$gitUsers = $this->loadModel('pipeline')->getUserBindedPairs($MR->hostID, $rawMR->gitService, 'openID,account');
foreach($this->config->mr->maps->sync as $syncField => $config)
{
$value = '';
list($field, $optionType, $options) = explode('|', $config);
if($optionType == 'field') $value = $rawMR->$field;
if($optionType == 'userPairs')
{
$gitUserID = '';
if(isset($rawMR->$field))
{
$values = $rawMR->$field;
if(isset($values[0])) $gitUserID = $values[0]->$options;
}
$value = zget($gitUsers, $gitUserID, '');
}
if($value) $newMR->$syncField = $value;
}
/* For compatibility with PHP 5.4 . */
$condition = (array)$newMR;
if(empty($condition)) return false;
/* Update compile status of current MR object */
if(isset($MR->needCI) && $MR->needCI == '1')
{
$compile = $this->loadModel('compile')->getByID($MR->id);
$newMR->compileStatus = $compile ? $compile->status : 'failed';
}
/* Update MR in Zentao database. */
$newMR->editedBy = $this->app->user->account;
$newMR->editedDate = helper::now();
$this->dao->update(TABLE_MR)->data($newMR)
->where('id')->eq($MR->id)
->exec();
}
return $this->fetchByID($MR->id);
}
/**
* 通过API批量同步合并请求。
* Batch Sync GitLab MR Database.
*
* @param array $MRList
* @access public
* @return array
*/
public function batchSyncMR(array $MRList): array
{
if(empty($MRList)) return array();
foreach($MRList as $key => $MR)
{
if($MR->status != 'opened') continue;
$MRList[$key] = $this->apiSyncMR($MR);
}
return $MRList;
}
/**
* 创建远程合并请求。
* Create MR by API.
*
* @param int $hostID
* @param object $MR
* @access public
* @return object|null
*/
public function apiCreateMR(int $hostID, object $MR): object|null
{
$repo = $this->loadModel('repo')->getByID($MR->repoID);
if(!$repo) return null;
$openID = $this->loadModel('pipeline')->getOpenIdByAccount($hostID, strtolower($repo->SCM), $this->app->user->account);
$assignee = '';
if($MR->assignee) $assignee = $this->pipeline->getOpenIdByAccount($hostID, strtolower($repo->SCM), $MR->assignee);
$scm = $this->app->loadClass('scm');
$scm->setEngine($repo);
$result = $scm->createMR($MR, $openID, $assignee);
if(!empty($result->message)) $result->message = $this->convertApiError($result->message);
return $result;
}
/**
* 通过API查看是否有相同的合并请求。
* Get same opened mr by api.
*
* @param int $hostID
* @param string $sourceProject
* @param string $sourceBranch
* @param string $targetProject
* @param string $targetBranch
* @access public
* @return object|null
*/
public function apiGetSameOpened(int $hostID, string $sourceProject, string $sourceBranch, string $targetProject, string $targetBranch): object|null
{
if(empty($hostID) || empty($sourceProject) || empty($sourceBranch) || empty($targetProject) || empty($targetBranch)) return null;
$url = sprintf($this->loadModel('gitlab')->getApiRoot((int)$hostID), "/projects/{$sourceProject}/merge_requests") . "&state=opened&source_branch={$sourceBranch}&target_branch={$targetBranch}";
$response = json_decode(commonModel::http($url));
if($response)
{
foreach($response as $MR)
{
if(empty($MR->source_project_id) || empty($MR->target_project_id)) return null;
if($MR->source_project_id == $sourceProject && $MR->target_project_id == $targetProject) return $MR;
}
}
return null;
}
/**
* 通过API获取单个合并请求。
* Get single MR by API.
*
* @param int $repoID
* @param int $MRID
* @access public
* @return object|null
*/
public function apiGetSingleMR(int $repoID, int $MRID): object|null
{
$repo = $this->loadModel('repo')->getByID($repoID);
if(!$repo) return null;
$scm = $this->app->loadClass('scm');
$scm->setEngine($repo);
$MR = $scm->getSingleMR($MRID);
if($MR)
{
if(!isset($MR->flow)) $MR->flow = 0;
$MR->gitService = strtolower($repo->SCM);
if($MR->state == 'open') $MR->state = 'opened';
}
return $MR;
}
/**
* 通过API获取合并请求的提交信息。
* Get MR commits by API.
*
* @param int $hostID
* @param string $projectID targetProject
* @param int $MRID
* @access public
* @return array|null
*/
public function apiGetMRCommits(int $hostID, string $projectID, int $MRID): array|null
{
$host = $this->loadModel('pipeline')->getByID($hostID);
$apiRoot = $this->loadModel($host->type)->getApiRoot($hostID);
if($host->type == 'gitlab')
{
$url = sprintf($apiRoot, "/projects/$projectID/merge_requests/$MRID/commits");
}
else
{
$url = sprintf($apiRoot, "/repos/$projectID/pulls/$MRID/commits");
}
return (array)json_decode(commonModel::http($url, null, array(), is_object($apiRoot) ? $apiRoot->header : array()));
}
/**
* 通过API更新合并请求。
* Update MR by API.
*
* @param object $oldMR
* @param object $MR
* @access public
* @return object|null
*/
public function apiUpdateMR(object $oldMR, object $MR): object|null
{
$host = $this->loadModel('pipeline')->getByID($oldMR->hostID);
if(!$host) return null;
if(!empty($MR->assignee)) $assignee = $this->pipeline->getOpenIdByAccount($host->id, $host->type, $MR->assignee);
$apiRoot = $this->loadModel($host->type)->getApiRoot($host->id);
$MRObject = new stdclass();
$MRObject->title = $MR->title;
if($host->type == 'gitlab')
{
if(isset($MR->targetBranch)) $MRObject->target_branch = zget($MR, 'targetBranch', $oldMR->targetBranch);
if(isset($MR->description)) $MRObject->description = $MR->description;
if(isset($MR->remove_source_branch)) $MRObject->remove_source_branch = $MR->removeSourceBranch == '1' ? true : false;
if(isset($MR->squash)) $MRObject->squash = $MR->squash == '1' ? 1 : 0;
if(!empty($assignee)) $MRObject->assignee_ids = $assignee;
$url = sprintf($apiRoot, "/projects/{$oldMR->sourceProject}/merge_requests/{$oldMR->mriid}");
return json_decode(commonModel::http($url, $MRObject, array(CURLOPT_CUSTOMREQUEST => 'PUT')));
}
else
{
if(isset($MR->targetBranch)) $MRObject->base = zget($MR, 'targetBranch', $oldMR->targetBranch);
if(isset($MR->description)) $MRObject->body = $MR->description;
if(!empty($assignee)) $MRObject->assignee = $assignee;
$url = sprintf($apiRoot, "/repos/{$oldMR->sourceProject}/pulls/{$oldMR->mriid}");
$mergeResult = json_decode(commonModel::http($url, $MRObject, array(), array(), 'json', 'PATCH'));
if(isset($mergeResult->number)) $mergeResult->iid = $host->type == 'gitea' ? $mergeResult->number : $mergeResult->id;
if(isset($mergeResult->mergeable))
{
if($mergeResult->mergeable) $mergeResult->merge_status = 'can_be_merged';
if(!$mergeResult->mergeable) $mergeResult->merge_status = 'cannot_be_merged';
}
if(isset($mergeResult->state) && $mergeResult->state == 'open') $mergeResult->state = 'opened';
if(isset($mergeResult->merged) && $mergeResult->merged) $mergeResult->state = 'merged';
return $mergeResult;
}
}
/**
* 通过API删除合并请求。
* Delete MR by API.
*
* @param int $hostID
* @param string $projectID
* @param int $MRID
* @access public
* @return object|null
*/
public function apiDeleteMR(int $hostID, string $projectID, int $MRID): object|null
{
$host = $this->loadModel('pipeline')->getByID($hostID);
if(!$host) return null;
if($host->type == 'gitlab')
{
$url = sprintf($this->loadModel('gitlab')->getApiRoot($hostID), "/projects/$projectID/merge_requests/$MRID");
return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'DELETE')));
}
$repoID = $this->dao->select('repoID')->from(TABLE_MR)
->where('hostID')->eq($hostID)
->andWhere('sourceProject')->eq($projectID)
->andWhere('mriid')->eq($MRID)
->andWhere('deleted')->eq('0')
->fetch('repoID');
$rowMR = $this->apiGetSingleMR((int)$repoID, $MRID);
if($rowMR && $rowMR->state == 'opened')
{
$apiRoot = $this->loadModel($host->type)->getApiRoot($hostID);
$header = is_string($apiRoot) ? array() : $apiRoot->header;
if(is_object($apiRoot)) $apiRoot = $apiRoot->url;
$api = "/repos/$projectID/pulls/$MRID";
return json_decode(commonModel::http(sprintf($apiRoot, $api), array('state' => 'closed'), array(), $header, 'json', 'PATCH'));
}
return null;
}
/**
* 通过API关闭合并请求。
* Close MR by API.
*
* @param int $hostID
* @param string $projectID
* @param int $MRID
* @access public
* @return object|null
*/
public function apiCloseMR(int $hostID, string $projectID, int $MRID): object|null
{
$host = $this->loadModel('pipeline')->getByID($hostID);
if(!$host) return null;
if($host->type == 'gitlab')
{
$url = sprintf($this->loadModel('gitlab')->getApiRoot($hostID), "/projects/$projectID/merge_requests/$MRID") . '&state_event=close';
return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'PUT')));
}
else
{
return $this->apiDeleteMR($hostID, $projectID, $MRID);
}
}
/**
* 通过API重新打开合并请求。
* Reopen MR by API.
*
* @param int $hostID
* @param string $projectID
* @param int $MRID
* @access public
* @return object|null
*/
public function apiReopenMR(int $hostID, string $projectID, int $MRID): object|null
{
$host = $this->loadModel('pipeline')->getByID($hostID);
if(!$host) return null;
$apiRoot = $this->loadModel($host->type)->getApiRoot($hostID);
if($host->type == 'gitlab')
{
$url = sprintf($apiRoot, "/projects/$projectID/merge_requests/$MRID") . '&state_event=reopen';
return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'PUT')));
}
else
{
$url = sprintf($apiRoot, "/repos/$projectID/pulls/$MRID");
return json_decode(commonModel::http($url, array('state' => 'open'), array(), array(), 'json', 'PATCH'));
}
}
/**
* 通过API接受合并请求。
* Accept MR by API.
*
* @param object $MR
* @access public
* @return object|null
*/
public function apiAcceptMR(object $MR): object|null
{
$host = $this->loadModel('pipeline')->getByID($MR->hostID);
if(!$host) return null;
$apiRoot = $this->loadModel($host->type)->getApiRoot($MR->hostID);
if($host->type == 'gitlab')
{
$approveUrl = sprintf($apiRoot, "/projects/$MR->targetProject/merge_requests/$MR->mriid/approved");
commonModel::http($approveUrl, null, array(CURLOPT_CUSTOMREQUEST => 'POST'));
$url = sprintf($apiRoot, "/projects/$MR->targetProject/merge_requests/$MR->mriid/merge");
return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'PUT')));
}
else
{
$url = sprintf($apiRoot, "/repos/$MR->targetProject/pulls/$MR->mriid/merge");
$data = array('Do' => $MR->squash == '1' ? 'squash' : 'merge');
if($MR->removeSourceBranch == '1') $data['delete_branch_after_merge'] = true;
}
$rowMR = json_decode(commonModel::http($url, $data, array(), array(), 'json', 'POST'));
if(!isset($rowMR->massage))
{
$rowMR = $this->apiGetSingleMR($MR->repoID, $MR->mriid);
if($MR->removeSourceBranch == '1' && $host->type == 'gogs') $this->loadModel($host->type)->apiDeleteBranch($MR->hostID, $MR->sourceProject, $MR->sourceBranch);
}
return $rowMR;
}
/**
* 获取合并请求的对比信息。
* Get MR diff versions by API.
*
* @param object $MR
* @access public
* @return array
*/
public function getDiffs(object $MR): array
{
if(!isset($MR->repoID)) return array();
$repo = $this->loadModel('repo')->getByID($MR->repoID);
if(!$repo) return array();
$host = $this->loadModel('pipeline')->getByID($MR->hostID);
$scm = $this->app->loadClass('scm');
$scm->setEngine($repo);
$lines = array();
if($host->type == 'gitlab')
{
$diffVersions = array();
if($MR->synced) $diffVersions = $this->apiGetDiffVersions($MR->hostID, $MR->targetProject, $MR->mriid);
foreach($diffVersions as $diffVersion)
{
$singleDiff = $this->apiGetSingleDiffVersion($MR->hostID, $MR->targetProject, $MR->mriid, $diffVersion->id);
if($singleDiff->state == 'empty') continue;
$diffs = $singleDiff->diffs;
foreach($diffs as $diff)
{
$lines[] = sprintf("diff --git a/%s b/%s", $diff->old_path, $diff->new_path);
$lines[] = sprintf("index %s ... %s %s ", $singleDiff->head_commit_sha, $singleDiff->base_commit_sha, $diff->b_mode);
$lines[] = sprintf("--a/%s", $diff->old_path);
$lines[] = sprintf("--b/%s", $diff->new_path);
$diffLines = explode("\n", $diff->diff);
foreach($diffLines as $diffLine) $lines[] = $diffLine;
}
}
}
else
{
$lines = $this->apiGetDiffs($MR->hostID, $MR->targetProject, $MR->mriid);
}
if(empty($MR->synced)) $lines = preg_replace('/^\s*$\n?\r?/m', '', $MR->diffs);
if(is_string($lines)) $lines = explode("\n", $lines);
return $scm->engine->parseDiff($lines);
}
/**
* 通过API创建合并请求待办。
* Create a todo item for merge request.
*
* @param int $hostID
* @param string $projectID
* @param int $MRID
* @access public
* @return object|null
*/
public function apiCreateMRTodo(int $hostID, string $projectID, int $MRID): object|null
{
$url = sprintf($this->loadModel('gitlab')->getApiRoot($hostID), "/projects/$projectID/merge_requests/$MRID/todo");
return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'POST')));
}
/**
* 通过API获取合并请求的对比版本信息。
* Get diff versions of MR from GitLab API.
*
* @param int $hostID
* @param string $projectID
* @param int $MRID
* @access public
* @return array|null
*/
public function apiGetDiffVersions(int $hostID, string $projectID, int $MRID): array|null
{
$url = sprintf($this->loadModel('gitlab')->getApiRoot($hostID), "/projects/$projectID/merge_requests/$MRID/versions");
return json_decode(commonModel::http($url));
}
/**
* 通过API获取合并请求的单个对比版本信息。
* Get a single diff version of MR from GitLab API.
*
* @param int $hostID
* @param string $projectID
* @param int $MRID
* @param int $versionID
* @access public
* @return object|null
*/
public function apiGetSingleDiffVersion(int $hostID, string $projectID, int $MRID, int $versionID): object|null
{
$url = sprintf($this->loadModel('gitlab')->getApiRoot($hostID), "/projects/$projectID/merge_requests/$MRID/versions/$versionID");
return json_decode(commonModel::http($url));
}
/**
* 通过Gitea API获取合并请求的对比信息。
* Get diff of MR from Gitea API.
*
* @param int $hostID
* @param string $projectID
* @param int $MRID
* @access public
* @return string
*/
public function apiGetDiffs(int $hostID, string $projectID, int $MRID): string
{
$host = $this->loadModel('pipeline')->getByID($hostID);
if(!$host || $host->type == 'gitlab') return '';
$url = sprintf($this->loadModel($host->type)->getApiRoot($hostID), "/repos/$projectID/pulls/$MRID.diff");
return commonModel::http($url);
}
/**
* 审核合并请求。
* Reject or Approve this MR.
*
* @param object $MR
* @param string $action approve|reject
* @param string $comment
* @return array
*/
public function approve(object $MR, string $action = 'approve', string $comment = ''): array
{
if(isset($MR->status) && $MR->status == 'opened')
{
$oldMR = clone $MR;
$rawApprovalStatus = zget($MR, 'approvalStatus', '');
if($action == 'reject' && $rawApprovalStatus != 'rejected') $MR->approvalStatus = 'rejected';
if($action == 'approve' && $rawApprovalStatus != 'approved') $MR->approvalStatus = 'approved';
if(isset($MR->approvalStatus) && $rawApprovalStatus != $MR->approvalStatus)
{
$changes = common::createChanges($oldMR, $MR);
unset($MR->editedDate);
$MR->approver = $this->app->user->account;
$this->dao->update(TABLE_MR)->data($MR)
->where('id')->eq($MR->id)
->exec();
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
/* Save approval history into db. */
$approval = new stdClass;
$approval->date = helper::now();
$approval->mrID = $MR->id;
$approval->account = $MR->approver;
$approval->action = $action;
$approval->comment = $comment;
$this->dao->insert(TABLE_MRAPPROVAL)->data($approval, $this->config->mrapproval->create->skippedFields)
->batchCheck($this->config->mrapproval->create->requiredFields, 'notempty')
->autoCheck()
->exec();
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
$actionID = $this->loadModel('action')->create($this->moduleName, $MR->id, $action);
$this->action->logHistory($actionID, $changes);
return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => true);
}
}
return array('result' => 'fail', 'message' => $this->lang->mr->repeatedOperation, 'load' => helper::createLink($this->moduleName, 'view', "mr={$MR->id}"));
}
/**
* 关闭合并请求。
* Close this MR.
*
* @param object $MR
* @access public
* @return array
*/
public function close(object $MR): array
{
if($MR->status == 'closed') return array('result' => 'fail', 'message' => $this->lang->mr->repeatedOperation);
$actionID = $this->loadModel('action')->create($this->moduleName, $MR->id, 'closed');
$rawMR = $this->apiCloseMR($MR->hostID, $MR->targetProject, $MR->mriid);
$changes = common::createChanges($MR, $rawMR);
$this->action->logHistory($actionID, $changes);
if(isset($rawMR->state) && $rawMR->state == 'closed') return array('result' => 'success', 'message' => $this->lang->mr->closeSuccess, 'load' => 'reload');
return array('result' => 'fail', 'message' => $this->lang->fail);
}
/**
* 重新打开合并请求。
* Reopen this MR.
*
* @param object $MR
* @access public
* @return array
*/
public function reopen(object $MR): array
{
if($MR->status == 'opened') return array('result' => 'fail', 'message' => $this->lang->mr->repeatedOperation);
$this->loadModel('action')->create($this->moduleName, $MR->id, 'reopen');
$rawMR = $this->apiReopenMR($MR->hostID, $MR->targetProject, $MR->mriid);
if(!empty($rawMR) && empty($rawMR->message)) return array('result' => 'success', 'message' => $this->lang->mr->reopenSuccess, 'load' => 'reload');
return array('result' => 'fail', 'message' => $this->lang->fail);
}
/**
* 获取合并请求关联的对象。
* Get mr link list.
*
* @param int $MRID
* @param string $type story|task|bug
* @param string $orderBy
* @param object $pager
* @access public
* @return array
*/
public function getLinkList(int $MRID, string $type, string $orderBy = 'id_desc', ?object $pager = null): array
{
if(!isset($this->config->objectTables[$type])) return array();
$orderBy = str_replace('name_', 'title_', $orderBy);
if($type == 'task') $orderBy = str_replace('title_', 'name_', $orderBy);
return $this->dao->select('t1.*')->from($this->config->objectTables[$type])->alias('t1')
->leftJoin(TABLE_RELATION)->alias('t2')->on('t1.id=t2.BID')
->where('t2.relation')->eq('interrated')
->andWhere('t2.AType')->eq($this->moduleName)
->andWhere('t2.AID')->eq($MRID)
->andWhere('t2.BType')->eq($type)
->andWhere('t1.deleted')->eq(0)
->orderBy($orderBy)
->page($pager)
->fetchAll('id');
}
/**
* 根据对象信息获取合并请求列表。
* Get linked MR pairs.
*
* @param int $objectID
* @param string $objectType
* @param string $module mr|pullreq
* @access public
* @return array
*/
public function getLinkedMRPairs(int $objectID, string $objectType = 'story', string $module = ''): array
{
if(!$module) $module = $this->moduleName;
return $this->dao->select("t2.id,t2.title,t2.status")->from(TABLE_RELATION)->alias('t1')
->leftJoin(TABLE_MR)->alias('t2')->on('t1.AID = t2.id')
->where('t1.AType')->eq($module)
->andWhere('t1.BType')->eq($objectType)
->andWhere('t1.BID')->eq($objectID)
->andWhere('t2.id')->ne(0)
->fetchAll('id');
}
/**
* 合并请求关联对象。
* Create an mr link.
*
* @param int $MRID
* @param string $type story|task|bug
* @param array $objects
* @access public
* @return bool
*/
public function link(int $MRID, string $type, array $objects): bool
{
if(!isset($this->config->objectTables[$type])) return false;
$MR = $this->fetchByID($MRID);
if(!$MR) return false;
/* Set link action text. */
$user = $this->loadModel('user')->getRealNameAndEmails($MR->createdBy);
$comment = $MR->createdDate . '::' . zget($user, 'realname', $this->app->user->realname) . '::' . helper::createLink($this->moduleName, 'view', "mr={$MR->id}");
$this->loadModel('action');
foreach($objects as $objectID)
{
$relation = new stdclass();
$relation->product = 0;
$relation->AType = $this->moduleName;
$relation->AID = $MRID;
$relation->relation = 'interrated';
$relation->BType = $type;
$relation->BID = $objectID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
$this->action->create($type, (int)$objectID, 'create' . $this->moduleName, '', $comment);
}
return !dao::isError();
}
/**
* 保存合并请求关联的对象。
* Save linked objects.
*
* @param object $MR
* @access public
* @return bool
*/
public function linkObjects(object $MR): bool
{
/* Get commits by MR. */
$commits = $this->apiGetMRCommits($MR->hostID, (string)$MR->targetProject, $MR->mriid);
if(empty($commits)) return true;
/* Init objects. */
$objectList = array('story' => array(), 'bug' => array(), 'task' => array());
$this->loadModel('repo');
foreach($commits as $commit)
{
if(empty($commit->message)) $commit->message = zget($commit, 'title', '');
$objects = $this->repo->parseComment($commit->message);
$objectList['story'] = array_merge($objectList['story'], $objects['stories']);
$objectList['bug'] = array_merge($objectList['bug'], $objects['bugs']);
$objectList['task'] = array_merge($objectList['task'], $objects['tasks']);
}
$users = $this->loadModel('user')->getPairs('noletter');
$MRCreateAction = $MR->createdDate . '::' . zget($users, $MR->createdBy) . '::' . $MR->id;
$product = $this->getMRProduct($MR);
$this->loadModel('action');
foreach($objectList as $type => $objectIDs)
{
$relation = new stdclass();
$relation->product = $product ? $product->id : 0;
$relation->AType = $this->moduleName;
$relation->AID = $MR->id;
$relation->relation = 'interrated';
$relation->BType = $type;
foreach($objectIDs as $objectID)
{
$relation->BID = $objectID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
$this->action->create($type, (int)$objectID, 'create' . $this->moduleName, '', $MRCreateAction);
}
}
return !dao::isError();
}
/**
* 解除合并请求关联的对象。
* Unlink an mr link.
*
* @param int $MRID
* @param string $type
* @param int $objectID
* @access public
* @return bool
*/
public function unlink(int $MRID, string $type, int $objectID): bool
{
if(!isset($this->config->objectTables[$type])) return false;
$this->dao->delete()->from(TABLE_RELATION)
->where('AType')->eq($this->moduleName)
->andWhere('AID')->eq($MRID)
->andWhere('relation')->eq('interrated')
->andWhere('BType')->eq($type)
->andWhere('BID')->eq($objectID)
->exec();
$this->loadModel('action')->create($type, $objectID, 'deletemr', '', helper::createLink($this->moduleName, 'view', "mr={$MRID}"));
return !dao::isError();
}
/**
* 获取合并请求的产品。
* Get mr product.
*
* @param object $MR
* @access public
* @return object|false
*/
public function getMRProduct(object $MR): object|false
{
$productID = $this->dao->select('product')->from(TABLE_REPO)->where('id')->eq($MR->repoID)->fetch('product');
if(!$productID) return false;
return $this->loadModel('product')->getById((int)$productID);
}
/**
* 获取合并请求的收件人和抄送人。
* Get toList and ccList.
*
* @param object $MR
* @access public
* @return array
*/
public function getToAndCcList(object $MR): array
{
return array($MR->createdBy, $MR->assignee);
}
/**
* 将合并的操作记录到链接
* Log merged action to links.
*
* @param object $MR
* @access public
* @return bool
*/
public function logMergedAction(object $MR): bool
{
$this->loadModel('action')->create($this->moduleName, $MR->id, 'merged' . $this->moduleName);
foreach(array('story', 'bug', 'task') as $type)
{
$objects = $this->getLinkList($MR->id, $type);
foreach($objects as $object)
{
$this->action->create($type, $object->id, 'merged' . $this->moduleName, '', helper::createLink($this->moduleName, 'view', "mr={$MR->id}"));
}
}
$this->dao->update(TABLE_MR)->data(array('status' => 'merged'))->where('id')->eq($MR->id)->exec();
return !dao::isError();
}
/**
* 检查是否有相同的未关闭合并请求。
* Check same opened mr for source branch.
*
* @param int $hostID
* @param string $sourceProject
* @param string $sourceBranch
* @param string $targetProject
* @param string $targetBranch
* @access public
* @return array
*/
public function checkSameOpened(int $hostID, string $sourceProject, string $sourceBranch, string $targetProject, string $targetBranch): array
{
if($sourceProject == $targetProject && $sourceBranch == $targetBranch) return array('result' => 'fail', 'message' => $this->lang->mr->errorLang[1]);
$dbOpenedID = $this->dao->select('id')->from(TABLE_MR)
->where('hostID')->eq($hostID)
->andWhere('sourceProject')->eq($sourceProject)
->andWhere('sourceBranch')->eq($sourceBranch)
->andWhere('targetProject')->eq($targetProject)
->andWhere('targetBranch')->eq($targetBranch)
->andWhere('status')->eq('opened')
->andWhere('deleted')->eq('0')
->fetch('id');
if(!empty($dbOpenedID)) return array('result' => 'fail', 'message' => sprintf($this->lang->mr->hasSameOpenedMR, $dbOpenedID));
$MR = $this->apiGetSameOpened($hostID, (string)$sourceProject, $sourceBranch, (string)$targetProject, $targetBranch);
if($MR) return array('result' => 'fail', 'message' => sprintf($this->lang->mr->errorLang[2], $MR->iid));
return array('result' => 'success');
}
/**
* 解析API错误信息。
* Convert API error.
*
* @param array $message
* @access public
* @return string
*/
public function convertApiError(array|string $message): string
{
if(is_array($message)) $message = $message[0];
if(!is_string($message)) return $message;
foreach($this->lang->mr->apiErrorMap as $key => $errorMsg)
{
if(strpos($errorMsg, '/') === 0)
{
$result = preg_match($errorMsg, $message, $matches);
if($result) $errorMessage = sprintf(zget($this->lang->mr->errorLang, $key), zget($matches, 1, $matches[0]));
}
elseif($message == $errorMsg)
{
$errorMessage = zget($this->lang->mr->errorLang, $key, $message);
}
if(isset($errorMessage)) break;
}
return isset($errorMessage) ? $errorMessage : $message;
}
/**
* 判断按钮是否可点击。
* Adjust the action clickable.
*
* @param object $MR
* @param string $action
* @access public
* @return bool
*/
public static function isClickable(object $MR, string $action): bool
{
if($action == 'edit' && !$MR->synced) return false;
if($action == 'edit') return $MR->canEdit != 'disabled';
if($action == 'delete') return $MR->canDelete != 'disabled';
return true;
}
/**
* 根据ID删除合并请求。
* Delete MR by ID.
*
* @param int $MRID
* @access public
* @return bool
*/
public function deleteByID(int $MRID): bool
{
$MR = $this->fetchByID($MRID);
if(!$MR) return false;
if($MR->synced)
{
$res = $this->apiDeleteMR($MR->hostID, $MR->targetProject, $MR->mriid);
if(isset($res->message)) dao::$errors[] = $this->convertApiError($res->message);
if(dao::isError()) return false;
}
$this->dao->delete()->from(TABLE_MR)->where('id')->eq($MRID)->exec();
$this->loadModel('action')->create($this->moduleName, $MRID, 'deleted', '', $MR->title);
$this->createMRLinkedAction($MRID, 'remove' . $this->moduleName);
return !dao::isError();
}
/**
* 执行合并请求流水线。
* Execute MR pipeline.
*
* @param int $MRID
* @param int $jobID
* @access public
* @return bool
*/
public function execJob(int $MRID, int $jobID): bool
{
if(empty($MRID) || empty($jobID)) return false;
$MR = $this->fetchByID($MRID);
if(!$MR) return false;
$compile = $this->loadModel('job')->exec($jobID, array('sourceBranch' => $MR->sourceBranch, 'targetBranch' => $MR->targetBranch), 'commit');
if(!$compile) return false;
$newMR = new stdclass();
$newMR->compileID = $compile->id;
$newMR->compileStatus = $compile->status;
if($newMR->compileStatus == 'failure') $newMR->status = 'closed';
if($newMR->compileStatus == 'create_fail') $newMR->status = 'closed';
$this->loadModel('repo')->saveRelation($MRID, 'mr', $compile->id, 'compile', 'mrjob');
$this->dao->update(TABLE_MR)->data($newMR)->where('id')->eq($MRID)->autoCheck()->exec();
return dao::isError();
}
/**
* 创建合并请求。
* Insert a merge request.
*
* @param object $MR
* @access public
* @return int|false
*/
public function insertMr(object $MR): int|false
{
$this->dao->insert(TABLE_MR)->data($MR, $this->config->mr->create->skippedFields)
->batchCheck($this->config->mr->create->requiredFields, 'notempty')
->checkIF(!empty($MR->needCI), 'jobID', 'notempty')
->exec();
if(dao::isError()) return false;
return $this->dao->lastInsertID();
}
}