* @package build * @version $Id: model.php 4970 2013-07-02 05:58:11Z wyd621@gmail.com $ * @link https://www.zentao.net */ ?> loadModel('tutorial')->getBuild(); $build = $this->dao->select('t1.*, t2.name as executionName, t3.name as productName, t3.type as productType') ->from(TABLE_BUILD)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution = t2.id') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product = t3.id') ->where('t1.id')->eq((int)$buildID) ->fetch(); if(!$build) return false; $build = $this->joinChildBuilds($build); $build = $this->loadModel('file')->replaceImgURL($build, 'desc'); $build->files = $this->file->getByObject('build', $buildID); if($setImgSize) $build->desc = $this->file->setImgSize($build->desc); $build->isChild = false; if($this->app->rawMethod == 'edit') { if($this->dao->select('id')->from(TABLE_BUILD)->where("FIND_IN_SET($buildID, `builds`)")->fetch()) $build->isChild = true; if(!$build->isChild && $this->dao->select('id')->from(TABLE_RELEASE)->where("FIND_IN_SET($buildID, `build`)")->fetch()) $build->isChild = true; } return $build; } /** * 通过版本ID列表获取版本信息。 * Get builds by id list. * * @param array $idList * @access public * @return array */ public function getByList(array $idList): array { return $this->dao->select('*')->from(TABLE_BUILD)->where('id')->in($idList)->fetchAll('id', false); } /** * 通过项目ID获取版本信息。 * Get builds of a project. * * @param int $projectID * @param string $type * @param string $param * @param string $orderBy * @param object $pager * @access public * @return array */ public function getProjectBuilds(int $projectID = 0, string $type = 'all', string $param = '', string $orderBy = 't1.date_desc,t1.id_desc', ?object $pager = null): array { $shadows = $this->dao->select('shadow')->from(TABLE_RELEASE)->where("FIND_IN_SET({$projectID}, project)")->fetchPairs('shadow', 'shadow'); $builds = $this->dao->select('t1.*, t2.name as productName') ->from(TABLE_BUILD)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') ->where('t1.deleted')->eq(0) ->andWhere('t1.project')->ne(0) ->beginIF(!empty($shadows))->andWhere('t1.id')->notIN($shadows)->fi() ->beginIF($projectID)->andWhere('t1.project')->eq((int)$projectID)->fi() ->beginIF($type == 'product' && $param)->andWhere('t1.product')->eq((int)$param)->fi() ->beginIF($type == 'bysearch')->andWhere($param)->fi() ->beginIF($type == 'review')->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); $executionIdList = array(); foreach($builds as $build) { $build->builds = $this->getByList(explode(',', $build->builds)); if(!empty($build->builds)) { foreach($build->builds as $child) { if(!isset($executionIdList[$child->execution])) $executionIdList[$child->execution] = $child->execution; } } if(!isset($executionIdList[$build->execution])) $executionIdList[$build->execution] = $build->execution; } $executions = $this->loadModel('execution')->getByIdList($executionIdList, 'all'); foreach($builds as $build) { $build->executionDeleted = $build->execution ? $executions[$build->execution]->deleted : 0; $build->executionName = $build->execution ? $executions[$build->execution]->name : ''; if(!empty($build->builds)) { foreach($build->builds as $child) $child->executionName = $child->execution ? $executions[$child->execution]->name : ''; } } return $builds; } /** * 根据搜索条件获取项目版本。 * Get builds of a project by search. * * @param int $projectID * @param int $queryID * @param string $orderBy * @param object $pager * @access public * @return array */ public function getProjectBuildsBySearch(int $projectID, int $queryID, string $orderBy = 't1.date_desc,t1.id_desc', ?object $pager = null): array { /* If there are saved query conditions, reset the session. */ if((int)$queryID) { $query = $this->loadModel('search')->getQuery($queryID); if($query) { $this->session->set('projectBuildQuery', $query->sql); $this->session->set('projectBuildForm', $query->form); } } if($this->session->projectBuildQuery == false) $this->session->set('projectBuildQuery', ' 1 = 1'); $buildQuery = $this->session->projectBuildQuery; /* Distinguish between repeated fields. */ $fields = array('id' => '`id`', 'name' => '`name`', 'product' => '`product`', 'desc' => '`desc`', 'project' => '`project`'); foreach($fields as $field) { if(strpos($buildQuery, $field) !== false) { $buildQuery = str_replace($field, "t1." . $field, $buildQuery); } } return $this->getProjectBuilds($projectID, 'bysearch', (string)$buildQuery, $orderBy, $pager); } /** * 根据执行ID获取版本信息。 * Get builds of a execution. * * @param int $executionID * @param string $type all|product|bysearch * @param string $param productID|buildQuery * @param string $orderBy * @param object $pager * @access public * @return array */ public function getExecutionBuilds(int $executionID, string $type = '', string $param = '', string $orderBy = 't1.date_desc,t1.id_desc', ?object $pager = null): array { if(common::isTutorialMode()) return $this->loadModel('tutorial')->getBuilds(); return $this->dao->select('t1.*, t2.name as executionName, t3.name as productName') ->from(TABLE_BUILD)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution = t2.id') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product = t3.id') ->where('t1.deleted')->eq(0) ->beginIF($executionID)->andWhere('t1.execution')->eq((int)$executionID)->fi() ->beginIF($type == 'product' && $param)->andWhere('t1.product')->eq((int)$param)->fi() ->beginIF($type == 'bysearch')->andWhere($param)->fi() ->beginIF($type == 'review')->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); } /** * 通过条件获取执行下版本列表。 * Get builds of a execution by search. * * @param int $executionID * @param int $queryID * @param object $pager * @access public * @return object[] */ public function getExecutionBuildsBySearch(int $executionID, int $queryID, ?object $pager = null): array { /* If there are saved query conditions, reset the session. */ if($queryID) { $query = $this->loadModel('search')->getQuery($queryID); if($query) { $this->session->set('executionBuildQuery', $query->sql); $this->session->set('executionBuildForm', $query->form); } } if($this->session->executionBuildQuery === false) $this->session->set('executionBuildQuery', ' 1 = 1'); $buildQuery = $this->session->executionBuildQuery; /* Distinguish between repeated fields. */ $fields = array('id' => '`id`', 'name' => '`name`', 'product' => '`product`', 'desc' => '`desc`'); foreach($fields as $field) { if(strpos($this->session->executionBuildQuery, $field) !== false) { $buildQuery = str_replace($field, "t1." . $field, $buildQuery); } } return $this->getExecutionBuilds($executionID, 'bysearch', (string)$buildQuery, 't1.date_desc,t1.id_desc', $pager); } /** * 根据需求ID获取版本列表。 * Get builds by story ID. * * @param int $storyID * @access public * @return array */ public function getStoryBuilds(int $storyID): array { if(empty($storyID)) return array(); return $this->dao->select('*')->from(TABLE_BUILD) ->where('deleted')->eq(0) ->andWhere("CONCAT(',', stories, ',')")->like("%,$storyID,%") ->orderBy('id_desc') ->fetchAll('id'); } /** * 通过条件获取版本id:name的键值对。 * Get build pairs by condition. * * @param array|int $productIdList * @param string|int $branch * @param string $params noempty|notrunk|noterminate|withbranch|hasproject|noDeleted|singled|noreleased|releasedtag, can be a set of them * @param int $objectID * @param string $objectType * @param string $buildIdList * @param bool $replace * @access public * @return array */ public function getBuildPairs(array|int $productIdList, string|int $branch = 'all', string $params = 'noterminate, nodone', int $objectID = 0, string $objectType = 'execution', string|int $buildIdList = '', bool $replace = true, int $system = 0): array { if(common::isTutorialMode()) return $this->loadModel('tutorial')->getBuildPairs(); $sysBuilds = array(); if(strpos($params, 'notrunk') === false) $sysBuilds = array('trunk' => $this->lang->trunk); if(is_string($buildIdList)) $buildIdList = str_replace('trunk', '0', $buildIdList); $shadows = array(); if(!empty($productIdList)) $shadows = $this->dao->select('shadow')->from(TABLE_RELEASE)->where('product')->in($productIdList)->fetchPairs('shadow', 'shadow'); // Get the buildID under the shadow product. if($objectType == 'project') $shadows = $this->dao->select('shadow')->from(TABLE_RELEASE)->where("FIND_IN_SET({$objectID}, project)")->fetchPairs('shadow', 'shadow'); $selectedBuilds = $this->buildTao->selectedBuildPairs($buildIdList, $productIdList, $params, $objectID, $objectType); $allBuilds = $this->buildTao->fetchBuilds($productIdList, $params, $objectID, $objectType, $shadows, $system); /* Set builds and filter done executions and terminate releases. */ list($builds, $excludedReleaseIdList) = $this->setBuildDateGroup($allBuilds, $branch, $params); if(empty($builds) && empty($shadows)) return $sysBuilds + $selectedBuilds; /* if the build has been released and replace is true, replace build name with release name. */ if($replace) { $releases = $this->getRelatedReleases($productIdList, $buildIdList, $shadows, $objectType, $objectID, $params); $builds = $this->replaceNameWithRelease($allBuilds, $builds, $releases, $branch, $params, $excludedReleaseIdList); } krsort($builds); $buildPairs = array(); foreach($builds as $childBuilds) $buildPairs += $childBuilds; return $sysBuilds + $buildPairs + $selectedBuilds; } /** * 根据版本日期分组设置版本信息。 * Set build date group. * * @param array $allBuilds * @param string|int $branch * @param string $params nodone|noterminate|withexecution|withbranch * @access public * @return array */ public function setBuildDateGroup(array $allBuilds, string|int $branch, string $params): array { $this->app->loadLang('branch'); $deletedProjects = $this->dao->select('id, deleted')->from(TABLE_PROJECT)->where('type')->eq('project')->andWhere('deleted')->eq('1')->fetchPairs(); $deletedExecutions = $this->dao->select('id, deleted')->from(TABLE_EXECUTION)->where('type')->in('sprint,stage,kanban')->andWhere('deleted')->eq('1')->fetchPairs(); $branchPairs = $this->dao->select('id,name')->from(TABLE_BRANCH)->fetchPairs(); $builds = array(); $excludedReleaseIdList = array(); foreach($allBuilds as $id => $build) { if($build->branch === '') $build->branch = 0; $isDeleted = false; if(strpos($params, 'withexecution') !== false && $build->execution && isset($deletedExecutions[$build->execution])) $isDeleted = true; if($build->project && !$build->execution && isset($deletedProjects[$build->project])) $isDeleted = true; $isDone = empty($build->releaseID) && strpos($params, 'nodone') !== false && !empty($build->objectStatus) && $build->objectStatus === 'done'; $isTerminate = strpos($params, 'noterminate') !== false && $build->releaseStatus === 'terminate'; $isNotInBranch = $branch !== '' && $branch !== 'all' && strpos(",{$build->branch},", ",{$branch},") === false; if(in_array(true, array($isDone, $isTerminate, $isDeleted, $isNotInBranch))) { $excludedReleaseIdList[] = $build->releaseID; continue; } if($build->deleted == 1) $build->name .= ' (' . $this->lang->build->deleted . ')'; if(!empty($build->branch)) { $branchName = ''; foreach(explode(',', $build->branch) as $buildBranch) { if(empty($buildBranch)) { $branchName .= $this->lang->branch->main; } else { $branchName .= isset($branchPairs[$buildBranch]) ? $branchPairs[$buildBranch] : ''; } $branchName .= ','; } $branchName = trim($branchName, ','); } else { $branchName = $this->lang->branch->main; } $buildName = $build->name; if(strpos($params, 'withbranch') !== false && $build->productType != 'normal') $buildName = $branchName . '/' . $buildName; $builds[$build->date][$id] = $buildName; } return array($builds, $excludedReleaseIdList); } /** * 将版本名称替换为发布名称。 * Replace the build name with release name. * * @param array $allBuilds * @param array $builds * @param array $releases * @param string|int $branch * @param string $params separate|noterminate|withbranch|releasetag|noreleased * @param array $excludedReleaseIdList * @access public * @return array */ public function replaceNameWithRelease(array $allBuilds, array $builds, array $releases, string|int $branch, string $params, array $excludedReleaseIdList): array { $this->app->loadLang('branch'); $branches = strpos($params, 'separate') === false ? "0,$branch" : $branch; foreach($releases as $release) { if(strpos($params, 'noterminate') !== false && in_array($release->id, $excludedReleaseIdList)) continue; if($branch !== 'all') { $inBranch = false; foreach(explode(',', trim($release->branch, ',')) as $branchID) { if($branchID === '') continue; if(strpos(",{$branches},", ",{$branchID},") !== false) $inBranch = true; } if(!$inBranch) continue; } /* Set release name based on the condition. */ $releaseName = $release->name; $branchName = $release->branchName ? $release->branchName : $this->lang->branch->main; if($release->productType != 'normal') $releaseName = (strpos($params, 'withbranch') !== false ? $branchName . '/' : '') . $releaseName; if(strpos($params, 'releasetag') !== false) $releaseName = $releaseName . " [{$this->lang->build->released}]"; $builds[$release->date][$release->shadow] = $releaseName; if(strpos($params, 'noreplace') === false) // 勾选所有版本时,展示所有发布,不替换版本名称。 { foreach(explode(',', trim($release->build, ',')) as $buildID) { if(!isset($allBuilds[$buildID])) continue; $build = $allBuilds[$buildID]; if(strpos($params, 'noreleased') !== false) unset($builds[$build->date][$buildID]); } } } return $builds; } /** * 获取关联的发布。 * Get releated release. * * @param array|int $productIdList * @param string|int $buildIdList * @param array|bool $shadows * @param string $objectType * @param int $objectID * @param string $params * @access public * @return array */ public function getRelatedReleases(array|int $productIdList, string|int $buildIdList = '', array|bool $shadows = false, string $objectType = '', int $objectID = 0, string $params = ''): array { $releases = $this->dao->select('DISTINCT t1.id,t1.shadow,t1.product,t1.branch,t1.build,t1.name,t1.date,t1.status,t3.name as branchName,t4.type as productType')->from(TABLE_RELEASE)->alias('t1') ->leftJoin(TABLE_RELEASERELATED)->alias('t5')->on("t1.id=t5.release AND t5.objectType='build'") ->leftJoin(TABLE_BUILD)->alias('t2')->on('t5.objectID=t2.id') ->leftJoin(TABLE_RELEASERELATED)->alias('t6')->on("t1.id=t6.release AND t6.objectType='branch'") ->leftJoin(TABLE_BRANCH)->alias('t3')->on('t6.objectID=t3.id') ->leftJoin(TABLE_PRODUCT)->alias('t4')->on('t1.product=t4.id') ->where('t1.product')->in($productIdList) ->beginIF($objectType === 'project' && $objectID)->andWhere("(FIND_IN_SET('$objectID', t1.project)")->orWhere('t1.project')->eq('0')->markRight(1)->fi() ->beginIF($objectType === 'execution' && $objectID)->andWhere('t2.execution')->eq($objectID)->fi() ->beginIF(strpos($params, 'nowaitrelease') !== false)->andWhere('t1.status')->ne('wait')->fi() ->beginIF(strpos($params, 'nofail') !== false)->andWhere('t1.status')->ne('fail')->fi() ->andWhere('((t1.deleted')->eq(0) ->andWhere('t1.shadow')->ne(0) ->markRight(true) ->beginIF(!empty($buildIdList))->orWhere('t2.id')->in($buildIdList)->fi() ->markRight(true) ->fetchAll('id'); if($shadows === false) $shadows = $this->dao->select('shadow')->from(TABLE_RELEASE)->where('product')->in($productIdList)->fetchPairs('shadow', 'shadow'); // Get the buildID under the shadow product. if($shadows) { /* Append releases of only shadow and not link build. */ $releases += $this->dao->select('DISTINCT t1.id,t1.shadow,t1.product,t1.branch,t1.build,t1.name,t1.date,t2.name as branchName,t3.type as productType')->from(TABLE_RELEASE)->alias('t1') ->leftJoin(TABLE_RELEASERELATED)->alias('t4')->on("t1.id=t4.release AND t4.objectType='branch'") ->leftJoin(TABLE_BRANCH)->alias('t2')->on('t4.objectID=t2.id') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product=t3.id') ->where('t1.shadow')->in($shadows) ->beginIF($objectType === 'project' && $objectID)->andWhere("(FIND_IN_SET('$objectID', t1.project)")->orWhere('t1.project')->eq('0')->markRight(1)->fi() ->andWhere('t1.build')->eq(0) ->andWhere('t1.deleted')->eq(0) ->fetchAll('id'); } return $releases; } /** * 获取最后一次创建的版本信息。 * Get the last build. * * @param int $executionID * @param int $projectID * @access public * @return object|false */ public function getLast(int $executionID = 0, int $projectID = 0): object|false { return $this->dao->select('id, name')->from(TABLE_BUILD) ->where('deleted')->eq(0) ->andWhere('execution')->eq($executionID) ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi() ->orderBy('date DESC,id DESC') ->fetch(); } /** * 创建一个版本。 * Create a build. * * @param object $build * @access public * @return int|false */ public function create(object $build): int|false { /* Integrated version merging branch. */ if($this->post->isIntegrated == 'yes') { $build->execution = 0; $branchPairs = $this->dao->select('branch')->from(TABLE_BUILD)->where('id')->in($build->builds)->fetchPairs(); $relationBranch = array(); foreach($branchPairs as $branches) { foreach(explode(',', $branches) as $branch) { if(!isset($relationBranch[$branch])) $relationBranch[$branch] = $branch; } } if($relationBranch) $build->branch = implode(',', $relationBranch); $this->config->build->create->requiredFields = str_replace('execution,', '', $this->config->build->create->requiredFields); } else { $product = $this->loadModel('product')->getByID((int)$build->product); if(!empty($product) && $product->type != 'normal'&& $this->post->branch === false) { $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); dao::$errors['branch'] = sprintf($this->lang->error->notempty, $this->lang->product->branch); } } if(dao::isError()) return false; /* Process and insert build data. */ $requiredFields = $this->config->build->create->requiredFields; $project = $this->loadModel('project')->getByID((int)$build->project); if($project && !$project->hasProduct) $requiredFields = str_replace('product,', '', $requiredFields); $build = $this->loadModel('file')->processImgURL($build, $this->config->build->editor->create['id'], (string)$this->post->uid); $this->dao->insert(TABLE_BUILD)->data($build) ->autoCheck() ->batchCheck($requiredFields, 'notempty') ->check('name', 'unique', "product = {$build->product} AND branch = '{$build->branch}' AND deleted = '0'") ->checkFlow() ->exec(); if(dao::isError()) return false; /* Set file linkage and score info. */ $buildID = $this->dao->lastInsertID(); $this->file->updateObjectID($this->post->uid, $buildID, 'build'); $this->file->saveUpload('build', $buildID); $this->loadModel('score')->create('build', 'create', $buildID); $this->loadModel('action')->create('build', $buildID, 'opened'); return $buildID; } /** * 更新一个版本。 * Update a build. * * @param int $buildID * @param object $build * @access public * @return array|false */ public function update(int $buildID, object $build): array|false { $oldBuild = $this->fetchByID($buildID); $product = $this->loadModel('product')->getByID((int) $build->product); $branch = $this->post->branch === false || ($product && $product->type) == 'normal' ? 0 : $oldBuild->branch; if(empty($oldBuild->execution)) $build = $this->processBuildForUpdate($build, $oldBuild); $product = $this->loadModel('product')->getByID($build->product); if(!empty($product) && $product->type != 'normal' && !isset($_POST['branch']) && isset($_POST['product'])) { $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); dao::$errors['branch'] = sprintf($this->lang->error->notempty, $this->lang->product->branch); } if(dao::isError()) return false; $project = $this->loadModel('project')->getByID((int)$oldBuild->project); $requiredFields = $this->config->build->edit->requiredFields; if(!$project->hasProduct) $requiredFields = str_replace('product,', '', $requiredFields); $build = $this->loadModel('file')->processImgURL($build, $this->config->build->editor->edit['id'], (string)$this->post->uid); $this->dao->update(TABLE_BUILD)->data($build, 'deleteFiles,renameFiles,files') ->autoCheck() ->batchCheck($requiredFields, 'notempty') ->where('id')->eq($buildID) ->check('name', 'unique', "id != $buildID AND product = {$build->product} AND branch = '{$build->branch}' AND deleted = '0'") ->checkFlow() ->exec(); if(dao::isError()) return false; if(isset($build->branch) && $oldBuild->branch != $build->branch) $this->dao->update(TABLE_RELEASE)->set('branch')->eq($build->branch)->where('build')->eq($buildID)->exec(); if(dao::isError()) return false; $oldBuild->files = $this->file->getByObject('build', $buildID); $this->file->processFileDiffsForObject('build', $oldBuild, $build); return common::createChanges($oldBuild, $build); } /** * 更新关联Bug的解决原因。 * Update linked bug to resolved. * * @param object $build * @param array $bugIdList * @param array $resolvedByList * @access public * @return bool */ public function updateLinkedBug(object $build, array $bugIdList = array(), array $resolvedByList = array()): bool { $bugs = empty($bugIdList) ? array() : $this->dao->select('*')->from(TABLE_BUG)->where('id')->in($bugIdList)->fetchAll(); if(!$bugs) return false; $this->loadModel('action'); $now = helper::now(); foreach($bugs as $bug) { if($bug->status == 'resolved' || $bug->status == 'closed') continue; if(helper::isZeroDate($bug->activatedDate)) unset($bug->activatedDate); if(helper::isZeroDate($bug->closedDate)) unset($bug->closedDate); $bug->resolvedBy = zget($resolvedByList, $bug->id, ''); $bug->resolvedDate = $now; $bug->status = 'resolved'; $bug->confirmed = 1; $bug->assignedDate = $now; $bug->assignedTo = $bug->openedBy; $bug->lastEditedBy = $this->app->user->account; $bug->lastEditedDate = $now; $bug->resolution = 'fixed'; $bug->resolvedBuild = $build->id; $bug->deadline = !empty($bug->deadline) ? $bug->deadline : null; $this->dao->update(TABLE_BUG)->data($bug)->where('id')->eq($bug->id)->exec(); $this->action->create('bug', $bug->id, 'Resolved', '', 'fixed', $bug->resolvedBy); } return !dao::isError(); } /** * 版本关联需求。 * Link stories to a build. * * @param int $buildID * @param array $storyIdList * @access public * @return void */ public function linkStory(int $buildID, array $storyIdList): bool { if(empty($storyIdList)) return false; $build = $this->getByID($buildID); foreach($storyIdList as $i => $storyID) { if(strpos(",{$build->stories},", ",{$storyID},") !== false) unset($storyIdList[$i]); } $build->stories .= ',' . implode(',', $storyIdList); $this->dao->update(TABLE_BUILD)->set('stories')->eq($build->stories)->where('id')->eq((int)$buildID)->exec(); $this->loadModel('action'); foreach($storyIdList as $storyID) $this->action->create('story', (int)$storyID, 'linked2build', '', $buildID); $this->action->create('build', $buildID, 'linkstory', '', implode(',', $storyIdList)); return !dao::isError(); } /** * 解除需求关联。 * Unlink story. * * @param int $buildID * @param int $storyID * @access public * @return bool */ public function unlinkStory(int $buildID, int $storyID): bool { $build = $this->getByID($buildID); $build->stories = trim(str_replace(",$storyID,", ',', ",$build->stories,"), ','); if($build->stories) $build->stories = ',' . $build->stories; $this->dao->update(TABLE_BUILD)->set('stories')->eq($build->stories)->where('id')->eq($buildID)->exec(); $this->loadModel('action')->create('story', $storyID, 'unlinkedfrombuild', '', $buildID, '', false); return !dao::isError(); } /** * 批量解除需求关联。 * Batch unlink story. * * @param int $buildID * @param array $storyIDList * @access public * @return bool */ public function batchUnlinkStory(int $buildID, array $storyIDList): bool { if(empty($storyIDList)) return true; $build = $this->getByID($buildID); $build->stories = ",$build->stories,"; foreach($storyIDList as $storyID) $build->stories = str_replace(",$storyID,", ',', $build->stories); $build->stories = trim($build->stories, ','); $this->dao->update(TABLE_BUILD)->set('stories')->eq($build->stories)->where('id')->eq((int)$buildID)->exec(); $this->loadModel('action'); foreach($storyIDList as $storyID) $this->action->create('story', (int)$storyID, 'unlinkedfrombuild', '', $buildID); return !dao::isError(); } /** * 版本关联Bug。 * Link bugs. * * @param int $buildID * @param array $bugIdList * @param array $resolvedList * @access public * @return bool */ public function linkBug(int $buildID, array $bugIdList, array $resolvedList = array()): bool { $build = $this->getByID($buildID); if(!$build) return false; foreach($bugIdList as $i => $bugID) { if(strpos(",{$build->bugs},", ",{$bugID},") !== false) unset($bugIdList[$i]); } $build->bugs .= ',' . implode(',', $bugIdList); $this->updateLinkedBug($build, $bugIdList, $resolvedList); $this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq((int)$buildID)->exec(); $this->loadModel('action'); foreach($bugIdList as $bugID) $this->action->create('bug', (int)$bugID, 'linked2bug', '', $buildID); return !dao::isError(); } /** * 解除Bug跟版本的关联关系。 * Unlink bug. * * @param int $buildID * @param int $bugID * @access public * @return void */ public function unlinkBug(int $buildID, int $bugID): void { $build = $this->getByID($buildID); if(!$build) return; $build->bugs = trim(str_replace(",$bugID,", ',', ",$build->bugs,"), ','); if($build->bugs) $build->bugs = ',' . $build->bugs; $this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq($buildID)->exec(); $this->loadModel('action')->create('bug', $bugID, 'unlinkedfrombuild', '', $buildID, '', false); } /** * 批量解除Bug跟版本的关联关系。 * Batch unlink bug. * * @param int $buildID * @param array $bugIdList * @access public * @return bool */ public function batchUnlinkBug(int $buildID, array $bugIdList): bool { if(empty($bugIdList)) return true; $build = $this->getByID($buildID); if(!$build) return false; $build->bugs = ",$build->bugs,"; foreach($bugIdList as $bugID) $build->bugs = str_replace(",$bugID,", ',', $build->bugs); $build->bugs = trim($build->bugs, ','); $this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq((int)$buildID)->exec(); $this->loadModel('action'); foreach($bugIdList as $unlinkBugID) $this->action->create('bug', (int)$unlinkBugID, 'unlinkedfrombuild', '', $buildID); return !dao::isError(); } /** * 更新子版本关联的Bug。 * Bugs and stories associated with child builds. * * @param object $build * @access public * @return object */ public function joinChildBuilds(object $build): object { $build->allBugs = $build->bugs; $build->allStories = $build->stories; $childBuilds = $this->getByList(explode(',', $build->builds)); foreach($childBuilds as $childBuild) { if($childBuild->bugs) $build->allBugs .= ",{$childBuild->bugs}"; if($childBuild->stories) $build->allStories .= ",{$childBuild->stories}"; } $build->allBugs = explode(',', $build->allBugs); $build->allBugs = implode(',', array_unique(array_filter($build->allBugs))); $build->allStories = explode(',', $build->allStories); $build->allStories = implode(',', array_unique(array_filter($build->allStories))); return $build; } /** * 检查按钮是否可用。 * Adjust the action is clickable. * * @param object $buikd * @param string $action * @param string $module * @access public * @return bool */ public static function isClickable(object $build, string $action, string $module = 'bug'): bool { $action = strtolower($action); if($module == 'testtask' && $action == 'create') return !$build->executionDeleted; return true; } /** * 为区块获取版本数据。 * Get build's data for block. * * @param int $projectID * @param string $orderBy * @param int $limit * @access public * @return object[] */ public function getBuildBlockData(int $projectID = 0, string $orderBy = 'id_desc', int $limit = 10): array { return $this->dao->select('*')->from(TABLE_BUILD) ->where('deleted')->eq('0') ->beginIF(!$this->app->user->admin)->andWhere('execution')->in($this->app->user->view->sprints)->fi() ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi() ->orderBy($orderBy) ->limit($limit) ->fetchAll(); } /** * 处理版本编辑前没有执行的情况。 * Process build for update when the build has no execution. * * @param object $build * @param object $oldBuild * @access public * @return object */ public function processBuildForUpdate(object $build, object $oldBuild): object { if(!empty($oldBuild->execution)) return $build; $buildBranch = array(); foreach(explode(',', trim($build->branch, ',')) as $branchID) $buildBranch[$branchID] = $branchID; /* Get delete builds. */ $deleteBuilds = array(); $newBuilds = isset($build->builds) ? explode(',', $build->builds) : array(); foreach($newBuilds as $oldBuildID) { if(empty($oldBuildID)) continue; if(!in_array($oldBuildID, $newBuilds)) $deleteBuilds[$oldBuildID] = $oldBuildID; } /* Delete the branch when the branch of the deleted build has no linked stories. */ $storyBranches = $this->dao->select('branch')->from(TABLE_STORY)->where('id')->in($oldBuild->stories)->fetchPairs('branch'); $branches = $this->dao->select('branch')->from(TABLE_BUILD)->where('id')->in($newBuilds + $deleteBuilds)->fetchPairs(); foreach($branches as $branch) { foreach(explode(',', $branch) as $branchID) { if(empty($branchID)) continue; if(in_array($branchID, $deleteBuilds) && isset($storyBranches[$branchID])) continue; if(in_array($branchID, $newBuilds) && isset($buildBranch[$branchID])) continue; if(in_array($branchID, $deleteBuilds)) unset($buildBranch[$branchID]); if(in_array($branchID, $newBuilds)) $buildBranch[$branchID] = $branchID; } } $build->branch = implode(',', $buildBranch); return $build; } /** * 获取版本关联的bug列表。 * Get bug list of build. * * @param string $bugIdList * @param string $orderBy * @param object $pager * @access public * @return array */ public function getBugList(string $bugIdList, string $orderBy = '', ?object $pager = null): array { return $this->dao->select('*')->from(TABLE_BUG) ->where('id')->in($bugIdList) ->andWhere('deleted')->eq(0) ->beginIF($orderBy)->orderBy($orderBy)->fi() ->page($pager) ->fetchAll(); } /** * 获取版本关联的story列表。 * Get story list of build. * * @param string $storyIdList * @param string $orderBy * @param object $pager * @access public * @return array */ public function getStoryList(string $storyIdList, int $branch = 0, string $orderBy = '', ?object $pager = null): array { $stories = $this->dao->select("*, IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) as priOrder")->from(TABLE_STORY) ->where('id')->in($storyIdList) ->andWhere('deleted')->eq(0) ->beginIF($orderBy)->orderBy($orderBy)->fi() ->page($pager) ->fetchAll('id'); $stages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->in(array_keys($stories))->andWhere('branch')->eq($branch)->fetchPairs('story', 'stage'); foreach($stages as $storyID => $stage) $stories[$storyID]->stage = $stage; return $stories; } /** * 给版本中的发布增加标识。 * Add label for the release in the builds. * * @param int $productID * @param array $builds * @access protected * @return array */ public function addReleaseLabelForBuilds(int $productID, array $builds): array { $releases = $this->getRelatedReleases(array($productID)); $buildItems = array(); $buildsPinyin = common::convert2Pinyin($builds); foreach($builds as $buildID => $buildName) $buildItems[$buildID] = array('value' => $buildID, 'text' => $buildName, 'keys' => $buildID . $buildName . (!empty($buildsPinyin[$buildID]) ? $buildsPinyin[$buildID] : '')); foreach($releases as $release) { if(isset($buildItems[$release->shadow])) $buildItems[$release->shadow]['content'] = array('html' => "
{$buildItems[$release->shadow]['text']}
", 'class' => 'w-full flex nowrap'); } return array_values($buildItems); } }