diff --git a/.gitignore b/.gitignore index 786aa94c4a..dd804eae67 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ test/data/sql/ .gitkeep .bak .idea +.vscode .gitignore .DS_Store vendor/ diff --git a/api/v1/entries/bug.php b/api/v1/entries/bug.php index 2e79f9889a..26db63629b 100644 --- a/api/v1/entries/bug.php +++ b/api/v1/entries/bug.php @@ -20,6 +20,8 @@ class bugEntry extends entry */ public function get($bugID) { + $this->resetOpenApp($this->param('tab', 'product')); + $control = $this->loadController('bug', 'view'); $control->view($bugID); @@ -91,6 +93,11 @@ class bugEntry extends entry $bug->actions = $this->loadModel('action')->processActionForAPI($data->data->actions, $data->data->users, $this->lang->bug); + $preAndNext = $data->data->preAndNext; + $bug->preAndNext = array(); + $bug->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : ''; + $bug->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : ''; + $this->send(200, $this->format($bug, 'activatedDate:time,openedDate:time,assignedDate:time,resolvedDate:time,closedDate:time,lastEditedDate:time,deadline:date,deleted:bool')); } diff --git a/api/v1/entries/doc.php b/api/v1/entries/doc.php index 7be2c0d40b..be2309917a 100644 --- a/api/v1/entries/doc.php +++ b/api/v1/entries/doc.php @@ -20,6 +20,8 @@ class docEntry extends entry */ public function get($docID) { + $this->resetOpenApp($this->param('tab', 'doc')); + $control = $this->loadController('doc', 'view'); $control->view($docID); @@ -44,6 +46,11 @@ class docEntry extends entry $doc->addedBy = zget($usersWithAvatar, $doc->addedBy); } + $preAndNext = $data->data->preAndNext; + $doc->preAndNext = array(); + $doc->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : ''; + $doc->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : ''; + $this->send(200, $this->format($doc, 'addedDate:time,assignedDate:date,editedDate:time')); } diff --git a/api/v1/entries/gitlabWebhook.php b/api/v1/entries/gitlabWebhook.php new file mode 100644 index 0000000000..2a416234d8 --- /dev/null +++ b/api/v1/entries/gitlabWebhook.php @@ -0,0 +1,39 @@ + + * @package repo + * @version 1 + * @link http://www.zentao.net + */ +class gitlabWebhookEntry extends baseEntry +{ + + /** + * Repo webhook. + * + * @access public + * @return void + */ + public function post() + { + $repoID= $this->param('repoID'); + if(empty($id)) return; + + $this->loadModel('repo'); + + $repo = $this->repo->getRepoByID($repoID); + if(empty($repo)) return; + + $headers = getallheaders(); /* Fetch all HTTP request headers. */ + $event = isset($headers['X-Gitlab-Event']) ? $headers['X-Gitlab-Event'] : ''; + $token = isset($headers['X-Gitlab-Token']) ? $headers['X-Gitlab-Token'] : ''; + if(empty($event) || empty($token)) return; + + $this->repo->handleWebhook($event, $token, $this->requestBody, $repo); + } + +} diff --git a/api/v1/entries/productplan.php b/api/v1/entries/productplan.php index ab10d35d26..0ae5653eab 100644 --- a/api/v1/entries/productplan.php +++ b/api/v1/entries/productplan.php @@ -29,7 +29,11 @@ class productplanEntry extends Entry if(!$data or !isset($data->status)) return $this->send400('error'); if(isset($data->status) and $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message); - $plan = $this->format($data->data->plan, 'begin:date,end:date,deleted:bool'); + $plan = $data->data->plan; + $plan->stories = $data->data->planStories; + $plan->bugs = $data->data->planBugs; + + $plan = $this->format($plan, 'begin:date,end:date,deleted:bool,stories:array,bugs:array'); return $this->send(200, $plan); } diff --git a/api/v1/entries/productplanlinkbug.php b/api/v1/entries/productplanlinkbug.php new file mode 100644 index 0000000000..b553a092b4 --- /dev/null +++ b/api/v1/entries/productplanlinkbug.php @@ -0,0 +1,50 @@ + + * @package entries + * @version 1 + * @link http://www.zentao.net + */ +class productplanLinkBugEntry extends entry +{ + /** + * POST method. + * + * @param int $planID + * @access public + * @return void + */ + public function post($planID) + { + $fields = 'bugs'; + $this->batchSetPost($fields); + + $control = $this->loadController('productplan', 'linkBug'); + $control->linkBug($planID); + + $data = $this->getData(); + if(isset($data->result) and $data->result == 'success') + { + $control = $this->loadController('productplan', 'view'); + $control->view($planID); + + $data = $this->getData(); + if(!$data or !isset($data->status)) return $this->send400('error'); + if(isset($data->status) and $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message); + + $plan = $data->data->plan; + $plan->stories = $data->data->planStories; + $plan->bugs = $data->data->planBugs; + + $plan = $this->format($plan, 'begin:date,end:date,deleted:bool,stories:array,bugs:array'); + + return $this->send(200, $plan); + } + + $this->sendError(400, array('message' => isset($data->message) ? $data->message : 'error')); + } +} diff --git a/api/v1/entries/productplanlinkstory.php b/api/v1/entries/productplanlinkstory.php new file mode 100644 index 0000000000..c7956c3a4f --- /dev/null +++ b/api/v1/entries/productplanlinkstory.php @@ -0,0 +1,50 @@ + + * @package entries + * @version 1 + * @link http://www.zentao.net + */ +class productplanLinkStoryEntry extends entry +{ + /** + * POST method. + * + * @param int $planID + * @access public + * @return void + */ + public function post($planID) + { + $fields = 'stories'; + $this->batchSetPost($fields); + + $control = $this->loadController('productplan', 'linkStory'); + $control->linkStory($planID); + + $data = $this->getData(); + if(isset($data->result) and $data->result == 'success') + { + $control = $this->loadController('productplan', 'view'); + $control->view($planID); + + $data = $this->getData(); + if(!$data or !isset($data->status)) return $this->send400('error'); + if(isset($data->status) and $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message); + + $plan = $data->data->plan; + $plan->stories = $data->data->planStories; + $plan->bugs = $data->data->planBugs; + + $plan = $this->format($plan, 'begin:date,end:date,deleted:bool,stories:array,bugs:array'); + + return $this->send(200, $plan); + } + + $this->sendError(400, array('message' => isset($data->message) ? $data->message : 'error')); + } +} diff --git a/api/v1/entries/productplans.php b/api/v1/entries/productplans.php index e2f0296c32..5fe3ace5e0 100644 --- a/api/v1/entries/productplans.php +++ b/api/v1/entries/productplans.php @@ -57,4 +57,34 @@ class productplansEntry extends entry return $this->sendError(400, 'error'); } + + /** + * POST method. + * + * @param int $productID + * @access public + * @return void + */ + public function post($productID = 0) + { + if(!$productID) $productID = $this->param('product', 0); + if(!$productID) return $this->sendError(400, 'No product id.'); + + $fields = 'branch,begin,end,title,desc'; + $this->batchSetPost($fields); + + $control = $this->loadController('productplan', 'create'); + $control->create($productID, $this->param('branch', 0), $this->param('parent', 0)); + + $data = $this->getData(); + if(isset($data->result) and $data->result == 'success') + { + $plan = $this->loadModel('productplan')->getByID($data->id); + $plan->stories = array(); + $plan->bugs = array(); + return $this->send(200, $plan); + } + + $this->sendError(400, array('message' => isset($data->message) ? $data->message : 'error')); + } } diff --git a/api/v1/entries/productplanunlinkbug.php b/api/v1/entries/productplanunlinkbug.php new file mode 100644 index 0000000000..12b05f9982 --- /dev/null +++ b/api/v1/entries/productplanunlinkbug.php @@ -0,0 +1,45 @@ + + * @package entries + * @version 1 + * @link http://www.zentao.net + */ +class productplanUnlinkBugEntry extends entry +{ + /** + * POST method. + * + * @param int $planID + * @access public + * @return void + */ + public function post($planID) + { + $productplan = $this->loadModel('productplan'); + foreach($this->request('bugs', array()) as $bugID) + { + $productplan->unlinkBug($bugID, $planID); + if(dao::isError()) return $this->sendError('error'); + } + + $control = $this->loadController('productplan', 'view'); + $control->view($planID); + + $data = $this->getData(); + if(!$data or !isset($data->status)) return $this->send400('error'); + if(isset($data->status) and $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message); + + $plan = $data->data->plan; + $plan->stories = $data->data->planStories; + $plan->bugs = $data->data->planBugs; + + $plan = $this->format($plan, 'begin:date,end:date,deleted:bool,stories:array,bugs:array'); + + return $this->send(200, $plan); + } +} diff --git a/api/v1/entries/productplanunlinkstory.php b/api/v1/entries/productplanunlinkstory.php new file mode 100644 index 0000000000..94d24e7676 --- /dev/null +++ b/api/v1/entries/productplanunlinkstory.php @@ -0,0 +1,45 @@ + + * @package entries + * @version 1 + * @link http://www.zentao.net + */ +class productplanUnlinkStoryEntry extends entry +{ + /** + * POST method. + * + * @param int $planID + * @access public + * @return void + */ + public function post($planID) + { + $productplan = $this->loadModel('productplan'); + foreach($this->request('stories', array()) as $storyID) + { + $productplan->unlinkStory($storyID, $planID); + if(dao::isError()) return $this->sendError('error'); + } + + $control = $this->loadController('productplan', 'view'); + $control->view($planID); + + $data = $this->getData(); + if(!$data or !isset($data->status)) return $this->send400('error'); + if(isset($data->status) and $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message); + + $plan = $data->data->plan; + $plan->stories = $data->data->planStories; + $plan->bugs = $data->data->planBugs; + + $plan = $this->format($plan, 'begin:date,end:date,deleted:bool,stories:array,bugs:array'); + + return $this->send(200, $plan); + } +} diff --git a/api/v1/entries/projectcases.php b/api/v1/entries/projectcases.php index efdbde7ba0..e96f69196a 100644 --- a/api/v1/entries/projectcases.php +++ b/api/v1/entries/projectcases.php @@ -23,7 +23,7 @@ class projectCasesEntry extends entry if(!$projectID) $projectID = $this->param('project', 0); if(empty($projectID)) return $this->sendError(400, 'Need project id.'); - $this->app->tab = 'project'; + $this->resetOpenApp('project'); $this->app->session->set('project', $projectID, $this->app->tab); $control = $this->loadController('project', 'testcase'); diff --git a/api/v1/entries/story.php b/api/v1/entries/story.php index 87b51ba6aa..457d41bb1b 100644 --- a/api/v1/entries/story.php +++ b/api/v1/entries/story.php @@ -20,6 +20,8 @@ class storyEntry extends Entry */ public function get($storyID) { + $this->resetOpenApp($this->param('tab', 'product')); + $control = $this->loadController('story', 'view'); $control->view($storyID); @@ -105,6 +107,11 @@ class storyEntry extends Entry $story->actions = $this->loadModel('action')->processActionForAPI($data->data->actions, $data->data->users, $this->lang->story); + $preAndNext = $data->data->preAndNext; + $story->preAndNext = array(); + $story->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : ''; + $story->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : ''; + $this->send(200, $this->format($story, 'openedDate:time,assignedDate:time,reviewedDate:time,lastEditedDate:time,closedDate:time')); } diff --git a/api/v1/entries/task.php b/api/v1/entries/task.php index a900057719..a25b2787b3 100644 --- a/api/v1/entries/task.php +++ b/api/v1/entries/task.php @@ -20,6 +20,8 @@ class taskEntry extends Entry */ public function get($taskID) { + $this->resetOpenApp($this->param('tab', 'execution')); + $control = $this->loadController('task', 'view'); $control->view($taskID); @@ -99,6 +101,11 @@ class taskEntry extends Entry $task->actions = $this->loadModel('action')->processActionForAPI($data->data->actions, $data->data->users, $this->lang->task); + $preAndNext = $data->data->preAndNext; + $task->preAndNext = array(); + $task->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : ''; + $task->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : ''; + $this->send(200, $this->format($task, 'openedDate:time,assignedDate:time,realStarted:time,finishedDate:time,canceledDate:time,closedDate:time,lastEditedDate:time,deleted:bool')); } diff --git a/config/filter.php b/config/filter.php index d80724ca83..1b2e6f2ec5 100644 --- a/config/filter.php +++ b/config/filter.php @@ -151,6 +151,7 @@ $filter->search->index = new stdclass(); $filter->gitlab->webhook = new stdclass(); $filter->gitlab->importissue = new stdclass(); $filter->mr->diff = new stdclass(); +$filter->mr->browse = new stdclass(); $filter->ci->checkCompileStatus = new stdclass(); $filter->execution->export = new stdclass(); $filter->tree->browse = new stdclass(); @@ -389,6 +390,9 @@ $filter->gitlab->importissue->get['repo'] = 'int'; $filter->mr->diff->cookie['arrange'] = 'reg::word'; +$filter->mr->browse->get['mode'] = 'string'; +$filter->mr->browse->get['param'] = 'string'; + $filter->ci->checkCompileStatus->get['gitlabOnly'] = 'string'; $filter->tree->browse->cookie['preProductID'] = 'int'; diff --git a/config/routes.php b/config/routes.php index 547f0461a2..0141f6f578 100644 --- a/config/routes.php +++ b/config/routes.php @@ -24,9 +24,13 @@ $routes['/products/:id'] = 'product'; $routes['/productlines'] = 'productLines'; $routes['/productlines/:id'] = 'productLine'; -$routes['/productplans'] = 'productPlans'; -$routes['/products/:id/plans'] = 'productPlans'; -$routes['/productplans/:id'] = 'productPlan'; +$routes['/productplans'] = 'productPlans'; +$routes['/products/:id/plans'] = 'productPlans'; +$routes['/productplans/:id'] = 'productPlan'; +$routes['/productplans/:id/linkstory'] = 'productPlanLinkStory'; +$routes['/productplans/:id/unlinkstory'] = 'productPlanUnlinkStory'; +$routes['/productplans/:id/linkbug'] = 'productPlanLinkBug'; +$routes['/productplans/:id/unlinkbug'] = 'productPlanUnlinkBug'; $routes['/releases'] = 'releases'; $routes['/products/:id/releases'] = 'releases'; @@ -119,4 +123,6 @@ $routes['/z/folders/:id'] = 'zfolder'; $routes['/z/files/:id'] = 'zfile'; $routes['/z/files/:id/content'] = 'zfileContent'; +$routes['/gitlab/webhook'] = 'gitlabWebhook'; + $config->routes = $routes; diff --git a/config/zentaopms.php b/config/zentaopms.php index 4f29e4d7d0..654b2ba32c 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -230,6 +230,7 @@ define('TABLE_PIPELINE', '`' . $config->db->prefix . 'pipeline`'); define('TABLE_JOB', '`' . $config->db->prefix . 'job`'); define('TABLE_COMPILE', '`' . $config->db->prefix . 'compile`'); define('TABLE_MR', '`' . $config->db->prefix . 'mr`'); +define('TABLE_MRAPPROVAL', '`' . $config->db->prefix . 'mrapproval`'); define('TABLE_REPO', '`' . $config->db->prefix . 'repo`'); define('TABLE_RELATION', '`' . $config->db->prefix . 'relation`'); @@ -284,4 +285,4 @@ $config->newFeatures = array('introduction', 'tutorial', 'youngBlueTheme'); /* Program privs.*/ $config->programPriv = new stdclass(); $config->programPriv->scrum = array('projectstory', 'projectrelease', 'project', 'build', 'bug', 'testcase', 'testreport', 'caselib', 'doc', 'repo', 'meeting', 'stakeholder'); -$config->programPriv->waterfall = array_merge($config->programPriv->scrum, array('workestimation', 'durationestimation', 'budget', 'programplan', 'review', 'reviewissue', 'weekly', 'milestone', 'design', 'issue', 'risk', 'opportunity', 'measrecord', 'auditplan', 'trainplan', 'gapanalysis', 'pssp')); +$config->programPriv->waterfall = array_merge($config->programPriv->scrum, array('workestimation', 'durationestimation', 'budget', 'programplan', 'review', 'reviewissue', 'weekly', 'cm', 'milestone', 'design', 'issue', 'risk', 'opportunity', 'measrecord', 'auditplan', 'trainplan', 'gapanalysis', 'pssp', 'researchplan', 'researchreport')); diff --git a/db/update15.7.1.sql b/db/update15.7.1.sql index f1bb243e51..b46375e19e 100644 --- a/db/update15.7.1.sql +++ b/db/update15.7.1.sql @@ -142,3 +142,13 @@ ADD `feedbackBy` varchar(100) NOT NULL AFTER `version`, ADD `notifyEmail` varchar(100) NOT NULL AFTER `feedbackBy`; ALTER TABLE `zt_product` ADD `reviewer` varchar(255) NOT NULL AFTER `whitelist`; +UPDATE `zt_testtask` SET `pri`=3 WHERE `pri`=0; + +ALTER table zt_mr ADD `approver` varchar(255) NOT NULL, +ADD `approvalStatus` char(30) NOT NULL, +ADD `needApproved` enum('0','1') NOT NULL DEFAULT '0', +ADD `needCI` enum('0','1') NOT NULL DEFAULT '0', +ADD `repoID` mediumint(8) unsigned NOT NULL, +ADD `jobID` mediumint(8) unsigned NOT NULL, +ADD `compileID` mediumint(8) unsigned NOT NULL, +ADD `compileStatus` char(30) NOT NULL; diff --git a/db/zentao.sql b/db/zentao.sql index a25b303d5c..d05177fe58 100644 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -731,6 +731,7 @@ CREATE TABLE IF NOT EXISTS `zt_mr` ( `description` text NOT NULL, `assignee` varchar(255) NOT NULL, `reviewer` varchar(255) NOT NULL, + `approver` varchar(255) NOT NULL, `createdBy` varchar(30) NOT NULL, `createdDate` datetime NOT NULL, `editedBy` varchar(30) NOT NULL, @@ -738,6 +739,23 @@ CREATE TABLE IF NOT EXISTS `zt_mr` ( `deleted` enum('0','1') NOT NULL DEFAULT '0', `status` char(30) NOT NULL, `mergeStatus` char(30) NOT NULL, + `approvalStatus` char(30) NOT NULL, + `needApproved` enum('0','1') NOT NULL DEFAULT '0', + `needCI` enum('0','1') NOT NULL DEFAULT '0', + `repoID` mediumint(8) unsigned NOT NULL, + `jobID` mediumint(8) unsigned NOT NULL, + `compileID` mediumint(8) unsigned NOT NULL, + `compileStatus` char(30) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_mrapproval`; +CREATE TABLE IF NOT EXISTS `zt_mrapproval` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `mrID` mediumint(8) unsigned NOT NULL, + `account` varchar(255) NOT NULL, + `date` datetime NOT NULL, + `action` char(30) NOT NULL, + `comment` text NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_notify`; diff --git a/framework/api/entry.class.php b/framework/api/entry.class.php index f88046f590..7e67f1203b 100644 --- a/framework/api/entry.class.php +++ b/framework/api/entry.class.php @@ -576,43 +576,47 @@ class baseEntry { switch($type) { - case 'time': - $timeFormat = $this->param('timeFormat', 'utc'); - if($timeFormat == 'utc') - { - if(!$value or $value == '0000-00-00 00:00:00') return null; - return gmdate("Y-m-d\TH:i:s\Z", strtotime($value)); - } - return $value; - case 'date': - if(!$value or $value == '0000-00-00') return null; - return $value; - case 'bool': - return !empty($value); - case 'int': - return (int) $value; - case 'idList': - $values = explode(',', $value); - if(empty($values)) return array(); + case 'time': + $timeFormat = $this->param('timeFormat', 'utc'); + if($timeFormat == 'utc') + { + if(!$value or $value == '0000-00-00 00:00:00') return null; + return gmdate("Y-m-d\TH:i:s\Z", strtotime($value)); + } + return $value; + case 'date': + if(!$value or $value == '0000-00-00') return null; + return $value; + case 'bool': + return !empty($value); + case 'int': + return (int) $value; + case 'idList': + $values = explode(',', $value); + if(empty($values)) return array(); - $idList = array(); - foreach($values as $val) - { - if($val !== '') $idList[] = (int) $val; - } - return $idList; - case 'stringList': - $values = explode(',', $value); - if(empty($values)) return array(); + $idList = array(); + foreach($values as $val) + { + if($val !== '') $idList[] = (int) $val; + } + return $idList; + case 'stringList': + $values = explode(',', $value); + if(empty($values)) return array(); - $stringList = array(); - foreach($values as $val) - { - if($val !== '') $stringList[] = $val; - } - return $stringList; - default: - return $value; + $stringList = array(); + foreach($values as $val) + { + if($val !== '') $stringList[] = $val; + } + return $stringList; + case 'array': + $array = array(); + if(!empty($value)) foreach($value as $v) $array[] = $v; + return $array; + default: + return $value; } } @@ -650,4 +654,18 @@ class baseEntry $this->send(403, array('error' => 'Access not allowed')); } } + + /** + * Reset open app. + * + * @param string $tab + * @access public + * @return void + */ + public function resetOpenApp($tab) + { + $_COOKIE['tab'] = $tab; + $this->app->tab = $tab; + $this->app->session->tab = $tab; + } } diff --git a/framework/base/control.class.php b/framework/base/control.class.php index 1623500402..26f79fb7bb 100644 --- a/framework/base/control.class.php +++ b/framework/base/control.class.php @@ -928,6 +928,14 @@ class baseControl { if(!empty($data['message'])) { + if(is_string($data['message'])) + { + echo js::alert($data['message']); + $locate = isset($data['locate']) ? $data['locate'] : (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''); + if (!empty($locate)) die(js::locate($locate)); + die(isset($data['message']) ? $data['message'] : 'fail'); + } + $message = json_decode(json_encode((array)$data['message'])); foreach((array)$message as $item => $errors) $message->$item = implode(',', $errors); die(js::alert(strip_tags(implode('\n', (array)$message)))); diff --git a/framework/base/helper.class.php b/framework/base/helper.class.php index 7eb67535a0..b159d9f0c3 100644 --- a/framework/base/helper.class.php +++ b/framework/base/helper.class.php @@ -517,7 +517,7 @@ class baseHelper * Get now time use the DT_DATETIME1 constant defined in the lang file. * * @access public - * @return datetime now + * @return string now */ static public function now() { @@ -529,7 +529,7 @@ class baseHelper * Get today according to the DT_DATE1 constant defined in the lang file. * * @access public - * @return date today + * @return string today */ static public function today() { @@ -541,7 +541,7 @@ class baseHelper * Get now time use the DT_TIME1 constant defined in the lang file. * * @access public - * @return date today + * @return string today */ static public function time() { @@ -748,10 +748,10 @@ class baseHelper * @param string $viewType * @return string the link string. */ -function inLink($methodName = 'index', $vars = '', $viewType = '') +function inLink($methodName = 'index', $vars = '', $viewType = '', $onlybody = false) { global $app; - return helper::createLink($app->getModuleName(), $methodName, $vars, $viewType); + return helper::createLink($app->getModuleName(), $methodName, $vars, $viewType, $onlybody); } /** @@ -855,7 +855,7 @@ function getWebRoot($full = false) * @param mixed $valueWhenNone value when the key not exits. * @param mixed $valueWhenExists value when the key exits. * @access public - * @return string + * @return mixed */ function zget($var, $key, $valueWhenNone = false, $valueWhenExists = false) { diff --git a/lib/htmlup/htmlup.class.php b/lib/htmlup/htmlup.class.php new file mode 100644 index 0000000000..9da987283d --- /dev/null +++ b/lib/htmlup/htmlup.class.php @@ -0,0 +1,566 @@ + +* +* +* Licensed under MIT license. +*/ + +trait HtmlHelper +{ +public function escape($input) +{ + return \htmlspecialchars($input); +} + +public function h($level, $line) +{ + if (\is_string($level)) { + $level = \trim($level, '- ') === '' ? 2 : 1; + } + + if ($level < 7) { + return "\n" . \ltrim(\ltrim($line, '# ')) . ""; + } + + return ''; +} + +public function hr($prevLine, $line) +{ + if ($prevLine === '' && \preg_match(BlockElementParser::RE_MD_RULE, $line)) { + return "\n
"; + } + } + + public function codeStart($lang) + { + $lang = isset($lang[1]) + ? ' class="language-' . $lang[1] . '"' + : ''; + + return "\n
";
+    }
+
+    public function codeLine($line, $isBlock, $indentLen = 4)
+    {
+        $code  = "\n"; // @todo: donot use \n for first line
+        $code .= $isBlock ? $line : \substr($line, $indentLen);
+
+        return $code;
+    }
+
+    public function tableStart($line, $delim = '|')
+    {
+        $table = "\n\n\n";
+
+        foreach (\explode($delim, \trim($line, $delim)) as $hdr) {
+            $table .= '\n";
+        }
+
+        $table .= "\n\n\n";
+
+        return $table;
+    }
+
+    public function tableRow($line, $colCount, $delim = '|')
+    {
+        $row = "\n";
+
+        foreach (\explode($delim, \trim($line, $delim)) as $i => $col) {
+            if ($i > $colCount) {
+                break;
+            }
+
+            $col  = \trim($col);
+            $row .= "\n";
+        }
+
+        $row .= "\n";
+
+        return $row;
+    }
+}
+
+abstract class BlockElementParser
+{
+    use HtmlHelper;
+
+    const RE_MD_QUOTE  = '~^\s*(>+)\s+~';
+    const RE_RAW       = '/^<\/?\w.*?\/?>/';
+    const RE_MD_SETEXT = '~^\s*(={3,}|-{3,})\s*$~';
+    const RE_MD_CODE   = '/^```\s*([\w-]+)?/';
+    const RE_MD_RULE   = '~^(_{3,}|\*{3,}|\-{3,})$~';
+    const RE_MD_TCOL   = '~(\|\s*\:)?\s*\-{3,}\s*(\:\s*\|)?~';
+    const RE_MD_OL     = '/^\d+\. /';
+
+    protected $lines       = [];
+    protected $stackList   = [];
+    protected $stackBlock  = [];
+    protected $stackTable  = [];
+
+    protected $pointer     = -1;
+    protected $listLevel   = 0;
+    protected $quoteLevel  = 0;
+    protected $indent      = 0;
+    protected $nextIndent  = 0;
+    protected $indentLen   = 4;
+
+    protected $indentStr       = '    ';
+    protected $line            = '';
+    protected $trimmedLine     = '';
+    protected $prevLine        = '';
+    protected $trimmedPrevLine = '';
+    protected $nextLine        = '';
+    protected $trimmedNextLine = '';
+    protected $markup          = '';
+
+    protected $inList  = \false;
+    protected $inQuote = \false;
+    protected $inPara  = \false;
+    protected $inHtml  = \false;
+    protected $inTable = \false;
+
+    protected function parseBlockElements()
+    {
+        while (isset($this->lines[++$this->pointer])) {
+            $this->init();
+
+            if ($this->flush() || $this->raw()) {
+                continue;
+            }
+
+            $this->quote();
+
+            if (($block = $this->isBlock()) || $this->inList) {
+                $this->markup .= $block ? '' : $this->trimmedLine;
+
+                continue;
+            }
+
+            $this->table() || $this->paragraph();
+        }
+    }
+
+    protected function isBlock()
+    {
+        return $this->atx() || $this->setext() || $this->code() || $this->rule() || $this->listt();
+    }
+
+    protected function atx()
+    {
+        if (\substr($this->trimmedLine, 0, 1) === '#') {
+            $level = \strlen($this->trimmedLine) - \strlen(\ltrim($this->trimmedLine, '#'));
+            $head  = $this->h($level, $this->trimmedLine);
+
+            $this->markup .= $head;
+
+            return (bool) $head;
+        }
+    }
+
+    protected function setext()
+    {
+        if (\preg_match(static::RE_MD_SETEXT, $this->nextLine)) {
+            $this->markup .= $this->h($this->nextLine, $this->trimmedLine);
+
+            $this->pointer++;
+
+            return \true;
+        }
+    }
+
+    protected function code()
+    {
+        $isShifted = ($this->indent - $this->nextIndent) >= $this->indentLen;
+        $codeBlock = \preg_match(static::RE_MD_CODE, $this->line, $codeMatch);
+
+        if ($codeBlock || (!$this->inList && !$this->inQuote && $isShifted)) {
+            $this->markup .= $this->codeStart($codeMatch);
+
+            if (!$codeBlock) {
+                $this->markup .= $this->escape(\substr($this->line, $this->indentLen));
+            }
+
+            $this->codeInternal($codeBlock);
+
+            $this->pointer++;
+
+            $this->markup .= '';
+
+            return \true;
+        }
+    }
+
+    private function codeInternal($codeBlock)
+    {
+        while (isset($this->lines[$this->pointer + 1])) {
+            $this->line = $this->escape($this->lines[$this->pointer + 1]);
+
+            if (($codeBlock && \substr(\ltrim($this->line), 0, 3) !== '```')
+                || \strpos($this->line, $this->indentStr) === 0
+            ) {
+                $this->markup .= $this->codeLine($this->line, $codeBlock, $this->indentLen);
+
+                $this->pointer++;
+
+                continue;
+            }
+
+            break;
+        }
+    }
+
+    protected function rule()
+    {
+        $this->markup .= $hr = $this->hr($this->trimmedPrevLine, $this->trimmedLine);
+
+        return (bool) $hr;
+    }
+
+    protected function listt()
+    {
+        $isUl = \in_array(\substr($this->trimmedLine, 0, 2), ['- ', '* ', '+ ']);
+
+        if ($isUl || \preg_match(static::RE_MD_OL, $this->trimmedLine)) {
+            $wrapper = $isUl ? 'ul' : 'ol';
+
+            if (!$this->inList) {
+                $this->stackList[] = "";
+
+                $this->markup .= "\n<$wrapper>\n";
+                $this->inList  = \true;
+
+                $this->listLevel++;
+            }
+
+            $this->markup .= '
  • ' . \ltrim($this->trimmedLine, '+-*0123456789. '); + + $this->listInternal(); + + return \true; + } + } + + private function listInternal() + { + $isUl = \in_array(\substr($this->trimmedNextLine, 0, 2), ['- ', '* ', '+ ']); + + if ($isUl || \preg_match(static::RE_MD_OL, $this->trimmedNextLine)) { + $wrapper = $isUl ? 'ul' : 'ol'; + if ($this->nextIndent > $this->indent) { + $this->stackList[] = "
  • \n"; + $this->stackList[] = ""; + $this->markup .= "\n<$wrapper>\n"; + + $this->listLevel++; + } else { + $this->markup .= "\n"; + } + + if ($this->nextIndent < $this->indent) { + $shift = \intval(($this->indent - $this->nextIndent) / $this->indentLen); + + while ($shift--) { + $this->markup .= \array_pop($this->stackList); + + if ($this->listLevel > 2) { + $this->markup .= \array_pop($this->stackList); + } + } + } + } else { + $this->markup .= "\n"; + } + } + + protected function table() + { + static $headerCount = 0; + + if (!$this->inTable) { + $headerCount = \substr_count(\trim($this->trimmedLine, '|'), '|'); + + return $this->tableInternal($headerCount); + } + + $this->markup .= $this->tableRow($this->trimmedLine, $headerCount); + + if (empty($this->trimmedNextLine) + || !\substr_count(\trim($this->trimmedNextLine, '|'), '|') + ) { + $headerCount = 0; + $this->inTable = \false; + $this->stackTable[] = "\n
    ' . \trim($hdr) . "
    {$col}
    "; + } + + return \true; + } + + private function tableInternal($headerCount) + { + $columnCount = \preg_match_all(static::RE_MD_TCOL, \trim($this->trimmedNextLine, '|')); + + if ($headerCount > 0 && $headerCount <= $columnCount) { + $this->pointer++; + + $this->inTable = \true; + $this->markup .= $this->tableStart($this->trimmedLine); + + return \true; + } + } +} + +class SpanElementParser +{ + use HtmlHelper; + + const RE_URL = '~<(https?:[\/]{2}[^\s]+?)>~'; + const RE_EMAIL = '~<(\S+?@\S+?)>~'; + const RE_MD_IMG = '~!\[(.+?)\]\s*\((.+?)\s*(".+?")?\)~'; + const RE_MD_URL = '~\[(.+?)\]\s*\((.+?)\s*(".+?")?\)~'; + const RE_MD_FONT = '!(\*{1,2}|_{1,2}|`|~~)(.+?)\\1!'; + + public function parse($markup) + { + return $this->spans( + $this->anchors( + $this->links($markup) + ) + ); + } + + protected function links($markup) + { + $markup = $this->emails($markup); + + return \preg_replace( + static::RE_URL, + '$1', + $markup + ); + } + + protected function emails($markup) + { + return \preg_replace( + static::RE_EMAIL, + '$1', + $markup + ); + } + + protected function anchors($markup) + { + $markup = $this->images($markup); + + return \preg_replace_callback(static::RE_MD_URL, function ($a) { + $title = isset($a[3]) ? " title={$a[3]} " : ''; + + return "{$a[1]}"; + }, $markup); + } + + protected function images($markup) + { + return \preg_replace_callback(static::RE_MD_IMG, function ($img) { + $title = isset($img[3]) ? " title={$img[3]} " : ''; + $alt = $img[1] ? " alt=\"{$img[1]}\" " : ''; + + return ""; + }, $markup); + } + + protected function spans($markup) + { + // em/code/strong/del + return \preg_replace_callback(static::RE_MD_FONT, function ($em) { + switch (\substr($em[1], 0, 2)) { + case '**': + case '__': + $tag = 'strong'; + break; + + case '~~': + $tag = 'del'; + break; + + case $em[1] === '*': + case $em[1] === '_': + $tag = 'em'; + break; + + default: + $tag = 'code'; + $em[2] = $this->escape($em[2]); + } + + return "<$tag>{$em[2]}"; + }, $markup); + } +} + +/** + * HtmlUp - A **lightweight** && **fast** `markdown` to HTML Parser. + * + * Supports most of the markdown specs except deep nested elements. + * Check readme.md for the details of its features && limitations. + * + * @author adhocore | Jitendra Adhikari + * @copyright (c) 2014 Jitendra Adhikari + */ +class htmlup extends BlockElementParser +{ + /** + * Constructor. + * + * @param string $markdown + * @param int $indentWidth + */ + public function __construct($markdown = \null, $indentWidth = 4) + { + $this->scan($markdown, $indentWidth); + } + + protected function scan($markdown, $indentWidth = 4) + { + if ('' === \trim($markdown)) { + return; + } + + $this->indentLen = $indentWidth == 2 ? 2 : 4; + $this->indentStr = $indentWidth == 2 ? ' ' : ' '; + + // Normalize whitespaces + $markdown = \str_replace("\t", $this->indentStr, $markdown); + $markdown = \str_replace(["\r\n", "\r"], "\n", $markdown); + + $this->lines = \array_merge([''], \explode("\n", $markdown), ['']); + } + + public function __toString() + { + return $this->parse(); + } + + /** + * Parse markdown. + * + * @param string $markdown + * @param int $indentWidth + * + * @return string + */ + public function parse($markdown = \null, $indentWidth = 4) + { + if (\null !== $markdown) { + $this->reset(\true); + + $this->scan($markdown, $indentWidth); + } + + if (empty($this->lines)) { + return ''; + } + + $this->parseBlockElements(); + + return (new SpanElementParser)->parse($this->markup); + } + + protected function init() + { + list($this->prevLine, $this->trimmedPrevLine) = [$this->line, $this->trimmedLine]; + + $this->line = $this->lines[$this->pointer]; + $this->trimmedLine = \trim($this->line); + + $this->indent = \strlen($this->line) - \strlen(\ltrim($this->line)); + $this->nextLine = isset($this->lines[$this->pointer + 1]) + ? $this->lines[$this->pointer + 1] + : ''; + $this->trimmedNextLine = \trim($this->nextLine); + $this->nextIndent = \strlen($this->nextLine) - \strlen(\ltrim($this->nextLine)); + } + + protected function reset($all = \false) + { + $except = $all ? [] : \array_flip(['lines', 'pointer', 'markup', 'indentStr', 'indentLen']); + + // Reset all current values. + foreach (\get_class_vars(__CLASS__) as $prop => $value) { + isset($except[$prop]) || $this->{$prop} = $value; + } + } + + protected function flush() + { + if ('' !== $this->trimmedLine) { + return \false; + } + + while (!empty($this->stackList)) { + $this->markup .= \array_pop($this->stackList); + } + + while (!empty($this->stackBlock)) { + $this->markup .= \array_pop($this->stackBlock); + } + + while (!empty($this->stackTable)) { + $this->markup .= \array_pop($this->stackTable); + } + + $this->markup .= "\n"; + + $this->reset(\false); + + return \true; + } + + protected function raw() + { + if ($this->inHtml || \preg_match(static::RE_RAW, $this->trimmedLine)) { + $this->markup .= "\n$this->line"; + if (!$this->inHtml && empty($this->lines[$this->pointer - 1])) { + $this->inHtml = \true; + } + + return \true; + } + } + + protected function quote() + { + if (\preg_match(static::RE_MD_QUOTE, $this->line, $quoteMatch)) { + $this->line = \substr($this->line, \strlen($quoteMatch[0])); + $this->trimmedLine = \trim($this->line); + + if (!$this->inQuote || $this->quoteLevel < \strlen($quoteMatch[1])) { + $this->markup .= "\n
    "; + + $this->stackBlock[] = "\n
    "; + + $this->quoteLevel++; + } + + return $this->inQuote = \true; + } + } + + protected function paragraph() + { + $this->markup .= $this->inPara ? "\n
    " : "\n

    "; + $this->markup .= $this->trimmedLine; + + if (empty($this->trimmedNextLine)) { + $this->markup .= '

    '; + $this->inPara = \false; + } else { + $this->inPara = \true; + } + } +} diff --git a/lib/scm/gitlab.class.php b/lib/scm/gitlab.class.php index 7aea29306d..c84be1287b 100644 --- a/lib/scm/gitlab.class.php +++ b/lib/scm/gitlab.class.php @@ -93,7 +93,8 @@ class gitlab * @param string $path * @param string $ref * @access public - * @return array + * @return object + * @doc https://docs.gitlab.com/ee/api/repository_files.html */ public function files($path, $ref = 'master') { @@ -365,7 +366,7 @@ class gitlab * * @param string $cmd * @access public - * @todo Exec commads by gitlab api. + * @todo Exec commands by gitlab api. * @return array */ public function exec($cmd) @@ -535,10 +536,24 @@ class gitlab if($version and $version != 'HEAD') { - $committedDate = $this->getCommitedDate($version); + /* Get since param. */ + if(substr($version, 0, 5) == 'since') + { + $since = true; + $version = substr($version, 5); + } + + $committedDate = $this->getCommittedDate($version); if(!$committedDate) return array('commits' => array(), 'files' => array()); - $params['until'] = $committedDate; + if(!empty($since)) + { + $params['since'] = $committedDate; + } + else + { + $params['until'] = $committedDate; + } } $list = $this->fetch($api, $params); @@ -566,7 +581,7 @@ class gitlab * @access public * @return void */ - public function getCommitedDate($sha) + public function getCommittedDate($sha) { if(!scm::checkRevision($sha)) return null; @@ -594,8 +609,8 @@ class gitlab $param->path = urldecode($path); $param->ref_name = $this->branch; - $fromDate = $this->getCommitedDate($fromRevision); - $toDate = $this->getCommitedDate($toRevision); + $fromDate = $this->getCommittedDate($fromRevision); + $toDate = $this->getCommittedDate($toRevision); $since = ''; $until = ''; @@ -663,7 +678,7 @@ class gitlab * @param string $path * @param bool $recursive * @access public - * @return void + * @return mixed */ public function tree($path, $recursive = 1) { @@ -681,7 +696,7 @@ class gitlab * * @param string $api * @access public - * @return void + * @return mixed */ public function fetch($api, $params = array()) { diff --git a/module/action/config.php b/module/action/config.php index 54d57c603e..81c7f5e250 100755 --- a/module/action/config.php +++ b/module/action/config.php @@ -32,6 +32,7 @@ $config->action->objectNameFields['budget'] = 'name'; $config->action->objectNameFields['job'] = 'name'; $config->action->objectNameFields['team'] = 'name'; $config->action->objectNameFields['pipeline'] = 'name'; +$config->action->objectNameFields['mr'] = 'title'; $config->action->objectNameFields['kanbancolumn'] = 'name'; $config->action->objectNameFields['kanbanlane'] = 'name'; diff --git a/module/action/lang/en.php b/module/action/lang/en.php index 3c6653965a..f209125344 100755 --- a/module/action/lang/en.php +++ b/module/action/lang/en.php @@ -76,44 +76,48 @@ $lang->action->periods['lastweek'] = $lang->action->dynamic->lastWeek; $lang->action->periods['thismonth'] = $lang->action->dynamic->thisMonth; $lang->action->periods['lastmonth'] = $lang->action->dynamic->lastMonth; -$lang->action->objectTypes['product'] = $lang->productCommon; -$lang->action->objectTypes['branch'] = 'Branch'; -$lang->action->objectTypes['story'] = $lang->SRCommon; -$lang->action->objectTypes['design'] = 'Design'; -$lang->action->objectTypes['productplan'] = 'Plan'; -$lang->action->objectTypes['release'] = 'Release'; -$lang->action->objectTypes['program'] = 'Program'; -$lang->action->objectTypes['project'] = 'Project'; -$lang->action->objectTypes['execution'] = $lang->executionCommon; -$lang->action->objectTypes['task'] = 'Task'; -$lang->action->objectTypes['build'] = 'Build'; -$lang->action->objectTypes['job'] = 'Job'; -$lang->action->objectTypes['bug'] = 'Bug'; -$lang->action->objectTypes['case'] = 'Case'; -$lang->action->objectTypes['caseresult'] = 'Case Result'; -$lang->action->objectTypes['stepresult'] = 'Case Steps'; -$lang->action->objectTypes['caselib'] = 'Library'; -$lang->action->objectTypes['testsuite'] = 'Suite'; -$lang->action->objectTypes['testtask'] = 'Test Build'; -$lang->action->objectTypes['testreport'] = 'Report'; -$lang->action->objectTypes['doc'] = 'Document'; -$lang->action->objectTypes['api'] = 'Interface'; -$lang->action->objectTypes['doclib'] = 'Document Library'; -$lang->action->objectTypes['apistruct'] = 'API struct'; -$lang->action->objectTypes['todo'] = 'Todo'; -$lang->action->objectTypes['risk'] = 'Risk'; -$lang->action->objectTypes['issue'] = 'Issue'; -$lang->action->objectTypes['module'] = 'Module'; -$lang->action->objectTypes['user'] = 'User'; -$lang->action->objectTypes['stakeholder'] = 'Stakeholder'; -$lang->action->objectTypes['budget'] = 'Cost Estimate'; -$lang->action->objectTypes['entry'] = 'Entry'; -$lang->action->objectTypes['webhook'] = 'Webhook'; -$lang->action->objectTypes['team'] = 'Team'; -$lang->action->objectTypes['whitelist'] = 'Whitelist'; -$lang->action->objectTypes['pipeline'] = 'GitLab'; -$lang->action->objectTypes['gitlab'] = 'GitLab'; -$lang->action->objectTypes['jenkins'] = 'Jenkins'; +$lang->action->objectTypes['product'] = $lang->productCommon; +$lang->action->objectTypes['branch'] = 'Branch'; +$lang->action->objectTypes['story'] = $lang->SRCommon; +$lang->action->objectTypes['design'] = 'Design'; +$lang->action->objectTypes['productplan'] = 'Plan'; +$lang->action->objectTypes['release'] = 'Release'; +$lang->action->objectTypes['program'] = 'Program'; +$lang->action->objectTypes['project'] = 'Project'; +$lang->action->objectTypes['execution'] = $lang->executionCommon; +$lang->action->objectTypes['task'] = 'Task'; +$lang->action->objectTypes['build'] = 'Build'; +$lang->action->objectTypes['job'] = 'Job'; +$lang->action->objectTypes['bug'] = 'Bug'; +$lang->action->objectTypes['case'] = 'Case'; +$lang->action->objectTypes['caseresult'] = 'Case Result'; +$lang->action->objectTypes['stepresult'] = 'Case Steps'; +$lang->action->objectTypes['caselib'] = 'Library'; +$lang->action->objectTypes['testsuite'] = 'Suite'; +$lang->action->objectTypes['testtask'] = 'Test Build'; +$lang->action->objectTypes['testreport'] = 'Report'; +$lang->action->objectTypes['doc'] = 'Document'; +$lang->action->objectTypes['api'] = 'Interface'; +$lang->action->objectTypes['doclib'] = 'Document Library'; +$lang->action->objectTypes['apistruct'] = 'API struct'; +$lang->action->objectTypes['todo'] = 'Todo'; +$lang->action->objectTypes['risk'] = 'Risk'; +$lang->action->objectTypes['issue'] = 'Issue'; +$lang->action->objectTypes['module'] = 'Module'; +$lang->action->objectTypes['user'] = 'User'; +$lang->action->objectTypes['stakeholder'] = 'Stakeholder'; +$lang->action->objectTypes['budget'] = 'Cost Estimate'; +$lang->action->objectTypes['entry'] = 'Entry'; +$lang->action->objectTypes['webhook'] = 'Webhook'; +$lang->action->objectTypes['team'] = 'Team'; +$lang->action->objectTypes['whitelist'] = 'Whitelist'; +$lang->action->objectTypes['pipeline'] = 'GitLab'; +$lang->action->objectTypes['gitlab'] = 'GitLab'; +$lang->action->objectTypes['jenkins'] = 'Jenkins'; +$lang->action->objectTypes['mr'] = 'Merge Request'; +$lang->action->objectTypes['gitlabproject'] = 'GitLab Project'; +$lang->action->objectTypes['gitlabuser'] = 'GitLab User'; +$lang->action->objectTypes['gitlabgroup'] = 'GitLab Group'; /* Used to describe operation history. */ $lang->action->desc = new stdclass(); @@ -287,6 +291,11 @@ $lang->action->label->syncprogram = 'start'; $lang->action->label->syncproject = 'start'; $lang->action->label->syncexecution = 'start'; $lang->action->label->startProgram = '(The start of the project sets the status of the program as Ongoing)'; +$lang->action->label->createmr = 'MR Linked'; +$lang->action->label->mergedmr = 'MR Merged'; +$lang->action->label->reopen = 'Reopen'; +$lang->action->label->approve = 'Passed'; +$lang->action->label->reject = 'Rejected'; /* Dynamic information is grouped by object. */ $lang->action->dynamicAction = new stdclass; @@ -694,3 +703,13 @@ $lang->action->apiTitle->reviewrejected = 'Review rejected'; $lang->action->apiTitle->reviewclarified = 'Review clarified'; $lang->action->apiTitle->commitsummary = 'Commit summary'; $lang->action->apiTitle->updatetrainee = 'Update trainee'; + +/* Code Review in Repo or Merge Request module. */ +$lang->action->desc->repocreated = '$date, created and reviewed by $actor: $extra.' . "\n"; +$lang->action->label->repocreated = "create and review"; +$lang->action->dynamicAction->task['gitcommited'] = 'Git Commit'; +$lang->action->dynamicAction->bug['repocreated'] = $lang->action->label->repocreated; +$lang->action->desc->createmr = '$extra'; +$lang->action->desc->mergedmr = '$date, $actor merged code.'; +$lang->action->desc->approve = '$date, $actor approved.'; +$lang->action->desc->reject = '$date, $actor rejected.'; diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php index 1b67311e4f..b0644a573b 100755 --- a/module/action/lang/zh-cn.php +++ b/module/action/lang/zh-cn.php @@ -79,44 +79,48 @@ $lang->action->periods['lastweek'] = $lang->action->dynamic->lastWeek; $lang->action->periods['thismonth'] = $lang->action->dynamic->thisMonth; $lang->action->periods['lastmonth'] = $lang->action->dynamic->lastMonth; -$lang->action->objectTypes['product'] = $lang->productCommon; -$lang->action->objectTypes['branch'] = '分支'; -$lang->action->objectTypes['story'] = $lang->SRCommon; -$lang->action->objectTypes['design'] = '设计'; -$lang->action->objectTypes['productplan'] = '计划'; -$lang->action->objectTypes['release'] = '发布'; -$lang->action->objectTypes['program'] = '项目集'; -$lang->action->objectTypes['project'] = '项目'; -$lang->action->objectTypes['execution'] = $config->systemMode == 'new' ? '执行' : $lang->executionCommon; -$lang->action->objectTypes['task'] = '任务'; -$lang->action->objectTypes['build'] = '版本'; -$lang->action->objectTypes['job'] = '构建'; -$lang->action->objectTypes['bug'] = 'Bug'; -$lang->action->objectTypes['case'] = '用例'; -$lang->action->objectTypes['caseresult'] = '用例结果'; -$lang->action->objectTypes['stepresult'] = '用例步骤'; -$lang->action->objectTypes['caselib'] = '用例库'; -$lang->action->objectTypes['testsuite'] = '套件'; -$lang->action->objectTypes['testtask'] = '测试单'; -$lang->action->objectTypes['testreport'] = '报告'; -$lang->action->objectTypes['doc'] = '文档'; -$lang->action->objectTypes['api'] = '接口'; -$lang->action->objectTypes['doclib'] = '文档库'; -$lang->action->objectTypes['apistruct'] = '数据结构'; -$lang->action->objectTypes['todo'] = '待办'; -$lang->action->objectTypes['risk'] = '风险'; -$lang->action->objectTypes['issue'] = '问题'; -$lang->action->objectTypes['module'] = '模块'; -$lang->action->objectTypes['user'] = '用户'; -$lang->action->objectTypes['stakeholder'] = '干系人'; -$lang->action->objectTypes['budget'] = '费用估算'; -$lang->action->objectTypes['entry'] = '应用'; -$lang->action->objectTypes['webhook'] = 'Webhook'; -$lang->action->objectTypes['team'] = '团队'; -$lang->action->objectTypes['whitelist'] = '白名单'; -$lang->action->objectTypes['pipeline'] = 'GitLab'; -$lang->action->objectTypes['gitlab'] = 'GitLab'; -$lang->action->objectTypes['jenkins'] = 'Jenkins'; +$lang->action->objectTypes['product'] = $lang->productCommon; +$lang->action->objectTypes['branch'] = '分支'; +$lang->action->objectTypes['story'] = $lang->SRCommon; +$lang->action->objectTypes['design'] = '设计'; +$lang->action->objectTypes['productplan'] = '计划'; +$lang->action->objectTypes['release'] = '发布'; +$lang->action->objectTypes['program'] = '项目集'; +$lang->action->objectTypes['project'] = '项目'; +$lang->action->objectTypes['execution'] = $config->systemMode == 'new' ? '执行' : $lang->executionCommon; +$lang->action->objectTypes['task'] = '任务'; +$lang->action->objectTypes['build'] = '版本'; +$lang->action->objectTypes['job'] = '构建'; +$lang->action->objectTypes['bug'] = 'Bug'; +$lang->action->objectTypes['case'] = '用例'; +$lang->action->objectTypes['caseresult'] = '用例结果'; +$lang->action->objectTypes['stepresult'] = '用例步骤'; +$lang->action->objectTypes['caselib'] = '用例库'; +$lang->action->objectTypes['testsuite'] = '套件'; +$lang->action->objectTypes['testtask'] = '测试单'; +$lang->action->objectTypes['testreport'] = '报告'; +$lang->action->objectTypes['doc'] = '文档'; +$lang->action->objectTypes['api'] = '接口'; +$lang->action->objectTypes['doclib'] = '文档库'; +$lang->action->objectTypes['apistruct'] = '数据结构'; +$lang->action->objectTypes['todo'] = '待办'; +$lang->action->objectTypes['risk'] = '风险'; +$lang->action->objectTypes['issue'] = '问题'; +$lang->action->objectTypes['module'] = '模块'; +$lang->action->objectTypes['user'] = '用户'; +$lang->action->objectTypes['stakeholder'] = '干系人'; +$lang->action->objectTypes['budget'] = '费用估算'; +$lang->action->objectTypes['entry'] = '应用'; +$lang->action->objectTypes['webhook'] = 'Webhook'; +$lang->action->objectTypes['team'] = '团队'; +$lang->action->objectTypes['whitelist'] = '白名单'; +$lang->action->objectTypes['pipeline'] = 'GitLab'; +$lang->action->objectTypes['gitlab'] = 'GitLab'; +$lang->action->objectTypes['jenkins'] = 'Jenkins'; +$lang->action->objectTypes['mr'] = '合并请求'; +$lang->action->objectTypes['gitlabproject'] = 'GitLab项目'; +$lang->action->objectTypes['gitlabuser'] = 'GitLab用户'; +$lang->action->objectTypes['gitlabgroup'] = 'GitLab群组'; /* 用来描述操作历史记录。*/ $lang->action->desc = new stdclass(); @@ -290,6 +294,13 @@ $lang->action->label->syncprogram = '开始了'; $lang->action->label->syncproject = '开始了'; $lang->action->label->syncexecution = '开始了'; $lang->action->label->startProgram = '(因项目开始而启动项目集)'; +$lang->action->label->createmr = 'MR关联了'; +$lang->action->label->mergedmr = 'MR合并了'; +$lang->action->label->compilepass = '构建成功'; +$lang->action->label->compilefail = '构建失败'; +$lang->action->label->reopen = '重新打开'; +$lang->action->label->approve = '通过了'; +$lang->action->label->reject = '拒绝了'; /* 动态信息按照对象分组 */ $lang->action->dynamicAction = new stdclass(); @@ -697,3 +708,13 @@ $lang->action->apiTitle->reviewrejected = '拒绝'; $lang->action->apiTitle->reviewclarified = '有待明确'; $lang->action->apiTitle->commitsummary = '提交培训总结'; $lang->action->apiTitle->updatetrainee = '更新培训人员'; + +/* Code Review in Repo or Merge Request module. */ +$lang->action->desc->repocreated = '$date, 由 $actor 评审创建:$extra。' . "\n"; +$lang->action->label->repocreated = "创建评审"; +$lang->action->dynamicAction->task['gitcommited'] = 'git提交'; +$lang->action->dynamicAction->bug['repocreated'] = '创建代码评审'; +$lang->action->desc->createmr = '$extra'; +$lang->action->desc->mergedmr = '$date, 由 $actor 合并了 代码。'; +$lang->action->desc->approve = '$date, 由 $actor 审核通过。'; +$lang->action->desc->reject = '$date, 由 $actor 拒绝。'; diff --git a/module/action/lang/zh-tw.php b/module/action/lang/zh-tw.php index b33ac8d1f5..0704f2062e 100755 --- a/module/action/lang/zh-tw.php +++ b/module/action/lang/zh-tw.php @@ -689,3 +689,9 @@ $lang->action->apiTitle->reviewrejected = '拒絶'; $lang->action->apiTitle->reviewclarified = '有待明確'; $lang->action->apiTitle->commitsummary = '提交培訓總結'; $lang->action->apiTitle->updatetrainee = '更新培訓人員'; + +/* Code Review in Repo or Merge Request module. */ +$lang->action->desc->repocreated = '$date, 由 $actor 評審創建:$extra。' . "\n"; +$lang->action->label->repocreated = "創建評審"; +$lang->action->dynamicAction->task['gitcommited'] = 'git提交'; +$lang->action->dynamicAction->bug['repocreated'] = '創建代碼評審'; diff --git a/module/action/model.php b/module/action/model.php index ad7a2819ac..b6a12fa9f3 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -641,7 +641,7 @@ class actionModel extends model /** * Print actions of an object. * - * @param array $action + * @param object $action * @param string $desc * @access public * @return void @@ -945,9 +945,9 @@ class actionModel extends model /** * Transform the actions for display. * - * @param int $actions + * @param array $actions * @access public - * @return void + * @return object */ public function transformActions($actions) { @@ -988,6 +988,9 @@ class actionModel extends model /* If action type is login or logout, needn't link. */ if($actionType == 'svncommited' or $actionType == 'gitcommited') $action->actor = zget($commiters, $action->actor); + /* Get gitlab objectname. */ + if(substr($objectType, 0,6) == 'gitlab') $action->objectName = $action->extra; + /* Other actions, create a link. */ if(!$this->setObjectLink($action, $deptUsers)) { @@ -1482,12 +1485,14 @@ class actionModel extends model { $dateGroup = array_reverse($dateGroup); } - elseif($this->app->rawModule == 'company' and (($direction == 'next' and $orderBy == 'date_asc') or ($direction == 'pre' and $orderBy == 'date_desc'))) + elseif($this->app->rawModule == 'company') { - $dateGroup = array_reverse($dateGroup); - foreach($dateGroup as $key => $dateItem) $dateGroup[$key] = array_reverse($dateItem); + if($direction == 'pre') $dateGroup = array_reverse($dateGroup); + if(($direction == 'next' and $orderBy == 'date_asc') or ($direction == 'pre' and $orderBy == 'date_desc')) + { + foreach($dateGroup as $key => $dateItem) $dateGroup[$key] = array_reverse($dateItem); + } } - return $dateGroup; } @@ -1565,7 +1570,7 @@ class actionModel extends model /** * Print actions of an object for API(JIHU). * - * @param array $action + * @param object $action * @access public * @return void */ diff --git a/module/branch/model.php b/module/branch/model.php index 37671f727c..09b1c7c349 100644 --- a/module/branch/model.php +++ b/module/branch/model.php @@ -510,4 +510,39 @@ class branchModel extends model return $branches; } + + /** + * Display of branch label. + * + * @param int $productID + * @param int $moduleID + * @param int $executionID + * @access public + * @return bool + */ + public function showBranch($productID, $moduleID = 0, $executionID = 0) + { + $this->loadModel('product'); + if(empty($productID) and empty($moduleID)) + { + $productPairs = $this->product->getProductPairsByProject($executionID); + if($this->app->tab != 'project') $productID = count($productPairs) == 1 ? key($productPairs) : 0; + } + elseif(empty($productID) and !empty($moduleID)) + { + $module = $this->loadModel('tree')->getById($moduleID); + $productID = $module->type != 'task' ? $module->root : 0; + } + + $product = $productID ? $this->product->getById($productID) : ''; + + if($product and $product->type != 'normal') + { + $this->app->loadLang('datatable'); + $this->lang->datatable->showBranch = sprintf($this->lang->datatable->showBranch, $this->lang->product->branchName[$product->type]); + return true; + } + + return false; + } } diff --git a/module/branch/view/ajaxgetdropmenu.html.php b/module/branch/view/ajaxgetdropmenu.html.php index 92114b05af..e31ed3807b 100644 --- a/module/branch/view/ajaxgetdropmenu.html.php +++ b/module/branch/view/ajaxgetdropmenu.html.php @@ -13,7 +13,7 @@ foreach($branches as $branchID => $branch) if($branchID == 'all' or empty($branchID) or $statusList[$branchID] == 'active') { - $activeBranchesHtml .= html::a($linkHtml, $branch, '', "class='$selected' data-key='{$branchesPinyin[$branch]}' data-app='{$this->app->tab}'"); + $activeBranchesHtml .= html::a($linkHtml, $branch, '', "class='$selected' data-key='{$branchesPinyin[$branch]}'"); } else { diff --git a/module/bug/control.php b/module/bug/control.php index d2ddbfb24d..2f4ea11a6e 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -202,13 +202,10 @@ class bug extends control $showModule = !empty($this->config->datatable->bugBrowse->showModule) ? $this->config->datatable->bugBrowse->showModule : ''; $productName = ($productID and isset($this->products[$productID])) ? $this->products[$productID] : $this->lang->product->allProduct; + $product = $this->product->getById($productID); - $product = $this->product->getById($productID); - if($product and $product->type != 'normal') - { - $this->app->loadLang('datatable'); - $this->lang->datatable->showBranch = sprintf($this->lang->datatable->showBranch, $this->lang->product->branchName[$product->type]); - } + /* Display of branch label. */ + $showBranch = $this->loadModel('branch')->showBranch($productID); /* Set view. */ $this->view->title = $productName . $this->lang->colon . $this->lang->bug->common; @@ -240,6 +237,7 @@ class bug extends control $this->view->setModule = true; $this->view->isProjectBug = ($productID and !$this->projectID) ? false : true; $this->view->modulePairs = $showModule ? $this->tree->getModulePairs($productID, 'bug', $showModule) : array(); + $this->view->showBranch = $showBranch; $this->display(); } @@ -1080,7 +1078,7 @@ class bug extends control } } - $this->loadModel('my')->setMenu(); + $this->app->loadLang('my'); $this->lang->task->menu = $this->lang->my->menu->work; $this->lang->my->menu->work['subModule'] = 'bug'; diff --git a/module/bug/js/common.js b/module/bug/js/common.js index f0c1927b20..5f82fb6680 100644 --- a/module/bug/js/common.js +++ b/module/bug/js/common.js @@ -204,6 +204,7 @@ function loadProductModules(productID) branch = $('#branch').val(); if(typeof(branch) == 'undefined') branch = 0; if(typeof(moduleID) == 'undefined') moduleID = 0; + if(config.currentMethod == 'edit') moduleID = $('#module').val(); link = createLink('tree', 'ajaxGetOptionMenu', 'productID=' + productID + '&viewtype=bug&branch=' + branch + '&rootModuleID=0&returnType=html&fieldID=&needManage=true&extra=¤tModuleID=' + moduleID); $('#moduleIdBox').load(link, function() { @@ -626,7 +627,8 @@ function notice() */ function setBranchRelated(branchID, productID, num) { - moduleLink = createLink('tree', 'ajaxGetModules', 'productID=' + productID + '&viewType=bug&branch=' + branchID + '&num=' + num); + var currentModuleID = config.currentMethod == 'batchedit' ? $('#modules' + num).val() : 0; + moduleLink = createLink('tree', 'ajaxGetModules', 'productID=' + productID + '&viewType=bug&branch=' + branchID + '&num=' + num + '¤tModuleID=' + currentModuleID); $.get(moduleLink, function(modules) { if(!modules) modules = ''; diff --git a/module/bug/model.php b/module/bug/model.php index 1e6d6c2c1d..564756e296 100644 --- a/module/bug/model.php +++ b/module/bug/model.php @@ -570,7 +570,7 @@ class bugModel extends model ->andWhere('t1.deleted')->eq(0) ->orderBy('id desc') ->page($pager) - ->fetchAll(); + ->fetchAll('id'); } /** diff --git a/module/bug/view/view.html.php b/module/bug/view/view.html.php index bb5d11e559..bca8d23892 100644 --- a/module/bug/view/view.html.php +++ b/module/bug/view/view.html.php @@ -124,7 +124,7 @@ bug->product;?> - product", $product->name, '', "data-app='product'")) echo $product->name;?> + product", $product->name, '', "data-app='product'")) echo $product->name;?> type != 'normal'):?> diff --git a/module/build/control.php b/module/build/control.php index b21ca4419d..46550eaadc 100644 --- a/module/build/control.php +++ b/module/build/control.php @@ -613,12 +613,14 @@ class build extends control $this->config->bug->search['style'] = 'simple'; $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($build->product, $build->branch, 'skipParent'); $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($build->product, 'bug', 0, $build->branch); - $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($build->product, 0, 'id_desc', $this->session->project); + $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($build->product, $build->branch, 'id_desc', $this->session->project); $this->config->bug->search['params']['openedBuild']['values'] = $this->build->getProductBuildPairs($build->product, $branch = 0, $params = ''); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values']; unset($this->config->bug->search['fields']['product']); unset($this->config->bug->search['params']['product']); + unset($this->config->bug->search['fields']['project']); + unset($this->config->bug->search['params']['project']); if($product->type == 'normal') { unset($this->config->bug->search['fields']['branch']); @@ -626,9 +628,8 @@ class build extends control } else { - $branchPairs = $this->loadModel('branch')->getPairs($build->product, 'noempty'); - $branches = array('' => '') + array(BRANCH_MAIN => $this->lang->branch->main); - if($build->branch) $branches += array($build->branch => $branchPairs[$build->branch]); + $branchName = $this->loadModel('branch')->getById($build->branch); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $build->branch => $branchName); $this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); $this->config->bug->search['params']['branch']['values'] = $branches; diff --git a/module/build/lang/en.php b/module/build/lang/en.php index 20a3d4b1bd..70ccd580f8 100644 --- a/module/build/lang/en.php +++ b/module/build/lang/en.php @@ -31,6 +31,7 @@ $lang->build->id = 'ID'; $lang->build->product = $lang->productCommon; $lang->build->project = 'Project'; $lang->build->branch = 'Platform/Branch'; +$lang->build->branchName = '%s'; $lang->build->execution = $lang->executionCommon; $lang->build->name = 'Name'; $lang->build->date = 'Date'; diff --git a/module/build/lang/zh-cn.php b/module/build/lang/zh-cn.php index 2470d7e637..34647aec6d 100644 --- a/module/build/lang/zh-cn.php +++ b/module/build/lang/zh-cn.php @@ -31,6 +31,7 @@ $lang->build->id = 'ID'; $lang->build->product = $lang->productCommon; $lang->build->project = '所属项目'; $lang->build->branch = '平台/分支'; +$lang->build->branchName = '所属%s'; $lang->build->execution = '所属' . $lang->executionCommon; $lang->build->name = '名称编号'; $lang->build->date = '打包日期'; diff --git a/module/caselib/control.php b/module/caselib/control.php index 80dcf2fbd7..6fd78cb1fc 100644 --- a/module/caselib/control.php +++ b/module/caselib/control.php @@ -231,6 +231,7 @@ class caselib extends control $this->view->moduleName = $moduleID ? $this->tree->getById($moduleID)->name : $this->lang->tree->all; $this->view->param = $param; $this->view->setModule = true; + $this->view->showBranch = false; $this->display(); } diff --git a/module/ci/model.php b/module/ci/model.php index 9943a6fcaa..7d632d1519 100644 --- a/module/ci/model.php +++ b/module/ci/model.php @@ -156,6 +156,14 @@ class ciModel extends model $this->dao->update(TABLE_COMPILE)->data($data)->where('id')->eq($compile->id)->exec(); $this->dao->update(TABLE_JOB)->set('lastExec')->eq($now)->set('lastStatus')->eq($pipeline->status)->where('id')->eq($compile->job)->exec(); + + /* Send mr message by compile status. */ + $relateMR = $this->dao->select('*')->from(TABLE_MR)->where('compileID')->eq($compile->id)->fetch(); + if($relateMR) + { + if($data->status == 'success') $this->loadModel('action')->create('mr', $relateMR->id, 'compilePass'); + if($data->status == 'failed') $this->loadModel('action')->create('mr', $relateMR->id, 'compileFail'); + } } /** diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php index 0ad42439f1..de2e87f1dd 100644 --- a/module/common/lang/menu.php +++ b/module/common/lang/menu.php @@ -247,6 +247,7 @@ $lang->waterfall->menu->execution = array('link' => "{$lang->stage->common}|pr $lang->waterfall->menu->story = array('link' => "$lang->SRCommon|projectstory|story|project=%s", 'subModule' => 'projectstory,tree', 'exclude' => 'projectstory-track'); $lang->waterfall->menu->design = array('link' => "{$lang->design->common}|design|browse|project=%s"); $lang->waterfall->menu->qa = array('link' => "{$lang->qa->common}|project|bug|projectID=%s", 'subModule' => 'testcase,testtask,bug', 'alias' => 'bug,testtask,testcase'); +$lang->waterfall->menu->doc = array('link' => "{$lang->doc->common}|doc|tableContents|type=project&objectID=%s"); $lang->waterfall->menu->devops = array('link' => "{$lang->repo->common}|repo|browse|repoID=0&branchID=&objectID=%s", 'subModule' => 'repo'); $lang->waterfall->menu->build = array('link' => "{$lang->build->common}|project|build|project=%s"); $lang->waterfall->menu->release = array('link' => "{$lang->release->common}|projectrelease|browse|project=%s", 'subModule' => 'projectrelease'); diff --git a/module/common/model.php b/module/common/model.php index d6c7678326..85f7f9fd54 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -1845,13 +1845,18 @@ EOD; $typeOnlyCondition = $type . 'OnlyCondition'; $queryCondition = $this->session->$queryCondition; + $preAndNextObject = new stdClass(); + $preAndNextObject->pre = ''; + $preAndNextObject->next = ''; + if(empty($queryCondition)) return $preAndNextObject; + $table = $this->config->objectTables[$type]; $orderBy = $type . 'OrderBy'; $orderBy = $this->session->$orderBy; - if(empty($queryCondition) or $this->session->$typeOnlyCondition) + if($this->session->$typeOnlyCondition) { $sql = $this->dao->select('*')->from($table) - ->beginIF($queryCondition != false)->where($queryCondition)->fi() + ->where($queryCondition) ->beginIF($orderBy != false)->orderBy($orderBy)->fi() ->get(); } @@ -1879,10 +1884,6 @@ EOD; $existsObjectList = $this->session->$objectIdListKey; } - $preAndNextObject = new stdClass(); - $preAndNextObject->pre = ''; - $preAndNextObject->next = ''; - $preObj = false; if(isset($existsObjectList['objectList'])) { @@ -2613,6 +2614,90 @@ EOD; } } + + /** + * Http response with header. + * + * @param string $url + * @param string|array $data + * @param array $options This is option and value pair, like CURLOPT_HEADER => true. Use curl_setopt function to set options. + * @param array $headers Set request headers. + * @static + * @access public + * @return string + */ + public static function httpWithHeader($url, $data = null, $options = array(), $headers = array()) + { + global $lang, $app; + if(!extension_loaded('curl')) return json_encode(array('result' => 'fail', 'message' => $lang->error->noCurlExt)); + + commonModel::$requestErrors = array(); + + if(!is_array($headers)) $headers = (array)$headers; + $headers[] = "API-RemoteIP: " . zget($_SERVER, 'REMOTE_ADDR', ''); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + curl_setopt($curl, CURLOPT_USERAGENT, 'Sae T OAuth2 v0.1'); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 30); + curl_setopt($curl, CURLOPT_TIMEOUT, 30); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); + curl_setopt($curl, CURLOPT_ENCODING, ""); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLINFO_HEADER_OUT, TRUE); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_URL, $url); + + if(!empty($data)) + { + if(is_object($data)) $data = (array) $data; + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $data); + } + + if($options) curl_setopt_array($curl, $options); + $response = curl_exec($curl); + $errors = curl_error($curl); + + $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $headerString = substr($response, 0, $headerSize); + $body = substr($response, $headerSize); + + /* Parse header. */ + $header = explode("\n", $headerString); + $newHeader = array(); + foreach($header as $item) + { + $field = explode(':', $item); + if(count($field) < 2) continue; + $headerkey = array_shift($field); + $newHeader[$headerkey] = join('', $field); + } + curl_close($curl); + + + $logFile = $app->getLogRoot() . 'saas.'. date('Ymd') . '.log.php'; + if(!file_exists($logFile)) file_put_contents($logFile, ''); + + $fh = @fopen($logFile, 'a'); + if($fh) + { + fwrite($fh, date('Ymd H:i:s') . ": " . $app->getURI() . "\n"); + fwrite($fh, "url: " . $url . "\n"); + if(!empty($data)) fwrite($fh, "data: " . print_r($data, true) . "\n"); + fwrite($fh, "results:" . print_r($response, true) . "\n"); + if(!empty($errors)) fwrite($fh, "errors: " . $errors . "\n"); + fclose($fh); + } + + if($errors) commonModel::$requestErrors[] = $errors; + + return array('body' => $body, 'header' => $newHeader); + } + /** * Http. * diff --git a/module/common/view/datatable.fix.html.php b/module/common/view/datatable.fix.html.php index 33a4c28c46..e1bd84adf7 100644 --- a/module/common/view/datatable.fix.html.php +++ b/module/common/view/datatable.fix.html.php @@ -9,7 +9,7 @@ $(function() { - $('#sidebar .cell .text-center:last').append("tab == 'product' or $app->tab == 'qa') ? $lang->datatable->displaySetting : $lang->datatable->moduleSetting;?>
    "); + $('#sidebar .cell .text-center:last').append("datatable->displaySetting;?>
    "); var addSettingButton = function() @@ -89,7 +89,7 @@ $(function() + diff --git a/module/gitlab/view/editproject.html.php b/module/gitlab/view/editproject.html.php new file mode 100644 index 0000000000..b160c44bcd --- /dev/null +++ b/module/gitlab/view/editproject.html.php @@ -0,0 +1,50 @@ + + * @package gitlab + * @version $Id$ + * @link http://www.zentao.net + */ +?> + +
    +
    +
    +
    +

    gitlab->project->edit;?>

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    gitlab->project->id;?>id, "class='form-control' readonly placeholder='{$lang->gitlab->project->id}'");?>
    gitlab->project->name;?>name, "class='form-control' placeholder='{$lang->gitlab->project->name}'");?>
    gitlab->project->description;?>description, "rows='10' class='form-control' placeholder='{$lang->gitlab->project->description}'");?>
    gitlab->project->visibility;?>gitlab->project->visibilityList, $project->visibility, "", 'block'));?>
    + + goback, '', 'class="btn btn-wide"');?> +
    +
    +
    +
    +
    + diff --git a/module/gitlab/view/edituser.html.php b/module/gitlab/view/edituser.html.php new file mode 100644 index 0000000000..105585c933 --- /dev/null +++ b/module/gitlab/view/edituser.html.php @@ -0,0 +1,95 @@ + + * @package gitlab + * @version $Id$ + * @link http://www.zentao.net + */ +?> + +
    +
    +
    +
    +

    gitlab->user->edit;?>

    +
    +
    + id);?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    gitlab->user->bind;?>
    gitlab->user->name;?>name, "class='form-control' placeholder='{$lang->gitlab->user->name}'");?>
    gitlab->user->username;?>username, "readonly class='form-control' placeholder='{$lang->gitlab->user->username}'");?>
    gitlab->user->email;?>email, "class='form-control' placeholder='{$lang->gitlab->user->email}'");?>
    gitlab->user->password;?>gitlab->user->password}'");?>
    gitlab->user->passwordRepeat;?>gitlab->user->passwordRepeat}'");?>
    gitlab->user->canCreateGroup;?> +
    + can_create_group) echo 'checked';?> /> +
    +
    gitlab->user->external;?> +
    + external) echo 'checked';?> /> +
    +
    gitlab->user->avatar;?> +
    + $user->avatar_url, 'account'=>''), 50); ?> + + ', '', "class='btn-avatar' id='avatarUploadBtn' data-toggle='tooltip' data-container='body' data-placement='bottom' title='{$lang->gitlab->user->avatar}'");?> +
    +
    + + goback, '', 'class="btn btn-wide"');?> +
    +
    +
    +
    +
    + diff --git a/module/gitlab/view/managegroupmembers.html.php b/module/gitlab/view/managegroupmembers.html.php new file mode 100644 index 0000000000..9f75db25ba --- /dev/null +++ b/module/gitlab/view/managegroupmembers.html.php @@ -0,0 +1,82 @@ + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    gitlab->group->memberName;?>gitlab->group->memberAccessLevel;?>gitlab->group->memberExpiresAt;?>actions;?>
    name, "class='form-control' readonly");?>'') + $this->lang->gitlab->accessLevels, $member->access_level, "class='form-control chosen'");?> + expires_at, "class='form-control form-date'");?> + id);?> + + ", '', "onclick='addItem(this)' class='btn btn-link'");?> + ", '', "onclick='deleteItem(this)' class='btn btn-link'");?> +
    '') + $this->lang->gitlab->accessLevels, '', "class='form-control chosen'");?> + ", '', "onclick='addItem(this)' class='btn btn-link'");?> + ", '', "onclick='deleteItem(this)' class='btn btn-link'");?> +
    + save, 'onclick="saveMembers()" id="saveBtn"', 'btn btn-wide btn-primary'); + + echo html::backButton(); + ?> +
    + +
    +
    +
    + + + + + + + + + +
    + diff --git a/module/gitlab/view/manageprojectmembers.html.php b/module/gitlab/view/manageprojectmembers.html.php new file mode 100644 index 0000000000..123d20e2f6 --- /dev/null +++ b/module/gitlab/view/manageprojectmembers.html.php @@ -0,0 +1,83 @@ + + +
    +
    + + + + + + + + + + + + acl->users)):?> + acl->users as $user):?> + + + + + + + + + + + + + + + + + + + + + + + + + +
    gitlab->group->memberName;?>gitlab->group->memberAccessLevel;?>gitlab->group->memberExpiresAt;?>actions;?>
    '') + $this->lang->gitlab->accessLevels, isset($userAccessData[$user]) ? $userAccessData[$user]->access_level : '', "class='form-control chosen'");?> + expires_at : '', "class='form-control form-date'");?> + + + ", '', "onclick='addItem(this)' class='btn btn-link'");?> + ", '', "onclick='deleteItem(this)' class='btn btn-link'");?> +
    '') +$users, '', "class='form-control chosen'");?>'') + $this->lang->gitlab->accessLevels, '', "class='form-control chosen'");?> + ", '', "onclick='addItem(this)' class='btn btn-link'");?> + ", '', "onclick='deleteItem(this)' class='btn btn-link'");?> +
    + save, 'onclick="saveMembers()" id="saveBtn"', 'btn btn-wide btn-primary'); + + echo html::backButton(); + ?> +
    + +
    +
    +
    + + + + + + + + + +
    + diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index 26caa1ae42..0dcf165106 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -1226,34 +1226,73 @@ $lang->svn->methodOrder[15] = 'apiSync'; /* GitLab. */ $lang->resource->gitlab = new stdclass(); -$lang->resource->gitlab->browse = 'browse'; -$lang->resource->gitlab->create = 'create'; -$lang->resource->gitlab->edit = 'edit'; -$lang->resource->gitlab->view = 'view'; -$lang->resource->gitlab->importIssue = 'importIssue'; -$lang->resource->gitlab->delete = 'delete'; -$lang->resource->gitlab->bindUser = 'bindUser'; -$lang->resource->gitlab->bindProduct = 'bindProduct'; +$lang->resource->gitlab->browse = 'browse'; +$lang->resource->gitlab->create = 'create'; +$lang->resource->gitlab->edit = 'edit'; +$lang->resource->gitlab->view = 'view'; +$lang->resource->gitlab->importIssue = 'importIssue'; +$lang->resource->gitlab->delete = 'delete'; +$lang->resource->gitlab->bindUser = 'bindUser'; +$lang->resource->gitlab->bindProduct = 'bindProduct'; +$lang->resource->gitlab->browseProject = 'browseProject'; +$lang->resource->gitlab->createProject = 'createProject'; +$lang->resource->gitlab->editProject = 'editProject'; +$lang->resource->gitlab->deleteProject = 'deleteProject'; +$lang->resource->gitlab->browseGroup = 'browseGroup'; +$lang->resource->gitlab->createGroup = 'createGroup'; +$lang->resource->gitlab->editGroup = 'editGroup'; +$lang->resource->gitlab->deleteGroup = 'deleteGroup'; +$lang->resource->gitlab->manageGroupMembers = 'manageGroupMembers'; +$lang->resource->gitlab->browseUser = 'browseUser'; +$lang->resource->gitlab->createUser = 'createUser'; +$lang->resource->gitlab->editUser = 'editUser'; +$lang->resource->gitlab->deleteUser = 'deleteUser'; +$lang->resource->gitlab->webhook = 'webhook'; +$lang->resource->gitlab->createWebhook = 'createWebhook'; +$lang->resource->gitlab->manageProjectMembers = 'manageProjectMembers'; -//$lang->resource->gitlab->webhook = 'webhook'; - -$lang->gitlab->methodOrder[5] = 'browse'; -$lang->gitlab->methodOrder[10] = 'create'; -$lang->gitlab->methodOrder[15] = 'edit'; -$lang->gitlab->methodOrder[20] = 'view'; -$lang->gitlab->methodOrder[25] = 'importIssue'; -$lang->gitlab->methodOrder[30] = 'delete'; -$lang->gitlab->methodOrder[35] = 'bindUser'; -//$lang->gitlab->methodOrder[45] = 'webhook'; +$lang->gitlab->methodOrder[5] = 'browse'; +$lang->gitlab->methodOrder[10] = 'create'; +$lang->gitlab->methodOrder[15] = 'edit'; +$lang->gitlab->methodOrder[20] = 'view'; +$lang->gitlab->methodOrder[25] = 'importIssue'; +$lang->gitlab->methodOrder[30] = 'delete'; +$lang->gitlab->methodOrder[35] = 'bindUser'; +$lang->gitlab->methodOrder[45] = 'browseProject'; +$lang->gitlab->methodOrder[50] = 'createProject'; +$lang->gitlab->methodOrder[55] = 'editProject'; +$lang->gitlab->methodOrder[60] = 'deleteProject'; +$lang->gitlab->methodOrder[65] = 'browseGroup'; +$lang->gitlab->methodOrder[70] = 'createGroup'; +$lang->gitlab->methodOrder[75] = 'editGroup'; +$lang->gitlab->methodOrder[80] = 'deleteGroup'; +$lang->gitlab->methodOrder[85] = 'manageGroupMembers'; +$lang->gitlab->methodOrder[90] = 'browseUser'; +$lang->gitlab->methodOrder[95] = 'createUser'; +$lang->gitlab->methodOrder[100] = 'editUser'; +$lang->gitlab->methodOrder[105] = 'deleteUser'; +$lang->gitlab->methodOrder[110] = 'webhook'; +$lang->gitlab->methodOrder[115] = 'createWebhook'; +$lang->gitlab->methodOrder[120] = 'manageProjectMembers'; /* merge request. */ $lang->resource->mr = new stdclass(); -$lang->resource->mr->create = 'create'; -$lang->resource->mr->browse = 'browse'; -$lang->resource->mr->edit = 'edit'; -$lang->resource->mr->delete = 'delete'; -$lang->resource->mr->view = 'view'; -$lang->resource->mr->accept = 'accept'; +$lang->resource->mr->create = 'create'; +$lang->resource->mr->browse = 'browse'; +$lang->resource->mr->edit = 'edit'; +$lang->resource->mr->delete = 'delete'; +$lang->resource->mr->view = 'view'; +$lang->resource->mr->accept = 'accept'; +$lang->resource->mr->diff = 'viewDiff'; +$lang->resource->mr->link = 'linkList'; +$lang->resource->mr->linkStory = 'linkStory'; +$lang->resource->mr->linkBug = 'linkBug'; +$lang->resource->mr->linkTask = 'linkTask'; +$lang->resource->mr->unlink = 'unlink'; +$lang->resource->mr->approval = 'approval'; +$lang->resource->mr->close = 'close'; +$lang->resource->mr->reopen = 'reopen'; +$lang->resource->mr->addBug = 'addBug'; $lang->mr->methodOrder[10] = 'create'; $lang->mr->methodOrder[15] = 'browse'; @@ -1261,6 +1300,16 @@ $lang->mr->methodOrder[20] = 'edit'; $lang->mr->methodOrder[25] = 'delete'; $lang->mr->methodOrder[35] = 'view'; $lang->mr->methodOrder[45] = 'accept'; +$lang->mr->methodOrder[50] = 'diff'; +$lang->mr->methodOrder[55] = 'link'; +$lang->mr->methodOrder[60] = 'linkStory'; +$lang->mr->methodOrder[65] = 'linkBug'; +$lang->mr->methodOrder[70] = 'linkTask'; +$lang->mr->methodOrder[75] = 'unlink'; +$lang->mr->methodOrder[80] = 'approval'; +$lang->mr->methodOrder[85] = 'close'; +$lang->mr->methodOrder[90] = 'reopen'; +$lang->mr->methodOrder[95] = 'addBug'; /* Git. */ $lang->resource->git = new stdclass(); diff --git a/module/group/view/privbygroup.html.php b/module/group/view/privbygroup.html.php index f1b357891f..d3ed83af4d 100644 --- a/module/group/view/privbygroup.html.php +++ b/module/group/view/privbygroup.html.php @@ -115,7 +115,7 @@ $moduleName->menus) and $action == 'browse') continue;;?>
    - $lang->$moduleName->$actionLabel), isset($groupPrivs[$moduleName][$action]) ? $action : '', '', 'inline');?> + $lang->$moduleName->$actionLabel), isset($groupPrivs[$moduleName][$action]) ? $action : '', "title='{$lang->$moduleName->$actionLabel}'", 'inline');?>
    diff --git a/module/group/view/privbymodule.html.php b/module/group/view/privbymodule.html.php index 77f6009d24..2f2dad1269 100644 --- a/module/group/view/privbymodule.html.php +++ b/module/group/view/privbymodule.html.php @@ -17,7 +17,7 @@ group->byModuleTips; ?> - +
    diff --git a/module/job/control.php b/module/job/control.php index a206bac9f7..f4ba69c1dc 100644 --- a/module/job/control.php +++ b/module/job/control.php @@ -69,7 +69,7 @@ class job extends control $errors = dao::getError(); if($this->post->engine == 'gitlab' and isset($errors['server'])) { - $errors['gitlabRepo'][] = sprintf($this->lang->error->notempty, $this->lang->job->repo); + if(!isset($errors['repo'])) $errors['repo'][] = sprintf($this->lang->error->notempty, $this->lang->job->repoServer); unset($errors['server']); unset($errors['pipeline']); } @@ -105,8 +105,8 @@ class job extends control $repoTypes[$repo->id] = $repo->SCM; if(strtolower($repo->SCM) == 'gitlab') { - $gitlab = $this->loadModel('gitlab')->getByID($repo->gitlab); - $tokenUser = $this->gitlab->apiGetCurrentUser($gitlab->url, $gitlab->token); + if(isset($repo->gitlab)) $gitlab = $this->loadModel('gitlab')->getByID($repo->gitlab); + if(!empty($gitlab)) $tokenUser = $this->gitlab->apiGetCurrentUser($gitlab->url, $gitlab->token); if(!isset($tokenUser->is_admin) or !$tokenUser->is_admin) continue; $gitlabRepos[$repo->id] = $repo->name; } @@ -369,8 +369,49 @@ class job extends control public function ajaxGetRefList($repoID) { $repo = $this->loadModel('repo')->getRepoByID($repoID); - if($repo->SCM != 'Gitlab') $this->send(array('result' => 'fail')); - $refList = $this->loadModel('gitlab')->getReferenceOptions($repo->gitlab, $repo->project); + if($repo->SCM == 'Gitlab') $refList = $this->loadModel('gitlab')->getReferenceOptions($repo->gitlab, $repo->project); + if($repo->SCM != 'Gitlab') $refList = $this->repo->getBranches($repo, true); $this->send(array('result' => 'success', 'refList' => $refList)); } + + /** + * Ajax get repo list. + * + * @param int $engine + * @access public + * @return void + */ + public function ajaxGetRepoList($engine) + { + $repoList = $this->loadModel('repo')->getList($this->projectID); + $repoPairs = array(0 => ''); + foreach($repoList as $repo) + { + if(empty($repo->synced)) continue; + if($engine == 'gitlab') + { + if(strtolower($repo->SCM) == 'gitlab') $repoPairs[$repo->id] = $repo->name; + } + else + { + $repoPairs[$repo->id] = $repo->name; + } + } + echo html::select('repo', $repoPairs, '', "class='form-control chosen'"); + die(); + } + + /** + * Ajax get an repo type. + * + * @param int $repoID + * @access public + * @return void + */ + public function ajaxGetRepoType($repoID) + { + $repo = $this->loadModel('repo')->getRepoByID($repoID); + $this->send(array('result' => 'success', 'type' => strtolower($repo->SCM))); + } + } diff --git a/module/job/js/common.js b/module/job/js/common.js index 475b78e062..872d9d8568 100644 --- a/module/job/js/common.js +++ b/module/job/js/common.js @@ -64,3 +64,17 @@ function setValueInput(obj) $(obj).closest('.input-group').find('select').removeAttr('disabled'); } } + +function loadRepoList(engine = '') +{ + var link = createLink('job', 'ajaxGetRepoList', 'engine=' + engine); + $.get(link, function(data) + { + if(data) + { + $('#repo').replaceWith(data) + $('#repo_chosen').remove(); + $('#repo').chosen(); + } + }); +} diff --git a/module/job/js/create.js b/module/job/js/create.js index 18b121ba44..635ce8abc2 100644 --- a/module/job/js/create.js +++ b/module/job/js/create.js @@ -1,13 +1,10 @@ $(document).ready(function() { - $('#gitlabRepo').change(function() - { - $('#repo').val($(this).val()).change(); - }) - - $('#repo').change(function() + $(document).on('change', '#repo', function() { var repoID = $(this).val(); + if(repoID <= 0) return; + var link = createLink('repo', 'ajaxLoadProducts', 'repoID=' + repoID); $.get(link, function(data) { @@ -19,33 +16,54 @@ $(document).ready(function() } }); - var type = 'Git'; - if(typeof(repoTypes[repoID]) != 'undefined') type = repoTypes[repoID]; - - $('.svn-fields').addClass('hidden'); - if(type == 'Subversion' && $('#triggerType').val() == 'tag') $('.svn-fields').removeClass('hidden'); - - $('#repoType').val(type); - $('#triggerType option[value=tag]').html(type == 'Subversion' ? dirChange : buildTag).trigger('chosen:updated'); - if(type == 'Subversion') + /* Add new way get repo type. */ + var link = createLink('job', 'ajaxGetRepoType', 'repoID=' + repoID); + $.getJSON(link, function(data) { - $('#svnDirBox .input-group').empty(); - $('#svnDirBox .input-group').append("
    "); - $.getJSON(createLink('repo', 'ajaxGetSVNDirs', 'repoID=' + repoID), function(tags) + if(data.result == 'success') { - html = "'; - $('#svnDirBox .loading').remove(); - $('#svnDirBox .input-group').append(html); - $('#svnDirBox #svnDir').chosen(); - }) - } - }) + else + { + if($('#triggerType').val() == 'tag') $('.svn-fields').removeClass('hidden'); + + $('#svnDirBox .input-group').empty(); + $('#svnDirBox .input-group').append("
    "); + $.getJSON(createLink('repo', 'ajaxGetSVNDirs', 'repoID=' + repoID), function(tags) + { + html = "'; + $('#svnDirBox .loading').remove(); + $('#svnDirBox .input-group').append(html); + $('#svnDirBox #svnDir').chosen(); + }) + } + $('#triggerggerType option[value=tag]').html(data.type == 'gitlab' ? buildTag : dirChange).trigger('chosen:updated'); + } + }); + }); $(document).on('change', '[name^=svnDir]', function() { @@ -61,7 +79,8 @@ $(document).ready(function() { html = ''; length = $('#svnDirBox .input-group [name^=svnDir]').length; - length += 1; + length++; + if(tags.length != 0) { html = "'; } + $('#svnDirBox .loading').remove(); $('#svnDirBox .input-group').append(html); $('#svnDirBox #svnDir' + length).chosen(); @@ -124,15 +144,19 @@ $(document).ready(function() var scheduleOption = ""; $('#engine').change(function() { - $('#jenkinsServerTR').toggle($('#engine').val() == 'jenkins'); - $('#gitlabServerTR').toggle($('#engine').val() == 'gitlab'); + var engine = $(this).val(); + $('.reference').hide(); + loadRepoList(engine); + $('#jenkinsServerTR').toggle(engine == 'jenkins'); + $('#gitlabServerTR').toggle(engine == 'gitlab'); - if($(this).val() == 'gitlab') + if(engine == 'gitlab') { $('tr.gitlabRepo').show(); $('tr.commonRepo').hide(); } - else if($('#triggerType').find('[value=schedule]').size() == 0 ) + //else if($('#triggerType').find('[value=schedule]').size() == 0) + else { $('tr.gitlabRepo').hide(); $('tr.commonRepo').show(); @@ -140,26 +164,6 @@ $(document).ready(function() }); $('#engine').change(); - $('#gitlabRepo').change(function() - { - $('#reference option').remove(); - - var repoID = $(this).val(); - if(repoID > 0) - { - $.getJSON(createLink('job', 'ajaxGetRefList', "repoID=" + repoID), function(response) - { - if(response.result == 'success') - { - $.each(response.refList, function(reference, name) - { - $('#reference').append(""); - }); - } - $('#reference').trigger('chosen:updated'); - }); - } - }); $('#triggerType').change(); }); diff --git a/module/job/lang/en.php b/module/job/lang/en.php index 17c4b011a6..f2353087d6 100644 --- a/module/job/lang/en.php +++ b/module/job/lang/en.php @@ -44,6 +44,7 @@ $lang->job->editedBy = 'Edited By'; $lang->job->editedDate = 'Edited Date'; $lang->job->lastTag = 'Last Tag'; $lang->job->deleted = 'Deleted'; +$lang->job->repoServer = 'Repo Server'; $lang->job->lblBasic = 'Basic Info'; diff --git a/module/job/lang/zh-cn.php b/module/job/lang/zh-cn.php index efcca21100..780ec84159 100644 --- a/module/job/lang/zh-cn.php +++ b/module/job/lang/zh-cn.php @@ -44,6 +44,7 @@ $lang->job->editedBy = '由谁编辑'; $lang->job->editedDate = '编辑日期'; $lang->job->lastTag = '最后标签'; $lang->job->deleted = '已删除'; +$lang->job->repoServer = '版本库服务器'; $lang->job->lblBasic = '基本信息'; diff --git a/module/job/model.php b/module/job/model.php index c9344e89a4..72514a4d49 100644 --- a/module/job/model.php +++ b/module/job/model.php @@ -50,7 +50,23 @@ class jobModel extends model ->fetchAll('id'); } - /** + /** + * Get job list by RepoID. + * + * @param int $repoID + * @access public + * @return array + */ + public function getListByRepoID($repoID) + { + return $this->dao->select('id, name, lastStatus')->from(TABLE_JOB) + ->where('deleted')->eq('0') + ->andWhere('repo')->eq($repoID) + ->orderBy('id_desc') + ->fetchAll('id'); + } + + /** * Get list by triggerType field. * * @param string $triggerType @@ -136,10 +152,9 @@ class jobModel extends model if(strtolower($job->engine) == 'gitlab') { - $repo = $this->loadModel('repo')->getRepoByID($job->gitlabRepo); + $repo = $this->loadModel('repo')->getRepoByID($job->repo); $project = zget($repo, 'project'); - $job->repo = $job->gitlabRepo; $job->server = (int)zget($repo, 'gitlab', 0); $job->pipeline = json_encode(array('project' => $project, 'reference' => $this->post->reference)); } @@ -183,7 +198,6 @@ class jobModel extends model $this->dao->insert(TABLE_JOB)->data($job) ->batchCheck($this->config->job->create->requiredFields, 'notempty') - ->batchCheckIF($job->triggerType === 'schedule', "atDay,atTime", 'notempty') ->batchCheckIF($job->triggerType === 'commit', "comment", 'notempty') ->batchCheckIF(($this->post->repoType == 'Subversion' and $job->triggerType == 'tag'), "svnDir", 'notempty') diff --git a/module/job/view/create.html.php b/module/job/view/create.html.php index 1822bcbe2d..ac885915c0 100644 --- a/module/job/view/create.html.php +++ b/module/job/view/create.html.php @@ -42,14 +42,10 @@ job->engineTips->success;?> - + - - - - - - + + diff --git a/module/mail/model.php b/module/mail/model.php index c26ae43810..ad23d82522 100644 --- a/module/mail/model.php +++ b/module/mail/model.php @@ -730,7 +730,7 @@ class mailModel extends model chdir($modulePath . 'ext/view'); } ob_start(); - include $viewFile; + if($objectType != 'mr') include $viewFile; foreach(glob($modulePath . 'ext/view/sendmail.*.html.hook.php') as $hookFile) include $hookFile; $mailContent = ob_get_contents(); ob_end_clean(); @@ -754,7 +754,30 @@ class mailModel extends model list($toList, $ccList) = $sendUsers; /* Send it. */ - $this->send($toList, $subject, $mailContent, $ccList); + if($objectType == 'mr') + { + $MRLink = common::getSysURL() . helper::createLink('mr', 'view', "id={$object->id}"); + if($action->action == 'compilepass') + { + $mailContent = sprintf($this->lang->mr->toCreatedMessage, $MRLink, $title); + $this->send($toList, $subject, $mailContent); + + $mailContent = sprintf($this->lang->mr->toReviewerMessage, $MRLink, $title); + $this->send($ccList, $subject, $mailContent); + + /* Create a todo item for this MR. */ + $this->loadModel('mr')->apiCreateMRTodo($object->gitlabID, $object->targetProject, $object->mriid); + } + elseif($action->action == 'compilefail') + { + $mailContent = sprintf($this->lang->mr->failMessage, $MRLink, $title); + $this->send($toList, $subject, $mailContent, $ccList); + } + } + else + { + $this->send($toList, $subject, $mailContent, $ccList); + } if($this->isError()) error_log(join("\n", $this->getError())); } diff --git a/module/message/lang/en.php b/module/message/lang/en.php index 88fa9bf34e..c584d5a185 100644 --- a/module/message/lang/en.php +++ b/module/message/lang/en.php @@ -17,3 +17,30 @@ $lang->message->browserSetting->pollTimePlaceholder = 'Notify the time intervals $lang->message->browserSetting->turnonList[1] = 'On'; $lang->message->browserSetting->turnonList[0] = 'Off'; + +$lang->message->label = new stdclass(); +$lang->message->label->created = 'create'; +$lang->message->label->opened = 'open'; +$lang->message->label->changed = 'change'; +$lang->message->label->edited = 'edit'; +$lang->message->label->assigned = 'assign'; +$lang->message->label->closed = 'close'; +$lang->message->label->deleted = 'delete'; +$lang->message->label->undeleted = 'restore'; +$lang->message->label->commented = 'comment'; +$lang->message->label->activated = 'activate'; +$lang->message->label->resolved = 'resolve'; +$lang->message->label->reviewed = 'review'; +$lang->message->label->confirmed = 'confirm Story'; +$lang->message->label->frombug = 'convert from Bug'; +$lang->message->label->started = 'start'; +$lang->message->label->delayed = 'delay'; +$lang->message->label->suspended = 'suspend'; +$lang->message->label->finished = 'finish'; +$lang->message->label->paused = 'pause'; +$lang->message->label->canceled = 'cancel'; +$lang->message->label->restarted = 'continue'; +$lang->message->label->blocked = 'block'; +$lang->message->label->bugconfirmed = 'confirm'; +$lang->message->label->compilepass = 'compile pass'; +$lang->message->label->compilefail = 'compile fail'; diff --git a/module/message/lang/zh-cn.php b/module/message/lang/zh-cn.php index 600d5ff70c..1e1610ab6b 100644 --- a/module/message/lang/zh-cn.php +++ b/module/message/lang/zh-cn.php @@ -17,3 +17,30 @@ $lang->message->browserSetting->pollTimePlaceholder = '通知的时间间隔, $lang->message->browserSetting->turnonList[1] = '打开'; $lang->message->browserSetting->turnonList[0] = '关闭'; + +$lang->message->label = new stdclass(); +$lang->message->label->created = '创建'; +$lang->message->label->opened = '创建'; +$lang->message->label->changed = '变更'; +$lang->message->label->edited = '编辑'; +$lang->message->label->assigned = '指派'; +$lang->message->label->closed = '关闭'; +$lang->message->label->deleted = '删除'; +$lang->message->label->undeleted = '还原'; +$lang->message->label->commented = '评论'; +$lang->message->label->activated = '激活'; +$lang->message->label->resolved = '解决'; +$lang->message->label->reviewed = '评审'; +$lang->message->label->confirmed = "确认{$lang->SRCommon}"; +$lang->message->label->frombug = "转{$lang->SRCommon}"; +$lang->message->label->started = '开始'; +$lang->message->label->delayed = '延期'; +$lang->message->label->suspended = '挂起'; +$lang->message->label->finished = '完成'; +$lang->message->label->paused = '暂停'; +$lang->message->label->canceled = '取消'; +$lang->message->label->restarted = '继续'; +$lang->message->label->blocked = '阻塞'; +$lang->message->label->bugconfirmed = '确认'; +$lang->message->label->compilepass = '构建通过'; +$lang->message->label->compilefail = '构建失败'; diff --git a/module/message/model.php b/module/message/model.php index bae3422d26..1a6df4425b 100644 --- a/module/message/model.php +++ b/module/message/model.php @@ -163,6 +163,7 @@ class messageModel extends model if(empty($toList) and $objectType == 'todo') $toList = $object->account; if(empty($toList) and $objectType == 'testtask') $toList = $object->owner; if(empty($toList) and $objectType == 'meeting') $toList = $object->host . $object->participant; + if(empty($toList) and $objectType == 'mr') $toList = $object->createdBy . ',' . $object->assignee; if(empty($toList) and $objectType == 'release') { /* Get notifiy persons. */ diff --git a/module/mr/config.php b/module/mr/config.php index 527a279eba..15d15a4851 100644 --- a/module/mr/config.php +++ b/module/mr/config.php @@ -2,11 +2,15 @@ $config->mr = new stdclass(); $config->mr->create = new stdclass(); -$config->mr->create->skippedFields = 'projectID'; -$config->mr->create->requiredFields = 'gitlabID,sourceProject,sourceBranch,targetProject,targetBranch,title'; +$config->mr->create->skippedFields = 'projectID,compile'; +$config->mr->create->requiredFields = 'gitlabID,sourceProject,sourceBranch,targetProject,targetBranch,title,repoID'; $config->mr->edit = new stdclass; -$config->mr->edit->requiredFields = 'gitlabID,sourceProject,sourceBranch,targetProject,targetBranch,title'; +$config->mr->edit->skippedFields = 'projectID,compile'; +$config->mr->edit->requiredFields = 'gitlabID,sourceProject,sourceBranch,targetProject,targetBranch,title,repoID'; + +$config->mr->editor = new stdclass(); +$config->mr->editor->diff = array('id' => 'commentText', 'tools' => 'simpleTools'); $config->mr->maps = new stdclass; $config->mr->maps->sync = array(); @@ -20,3 +24,8 @@ $config->mr->maps->sync['sourceProject'] = 'source_project_id|field|'; $config->mr->maps->sync['targetProject'] = 'target_project_id|field|'; $config->mr->maps->sync['status'] = 'state|field|'; $config->mr->maps->sync['mergeStatus'] = 'merge_status|field|'; + +$config->mrapproval = new stdclass(); +$config->mrapproval->create = new stdclass(); +$config->mrapproval->create->skippedFields = ''; +$config->mrapproval->create->requiredFields = 'mrID,account,date,action'; diff --git a/module/mr/control.php b/module/mr/control.php index 6b05a0b1f3..e6dd5b26bb 100644 --- a/module/mr/control.php +++ b/module/mr/control.php @@ -18,6 +18,8 @@ class mr extends control /** * Browse mr. * + * @param string $mode + * @param string $param * @param int $objectID * @param string $orderBy * @param int $recTotal @@ -26,11 +28,11 @@ class mr extends control * @access public * @return void */ - public function browse($objectID = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1) + public function browse($mode = 'all', $param = 'all', $objectID = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1) { $this->app->loadClass('pager', $static = true); $pager = new pager($recTotal, $recPerPage, $pageID); - $MRList = $this->mr->getList($orderBy, $pager); + $MRList = $this->mr->getList($mode, $param, $orderBy, $pager); /* Save current URI to session. */ $this->session->set('mrList', $this->app->getURI(true), 'repo'); @@ -38,11 +40,24 @@ class mr extends control /* Sync GitLab MR to ZenTao Database. */ $MRList = $this->mr->batchSyncMR($MRList); - $this->view->title = $this->lang->mr->common . $this->lang->colon . $this->lang->mr->browse; - $this->view->MRList = $MRList; - $this->view->orderBy = $orderBy; - $this->view->objectID = $objectID; - $this->view->pager = $pager; + /* Check whether Mr is linked with the product. */ + $this->loadModel('gitlab'); + foreach($MRList as $MR) + { + $product = $this->mr->getMRProduct($MR); + $MR->linkButton = empty($product) ? false : true; + } + + /* Load lang from compile module */ + $this->app->loadLang('compile'); + + $this->view->title = $this->lang->mr->common . $this->lang->colon . $this->lang->mr->browse; + $this->view->MRList = $MRList; + $this->view->pager = $pager; + $this->view->mode = $mode; + $this->view->param = $param; + $this->view->objectID = $objectID; + $this->view->orderBy = $orderBy; $this->display(); } @@ -60,7 +75,11 @@ class mr extends control return $this->send($result); } + $this->app->loadLang('repo'); /* Import lang in repo module. */ + $this->app->loadLang('compile'); $this->view->title = $this->lang->mr->create; + $this->view->users = $this->loadModel('user')->getPairs('noletter|noclosed'); + $this->view->jobList = $this->loadModel('job')->getList(); $this->view->gitlabHosts = $this->loadModel('gitlab')->getPairs(); $this->display(); } @@ -80,6 +99,11 @@ class mr extends control } $MR = $this->mr->getByID($MRID); + if(isset($MR->gitlabID)) $rawMR = $this->mr->apiGetSingleMR($MR->gitlabID, $MR->targetProject, $MR->mriid); + $this->view->title = $this->lang->mr->edit; + $this->view->MR = $MR; + $this->view->rawMR = isset($rawMR) ? $rawMR : false; + if(!isset($rawMR->id) or (isset($rawMR->message) and $rawMR->message == '404 Not found') or empty($rawMR)) return $this->display(); $branchList = $this->loadModel('gitlab')->getBranches($MR->gitlabID, $MR->targetProject); $targetBranchList = array(); @@ -97,11 +121,32 @@ class mr extends control $gitlabUsers = $this->gitlab->getUserAccountIdPairs($MR->gitlabID); + /* Import lang for required modules. */ + $this->loadModel('repo'); + $this->loadModel('job'); + $this->loadModel('compile'); + + $repoList = array(); + $rawRepoList = $this->repo->getGitLabRepoList($MR->gitlabID, $MR->sourceProject); + foreach($rawRepoList as $rawRepo) $repoList[$rawRepo->id] = "[$rawRepo->id] $rawRepo->name"; + + $jobList = array(); + $rawJobList = $this->job->getListByRepoID($MR->repoID); + foreach($rawJobList as $rawJob) $jobList[$rawJob->id] = "[$rawJob->id] $rawJob->name"; + + $compileList = array(); + $rawCompileList = $this->compile->getListByJobID($MR->jobID); + foreach($rawCompileList as $rawCompile) $compileList[$rawCompile->id] = "[$rawCompile->id] [{$this->lang->compile->statusList[$rawCompile->status]}] $rawCompile->name"; + + $this->view->repoList = $repoList; + $this->view->jobList = !empty($MR->repoID) ? $jobList : array(); + $this->view->compileList = !empty($MR->jobID) ? $compileList : array(); + $this->view->title = $this->lang->mr->edit; $this->view->MR = $MR; $this->view->targetBranchList = $targetBranchList; - $this->view->users = array("" => "") + $users; - $this->view->assignee = zget($gitlabUsers, $MR->assignee, ''); + $this->view->users = $this->loadModel('user')->getPairs('noletter|noclosed'); + $this->view->assignee = $MR->assignee; $this->view->reviewer = zget($gitlabUsers, $MR->reviewer, ''); $this->display(); @@ -114,9 +159,9 @@ class mr extends control * @access public * @return void */ - public function delete($id, $confim = 'no') + public function delete($id, $confirm = 'no') { - if($confim != 'yes') die(js::confirm($this->lang->mr->confirmDelete, inlink('delete', "id=$id&confirm=yes"))); + if($confirm != 'yes') die(js::confirm($this->lang->mr->confirmDelete, inlink('delete', "id=$id&confirm=yes"))); $MR = $this->mr->getByID($id); @@ -129,18 +174,18 @@ class mr extends control /** * View a MR. * + * @param int $id * @access public * @return void */ public function view($id) { $MR = $this->mr->getByID($id); + if(!$MR) die(js::error($this->lang->notFound) . js::locate($this->createLink('mr', 'browse'))); if(isset($MR->gitlabID)) $rawMR = $this->mr->apiGetSingleMR($MR->gitlabID, $MR->targetProject, $MR->mriid); + if(!isset($rawMR->id) or (isset($rawMR->message) and $rawMR->message == '404 Not found') or empty($rawMR)) return $this->display(); - $this->view->title = $this->lang->mr->view; - $this->view->MR = $MR; - $this->view->rawMR = isset($rawMR) ? $rawMR : false; - + $MR = $this->mr->apiSyncMR($MR); /* Sync MR from GitLab to ZentaoPMS. */ $this->loadModel('gitlab'); $sourceProject = $this->gitlab->apiGetSingleProject($MR->gitlabID, $MR->sourceProject); $targetProject = $this->gitlab->apiGetSingleProject($MR->gitlabID, $MR->targetProject); @@ -149,13 +194,28 @@ class mr extends control $this->view->sourceProjectName = $sourceProject->name_with_namespace; $this->view->targetProjectName = $targetProject->name_with_namespace; - $this->view->sourceProjectURL = $sourceBranch ->web_url; - $this->view->targetProjectURL = $targetBranch ->web_url; + $this->view->sourceProjectURL = isset($sourceBranch->web_url) ? $sourceBranch->web_url : ''; + $this->view->targetProjectURL = isset($targetBranch->web_url) ? $targetBranch->web_url : ''; /* Those variables are used to render $lang->mr->commandDocument. */ $this->view->httpRepoURL = $sourceProject->http_url_to_repo; $this->view->branchPath = $sourceProject->path_with_namespace . '-' . $rawMR->source_branch; + /* Get mr linked list. */ + $this->app->loadLang('productplan'); + $product = $this->mr->getMRProduct($MR); + + $this->view->compile = $this->loadModel('compile')->getById($MR->compileID); + $this->view->compileJob = $MR->jobID ? $this->loadModel('job')->getById($MR->jobID) : false; + + $this->view->title = $this->lang->mr->view; + $this->view->MR = $MR; + $this->view->rawMR = isset($rawMR) ? $rawMR : false; + $this->view->product = $product; + $this->view->stories = $this->mr->getLinkList($MR->id, $product->id, 'story'); + $this->view->bugs = $this->mr->getLinkList($MR->id, $product->id, 'bug'); + $this->view->tasks = $this->mr->getLinkList($MR->id, $product->id, 'task'); + $this->display(); } @@ -190,19 +250,41 @@ class mr extends control { $MR = $this->mr->getByID($MRID); + /* Judge that if this MR can be accepted. */ + if(isset($MR->needCI) and $MR->needCI == '1') + { + $compileStatus = empty($MR->compileID) ? 'fail' : $this->loadModel('compile')->getByID($MR->compileID)->status; + + if(isset($compileStatus) and $compileStatus != 'success') + { + return $this->send(array('result' => 'fail', 'message' => $this->lang->mr->needCI, 'locate' => helper::createLink('mr', 'view', "mr={$MRID}"))); + } + } + if(isset($MR->needApproved) and $MR->needApproved == '1') + { + if($MR->approvalStatus != 'approved') + { + return $this->send(array('result' => 'fail', 'message' => $this->lang->mr->needApproved, 'locate' => helper::createLink('mr', 'view', "mr={$MRID}"))); + } + } + /* Accept MR by using the mapped user in GitLab. */ $sudoUser = $this->mr->getSudoUsername($MR->gitlabID, $MR->targetProject); if(isset($MR->gitlabID)) { - if(!empty($sudoUser)) $rawMR = $this->mr->apiAcceptMR($MR->gitlabID, $MR->targetProject, $MR->mriid, $sudo = $sudoUser); + if(!empty($sudoUser)) $rawMR = $this->mr->apiAcceptMR($MR->gitlabID, $MR->targetProject, $MR->mriid, $sudoUser); if(empty($sudoUser)) $rawMR = $this->mr->apiAcceptMR($MR->gitlabID, $MR->targetProject, $MR->mriid); } if(isset($rawMR->state) and $rawMR->state == 'merged') { - /* Force reload when locate to the url. */ - $random = uniqid(); - return $this->send(array('result' => 'success', 'message' => $this->lang->mr->mergeSuccess, 'locate' => helper::createLink('mr', 'browse', "random={$random}"))); + ///* Force reload when locate to the url. */ + //$random = uniqid(); + //return $this->send(array('result' => 'success', 'message' => $this->lang->mr->mergeSuccess, 'locate' => helper::createLink('mr', 'browse', "random={$random}"))); + + $this->mr->logMergedAction($MR); + + return $this->send(array('result' => 'success', 'message' => $this->lang->mr->mergeSuccess, 'locate' => helper::createLink('mr', 'browse'))); } /* The type of variable `$rawMR->message` is string. This is different with apiCreateMR. */ @@ -211,6 +293,541 @@ class mr extends control return $this->send(array('result' => 'fail', 'message' => $this->lang->mr->mergeFailed, 'locate' => helper::createLink('mr', 'view', "mr={$MRID}"))); } + /** + * View diff between MR source and target branches. + * + * @param int $MRID + * @access public + * @return void + */ + public function diff($MRID, $encoding= '') + { + $this->app->loadLang('productplan'); + $this->app->loadLang('bug'); + $this->app->loadLang('task'); + + $encoding = empty($encoding) ? 'utf-8' : $encoding; + $encoding = strtolower(str_replace('_', '-', $encoding)); /* Revert $config->requestFix in $encoding. */ + + $MR = $this->mr->getByID($MRID); + if(isset($MR->gitlabID)) $rawMR = $this->mr->apiGetSingleMR($MR->gitlabID, $MR->targetProject, $MR->mriid); + $this->view->title = $this->lang->mr->viewDiff; + $this->view->MR = $MR; + $this->view->rawMR = $rawMR; + if(!isset($rawMR->id) or (isset($rawMR->message) and $rawMR->message == '404 Not found') or empty($rawMR)) return $this->display(); + + $diffs = $this->mr->getDiffs($MR, $encoding = ''); + $arrange = $this->cookie->arrange ? $this->cookie->arrange : 'inline'; + + if($this->server->request_method == 'POST') + { + if($this->post->arrange) + { + $arrange = $this->post->arrange; + setcookie('arrange', $arrange); + } + if($this->post->encoding) $encoding = $this->post->encoding; + } + + if($arrange == 'appose') + { + 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; + } + } + } + + $this->view->repo = $this->loadModel('repo')->getRepoByID($MR->repoID); + $this->view->repoID = $MR->repoID; + $this->view->diffs = $diffs; + $this->view->encoding = $encoding; + $this->view->arrange = $arrange; + $this->display(); + } + + /** + * Approval for this MR. + * + * @param int $MRID + * @param string $action + * @return void + */ + public function approval($MRID, $action = 'approve') + { + $MR = $this->mr->getByID($MRID); + + if($_POST) + { + $comment = $this->post->comment; + $result = $this->mr->approve($MR, $action, $comment); + return $this->send($result); + } + + $showCompileResult = false; + if(!empty($MR->compileStatus)) + { + $showCompileResult = true; + $this->app->loadLang('compile'); /* Import lang. */ + $this->view->compileUrl = $this->createLink('job', 'view', "jobID={$MR->jobID}&compileID={$MR->compileID}"); + } + $this->view->showCompileResult = $showCompileResult; + + $this->view->MR = $MR; + $this->view->action = $action; + $this->view->actions = $this->loadModel('action')->getList('mrapproval', $MRID); + $this->view->users = $this->loadModel('user')->getPairs('noletter|noclosed'); + $this->display(); + } + + /** + * Close this MR. + * + * @param int $MRID + * @return void + */ + public function close($MRID) + { + $MR = $this->mr->getByID($MRID); + return $this->send($this->mr->close($MR)); + } + + /** + * Reopen this MR. + * + * @param int $MRID + * @return void + */ + public function reopen($MRID) + { + $MR = $this->mr->getByID($MRID); + return $this->send($this->mr->reopen($MR)); + } + + /** + * link MR list. + * + * @param int $MRID + * @param string $type + * @param string $orderBy + * @param string $link + * @param string $param + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + * @return void + */ + public function link($MRID, $type = 'story', $orderBy = 'id_desc', $link = 'false', $param = '', $recTotal = 0, $recPerPage = 100, $pageID = 1) + { + $this->app->loadLang('productplan'); + $this->app->loadLang('bug'); + $this->app->loadLang('task'); + + $MR = $this->mr->getByID($MRID); + $product = $this->mr->getMRProduct($MR); + + /* Load pager. */ + $this->app->loadClass('pager', $static = true); + $storyPager = new pager(0, $recPerPage, $type == 'story' ? $pageID : 1); + $bugPager = new pager(0, $recPerPage, $type == 'bug' ? $pageID : 1); + $taskPager = new pager(0, $recPerPage, $type == 'task' ? $pageID : 1); + + $stories = $this->mr->getLinkList($MRID, $product->id, 'story', $orderBy, $storyPager); + $bugs = $this->mr->getLinkList($MRID, $product->id, 'bug', $orderBy, $bugPager); + $tasks = $this->mr->getLinkList($MRID, $product->id, 'task', $orderBy, $taskPager); + + $this->view->title = $this->lang->mr->common . $this->lang->colon . $this->lang->mr->link; + $this->view->MR = $MR; + $this->view->canBeChanged = true; + $this->view->modulePairs = $this->loadModel('tree')->getOptionMenu($product->id, 'story'); + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + $this->view->stories = $stories; + $this->view->summary = $this->loadModel('product')->summary($stories); + $this->view->bugs = $bugs; + $this->view->tasks = $tasks; + $this->view->product = $product; + $this->view->storyPager = $storyPager; + $this->view->bugPager = $bugPager; + $this->view->taskPager = $taskPager; + $this->view->type = $type; + $this->view->orderBy = $orderBy; + $this->view->link = $link; + $this->view->param = $param; + $this->display(); + } + + /** + * Link story to mr. + * + * @param int $MRID + * @param int $productID + * @param string $browseType + * @param int $param + * @param string $orderBy + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + * @access public + * @return void + */ + public function linkStory($MRID, $productID = 0, $browseType = '', $param = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 100, $pageID = 1) + { + if(!empty($_POST['stories'])) + { + $this->mr->link($MRID, $productID, 'story'); + + if(dao::isError()) die(js::error(dao::getError())); + die(js::locate(inlink('link', "MRID=$MRID&type=story&orderBy=$orderBy"), 'parent')); + } + + $this->loadModel('story'); + $this->app->loadLang('productplan'); + + $product = $this->loadModel('product')->getById($productID); + $modules = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'story'); + + /* Load pager. */ + $this->app->loadClass('pager', $static = true); + $pager = new pager($recTotal, $recPerPage, $pageID); + + /* Build search form. */ + $storyStatusList = $this->lang->story->statusList; + unset($storyStatusList['closed']); + $queryID = ($browseType == 'bySearch') ? (int) $param : 0; + + unset($this->config->product->search['fields']['product']); + $this->config->product->search['actionURL'] = $this->createLink('mr', 'link', "$MRID=$MRID&type=story&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); + $this->config->product->search['queryID'] = $queryID; + $this->config->product->search['style'] = 'simple'; + $this->config->product->search['params']['product']['values'] = array($product) + array('all' => $this->lang->product->allProductsOfProject); + $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($productID => $productID)); + $this->config->product->search['params']['module']['values'] = $modules; + $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $storyStatusList); + + if($product->type == 'normal') + { + unset($this->config->product->search['fields']['branch']); + unset($this->config->product->search['params']['branch']); + } + else + { + $this->product->setMenu($productID, 0); + $this->config->product->search['fields']['branch'] = $this->lang->product->branch; + $branches = array('' => '') + $this->loadModel('branch')->getPairs($productID, 'noempty'); + $this->config->product->search['params']['branch']['values'] = $branches; + } + $this->loadModel('search')->setSearchParams($this->config->product->search); + + $MR = $this->mr->getByID($MRID); + $relatedStories = $this->mr->getCommitedLink($MR->gitlabID, $MR->targetProject, $MR->mriid, 'story'); + + $linkedStories = $this->mr->getLinkList($MRID, $product->id, 'story'); + if($browseType == 'bySearch') + { + $allStories = $this->story->getBySearch($productID, 0, $queryID, 'id', '', 'story', array_keys($linkedStories), $pager); + } + else + { + $allStories = $this->story->getProductStories($productID, 0, $moduleID = '0', $status = 'draft,active,changed', 'story', 'id_desc', $hasParent = false, array_keys($linkedStories), $pager); + } + + $this->view->modules = $modules; + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + $this->view->allStories = $allStories; + $this->view->relatedStories = $relatedStories; + $this->view->product = $product; + $this->view->MRID = $MRID; + $this->view->browseType = $browseType; + $this->view->param = $param; + $this->view->orderBy = $orderBy; + $this->view->pager = $pager; + $this->display(); + } + + /** + * Link bug to mr. + * + * @param int $MRID + * @param int $productID + * @param string $browseType + * @param int $param + * @param string $orderBy + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + * @access public + * @return void + */ + public function linkBug($MRID, $productID = 0, $browseType = '', $param = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 100, $pageID = 1) + { + if(!empty($_POST['bugs'])) + { + $this->mr->link($MRID, $productID, 'bug'); + + if(dao::isError()) die(js::error(dao::getError())); + die(js::locate(inlink('link', "MRID=$MRID&type=bug&orderBy=$orderBy"), 'parent')); + } + + $this->loadModel('bug'); + $this->app->loadLang('productplan'); + $queryID = ($browseType == 'bysearch') ? (int)$param : 0; + + $product = $this->loadModel('product')->getById($productID); + $modules = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'bug'); + + /* Load pager. */ + $this->app->loadClass('pager', $static = true); + $pager = new pager($recTotal, $recPerPage, $pageID); + + /* Build search form. */ + $this->config->bug->search['actionURL'] = $this->createLink('mr', 'link', "$MRID=$MRID&type=bug&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); + $this->config->bug->search['queryID'] = $queryID; + $this->config->bug->search['style'] = 'simple'; + $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($productID => $productID)); + $this->config->bug->search['params']['module']['values'] = $modules; + $this->config->bug->search['params']['execution']['values'] = $this->product->getExecutionPairsByProduct($productID); + $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($productID, $branch = 0, $params = ''); + $this->config->bug->search['params']['resolvedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($productID, $branch = 0, $params = ''); + + unset($this->config->bug->search['fields']['product']); + if($product->type == 'normal') + { + unset($this->config->bug->search['fields']['branch']); + unset($this->config->bug->search['params']['branch']); + } + else + { + $this->product->setMenu($productID, 0); + $this->config->bug->search['fields']['branch'] = $this->lang->product->branch; + $branches = array('' => '') + $this->loadModel('branch')->getPairs($productID, 'noempty'); + $this->config->bug->search['params']['branch']['values'] = $branches; + } + $this->loadModel('search')->setSearchParams($this->config->bug->search); + + $MR = $this->mr->getByID($MRID); + $relatedBugs = $this->mr->getCommitedLink($MR->gitlabID, $MR->targetProject, $MR->mriid, 'bug'); + + $linkedBugs = $this->mr->getLinkList($MRID, $product->id, 'bug'); + if($browseType == 'bySearch') + { + $allBugs = $this->bug->getBySearch($productID, 0, $queryID, 'id_desc', array_keys($linkedBugs), $pager); + } + else + { + $allBugs = $this->bug->getActiveBugs($productID, 0, '0', array_keys($linkedBugs), $pager); + } + + $this->view->modules = $modules; + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + $this->view->allBugs = $allBugs; + $this->view->relatedBugs = $relatedBugs; + $this->view->product = $product; + $this->view->MRID = $MRID; + $this->view->browseType = $browseType; + $this->view->param = $param; + $this->view->orderBy = $orderBy; + $this->view->pager = $pager; + $this->display(); + } + + /** + * Link task to mr. + * + * @param int $MRID + * @param int $productID + * @param string $browseType + * @param int $param + * @param string $orderBy + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + * @access public + * @return void + */ + public function linkTask($MRID, $productID = 0, $browseType = 'unclosed', $param = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 100, $pageID = 1) + { + if(!empty($_POST['tasks'])) + { + $this->mr->link($MRID, $productID, 'task'); + + if(dao::isError()) die(js::error(dao::getError())); + die(js::locate(inlink('link', "MRID=$MRID&type=task&orderBy=$orderBy"), 'parent')); + } + + $this->loadModel('execution'); + $this->loadModel('product'); + $this->app->loadLang('task'); + + /* Set browse type. */ + $browseType = strtolower($browseType); + $queryID = ($browseType == 'bysearch') ? (int)$param : 0; + + $product = $this->loadModel('product')->getById($productID); + $modules = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'task'); + + /* Load pager. */ + $this->app->loadClass('pager', $static = true); + $pager = new pager($recTotal, $recPerPage, $pageID); + + /* Build search form. */ + $this->config->execution->search['actionURL'] = $this->createLink('mr', 'link', "$MRID=$MRID&type=task&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); + $this->config->execution->search['queryID'] = $queryID; + $this->config->execution->search['params']['module']['values'] = $modules; + $this->config->execution->search['params']['execution']['values'] = $this->product->getExecutionPairsByProduct($productID); + $this->loadModel('search')->setSearchParams($this->config->execution->search); + + $MR = $this->mr->getByID($MRID); + $relatedTasks = $this->mr->getCommitedLink($MR->gitlabID, $MR->targetProject, $MR->mriid, 'task'); + $linkedTasks = $this->mr->getLinkList($MRID, $product->id, 'task'); + + /* Get executions by product. */ + $productExecutions = $this->product->getExecutionPairsByProduct($productID); + $productExecutionIDs = array_filter(array_keys($productExecutions)); + $this->config->execution->search['params']['execution']['values'] = array_filter($productExecutions); + + /* Get tasks by executions. */ + $allTasks = array(); + foreach($productExecutionIDs as $productExecutionID) + { + $tasks = $this->execution->getTasks(0, $productExecutionID, array(), $browseType, $queryID, 0, $orderBy, null); + $allTasks = array_merge($tasks, $allTasks); + } + /* Filter linked tasks. */ + $linkedTaskIDs = array_keys($linkedTasks); + foreach($allTasks as $key => $task) + { + if(in_array($task->id, $linkedTaskIDs)) unset($allTasks[$key]); + } + + /* Page the records. */ + $pager->setRecTotal(count($allTasks)); + $pager->setPageTotal(); + if($pager->pageID > $pager->pageTotal) $pager->setPageID($pager->pageTotal); + $count = 1; + $limitMin = ($pager->pageID - 1) * $pager->recPerPage; + $limitMax = $pager->pageID * $pager->recPerPage; + foreach($allTasks as $key => $task) + { + if($count <= $limitMin or $count > $limitMax) unset($allTasks[$key]); + + $count ++; + } + + $this->view->modules = $modules; + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + $this->view->allTasks = $allTasks; + $this->view->relatedTasks = $relatedTasks; + $this->view->product = $product; + $this->view->MRID = $MRID; + $this->view->browseType = $browseType; + $this->view->param = $param; + $this->view->orderBy = $orderBy; + $this->view->pager = $pager; + $this->display(); + } + + /** + * UnLink an mr link. + * + * @param int $MRID + * @param int $productID + * @param string $type + * @param int $linkID + * @param string $confirm + * @access public + * @return mix + */ + public function unlink($MRID, $productID, $type, $linkID, $confirm = 'no') + { + $this->app->loadLang('productplan'); + + if($confirm == 'no') + { + die(js::confirm($this->lang->productplan->confirmUnlinkStory, $this->createLink('mr', 'unlink', "MRID=$MRID&productID=$productID&linkID=$linkID&type=$type&confirm=yes"))); + } + else + { + $this->mr->unlink($MRID, $productID, $type, $linkID); + + /* if ajax request, send result. */ + if($this->server->ajax) + { + if(dao::isError()) + { + $response['result'] = 'fail'; + $response['message'] = dao::getError(); + } + else + { + $response['result'] = 'success'; + $response['message'] = ''; + } + return $this->send($response); + } + die(js::reload('parent')); + } + } + + /** + * Add a Bug for this review. + * + * @param int $repoID + * @param string $file + * @param int $v1 + * @param int $v2 + * @access public + * @return void + */ + public function addBug($repoID, $file, $v1, $v2) + { + /* Handle the exception that when $repoID is empty. */ + if($repoID == "0") $this->send(array()); + + $this->loadModel('repo'); + if($this->get->repoPath) $file = $this->get->repoPath; + if(!empty($_POST)) + { + $result = $this->mr->saveBug($repoID, $file, $v1, $v2); + if(dao::isError()) die(json_encode($result)); + + $bugID = $result['id']; + $repo = $this->repo->getRepoById($repoID); + /* Handle the exception that when $repo is empty. */ + if(empty($repo)) $this->send(array()); + + $entry = isset($repo->name) ? $repo->name . '/' . $this->repo->decodePath($file) : ''; + $location = sprintf($this->lang->repo->reviewLocation, $entry, $repo->SCM != 'Subversion' ? substr($v2, 0, 10) : $v2, $this->post->begin, $this->post->end); + if(empty($v1)) + { + $revision = $repo->SCM != 'Subversion' ? substr($v2, 0, 10) : $v2; + $link = $this->repo->createLink('view', "repoID=$repoID&objectID=0&entry={$file}&revision=$v2&showBug=true") . '#L' . $this->post->begin; + } + else + { + $revision = $repo->SCM != 'Subversion' ? substr($v1, 0, 10) : $v1; + $revision .= ' : '; + $revision .= $repo->SCM != 'Subversion' ? substr($v2, 0, 10) : $v2; + $link = $this->repo->createLink('diff', "repoID=$repoID&objectID=0&entry={$file}&oldRevision=$v1&newRevision=$v2&showBug=true") . '#L' . $this->post->begin; + } + + $actionID = $this->loadModel('action')->create('bug', $bugID, 'repoCreated', '', html::a($link, $location)); + $this->loadModel('mail')->sendmail($bugID, $actionID); + + echo json_encode($result); + } + } + /** * AJAX: Get MR target projects. * @@ -252,51 +869,54 @@ class mr extends control } /** - * View diff between MR source and target branches. + * AJAX: Get repo list. * - * @param int $MRID - * @access public + * @param int $gitlabID + * @param int $projectID * @return void */ - public function diff($MRID) + public function ajaxGetRepoList($gitlabID, $projectID) { - $MR = $this->mr->getByID($MRID); - $diffs = $this->mr->getDiffs($MR); - $arrange = $this->cookie->arrange ? $this->cookie->arrange : 'inline'; + $this->loadModel('repo'); + $repoList = $this->repo->getGitLabRepoList($gitlabID, $projectID); - if($this->server->request_method == 'POST') - { - if($this->post->arrange) - { - $arrange = $this->post->arrange; - setcookie('arrange', $arrange); - } - if($this->post->encoding) $encoding = $this->post->encoding; - } - - if($arrange == 'appose') - { - 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; - } - } - } - - $this->view->title = $this->lang->mr->viewDiff; - $this->view->diffs = $diffs; - $this->view->arrange = $arrange; - $this->display(); + if(!$repoList) return $this->send(array('message' => array())); + $options = ""; + foreach($repoList as $repo) $options .= ""; + $this->send($options); } + + /** + * AJAX: Get job list. + * + * @param int $repoID + * @return void + */ + public function ajaxGetJobList($repoID) + { + $this->loadModel('job'); + $jobList = $this->job->getListByRepoID($repoID); + + if(!$jobList) return $this->send(array('message' => array())); + $options = ""; + foreach($jobList as $job) $options .= ""; + $this->send($options); + } + + /** + * AJAX: Get compile list. + * + * @param int $jobID + * @return void + */ + public function ajaxGetCompileList($jobID) + { + $this->loadModel('compile'); + $compileList = $this->compile->getListByJobID($jobID); + + if(!$compileList) return $this->send(array('message' => array())); + $options = ""; + foreach($compileList as $compile) $options .= ""; + $this->send($options); + } } diff --git a/module/mr/css/common.css b/module/mr/css/common.css new file mode 100644 index 0000000000..a2da70383a --- /dev/null +++ b/module/mr/css/common.css @@ -0,0 +1,187 @@ +a {color: #169;} +a:hover, a:active {text-decoration:underline; color: #C61A1A;} +#swapper a {text-decoration: none;} +#swapper .col-footer a:hover {color: #0c64eb;} +h2,h3 {font-size: 20px; margin: 0; clear: both;} +h3 {font-size: 16px;} +.revision {font-size: 12px; line-height: 20px; text-align: right; padding-right: 8px;} +.directory {background-image:url('theme/default/images/repo/dir.png');} +.file {background-image:url('theme/default/images/repo/txt.png');} +/* .icon {width: 17px; padding-left: 10px; padding-right: 2px;} */ +.mini-icon {display: inline-block; height: 16px; width: 16px; background-color: transparent; background-position: 0 0; background-repeat: no-repeat; vertical-align: text-bottom;} +.action {float: left;} +.input-group select#encrypt {border-left: 0px;} +.arrange {float: right;} +.versions {position: relative;} +#diffRepo {position: absolute; left: 20px; z-index: 1000;} +#repoID {display: inline-block; width: auto;} +.repoCode a:hover {text-decoration: none;} +.commentButton +{ + background-repeat: no-repeat; + position: absolute; + left: -28px; + width: 40px; + z-index: 10; + cursor:pointer; + font-size: 18px; + color: #4183C4; + display: none; +} +.repoCode tr.over .commentButton {display: block;} +.bug +{ + background-repeat: no-repeat; + position: absolute; + left: -7px; + width: 20px; + z-index: 0; + cursor:pointer; + font-size: 18px; + color: #4183C4; + line-height: 18px; +} +.repoCode .icon {opacity: 1;} +.icon-comment-add:before {content: '\e74c'; transform: scale(-1, 1); display: inline-block; font-weight: normal;} +.icon-comment-add:after {content: '+'; display: block; font-weight: normal; position: absolute; left: 16px; top: 1px; font-family: Arial; font-weight: bold; font-size: 12px;} +.icon-comments:before {content: '\e750'; transform: scale(-1, 1); display: inline-block; font-weight: normal; font-size: 18px; line-height: 18px;} +.commentButton:hover,.bug:hover {color: #d20b0b;} +.commentBoard {border-top: 1px solid #E4E4E4; border-bottom: 1px solid #E4E4E4; padding: 0; white-space: normal; background-color: #eee; padding: 10px;} +.commentBoard .table-form th {background: none;} +.lines input {width: 40px;} +.commentSubmit, .commentCancel {margin-right: 10px;} +.commentFoot .optional {float: left;} +.commentBoard.using .bugContainer {border: 1px solid #ddd; background: #fff;} +.commentBoard.using .bugContainer > .commentHeader {background: #f1f1f1; padding: 0 10px; border-bottom: 1px solid #ddd;} +.commentHeaderAuthor {max-width: 600px; line-height: 33px; font-weight: bold; color: #222; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;} +.comment {width: 100%; word-break: keep-all; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;} +.commentContent {margin: 10px;} +.commentHeaderRight {float: right;} + +/* Sider */ +#mainContent > #sidebar > .side-body.affix {top: 0px; z-index: 10000;} + +/* Pre row */ +.repoCode table > tbody > tr.over td {background: #f8eec7;} +.repoCode table > tbody > tr.over th {background: #cdcdcd; color: #333;} + +/* Comment-btn */ +.comment-btn {position: relative; margin: 0; padding: 0; display: none;} +.repoCode tr.over .comment-btn {display: block;} +.comment-btn .icon-wrapper {display: block; position: absolute; background: #4183C4; border-radius: 2px; width: 24px; height: 20px; left: -6px; top: 0; line-height: 20px; text-align: center; color: #fff; cursor: pointer; transition: transform 0.2s;} +.comment-btn .icon-wrapper:before {display: block; content: ' '; right: -4px; top: 6px; position: absolute; border-left: 4px solid #4183C4; border-right: 0 solid transparent; border-bottom: 4px solid transparent; border-top: 4px solid transparent; width: 0; height: 0;} +.comment-btn .icon-wrapper:hover {background: #169; transform: scale(1.1);} +.comment-btn .icon-wrapper:hover:before {border-left-color: #169;} + +.repoCode tr.commented {cursor: pointer;} + +.repoCode tr.commented .comment-btn {display: block;} +.repoCode tr.commented .comment-btn .icon-wrapper {background: none; border: none; width: 24px; line-height: 18px; height: 18px; left: -6px; color: #4183c4;} +.repoCode tr.commented .comment-btn .icon-wrapper:hover {border-color: #169; color: #169;} +.repoCode tr.commented .comment-btn .icon-wrapper > i:before {font-size: 18px; transform: scale(-1, 1); display: inline-block;} +.repoCode tr.commented .comment-btn .icon-wrapper:before {display: none;} + +.repoCode tr.over.commented .comment-btn .icon-wrapper, .repoCode tr.selected.commented .comment-btn .icon-wrapper {line-height: 20px; height: 20px; background: #4183C4; color: #fff; left: -6px;} +.repoCode tr.over.commented .comment-btn .icon-wrapper > i:before {font-size: 14px;} +.repoCode tr.selected.commented .comment-btn .icon-wrapper > i:before {font-size: 14px;} +.repoCode tr.over.commented .comment-btn .icon-wrapper:before, .repoCode tr.selected.commented .comment-btn .icon-wrapper:before {display: block;} + +/* repo action form */ +.repoCode .comment-list, .repoCode .comment-actions {max-width: 900px;} +.repoCode .bugFormContainer {border: 1px solid #bbb; margin: 0 0 0 15px; padding: 10px 20px 10px 10px; max-width: 880px; background: #fff;} +.repoCode .bugFormContainer th {width: 70px;} + +.repoCode .action-row {display: none;} +.repoCode .with-action-row .action-row {display: table-row;} +.repoCode .action-cell {background: #EEE; white-space: normal; padding: 10px 15px 10px 0;} +.repoCode .with-action-row table tr.selected .comment-btn:last-child {display: block;} +.repoCode .with-action-row table tr.selected td {background: #f8eec7;} +.repoCode .with-action-row table tr.selected th {background: #cdcdcd;} +.repoCode .with-action-row table tr.selected .comment-btn .icon-wrapper > i:before, .repoCode #diff.with-action-row tr.selected .comment-btn .icon-wrapper > i:before {content: '\d7';} + +.repoCode .comment-row {display: none;} +.repoCode .comment-row.show {display: table-row !important;} +.repoCode .comment-cell {background: #eee; white-space: normal;} +.repoCode .comment-cell .panel {margin: 10px; border-color: #bbb;} +.repoCode .comment-cell .panel-body {padding: 6px 10px;} +.repoCode .comment-cell .panel-actions.pull-right {margin-right: 0; margin-top: 0;} +.repoCode .comment-cell .editing .panel-body, .repoCode .comment-cell .commentContainer.show-form .panel-body {display: none;} +.repoCode .comment-cell .bug-edit-form, {padding: 10px; display: none;} +.repoCode .comment-cell .editing .bug-edit-form, .repoCode .comment-cell .commentContainer.show-form .comment-edit-form {display: block;} + +.repoCode .comment {border: 1px solid #e5e5e5; background: #fafafa; padding: 5px 10px; margin-bottom: 10px;} +.repoCode .comment .comment-edit-form {margin-top: 10px;} +.repoCode .panel-bug .steps {background: #f1f1f1; padding: 5px 10px;} +.repoCode .panel-bug .bug-edit-form {margin-bottom: 10px;} +.repoCode .panel-bug .panel-body {display: none;} +.repoCode .panel-bug .panel-heading {cursor: pointer;} +.repoCode .panel-bug.show .panel-body {display: block;} +.repoCode .panel-bug.show .icon-chevron-sign-down:before {content: '\e711';} +.repoCode .panel-bug.show-edit-form .bug-edit-form, +.repoCode .panel-bug.show-form .commentForm, +.repoCode .comment.show-form .comment-edit-form {display: block;} +.repoCode .panel-bug .bug-edit-form, +.repoCode .panel-bug.show-form .addComment, +.repoCode .panel-bug .commentForm, +.repoCode .comment .comment-edit-form, +.repoCode .panel-bug.show-edit-form .panel-body .title, +.repoCode .panel-bug.show-edit-form .bug-date, +.repoCode .comment.show-form .comment-content, .repoCode .comment.show-form .comment-date {display: none;} + +.repoCode .text-content {white-space: normal; white-space: pre-line;} +.repoCode .text-muted {color: #aaa;} + +.repoCode tr {transition: all 1s;} +.repoCode tr.highlight {background: #fff4e5;} +.repoCode tr.highlight td, .repoCode tr.highlight th {background: none; border-top: 1px solid #e48600; border-bottom: 1px solid #e48600;} +.repoCode tr.highlight.commented th {color: #e48600;} +.repoCode tr.highlight.commented td, .repoCode tr.highlight.commented th {border-bottom: none;} +.repoCode tr.highlight + tr.highlight td, .repoCode tr.highlight + tr.highlight th {border-top: none;} + +.repoCode .row-tip {display: none;} +.repoCode tr.commented .row-tip {display: block; position: relative; right: -3px; bottom: -1px;} +.repoCode tr.commented .tip, .repoCode tr.commented.open .tip.on-collapse {display: block; position: absolute; right: 0; bottom: 0; color: #4183c4; opacity: 0; padding: 0 5px; background: #edf3ff; height: 20px; line-height: 20px; transition: opacity 0.2s;} +.repoCode tr.commented:hover .tip.on-expand {opacity: 1;} +.repoCode tr.commented.open .tip.on-collapse {opacity: 1;} +.repoCode tr.commented.open .tip.on-expand {display: none;} +.repoCode tr.commented.open .tip.on-collapse span {display: none;} +.repoCode tr.commented.open:hover .tip.on-collapse span {display: inline;} +.repoCode tr.commented.open {background: #f8fafe;} +.repoCode tr.commented .preview-icon {position: absolute; left: -6px; bottom: 0; width: 20px; height: 20px; line-height: 20px; text-align: center; color: #4183c4; background: #edf3ff; display: none;} +.repoCode tr.commented:hover .preview-icon {display: block; transform: scale(-1, 1);} +.repoCode tr.commented .preview-icon:before {font-size: 18px;} + +.repoCode #diff tr.commented .row-tip {right: 0;} +.repoCode #diff tr.commented .icon-chat-dot {left: 0;} + +.repoCode .panel, .bugFormContainer {transition: border 0.4s;} +.repoCode .panel.highlight, #bugForm.highlight .bugFormContainer {border-color: #e48600;} + +#bugsPreview {white-space: normal;} +#bugsPreview .dropdown-menu {top: -100%; left: 30%; padding-top: 0; min-width: 300px; max-width: 500px;} +#bugsPreview .dropdown-menu > li.dropdown-header {background: #f1f1f1; padding-top: 8px;} +#bugsPreview .dropdown-menu > li > a {border-top: 1px solid #e5e5e5; text-overflow : ellipsis; overflow: hidden;} +#bugsPreview .dropdown-menu.show {display: block;} + +.icon-comments {position: relative; left: -50px;} + +/* bug form */ +#bugForm, #bugForm table {margin: 0; padding: 0;} + +.panel .table + .panel-footer {border-top: 0; background: #fff;} + +.transparent {border-color:transparent; background: none repeat scroll 0 0 transparent;} +.transparent:hover {border-color:transparent; background: none repeat scroll 0 0 transparent;} + +.side-col {width: 600px;} +#sidebar > .side-body {width: 580px;} +.hide-sidebar #sidebar > .side-body {display: none;} + +#sidebar>.sidebar-toggle {left: 5px; right: auto;} +#sidebar>.sidebar-toggle>.icon {right: -4px; left: auto;} + +#logForm .fixed-footer a.allLogs {color: #fff !important;} + +#submitLabel {text-align: left;} + +.header-btn .btn > .text {text-overflow: unset !important;} diff --git a/module/mr/css/link.css b/module/mr/css/link.css new file mode 100644 index 0000000000..5cded22bcf --- /dev/null +++ b/module/mr/css/link.css @@ -0,0 +1,26 @@ +#storyList th {border: none;} +#bugList th {border: none;} +#taskList th {border: none;} +.hide-side .col-main {width: 100%;} +.hide-side .col-side .main-side {width: 0; display: none;} + +.dropdown-menu.with-search {padding: 0; min-width: 150px; overflow: hidden; max-height: 302px;} +.dropdown-menu > .menu-search .input-group {width: 100%;} +.dropdown-menu > .menu-search .input-group-addon {position: absolute; right: 10px; top: 0; z-index: 10; background: none; border: none; color: #666;} +.dropdown-menu > .menu-search .form-control {border: none !important; box-shadow: none !important; border-top: 1px solid #ddd !important;} +.dropdown-list {display: block; padding: 0; max-height: 270px; overflow-y: auto;} +.dropdown-list > li > a {display: block; padding: 3px 20px; clear: both; font-weight: normal; line-height: 1.53846154; color: #141414; white-space: nowrap;} +.dropdown-list > li > a:hover, +.dropdown-list > li > a:focus {color: #1a4f85; text-decoration: none; background-color: #ddd;} + +.checkbox.btn {margin-top: 0px;} + +.linkBox #queryBox .search-form .form-actions {padding-bottom: 5px;} +.linkBox .table-header {padding: 8px 15px; border-bottom: 1px solid #cbd0db;} +#unlinkStoryList, #unlinkBugList, #unlinkTaskList{border-top: 1px solid #cbd0db;} +.fixed-footer .text {color: #fff;} +ol, ul {padding-left: 20px;} +#tabsNav .tab-pane>.main-table {border-radius: 0;} +.body-modal #mainMenu>.btn-toolbar {width: auto;} +.body-modal #mainContent {min-height: 240px;} +.body-modal #mainMenu>.btn-toolbar .page-title>.text {overflow: visible} \ No newline at end of file diff --git a/module/mr/css/view.css b/module/mr/css/view.css new file mode 100644 index 0000000000..4e42eafa41 --- /dev/null +++ b/module/mr/css/view.css @@ -0,0 +1,12 @@ +#legendStories ul li, #legendBugs ul li, #legendTasks ul li {padding: 10px 0 0 12px; line-height: 20px;} +.side-col .cell {padding: 0px;} +.table-content tbody tr th{padding-left: 0; font-weight: 400; color: #838a9d; text-align: right; vertical-align: middle; border:none;} +.table-content tbody tr td{border:none;} +.table-content thead tr th{text-align: left; vertical-align: middle; border:none;} +.detail-content{margin-top:0;} +.main-row .cell { + border: 1px solid #efefef; + -webkit-box-shadow: 0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045); + box-shadow: 0 1px 1px rgba(0,0,0,.05),0 2px 6px 0 rgba(0,0,0,.045) +} +.side-col{padding-top:5px;} diff --git a/module/mr/js/create.js b/module/mr/js/create.js index 2194a15d88..5999afdfa9 100644 --- a/module/mr/js/create.js +++ b/module/mr/js/create.js @@ -35,8 +35,16 @@ $(function() $('#targetProject').html('').append(response); $('#targetProject').chosen().trigger("chosen:updated");; }); + + repoUrl = createLink('mr', 'ajaxGetRepoList', "gitlabID=" + gitlabID + "&projectID=" + sourceProject); + $.get(repoUrl, function(response) + { + $('#repoID').html('').append(response); + $('#repoID').chosen().trigger("chosen:updated");; + }); }); + /* $('#targetProject').change(function() { targetProject = $(this).val(); @@ -51,6 +59,44 @@ $(function() reviewer.chosen().trigger("chosen:updated");; }); }); + */ + $('#repoID').change(function() + { + repoID = $(this).val(); + jobUrl = createLink('mr', 'ajaxGetJobList', "repoID=" + repoID); + $.get(jobUrl, function(response) + { + $('#jobID').html('').append(response); + $('#jobID').chosen().trigger("chosen:updated");; + }); + }); + + $('#jobID').change(function() + { + jobID = $(this).val(); + compileUrl = createLink('mr', 'ajaxGetCompileList', "job=" + jobID); + $.get(compileUrl, function(response) + { + $('#compile').html('').append(response); + $('#compile').chosen().trigger("chosen:updated");; + }); + }); + + $("#needCI").change(function() + { + if(this.checked == false) + { + $("#jobID").prop("disabled", true); + $('#jobID').chosen().trigger("chosen:updated");; + $("#jobID").parent().parent().addClass('hidden'); + } + if(this.checked == true) + { + $("#jobID").prop("disabled", false); + $('#jobID').chosen().trigger("chosen:updated");; + $("#jobID").parent().parent().removeClass('hidden'); + } + }); }); diff --git a/module/mr/js/diff.js b/module/mr/js/diff.js index e44d3a65cb..a076f1628f 100644 --- a/module/mr/js/diff.js +++ b/module/mr/js/diff.js @@ -10,3 +10,499 @@ function changeEncoding(encoding) $('#encoding').val(encoding); $('#encoding').parents('form').submit(); } + +$(document).ready(function() +{ + var $diffCode = $('.diff'); + var hidePreview; + var $bugsPreview = $('#bugsPreview'); + var $bugsPreviewMenu = $('#bugsPreview').children('.dropdown-menu'); + var $rows = $diffCode.find('tr'); + var rowTip = $('#rowTip').html(); + var lastLine; + $rows.each(function() + { + var $row = $(this); + if(!$row.hasClass('empty')) + { + $row.children('th').first().prepend("
    "); + $row.children('td').first().append(rowTip); + } + + if(lastLine && !$row.data('line')) + { + $row.attr('data-line', lastLine); + } + else + { + lastLine = $row.data('line'); + } + }).hover(function() + { + var $this = $(this); + if($this.hasClass('empty')) return; + $this.addClass("over"); + }, + function() + { + $(this).removeClass("over"); + }); + + var isInline = $.cookie('arrange') == 'inline'; + var $bugFormRow = $('' + (isInline ? '' : '') + ''); + var $bugForm = $('#bugForm'); + var $commentCell = $('#commentCell'); + var $bugPanel = $('#bugPanel'); +// $bugForm.find('input[name="begin"], input[name="end"]').attr('max', lastLine); + $bugFormRow.find('td').append($bugForm.removeClass('hide')); + + var highlight = function($e) + { + $('.highlight').removeClass('highlight'); + $e.addClass('highlight'); + }; + + var createComment = function(comment, $comments) + { + console.log(); + var $comment = $commentCell.clone() + .removeClass('hide') + .attr('id', 'comment-' + comment.id) + .attr('data-comment', comment.id); + $comment.find('.realname').text(comment.realname); + $comment.find('.comment-content').text(comment.comment); + $comment.find('.date').text(comment.date); + $comment.find('.edit').toggle(comment.edit); + $comment.find('.comment-edit-form').attr('action', createLink('repo', 'editComment', 'commentID=' + comment.id)); + + if($comments) + { + if(typeof $comments !== 'object') $comments = $('#bug-' + $comments + ' .comments'); + ($comments.hasClass('comments') ? $comments : $comments.find('.comments')).append($comment); + } + + return $comment; + }; + + var createBug = function(bug, line, $commentRow, show) + { + var commentCount, j; + var $bug = $bugPanel.clone().removeClass('hide').attr('id', 'bug-' + bug.id).attr('data-bug', bug.id); + $bug.find('.bugid').text(bug.id); + $bug.find('.realname').text(bug.realname); + $bug.find('.openedDate').text(bug.openedDate); + $bug.find('.title').text(bug.title); + $bug.find('.steps').toggle(bug.steps != '').html(bug.steps); + $bug.find('.edit').toggle(bug.edit); + $bug.find('.code-lines').text(bug.lines); + $bug.find('.delete').toggle(bug.delete); + $bug.find('input[name="objectID"]').val(bug.id); + $bug.find('.bug-edit-form').attr('action', createLink('repo', 'editBug', 'bugID=' + bug.id)); + $bug.find('a.view-bug').attr('href', createLink('bug', 'view', "bugID=" + bug.id)); + $bug.data('data', bug); + $bug.toggleClass('show', show > 1); + if(show > 2) highlight($bug); + + if(bug.comments) + { + commentCount = bug.comments.length; + $bugComments = $bug.find('.comments'); + for(j = 0; j < commentCount; j++) + { + createComment(bug.comments[j], $bugComments); + } + } + + if(!line && bug.line) line = bug.line; + if(line) + { + if(!$commentRow) + { + var $row = $rows.filter('[data-line="' + line + '"]').last(); + $commentRow = $row.next('tr'); + if(!$commentRow.hasClass('comment-row')) + { + $commentRow = $('' + (isInline ? '' : '') + ''); + $row.addClass('commented').after($commentRow); + } + } + ($commentRow.hasClass('comment-list') ? $commentRow : $commentRow.find('.comment-list')).append($bug); + + if(show && $commentRow.hasClass('comment-row')) $commentRow.addClass('show'); + } + + return $bug; + }; + + var toggleComment = function($row, show) + { + var $commentRow; + if($row.hasClass('comment-row')) + { + $commentRow = $row; + $row = $commentRow.prev('tr'); + if($row.hasClass('action-row')) + { + $row = $row.prev('tr'); + } + } + else + { + $commentRow = $row.next('tr'); + if($commentRow.hasClass('action-row')) + { + $commentRow = $commentRow.next('tr'); + } + } + if(show === undefined) + { + show = !$row.hasClass('open'); + } + if($row.hasClass('commented') && $commentRow.hasClass('comment-row')) + { + $commentRow.toggleClass('show', show); + $row.toggleClass('open', show); + } + }; + + $diffCode.on('click', '.comment-btn', function(e) + { + $rows.removeClass('selected'); + var $row = $(this).closest('tr'); + if($diffCode.hasClass('with-action-row') && $row.hasClass('with-action-row')) + { + $diffCode.removeClass('with-action-row'); + } + else + { + $diffCode.addClass('with-action-row'); + var line = $row.data('line'); + if(!$row.hasClass('with-action-row')) + { + $rows.removeClass('with-action-row') + $row.addClass('with-action-row'); + + $bugForm.find('input[name="begin"]').val(line); + $bugForm.find('input[name="end"]').attr('min', line).val(line); + $bugForm.find('select#assignedTo').val(blamePairs[line]); + $bugForm.find('select#assignedTo').trigger("chosen:updated"); + + $row.after($bugFormRow); + + KindEditor.remove('#commentText'); + $('#commentText').kindeditor(); + + var getCommiterLink = createLink('repo', 'ajaxgetcommitter', 'repoID=' + repoID + "&entry=" + file + "&revision=" + revision + "&line=" + line); + var connector = getCommiterLink.indexOf('&') >= 0 ? '&' : '?'; + getCommiterLink = getCommiterLink + connector + 'entry=' + file; + $.ajax({url: getCommiterLink}).done(function(responseText) + { + $bugForm.find('#assignedTo').val(responseText).trigger("chosen:updated"); + }); + } + highlight($bugForm); + $bugForm.find('input[name="title"]').focus(); + $row.addClass('selected'); + } + e.stopPropagation(); + }).on('click', '.bugCancel', function() + { + $rows.removeClass('selected'); + $diffCode.removeClass('with-action-row'); + }).on('click', '.bugEdit', function(e) + { + var $panelBug = $(this).closest('.panel-bug'); + + if($panelBug.hasClass('show-edit-form')) + { + $panelBug.removeClass('show-edit-form'); + e.stopPropagation(); + return; + } + + $panelBug.addClass('show show-edit-form').find('input[name="commentText"]').val($panelBug.find('.title').first().text()).focus(); + e.stopPropagation(); + return false; + }).on('submit', '.bug-edit-form', function() + { + var $form = $(this); + $(this).ajaxSubmit( + { + success:function(text) + { + var $bug = $form.closest('.panel-bug'); + $bug.find('.title').text(text); + $bug.removeClass('show-edit-form'); + }, + beforeSubmit:function(formData, jqForm) + { + var form = jqForm[0]; + if(!form.commentText.value) + { + alert(contentError); + return false; + } + } + }); + return false; + }).on('click', '.bugEditCancel', function() + { + $(this).closest('.panel-bug').removeClass('show-edit-form'); + }).on('click', '.bugDelete', function(e) + { + var $bug = $(this).closest('.panel-bug'); + if(!$bug.length) return; + + if(confirm(confirmDelete)) + { + var link = createLink('repo', 'deleteBug', 'bugID=' + $bug.data('bug') + '&confirm=yes'); + $.get(link, function(data) + { + if(data == 'deleted') + { + var $commentRow = $bug.closest('.comment-row'); + if($commentRow.find('.panel-bug').length === 1) + { + $commentRow.removeClass('show').prev('tr').removeClass('commented'); + } + $bug.remove(); + } + }); + } + e.stopPropagation(); + return false; + }).on('click', '.addComment', function() + { + $(this).closest('.panel-bug').addClass('show-form').find('.commentForm textarea').focus(); + }).on('click', '.commentCancel', function() + { + $(this).closest('.panel-bug').removeClass('show-form'); + }).on('submit', '.commentForm', function() + { + var $form = $(this); + $form.ajaxSubmit( + { + success:function(json) + { + var $panelBug = $form.closest('.panel-bug'); + $form.find('textarea').val(''); + $panelBug.removeClass('show-form'); + createComment($.parseJSON(json), $panelBug.data('bug')); + }, + beforeSubmit:function(formData, jqForm) + { + var form = jqForm[0]; + if(!form.comment.value) + { + alert(commentError); + return false; + } + } + }); + return false; + }).on('click', '.commentEdit', function() + { + var $comment = $(this).closest('.comment'); + + if($comment.hasClass('show-form')) + { + $comment.removeClass('show-form'); + return; + } + $comment.addClass('show-form').find('textarea').val($comment.find('.comment-content').text()).focus(); + }).on('click', '.commentEditCancel', function() + { + $(this).closest('.comment').removeClass('show-form'); + }).on('submit', '.comment-edit-form', function() + { + var $form = $(this); + $form.ajaxSubmit( + { + success:function(html) + { + var $comment = $form.closest('.comment'); + $comment.find('.comment-content').html(html); + $comment.removeClass('show-form'); + }, + beforeSubmit:function(formData, jqForm) + { + var form = jqForm[0]; + if(!form.commentText.value) + { + alert(contentError); + return false; + } + } + }); + return false; + }).on('click', '.commentDelete', function() + { + var $container = $(this).closest('.commentContainer'); + if(!$container.length) return; + + if(confirm(confirmDeleteComment)) + { + var commentID = $container.data('comment'); + var link = createLink('repo', 'deleteComment', 'commentID=' + commentID + '&confirm=yes'); + + $.get(link, function(data) + { + if(data == 'deleted') + { + var $commentRow = $container.closest('.comment-row'); + if($commentRow.find('.bugContainer, .commentContainer').length === 1) + { + $commentRow.removeClass('show').prev('tr').removeClass('commented'); + } + $container.remove(); + } + }); + } + return false; + }).on('click', 'tr.commented', function() + { + toggleComment($(this)); + }).on('click', '.panel-bug > .panel-heading', function() + { + $(this).closest('.panel-bug').toggleClass('show'); + }).on('mouseenter', 'tr.commented td .preview-icon', function(e) + { + var $cell = $(this).closest('td'); + var $row = $cell.closest('tr'); + var $commentRow = $row.next('tr'); + + var $bugs = $commentRow.find('.panel-bug'), line = '?'; + $bugsPreviewMenu.children('li:not(.dropdown-header)').remove(); + $bugsPreviewMenu.find('.bug-count').text($bugs.length); + $bugsPreviewMenu.find('.comment-count').text($commentRow.find('.comment').length); + $bugs.each(function() + { + var bug = $(this).data('data'); + line = bug.line; + $bugsPreviewMenu.append('
  • #' + bug.id + ' ' + bug.title + '
  • '); + }); + $bugsPreviewMenu.find('.code-line').text(line); + + $bugsPreview.prependTo($cell); + clearTimeout(hidePreview); + $bugsPreviewMenu.css({top: 0-$bugsPreviewMenu.outerHeight(), left: Math.max(0, e.offsetX-$bugsPreviewMenu.outerWidth())}).addClass('show'); + setTimeout(function(){$bugsPreviewMenu.addClass('in');}, 50); + }).on('mouseleave', 'tr.commented td', function() + { + $bugsPreviewMenu.removeClass('in'); + hidePreview = setTimeout(function(){$bugsPreviewMenu.removeClass('show');}, 200); + }); + + $bugsPreviewMenu.on('click', 'li', function(e) + { + var $bug = $($(this).find('a').data('id')); + if($bug.length) + { + $bug.addClass('show'); + toggleComment($bug.closest('tr.comment-row'), true); + highlight($bug); + + $bugsPreviewMenu.removeClass('in'); + hidePreview = setTimeout(function(){$bugsPreviewMenu.removeClass('show');}, 200); + } + e.stopPropagation(); + }); + + $bugForm.submit(function() + { + $(this).ajaxSubmit( + { + success:function(json) + { + json = $.parseJSON(json); + if(json.result == 'fail') + { + alert(json.message); + return false; + } + + createBug(json, null, null, 3); + $diffCode.removeClass('with-action-row'); + $diffCode.find('tr.with-action-row.selected').removeClass('selected'); + $bugForm.find('#title').val(''); + KindEditor.html('#commentText', ''); + }, + beforeSubmit:function(formData, jqForm) + { + var form = jqForm[0]; + if(!form.product.value) + { + alert(productError); + return false; + } + if(!form.title.value) + { + alert(titleError); + $bugForm.find('input[name="title"]').focus(); + return false; + } + } + }); + return false; + }).on('change', 'input[name="begin"]', function() + { + var begin = $(this).val(); + var $end = $bugForm.find('input[name="end"]').attr('min', begin); + if(parseInt($end.val()) < parseInt(begin)) + { + $end.val(begin); + } + }); + + if(bugs) + { + var lineBugs, bugsCount, i; + for(var line in bugs) + { + if(line) + { + lineBugs = bugs[line]; + bugsCount = lineBugs.length; + + for(i = 0; i < bugsCount; i++) + { + createBug(lineBugs[i], line); + } + } + } + } + + setTimeout(anchor, 200); + + $(document).on('click', function() + { + $('.highlight').removeClass('highlight'); + }); + + function anchor() + { + var hash = window.location.hash; + if(hash) + { + var line = hash.substr(1).replace('L', ''); + var $row = $('.diff tr[data-line="' + line +'"]').first(); + if($row.length) + { + var anchor = $row.offset().top; + + $('body,html').animate({scrollTop:anchor - 50}, 500); + + $row.addClass('highlight'); + if($row.hasClass('commented')) + { + toggleComment($row, true); + var $commentRow = $row.next('tr'); + if($commentRow.hasClass('comment-row')) + { + $commentRow.addClass('highlight'); + } + } + } + } + } +}); diff --git a/module/mr/js/edit.js b/module/mr/js/edit.js new file mode 100644 index 0000000000..9c443fa984 --- /dev/null +++ b/module/mr/js/edit.js @@ -0,0 +1,31 @@ +$(function () + { + $('#repoID').change(function () + { + repoID = $(this).val(); + jobUrl = createLink('mr', 'ajaxGetJobList', "repoID=" + repoID); + $.get(jobUrl, function (response) + { + $('#jobID').html('').append(response); + $('#jobID').chosen().trigger("chosen:updated");; + }); + }); + + $('#jobID').change(function () + { + jobID = $(this).val(); + compileUrl = createLink('mr', 'ajaxGetCompileList', "job=" + jobID); + $.get(compileUrl, function (response) + { + $('#compile').html('').append(response); + $('#compile').chosen().trigger("chosen:updated");; + }); + }); + + $("#needCI").change(function() + { + if(this.checked == false) $("#jobID").parent().parent().addClass('hidden'); + if(this.checked == true) $("#jobID").parent().parent().removeClass('hidden'); + }); + $("#needCI").trigger('change'); + }); diff --git a/module/mr/js/link.js b/module/mr/js/link.js new file mode 100644 index 0000000000..343413fbf9 --- /dev/null +++ b/module/mr/js/link.js @@ -0,0 +1,86 @@ +function showLink(productID, type, orderBy, param) +{ + if(type == 'story') method = 'linkStory'; + else if(type == 'bug') method = 'linkBug'; + else if(type == 'task') method = 'linkTask'; + + loadURL(createLink('mr', method, 'MRID=' + MRID + '&productID=' + productID + (typeof(param) == 'undefined' ? '' : param) + (typeof(orderBy) == 'undefined' ? '' : "&orderBy=" + orderBy)), type); + + $('.actions').find("a[href*='" + type + "']").addClass('hidden'); +} + +/** + * Load URL. + * + * @param string $url + * @param string $type + * @access public + * @return void + */ +function loadURL(url, type) +{ + $.get(url, function(data) + { + var $pane = $(type == 'story' ? '#stories' : (type == 'bug' ? '#bugs' : '#tasks')); + $pane.find('.main-table').hide(); + var $linkBox = $pane.find('.linkBox').html(data).removeClass('hidden'); + $linkBox.html(data).removeClass('hidden'); + $linkBox.find('[data-ride="table"]').table(); + $linkBox.find('[data-ride="pager"]').pager(); + $linkBox.find('[data-ride="pager"] li a.pager-item').click(function() + { + loadURL($(this).attr('href'), type); + return false; + }); + $linkBox.find('[data-ride="pager"] .pager-size-menu a[data-size]').off('click'); + $linkBox.find('[data-ride="pager"] .pager-size-menu a[data-size]').click(function() + { + line = $linkBox.find('[data-ride="pager"]').attr('data-link-creator'); + line = line.replace('{recPerPage}', $(this).attr('data-size')).replace('{page}', $linkBox.find('[data-ride="pager"]').attr('data-page')); + $.cookie($linkBox.find('[data-ride="pager"]').attr('data-page-cookie'), $(this).attr('data-size'), {expires:config.cookieLife, path:config.webRoot}); + loadURL(line, type); + return false; + }); + $.toggleQueryBox(true, $linkBox.find('#queryBox')); + }); +} + +$(function() +{ + if(link == 'true') showLink(productID, type, orderBy, param); + var infoShowed = false; + $('.nav.nav-tabs a[data-toggle="tab"]').on('shown.zui.tab', function(e) + { + var href = $(e.target).attr('href'); + var tabPane = $(href + '.tab-pane'); + if(tabPane.size() == 0) return; + var formID = tabPane.find('.linkBox').find('form:last'); + if(formID.size() == 0) formID = tabPane.find('form:last'); + if(href == '#planInfo' && !infoShowed) + { + $('#planInfo img').each(function() + { + var $tr = $('#planInfo .detail-content .table-data tbody tr:first'); + width = $tr.width() - $tr.find('th').width(); + if($(this).parent().prop('tagName').toLowerCase() == 'a') $(this).unwrap(); + setImageSize($(this), width, 0); + }); + + infoShowed = true; + } + }); + + $('#storyList').on('sort.sortable', function(e, data) + { + var list = ''; + for(i = 0; i < data.list.length; i++) list += $(data.list[i].item).attr('data-id') + ','; + $.post(createLink('productplan', 'ajaxStorySort', 'productID=' + productID), {'stories' : list, 'orderBy' : orderBy, 'pageID' : storyPageID, 'recPerPage' : storyRecPerPage, 'recTotal' : storyRecTotal}, function() + { + var $target = $(data.element[0]); + $target.hide(); + $target.fadeIn(1000); + order = 'order_asc'; + history.pushState({}, 0, createLink('productplan', 'view', "productID=" + productID + '&type=story&orderBy=' + order)); + }); + }); +}); diff --git a/module/mr/lang/en.php b/module/mr/lang/en.php index 9778fcc660..224a750b11 100644 --- a/module/mr/lang/en.php +++ b/module/mr/lang/en.php @@ -1,6 +1,7 @@ mr = new stdclass; $lang->mr->common = "Merge Request"; +$lang->mr->overview = "Survey"; $lang->mr->create = "Create"; $lang->mr->browse = "Browse"; $lang->mr->list = "List"; @@ -11,7 +12,17 @@ $lang->mr->accept = "Accept"; $lang->mr->source = 'source'; $lang->mr->target = 'target'; $lang->mr->viewDiff = 'View diff'; +$lang->mr->diff = 'View diff'; $lang->mr->viewInGitlab = 'View in GitLab'; +$lang->mr->link = 'Link of stories,Bugs,tasks'; +$lang->mr->createAction = '%s, %s submitted a Merge Request.'; + +$lang->mr->linkList = 'Link List of stories,Bugs,tasks'; +$lang->mr->linkStory = 'Link Stories'; +$lang->mr->linkBug = 'Link Bugs'; +$lang->mr->linkTask = 'Link Tasks'; +$lang->mr->unlink = 'UnLink of stories,Bugs,tasks'; +$lang->mr->addBug = 'Add Review'; $lang->mr->id = 'ID'; $lang->mr->mriid = "raw MR ID"; @@ -24,11 +35,40 @@ $lang->mr->mergeStatus = 'Merge status'; $lang->mr->commits = 'commits'; $lang->mr->changes = 'changes'; $lang->mr->gitlabID = 'GitLab'; +$lang->mr->repoID = 'Repo'; +$lang->mr->jobID = 'Compile job'; + +$lang->mr->approval = 'Approval'; +$lang->mr->approve = 'Approve'; +$lang->mr->reject = 'Reject'; +$lang->mr->close = 'Close'; +$lang->mr->reopen = 'Reopen'; + +$lang->mr->approvalResult = 'Approval result'; +$lang->mr->approvalResultList = array(); +$lang->mr->approvalResultList['approve'] = 'Approve'; +$lang->mr->approvalResultList['reject'] = 'Reject'; + +$lang->mr->needApproved = 'This MR should be approved before merge'; +$lang->mr->needCI = 'This MR should be passed CI before merge'; + +$lang->mr->repeatedOperation = 'Do not repeat operations'; + +$lang->mr->approvalStatus = 'Approve status'; +$lang->mr->approvalStatusList = array(); +$lang->mr->approvalStatusList['notReviewed'] = 'notReviewed'; +$lang->mr->approvalStatusList['approved'] = 'Approved'; +$lang->mr->approvalStatusList['rejected'] = 'Rejected'; + +$lang->mr->notApproved = 'Rejected'; +$lang->mr->assignedToMe = 'AssignedToMe'; +$lang->mr->createdByMe = 'CreatedByMe'; $lang->mr->statusList = array(); +$lang->mr->statusList['all'] = 'all'; $lang->mr->statusList['opened'] = 'opened'; -$lang->mr->statusList['closed'] = 'closed'; $lang->mr->statusList['merged'] = 'merged'; +$lang->mr->statusList['closed'] = 'closed'; $lang->mr->mergeStatusList = array(); $lang->mr->mergeStatusList['checking'] = 'checking'; @@ -42,8 +82,11 @@ $lang->mr->sourceBranch = 'Source branch'; $lang->mr->targetProject = 'Target project'; $lang->mr->targetBranch = 'Target branch'; -$lang->mr->usersTips = 'Tip: If you cannot choose the assignee, please go to the GitLab page to bind the user first.'; -$lang->mr->notFound = "Merge Request does not exist!"; +$lang->mr->usersTips = 'Tip: If you cannot choose the assignee, please go to the GitLab page to bind the user first.'; +$lang->mr->notFound = "Merge Request does not exist!"; +$lang->mr->toCreatedMessage = "The merge request you submitted:%s, the build task succeeded."; +$lang->mr->toReviewerMessage = "You have one merge request %s waiting."; +$lang->mr->failMessage = "Your merge request %s failed. Please check its execution result. "; $lang->mr->apiError = new stdclass; $lang->mr->apiError->createMR = "Failed to create a merge request through API. Reason: %s"; @@ -51,6 +94,8 @@ $lang->mr->apiError->sudo = "Unable to operate with the GitLab account bound $lang->mr->createFailedFromAPI = "Failed to create Merge Request."; $lang->mr->accessGitlabFailed = "Unable to connect to the GitLab server."; +$lang->mr->reopenSuccess = "The merge request was reopened."; +$lang->mr->closeSuccess = "Merge request closed."; $lang->mr->from = "from"; $lang->mr->to = "to"; @@ -59,7 +104,7 @@ $lang->mr->at = "at"; $lang->mr->pipeline = "Pipeline"; $lang->mr->pipelineSuccess = "Success"; $lang->mr->pipelineFailed = "Failed"; -$lang->mr->pipelineCancled = "Canceled"; +$lang->mr->pipelineCanceled = "Canceled"; $lang->mr->pipelineUnknown = "Unknown"; $lang->mr->pipelineStatus = array(); @@ -70,8 +115,6 @@ $lang->mr->pipelineStatus['canceled'] = "canceled"; $lang->mr->MRHasConflicts = "Merge Request has a conflict"; $lang->mr->hasConflicts = "There are merge conflicts or wait for push"; $lang->mr->hasNoConflict = "Can merge"; -$lang->mr->mergeByManual = "This merge request can be merged manually, please refer to"; -$lang->mr->commandLine = "Merge Request command"; $lang->mr->acceptMR = "Accept Merge request "; $lang->mr->mergeFailed = "Unable to merge request, please check the merge request status"; $lang->mr->mergeSuccess = "Merge Request Successfully"; @@ -91,15 +134,15 @@ $lang->mr->todomessage = "project was assigned to you"; $lang->mr->commandDocument = <<< EOD
    Check out, review and merge locally
    -

    Note: This merge request status will be changed after you merge locally and you will need to delete this merge request or submit new code.

    +

    Note: This merge request status will be changed automatically after you merged locally.

    - step 1. Fetch and check out the branch for this merge request + step 1. Change directory to target project. Fetch and check out the branch for this merge request

         git fetch "%s" %s
         git checkout -b "%s" FETCH_HEAD

    - step 2. Review the changes locally + step 2. Review the changes locally. You can use git log to view the changes

    step 3. Merge the branch and fix any conflicts that come up @@ -114,3 +157,12 @@ $lang->mr->commandDocument = <<< EOD

    EOD; + +$lang->mr->noChanges = "Currently there are no changes in this merge request's source branch. Please push new commits or use a different branch."; + +$lang->mr->linkTask = "Link task"; +$lang->mr->unlinkTask = "Remove task"; +$lang->mr->linkedTasks = 'Task'; +$lang->mr->unlinkedTasks = 'Task not linked'; +$lang->mr->confirmUnlinkTask = "Are you sure to remove this task?"; +$lang->mr->taskSummary = "There are %s tasks on this page"; diff --git a/module/mr/lang/zh-cn.php b/module/mr/lang/zh-cn.php index 12aa3f859d..c5b77246d7 100644 --- a/module/mr/lang/zh-cn.php +++ b/module/mr/lang/zh-cn.php @@ -1,6 +1,7 @@ mr = new stdclass; $lang->mr->common = "合并请求"; +$lang->mr->overview = "概况"; $lang->mr->create = "创建{$lang->mr->common}"; $lang->mr->browse = "浏览{$lang->mr->common}"; $lang->mr->list = $lang->mr->browse; @@ -11,7 +12,17 @@ $lang->mr->accept = "合并请求"; $lang->mr->source = '源项目分支'; $lang->mr->target = '目标项目分支'; $lang->mr->viewDiff = '比对代码'; +$lang->mr->diff = '比对代码'; $lang->mr->viewInGitlab = '在GitLab查看'; +$lang->mr->link = '关联需求、Bug、任务'; +$lang->mr->createAction = '%s, 由 %s 提交了 合并请求。'; + +$lang->mr->linkList = '浏览关联需求、Bug、任务'; +$lang->mr->linkStory = '关联需求'; +$lang->mr->linkBug = '关联Bug'; +$lang->mr->linkTask = '关联任务'; +$lang->mr->unlink = '取消关联需求、Bug、任务'; +$lang->mr->addBug = '添加评审'; $lang->mr->id = 'ID'; $lang->mr->mriid = "MR原始ID"; @@ -24,26 +35,63 @@ $lang->mr->mergeStatus = '是否可合并'; $lang->mr->commits = '提交数'; $lang->mr->changes = '更改数'; $lang->mr->gitlabID = 'GitLab'; +$lang->mr->repoID = '版本库'; +$lang->mr->jobID = '构建任务'; + +$lang->mr->canMerge = "可合并"; +$lang->mr->cantMerge = "不可合并"; + +$lang->mr->approval = '评审'; +$lang->mr->approve = '通过'; +$lang->mr->reject = '拒绝'; +$lang->mr->close = '关闭'; +$lang->mr->reopen = '重新打开'; + +$lang->mr->approvalResult = '评审意见'; +$lang->mr->approvalResultList = array(); +$lang->mr->approvalResultList['approve'] = '通过'; +$lang->mr->approvalResultList['reject'] = '拒绝'; + +$lang->mr->needApproved = '需要通过评审才能合并'; +$lang->mr->needCI = '需要通过构建才能合并'; + +$lang->mr->repeatedOperation = '请勿重复操作'; + +$lang->mr->approvalStatus = '审核状态'; +$lang->mr->approvalStatusList = array(); +$lang->mr->approvalStatusList['notReviewed'] = '未评审'; +$lang->mr->approvalStatusList['approved'] = '已通过'; +$lang->mr->approvalStatusList['rejected'] = '已拒绝'; + +$lang->mr->notApproved = '审核拒绝的'; +$lang->mr->assignedToMe = '指派给我的'; +$lang->mr->createdByMe = '我创建的'; $lang->mr->statusList = array(); +$lang->mr->statusList['all'] = '所有'; $lang->mr->statusList['opened'] = '开放中'; -$lang->mr->statusList['closed'] = '已关闭'; $lang->mr->statusList['merged'] = '已合并'; +$lang->mr->statusList['closed'] = '已关闭'; $lang->mr->mergeStatusList = array(); $lang->mr->mergeStatusList['checking'] = '检查中'; $lang->mr->mergeStatusList['can_be_merged'] = '可合并'; $lang->mr->mergeStatusList['cannot_be_merged'] = '不可自动合并'; -$lang->mr->description = '描述'; -$lang->mr->confirmDelete = '确认删除该合并请求吗?'; -$lang->mr->sourceProject = '源项目'; -$lang->mr->sourceBranch = '源分支'; -$lang->mr->targetProject = '目标项目'; -$lang->mr->targetBranch = '目标分支'; +$lang->mr->description = '描述'; +$lang->mr->confirmDelete = '确认删除该合并请求吗?'; +$lang->mr->sourceProject = '源项目'; +$lang->mr->sourceBranch = '源分支'; +$lang->mr->targetProject = '目标项目'; +$lang->mr->targetBranch = '目标分支'; +$lang->mr->noCompileJob = '没有构建任务'; +$lang->mr->compileUnexecuted = '还未执行'; -$lang->mr->usersTips = '提示:如果无法选择指派人,请先前往GitLab页面绑定用户。'; -$lang->mr->notFound = "此{$lang->mr->common}不存在。"; +$lang->mr->usersTips = '提示:如果无法选择指派人,请先前往GitLab页面绑定用户。'; +$lang->mr->notFound = "此{$lang->mr->common}不存在。"; +$lang->mr->toCreatedMessage = "您提交的合并请求:%s 构建任务执行通过。"; +$lang->mr->toReviewerMessage = "有一个合并请求:%s 待审核。"; +$lang->mr->failMessage = "您提交的合并请求:%s 构建任务执行失败,查看执行结果。"; $lang->mr->apiError = new stdclass; $lang->mr->apiError->createMR = "通过API创建合并请求失败,失败原因:%s"; @@ -51,6 +99,8 @@ $lang->mr->apiError->sudo = "无法以当前用户绑定的GitLab账户进 $lang->mr->createFailedFromAPI = "创建合并请求失败。"; $lang->mr->accessGitlabFailed = "当前无法连接到GitLab服务器。"; +$lang->mr->reopenSuccess = "已重新打开合并请求。"; +$lang->mr->closeSuccess = "已关闭合并请求。"; $lang->mr->from = "从"; $lang->mr->to = "合并到"; @@ -59,7 +109,7 @@ $lang->mr->at = "于"; $lang->mr->pipeline = "流水线"; $lang->mr->pipelineSuccess = "已通过"; $lang->mr->pipelineFailed = "未通过"; -$lang->mr->pipelineCancled = "已取消"; +$lang->mr->pipelineCanceled = "已取消"; $lang->mr->pipelineUnknown = "未知"; $lang->mr->pipelineStatus = array(); @@ -70,8 +120,6 @@ $lang->mr->pipelineStatus['canceled'] = "已取消"; $lang->mr->MRHasConflicts = "是否存在冲突"; $lang->mr->hasConflicts = "存在冲突或等待提交"; $lang->mr->hasNoConflict = "可以合并"; -$lang->mr->mergeByManual = "此合并请求可以手动合并,请使用以下"; -$lang->mr->commandLine = "合并命令"; $lang->mr->acceptMR = "合并"; $lang->mr->mergeFailed = "无法合并,请核对合并请求状态"; $lang->mr->mergeSuccess = "已成功合并"; @@ -91,15 +139,15 @@ $lang->mr->todomessage = "项目中指派给你了"; $lang->mr->commandDocument = <<< EOD
    在本地检出、审核和手动合并
    -

    注意:您在本地合并后此合并请求将变为不可合并状态,需要删除此合并请求或者提交新的代码。

    +

    提示:您在本地合并完成后,该合并请求将自动更新为已合并状态。

    - 第 1 步. 获取并查看此合并请求的分支 + 第 1 步. 切换到目标项目所在目录,获取并查看此合并请求的分支

         git fetch "%s" %s
         git checkout -b "%s" FETCH_HEAD

    - 第 2 步. 在本地查看更改 + 第 2 步. 在本地查看更改,如使用git log等命令

    第 3 步. 合并分支并解决出现的任何冲突 @@ -114,3 +162,12 @@ $lang->mr->commandDocument = <<< EOD

    EOD; + +$lang->mr->noChanges = "目前在这个合并请求的源分支中没有变化,请推送新的提交或使用不同的分支。"; + +$lang->mr->linkTask = "关联任务"; +$lang->mr->unlinkTask = "移除任务"; +$lang->mr->linkedTasks = '任务'; +$lang->mr->unlinkedTasks = '未关联任务'; +$lang->mr->confirmUnlinkTask = "您确认移除该任务吗?"; +$lang->mr->taskSummary = "本页共 %s 个任务"; diff --git a/module/mr/lang/zh-tw.php b/module/mr/lang/zh-tw.php index d384a8ae23..3dd556094d 100644 --- a/module/mr/lang/zh-tw.php +++ b/module/mr/lang/zh-tw.php @@ -25,10 +25,37 @@ $lang->mr->commits = '提交數'; $lang->mr->changes = '更改數'; $lang->mr->gitlabID = 'GitLab'; +$lang->mr->approval = '評審'; +$lang->mr->approve = '通過'; +$lang->mr->reject = '拒絕'; +$lang->mr->close = '關閉'; +$lang->mr->reopen = '重新打開'; + +$lang->mr->approvalResult = '評審意見'; +$lang->mr->approvalResultList = array(); +$lang->mr->approvalResultList['approve'] = '通過'; +$lang->mr->approvalResultList['reject'] = '拒絕'; + +$lang->mr->needApproved = '需要通過評審才能合併'; +$lang->mr->needCI = '需要通過構建才能合併'; + +$lang->mr->repeatedOperation = '請勿重複操作'; + +$lang->mr->approvalStatus = '審核狀態'; +$lang->mr->approvalStatusList = array(); +$lang->mr->approvalStatusList['notReviewed'] = '未評審'; +$lang->mr->approvalStatusList['approved'] = '通過'; +$lang->mr->approvalStatusList['rejected'] = '拒絕'; + +$lang->mr->notApproved = '審核拒絕的'; +$lang->mr->assignedToMe = '指派給我的'; +$lang->mr->createdByMe = '我創建的'; + $lang->mr->statusList = array(); +$lang->mr->statusList['all'] = '所有'; $lang->mr->statusList['opened'] = '開放中'; -$lang->mr->statusList['closed'] = '已關閉'; $lang->mr->statusList['merged'] = '已合併'; +$lang->mr->statusList['closed'] = '已關閉'; $lang->mr->mergeStatusList = array(); $lang->mr->mergeStatusList['checking'] = '檢查中'; @@ -59,7 +86,7 @@ $lang->mr->at = "于"; $lang->mr->pipeline = "流水綫"; $lang->mr->pipelineSuccess = "已通過"; $lang->mr->pipelineFailed = "未通過"; -$lang->mr->pipelineCancled = "已取消"; +$lang->mr->pipelineCanceled = "已取消"; $lang->mr->pipelineUnknown = "未知"; $lang->mr->pipelineStatus = array(); @@ -70,8 +97,6 @@ $lang->mr->pipelineStatus['canceled'] = "已取消"; $lang->mr->MRHasConflicts = "是否存在衝突"; $lang->mr->hasConflicts = "存在衝突或等待提交"; $lang->mr->hasNoConflict = "可以合併"; -$lang->mr->mergeByManual = "此合併請求可以手動合併,請使用以下"; -$lang->mr->commandLine = "合併命令"; $lang->mr->acceptMR = "合併"; $lang->mr->mergeFailed = "無法合併,請核對合併請求狀態"; $lang->mr->mergeSuccess = "已成功合併"; @@ -91,15 +116,15 @@ $lang->mr->todomessage = "項目中指派給你了"; $lang->mr->commandDocument = <<< EOD
    在本地檢出、審核和手動合併
    -

    注意:您在本地合併後此合併請求將變為不可合併狀態,需要刪除此合併請求或者提交新的代碼。

    +

    提示:您在本地合併完成後,該合併請求將自動更新為以合併狀態。

    - 第 1 步. 獲取並查看此合併請求的分支 + 第 1 步. 切換到目標項目所在目錄,獲取並查看此合併請求的分支

         git fetch "%s" %s
         git checkout -b "%s" FETCH_HEAD

    - 第 2 步. 在本地查看更改 + 第 2 步. 在本地查看更改, 如使用git log等命令

    第 3 步. 合併分支並解決出現的任何衝突 @@ -114,3 +139,5 @@ $lang->mr->commandDocument = <<< EOD

    EOD; + +$lang->mr->noChanges = "目前在這個合併請求的源分支中沒有變化,請推送新的提交或使用不同的分支。"; diff --git a/module/mr/model.php b/module/mr/model.php index e7e185cd8b..93465c0bd2 100644 --- a/module/mr/model.php +++ b/module/mr/model.php @@ -38,16 +38,21 @@ class mrModel extends model /** * Get MR list of gitlab project. * + * @param string $mode + * @param string $param * @param string $orderBy * @param object $pager * @access public * @return array */ - public function getList($orderBy = 'id_desc', $pager = null) + public function getList($mode = 'all', $param = 'all', $orderBy = 'id_desc', $pager = null) { $MRList = $this->dao->select('*') ->from(TABLE_MR) ->where('deleted')->eq('0') + ->beginIF($mode == 'status' and $param != 'all')->andWhere('status')->eq($param)->fi() + ->beginIF($mode == 'assignee' and $param != 'all')->andWhere('assignee')->eq($param)->fi() + ->beginIF($mode == 'creator' and $param != 'all')->andWhere('createdBy')->eq($param)->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); @@ -80,12 +85,28 @@ class mrModel extends model public function create() { $MR = fixer::input('post') + ->setDefault('jobID', 0) + ->setDefault('repoID', 0) + ->setDefault('needCI', 0) ->add('createdBy', $this->app->user->account) ->add('createdDate', helper::now()) ->get(); + /* Exec Job */ + if(isset($MR->jobID) && $MR->jobID) + { + $pipeline = $this->loadModel('job')->exec($MR->jobID); + if(!empty($pipeline->queue)) + { + $compile = $this->loadModel('compile')->getByQueue($pipeline->queue); + $MR->compileID = $compile->id; + $MR->compileStatus = $compile->status; + } + } + $this->dao->insert(TABLE_MR)->data($MR, $this->config->mr->create->skippedFields) ->batchCheck($this->config->mr->create->requiredFields, 'notempty') + ->checkIF($MR->needCI, 'jobID', 'notempty') ->autoCheck() ->exec(); if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError()); @@ -98,7 +119,11 @@ class mrModel extends model $MRObject->target_branch = $MR->targetBranch; $MRObject->title = $MR->title; $MRObject->description = $MR->description; - $MRObject->assignee_ids = $MR->assignee; + if($MR->assignee) + { + $gitlabAssignee = $this->gitlab->getUserIDByZentaoAccount($this->post->gitlabID, $MR->assignee); + if($gitlabAssignee) $MRObject->assignee_ids = $gitlabAssignee; + } $rawMR = $this->apiCreateMR($this->post->gitlabID, $this->post->sourceProject, $MRObject); @@ -120,17 +145,13 @@ class mrModel extends model } /* Create a todo item for this MR. */ - $this->apiCreateMRTodo($this->post->gitlabID, $this->post->targetProject, $rawMR->iid); + if(empty($MR->jobID)) $this->apiCreateMRTodo($this->post->gitlabID, $this->post->targetProject, $rawMR->iid); $newMR = new stdclass; $newMR->mriid = $rawMR->iid; $newMR->status = $rawMR->state; $newMR->mergeStatus = $rawMR->merge_status; - /* Change gitlab user ID to zentao account. */ - $gitlabUsers = $this->gitlab->getUserIdAccountPairs($MR->gitlabID); - $newMR->assignee = zget($gitlabUsers, $MR->assignee, ''); - /* Update MR in Zentao database. */ $this->dao->update(TABLE_MR)->data($newMR) ->where('id')->eq($MRID) @@ -149,28 +170,47 @@ class mrModel extends model public function update($MRID) { $MR = fixer::input('post') + ->setDefault('jobID', 0) + ->setDefault('repoID', 0) + ->setDefault('needCI', 0) ->setDefault('editedBy', $this->app->user->account) ->setDefault('editedDate', helper::now()) + ->setIF($this->post->needCI == 0, 'jobID', 0) ->get(); + $oldMR = $this->getByID($MRID); + + $this->dao->update(TABLE_MR)->data($MR)->checkIF($MR->needCI, 'jobID', 'notempty'); + if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError()); + + /* Exec Job */ + if(isset($MR->jobID) && $MR->jobID) + { + $pipeline = $this->loadModel('job')->exec($MR->jobID); + + if(!empty($pipeline->queue) && $MR->jobID != $oldMR->jobID) + { + $compile = $this->loadModel('compile')->getByQueue($pipeline->queue); + $MR->compileID = $compile->id; + $MR->compileStatus = $compile->status; + } + } /* Update MR in GitLab. */ $newMR = new stdclass; $newMR->title = $MR->title; $newMR->description = $MR->description; - $newMR->assignee_ids = $MR->assignee; $newMR->target_branch = $MR->targetBranch; - - $oldMR = $this->getByID($MRID); + if($MR->assignee) + { + $gitlabAssignee = $this->gitlab->getUserIDByZentaoAccount($oldMR->gitlabID, $MR->assignee); + if($gitlabAssignee) $newMR->assignee_ids = $gitlabAssignee; + } /* Known issue: `reviewer_ids` takes no effect. */ $rawMR = $this->apiUpdateMR($oldMR->gitlabID, $oldMR->targetProject, $oldMR->mriid, $newMR); - /* Change gitlab user ID to zentao account. */ - $gitlabUsers = $this->gitlab->getUserIdAccountPairs($oldMR->gitlabID); - $MR->assignee = zget($gitlabUsers, $MR->assignee, ''); - /* Update MR in Zentao database. */ - $this->dao->update(TABLE_MR)->data($MR) + $this->dao->update(TABLE_MR)->data($MR, $this->config->mr->edit->skippedFields) ->where('id')->eq($MRID) ->batchCheck($this->config->mr->edit->requiredFields, 'notempty') ->autoCheck() @@ -207,9 +247,10 @@ class mrModel extends model if($optionType == 'userPairs') { $gitlabUserID = ''; - if(isset(($rawMR->$field)[0])) + if(isset($rawMR->$field)) { - $gitlabUserID = ($rawMR->$field)[0]->$options; + $values = $rawMR->$field; + if(isset($values[0])) $gitlabUserID = $values[0]->$options; } $value = zget($gitlabUsers, $gitlabUserID, ''); } @@ -258,9 +299,10 @@ class mrModel extends model if($optionType == 'userPairs') { $gitlabUserID = ''; - if(isset(($rawMR->$field)[0])) + if(isset($rawMR->$field)) { - $gitlabUserID = ($rawMR->$field)[0]->$options; + $values = $rawMR->$field; + if(isset($values[0])) $gitlabUserID = $values[0]->$options; } $value = zget($gitlabUsers, $gitlabUserID, ''); } @@ -272,6 +314,12 @@ class mrModel extends model $condition = (array)$newMR; if(empty($condition)) continue; + /* Update compile status of current MR object */ + if(isset($MR->needCI) and $MR->needCI == '1') + { + $newMR->compileStatus = empty($MR->compileID) ? 'failed' : $this->loadModel('compile')->getByID($MR->compileID)->status; + } + /* Update MR in Zentao database. */ $this->dao->update(TABLE_MR)->data($newMR) ->where('id')->eq($MR->id) @@ -359,11 +407,10 @@ class mrModel extends model return rtrim($gitlab->url, '/')."/dashboard/todos?project_id=$projectID&type=MergeRequest"; } - /** * Create MR by API. * - * @docs https://docs.gitlab.com/ee/api/merge_requests.html#create-mr + * @link https://docs.gitlab.com/ee/api/merge_requests.html#create-mr * @param int $gitlabID * @param int $projectID * @param object $MR @@ -379,7 +426,7 @@ class mrModel extends model /** * Get MR list by API. * - * @docs https://docs.gitlab.com/ee/api/merge_requests.html#list-project-merge-requests + * @link https://docs.gitlab.com/ee/api/merge_requests.html#list-project-merge-requests * @param int $gitlabID * @param int $projectID * @access public @@ -394,7 +441,7 @@ class mrModel extends model /** * Get single MR by API. * - * @docs https://docs.gitlab.com/ee/api/merge_requests.html#get-single-mr + * @link https://docs.gitlab.com/ee/api/merge_requests.html#get-single-mr * @param int $gitlabID * @param int $projectID targetProject * @param int $MRID @@ -410,7 +457,7 @@ class mrModel extends model /** * Update MR by API. * - * @docs https://docs.gitlab.com/ee/api/merge_requests.html#update-mr + * @link https://docs.gitlab.com/ee/api/merge_requests.html#update-mr * @param int $gitlabID * @param int $projectID * @param int $MRID @@ -427,7 +474,7 @@ class mrModel extends model /** * Delete MR by API. * - * @docs https://docs.gitlab.com/ee/api/merge_requests.html#delete-a-merge-request + * @link https://docs.gitlab.com/ee/api/merge_requests.html#delete-a-merge-request * @param int $gitlabID * @param int $projectID * @param int $MRID @@ -440,10 +487,42 @@ class mrModel extends model return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'DELETE'))); } + /** + * Close MR by API. + * + * @link https://docs.gitlab.com/ee/api/merge_requests.html#update-mr + * @param int $gitlabID + * @param int $projectID + * @param int $MRID + * @access public + * @return object + */ + public function apiCloseMR($gitlabID, $projectID, $MRID) + { + $url = sprintf($this->gitlab->getApiRoot($gitlabID), "/projects/$projectID/merge_requests/$MRID") . '&state_event=close'; + return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'PUT'))); + } + + /** + * Reopen MR by API. + * + * @link https://docs.gitlab.com/ee/api/merge_requests.html#update-mr + * @param int $gitlabID + * @param int $projectID + * @param int $MRID + * @access public + * @return object + */ + public function apiReopenMR($gitlabID, $projectID, $MRID) + { + $url = sprintf($this->gitlab->getApiRoot($gitlabID), "/projects/$projectID/merge_requests/$MRID") . '&state_event=reopen'; + return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'PUT'))); + } + /** * Accept MR by API. * - * @docs https://docs.gitlab.com/ee/api/merge_requests.html#accept-mr + * @link https://docs.gitlab.com/ee/api/merge_requests.html#accept-mr * @param int $gitlabID * @param int $projectID * @param int $MRID @@ -461,13 +540,15 @@ class mrModel extends model /** * Get MR diff versions by API. * - * @docs https://docs.gitlab.com/ee/api/merge_requests.html#get-mr-diff-versions + * @link https://docs.gitlab.com/ee/api/merge_requests.html#get-mr-diff-versions * @param object $MR + * @param string $encoding * @access public * @return object */ - public function getDiffs($MR) + public function getDiffs($MR, $encoding = '') { + $diffVersions = $this->apiGetDiffVersions($MR->gitlabID, $MR->targetProject, $MR->mriid); $gitlab = $this->gitlab->getByID($MR->gitlabID); $this->loadModel('repo'); @@ -478,13 +559,37 @@ class mrModel extends model $repo->path = sprintf($this->config->repo->gitlab->apiPath, $gitlab->url, $MR->targetProject); $repo->client = $gitlab->url; $repo->password = $gitlab->token; + $repo->account = ''; + $repo->encoding = $encoding; + $lines = array(); + $commitList = array(); + foreach ($diffVersions as $diffVersion) + { + $singleDiff = $this->apiGetSingleDiffVersion($MR->gitlabID, $MR->targetProject, $MR->mriid, $diffVersion->id); + if ($singleDiff->state == 'empty') continue; + $commits = $singleDiff->commits; + $diffs = $singleDiff->diffs; + foreach ($diffs as $index => $diff) + { + if(empty($commits[$index])) continue; + /* Make sure every file with same commitID is unique in $lines. */ + $shortID = $commits[$index]->short_id; + if(in_array($shortID, $commitList)) continue; + $commitList[] = $shortID; + + $lines[] = sprintf("diff --git a/%s b/%s", $diff->old_path, $diff->new_path); + $lines[] = sprintf("index %s ... %s %s ", $singleDiff->head_commit_sha, $singleDiff->base_commit_sha, $diff->b_mode); + $lines[] = sprintf("--a/%s", $diff->old_path); + $lines[] = sprintf("--b/%s", $diff->new_path); + $diffLines = explode("\n", $diff->diff); + foreach ($diffLines as $diffLine) $lines[] = $diffLine; + } + } $scm = $this->app->loadClass('scm'); $scm->setEngine($repo); - - $encoding = empty($encoding) ? $repo->encoding : $encoding; - $encoding = strtolower(str_replace('_', '-', $encoding)); - return $scm->diff('', $MR->sourceBranch, $MR->targetBranch, $parse = true, $MR->sourceProject); + $diff = $scm->engine->parseDiff($lines); + return $diff; } /** @@ -499,9 +604,9 @@ class mrModel extends model public function getSudoAccountPair($gitlabID, $projectID, $account) { $bindedUsers = $this->gitlab->getUserAccountIdPairs($gitlabID); - $accuntPair = array(); - if(isset($bindedUsers[$account])) $accuntPair[$account] = $bindedUsers[$account]; - return $accuntPair; + $accountPair = array(); + if(isset($bindedUsers[$account])) $accountPair[$account] = $bindedUsers[$account]; + return $accountPair; } /** @@ -525,7 +630,7 @@ class mrModel extends model if(!empty($bindedUsers[$rawProjectUser->username])) $users[$rawProjectUser->username] = $bindedUsers[$rawProjectUser->username]; } if(!empty($users[$zentaoUser])) return $users[$zentaoUser]; - return ""; + return ''; } /** @@ -542,4 +647,530 @@ class mrModel extends model $url = sprintf($this->gitlab->getApiRoot($gitlabID), "/projects/$projectID/merge_requests/$MRID/todo"); return json_decode(commonModel::http($url, $data = null, $options = array(CURLOPT_CUSTOMREQUEST => 'POST'))); } + + /** + * Get diff versions of MR from GitLab API. + * + * @param int $gitlabID + * @param int $projectID + * @param int $MRID + * @access public + * @return object + */ + public function apiGetDiffVersions($gitlabID, $projectID, $MRID) + { + $url = sprintf($this->gitlab->getApiRoot($gitlabID), "/projects/$projectID/merge_requests/$MRID/versions"); + return json_decode(commonModel::http($url)); + } + + /** + * Get a single diff version of MR from GitLab API. + * + * @param int $gitlabID + * @param int $projectID + * @param int $MRID + * @param int $versionID + * @access public + * @return object + */ + public function apiGetSingleDiffVersion($gitlabID, $projectID, $MRID, $versionID) + { + $url = sprintf($this->gitlab->getApiRoot($gitlabID), "/projects/$projectID/merge_requests/$MRID/versions/$versionID"); + return json_decode(commonModel::http($url)); + } + + /** + * Get diff commits of MR from GitLab API. + * + * @param int $gitlabID + * @param int $projectID + * @param int $MRID + * @access public + * @return object + */ + public function apiGetDiffCommits($gitlabID, $projectID, $MRID) + { + $url = sprintf($this->gitlab->getApiRoot($gitlabID), "/projects/$projectID/merge_requests/$MRID/commits"); + return json_decode(commonModel::http($url)); + } + + /** + * Reject or Approve this MR. + * + * @param object $MR + * @param string $action + * @param string $comment + * @return array + */ + public function approve($MR, $action = 'approve', $comment = '') + { + $this->loadModel('action'); + $actionID = $this->action->create('mrapproval', $MR->id, $action); + + $oldMR = $MR; + if(isset($MR->status) and $MR->status == 'opened') + { + $rawApprovalStatus = ''; + if(isset($MR->approvalStatus)) $rawApprovalStatus = $MR->approvalStatus; + $MR->approver = $this->app->user->account; + if ($action == 'reject' and $rawApprovalStatus != 'rejected') $MR->approvalStatus = 'rejected'; + if ($action == 'approve' and $rawApprovalStatus != 'approved') $MR->approvalStatus = 'approved'; + if (isset($MR->approvalStatus) and $rawApprovalStatus != $MR->approvalStatus) + { + $changes = common::createChanges($oldMR, $MR); + $this->action->logHistory($actionID, $changes); + $this->dao->update(TABLE_MR)->data($MR) + ->where('id')->eq($MR->id) + ->exec(); + if (dao::isError()) return array('result' => 'fail', 'message' => dao::getError()); + + /* Save approval history into db. */ + $approval = new stdClass; + $approval->date = helper::now(); + $approval->mrID = $MR->id; + $approval->account = $MR->approver; + $approval->action = $action; + $approval->comment = $comment; + $this->dao->insert(TABLE_MRAPPROVAL)->data($approval, $this->config->mrapproval->create->skippedFields) + ->batchCheck($this->config->mrapproval->create->requiredFields, 'notempty') + ->autoCheck() + ->exec(); + if (dao::isError()) return array('result' => 'fail', 'message' => dao::getError()); + + return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => 'parent.refresh()'); + } + } + return array('result' => 'fail', 'message' => $this->lang->mr->repeatedOperation, 'locate' => helper::createLink('mr', 'view', "mr={$MR->id}")); + } + + /** + * Close this MR. + * + * @param mixed $MR + * @return void + */ + public function close($MR) + { + $this->loadModel('action'); + $actionID = $this->action->create('mr', $MR->id, 'closed'); + $rawMR = $this->apiCloseMR($MR->gitlabID, $MR->targetProject, $MR->mriid); + $changes = common::createChanges($MR, $rawMR); + $this->action->logHistory($actionID, $changes); + if(isset($rawMR->state) and $rawMR->state == 'closed') return array('result' => 'success', 'message' => $this->lang->mr->closeSuccess, 'locate' => helper::createLink('mr', 'view', "mr={$MR->id}")); + return array('result' => 'fail', 'message' => $this->lang->fail, 'locate' => helper::createLink('mr', 'view', "mr={$MR->id}")); + } + + /** + * Reopen this MR. + * + * @param mixed $MR + * @return void + */ + public function reopen($MR) + { + $this->loadModel('action'); + $actionID = $this->action->create('mr', $MR->id, 'reopen'); + $rawMR = $this->apiReopenMR($MR->gitlabID, $MR->targetProject, $MR->mriid); + $changes = common::createChanges($MR, $rawMR); + $this->action->logHistory($actionID, $changes); + if(isset($rawMR->state) and $rawMR->state == 'opened') return array('result' => 'success', 'message' => $this->lang->mr->reopenSuccess, 'locate' => helper::createLink('mr', 'view', "mr={$MR->id}")); + return array('result' => 'fail', 'message' => $this->lang->fail, 'locate' => helper::createLink('mr', 'view', "mr={$MR->id}")); + } + + + /** + * Get review. + * + * @param int $repoID + * @param string $entry + * @param string $revision + * @access public + * @return array + */ + public function getReview($repoID, $entry, $revision) + { + $reviews = array(); + $bugs = $this->dao->select('t1.*, t2.realname')->from(TABLE_BUG)->alias('t1') + ->leftJoin(TABLE_USER)->alias('t2') + ->on('t1.openedBy = t2.account') + ->where('t1.repo')->eq($repoID) + ->andWhere('t1.entry')->eq($entry) + ->andWhere('t1.v2')->eq($revision) + ->andWhere('t1.deleted')->eq(0) + ->fetchAll('id'); + $comments = $this->dao->select('t1.*, t2.realname')->from(TABLE_ACTION)->alias('t1') + ->leftJoin(TABLE_USER)->alias('t2') + ->on('t1.actor = t2.account') + ->where('t1.objectType')->eq('bug') + ->andWhere('t1.objectID')->in(array_keys($bugs)) + ->andWhere('t1.action')->eq('commented') + ->fetchGroup('objectID', 'id'); + foreach($bugs as $bug) + { + if(common::hasPriv('bug', 'edit')) $bug->edit = true; + if(common::hasPriv('bug', 'delete')) $bug->delete = true; + $lines = explode(',', trim($bug->lines, ',')); + $line = $lines[0]; + $reviews[$line]['bugs'][$bug->id] = $bug; + + if(isset($comments[$bug->id])) + { + foreach($comments[$bug->id] as $key => $comment) + { + if($comment->actor == $this->app->user->account) $comment->edit = true; + } + $reviews[$line]['comments'] = $comments; + } + } + + return $reviews; + } + + /** + * Get bugs by repo. + * + * @param int $repoID + * @param string $browseType + * @param string $orderBy + * @param object $pager + * @access public + * @return array + */ + public function getBugsByRepo($repoID, $browseType, $orderBy, $pager) + { + /* Get execution that user can access. */ + $executions = $this->loadModel('execution')->getPairs($this->session->project, 'all', 'empty|withdelete'); + + $bugs = $this->dao->select('*')->from(TABLE_BUG) + ->where('repo')->eq($repoID) + ->andWhere('deleted')->eq('0') + ->beginIF(!$this->app->user->admin)->andWhere('product')->in($this->app->user->view->products)->fi() + ->beginIF(!$this->app->user->admin)->andWhere('execution')->in(array_keys($executions))->fi() + ->beginIF($browseType == 'assigntome')->andWhere('assignedTo')->eq($this->app->user->account)->fi() + ->beginIF($browseType == 'openedbyme')->andWhere('openedBy')->eq($this->app->user->account)->fi() + ->beginIF($browseType == 'resolvedbyme')->andWhere('resolvedBy')->eq($this->app->user->account)->fi() + ->beginIF($browseType == 'assigntonull')->andWhere('assignedTo')->eq('')->fi() + ->beginIF($browseType == 'unresolved')->andWhere('resolvedBy')->eq('')->fi() + ->beginIF($browseType == 'unclosed')->andWhere('status')->ne('closed')->fi() + ->orderBy($orderBy) + ->page($pager) + ->fetchAll(); + return $bugs; + } + + /** + * 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); + foreach($executions as $execution) $pairs[$execution->id] = $execution->name; + return $pairs; + } + + /** + * Save bug. + * + * @param int $repoID + * @param string $file + * @param int $v1 + * @param int $v2 + * @access public + * @return array + */ + public function saveBug($repoID, $file, $v1, $v2) + { + $now = helper::now(); + $data = fixer::input('post') + ->add('severity', 3) + ->add('openedBy', $this->app->user->account) + ->add('openedDate', $now) + ->add('openedBuild', 'trunk') + ->add('assignedDate', $now) + ->add('type', 'codeimprovement') + ->add('repo', $repoID) + ->add('entry', $file) + ->add('lines', $this->post->begin . ',' . $this->post->end) + ->add('v1', $v1) + ->add('v2', $v2) + ->remove('commentText,begin,end,uid') + ->get(); + + $data->steps = $this->loadModel('file')->pasteImage($this->post->commentText, $this->post->uid); + $this->dao->insert(TABLE_BUG)->data($data)->exec(); + + if(!dao::isError()) + { + $bugID = $this->dao->lastInsertID(); + $this->file->updateObjectID($this->post->uid, $bugID, 'bug'); + setcookie("repoPairs[$repoID]", $data->product); + + return array('result' => 'success', 'id' => $bugID, 'realname' => $this->app->user->realname, 'openedDate' => substr($now, 5, 11), 'edit' => true, 'delete' => true, 'lines' => $data->lines, 'line' => $this->post->begin, 'steps' => $data->steps, 'title' => $data->title); + } + + return array('result' => 'fail', 'message' => join("\n", dao::getError())); + } + + /** + * Update bug. + * + * @param int $bugID + * @param string $title + * @access public + * @return string + */ + public function updateBug($bugID, $title) + { + $this->dao->update(TABLE_BUG)->set('title')->eq($title)->where('id')->eq($bugID)->exec(); + return $title; + } + + /** + * Update comment. + * + * @param int $commentID + * @param string $comment + * @access public + * @return string + */ + public function updateComment($commentID, $comment) + { + $this->dao->update(TABLE_ACTION)->set('comment')->eq($comment)->where('id')->eq($commentID)->exec(); + return $comment; + } + + /** + * Delete comment. + * + * @param int $commentID + * @access public + * @return void + */ + public function deleteComment($commentID) + { + return $this->dao->delete()->from(TABLE_ACTION)->where('id')->eq($commentID)->exec(); + } + + /** + * Get last review info. + * + * @param string $entry + * @access public + * @return object + */ + public function getLastReviewInfo($entry) + { + return $this->dao->select('*')->from(TABLE_BUG)->where('entry')->eq($entry)->orderby('id_desc')->fetch(); + } + + /** + * Get mr link list. + * + * @param int $MRID + * @param int $productID + * @param string $type + * @param string $orderBy + * @param object $pager + * @access public + * @return array + */ + public function getLinkList($MRID, $productID, $type, $orderBy = 'id_desc', $pager = null) + { + $linkIDs = $this->dao->select('BID')->from(TABLE_RELATION) + ->where('product')->eq($productID) + ->andWhere('relation')->eq('interrated') + ->andWhere('AType')->eq('mr') + ->andWhere('AID')->eq($MRID) + ->andWhere('BType')->eq($type) + ->fetchPairs('BID'); + + $links = array(); + if($type == 'story' and !empty($linkIDs)) + { + $orderBy = str_replace('name_', 'title_', $orderBy); + $links = $this->dao->select('t1.*, t2.spec, t2.verify, t3.name as productTitle') + ->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_STORYSPEC)->alias('t2')->on('t1.id=t2.story') + ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product=t3.id') + ->where('t1.deleted')->eq(0) + ->andWhere('t1.version=t2.version') + ->andWhere('t1.id')->in($linkIDs) + ->orderBy($orderBy) + ->page($pager) + ->fetchAll('id'); + } + if($type == 'bug' and !empty($linkIDs)) + { + $orderBy = str_replace('name_', 'title_', $orderBy); + $links = $this->dao->select('*')->from(TABLE_BUG) + ->where('deleted')->eq(0) + ->andWhere('id')->in($linkIDs) + ->orderBy($orderBy) + ->page($pager) + ->fetchAll('id'); + } + if($type == 'task' and !empty($linkIDs)) + { + $orderBy = str_replace('title_', 'name_', $orderBy); + $links = $this->dao->select('*')->from(TABLE_TASK) + ->where('deleted')->eq(0) + ->andWhere('id')->in($linkIDs) + ->orderBy($orderBy) + ->page($pager) + ->fetchAll('id'); + } + + return $links; + } + + /** + * Create an mr link. + * + * @param int $MRID + * @param int $productID + * @param string $type + * @access public + * @return void + */ + public function link($MRID, $productID, $type) + { + $this->loadModel('action'); + if($type == 'story') $links = $this->post->stories; + if($type == 'bug') $links = $this->post->bugs; + if($type == 'task') $links = $this->post->tasks; + + /* Get link action text. */ + $MR = $this->getByID($MRID); + $users = $this->loadModel('user')->getPairs('noletter'); + $MRCreateAction = sprintf($this->lang->mr->createAction, $MR->createdDate, zget($users, $MR->createdBy), helper::createLink('mr', 'view', "mr={$MR->id}")); + + foreach($links as $linkID) + { + $relation = new stdclass; + $relation->product = $productID; + $relation->AType = 'mr'; + $relation->AID = $MRID; + $relation->relation = 'interrated'; + $relation->BType = $type; + $relation->BID = $linkID; + + $this->dao->replace(TABLE_RELATION)->data($relation)->exec(); + + if($type == 'story') $this->action->create('story', $linkID, 'createmr', '', $MRCreateAction); + if($type == 'bug') $this->action->create('bug', $linkID, 'createmr', '', $MRCreateAction); + if($type == 'task') $this->action->create('task', $linkID, 'createmr', '', $MRCreateAction); + } + } + + /** + * unLink an mr link. + * + * @param int $MRID + * @param int $productID + * @param string $type + * @param int $linkID + * @access public + * @return void + */ + public function unlink($MRID, $productID, $type, $linkID) + { + return $this->dao->delete()->from(TABLE_RELATION)->where('product')->eq($productID)->andWhere('AType')->eq('mr')->andWhere('AID')->eq($MRID)->andWhere('BType')->eq($type)->andWhere('BID')->eq($linkID)->exec(); + } + + /** + * Get links by mr commites. + * + * @param int $gitlabID + * @param int $projectID + * @param int $MRID + * @param string $type + * @access public + * @return array + */ + public function getCommitedLink($gitlabID, $projectID, $MRID, $type) + { + $DiffCommits = $this->apiGetDiffCommits($gitlabID, $projectID, $MRID); + + $commits = array(); + foreach($DiffCommits as $DiffCommit) + { + $commits[] = substr($DiffCommit->id, 0, 10); + } + + return $this->dao->select('objectID')->from(TABLE_ACTION)->where('objectType')->eq($type)->andWhere('extra')->in($commits)->fetchPairs('objectID'); + } + + /** + * Get mr product. + * + * @param object $MR + * @access public + * @return mix + */ + public function getMRProduct($MR) + { + $product = array(); + + if($MR->repoID) + { + $productID = $this->dao->select('product')->from(TABLE_REPO)->where('id')->eq($MR->repoID)->fetch('product'); + } + else + { + $products = $this->loadModel('gitlab')->getProductsByProjects(array($MR->targetProject, $MR->sourceProject)); + $productID = array_shift($products); + } + + if($productID) $product = $this->loadModel('product')->getById($productID); + return $product; + } + + /** + * Get toList and ccList. + * + * @param object $mr + * @access public + * @return bool|array + */ + public function getToAndCcList($mr) + { + return array($mr->createdBy, $mr->assignee); + } + + /** + * Log merged action to links. + * + * @param object $MR + * @access public + * @return void + */ + public function logMergedAction($MR) + { + $this->loadModel('action'); + $product = $this->getMRProduct($MR); + + $stories = $this->getLinkList($MR->id, $product->id, 'story'); + foreach($stories as $story) + { + $this->action->create('story', $story->id, 'mergedmr', '', helper::createLink('mr', 'view', "mr={$MR->id}")); + } + + $bugs = $this->getLinkList($MR->id, $product->id, 'bug'); + foreach($bugs as $bug) + { + $this->action->create('bug', $bug->id, 'mergedmr', '', helper::createLink('mr', 'view', "mr={$MR->id}")); + } + + $tasks = $this->getLinkList($MR->id, $product->id, 'task'); + foreach($tasks as $task) + { + $this->action->create('task', $task->id, 'mergedmr', '', helper::createLink('mr', 'view', "mr={$MR->id}")); + } + } } diff --git a/module/mr/view/approval.html.php b/module/mr/view/approval.html.php new file mode 100644 index 0000000000..bb47c9a38f --- /dev/null +++ b/module/mr/view/approval.html.php @@ -0,0 +1,61 @@ + + * @package mr + * @version $Id$ + * @link http://www.zentao.net + */ +?> + + + +
    +
    +
    + id; ?> +

    + title'>" . $MR->title . ' - ' . zget($lang->mr->approvalResultList, $action) . '') : html::a($this->createLink('mr', 'view', 'MR=' . $MR->id), $MR->title); ?> + + arrow . $lang->mr->approval; ?> + +

    +
    + id&action=$action")?>'> +
    job->repo; ?>
    job->repo; ?>
    job->product; ?>
    + needCI and $showCompileResult): ?> + + + + + + + + + + + + + + + + + + +
    compile->result; ?> + compile->statusList[$MR->compileStatus], '_blank'); ?> +
    mr->assignee; ?> + createdBy, "class='form-control chosen'"); ?> +
    comment; ?>
    + save); ?> + goback, $this->createLink('mr', 'view', 'MR=' . $MR->id), 'self', '', 'btn btn-wide'); ?> +
    +
    +
    +
    + + + diff --git a/module/mr/view/browse.html.php b/module/mr/view/browse.html.php index 1a42dfae53..1ff9c204a8 100644 --- a/module/mr/view/browse.html.php +++ b/module/mr/view/browse.html.php @@ -10,7 +10,30 @@ ?> - +
    - recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}"; ?> - - - - - - - - + recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}";?> + + + + + + + - - - - - - - + + + + + status == 'closed'):?> + + + + + + status == 'merged' or $MR->status == 'closed'):?> + + + + @@ -67,4 +97,4 @@ - + diff --git a/module/mr/view/create.html.php b/module/mr/view/create.html.php index 8326fe50a7..73daa976d0 100644 --- a/module/mr/view/create.html.php +++ b/module/mr/view/create.html.php @@ -1,4 +1,5 @@ - - + + - - + + - + - - + + + + + + + + + + + + + + - + @@ -60,7 +78,7 @@ @@ -70,4 +88,4 @@ - + diff --git a/module/mr/view/diff.html.php b/module/mr/view/diff.html.php index 3a5e0e20f0..85d2afadbe 100644 --- a/module/mr/view/diff.html.php +++ b/module/mr/view/diff.html.php @@ -9,139 +9,144 @@ */ ?> - - + + - -
    -
    -
    -
    - repo->viewDiffList['inline'], "id='inline'", $arrange == 'inline' ? 'active btn btn-sm' : 'btn btn-sm')?> - repo->viewDiffList['appose'], "id='appose'", $arrange == 'appose' ? 'active btn btn-sm' : 'btn btn-sm')?> -
    -
    -
    -
    - repo->encodingList, $encoding, $lang->repo->encoding) . "", "data-toggle='dropdown'", 'btn dropdown-toggle btn-sm')?> - +
    +
    + +
    + + + + id)): ?> +
    +
    +

    + mr->notFound; ?> + createLink('mr', 'browse'), " " . $lang->mr->browse, '', "class='btn btn-info'"); ?> +

    + + +
    +
    + +
    + repo->viewDiffList['inline'], "id='inline'", $arrange == 'inline' ? 'active btn btn-sm' : 'btn btn-sm')?> + repo->viewDiffList['appose'], "id='appose'", $arrange == 'appose' ? 'active btn btn-sm' : 'btn btn-sm')?> +
    +
    +
    +
    + repo->encodingList, $encoding, $lang->repo->encoding) . "", "data-toggle='dropdown'", 'btn dropdown-toggle btn-sm')?> + +
    +
    +
    + + +
    + +
    +
    mr->id); ?>mr->title); ?>mr->sourceProject); ?>mr->sourceBranch); ?>mr->targetProject); ?>mr->targetBranch); ?>mr->mergeStatus); ?>actions; ?>mr->id);?>mr->title);?>mr->sourceBranch);?>mr->targetBranch);?>mr->mergeStatus);?>mr->approvalStatus);?>actions;?>
    id; ?>title; ?>loadModel('gitlab')->apiGetSingleProject($MR->gitlabID, $MR->sourceProject)->name_with_namespace; ?>sourceBranch;?>loadModel('gitlab')->apiGetSingleProject($MR->gitlabID, $MR->targetProject)->name_with_namespace; ?>targetBranch;?>status == 'merged') ? zget($lang->mr->statusList, $MR->status) : zget($lang->mr->mergeStatusList, $MR->mergeStatus); ?>id;?>id}"), $MR->title);?>loadModel('gitlab')->apiGetSingleProject($MR->gitlabID, $MR->sourceProject)->name_with_namespace . ':' . $MR->sourceBranch;?>loadModel('gitlab')->apiGetSingleProject($MR->gitlabID, $MR->targetProject)->name_with_namespace . ':' . $MR->targetBranch;?>mr->statusList, $MR->status);?>status == 'merged') ? zget($lang->mr->statusList, $MR->status) : zget($lang->mr->mergeStatusList, $MR->mergeStatus);?> approvalStatus) ? $lang->mr->approvalStatusList['notReviewed'] : $lang->mr->approvalStatusList[$MR->approvalStatus];?> id}", '', '', "title='{$lang->mr->view}' class='btn btn-info'"); common::printLink('mr', 'edit', "mr={$MR->id}", '', '', "title='{$lang->mr->edit}' class='btn btn-info'"); - /* Function diff is not ready yet. so comment it. */ - //common::printLink('mr', 'diff', "mr={$MR->id}", '', '', "title='{$lang->mr->viewDiff}' class='btn btn-info'"); + common::printLink('mr', 'diff', "mr={$MR->id}", '', '', "title='{$lang->mr->viewDiff}' class='btn btn-info'"); + common::printLink('mr', 'link', "mr={$MR->id}", '', '', "title='{$lang->mr->link}' class='btn btn-info'" . ($MR->linkButton == false ? 'disabled' : '')); common::printLink('mr', 'delete', "mr={$MR->id}", '', 'hiddenwin', "title='{$lang->mr->delete}' class='btn btn-info'"); ?>
    mr->sourceProject;?> -
    - - mr->sourceBranch ?> - -
    -
    mr->sourceProject;?> +
    + + mr->sourceBranch ?> + +
    +
    mr->targetProject;?> -
    - - mr->targetBranch ?> - -
    -
    mr->targetProject;?> +
    + + mr->targetBranch ?> + +
    +
    mr->title;?>
    mr->description; ?>mr->description;?>
    devops->repo;?>
    mr->needCI;?> +
    + + +
    +
    mr->assignee;?>
    - + goback, '', 'class="btn btn-wide"');?>
    + + contents)) continue;?> + contents as $content):?> + oldStartLine; + $newCurrentLine = $content->newStartLine; + ?> + + + + + + + + + + + + lines as $line):?> + + + + + + + + lines as $line):?> + + type == 'old') + { + $oldlc = $line->oldlc; + $newlc = ''; + if(isset($content->new[$oldlc])) + { + $newlc = $line->oldlc; + $line->type = 'custom'; + } + } + else + { + $oldlc = $line->oldlc; + $newlc = $line->newlc; + if(!isset($content->new[$newlc])) continue; + } + ?> + + + + + old[$oldlc])) unset($content->old[$oldlc]); + if(isset($content->new[$newlc])) unset($content->new[$newlc]); + ?> + + + + +
    fileName;?>
    ......
    type != 'new') echo $line->oldlc?>type != 'old') echo $line->newlc?>type == 'old' ? preg_replace('/^\-/', '–', $line->line) : ($line->type == 'new' ? $line->line : ' ' . $line->line); + ?>
    type?> type == 'custom') echo "line-old"?> code'>old[$oldlc])) $content->old[$oldlc] = ''; + if(!empty($oldlc)) echo $line->type != 'all' ? preg_replace('/^\-/', '–', $content->old[$oldlc]) : ' ' . $content->old[$oldlc]; + ?>type?> type == 'custom') echo "line-new"?> code'>new[$newlc])) $content->new[$newlc] = ''; + if(!empty($newlc)) echo $line->type != 'all' ? $content->new[$newlc] : ' ' . $content->new[$newlc]; + ?>
    + + - - - - -
    - - - contents)) continue;?> - contents as $content):?> - oldStartLine; - $newCurrentLine = $content->newStartLine; - ?> - - - - - - - - - - - - lines as $line):?> - - - - - - - - lines as $line):?> - - type == 'old') - { - $oldlc = $line->oldlc; - $newlc = ''; - if(isset($content->new[$oldlc])) - { - $newlc = $line->oldlc; - $line->type = 'custom'; - } - } - else - { - $oldlc = $line->oldlc; - $newlc = $line->newlc; - if(!isset($content->new[$newlc])) continue; - } - ?> - - - - - old[$oldlc])) unset($content->old[$oldlc]); - if(isset($content->new[$newlc])) unset($content->new[$newlc]); - ?> - - + mr->noChanges;?> + + + + - -
    fileName;?>
    ......
    type != 'new') echo $line->oldlc?>type != 'old') echo $line->newlc?>line = $repo->SCM == 'Subversion' ? htmlSpecialString($line->line) : $line->line; - echo $line->type == 'old' ? preg_replace('/^\-/', '–', $line->line) : ($line->type == 'new' ? $line->line : ' ' . $line->line); - ?>
    type?> type == 'custom') echo "line-old"?> code'>old[$oldlc])) $content->old[$oldlc] = ''; - $content->old[$oldlc] = $repo->SCM == 'Subversion' ? htmlSpecialString($content->old[$oldlc]) : $content->old[$oldlc]; - if(!empty($oldlc)) echo $line->type != 'all' ? preg_replace('/^\-/', '–', $content->old[$oldlc]) : ' ' . $content->old[$oldlc]; - ?>type?> type == 'custom') echo "line-new"?> code'>new[$newlc])) $content->new[$newlc] = ''; - $content->new[$newlc] = $repo->SCM == 'Subversion' ? htmlSpecialString($content->new[$newlc]) : $content->new[$newlc]; - if(!empty($newlc)) echo $line->type != 'all' ? $content->new[$newlc] : ' ' . $content->new[$newlc]; - ?>
    +
    - - - diff --git a/module/mr/view/edit.html.php b/module/mr/view/edit.html.php index e00b51a734..5527387a32 100644 --- a/module/mr/view/edit.html.php +++ b/module/mr/view/edit.html.php @@ -9,6 +9,20 @@ */ ?> +gitlabID);?> +sourceProject);?> + +id)): ?> +
    +
    +

    + mr->notFound;?> + createLink('mr', 'browse'), " " . $lang->mr->browse, '', "class='btn btn-info'");?> +

    +
    +
    + +
    @@ -26,7 +40,7 @@
    - loadModel('gitlab')->apiGetSingleProject($MR->gitlabID, $MR->sourceProject)->name_with_namespace; ?>: + loadModel('gitlab')->apiGetSingleProject($MR->gitlabID, $MR->sourceProject)->name_with_namespace;?>: sourceBranch;?>
    @@ -45,15 +59,33 @@ mr->title;?> - title, "class='form-control'"); ?> + title, "class='form-control'");?> - mr->description; ?> - description, "rows='3' class='form-control'"); ?> + mr->description;?> + description, "rows='3' class='form-control'");?> + + + devops->repo;?> + repoID, "class='form-control chosen'");?> + + + mr->needCI;?> + +
    + needCI == '1' ? 'checked' : '' ?> + name="needCI" value="1" id="needCI"> + +
    + + + + job->common;?> + jobID, "class='form-control chosen'");?> mr->assignee;?> - + @@ -61,7 +93,7 @@ - + goback, '', 'class="btn btn-wide"');?> @@ -71,4 +103,4 @@
    - + diff --git a/module/mr/view/header.review.html.php b/module/mr/view/header.review.html.php new file mode 100644 index 0000000000..0bcae252cc --- /dev/null +++ b/module/mr/view/header.review.html.php @@ -0,0 +1,272 @@ +mr->getLastReviewInfo($file); +$repoModule = isset($lastReview) && isset($lastReview->module) ? $lastReview->module : ''; + +/* Get product pairs. */ +if(isset($repo->product) and $repo->product) +{ + $products = $this->dao->select('id,name')->from(TABLE_PRODUCT)->where('`id`')->in($repo->product)->fetchPairs(); +} +else +{ + $products = $this->loadModel('product')->getPairs(); +} + +/* get product by cookie or last review in this file. */ +$repoProduct = isset($_COOKIE['repoPairs'][$repoID]) ? $_COOKIE['repoPairs'][$repoID] : ''; +$repoProduct = isset($lastReview) && isset($lastReview->product) ? $lastReview->product : $repoProduct; +$repoProduct = isset($products[$repoProduct]) ? $repoProduct : key($products); +$executions = $this->mr->getExecutionPairs($repoProduct); +$modules = $this->loadModel('tree')->getOptionMenu($repoProduct, $viewType = 'bug', $startModuleID = 0); +$users = $this->loadModel('user')->getPairs('devfirst|nodeleted|noclosed'); +$products = array('' => '') + $products; +$executions = array('' => '') + $executions; + +$cwd = getcwd(); +$commiters = $this->user->getCommiters(); +$blamePairs = array(); +if($suffix and $suffix != 'binary' and strpos($this->config->repo->images, "|$suffix|") === false) +{ + $blames = $this->scm->blame($entry, $info->revision); + foreach($blames as $line => $blame) + { + if(!isset($blame['committer'])) + { + if(isset($blamePairs[$line - 1])) $blamePairs[$line] = $blamePairs[$line - 1]; + continue; + } + $blamePairs[$line] = zget($commiters, $blame['committer'], $blame['committer']); + } +} +chdir($cwd); + +//$reviews = $this->mr->getReview($repoID, $file, $info->revision); +$reviews = $this->mr->getReview($repoID, $file, ''); +$v1 = isset($oldRevision) ? $oldRevision : 0; +$this->loadModel('repo'); +// $bugUrl = $this->repo->createLink('addBug', "repoID=$repoID&file=$file&v1=$v1&v2={$info->revision}"); +$bugUrl = $this->createLink('mr', 'addBug', "repoID=$repoID&file=$file&v1=$v1&v2="); +$commentUrl = $this->createLink('mr', 'addComment'); +$productSelect = html::select('product', $products, $repoProduct, 'class="product form-control chosen" onchange="changeProduct(this)"'); +$branches = $this->loadModel('branch')->getPairs($repoProduct); +$moduleSelect = html::select('module', $modules, $repoModule, 'class="form-control chosen"'); +$executionSelect = html::select('execution', $executions, '', 'class="form-control chosen"'); +$typeSelect = html::select('repoType', $lang->repo->typeList, '', 'class="form-control chosen"'); +$userSelect = html::select('assignedTo', $users, '', 'class="form-control chosen assignedTo"'); +$bugs = array(); +foreach($reviews as $line => $lineReview) +{ + $lineBugs = array(); + foreach ($lineReview['bugs'] as $bugID => $bug) + { + $lineBug = array(); + $lineBug['id'] = $bugID; + $lineBug['line'] = $line; + $lineBug['title'] = $bug->title; + $lineBug['steps'] = $bug->steps; + $lineBug['realname'] = $bug->realname; + $lineBug['openedDate'] = substr($bug->openedDate, 5, 11); + $lineBug['lines'] = $bug->lines; + if($bug->edit) $lineBug['edit'] = true; + if($bug->delete) $lineBug['delete'] = true; + + if(isset($lineReview['comments'])) + { + if(isset($lineReview['comments'][$bugID])) + { + $comments = $lineReview['comments'][$bugID]; + $bugComments = array(); + foreach ($comments as $commentID => $comment) + { + $bugComment = array( + 'id' => $comment->id, + 'edit' => $comment->edit, + 'realname' => $comment->realname, + 'date' => substr($comment->date, 5, 11), + 'comment' => $comment->comment, + ); + $bugComments[] = $bugComment; + } + $lineBug['comments'] = $bugComments; + } + } + $lineBugs[] = $lineBug; + } + + $bugs[$line] = $lineBugs; +} + +js::set('bugs', $bugs); +js::set('productError', $lang->repo->error->product); +js::set('contentError', $lang->repo->error->commentText); +js::set('titleError', $lang->repo->error->title); +js::set('commentError', $lang->repo->error->comment); +js::set('submit', $lang->repo->submit); +js::set('cancel', $lang->repo->cancel); +js::set('confirmDelete', $lang->repo->notice->deleteBug); +js::set('confirmDeleteComment', $lang->repo->notice->deleteComment); +js::set('repoID', $repoID); +// js::set('revision', $info->revision); +js::set('revision', ''); +js::set('file', $file); +js::set('blamePairs', $blamePairs); +?> + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    repo->product?> +
    + + +
    +
    repo->module?>
    repo->execution?>repo->type?>
    repo->assign?>repo->lines?> +
    + + - + +
    +
    repo->title?> + +
    repo->detile?>
    + repo->submit, '', 'btn btn-wide btn-primary bugSubmit');?> + cancel, "onclick='hiddenForm()'", 'btn btn-wide');?> +
    +
    +
    +
    + +
    +user->errorDeny, $lang->repo->common, $lang->repo->addBug);?> +
    + + +
    +
    +
    + + Bug# + + Bug# + + + + + + + + +
    + +
    +
    +

    repo->lines?>    

    +
    + + + +
    +

    +
    + + +
    + + + + +
    +
    + +
    +
    +
    + :     +
    + + + +
    +
    + +
    repo->expand?>
    repo->collapse?>
    + diff --git a/module/mr/view/link.html.php b/module/mr/view/link.html.php new file mode 100644 index 0000000000..8545591db2 --- /dev/null +++ b/module/mr/view/link.html.php @@ -0,0 +1,270 @@ + + + + +productplan->confirmUnlinkStory)?> +productplan->confirmUnlinkBug)?> +mr->confirmUnlinkTask)?> +id);?> +id);?> +pageID);?> +recPerPage);?> +recTotal);?> + +
    +
    + +
    +
    '> + +
    + id, \"story\")", ' ' . $lang->productplan->linkStory, '', "class='btn btn-primary'");?> +
    + +
    id&orderBy=$orderBy");?>"> + + id}&type=story&orderBy=%s&link=$link¶m=$param"; + ?> + + + + + + + + + + + + + + + + + + + + + createLink('story', 'view', "storyID=$story->id"); + $totalEstimate += $story->estimate; + ?> + + + + + + + + + + + + + + + +
    + idAB);?> + productplan->updateOrder);?> priAB);?>story->module);?>story->title);?> openedByAB);?> assignedToAB);?> story->estimateAB);?> statusAB);?> story->stageAB);?> actions?>
    + id);?> + pri;?>' title='story->priList, $story->pri, $story->pri);?>'>story->priList, $story->pri, $story->pri);?>module, '');?> + parent > 0) echo "story->children}>{$lang->story->childrenAB}"; + echo html::a($viewLink , $story->title); + ?> + openedBy);?>assignedTo);?>estimate . $config->hourUnit;?> + + processStatus('story', $story);?> + + story->stageList[$story->stage];?> + createLink('mr', 'unlink', "MRID=$MR->id&productID=$product->id&type=story&linkID=$story->id&confirm=yes"); + echo html::a("javascript:ajaxDelete(\"$unlinkURL\", \"storyList\", confirmUnlinkStory)", '', '', "class='btn' title='{$lang->productplan->unlinkStory}'"); + } + ?> +
    + + + +
    +
    +
    '> +
    + id, \"bug\")", ' ' . $lang->productplan->linkBug, '', "class='btn btn-primary'");?> +
    + +
    id&orderBy=$orderBy");?>"> + + + id}&type=bug&orderBy=%s&link=$link¶m=$param"; ?> + + + + + + + + + + + + + + + + + + + + + + + + +
    + idAB);?> + priAB);?>bug->title);?> openedByAB);?> bug->assignedToAB);?>bug->status);?> actions?>
    + id);?> + bug->priList, $bug->pri, $bug->pri);?>createLink('bug', 'view', "bugID=$bug->id"), $bug->title, '', 'data-app="product"');?>openedBy);?>assignedTo);?> + + processStatus('bug', $bug);?> + + + createLink('mr', 'unlink', "MRID=$MR->id&productID=$product->id&type=bug&linkID=$bug->id&confirm=yes"); + echo html::a("javascript:ajaxDelete(\"$unlinkURL\", \"bugList\", confirmUnlinkBug)", '', '', "class='btn' title='{$lang->productplan->unlinkBug}'"); + } + ?> +
    + + + +
    +
    +
    '> +
    + id, \"task\")", ' ' . $lang->mr->linkTask, '', "class='btn btn-primary'");?> +
    + +
    id&orderBy=$orderBy");?>"> + + + id}&type=task&orderBy=%s&link=$link¶m=$param"; ?> + + + + + + + + + + + + + + + + + + + + + + + + +
    + idAB);?> + priAB);?>task->name);?> task->finishedByAB);?> task->assignedToAB);?>task->status);?> actions?>
    + id);?> + task->priList, $task->pri, $task->pri);?>createLink('task', 'view', "taskID=$task->id"), $task->name, '', 'data-app="product"');?>finishedBy);?>assignedTo);?> + + processStatus('task', $task);?> + + + createLink('mr', 'unlink', "MRID=$MR->id&productID=$product->id&type=task&linkID=$task->id&confirm=yes"); + echo html::a("javascript:ajaxDelete(\"$unlinkURL\", \"taskList\", confirmUnlinkTask)", '', '', "class='btn' title='{$lang->mr->unlinkTask}'"); + ?> +
    + + + +
    +
    +
    +
    +
    + + + + + diff --git a/module/mr/view/linkbug.html.php b/module/mr/view/linkbug.html.php new file mode 100644 index 0000000000..49daaa7865 --- /dev/null +++ b/module/mr/view/linkbug.html.php @@ -0,0 +1,79 @@ + + * @package mr + * @version $Id: linkbug.html.php$ + * @link http://www.zentao.net + */ +?> +
    +
    +
    id&browseType=$browseType¶m=$param&orderBy=$orderBy")?>'> +
    + productplan->unlinkedBugs;?> +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    + + idAB;?> +
    priAB;?>bug->title;?>openedByAB;?>bug->assignedToAB;?>bug->status;?>
    + id => sprintf('%03d', $bug->id)), $relatedBugs);?> + bug->priList, $bug->pri, $bug->pri)?>createLink('bug', 'view', "bugID=$bug->id", '', true), $bug->title, '', "data-toggle='modal' data-type='iframe' data-width='90%'");?>openedBy);?>assignedTo);?> + + processStatus('bug', $bug);?> + +
    + +
    +
    + diff --git a/module/mr/view/linkstory.html.php b/module/mr/view/linkstory.html.php new file mode 100644 index 0000000000..9d334b9bdc --- /dev/null +++ b/module/mr/view/linkstory.html.php @@ -0,0 +1,92 @@ + +
    +
    +
    id&browseType=$browseType¶m=$param&orderBy=$orderBy")?>"> +
    + productplan->unlinkedStories;?> +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    + + idAB;?> +
    priAB;?>story->plan;?>story->module;?>story->title;?>openedByAB;?>assignedToAB;?>story->estimateAB;?>statusAB;?>story->stageAB;?>
    + id => sprintf('%03d', $story->id)), $relatedStories);?> + pri;?>' title='story->priList, $story->pri, $story->pri)?>'>story->priList, $story->pri, $story->pri)?>planTitle;?>module];?> + parent > 0) echo "story->children}>{$lang->story->childrenAB}"; + echo html::a($this->createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', "data-toggle='modal' data-type='iframe' data-width='90%'"); + ?> + openedBy);?>assignedTo);?>estimate . $config->hourUnit;?> + + processStatus('story', $story);?> + + story->stageList[$story->stage];?>
    + +
    +
    + diff --git a/module/mr/view/linktask.html.php b/module/mr/view/linktask.html.php new file mode 100644 index 0000000000..e231dee43e --- /dev/null +++ b/module/mr/view/linktask.html.php @@ -0,0 +1,79 @@ + + * @package mr + * @version $Id: linktask.html.php$ + * @link http://www.zentao.net + */ +?> +
    +
    +
    id&browseType=$browseType¶m=$param&orderBy=$orderBy")?>'> +
    + mr->unlinkedTasks;?> +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    + + idAB;?> +
    priAB;?>task->name;?>task->finishedByAB;?>task->assignedToAB;?>task->status;?>
    + id => sprintf('%03d', $task->id)), $relatedTasks);?> + task->priList, $task->pri, $task->pri)?>createLink('task', 'view', "taskID=$task->id", '', true), $task->name, '', "data-toggle='modal' data-type='iframe' data-width='90%'");?>finishedBy);?>assignedTo);?> + + processStatus('task', $task);?> + +
    + +
    +
    + diff --git a/module/mr/view/view.html.php b/module/mr/view/view.html.php index cbc02d1221..fa40b82729 100644 --- a/module/mr/view/view.html.php +++ b/module/mr/view/view.html.php @@ -1,67 +1,171 @@ - -id)):?> -
    -
    -

    - mr->notFound;?> - - createLink('mr', 'create'), " " . $lang->mr->create, '', "class='btn btn-info'");?> - -

    -
    + +id)): ?> +
    +
    +

    + mr->notFound;?> + + createLink('mr', 'create'), " " . $lang->mr->create, '', "class='btn btn-info'"); ?> + +

    - +
    + +id)): ?> +
    +
    +

    + mr->notFound; ?> + createLink('mr', 'browse'), " " . $lang->mr->browse, '', "class='btn btn-info'"); ?> +

    +
    +
    + + -
    -
    -
    -
    -
    -
    mr->from . html::a($sourceProjectURL, $sourceProjectName . ":" . $MR->sourceBranch, "_blank", "class='btn btn-link btn-active-text' style='color: blue'") . $lang->mr->to . html::a($targetProjectURL, $targetProjectName . ":" . $MR->targetBranch, "_blank", "class='btn btn-link btn-active-text' style='color: blue'");?>
    +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + + + + head_pipeline->status)): ?> + + + + + + + + changes_count)):?> + + + + + + + + + + + + + + +
    + mr->from . html::a($sourceProjectURL, $sourceProjectName . ":" . $MR->sourceBranch, "_blank", "class='btn btn-link btn-active-text' style='color: blue'") . $lang->mr->to . html::a($targetProjectURL, $targetProjectName . ":" . $MR->targetBranch, "_blank", "class='btn btn-link btn-active-text' style='color: blue'"); ?> +
    mr->status;?>mr->statusList, $MR->status);?>
    mr->pipeline}{$lang->mr->status}";?>mr->pipelineStatus, $rawMR->head_pipeline->status, $lang->mr->pipelineUnknown); ?>
    mr->mergeStatus; ?> + mr->cantMerge; ?> + mr->noChanges;?> + mr->mergeStatusList, $rawMR->merge_status);?>
    mr->MRHasConflicts; ?>has_conflicts ? $lang->mr->hasConflicts : $lang->mr->hasNoConflict);?>
    mr->description;?> + description) ? $MR->description : $lang->noData; ?> +
    + +
    -
    - mr->status;?> - mr->statusList, $MR->status);?> -
    - head_pipeline->status)):?> -
    - mr->pipeline}{$lang->mr->status}";?> - mr->pipelineStatus, $rawMR->head_pipeline->status, $lang->mr->pipelineUnknown);?> -
    +
    + + state == 'opened'): ?> +
    mr->commandDocument, $httpRepoURL, $MR->sourceBranch, $branchPath, $MR->targetBranch, $branchPath, $MR->targetBranch); ?>
    + + +
    +
    + + approvalStatus != 'approved' or ($MR->compileID !=0 and $MR->compileStatus != 'success')) ? ' disabled' : ''; ?> + state == 'opened' and !$rawMR->has_conflicts) echo html::a(inlink('accept', "mr=$MR->id", '', true), ' ' . $lang->mr->acceptMR, '', "id='mergeButton' class='btn' $acceptDisabled"); ?> + state == 'opened'): ?> + has_conflicts or ($MR->compileID !=0 and $MR->compileStatus != 'success') or $MR->approvalStatus == 'approved'):?> + id&action=approve", '', true), ' ' . $lang->mr->approve, '', "id='mergeButton' class='btn iframe showinonlybody' disabled"); ?> + + id&action=approve", '', true), ' ' . $lang->mr->approve, '', "id='mergeButton' class='btn iframe showinonlybody'"); ?> + + id&action=reject", '', true), ' ' . $lang->mr->reject, '', "id='mergeButton' class='btn iframe showinonlybody'" . ($MR->approvalStatus == 'rejected' ? 'disabled' : '')); ?> + id"), ' ' . $lang->mr->close, '', "id='mergeButton' class='btn'"); ?> + id"), ' ' . str_replace($lang->mr->common, '', $lang->mr->edit), '', "id='mergeButton' class='btn'"); ?> - mr->mergeStatus;?> - mr->mergeStatusList, $rawMR->merge_status);?> -
    - mr->MRHasConflicts;?> - has_conflicts ? $lang->mr->hasConflicts : $lang->mr->hasNoConflict);?> + state == 'closed') echo html::a(inlink('reopen', "mr=$MR->id"), ' ' . $lang->mr->reopen, '', "id='mergeButton' class='btn'"); ?> + id", $MR, 'button', 'trash', 'hiddenwin');?>
    -
    -
    -
    mr->description;?>
    -
    - description) ? $MR->description : "
    " . $lang->noData . '
    ';?> -
    + +
    +
    +
    + compile->job;?> +
    + compileID):?> + + + + + + + + + + + + + + + + + +
    job->common;?>name;?>
    compile->atTime;?>lastExec : $lang->mr->compileUnexecuted;?>
    compile->result;?> + compile->statusList, $compile->status);?>    + createLink('job', 'view', "jobID=$compileJob->id&compileID=$compile->id", '', true), "{$lang->compile->logs}", "", "class='iframe'");?> +
    + needCI):?> + createLink('job', 'view', "jobID={$MR->jobID}");?> +
    compile->statusList[$MR->compileStatus], '_blank');?>
    + +
    mr->noCompileJob;?>
    + +
    +
    - state == 'opened'):?> -
    mr->commandDocument, $httpRepoURL, $MR->sourceBranch, $branchPath, $MR->targetBranch, $branchPath, $MR->targetBranch);?>
    -
    -
    - state == 'opened' and !$rawMR->has_conflicts):?> - id"), ' ' . $lang->mr->acceptMR, '', "id='mergeButton' class='btn btn-wide btn-primary'");?> - - - +
    + + + + diff --git a/module/my/control.php b/module/my/control.php index 949d479cca..9f092633fa 100644 --- a/module/my/control.php +++ b/module/my/control.php @@ -190,7 +190,7 @@ class my extends control public function story($type = 'assignedTo', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1) { /* Save session. */ - if($this->app->viewType != 'json') $this->session->set('storyList', $this->app->getURI(true), 'product'); + if($this->app->viewType != 'json') $this->session->set('storyList', $this->app->getURI(true), 'my'); /* Load pager. */ $this->app->loadClass('pager', $static = true); @@ -234,7 +234,7 @@ class my extends control public function requirement($type = 'assignedTo', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1) { /* Save session. */ - if($this->app->viewType != 'json') $this->session->set('storyList', $this->app->getURI(true), 'product'); + if($this->app->viewType != 'json') $this->session->set('storyList', $this->app->getURI(true), 'my'); /* Load pager. */ $this->app->loadClass('pager', $static = true); diff --git a/module/my/view/testcase.html.php b/module/my/view/testcase.html.php index ca9fd9f131..b2d275fa91 100644 --- a/module/my/view/testcase.html.php +++ b/module/my/view/testcase.html.php @@ -116,7 +116,7 @@ createLink('testcase', 'batchEdit', "productID=0&branch=all"); + $actionLink = $this->createLink('testcase', 'batchEdit', "productID=0&branch=all&type=case&tab=my"); $misc = "data-form-action='$actionLink'"; echo html::commonButton($lang->edit, $misc); } diff --git a/module/product/control.php b/module/product/control.php index c1d38e293b..8dbc91141b 100644 --- a/module/product/control.php +++ b/module/product/control.php @@ -211,16 +211,16 @@ class product extends control $this->app->loadClass('pager', $static = true); $pager = new pager($recTotal, $recPerPage, $pageID); + /* Display of branch label. */ + $showBranch = $this->loadModel('branch')->showBranch($productID); + $product = $this->product->getById($productID); - if($product and $product->type != 'normal') - { - $this->app->loadLang('datatable'); - $this->lang->datatable->showBranch = sprintf($this->lang->datatable->showBranch, $this->lang->product->branchName[$product->type]); - } /* Get stories and branches. */ if($this->app->rawModule == 'projectstory') { + $showBranch = $this->loadModel('branch')->showBranch($productID, 0, $projectID); + $branches = array(); if(!empty($product)) { @@ -279,7 +279,7 @@ class product extends control $actionURL = $this->createLink($rawModule, $rawMethod, $params . "productID=$productID&branch=$branch&browseType=bySearch&queryID=myQueryID&storyType=$storyType"); $this->config->product->search['onMenuBar'] = 'yes'; - $this->product->buildSearchForm($productID, $this->products, $queryID, $actionURL); + $this->product->buildSearchForm($productID, $this->products, $queryID, $actionURL, $branch); $showModule = !empty($this->config->datatable->productBrowse->showModule) ? $this->config->datatable->productBrowse->showModule : ''; @@ -309,6 +309,7 @@ class product extends control $this->view->branch = $branch; $this->view->branchID = $branchID; $this->view->branches = $branches; + $this->view->showBranch = $showBranch; $this->view->storyStages = $this->product->batchGetStoryStage($stories); $this->view->setModule = true; $this->view->storyTasks = $storyTasks; diff --git a/module/product/js/kanban.js b/module/product/js/kanban.js index 60c0eff6cf..30d2879cc2 100644 --- a/module/product/js/kanban.js +++ b/module/product/js/kanban.js @@ -10,12 +10,28 @@ function processKanbanData(key, programsData) /* Generate columns */ var columns = []; + var hasDoingProject = false; $.each(kanbanColumns, function(_, column) { + var colType = column.type; + if(colType === 'doingProject') + { + hasDoingProject = true; + columns.push( + { + kanban: kanbanId, + id: kanbanId + '-doing', + type: 'doing', + asParent: true, + name: doingText, + count: '' + }); + } columns.push($.extend({}, column, { - kanban: kanbanId, - id: kanbanId + '-' + column.type, + kanban: kanbanId, + id: kanbanId + '-' + colType, + parentType: (hasDoingProject && (colType === 'doingProject' || colType === 'doingExecution')) ? 'doing' : false, })); }); @@ -45,7 +61,7 @@ function processKanbanData(key, programsData) } /* doing projects */ - if(kanbanColumns.doingProject) + if(hasDoingProject) { items.doingProject = []; var productProjects = projectProduct[productID]; @@ -69,6 +85,7 @@ function processKanbanData(key, programsData) /* doing execution */ items.doingExecution = []; var productExecutions = classicExecution[productID]; + console.log('productExecutions', productID, productExecutions); if(productExecutions) { $.each(productExecutions, function(_, execution) @@ -106,6 +123,12 @@ function processKanbanData(key, programsData) return {id: kanbanId, columns: columns, lanes: lanes}; } +/** Calculate column height */ +function calcColHeight(col, lane, colCards, colHeight) +{ + if (col.type !== 'doingProject') return colHeight; + return colCards.length * 62; +} $(function() { @@ -115,6 +138,6 @@ $(function() var $kanban = $('#kanban-' + key); if(!$kanban.length) return; var data = processKanbanData(key, programsData); - $kanban.kanban({data: data, noLaneName: isClassicMode}); + $kanban.kanban({data: data, noLaneName: isClassicMode, calcColHeight: calcColHeight}); }); }); diff --git a/module/product/model.php b/module/product/model.php index dad456e69e..0f3bef5504 100644 --- a/module/product/model.php +++ b/module/product/model.php @@ -155,13 +155,12 @@ class productModel extends model if(empty($product)) $productID = key($products); $this->session->set('product', (int)$productID, $this->app->tab); if($productID && strpos(",{$this->app->user->view->products},", ",{$productID},") === false) $this->accessDenied(); - - setcookie('preProductID', $productID, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, true); } + setcookie('preProductID', (int)$productID, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, true); + if($this->cookie->preProductID != $this->session->product) { - $this->cookie->set('preBranch', 0); setcookie('preBranch', 0, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, true); } return $this->session->product; @@ -701,6 +700,7 @@ class productModel extends model $oldProducts = $this->getByIdList($this->post->productIDList); $nameList = array(); + $extendFields = $this->getFlowExtendFields(); foreach($data->productIDList as $productID) { $productName = $data->names[$productID]; @@ -717,6 +717,16 @@ class productModel extends model $products[$productID]->status = $data->statuses[$productID]; $products[$productID]->desc = strip_tags($this->post->descs[$productID], $this->config->allowedTags); $products[$productID]->acl = $data->acls[$productID]; + + foreach($extendFields as $extendField) + { + $products[$productID]->{$extendField->field} = $this->post->{$extendField->field}[$productID]; + if(is_array($products[$productID]->{$extendField->field})) $products[$productID]->{$extendField->field} = join(',', $products[$productID]->{$extendField->field}); + + $products[$productID]->{$extendField->field} = htmlSpecialString($products[$productID]->{$extendField->field}); + $message = $this->checkFlowRule($extendField, $products[$productID]->{$extendField->field}); + if($message) die(js::alert($message)); + } } if(dao::isError()) die(js::error(dao::getError())); @@ -886,26 +896,63 @@ class productModel extends model * @param array $products * @param int $queryID * @param int $actionURL + * @param int $branch * @access public * @return void */ - public function buildSearchForm($productID, $products, $queryID, $actionURL) + public function buildSearchForm($productID, $products, $queryID, $actionURL, $branch = 0) { + $productIdList = ($this->app->tab == 'project' and empty($productID)) ? array_keys($products) : $productID; + $branchParam = ($this->app->tab == 'project' and empty($productID)) ? '' : $branch; + $this->config->product->search['actionURL'] = $actionURL; $this->config->product->search['queryID'] = $queryID; - $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairs(($this->app->tab == 'project' and empty($productID)) ? array_keys($products) : $productID); + $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairs($productIdList, $branchParam); $product = ($this->app->tab == 'project' and empty($productID)) ? $products : array($productID => $products[$productID]); $this->config->product->search['params']['product']['values'] = $product + array('all' => $this->lang->product->allProduct); - /* Get module of all products.*/ - $module = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'story', $startModuleID = 0); - if(!$productID) + /* Get modules. */ + $this->loadModel('tree'); + if($this->app->tab == 'project') { - $module = array(); - foreach($products as $id => $product) $module += $this->loadModel('tree')->getOptionMenu($id, $viewType = 'story', $startModuleID = 0); + if($productID) + { + $modules = array(); + $branchList = $this->loadModel('branch')->getPairs($productID, '', $this->session->project); + $branchModuleList = $this->tree->getOptionMenu($productID, 'story', 0, array_keys($branchList)); + foreach($branchModuleList as $branchID => $branchModules) $modules += $branchModules; + } + else + { + $moduleList = array(); + $modules = array('' => '/'); + $branchGroup = $this->loadModel('execution')->getBranchByProduct(array_keys($products), $this->session->project); + foreach($products as $productID => $productName) + { + if(isset($branchGroup[$productID])) + { + $branchModuleList = $this->tree->getOptionMenu($productID, 'story', 0, array_keys($branchGroup[$productID])); + foreach($branchModuleList as $branchID => $branchModules) $moduleList += $branchModules; + } + else + { + $moduleList = $this->tree->getOptionMenu($productID, 'story', 0, $branch); + } + + foreach($moduleList as $moduleID => $moduleName) + { + if(empty($moduleID)) continue; + $modules[$moduleID] = $productName . $moduleName; + } + } + } } - $this->config->product->search['params']['module']['values'] = $module; + else + { + $modules = $this->tree->getOptionMenu($productID, 'story', 0, $branch); + } + $this->config->product->search['params']['module']['values'] = $modules; $productInfo = $this->getById($productID); if(!$productID or $productInfo->type == 'normal' or $this->app->tab == 'assetlib') @@ -1936,7 +1983,9 @@ class productModel extends model } elseif(($module == 'testcase' and $method == 'groupCase') or ($module == 'story' and $method == 'zeroCase') and $this->app->tab == 'project') { - $link = helper::createLink($module, $method, "productID=%s" . ($branch ? "&branch=%s" : '')) . "#app=project"; + parse_str($extra, $output); + $projectID = isset($output['projectID']) ? $output['projectID'] : 0; + $link = helper::createLink($module, $method, "productID=%s&branch=" . ($branch ? "%s" : '') . "&groupBy=&projectID=$projectID") . "#app=project"; } else { diff --git a/module/product/view/ajaxgetdropmenu.html.php b/module/product/view/ajaxgetdropmenu.html.php index fd55464514..e5e4614385 100644 --- a/module/product/view/ajaxgetdropmenu.html.php +++ b/module/product/view/ajaxgetdropmenu.html.php @@ -78,10 +78,11 @@ foreach($products as $programID => $programProducts) $selected = $product->id == $productID ? 'selected' : ''; $productName = $product->line ? zget($lines, $product->line, '') . ' / ' . $product->name : $product->name; $linkHtml = $this->product->setParamsForLink($module, $link, $projectID, $product->id); + $locateTab = ($module == 'testtask' and $method == 'browseUnits' and $app->tab == 'project') ? '' : "data-app='$app->tab'"; if($product->status == 'normal' and $product->PO == $this->app->user->account) { - $myProductsHtml .= '
  • ' . html::a($linkHtml, $productName, '', "class='$selected productName' title='{$productName}' data-key='" . zget($productsPinYin, $product->name, '') . "' data-app='$app->tab'") . '
  • '; + $myProductsHtml .= '
  • ' . html::a($linkHtml, $productName, '', "class='$selected productName' title='{$productName}' data-key='" . zget($productsPinYin, $product->name, '') . "' " . $locateTab) . '
  • '; if($selected == 'selected') $tabActive = 'myProduct'; diff --git a/module/product/view/all.html.php b/module/product/view/all.html.php index bbecc67ff4..7143aabb8b 100644 --- a/module/product/view/all.html.php +++ b/module/product/view/all.html.php @@ -56,6 +56,10 @@ bug->common;?> product->plan;?> product->release;?> + product->getFlowExtendFields(); + foreach($extendFields as $extendField) echo "{$extendField->name}"; + ?> actions;?> @@ -106,6 +110,7 @@ % + ";?> @@ -150,6 +155,7 @@ % + ";?> @@ -208,6 +214,7 @@ unResolved + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unResolved + $product->fixedBugs), 3) * 100;?>% plans;?> releases;?> + " . $this->loadModel('flow')->getFieldValue($extendField, $product) . "";?> id", $product, 'list', 'edit');?> diff --git a/module/product/view/batchedit.html.php b/module/product/view/batchedit.html.php index f1bf9d4071..5ff2efc7e4 100755 --- a/module/product/view/batchedit.html.php +++ b/module/product/view/batchedit.html.php @@ -64,6 +64,10 @@ '>product->status;?> '>product->desc;?> '>product->acl;?> + product->getFlowExtendFields(); + foreach($extendFields as $extendField) echo "{$extendField->name}"; + ?> @@ -92,6 +96,7 @@ '>product->statusList, $products[$productID]->status, "class='form-control'");?> '>desc), "rows='1' class='form-control autosize'");?> '> product->acls, $products[$productID]->acl));?> + control == 'select' or $extendField->control == 'multi-select') ? " style='overflow:visible'" : '') . ">" . $this->loadModel('flow')->getFieldControl($extendField, $products[$productID], $extendField->field . "[{$productID}]") . "";?> config->moreLinks["POs[$productID]"])) unset($this->config->moreLinks["POs[$productID]"]); diff --git a/module/product/view/kanban.html.php b/module/product/view/kanban.html.php index 7fc219f1b2..6e9228e03e 100644 --- a/module/product/view/kanban.html.php +++ b/module/product/view/kanban.html.php @@ -50,7 +50,7 @@ $userPrivs['project'] = common::hasPriv('project', 'index'); $userPrivs['execution'] = common::hasPriv('execution', 'task'); $userPrivs['release'] = common::hasPriv('release', 'view'); js::set('isClassicMode', $config->systemMode != 'new'); -js::set('kanbanColumns', $kanbanColumns); +js::set('kanbanColumns', array_values($kanbanColumns)); js::set('userPrivs', $userPrivs); js::set('kanbanList', $kanbanList); js::set('programList', $programList); diff --git a/module/productplan/control.php b/module/productplan/control.php index bc11cfbed9..dd6e49e9a9 100644 --- a/module/productplan/control.php +++ b/module/productplan/control.php @@ -396,6 +396,7 @@ class productplan extends control if(!empty($_POST['stories'])) { $this->productplan->linkStory($planID); + if($this->viewType == 'json') return $this->send(array('result' => 'success')); die(js::locate(inlink('view', "planID=$planID&type=story&orderBy=$orderBy"), 'parent')); } @@ -418,7 +419,7 @@ class productplan extends control $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; $this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject); - $this->config->product->search['params']['plan']['values'] = $this->productplan->getPairsForStory($plan->product, $plan->branch, 'skipParent'); + $this->config->product->search['params']['plan']['values'] = $this->productplan->getPairsForStory($plan->product, $plan->branch, 'skipParent|withMainPlan'); $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, $plan->branch); $storyStatusList = $this->lang->story->statusList; unset($storyStatusList['closed']); @@ -533,6 +534,7 @@ class productplan extends control if(!empty($_POST['bugs'])) { $this->productplan->linkBug($planID); + if($this->viewType == 'json') return $this->send(array('result' => 'success')); die(js::locate(inlink('view', "planID=$planID&type=bug&orderBy=$orderBy"), 'parent')); } @@ -558,7 +560,7 @@ class productplan extends control $this->config->bug->search['actionURL'] = $this->createLink('productplan', 'view', "planID=$planID&type=bug&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; - $this->config->bug->search['params']['plan']['values'] = $this->productplan->getPairsForStory($productID, $plan->branch, 'skipParent'); + $this->config->bug->search['params']['plan']['values'] = $this->productplan->getPairsForStory($productID, $plan->branch, 'skipParent|withMainPlan'); $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($plan->product, $plan->branch); $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($productID, $branch = 0, $params = ''); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->build->getProductBuildPairs($productID, $branch = 0, $params = ''); diff --git a/module/productplan/model.php b/module/productplan/model.php index d9ddf69197..dcbc5ebd3c 100644 --- a/module/productplan/model.php +++ b/module/productplan/model.php @@ -94,7 +94,13 @@ class productplanModel extends model $plans = $this->reorder4Children($plans); $planIdList = array_keys($plans); - $planProjects = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('product')->eq($product)->andWhere('plan')->in(array_keys($plans))->fetchPairs('plan', 'project'); + $planProjects = $this->dao->select('t1.*,t2.type')->from(TABLE_PROJECTPRODUCT)->alias('t1') + ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') + ->where('t1.product')->eq($product) + ->andWhere('t1.plan')->in(array_keys($plans)) + ->andWhere('t2.type')->in('sprint,stage') + ->fetchPairs('plan', 'project'); + $storyCountInTable = $this->dao->select('plan,count(story) as count')->from(TABLE_PLANSTORY)->where('plan')->in($planIdList)->groupBy('plan')->fetchPairs('plan', 'count'); $product = $this->loadModel('product')->getById($product); if($product->type == 'normal') @@ -127,6 +133,7 @@ class productplanModel extends model $plan->hour = array_sum($storyPairs); $plan->project = zget($planProjects, $plan->id, ''); $plan->projectID = $plan->project; + $plan->expired = $plan->end < $date ? true : false; /* Sync linked stories. */ if(!isset($storyCountInTable[$plan->id]) or $storyCountInTable[$plan->id] != $plan->stories) @@ -478,6 +485,7 @@ class productplanModel extends model $purifier = new HTMLPurifier($config); $plans = array(); + $extendFields = $this->getFlowExtendFields(); foreach($data->id as $planID) { $plan = new stdclass(); @@ -491,6 +499,16 @@ class productplanModel extends model if(empty($plan->end)) die(js::alert(sprintf($this->lang->productplan->errorNoEnd, $planID))); if($plan->begin > $plan->end) die(js::alert(sprintf($this->lang->productplan->beginGeEnd, $planID))); + foreach($extendFields as $extendField) + { + $plan->{$extendField->field} = $this->post->{$extendField->field}[$planID]; + if(is_array($plan->{$extendField->field})) $plan->{$extendField->field} = join(',', $plan->{$extendField->field}); + + $plan->{$extendField->field} = htmlSpecialString($plan->{$extendField->field}); + $message = $this->checkFlowRule($extendField, $plan->{$extendField->field}); + if($message) die(js::alert($message)); + } + $plans[$planID] = $plan; } diff --git a/module/productplan/view/batchedit.html.php b/module/productplan/view/batchedit.html.php index f4338569d8..0a44dd63c1 100644 --- a/module/productplan/view/batchedit.html.php +++ b/module/productplan/view/batchedit.html.php @@ -26,6 +26,10 @@ productplan->begin?> productplan->end?> productplan->future?> + productplan->getFlowExtendFields(); + foreach($extendFields as $extendField) echo "{$extendField->name}"; + ?> @@ -40,6 +44,7 @@ id]", $plan->begin, "class='form-control form-date $hiddenInput'");echo html::input("begin$plan->id", '', "class='form-control $showInput' disabled='disabled'");?> id]", $plan->end, "class='form-control form-date $hiddenInput'");echo html::input("end$plan->id", '', "class='form-control $showInput' disabled='disabled'");?>
    onclick="changeDate(id;?>);"/>
    + control == 'select' or $extendField->control == 'multi-select') ? " style='overflow:visible'" : '') . ">" . $this->loadModel('flow')->getFieldControl($extendField, $plan, $extendField->field . "[{$plan->id}]") . "";?> diff --git a/module/productplan/view/browse.html.php b/module/productplan/view/browse.html.php index 7ed055988c..e082210d53 100644 --- a/module/productplan/view/browse.html.php +++ b/module/productplan/view/browse.html.php @@ -128,16 +128,17 @@ " . $this->loadModel('flow')->getFieldValue($extendField, $plan) . "";?> expired ? "disabled='disabled'" : ''; if(common::hasPriv('execution', 'create', $plan) and $plan->parent >= 0) { $executionLink = $config->systemMode == 'new' ? '#projects' : $this->createLink('execution', 'create', "projectID=0&executionID=0©ExecutionID=0&plan=$plan->id&confirm=no&productID=$productID"); if($config->systemMode == 'new') { - echo html::a($executionLink, '', '', "data-toggle='modal' data-id='$plan->id' onclick='getPlanID(this, $plan->branch)' class='btn' title='{$lang->productplan->createExecution}'"); + echo html::a($executionLink, '', '', "data-toggle='modal' data-id='$plan->id' onclick='getPlanID(this, $plan->branch)' class='btn' title='{$lang->productplan->createExecution}' $attr"); } else { - echo html::a($executionLink, '', '', "class='btn' title='{$lang->productplan->createExecution}'"); + echo html::a($executionLink, '', '', "class='btn' title='{$lang->productplan->createExecution}' $attr"); } } if(common::hasPriv('productplan', 'linkStory', $plan) and $plan->parent >= 0) echo html::a(inlink('view', "planID=$plan->id&type=story&orderBy=id_desc&link=true"), '', '', "class='btn' title='{$lang->productplan->linkStory}'"); diff --git a/module/productplan/view/linkstory.html.php b/module/productplan/view/linkstory.html.php index 76abdaf6e8..c026b12eef 100644 --- a/module/productplan/view/linkstory.html.php +++ b/module/productplan/view/linkstory.html.php @@ -47,7 +47,7 @@ pri;?>' title='story->priList, $story->pri, $story->pri)?>'>story->priList, $story->pri, $story->pri)?> planTitle;?> - module];?> + module];?> parent > 0) echo "story->children}>{$lang->story->childrenAB}"; diff --git a/module/program/js/kanban.js b/module/program/js/kanban.js index c1574a2fb1..5824925373 100644 --- a/module/program/js/kanban.js +++ b/module/program/js/kanban.js @@ -12,10 +12,25 @@ function processKanbanData(key, programsData) var columns = []; $.each(kanbanColumns, function(_, column) { + var colType = column.type; + if(colType === 'doingProject') + { + columns.push( + { + kanban: kanbanId, + id: kanbanId + '-doing', + type: 'doing', + asParent: true, + name: doingText, + count: '' + }); + } + columns.push($.extend({}, column, { - kanban: kanbanId, - id: kanbanId + '-' + column.type, + kanban: kanbanId, + id: kanbanId + '-' + column.type, + parentType: (colType === 'doingProject' || colType === 'doingExecution') ? 'doing' : false, })); }); @@ -107,6 +122,6 @@ $(function() { var $kanban = $('#kanban-' + key); if(!$kanban.length) return; - $kanban.kanban({data: processKanbanData(key, programsData)}); + $kanban.kanban({data: processKanbanData(key, programsData), virtualize: true}); }); }); diff --git a/module/program/view/browsebylist.html.php b/module/program/view/browsebylist.html.php index f061c8dd67..51d684638e 100644 --- a/module/program/view/browsebylist.html.php +++ b/module/program/view/browsebylist.html.php @@ -14,6 +14,10 @@ project->begin);?> project->end);?> project->progress;?> + program->getFlowExtendFields(); + foreach($extendFields as $extendField) echo "{$extendField->name}"; + ?> actions;?> @@ -79,6 +83,7 @@
    + " . $this->loadModel('flow')->getFieldValue($extendField, $program) . "";?> type == 'program'):?> status == 'wait' || $program->status == 'suspended') common::printIcon('program', 'start', "programID=$program->id", $program, 'list', 'play', '', 'iframe', true, '', $this->lang->program->start);?> diff --git a/module/program/view/kanban.html.php b/module/program/view/kanban.html.php index d055074eab..2ade64655c 100644 --- a/module/program/view/kanban.html.php +++ b/module/program/view/kanban.html.php @@ -43,7 +43,7 @@ $userPrivs['productplan'] = common::hasPriv('productplan', 'view'); $userPrivs['project'] = common::hasPriv('project', 'index'); $userPrivs['execution'] = common::hasPriv('execution', 'task'); $userPrivs['release'] = common::hasPriv('release', 'view'); -js::set('kanbanColumns', $kanbanColumns); +js::set('kanbanColumns', array_values($kanbanColumns)); js::set('userPrivs', $userPrivs); js::set('kanbanGroup', $kanbanGroup); js::set('doingText', $lang->program->statusList['doing']); diff --git a/module/project/config.php b/module/project/config.php index 0559cdf897..25a8fcf18b 100644 --- a/module/project/config.php +++ b/module/project/config.php @@ -18,6 +18,12 @@ $config->project->edit = new stdclass(); $config->project->create->requiredFields = 'name,code,begin,end'; $config->project->edit->requiredFields = 'name,code,begin,end'; +$config->project->start = new stdclass(); +$config->project->start->requiredFields = 'realBegan'; + +$config->project->close = new stdclass(); +$config->project->close->requiredFields = 'realEnd'; + $config->project->sortFields = new stdclass(); $config->project->sortFields->id = 'id'; $config->project->sortFields->begin = 'begin'; @@ -49,7 +55,7 @@ $config->project->datatable->fieldList['code']['width'] = '100'; $config->project->datatable->fieldList['code']['minWidth'] = '180'; $config->project->datatable->fieldList['code']['required'] = 'no'; $config->project->datatable->fieldList['code']['sort'] = 'no'; -$config->project->datatable->fieldList['code']['pri'] = '1'; +$config->project->datatable->fieldList['code']['pri'] = '1'; $config->project->datatable->fieldList['PM']['title'] = 'PM'; $config->project->datatable->fieldList['PM']['fixed'] = 'no'; @@ -119,6 +125,7 @@ $config->project->datatable->fieldList['actions']['width'] = '180'; $config->project->datatable->fieldList['actions']['required'] = 'yes'; $config->project->datatable->fieldList['actions']['pri'] = '1'; -$config->project->removePriv['project'] = array('browse', 'kanban', 'create', 'batchEdit', 'qa', 'updateOrder', 'createGuide', 'programTitle'); -$config->project->removePriv['bug'] = array('batchChangePlan'); -$config->project->removePriv['doc'] = array('catalog', 'index'); +$config->project->removePriv['project'] = array('browse', 'kanban', 'create', 'batchEdit', 'qa', 'updateOrder', 'createGuide', 'programTitle'); +$config->project->removePriv['bug'] = array('batchChangePlan'); +$config->project->removePriv['doc'] = array('catalog', 'index'); +$config->project->removePriv['auditplan'] = array('delete'); diff --git a/module/project/control.php b/module/project/control.php index 0301af3a81..022f155e7d 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -1073,6 +1073,15 @@ class project extends control $this->config->build->search['fields']['execution'] = $this->project->lang->executionCommon; $this->config->build->search['params']['execution'] = array('operator' => '=', 'control' => 'select', 'values' => array('' => '') + $executions); + $product = $param ? $this->loadModel('product')->getById($param) : ''; + if($product and $product->type != 'normal') + { + $this->loadModel('build'); + $this->loadModel('branch'); + $branches = array(BRANCH_MAIN => $this->lang->branch->main) + $this->branch->getPairs($product->id, '', $projectID); + $this->config->build->search['fields']['branch'] = sprintf($this->lang->build->branchName, $this->lang->product->branchName[$product->type]); + $this->config->build->search['params']['branch'] = array('operator' => '=', 'control' => 'select', 'values' => $branches); + } $this->project->buildProjectBuildSearchForm($products, $queryID, $actionURL, 'project'); if($type == 'bysearch') @@ -1785,7 +1794,7 @@ class project extends control $this->view->unmodifiableProducts = $unmodifiableProducts; $this->view->unmodifiableBranches = $unmodifiableBranches; $this->view->unmodifiableMainBranches = $unmodifiableMainBranches; - $this->view->branchGroups = $this->loadModel('branch')->getByProducts(array_keys($allProducts), 'ignoreNormal'); + $this->view->branchGroups = $this->loadModel('branch')->getByProducts(array_keys($allProducts), 'ignoreNormal|noclosed'); $this->display(); } diff --git a/module/project/js/common.js b/module/project/js/common.js index a7c18ec6f8..48aa7f8f1d 100644 --- a/module/project/js/common.js +++ b/module/project/js/common.js @@ -188,9 +188,7 @@ function loadPlans(product, branchID) var branchID = typeof(branchID) == 'undefined' ? 0 : branchID; var index = $(product).attr('id').replace('products', ''); - if(typeof(planID) == 'undefined') planID = 0; - planID = $("select#plans" + productID).val() != '' ? $("select#plans" + productID).val() : planID; - $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=' + (config.currentMethod == 'create' ? 'unexpired' : '') + '¶m=skipParent'), function(data) + $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=0&fieldID&needCreate=&expired=' + (config.currentMethod == 'create' ? 'unexpired' : '') + '¶m=skipParent'), function(data) { if(data) { diff --git a/module/project/js/create.js b/module/project/js/create.js index b08a1ba690..1ca98e6111 100644 --- a/module/project/js/create.js +++ b/module/project/js/create.js @@ -199,9 +199,7 @@ function loadPlans(product, branchID) if(productID != 0) { - if(typeof(planID) == 'undefined') planID = 0; - planID = $("select#plans" + productID).val() != '' ? $("select#plans" + productID).val() : planID; - $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=unexpired¶m=skipParent'), function(data) + $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=0&fieldID&needCreate=&expired=unexpired¶m=skipParent'), function(data) { if(data) { diff --git a/module/project/js/kanban.js b/module/project/js/kanban.js index 147c67aad2..506d68b027 100644 --- a/module/project/js/kanban.js +++ b/module/project/js/kanban.js @@ -12,10 +12,25 @@ function processKanbanData(key, programGroup) var columns = []; $.each(kanbanColumns, function(_, column) { + var colType = column.type; + if(colType === 'doingProject') + { + columns.push( + { + kanban: kanbanId, + id: kanbanId + '-doing', + type: 'doing', + asParent: true, + name: doingText, + count: '' + }); + } + columns.push($.extend({}, column, { - kanban: kanbanId, - id: kanbanId + '-' + column.type, + kanban: kanbanId, + id: kanbanId + '-' + column.type, + parentType: (colType === 'doingProject' || colType === 'doingExecution') ? 'doing' : false, })); }); /* Format lanes data */ @@ -68,7 +83,6 @@ function findDropColumns($element, $root) var col = $col.data(); var lane = $col.closest('.kanban-lane').data(); var kanbanID = $root.data('id'); - console.log('findDropColumns', {$element, $root, kanbanID, col, lane}); var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null; if(!kanbanRules) return $root.find('.kanban-lane[data-id="' + lane.id + '"] .kanban-lane-col:not([data-type="doingExecution"],[data-type="' + col.type + '"])'); @@ -162,6 +176,13 @@ function handleFinishDrop(event) changeCardColType(card, fromColType, toColType, kanbanID); } +/** Calculate column height */ +function calcColHeight(col, lane, colCards, colHeight) +{ + if (col.type !== 'doingProject') return colHeight; + return colCards.length * 62; +} + $(function() { /* Init all kanbans */ @@ -171,8 +192,9 @@ $(function() if(!$kanban.length) return; $kanban.kanban( { - data: processKanbanData(key, programGroup), - maxColHeight: 'auto', + data: processKanbanData(key, programGroup), + calcColHeight: calcColHeight, + virtualize: true, droppable: { selector: '.kanban-item:not(.execution-item)', diff --git a/module/project/lang/en.php b/module/project/lang/en.php index a8d9f27d7e..4fab2f1b2c 100644 --- a/module/project/lang/en.php +++ b/module/project/lang/en.php @@ -71,8 +71,8 @@ $lang->project->category = 'Category'; $lang->project->desc = 'Description'; $lang->project->code = 'Code'; $lang->project->copy = 'Copy'; -$lang->project->begin = 'Begin'; -$lang->project->end = 'End'; +$lang->project->begin = 'Planned Begin'; +$lang->project->end = 'Planned End'; $lang->project->status = 'Status'; $lang->project->subStatus = 'Sub Status'; $lang->project->type = 'Type'; @@ -273,6 +273,7 @@ $lang->project->parentBeginEnd = "Parent begin&end date: %s ~ %s"; $lang->project->childLongTime = "If a child as long-term projects, the parent should be long-term too."; $lang->project->readjustTime = 'Change the project begin&end date.'; $lang->project->notAllowRemoveProducts = "Stories of this product are linked to projects or {$lang->execution->common} of this project is linked to this product. Please unlink it and try again."; +$lang->project->ge = "『%s』should be >= actual begin『%s』."; $lang->project->programTitle['0'] = 'Hidden'; $lang->project->programTitle['base'] = 'Base-level project only'; diff --git a/module/project/lang/zh-cn.php b/module/project/lang/zh-cn.php index 4f4e07f18b..bcf5e8bc26 100644 --- a/module/project/lang/zh-cn.php +++ b/module/project/lang/zh-cn.php @@ -180,9 +180,6 @@ $lang->project->createExecution = "该项目下没有{$lang->executionCom $lang->project->unlinkExecutionMember = "该用户参与了%s%s%s个{$lang->execution->common},是否同时将其移除?(该用户所产生的数据不会受影响。)"; $lang->project->unlinkExecutionMembers = "移除的团队成员还参与了项目下的执行,是否同步从执行团队中移除?"; -$lang->project->realEndNotEmpty = "实际完成不能为空。"; -$lang->project->realEndNotFuture = "实际完成不能大于当前日期。"; - $lang->project->tenThousand = '万'; $lang->project->unitList['CNY'] = '人民币'; @@ -273,6 +270,7 @@ $lang->project->parentBeginEnd = "父项目起止时间:%s ~ %s"; $lang->project->childLongTime = "子项目中有长期项目,父项目也应该是长期项目"; $lang->project->readjustTime = '重新调整项目起止时间'; $lang->project->notAllowRemoveProducts = "该产品中的需求与项目进行了关联或者项目下的{$lang->execution->common}关联了该产品,请取消关联后再操作。"; +$lang->project->ge = "『%s』应当不小于实际开始时间『%s』。"; $lang->project->programTitle['0'] = '不显示'; $lang->project->programTitle['base'] = '只显示一级项目集'; diff --git a/module/project/model.php b/module/project/model.php index 4b49d23069..68616da49d 100644 --- a/module/project/model.php +++ b/module/project/model.php @@ -1096,6 +1096,7 @@ class projectModel extends model $oldProjects = $this->getByIdList($this->post->projectIdList); $nameList = array(); + $extendFields = $this->getFlowExtendFields(); foreach($data->projectIdList as $projectID) { $projectID = (int)$projectID; @@ -1126,7 +1127,18 @@ class projectModel extends model } } + + foreach($extendFields as $extendField) + { + $projects[$projectID]->{$extendField->field} = $this->post->{$extendField->field}[$projectID]; + if(is_array($projects[$projectID]->{$extendField->field})) $projects[$projectID]->{$extendField->field} = join(',', $projects[$projectID]->{$extendField->field}); + + $projects[$projectID]->{$extendField->field} = htmlSpecialString($projects[$projectID]->{$extendField->field}); + $message = $this->checkFlowRule($extendField, $projects[$projectID]->{$extendField->field}); + if($message) die(js::alert($message)); + } } + if(dao::isError()) die(js::error(dao::getError())); foreach($projects as $projectID => $project) { @@ -1174,13 +1186,20 @@ class projectModel extends model $now = helper::now(); $project = fixer::input('post') - ->add('realBegan', helper::today()) ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->remove('comment')->get(); - $this->dao->update(TABLE_PROJECT)->data($project)->autoCheck()->where('id')->eq((int)$projectID)->exec(); + $this->dao->update(TABLE_PROJECT)->data($project) + ->autoCheck() + ->check($this->config->project->start->requiredFields, 'notempty') + ->checkIF($project->realBegan != '', 'realBegan', 'le', helper::today()) + ->where('id')->eq((int)$projectID) + ->exec(); + + /* When it has multiple errors, only the first one is prompted */ + if(dao::isError() and count(dao::$errors['realBegan']) > 1) dao::$errors['realBegan'] = dao::$errors['realBegan'][0]; if(!dao::isError()) return common::createChanges($oldProject, $project); } @@ -1326,22 +1345,19 @@ class projectModel extends model ->remove('comment') ->get(); - if($project->realEnd == '') - { - dao::$errors['realEnd'] = $this->lang->project->realEndNotEmpty; - return false; - } - if($project->realEnd > helper::today()) - { - dao::$errors['realEnd'] = $this->lang->project->realEndNotFuture; - return false; - } + $this->lang->error->ge = $this->lang->project->ge; $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() + ->check($this->config->project->close->requiredFields, 'notempty') + ->checkIF($project->realEnd != '', 'realEnd', 'le', helper::today()) + ->checkIF($project->realEnd != '', 'realEnd', 'ge', $oldProject->realBegan) ->where('id')->eq((int)$projectID) ->exec(); + /* When it has multiple errors, only the first one is prompted */ + if(dao::isError() and count(dao::$errors['realEnd']) > 1) dao::$errors['realEnd'] = dao::$errors['realEnd'][0]; + if(!dao::isError()) { $this->loadModel('score')->create('project', 'close', $oldProject); @@ -1493,13 +1509,13 @@ class projectModel extends model $class = "c-$id" . (in_array($id, array('budget', 'teamCount', 'estimate', 'consume')) ? ' c-number' : ''); if($id == 'id') $class .= ' cell-id'; - + if($id == 'code') { - $class .= ' c-name'; + $class .= ' c-name'; $title = "title={$project->code}"; - } - + } + if($id == 'name') { $class .= ' text-left'; diff --git a/module/project/view/batchedit.html.php b/module/project/view/batchedit.html.php index 00c3b2ee42..3dbc7bed55 100644 --- a/module/project/view/batchedit.html.php +++ b/module/project/view/batchedit.html.php @@ -30,6 +30,10 @@ project->begin;?> project->end;?> project->acl;?> + project->getFlowExtendFields(); + foreach($extendFields as $extendField) echo "{$extendField->name}"; + ?> @@ -56,6 +60,7 @@ ?> acl));?> + control == 'select' or $extendField->control == 'multi-select') ? " style='overflow:visible'" : '') . ">" . $this->loadModel('flow')->getFieldControl($extendField, $project, $extendField->field . "[{$projectID}]") . "";?> diff --git a/module/project/view/kanban.html.php b/module/project/view/kanban.html.php index a96ed7ee8d..f29a074a49 100644 --- a/module/project/view/kanban.html.php +++ b/module/project/view/kanban.html.php @@ -38,7 +38,7 @@ $kanbanColumns['closedProject'] = array('name' => $lang->project->closedProject $userPrivs = array(); $userPrivs['project'] = common::hasPriv('project', 'index'); $userPrivs['execution'] = common::hasPriv('execution', 'task'); -js::set('kanbanColumns', $kanbanColumns); +js::set('kanbanColumns', array_values($kanbanColumns)); js::set('userPrivs', $userPrivs); js::set('kanbanGroup', $kanbanGroup); js::set('latestExecutions', $latestExecutions); diff --git a/module/project/view/managepriv.html.php b/module/project/view/managepriv.html.php index c5b75fb85e..bf223d2e70 100644 --- a/module/project/view/managepriv.html.php +++ b/module/project/view/managepriv.html.php @@ -120,7 +120,7 @@ td.menus + td {border-left: 0;} $moduleName->menus) and $action == 'browse') continue;;?>
    - $lang->$moduleName->$actionLabel), isset($groupPrivs[$moduleName][$action]) ? $action : '', '', 'inline');?> + $lang->$moduleName->$actionLabel), isset($groupPrivs[$moduleName][$action]) ? $action : '', "title='{$lang->$moduleName->$actionLabel}'", 'inline');?>
    diff --git a/module/project/view/manageproducts.html.php b/module/project/view/manageproducts.html.php index e4b6ca5b82..d42d76fe6a 100644 --- a/module/project/view/manageproducts.html.php +++ b/module/project/view/manageproducts.html.php @@ -29,7 +29,7 @@ - project->notAllowRemoveProducts : $productName;?> + project->notAllowRemoveProducts : $productName;?>
    '> diff --git a/module/projectrelease/control.php b/module/projectrelease/control.php index cc213ef793..a3e63f1bf6 100644 --- a/module/projectrelease/control.php +++ b/module/projectrelease/control.php @@ -498,7 +498,7 @@ class projectrelease extends control $this->config->product->search['actionURL'] = $this->createLink('projectrelease', 'view', "releaseID=$releaseID&type=story&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; - $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent'); + $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent|withMainPlan'); $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, 'story', 0, $release->branch);; $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->story->statusList); if($release->productType == 'normal') @@ -617,7 +617,7 @@ class projectrelease extends control $this->config->bug->search['actionURL'] = $this->createLink('projectrelease', 'view', "releaseID=$releaseID&type=$type&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; - $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent'); + $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent|withMainPlan'); $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, 'bug', 0, $release->branch); $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($release->product, $release->branch, 'id_desc', $release->project); $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($release->product, $branch = 0, $params = ''); diff --git a/module/projectrelease/view/linkstory.html.php b/module/projectrelease/view/linkstory.html.php index d2028313b6..f75c38af81 100644 --- a/module/projectrelease/view/linkstory.html.php +++ b/module/projectrelease/view/linkstory.html.php @@ -48,7 +48,7 @@ id);?> pri;?>' title='story->priList, $story->pri)?>'>story->priList, $story->pri)?> - + parent > 0) echo "{$lang->story->childrenAB}"; echo html::a($this->createLink('story', 'view', "storyID={$story->id}&version=0¶m=$projectID", '', true), $story->title, '', "data-toggle='modal' data-type='iframe' data-width='90%'"); diff --git a/module/release/control.php b/module/release/control.php index 3b7a0cbf12..b3ecea2119 100644 --- a/module/release/control.php +++ b/module/release/control.php @@ -447,7 +447,7 @@ class release extends control $this->config->product->search['actionURL'] = $this->createLink('release', 'view', "releaseID=$releaseID&type=story&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; - $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent'); + $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent|withMainPlan'); $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->story->statusList); $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($release->product, 'story', 0, $release->branch);; if($this->session->currentProductType == 'normal') @@ -565,7 +565,7 @@ class release extends control $this->config->bug->search['actionURL'] = $this->createLink('release', 'view', "releaseID=$releaseID&type=$type&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; - $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent'); + $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent|withMainPlan'); $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($release->product, $release->branch); $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($release->product, $branch = 0, $params = ''); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values']; diff --git a/module/release/view/linkstory.html.php b/module/release/view/linkstory.html.php index f8e8ef906d..c8d9ca1e8f 100644 --- a/module/release/view/linkstory.html.php +++ b/module/release/view/linkstory.html.php @@ -48,7 +48,7 @@ id);?> pri;?>' title='story->priList, $story->pri)?>'>story->priList, $story->pri)?> - + parent > 0) echo "{$lang->story->childrenAB}"; echo html::a($this->createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', "data-toggle='modal' data-type='iframe' data-width='90%'"); diff --git a/module/repo/control.php b/module/repo/control.php index 8e11c63d0a..2bc9dd1f34 100644 --- a/module/repo/control.php +++ b/module/repo/control.php @@ -169,7 +169,8 @@ class repo extends control if(strtolower($repo->SCM) == 'gitlab') { - $projects = $this->loadModel('gitlab')->apiGetProjects($repo->gitlab); + $gitlabID = isset($repo->gitlab) ? $repo->gitlab : 0; + $projects = $this->loadModel('gitlab')->apiGetProjects($gitlabID); $options = array(); foreach($projects as $project) $options[$project->id] = $project->name_with_namespace; @@ -184,7 +185,7 @@ class repo extends control $this->view->groups = $this->loadModel('group')->getPairs(); $this->view->users = $this->loadModel('user')->getPairs('noletter|noempty|nodeleted'); $this->view->products = $objectID ? $this->loadModel('product')->getProductPairsByProject($objectID) : $this->loadModel('product')->getPairs(); - $this->view->gitlabHosts = $this->loadModel('gitlab')->getPairs(); + $this->view->gitlabHosts = array('' => '') + $this->loadModel('gitlab')->getPairs(); $this->view->position[] = html::a(inlink('maintain'), $this->lang->repo->common); $this->view->position[] = $this->lang->repo->edit; @@ -401,6 +402,8 @@ class repo extends control /* Cache infos. */ if($refresh or !$cacheFile or !file_exists($cacheFile) or (time() - filemtime($cacheFile)) / 60 > $this->config->repo->cacheTime) { + $this->repo->syncCommit($repoID, $branchID); + /* Get cache infos. */ $infos = $this->scm->ls($path, $revision); @@ -1172,4 +1175,5 @@ class repo extends control $productPairs = $this->repo->getProductsByRepo($repoID); echo html::select('product', array('') + $productPairs, key($productPairs), "class='form-control chosen'"); } + } diff --git a/module/repo/lang/en.php b/module/repo/lang/en.php index 1c58036410..9bdeda0674 100644 --- a/module/repo/lang/en.php +++ b/module/repo/lang/en.php @@ -23,6 +23,7 @@ $lang->repo->encrypt = 'Encrypt'; $lang->repo->repo = 'Repository'; $lang->repo->parent = 'Parent File'; $lang->repo->branch = 'Branch'; +$lang->repo->addWebHook = 'Add Webhook'; $lang->repo->browseAction = 'Browse Repo'; $lang->repo->createAction = 'Create Repo'; diff --git a/module/repo/lang/zh-cn.php b/module/repo/lang/zh-cn.php index 76ae2272b8..4cec692ac4 100644 --- a/module/repo/lang/zh-cn.php +++ b/module/repo/lang/zh-cn.php @@ -23,6 +23,7 @@ $lang->repo->encrypt = '加密方式'; $lang->repo->repo = '代码库'; $lang->repo->parent = '父文件夹'; $lang->repo->branch = '分支'; +$lang->repo->addWebHook = '添加Webhook'; $lang->repo->browseAction = '浏览版本库'; $lang->repo->createAction = '创建版本库'; diff --git a/module/repo/model.php b/module/repo/model.php index d61b0b5bee..0f6766f8f0 100644 --- a/module/repo/model.php +++ b/module/repo/model.php @@ -232,7 +232,16 @@ class repoModel extends model if(!dao::isError()) $this->rmClientVersionFile(); - return $this->dao->lastInsertID(); + $repoID = $this->dao->lastInsertID(); + + if($this->post->SCM == 'Gitlab') + { + /* Add webhook. */ + $repo = $this->getRepoByID($repoID); + $this->loadModel('gitlab')->addPushWebhook($repo); + } + + return $repoID; } /** @@ -407,15 +416,23 @@ class repoModel extends model /** * Get git branches. * - * @param object $repo + * @param object $repo + * @param bool $printLabel * @access public * @return array */ - public function getBranches($repo) + public function getBranches($repo, $printLabel = false) { $this->scm = $this->app->loadClass('scm'); $this->scm->setEngine($repo); - return $this->scm->branch(); + $branches = $this->scm->branch(); + + if($printLabel) + { + foreach($branches as &$branch) $branch = 'Branch::' . $branch; + } + + return $branches; } /** @@ -770,7 +787,7 @@ class repoModel extends model * @access public * @return array */ - public function getUnsyncCommits($repo) + public function getUnsyncedCommits($repo) { $repoID = $repo->id; $lastInDB = $this->getLatestCommit($repoID); @@ -1219,10 +1236,10 @@ class repoModel extends model /** * Add link. * - * @param string $matches + * @param array $matches * @param string $method * @access public - * @return string + * @return array */ public function addLink($matches, $method) { @@ -1434,8 +1451,11 @@ class repoModel extends model */ public function saveAction2PMS($objects, $log, $repoRoot = '', $encodings = 'utf-8', $scm = 'svn') { - $account = $this->app->user->account; - $this->app->user->account = $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; @@ -1613,7 +1633,7 @@ class repoModel extends model } } - $this->app->user->account = $account; + if(isset($this->app->user)) $this->app->user->account = $account; } /** @@ -1763,13 +1783,94 @@ class repoModel extends model public function processGitlab($repo) { $gitlab = $this->loadModel('gitlab')->getByID($repo->client); // The $repo->client is gitlabID. - if(!$gitlab) return $repo; - $repo->gitlab = $gitlab->id; - $repo->project = $repo->path; // The projectID in gitlab. - $repo->path = sprintf($this->config->repo->gitlab->apiPath, $gitlab->url, $repo->path); - $repo->client = $gitlab->url; - $repo->password = $gitlab->token; + $repo->gitlab = $gitlab ? $gitlab->id : 0; + $repo->project = $gitlab ? $repo->path : ''; // The projectID in gitlab. + $repo->path = $gitlab ? sprintf($this->config->repo->gitlab->apiPath, $gitlab->url, $repo->path) : ''; + $repo->client = $gitlab ? $gitlab->url : ''; + $repo->password = $gitlab ? $gitlab->token : ''; return $repo; } + + /** + * Get repositories which scm is GitLab and specified gitlabID and projectID. + * + * @param int $gitlabID + * @param int $projectID + * @return array + */ + public function getGitLabRepoList($gitlabID, $projectID) + { + return $this->dao->select('*')->from(TABLE_REPO)->where('deleted')->eq('0') + ->andWhere('SCM')->eq('Gitlab') + ->andWhere('synced')->eq(1) + ->andWhere('client')->eq($gitlabID) + ->andWhere('path')->eq($projectID) + ->fetchAll(); + } + + /** + * Handle received GitLab webhook. + * + * @param string $event + * @param string $token + * @param string $data + * @param object $repo + * @access public + * @return void + */ + public function handleWebhook($event, $token, $data, $repo) + { + if($event == 'Push Hook' or $event == 'Merge Request Hook') + { + /* Update code commit history. */ + $commentGroup = $this->loadModel('job')->getTriggerGroup('commit', array($repo->id)); + $this->loadModel('git')->updateCommit($repo, $commentGroup, false); + } + } + + /** + * Get products which scm is GitLab by projects. + * + * @param array $projectIDs + * @return array + */ + public function getGitlabProductsByProjects($projectIDs) + { + return $this->dao->select('path,product')->from(TABLE_REPO)->where('deleted')->eq('0') + ->andWhere('SCM')->eq('Gitlab') + ->andWhere('path')->in($projectIDs) + ->fetchPairs('path', 'product'); + } + + /** + * Sync the latest commit. + * + * @param int $repoID + * @param string $branchID + * @access public + * @return void + */ + public function syncCommit($repoID, $branchID) + { + $repo = $this->getRepoByID($repoID); + $this->scm->setEngine($repo); + + $latestInDB = $this->dao->select('DISTINCT t1.*')->from(TABLE_REPOHISTORY)->alias('t1') + ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision') + ->where('t1.repo')->eq($repoID) + ->beginIF($repo->SCM == 'Git' and $branchID)->andWhere('t2.branch')->eq($branchID)->fi() + ->beginIF($repo->SCM == 'Gitlab' and $branchID)->andWhere('t2.branch')->eq($branchID)->fi() + ->orderBy('t1.time desc') + ->limit(1) + ->fetch(); + $version = empty($latestInDB) ? 1 : $latestInDB->commit + 1; + $revision = $version == 1 ? 'HEAD' : ($repo->SCM == 'Git' ? $latestInDB->commit : $latestInDB->revision); + + $logs = $this->scm->getCommits('since' . $revision, $this->config->repo->batchNum, $branchID); + $commitCount = $this->saveCommit($repoID, $logs, $version, $branchID); + $this->dao->update(TABLE_REPO)->set('commits=commits + ' . $commitCount)->where('id')->eq($repoID)->exec(); + + $this->fixCommit($repoID); + } } diff --git a/module/repo/view/edit.html.php b/module/repo/view/edit.html.php index ebfda168e9..8fbb8db609 100644 --- a/module/repo/view/edit.html.php +++ b/module/repo/view/edit.html.php @@ -38,11 +38,11 @@ repo->gitlabHost;?> - gitlab, "class='form-control' placeholder='{$lang->repo->placeholder->gitlabHost}'");?> + gitlab) ? $repo->gitlab : '', "class='form-control' placeholder='{$lang->repo->placeholder->gitlabHost}'");?> repo->gitlabProject;?> - project, "class='form-control chosen'");?> + project) ? $repo->project : '', "class='form-control chosen'");?> repo->name; ?> diff --git a/module/repo/view/maintain.html.php b/module/repo/view/maintain.html.php index a59d18a755..000c42f1d9 100644 --- a/module/repo/view/maintain.html.php +++ b/module/repo/view/maintain.html.php @@ -27,7 +27,7 @@ repo->name); ?> repo->product); ?> repo->path; ?> - actions; ?> + actions; ?> @@ -53,8 +53,13 @@ id&objectID=$objectID", '', 'list', 'edit'); - if(strtolower($repo->SCM) == "gitlab") common::printIcon('gitlab', 'importIssue', "repo={$repo->id}", '', 'list', 'link'); - if(common::hasPriv('repo', 'delete')) echo html::a($this->createLink('repo', 'delete', "repoID=$repo->id&objectID=$objectID"), '', 'hiddenwin', "title='{$lang->repo->delete}' class='btn'"); + if(strtolower($repo->SCM) == "gitlab") + { + common::printIcon('gitlab', 'createWebhook', "repoID=$repo->id", '', 'list', 'change', 'hiddenwin'); + common::printIcon('gitlab', 'importIssue', "repo={$repo->id}", '', 'list', 'link'); + common::printIcon('gitlab', 'manageProjectMembers', "repo={$repo->id}", '', 'list', 'team'); + } + common::printIcon('repo', 'delete', "repoID=$repo->id&objectID=$objectID", '', 'list', 'trash', 'hiddenwin'); ?> diff --git a/module/story/control.php b/module/story/control.php index 5b47f55529..a7d2e27a2c 100644 --- a/module/story/control.php +++ b/module/story/control.php @@ -1724,6 +1724,7 @@ class story extends control */ public function zeroCase($productID = 0, $branchID = 0, $orderBy = 'id_desc', $projectID = 0) { + $orderBy = empty($orderBy) ? 'id_desc' : $orderBy; $this->session->set('storyList', $this->app->getURI(true) . '#app=' . $this->app->tab, 'product'); $this->session->set('caseList', $this->app->getURI(true), $this->app->tab); diff --git a/module/story/js/batchedit.js b/module/story/js/batchedit.js index fd823b61f7..6847a40766 100644 --- a/module/story/js/batchedit.js +++ b/module/story/js/batchedit.js @@ -30,7 +30,8 @@ function loadBranches(product, branch, storyID) if(typeof(branch) == 'undefined') branch = 0; if(!branch) branch = 0; - moduleLink = createLink('tree', 'ajaxGetOptionMenu', 'productID=' + product + '&viewtype=story&branch=' + branch + '&rootModuleID=0&returnType=html&fieldID=' + storyID); + var currentModuleID = $('#modules' + storyID).val(); + moduleLink = createLink('tree', 'ajaxGetOptionMenu', 'productID=' + product + '&viewtype=story&branch=' + branch + '&rootModuleID=0&returnType=html&fieldID=' + storyID + '&needManage=false&extra=¤tModuleID=' + currentModuleID); $('#modules' + storyID).parent('td').load(moduleLink, function(){$('#modules' + storyID).chosen();}); planID = $('#plans' + storyID).val(); diff --git a/module/story/model.php b/module/story/model.php index b178fa8cb2..d539d4f382 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -2491,7 +2491,7 @@ class storyModel extends model { $storyQuery = str_replace($allBranch, '1', $storyQuery); } - elseif($branch) + elseif($branch !== 'all') { if($branch and strpos($storyQuery, '`branch` =') === false) $storyQuery .= " AND `branch` in($branch)"; } diff --git a/module/story/view/header.html.php b/module/story/view/header.html.php index ec1e52aed1..aa38c81062 100644 --- a/module/story/view/header.html.php +++ b/module/story/view/header.html.php @@ -1,5 +1,6 @@ +app->rawMethod);?>