* @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($repo) { $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($repos, $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 * @param bool $lastSubmitTime * @param string $type * @param int $param */ public function getList($projectID = 0, $SCM = '', $orderBy = 'id_desc', $pager = null, $getCodePath = false, $lastSubmitTime = false, $type = '', $param = 0) { $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($scm, $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($repo, $isPipelineServer) { $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($repo) { $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 string $namespace * @access public * @return object|false */ public function createGitlabRepo($repo, $namespace) { $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($repos, $serviceHost, $scm) { $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($data, $repo, $isPipelineServer) { 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($repoID, $revision, $type = 'story', $from = 'repo') { $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($repoID) { $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($repoID, $revision, $objectType, $objectID) { $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($repoID = 0, $objectID = 0) { 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($type, $projectID = 0, $showScm = true) { 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($type, $projectID = 0, $scmList = 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($repoID) { 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($url) { 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($url = '') { 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($idList) { $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($repo, $printLabel = false, $source = 'scm') { 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($revisions) { 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 mixed $query * @access public * @return array */ public function getCommits($repo, $entry, $revision = 'HEAD', $type = 'dir', $pager = null, $begin = '', $end = '', $query = null) { 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($repoID, $checkCount = true) { $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($repoID, $limit = 0, $maxRevision = '', $minRevision = '') { $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($repoID, $revisions) { 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($revision, $commit) { if(empty($commit)) return substr($revision, 0, 10); return substr($revision, 0, 10) . ' (' . $commit . ') '; } /** * 获取缓存文件位置。 * Get cache file. * * @param int $repoID * @param string $path * @param string $revision * @access public * @return string */ public function getCacheFile($repoID, $path, $revision) { $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($repoID) { $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($repoID, $logs, $version, $branch = '') { $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($repoID, $commit, $version, $branch = '') { $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($repoID, $branch) { $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($repoID, $count) { $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($repo) { $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($method, $params = '', $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($repoID) { $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($repoID) { $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($path = '') { if(empty($path)) return $path; return helper::safe64Encode(urlencode($path)); } /** * 解析代码库文件路径。 * Decode repo path. * * @param string $path * @access public * @return string */ public function decodePath($path = '') { 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($comment) { $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($matches, $method) { 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($comment) { $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($comment, $encodings) { /* 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() { 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($objects, $log, $repoRoot = '', $encodings = 'utf-8', $scm = 'svn', $gitlabAccountPairs = array()) { $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 . "
" . 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($action, $changes) { /* 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 * @param string $scm */ public function createActionChanges($log, $repoRoot, $scm = 'svn') { 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($tasks) { $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 * @param string $scm */ public function buildURL($methodName, $url, $revision, $scm = 'svn') { $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($repo, $getCodePath = false) { $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 * @param object $repo */ public function handleWebhook($event, $data, $repo) { 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($product, $branch = 0) { $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($repo) { 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($repo, $branch, $parent = '') { /* 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($repo, $branch = '', $diffs = null) { 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($repoID, $commit, $type = '') { $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($objectID, $objectType) { 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 */ /** * @param mixed[] $productIDList * @param mixed[] $projectIDList */ public function filterProject($productIDList, $projectIDList = 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($repoID, $objectID = 0, $branchID = '') { $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($repoID, $latestBranches) { 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($repo, $action) { $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($gitlabID, $projectFilter = '') { 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($serverID, $groupID = 0) { $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($gitlabID) { $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($giteaID) { $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($repoID) { $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($scm, $name, $serviceHost, $serviceProject) { 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($repo, $path = '', $branch = '') { $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($repo, $path = '', $branch = '', $type = 'blobs') { 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($repoID, $revision, $withCommit = false, $condition = 'eq') { $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($task, $params, $action, $changes) { $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($task, $params, $action, $changes) { $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($task, $taskActions, $action, $changes, $scm) { 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($taskID, $params, $action, $changes) { 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('
', "\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($bugs, $actions, $action, $changes) { 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($objects, $action, $changes) { $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($diffs) { 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($repoQuery, $SCM, $orderBy = 'id_desc', $pager = null) { 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 */ /** * @param int $repoID * @param string $branch * @param int $objectID * @param string $objectType * @param string $relation */ public function saveRelation($repoID, $branch, $objectID, $objectType, $relation = 'linkrepobranch') { $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 * @param int $repoID */ public function getLinkedBranch($objectID = 0, $objectType = '', $repoID = 0) { 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($objectID, $objectType, $repoID, $branch) { $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($productID, $scm = '', $limit = 0) { 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($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($objectID) { $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($name) { $pattern = "/^[a-z_]{1}[a-z0-9_\-\.]*$/i"; return preg_match($pattern, $name); } }