diff --git a/.gitignore b/.gitignore
index b0628fb53a..ad25ba4508 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,5 @@ tmp/extension/*
.gitignore
.DS_Store
vendor/
+composer.lock
+
diff --git a/api/v1/entries/error.php b/api/v1/entries/error.php
new file mode 100644
index 0000000000..c2b5eece56
--- /dev/null
+++ b/api/v1/entries/error.php
@@ -0,0 +1,15 @@
+send(404, array('error' => 'not found'));
+ }
+}
diff --git a/api/v1/entries/product.php b/api/v1/entries/product.php
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/v1/entries/productline.php b/api/v1/entries/productline.php
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/v1/entries/productlines.php b/api/v1/entries/productlines.php
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/v1/entries/products.php b/api/v1/entries/products.php
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/v1/entries/program.php b/api/v1/entries/program.php
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/v1/entries/programs.php b/api/v1/entries/programs.php
new file mode 100644
index 0000000000..0c94910361
--- /dev/null
+++ b/api/v1/entries/programs.php
@@ -0,0 +1,9 @@
+loadController('program', 'browse');
+ $program->browse();
+ }
+}
diff --git a/api/v1/entries/project.php b/api/v1/entries/project.php
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/v1/entries/projects.php b/api/v1/entries/projects.php
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/v1/entries/task.php b/api/v1/entries/task.php
new file mode 100644
index 0000000000..00496ea8ef
--- /dev/null
+++ b/api/v1/entries/task.php
@@ -0,0 +1,44 @@
+loadController('task', 'view');
+ $control->view($taskID);
+
+ $data = $this->getData();
+ $task = $data->data->task;
+ $this->send(200, $task);
+ }
+
+ public function put($taskID)
+ {
+ $oldTask = $this->loadModel('task')->getByID($taskID);
+
+ /* Set $_POST variables. */
+ $fields = 'name,type,assignedTo,estimate,left,consumed,story,parent,execution,module,closedReason,status';
+ $this->batchSetPost($fields, $oldTask);
+
+ $control = $this->loadController('task', 'edit');
+ $control->edit($taskID);
+
+ $this->getData();
+ $this->sendSuccess(200, 'success');
+ }
+
+ public function delete($taskID)
+ {
+ $control = $this->loadController('task', 'delete');
+ $control->delete(0, $taskID, 'true');
+
+ $this->getData();
+ $this->sendSuccess(200, 'success');
+ }
+}
diff --git a/api/v1/entries/taskassignto.php b/api/v1/entries/taskassignto.php
new file mode 100644
index 0000000000..77feefa35c
--- /dev/null
+++ b/api/v1/entries/taskassignto.php
@@ -0,0 +1,30 @@
+loadModel('task')->getByID($taskID);
+
+ $fields = 'assignedTo,comment,left';
+ $this->batchSetPost($fields);
+
+ $control = $this->loadController('task', 'assignTo');
+ $this->requireFields('assignedTo');
+
+ $control->assignTo($task->execution, $taskID);
+
+ $data = $this->getData();
+ if($data->result == 'fail') return $this->sendError(400, $data->message);
+
+ $task = $this->loadModel('task')->getByID($dataID);
+
+ $this->send(200, $task);
+ }
+}
diff --git a/api/v1/entries/taskfinish.php b/api/v1/entries/taskfinish.php
new file mode 100644
index 0000000000..5f23f8e70a
--- /dev/null
+++ b/api/v1/entries/taskfinish.php
@@ -0,0 +1,35 @@
+loadModel('task')->getByID($taskID);
+
+ $fields = 'assignedTo,realStarted';
+ $this->batchSetPost($fields, $task);
+
+ $fields = 'finishedDate,comment';
+ $this->batchSetPost($fields);
+
+ $this->setPost('currentConsumed', $this->request('currentConsumed', 0));
+ $this->setPost('consumed', $this->request('currentConsumed', 0) + $task->consumed);
+
+ $control = $this->loadController('task', 'finish');
+ $this->requireFields('assignedTo,currentConsumed,realStarted,finishedDate');
+ $control->finish($taskID);
+
+ $data = $this->getData();
+ if($data->result == 'fail') return $this->sendError(400, $data->message);
+
+ $task = $this->loadModel('task')->getByID($taskID);
+
+ $this->send(200, $task);
+ }
+}
diff --git a/api/v1/entries/tasks.php b/api/v1/entries/tasks.php
new file mode 100644
index 0000000000..a1df8a596f
--- /dev/null
+++ b/api/v1/entries/tasks.php
@@ -0,0 +1,32 @@
+batchSetPost($fields);
+
+ $control = $this->loadController('task', 'create');
+ $this->requireFields('name,assignedTo,type');
+
+ $control->create($executionID, $this->request('storyID', 0), $this->request('moduleID', 0), $this->request('copyTaskID', 0), $this->request('copyTodoID', 0));
+
+ $data = $this->getData();
+ if(!isset($data->id)) return $this->sendError(400, $data->message);
+
+ $task = $this->loadModel('task')->getByID($data->id);
+
+ $this->send(200, $task);
+ }
+}
diff --git a/api/v1/entries/taskstart.php b/api/v1/entries/taskstart.php
new file mode 100644
index 0000000000..d81e69874e
--- /dev/null
+++ b/api/v1/entries/taskstart.php
@@ -0,0 +1,29 @@
+loadModel('task')->getByID($taskID);
+
+ $fields = 'assignedTo,realStarted,comment,left';
+ $this->batchSetPost($fields);
+
+ $control = $this->loadController('task', 'start');
+ $this->requireFields('left');
+ $control->start($taskID);
+
+ $data = $this->getData();
+ if($data->result == 'fail') return $this->sendError(400, $data->message);
+
+ $task = $this->loadModel('task')->getByID($dataID);
+
+ $this->send(200, $task);
+ }
+}
diff --git a/api/v1/entries/tokens.php b/api/v1/entries/tokens.php
new file mode 100644
index 0000000000..b4d7732643
--- /dev/null
+++ b/api/v1/entries/tokens.php
@@ -0,0 +1,26 @@
+request('account');
+ $password = $this->request('password');
+
+ $user = $this->loadModel('user')->identify($account, $password);
+
+ if($user)
+ {
+ $this->user->login($user);
+ $this->send(200, array('token' => session_id()));
+ }
+
+ $this->sendError(400, $this->app->lang->user->loginFailed);
+ }
+}
diff --git a/config/config.php b/config/config.php
index 186ed887a2..dca995a082 100644
--- a/config/config.php
+++ b/config/config.php
@@ -170,6 +170,10 @@ if(file_exists($myConfig)) include $myConfig;
$zentaopmsConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'zentaopms.php';
if(file_exists($zentaopmsConfig)) include $zentaopmsConfig;
+/* API路由配置。API route settings. */
+$routesConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'routes.php';
+if(file_exists($routesConfig)) include $routesConfig;
+
/* Include extension config files. */
$extConfigFiles = glob(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'ext/*.php');
if($extConfigFiles) foreach($extConfigFiles as $extConfigFile) include $extConfigFile;
diff --git a/config/filter.php b/config/filter.php
index f69a51cd97..42bd625242 100644
--- a/config/filter.php
+++ b/config/filter.php
@@ -56,6 +56,7 @@ $filter->webhook = new stdclass();
$filter->git = new stdclass();
$filter->svn = new stdclass();
$filter->search = new stdclass();
+$filter->gitlab = new stdclass();
$filter->block->default = new stdclass();
$filter->block->main = new stdclass();
@@ -130,6 +131,8 @@ $filter->webhook->bind = new stdclass();
$filter->user->ajaxgetmore = new stdclass();
$filter->repo->ajaxsynccommit = new stdclass();
$filter->search->index = new stdclass();
+$filter->gitlab->webhook = new stdclass();
+$filter->gitlab->importissue = new stdclass();
$filter->bug->batchcreate->cookie['preBranch'] = 'int';
$filter->bug->browse->cookie['bugModule'] = 'int';
@@ -308,3 +311,15 @@ $filter->webhook->bind->cookie['selectedDepts'] = 'reg::checked';
$filter->search->index->get['words'] = 'reg::any';
$filter->search->index->get['type'] = 'code';
+
+$filter->gitlab->webhook->get['gitlab'] = 'int';
+$filter->gitlab->webhook->get['product'] = 'int';
+$filter->gitlab->webhook->get['project'] = 'int';
+$filter->gitlab->webhook->get['token'] = 'reg::any';
+
+$filter->gitlab->importissue->get['gitlab'] = 'int';
+$filter->gitlab->importissue->get['product'] = 'int';
+$filter->gitlab->importissue->get['product'] = 'string';
+$filter->gitlab->importissue->get['project'] = 'int';
+$filter->gitlab->importissue->get['repo'] = 'int';
+
diff --git a/config/routes.php b/config/routes.php
new file mode 100644
index 0000000000..9d76ff8beb
--- /dev/null
+++ b/config/routes.php
@@ -0,0 +1,26 @@
+routes = $routes;
diff --git a/config/zentaopms.php b/config/zentaopms.php
index 870b7560c1..0b7db239c2 100644
--- a/config/zentaopms.php
+++ b/config/zentaopms.php
@@ -115,6 +115,29 @@ $config->charsets['fr']['GBK'] = 'GBK';
$config->charsets['vi']['utf-8'] = 'UTF-8';
$config->charsets['vi']['GBK'] = 'GBK';
+$config->openMethods = array();
+$config->openMethods[] = 'gitlab.webhook';
+$config->openMethods[] = 'upgrade.ajaxupdatefile';
+$config->openMethods[] = 'user.login';
+$config->openMethods[] = 'user.logout';
+$config->openMethods[] = 'user.deny';
+$config->openMethods[] = 'user.reset';
+$config->openMethods[] = 'user.refreshrandom';
+$config->openMethods[] = 'api.getsessionid';
+$config->openMethods[] = 'misc.checktable';
+$config->openMethods[] = 'misc.qrcode';
+$config->openMethods[] = 'misc.about';
+$config->openMethods[] = 'misc.checkupdate';
+$config->openMethods[] = 'misc.ping';
+$config->openMethods[] = 'misc.captcha';
+$config->openMethods[] = 'sso.login';
+$config->openMethods[] = 'sso.logout';
+$config->openMethods[] = 'sso.bind';
+$config->openMethods[] = 'sso.gettodolist';
+$config->openMethods[] = 'file.read';
+$config->openMethods[] = 'index.changelog';
+$config->openMethods[] = 'my.preference';
+
/* Define the tables. */
define('TABLE_COMPANY', '`' . $config->db->prefix . 'company`');
define('TABLE_DEPT', '`' . $config->db->prefix . 'dept`');
@@ -185,7 +208,7 @@ define('TABLE_LOG', '`' . $config->db->prefix . 'log`');
define('TABLE_SCORE', '`' . $config->db->prefix . 'score`');
define('TABLE_NOTIFY', '`' . $config->db->prefix . 'notify`');
define('TABLE_OAUTH', '`' . $config->db->prefix . 'oauth`');
-define('TABLE_JENKINS', '`' . $config->db->prefix . 'jenkins`');
+define('TABLE_PIPELINE', '`' . $config->db->prefix . 'pipeline`');
define('TABLE_JOB', '`' . $config->db->prefix . 'job`');
define('TABLE_COMPILE', '`' . $config->db->prefix . 'compile`');
@@ -231,5 +254,5 @@ $config->objectTables['team'] = TABLE_TEAM;
/* Program privs.*/
$config->programPriv = new stdclass();
-$config->programPriv->scrum = array('product', 'story', 'productplan', 'release', 'project', 'task', 'build', 'qa', 'bug', 'testcase', 'testsuite', 'testreport', 'caselib', 'doc', 'report', 'repo', 'svn', 'git', 'search', 'tree', 'file', 'jenkins', 'job', 'ci', 'branch');
+$config->programPriv->scrum = array('product', 'story', 'productplan', 'release', 'project', 'task', 'build', 'qa', 'bug', 'testcase', 'testsuite', 'testreport', 'caselib', 'doc', 'report', 'repo', 'svn', 'git', 'search', 'tree', 'file', 'jenkins', 'gitlab', 'job', 'ci', 'branch');
$config->programPriv->waterfall = $config->programPriv->scrum + array('workestimation', 'durationestimation', 'budget', 'programplan', 'review', 'reviewissue', 'weekly', 'milestone', 'design', 'issue', 'risk', 'auditplan', 'nc', 'cm', 'pssp');
diff --git a/db/standard/zentao15.0.rc3.sql b/db/standard/zentao15.0.rc3.sql
index d59660d05a..68b0271093 100644
--- a/db/standard/zentao15.0.rc3.sql
+++ b/db/standard/zentao15.0.rc3.sql
@@ -447,7 +447,7 @@ CREATE TABLE `zt_history` (
PRIMARY KEY (`id`),
KEY `action` (`action`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-CREATE TABLE `zt_jenkins` (
+CREATE TABLE `zt_pipeline` (
`id` smallint(8) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`url` varchar(255) DEFAULT NULL,
diff --git a/db/zentao.sql b/db/zentao.sql
index 328e3bcce9..3e1fc29691 100644
--- a/db/zentao.sql
+++ b/db/zentao.sql
@@ -471,14 +471,16 @@ CREATE TABLE IF NOT EXISTS `zt_history` (
PRIMARY KEY (`id`),
KEY `action` (`action`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
--- DROP TABLE IF EXISTS `zt_jenkins`;
-CREATE TABLE IF NOT EXISTS `zt_jenkins` (
+-- DROP TABLE IF EXISTS `zt_pipeline`;
+CREATE TABLE IF NOT EXISTS `zt_pipeline` (
`id` smallint(8) unsigned NOT NULL AUTO_INCREMENT,
+ `type` char(30) NOT NULL,
`name` varchar(50) NOT NULL,
`url` varchar(255) DEFAULT NULL,
`account` varchar(30) DEFAULT NULL,
`password` varchar(255) NOT NULL,
`token` varchar(255) DEFAULT NULL,
+ `private` char(32) DEFAULT NULL,
`createdBy` varchar(30) NOT NULL,
`createdDate` datetime NOT NULL,
`editedBy` varchar(30) NOT NULL,
@@ -749,7 +751,7 @@ CREATE TABLE IF NOT EXISTS `zt_relation` (
`BVersion` char(30) NOT NULL,
`extra` char(30) NOT NULL,
PRIMARY KEY (`id`),
- UNIQUE KEY `relation` (`relation`,`AType`,`BType`, `AID`, `BID`)
+ UNIQUE KEY `relation` (`product`,`relation`,`AType`,`BType`, `AID`, `BID`)
) ENGINE='MyISAM' DEFAULT CHARSET=utf8;
-- DROP TABLE IF EXISTS `zt_release`;
CREATE TABLE IF NOT EXISTS `zt_release` (
diff --git a/framework/api/entry.class.php b/framework/api/entry.class.php
new file mode 100644
index 0000000000..03aac91b8f
--- /dev/null
+++ b/framework/api/entry.class.php
@@ -0,0 +1,369 @@
+app->user)) $this->sendError(401, 'Unauthorized');
+ }
+}
+
+/**
+ * 禅道API的baseEntry类。
+ * The baseEntry class file of ZenTao API.
+ *
+ */
+class baseEntry
+{
+ /**
+ * 全局对象 $app。
+ * The global $app object.
+ *
+ * @var object
+ * @access public
+ */
+ public $app;
+
+ /**
+ * 提交的POST数据
+ * The decoded request body.
+ *
+ * @var object
+ * @access public
+ */
+ public $requestBody;
+
+ /**
+ * 构造方法。
+ * The construct function.
+ *
+ * @access public
+ * @return void
+ */
+ public function __construct()
+ {
+ global $app;
+ $this->app = $app;
+
+ $this->parseRequestBody();
+ }
+
+ /**
+ * 获取请求数据(POST PUT)
+ * Get request data(POST or PUT).
+ *
+ * @param string $key
+ * @param mixed $defaultValue
+ * @access public
+ * @return mixed
+ */
+ public function request($key, $defaultValue = '')
+ {
+ if(isset($this->requestBody->$key)) return $this->requestBody->$key;
+ return $defaultValue;
+ }
+
+ /**
+ * 解析请求数据
+ * Parse body of request data.
+ *
+ * @access public
+ * @return void
+ */
+ private function parseRequestBody()
+ {
+ $this->requestBody = new stdClass();
+
+ if($this->app->action == 'post' or $this->app->action == 'put')
+ {
+ $requestBody = file_get_contents("php://input");
+ if($requestBody) $this->requestBody = json_decode($requestBody);
+ }
+ }
+
+ /**
+ * HTTP状态码
+ * HTTP status code
+ *
+ * @access public
+ */
+ public $statusCode = array(
+ 100 => "100 Continue",
+ 101 => "101 Switching Protocols",
+ 102 => "102 Processing",
+
+ 200 => "200 OK",
+ 201 => "201 Created",
+ 202 => "202 Accepted",
+ 203 => "203 Non-Authoritative Information",
+ 204 => "204 No Content",
+ 205 => "205 Reset Content",
+ 206 => "206 Partial Content",
+ 207 => "207 Multi-Status",
+
+ 300 => "300 Multiple Choices",
+ 301 => "301 Moved Permanently",
+ 302 => "302 Found",
+ 303 => "303 See Other",
+ 304 => "304 Not Modified",
+ 305 => "305 Use Proxy",
+ 307 => "307 Temporary Redirect",
+
+ 400 => "400 Bad Request",
+ 401 => "401 Authorization Required",
+ 402 => "402 Payment Required",
+ 403 => "403 Forbidden",
+ 404 => "404 Not Found",
+ 405 => "405 Method Not Allowed",
+ 406 => "406 Not Acceptable",
+ 407 => "407 Proxy Authentication Required",
+ 408 => "408 Request Time-out",
+ 409 => "409 Conflict",
+ 410 => "410 Gone",
+ 411 => "411 Length Required",
+ 412 => "412 Precondition Failed",
+ 413 => "413 Request Entity Too Large",
+ 414 => "414 Request-URI Too Large",
+ 415 => "415 Unsupported Media Type",
+ 416 => "416 Requested Range Not Satisfiable",
+ 417 => "417 Expectation Failed",
+ 422 => "422 Unprocessable Entity",
+ 423 => "423 Locked",
+ 424 => "424 Failed Dependency",
+ 426 => "426 Upgrade Required",
+ );
+
+ /**
+ * 发送请求的响应数据
+ * Send response data
+ *
+ * @access public
+ * @return void
+ */
+ public function send($code, $data)
+ {
+ header("Content-type: application/json");
+ header("HTTP/1.1 {$this->statusCode[$code]}");
+ echo json_encode($data);
+ exit;
+ }
+
+ /**
+ * 发送错误信息
+ * Send error response
+ *
+ * @param int $code
+ * @param string $msg
+ * @access public
+ * @return void
+ */
+ public function sendError($code, $msg)
+ {
+ $response = new stdclass();
+ $response->error = $msg;
+
+ $this->send($code, $response);
+ }
+
+ /**
+ * 发送成功提示
+ * Send success response
+ *
+ * @param int $code
+ * @param string $msg
+ * @access public
+ * @return void
+ */
+ public function sendSuccess($code, $msg)
+ {
+ $response = new stdclass();
+ $response->message = $msg;
+
+ $this->send($code, $response);
+ }
+
+ /**
+ * 加载禅道的控制器类
+ * Load controller of zentaopms
+ *
+ * @param string $moduleName
+ * @param string $methodName
+ * @access public
+ * @return object
+ */
+ public function loadController($moduleName, $methodName)
+ {
+ global $app;
+ $app->setModuleName($moduleName);
+ $app->setMethodName($methodName);
+ $app->setControlFile();
+
+ /*
+ * 引入该模块的control文件。
+ * Include the control file of the module.
+ **/
+ $file2Included = $app->setActionExtFile() ? $app->extActionFile : $app->controlFile;
+ chdir(dirname($file2Included));
+ helper::import($file2Included);
+
+ /*
+ * 设置control的类名。
+ * Set the class name of the control.
+ **/
+ $className = class_exists("my$moduleName") ? "my$moduleName" : $moduleName;
+ if(!class_exists($className)) $app->triggerError("the control $className not found", __FILE__, __LINE__, $exit = true);
+
+ $controller = new $className();
+ $controller->viewType = 'json';
+ return $controller;
+ }
+
+ /**
+ * 加载指定模块的model文件。
+ * Load the model file of one module.
+ *
+ * @param string $moduleName 模块名,如果为空,使用当前模块。The module name, if empty, use current module's name.
+ * @param string $appName The app name, if empty, use current app's name.
+ * @access public
+ * @return object|bool 如果没有model文件,返回false,否则返回model对象。If no model file, return false, else return the model object.
+ */
+ public function loadModel($moduleName = '', $appName = '')
+ {
+ if(empty($moduleName)) $moduleName = $this->moduleName;
+ if(empty($appName)) $appName = $this->app->appName;
+
+ global $loadedModels;
+ if(isset($loadedModels[$appName][$moduleName]))
+ {
+ $this->$moduleName = $loadedModels[$appName][$moduleName];
+ $this->dao = $this->$moduleName->dao;
+ return $this->$moduleName;
+ }
+
+ $modelFile = $this->app->setModelFile($moduleName, $appName);
+
+ /**
+ * 如果没有model文件,尝试加载config配置信息。
+ * If no model file, try load config.
+ */
+ if(!helper::import($modelFile))
+ {
+ $this->app->loadModuleConfig($moduleName, $appName);
+ $this->app->loadLang($moduleName, $appName);
+ $this->dao = new dao();
+ return false;
+ }
+
+ /**
+ * 如果没有扩展文件,model类名是$moduleName + 'model',如果有扩展,还需要增加ext前缀。
+ * If no extension file, model class name is $moduleName + 'model', else with 'ext' as the prefix.
+ */
+ $modelClass = class_exists('ext' . $appName . $moduleName. 'model') ? 'ext' . $appName . $moduleName . 'model' : $appName . $moduleName . 'model';
+ if(!class_exists($modelClass))
+ {
+ $modelClass = class_exists('ext' . $moduleName. 'model') ? 'ext' . $moduleName . 'model' : $moduleName . 'model';
+ if(!class_exists($modelClass)) $this->app->triggerError(" The model $modelClass not found", __FILE__, __LINE__, $exit = true);
+ }
+
+ /**
+ * 初始化model对象,在control对象中可以通过$this->$moduleName来引用。同时将dao对象赋为control对象的成员变量,方便引用。
+ * Init the model object thus you can try $this->$moduleName to access it. Also assign the $dao object as a member of control object.
+ */
+ $loadedModels[$appName][$moduleName] = new $modelClass($appName);
+ $this->$moduleName = $loadedModels[$appName][$moduleName];
+ $this->dao = $this->$moduleName->dao;
+
+ return $this->$moduleName;
+ }
+
+ /**
+ * 获取控制器执行返回的数据,在output缓存中.
+ * Get controller data from output.
+ *
+ * @access public
+ * @return object.
+ */
+ public function getData()
+ {
+ $output = ob_get_clean();
+ $output = json_decode($output);
+
+ if(isset($output->data)) $output->data = json_decode($output->data);
+
+ return $output;
+ }
+
+ /**
+ * 添加$_POST全局变量.
+ * Add data to $_POST.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @access public
+ * @return void
+ */
+ public function setPost($key, $value)
+ {
+ $_POST[$key] = $value;
+ }
+
+ /**
+ * 批量添加$_POST全局变量.
+ * Batch set data to $_POST.
+ *
+ * @param string $fields
+ * @param mixed $object
+ * @access public
+ * @return void
+ */
+ public function batchSetPost($fields, $object = '')
+ {
+ $fields = explode(',', $fields);
+ foreach($fields as $field)
+ {
+ /*
+ * If the field exists in request body, use it.
+ * Otherwise set default value from $object.
+ */
+ if(isset($this->requestBody->$field))
+ {
+ $value = $this->requestBody->$field;
+ }
+ else
+ {
+ if(!$object or !isset($object->$field)) continue;
+ $value = $object->$field;
+ }
+
+ $this->setPost($field, $value);
+ }
+ }
+
+ /**
+ * 确保字段不能为空.
+ * Make sure the fields is not empty.
+ *
+ * @param string $fields
+ * @param mixed $object
+ * @access public
+ * @return void
+ */
+ public function requireFields($fields)
+ {
+ $fields = explode(',', $fields);
+ foreach($fields as $field)
+ {
+ if(!isset($_POST[$field]))
+ {
+ $module = $this->app->moduleName;
+ $name = isset($this->app->lang->$module->$field) ? $this->app->lang->$module->$field : $field;
+ $this->sendError(400, sprintf($this->app->lang->error->notempty, $name));
+ }
+ }
+ }
+}
diff --git a/framework/api/router.class.php b/framework/api/router.class.php
new file mode 100644
index 0000000000..9c6f96d773
--- /dev/null
+++ b/framework/api/router.class.php
@@ -0,0 +1,230 @@
+httpMethod = strtolower($_SERVER['REQUEST_METHOD']);
+
+ if(!empty($_SERVER['PATH_INFO']))
+ {
+ $this->path = rtrim($_SERVER['PATH_INFO'], '/');
+ }
+ else
+ {
+ $this->path = rtrim((strpos($_SERVER['REQUEST_URI'], '?') > 0 ? strstr($_SERVER['REQUEST_URI'], '?', true) : $_SERVER['REQUEST_URI']), '/');
+ }
+
+ $dir = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/');
+ if($dir != '') $this->path = substr($this->path, strlen($dir));
+
+ $subPos = strpos($this->path, '/', 1);
+
+ $this->version = substr($this->path, 1, $subPos-1);
+ $this->path = substr($this->path, $subPos);
+ }
+
+ /**
+ * 解析请求路径,找到处理方法
+ *
+ * Parse request path, find entry and action.
+ *
+ * @param array $routes
+ * @access private
+ * @return void
+ */
+ public function route($routes)
+ {
+ foreach($routes as $route => $target)
+ {
+ $patternAsRegex = preg_replace_callback(
+ '#:([\w]+)\+?#',
+ array($this, 'matchesCallback'),
+ str_replace(')', ')?', $route)
+ );
+ if(substr($route, -1) === '/') $patternAsRegex .= '?';
+
+ /* Cache URL params' names and values if this route matches the current HTTP request. */
+ if(!preg_match('#^' . $patternAsRegex . '$#', $this->path, $paramValues)) continue;
+
+ /* Set module and action */
+ $this->entry = $target;
+ $this->action = strtolower($_SERVER['REQUEST_METHOD']);
+
+ /* Set params */
+ foreach($this->paramNames as $name)
+ {
+ if(!isset($paramValues[$name])) continue;
+
+ if(isset($this->paramNamesPath[$name]))
+ {
+ $this->params[$name] = explode('/', urldecode($paramValues[$name]));
+ }
+ else
+ {
+ $this->params[$name] = urldecode($paramValues[$name]);
+ }
+ }
+ return;
+ }
+
+ $this->entry = 'error';
+ $this->action = 'notFound';
+ }
+
+ /**
+ * 将路由路径参数转化为正则
+ *
+ * Parse params of route to regular expression.
+ *
+ * @param string $param
+ * @access protected
+ * @return string
+ */
+ protected function matchesCallback($m)
+ {
+ $this->paramNames[] = $m[1];
+ return '(?P<' . $m[1] . '>[^/]+)';
+ }
+
+ /**
+ * 解析访问请求
+ *
+ * Parse request.
+ *
+ * @access public
+ * @return void
+ */
+ public function parseRequest()
+ {
+ /* If version of api don't exists, call parent method. */
+ if(!$this->version) return parent::parseRequest();
+
+ $this->route($this->config->routes);
+ }
+
+ /**
+ * 执行对应模块
+ *
+ * Load the running module.
+ *
+ * @access public
+ * @return void
+ */
+ public function loadModule()
+ {
+ /* If the version of api don't exists, call parent method. */
+ if(!$this->version) return parent::loadModule();
+
+ $entry = strtolower($this->entry);
+ include($this->appRoot . "api/$this->version/entries/$entry.php");
+
+ $entryName = $this->entry . 'Entry';
+ $entry = new $entryName();
+ call_user_func_array(array($entry, $this->action), $this->params);
+ }
+
+ /**
+ * 格式化旧版本API响应数据
+ *
+ * Format old version data.
+ *
+ * @param string
+ * @access public
+ * @return string
+ */
+ public function formatData($output)
+ {
+ /* If the version exists, return output directly. */
+ if($this->version) return $output;
+
+ $output = json_decode($output);
+
+ $data = new stdClass();
+ $data->status = isset($output->status) ? $output->status : $output->result;
+ if(isset($output->message)) $data->message = $output->message;
+ if(isset($output->data)) $data->data = json_decode($output->data);
+ $output = json_encode($data);
+
+ unset($_SESSION['ENTRY_CODE']);
+ unset($_SESSION['VALID_ENTRY']);
+
+ return $output;
+ }
+}
diff --git a/framework/base/control.class.php b/framework/base/control.class.php
index fb9a960bc3..f0d4a82df5 100644
--- a/framework/base/control.class.php
+++ b/framework/base/control.class.php
@@ -1,4 +1,5 @@
-viewType == 'json')
{
print(json_encode($data));
- die(helper::removeUTF8Bom(ob_get_clean()));
+ $response = helper::removeUTF8Bom(ob_get_clean());
+
+ if(defined('RUN_MODE') and RUN_MODE == 'api') return print($response);
+
+ die($response);
}
/**
diff --git a/framework/base/router.class.php b/framework/base/router.class.php
index ad4e48f97d..b79e0f036b 100644
--- a/framework/base/router.class.php
+++ b/framework/base/router.class.php
@@ -851,15 +851,15 @@ class baseRouter
{
if(!defined('SESSION_STARTED'))
{
+ /* If request header has token, use it as session for authentication. */
+ $this->sessionID = isset($_SERVER['HTTP_TOKEN']) ? session_id($_SERVER['HTTP_TOKEN']) : session_id();
+
$sessionName = $this->config->sessionVar;
session_name($sessionName);
session_set_cookie_params(0, $this->config->webRoot, '', $this->config->cookieSecure, true);
if($this->config->customSession) session_save_path($this->getTmpRoot() . 'session');
session_start();
- /* If request header has token, use it as session for authentication. */
- $this->sessionID = isset($_SERVER['HTTP_TOKEN']) ? session_id($_SERVER['HTTP_TOKEN']) : session_id();
-
if(isset($_GET[$this->config->sessionVar])) helper::restartSession($_GET[$this->config->sessionVar]);
define('SESSION_STARTED', true);
diff --git a/lib/scm/gitlab.class.php b/lib/scm/gitlab.class.php
index 1ea27285f7..341cfa2442 100644
--- a/lib/scm/gitlab.class.php
+++ b/lib/scm/gitlab.class.php
@@ -517,7 +517,7 @@ class gitlab
if(!scm::checkRevision($version)) return array();
$api = "commits";
- if(empty($count)) $count = 100;
+ if(empty($count)) $count = 1;
$params = array();
$params['ref_name'] = $branch;
$params['per_page'] = $count;
@@ -610,31 +610,32 @@ class gitlab
public function getFilesByCommit($revision)
{
if(!scm::checkRevision($revision)) return array();
- $api = "commits/{$revision}/diff";
- $files = array();
+ $api = "commits/{$revision}/diff";
+ $params = new stdclass;
+ $params->page = 1;
+ $params->per_page = 100;
- $params = array();
- $params['per_page'] = '100';
- for($page = 1; true; $page ++)
+ $allResults = array();
+ while(true)
{
- $params['page'] = $page;
- $allResults = $this->fetch($api, $params);
- if(empty($allResults)) break;
+ $results = $this->fetch($api, $params);
+ $params->page ++;
+ $allResults = $allResults + $results;
+ if(count($results) < 100) break;
+ }
- foreach($allResults as $row)
- {
- $file = new stdclass();
- $file->revision = $revision;
- $file->path = '/' . $row->new_path;
- $file->type = 'file';
+ foreach($allResults as $row)
+ {
+ $file = new stdclass();
+ $file->revision = $revision;
+ $file->path = '/' . $row->new_path;
+ $file->type = 'file';
- $file->action = 'M';
- if($row->new_file) $file->action = 'A';
- if($row->renamed_file) $file->action = 'R';
- if($row->deleted_file) $file->action = 'D';
- $files[] = $file;
- }
- if(count($allResults) < $params['per_page']) break;
+ $file->action = 'M';
+ if($row->new_file) $file->action = 'A';
+ if($row->renamed_file) $file->action = 'R';
+ if($row->deleted_file) $file->action = 'D';
+ $files[] = $file;
}
return $files;
diff --git a/module/bug/config.php b/module/bug/config.php
index ab1efb6707..bc508859ab 100644
--- a/module/bug/config.php
+++ b/module/bug/config.php
@@ -2,6 +2,7 @@
$config->bug = new stdClass();
$config->bug->batchCreate = 10;
$config->bug->longlife = 7;
+$config->bug->removeFields= 'objectTypeList,productList,executionList,gitlabID,gitlabProjectID';
$config->bug->create = new stdclass();
$config->bug->edit = new stdclass();
diff --git a/module/bug/control.php b/module/bug/control.php
index 04d34e3611..1c3b20fd13 100644
--- a/module/bug/control.php
+++ b/module/bug/control.php
@@ -557,6 +557,13 @@ class bug extends control
$this->view->customFields = $customFields;
$this->view->showFields = $this->config->bug->custom->createFields;
+ /* Set gitlabProjects. */
+ $allGitlabs = $this->loadModel('gitlab')->getPairs();
+ $gitlabProjects = $this->loadModel('gitlab')->getProjectsByExecution($executionID);
+ foreach($allGitlabs as $id => $name) if($id and !isset($gitlabProjects[$id])) unset($allGitlabs[$id]);
+ $this->view->gitlabList = $allGitlabs;
+ $this->view->gitlabProjects = $gitlabProjects;
+
$this->view->title = $this->products[$productID] . $this->lang->colon . $this->lang->bug->create;
$this->view->position[] = html::a($this->createLink('bug', 'browse', "productID=$productID"), $this->products[$productID]);
$this->view->position[] = $this->lang->bug->create;
@@ -1457,6 +1464,13 @@ class bug extends control
$bugs = $this->bug->getByList($bugIDList);
foreach($bugs as $bugID => $bug)
{
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if(!empty($relation))
+ {
+ $currentIssue = $this->loadModel('gitlab')->apiGetSingleIssue($relation->gitlabID, $relation->projectID, $relation->issueID);
+ if($currentIssue->state != 'closed') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug);
+ }
+
if($bug->status != 'resolved')
{
if($bug->status != 'closed') $skipBugs[$bugID] = $bugID;
@@ -1540,6 +1554,10 @@ class bug extends control
}
else
{
+ /* Delete related issue in gitlab. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if(!empty($relation)) $this->loadModel('gitlab')->deleteIssue('bug', $bugID, $relation->issueID);
+
$this->bug->delete(TABLE_BUG, $bugID);
if($bug->toTask != 0)
{
diff --git a/module/bug/js/create.js b/module/bug/js/create.js
index c9cdc7d5ec..831178a860 100644
--- a/module/bug/js/create.js
+++ b/module/bug/js/create.js
@@ -168,3 +168,23 @@ $(function()
$(window).unload(function(){
if(blockID) window.parent.refreshBlock($('#block' + blockID));
});
+
+$(document).ready(function()
+{
+ $('#gitlab').change(function()
+ {
+ host = $('#gitlab').val();
+ if(host == '') return false;
+ projects = '';
+ $.each(gitlabProjects[host], function(id, obj){projects = projects + ',' + obj.gitlabProject});
+ url = createLink('repo', 'ajaxgetgitlabprojects', "host=" + host + "&projects=" + projects);
+
+ $.get(url, function(response)
+ {
+ $('#gitlabProject').html('').append(response);
+ $('#gitlabProject').chosen().trigger("chosen:updated");;
+ });
+
+ });
+
+});
diff --git a/module/bug/lang/zh-cn.php b/module/bug/lang/zh-cn.php
index 65fc2086d9..a17aa251ad 100644
--- a/module/bug/lang/zh-cn.php
+++ b/module/bug/lang/zh-cn.php
@@ -24,6 +24,7 @@ $lang->bug->storyVersion = "{$lang->SRCommon}版本";
$lang->bug->color = '标题颜色';
$lang->bug->task = '相关任务';
$lang->bug->title = 'Bug标题';
+$lang->bug->sync2gitlab = '同步gitlab';
$lang->bug->severity = '严重程度';
$lang->bug->severityAB = '级别';
$lang->bug->pri = '优先级';
diff --git a/module/bug/model.php b/module/bug/model.php
index a310b1cb7f..5c26f44494 100644
--- a/module/bug/model.php
+++ b/module/bug/model.php
@@ -84,10 +84,13 @@ class bugModel extends model
/* Use classic mode to replace required project. */
if($this->config->systemMode == 'classic' and strpos($this->config->bug->create->requiredFields, 'project') !== false) $this->config->bug->create->requiredFields = str_replace('project', 'execution', $this->config->bug->create->requiredFields);
- $this->dao->insert(TABLE_BUG)->data($bug)->autoCheck()->batchCheck($this->config->bug->create->requiredFields, 'notempty')->exec();
+ $this->dao->insert(TABLE_BUG)->data($bug, $skip = 'gitlab,gitlabProject')->autoCheck()->batchCheck($this->config->bug->create->requiredFields, 'notempty')->exec();
if(!dao::isError())
{
$bugID = $this->dao->lastInsertID();
+
+ $this->loadModel('gitlab')->apiCreateIssue($this->post->gitlab, $this->post->gitlabProject, 'bug', $bugID, $bug);
+
$this->file->updateObjectID($this->post->uid, $bugID, 'bug');
$this->file->saveUpload('bug', $bugID);
empty($bug->case) ? $this->loadModel('score')->create('bug', 'create', $bugID) : $this->loadModel('score')->create('bug', 'createFormCase', $bug->case);
@@ -273,6 +276,32 @@ class bugModel extends model
return $actions;
}
+ /**
+ * Create bug from gitlab issue.
+ *
+ * @param object $bug
+ * @param int $executionID
+ * @access public
+ * @return int
+ */
+ public function createBugFromGitlabIssue($bug, $executionID)
+ {
+ $bug->openedBy = $this->app->user->account;
+ $bug->openedDate = helper::now(); // TODO(dingguodong) use from issue->created_at ?
+ $bug->assignedDate = isset($bug->assignedTo) ? helper::now() : 0;
+ $bug->openedBuild = 1;
+ $bug->story = 0;
+ $bug->task = 0;
+ $bug->pri = 3;
+ $bug->severity = 3;
+ $bug->project = $this->dao->select('parent')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch('parent');
+
+ $this->dao->insert(TABLE_BUG)->data($bug, $skip = 'gitlab,gitlabProject')->autoCheck()->batchCheck($this->config->bug->create->requiredFields, 'notempty')->exec();
+ if(!dao::isError()) return $this->dao->lastInsertID();
+
+ return false;
+ }
+
/**
* Get bugs.
*
@@ -645,6 +674,12 @@ class bugModel extends model
if(!empty($bug->resolvedBy)) $this->loadModel('score')->create('bug', 'resolve', $bugID);
$this->file->updateObjectID($this->post->uid, $bugID, 'bug');
+
+ if(!empty($bug))
+ {
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug, $bugID);
+ }
return common::createChanges($oldBug, $bug);
}
}
@@ -766,6 +801,10 @@ class bugModel extends model
$this->executeHooks($bugID);
$allChanges[$bugID] = common::createChanges($oldBug, $bug);
+
+ /* update bug to gitlab issue. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug, $bugID);
}
else
{
@@ -820,6 +859,12 @@ class bugModel extends model
/* Update bugs. */
foreach($activateBugs as $bugID => $bug)
{
+ if(!empty($bug))
+ {
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', (Object)$bug, $bugID);
+ }
+
$oldBug = $bugs[$bugID];
$this->dao->update(TABLE_BUG)->data($bug, $skipFields = 'comment')->autoCheck()->where('id')->eq((int)$bugID)->exec();
if(dao::isError()) die(js::error('bug#' . $bugID . dao::getError(true)));
@@ -854,6 +899,15 @@ class bugModel extends model
->autoCheck()
->where('id')->eq($bugID)->exec();
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ $bug = $this->getById($bugID); // get full bug object to update issue.
+ $bug->assignee_id = $this->loadModel('gitlab')->getGitlabUserID($relation->gitlabID, $bug->assignedTo);
+ if($bug->assignee_id != '')
+ {
+ // TODO(dingguodong) we should alert to operator when can not find the user, and the operator should reconfigure user binding.
+ $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug, $bugID);
+ }
+
if(!dao::isError()) return common::createChanges($oldBug, $bug);
}
@@ -908,6 +962,12 @@ class bugModel extends model
$bug->lastEditedDate = $now;
$bug->confirmed = 1;
+ if(!empty($bug))
+ {
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug, $bugID);
+ }
+
$this->dao->update(TABLE_BUG)->data($bug)->where('id')->eq($bugID)->exec();
$this->executeHooks($bugID);
}
@@ -991,6 +1051,7 @@ class bugModel extends model
->checkIF($bug->resolution == 'fixed', 'resolvedBuild','notempty')
->where('id')->eq((int)$bugID)
->exec();
+
if(!dao::isError())
{
$this->loadModel('score')->create('bug', 'resolve', $oldBug);
@@ -998,6 +1059,9 @@ class bugModel extends model
/* Link bug to build and release. */
$this->linkBugToBuild($bugID, $bug->resolvedBuild);
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug, $bugID);
+
return common::createChanges($oldBug, $bug);
}
@@ -1127,6 +1191,10 @@ class bugModel extends model
$this->dao->update(TABLE_BUG)->data($bug)->where('id')->eq($bugID)->exec();
$this->executeHooks($bugID);
+ /* batch resolve issure bugs.*/
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug, $bugID);
+
$changes[$bugID] = common::createChanges($oldBug, $bug);
}
@@ -1178,11 +1246,18 @@ class bugModel extends model
foreach($openedBuilds as $openedBuild)
{
$build = $this->build->getByID($openedBuild);
+ if(empty($build)) continue;
$build->bugs = trim(str_replace(",$bugID,", ',', ",$build->bugs,"), ',');
$this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq((int)$openedBuild)->exec();
}
}
+ if(!empty($bug))
+ {
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug, $bugID);
+ }
+
$bug->activatedCount += 1;
return common::createChanges($oldBug, $bug);
}
@@ -1212,6 +1287,12 @@ class bugModel extends model
$this->dao->update(TABLE_BUG)->data($bug)->autoCheck()->where('id')->eq((int)$bugID)->exec();
+ $relation = $this->loadModel('gitlab')->getRelationByObject('bug', $bugID);
+ if(!empty($relation))
+ {
+ $currentIssue = $this->loadModel('gitlab')->apiGetSingleIssue($relation->gitlabID, $relation->projectID, $relation->issueID);
+ if($currentIssue->state != 'closed') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $bug, $bugID);
+ }
return common::createChanges($oldBug, $bug);
}
diff --git a/module/bug/view/create.html.php b/module/bug/view/create.html.php
index a742898300..951d25b4fb 100644
--- a/module/bug/view/create.html.php
+++ b/module/bug/view/create.html.php
@@ -26,6 +26,7 @@ js::set('isStepsTemplate', $isStepsTemplate);
js::set('oldProjectID', $projectID);
js::set('blockID', $blockID);
js::set('moduleID', $moduleID);
+js::set('gitlabProjects', $gitlabProjects);
?>
@@ -312,6 +313,13 @@ js::set('moduleID', $moduleID);
+
+
+ | task->sync2Gitlab;?> |
+ |
+ |
+
+
| bug->status;?> |
|
diff --git a/module/ci/control.php b/module/ci/control.php
index 9e79c040cd..dd9a637b0c 100644
--- a/module/ci/control.php
+++ b/module/ci/control.php
@@ -104,7 +104,7 @@ class ci extends control
{
$compile = $this->dao->select('t1.*, t2.jkJob,t2.product,t2.frame,t3.name as jenkinsName,t3.url,t3.account,t3.token,t3.password')->from(TABLE_COMPILE)->alias('t1')
->leftJoin(TABLE_JOB)->alias('t2')->on('t1.job=t2.id')
- ->leftJoin(TABLE_JENKINS)->alias('t3')->on('t2.jkHost=t3.id')
+ ->leftJoin(TABLE_PIPELINE)->alias('t3')->on('t2.jkHost=t3.id')
->where('t1.id')->eq($compileID)
->fetch();
diff --git a/module/ci/model.php b/module/ci/model.php
index a3ae0144c0..81c1fffece 100644
--- a/module/ci/model.php
+++ b/module/ci/model.php
@@ -31,7 +31,7 @@ class ciModel extends model
$compiles = $this->dao->select('t1.*, t2.jkJob, t3.name as jenkinsName,t3.url,t3.account,t3.token,t3.password')
->from(TABLE_COMPILE)->alias('t1')
->leftJoin(TABLE_JOB)->alias('t2')->on('t1.job=t2.id')
- ->leftJoin(TABLE_JENKINS)->alias('t3')->on('t2.jkHost=t3.id')
+ ->leftJoin(TABLE_PIPELINE)->alias('t3')->on('t2.jkHost=t3.id')
->where('t1.status')->ne('success')
->andWhere('t1.status')->ne('failure')
->andWhere('t1.status')->ne('create_fail')
diff --git a/module/common/lang/common.php b/module/common/lang/common.php
index 00f346a769..20e9eebdcf 100644
--- a/module/common/lang/common.php
+++ b/module/common/lang/common.php
@@ -38,6 +38,7 @@ $lang->user = new stdclass();
$lang->report = new stdclass();
$lang->repo = new stdclass();
$lang->jenkins = new stdclass();
+$lang->gitlab = new stdclass();
$lang->compile = new stdclass();
$lang->job = new stdclass();
$lang->svn = new stdclass();
@@ -150,4 +151,4 @@ $lang->icons['unlock'] = 'unlock-alt';
$lang->icons['confirmStoryChange'] = 'search';
$lang->icons['score'] = 'tint';
-$lang->noMenuModule = array('report', 'my', 'todo', 'effort', 'program', 'product', 'execution', 'task', 'build', 'productplan', 'project', 'projectrelease', 'projectstory', 'story', 'branch', 'release', 'attend', 'leave', 'makeup', 'overtime', 'lieu', 'custom', 'admin', 'mail', 'extension', 'dev', 'backup', 'action', 'cron', 'pssp', 'sms', 'message', 'webhook', 'search', 'score', 'stage', 'entry', 'jenkins');
+$lang->noMenuModule = array('report', 'my', 'todo', 'effort', 'program', 'product', 'execution', 'task', 'build', 'productplan', 'project', 'projectrelease', 'projectstory', 'story', 'branch', 'release', 'attend', 'leave', 'makeup', 'overtime', 'lieu', 'custom', 'admin', 'mail', 'extension', 'dev', 'backup', 'action', 'cron', 'pssp', 'sms', 'message', 'webhook', 'search', 'score', 'stage', 'entry', 'jenkins','gitlab');
diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php
index 1cdbc98c1e..61def452eb 100644
--- a/module/common/lang/menu.php
+++ b/module/common/lang/menu.php
@@ -319,12 +319,14 @@ $lang->devops->menu = new stdclass();
$lang->devops->menu->code = array('link' => "{$lang->repo->common}|repo|browse|repoID=%s", 'alias' => 'diff,view,revision,log,blame,showsynccomment');
$lang->devops->menu->compile = array('link' => "{$lang->devops->compile}|job|browse", 'subModule' => 'compile,job');
$lang->devops->menu->jenkins = array('link' => "Jenkins|jenkins|browse", 'alias' => 'create,edit');
+$lang->devops->menu->gitlab = array('link' => "Gitlab|gitlab|browse", 'alias' => 'create,edit');
$lang->devops->menu->maintain = array('link' => "{$lang->devops->repo}|repo|maintain", 'alias' => 'create,edit');
$lang->devops->menu->rules = array('link' => "{$lang->devops->rules}|repo|setrules");
$lang->devops->menuOrder[5] = 'code';
$lang->devops->menuOrder[10] = 'compile';
$lang->devops->menuOrder[15] = 'jenkins';
+$lang->devops->menuOrder[14] = 'gitlab';
$lang->devops->menuOrder[20] = 'maintain';
$lang->devops->menuOrder[25] = 'rules';
@@ -521,6 +523,7 @@ $lang->navGroup->devops = 'devops';
$lang->navGroup->repo = 'devops';
$lang->navGroup->job = 'devops';
$lang->navGroup->jenkins = 'devops';
+$lang->navGroup->gitlab = 'devops';
$lang->navGroup->compile = 'devops';
$lang->navGroup->ci = 'devops';
$lang->navGroup->svn = 'devops';
diff --git a/module/common/model.php b/module/common/model.php
index 99671fb000..1a0168e12c 100644
--- a/module/common/model.php
+++ b/module/common/model.php
@@ -178,23 +178,9 @@ class commonModel extends model
*/
public function isOpenMethod($module, $method)
{
- if($module == 'upgrade' and $method == 'ajaxupdatefile') return true;
- if($module == 'user' and strpos('login|logout|deny|reset|refreshrandom', $method) !== false) return true;
- if($module == 'api' and $method == 'getsessionid') return true;
- if($module == 'misc' and $method == 'checktable') return true;
- if($module == 'misc' and $method == 'qrcode') return true;
- if($module == 'misc' and $method == 'about') return true;
- if($module == 'misc' and $method == 'checkupdate') return true;
- if($module == 'misc' and $method == 'ping') return true;
- if($module == 'misc' and $method == 'captcha') return true;
- if($module == 'sso' and $method == 'login') return true;
- if($module == 'sso' and $method == 'logout') return true;
- if($module == 'sso' and $method == 'bind') return true;
- if($module == 'sso' and $method == 'gettodolist') return true;
+ if(in_array("$module.$method", $this->config->openMethods)) return true;
+
if($module == 'block' and $method == 'main' and isset($_GET['hash'])) return true;
- if($module == 'file' and $method == 'read') return true;
- if($module == 'index' and $method == 'changelog') return true;
- if($module == 'my' and $method == 'preference') return true;
if($this->loadModel('user')->isLogon() or ($this->app->company->guest and $this->app->user->account == 'guest'))
{
@@ -2038,6 +2024,8 @@ EOD;
*/
public function checkEntry()
{
+ if($this->isOpenMethod($_GET[$this->config->moduleVar], $_GET[$this->config->methodVar])) return true;
+
$this->loadModel('entry');
if($this->session->valid_entry)
{
@@ -2050,10 +2038,10 @@ EOD;
if(!$this->get->token) $this->response('PARAM_TOKEN_MISSING');
$entry = $this->entry->getByCode($this->get->code);
- if(!$entry) $this->response('EMPTY_ENTRY');
- if(!$entry->key) $this->response('EMPTY_KEY');
- if(!$this->checkIP($entry->ip)) $this->response('IP_DENIED');
- if(!$this->checkEntryToken($entry)) $this->response('INVALID_TOKEN');
+ if(!$entry) $this->response('EMPTY_ENTRY');
+ if(!$entry->key) $this->response('EMPTY_KEY');
+ if(!$this->checkIP($entry->ip)) $this->response('IP_DENIED');
+ if(!$this->checkEntryToken($entry)) $this->response('INVALID_TOKEN');
if($entry->freePasswd == 0 and empty($entry->account)) $this->response('ACCOUNT_UNBOUND');
$isFreepasswd = ($_GET['m'] == 'user' and strtolower($_GET['f']) == 'apilogin' and $_GET['account'] and $entry->freePasswd);
@@ -2253,14 +2241,15 @@ EOD;
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);
+
curl_close($curl);
$logFile = $app->getLogRoot() . 'saas.'. date('Ymd') . '.log.php';
diff --git a/module/compile/model.php b/module/compile/model.php
index 7f82a344ce..f9f1646335 100644
--- a/module/compile/model.php
+++ b/module/compile/model.php
@@ -37,7 +37,7 @@ class compileModel extends model
return $this->dao->select('t1.id, t1.name, t1.job, t1.status, t1.createdDate,t1.testtask, t2.jkJob,t2.triggerType,t2.comment,t2.atDay,t2.atTime, t3.name as repoName, t4.name as jenkinsName')->from(TABLE_COMPILE)->alias('t1')
->leftJoin(TABLE_JOB)->alias('t2')->on('t1.job=t2.id')
->leftJoin(TABLE_REPO)->alias('t3')->on('t2.repo=t3.id')
- ->leftJoin(TABLE_JENKINS)->alias('t4')->on('t2.jkHost=t4.id')
+ ->leftJoin(TABLE_PIPELINE)->alias('t4')->on('t2.jkHost=t4.id')
->where('t1.deleted')->eq('0')
->andWhere('t1.job')->ne('0')
->beginIF(!empty($jobID))->andWhere('t1.job')->eq($jobID)->fi()
@@ -123,7 +123,7 @@ class compileModel extends model
{
$job = $this->dao->select('t1.id,t1.name,t1.repo,t1.jkJob,t2.name as jenkinsName,t2.url,t2.account,t2.token,t2.password')
->from(TABLE_JOB)->alias('t1')
- ->leftJoin(TABLE_JENKINS)->alias('t2')->on('t1.jkHost=t2.id')
+ ->leftJoin(TABLE_PIPELINE)->alias('t2')->on('t1.jkHost=t2.id')
->where('t1.id')->eq($compile->job)
->fetch();
diff --git a/module/execution/control.php b/module/execution/control.php
index 9a118778d1..5f96a4150d 100644
--- a/module/execution/control.php
+++ b/module/execution/control.php
@@ -2510,6 +2510,10 @@ class execution extends control
$_POST = array();
foreach($storyIdList as $storyID)
{
+ /* Delete related issue in gitlab. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ if(!empty($relation)) $this->loadModel('gitlab')->deleteIssue('story', $storyID, $relation->issueID);
+
$this->execution->unlinkStory($executionID, $storyID);
}
}
diff --git a/module/execution/model.php b/module/execution/model.php
index b1ea240b45..b43c8b03c3 100644
--- a/module/execution/model.php
+++ b/module/execution/model.php
@@ -1569,6 +1569,7 @@ class executionModel extends model
* Get products of a execution.
*
* @param int $executionID
+ * @param bool $withBranch
* @access public
* @return array
*/
diff --git a/module/gitlab/config.php b/module/gitlab/config.php
new file mode 100644
index 0000000000..84ee284a8d
--- /dev/null
+++ b/module/gitlab/config.php
@@ -0,0 +1,104 @@
+gitlab->create = new stdclass;
+$config->gitlab->create->requiredFields = 'name,url,token';
+
+$config->gitlab->edit = new stdclass;
+$config->gitlab->edit->requiredFields = 'name,url,token';
+
+$config->gitlab->labelPattern = new stdclass;
+$config->gitlab->labelPattern->task = '/^zentao_task\/\d+$/';
+$config->gitlab->labelPattern->bug = '/^zentao_bug\/\d+$/';
+$config->gitlab->labelPattern->story = '/^zentao_story\/\d+$/';
+
+$config->gitlab->actions = array();
+$config->gitlab->actions['issue'] = array();
+
+$config->gitlab->zentaoObjectLabel = new stdclass;
+$config->gitlab->zentaoObjectLabel->name = "zentao_%s/%s";
+$config->gitlab->zentaoObjectLabel->description = "%s";
+
+$config->gitlab->zentaoObjectLabel->color = new stdclass;
+$config->gitlab->zentaoObjectLabel->color->task = '#0033CC';
+$config->gitlab->zentaoObjectLabel->color->story = '#69D100';
+$config->gitlab->zentaoObjectLabel->color->bug = '#D10069';
+$config->gitlab->zentaoObjectLabel->priority = "0";
+
+$config->gitlab->webhookURL = "%s/api.php?m=gitlab&f=webhook&product=%s&gitlab=%s";
+
+$config->gitlab->skippedFields = new stdclass;
+$config->gitlab->skippedFields->issueCreate = array();
+$config->gitlab->skippedFields->issueCreate['story'] = array();
+$config->gitlab->skippedFields->issueCreate['task'] = array();
+$config->gitlab->skippedFields->issueCreate['bug'] = array();
+
+$config->gitlab->maps = new stdclass;
+$config->gitlab->maps->task = array();
+$config->gitlab->maps->task['name'] = 'title|field|';
+$config->gitlab->maps->task['desc'] = 'description|field|';
+$config->gitlab->maps->task['openedDate'] = 'created_at|field|datetime';
+$config->gitlab->maps->task['assignedTo'] = 'assignee_id|userPairs|';
+$config->gitlab->maps->task['lastEditedDate'] = 'updated_at|field|datetime';
+$config->gitlab->maps->task['deadline'] = 'due_date|field|date';
+$config->gitlab->maps->task['status'] = 'state|configItems|taskStateMap';
+$config->gitlab->maps->task['pri'] = 'weight|configItems|taskWeightMap';
+$config->gitlab->maps->task['lastEditedBy'] = 'updated_by_id|userPairs|';
+
+$config->gitlab->maps->story = array();
+$config->gitlab->maps->story['title'] = 'title|field|';
+$config->gitlab->maps->story['spec'] = 'description|fields|verify';
+$config->gitlab->maps->story['openedDate'] = 'created_at|field|datetime';
+$config->gitlab->maps->story['assignedTo'] = 'assignee_id|userPairs|';
+$config->gitlab->maps->story['status'] = 'state|configItems|storyStateMap';
+$config->gitlab->maps->story['pri'] = 'weight|configItems|storyWeightMap';
+
+$config->gitlab->maps->bug = array();
+$config->gitlab->maps->bug['title'] = 'title|field|';
+$config->gitlab->maps->bug['steps'] = 'description|field|';
+$config->gitlab->maps->bug['openedDate'] = 'created_at|field|datetime';
+$config->gitlab->maps->bug['deadline'] = 'due_date|field|date';
+$config->gitlab->maps->bug['assignedTo'] = 'assignee_id|userPairs|';
+$config->gitlab->maps->bug['status'] = 'state|configItems|bugStateMap';
+$config->gitlab->maps->bug['pri'] = 'weight|configItems|bugWeightMap';
+
+$config->gitlab->taskWeightMap = array();
+$config->gitlab->taskWeightMap['1'] = '1';
+$config->gitlab->taskWeightMap['2'] = '2';
+$config->gitlab->taskWeightMap['3'] = '3';
+
+$config->gitlab->taskStateMap = array();
+$config->gitlab->taskStateMap['doing'] = 'opened';
+$config->gitlab->taskStateMap['wait'] = 'opened';
+$config->gitlab->taskStateMap['closed'] = 'closed';
+$config->gitlab->taskStateMap['done'] = 'closed';
+$config->gitlab->taskStateMap['cancel'] = 'closed';
+
+$config->gitlab->taskTypesToSync = 'design,devel,request,discuss,ui,affair,misc';
+
+$config->gitlab->storyWeightMap = array();
+$config->gitlab->storyWeightMap['1'] = '1';
+$config->gitlab->storyWeightMap['2'] = '2';
+$config->gitlab->storyWeightMap['3'] = '3';
+
+$config->gitlab->storyStateMap = array();
+$config->gitlab->storyStateMap['active'] = 'opened';
+$config->gitlab->storyStateMap['resolved'] = 'closed';
+$config->gitlab->storyStateMap['closed'] = 'closed';
+
+$config->gitlab->bugWeightMap = array();
+$config->gitlab->bugWeightMap['1'] = '1';
+$config->gitlab->bugWeightMap['2'] = '2';
+$config->gitlab->bugWeightMap['3'] = '3';
+$config->gitlab->bugWeightMap['4'] = '4';
+
+$config->gitlab->bugStateMap = array();
+$config->gitlab->bugStateMap['active'] = 'opened';
+$config->gitlab->bugStateMap['resolved'] = 'closed';
+$config->gitlab->bugStateMap['closed'] = 'closed';
+
+$config->gitlab->objectTables = new stdclass;
+$config->gitlab->objectTables->story = TABLE_STORY;
+$config->gitlab->objectTables->task = TABLE_TASK;
+$config->gitlab->objectTables->bug = TABLE_BUG;
+
+$config->gitlab->objectTypes = array('', 'task', 'bug', 'story');
+
diff --git a/module/gitlab/control.php b/module/gitlab/control.php
new file mode 100644
index 0000000000..b8b72c1201
--- /dev/null
+++ b/module/gitlab/control.php
@@ -0,0 +1,373 @@
+
+ * @package product
+ * @version $Id: ${FILE_NAME} 5144 2020/1/8 8:10 下午 chenqi@cnezsoft.com $
+ * @link http://www.zentao.net
+ */
+class gitlab extends control
+{
+ /**
+ * Browse gitlab.
+ *
+ * @param string $orderBy
+ * @param int $recTotal
+ * @param int $recPerPage
+ * @param int $pageID
+ * @access public
+ * @return void
+ */
+ public function browse($orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
+ {
+ $this->app->loadClass('pager', $static = true);
+ $pager = new pager($recTotal, $recPerPage, $pageID);
+
+ $this->view->position[] = $this->lang->gitlab->common;
+ $this->view->position[] = $this->lang->gitlab->browse;
+
+ $this->view->title = $this->lang->gitlab->common . $this->lang->colon . $this->lang->gitlab->browse;
+ $this->view->gitlabList = $this->gitlab->getList($orderBy, $pager);
+ $this->view->orderBy = $orderBy;
+ $this->view->pager = $pager;
+
+ $this->display();
+ }
+
+ /**
+ * Create a gitlab.
+ *
+ * @access public
+ * @return void
+ */
+ public function create()
+ {
+ if($_POST)
+ {
+ $this->checkToken();
+ $gitlabID = $this->gitlab->create();
+
+ if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError()));
+ $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('browse')));
+ }
+
+ $this->view->title = $this->lang->gitlab->common . $this->lang->colon . $this->lang->gitlab->create;
+
+ $this->view->position[] = html::a(inlink('browse'), $this->lang->gitlab->common);
+ $this->view->position[] = $this->lang->gitlab->create;
+
+ $this->display();
+ }
+
+ /**
+ * Edit a gitlab.
+ *
+ * @param int $id
+ * @access public
+ * @return void
+ */
+ public function edit($id)
+ {
+ $gitlab = $this->gitlab->getByID($id);
+ if($_POST)
+ {
+ $this->checkToken();
+ $this->gitlab->update($id);
+ if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError()));
+ $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('browse')));
+ }
+
+ $this->view->position[] = html::a(inlink('browse'), $this->lang->gitlab->common);
+ $this->view->position[] = $this->lang->gitlab->edit;
+
+ $this->view->title = $this->lang->gitlab->common . $this->lang->colon . $this->lang->gitlab->edit;
+ $this->view->gitlab = $gitlab;
+
+ $this->display();
+ }
+
+ /**
+ * Bind gitlab user to zentao users.
+ *
+ * @access public
+ * @return void
+ */
+ public function bindUser($gitlabID)
+ {
+ $userPairs = $this->loadModel('user')->getPairs();
+
+ if($_POST)
+ {
+ $users = $this->post->zentaoUsers;
+ $accountList = array();
+ $repeatUsers = array();
+ foreach($users as $openID => $user)
+ {
+ if(empty($user)) continue;
+ if(isset($accountList[$user])) $repeatUsers[] = zget($userPairs, $user);
+ $accountList[$user] = $openID;
+ }
+
+ if(count($repeatUsers)) $this->send(array('result' => 'fail', 'message' => sprintf($this->lang->gitlab->bindUserError, join(',', $repeatUsers))));
+
+ $user = new stdclass;
+ $user->providerID = $gitlabID;
+ $user->providerType = 'gitlab';
+
+ /* Delete binded users and save new relationship.*/
+ $this->dao->delete()->from(TABLE_OAUTH)->where('providerType')->eq($user->providerType)->andWhere('providerID')->eq($user->providerID)->exec();
+ foreach($users as $openID => $account)
+ {
+ if(!$account) continue;
+ $user->account = $account;
+ $user->openID = $openID;
+
+ $this->dao->delete()
+ ->from(TABLE_OAUTH)
+ ->where('openID')->eq($user->openID)
+ ->andWhere('providerType')->eq($user->providerType)
+ ->andWhere('providerID')->eq($user->providerID)
+ ->andWhere('account')->eq($user->account)
+ ->exec();
+
+ $this->dao->insert(TABLE_OAUTH)->data($user)->exec();
+ }
+ $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->server->http_referer));
+ }
+
+ $gitlab = $this->gitlab->getByID($gitlabID);
+ $zentaoUsers = $this->dao->select('account,email,realname')->from(TABLE_USER)->fetchAll('account');
+
+ $this->view->title = $this->lang->gitlab->bindUser;
+ $this->view->userPairs = $userPairs;
+ $this->view->gitlabUsers = $this->gitlab->apiGetUsers($gitlab);
+ $this->view->matchedResult = $this->gitlab->getMatchedUsers($gitlabID, $this->view->gitlabUsers, $zentaoUsers);
+ $this->display();
+ }
+
+ /**
+ * Bind product and gitlab projects.
+ *
+ * @param int $gitlabID
+ * @access public
+ * @return void
+ */
+ public function bindProduct($gitlabID)
+ {
+ $this->view->projectPairs = $this->gitlab->getProjectPairs($gitlabID);
+ $this->view->title = $this->lang->gitlab->bindProduct;
+ $this->display();
+ }
+
+ /**
+ * Delete a gitlab.
+ *
+ * @param int $id
+ * @access public
+ * @return void
+ */
+ public function delete($id, $confim = 'no')
+ {
+ if($confim != 'yes') die(js::confirm($this->lang->gitlab->confirmDelete, inlink('delete', "id=$id&confirm=yes")));
+
+ $this->gitlab->delete(TABLE_PIPELINE, $id);
+ die(js::reload('parent'));
+ }
+
+ /**
+ * Check post token has admin permissions.
+ *
+ * @access public
+ * @return void
+ */
+ public function checkToken()
+ {
+ if(strpos($this->post->url, 'http') !== 0) $this->send(array('result' => 'fail', 'message' => array('url' => array($this->lang->gitlab->hostError))));
+ if(!$this->post->token) $this->send(array('result' => 'fail', 'message' => array('token' => array($this->lang->gitlab->tokenError))));
+
+ $user = $this->gitlab->apiGetCurrentUser($this->post->url, $this->post->token);
+
+ if(!is_object($user)) $this->send(array('result' => 'fail', 'message' => array('url' => array($this->lang->gitlab->hostError))));
+ if(!isset($user->is_admin) or !$user->is_admin) $this->send(array('result' => 'fail', 'message' => array('token' => array($this->lang->gitlab->tokenError))));
+ }
+
+ /**
+ * Webhook api.
+ *
+ * @access public
+ * @return void
+ */
+ public function webhook()
+ {
+ $this->gitlab->webhookCheckToken();
+ $product = $this->get->product;
+ $gitlab = $this->get->gitlab;
+ $project = $this->get->project;
+
+ $input = file_get_contents('php://input');
+ $requestBody = json_decode($input);
+ $result = $this->gitlab->webhookParseBody($requestBody, $gitlab);
+
+ $logFile = $this->app->getLogRoot() . 'webhook.'. 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') . ": " . $this->app->getURI() . "\n");
+ fwrite($fh, "JSON: \n " . $input . "\n");
+ fwrite($fh, "Parsed object: {$result->issue->objectType} :\n " . print_r($result->object, true) . "\n");
+ fclose($fh);
+ }
+
+ if($result->action == 'updateissue' and isset($result->changes->assignees)) $this->gitlab->webhookAssignIssue($result);
+
+ //if($result->action = 'reopenissue') $this->gitlab->webhookIssueReopen($gitlab, $result);
+
+ if($result->action == 'closeissue') $this->gitlab->webhookCloseIssue($result);
+
+ if($result->action == 'updateissue') $this->gitlab->webhookSyncIssue($gitlab, $result);
+
+ $this->view->result = 'success';
+ $this->view->status = 'ok';
+ $this->view->data = $result->object;
+ $this->display();
+ }
+
+ public function test($id = 11)
+ {
+ $gitlabID = 1; $projectID = 7;
+ $task = $this->loadModel('task')->getByID($id);
+ $relations = $this->gitlab->getRelationByObject('task', $task->id);
+
+ a($relations);exit;
+ $issue = $this->gitlab->taskToIssue($gitlabID, $projectID, $task);
+ $issue = $this->gitlab->apiCreateIssue($gitlabID, $projectID, $issue);
+ $this->gitlab->saveIssueRelation('task', $task, $gitlabID, $issue);
+ exit;
+ }
+
+ /**
+ * Import gitlab issue to zentaopms.
+ *
+ * @param int $repoID
+ * @access public
+ * @return void
+ */
+ public function importIssue($repoID)
+ {
+ $repo = $this->loadModel('repo')->getRepoByID($repoID);
+ $productIDList = explode(',', $repo->product);
+ $gitlabID = $repo->gitlab;
+ $projectID = $repo->project;
+
+ if($_POST)
+ {
+ $executionList = $this->post->executionList;
+ $objectTypeList = $this->post->objectTypeList;
+ $productList = $this->post->productList;
+
+ $failedIssues = array();
+ foreach($executionList as $issueID => $executionID)
+ {
+ if($executionID)
+ {
+ $objectType = $this->config->gitlab->objectTypes[$objectTypeList[$issueID]];
+ $issue = $this->gitlab->apiGetSingleIssue($gitlabID, $projectID, $issueID);
+ $issue->objectType = $objectType;
+ $issue->objectID = 0; // meet the required parameters for issueToZentaoObject.
+ if(isset($issue->assignee)) $issue->assignee_id = $issue->assignee->id;
+ $issue->updated_by_id = $issue->author->id; // here can be replaced by current zentao user.
+
+ $object = $this->gitlab->issueToZentaoObject($issue, $gitlabID);
+ $object->product = $productList[$issueID];
+ $object->execution = $executionID;
+ $clonedObject = clone $object;
+
+ if($objectType == 'task') $objectID = $this->loadModel('task')->createTaskFromGitlabIssue($clonedObject, $executionID);
+ if($objectType == 'bug') $objectID = $this->loadModel('bug')->createBugFromGitlabIssue($clonedObject, $executionID);
+ if($objectType == 'story') $objectID = $this->loadModel('story')->createStoryFromGitlabIssue($clonedObject, $executionID);
+
+ if($objectID)
+ {
+ $object->id = $objectID;
+ $this->gitlab->saveImportedIssue($gitlabID, $projectID, $objectType, $objectID, $issue, $object);
+ }
+ else
+ {
+ $failedIssues[] = $issue->iid;
+ }
+ }
+ else
+ {
+ if($productList[$issueID] != 0) $this->send(array('result' => 'fail', 'message' => $this->lang->gitlab->importIssueError, 'locate' => $this->server->http_referer));
+ }
+ }
+
+ if($failedIssues) $this->send(array('result' => 'success', 'message' => $this->lang->gitlab->importIssueWarn, 'locate' => $this->server->http_referer));
+ $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->server->http_referer));
+ }
+
+ $savedIssueIDList = $this->dao->select('BID as issueID')->from(TABLE_RELATION)
+ ->where('relation')->eq('gitlab')
+ ->andWhere('BType')->eq('issue')
+ ->andWhere('BVersion')->eq($projectID)
+ ->andWhere('extra')->eq($gitlabID)
+ ->fetchAll('issueID');
+
+ /* 'not[iids]' option in gitlab API has a issue when iids is too long. */
+ $gitlabIssues = $this->gitlab->apiGetIssues($gitlabID, $projectID, '&state=opened');
+ foreach($gitlabIssues as $index => $issue)
+ {
+ foreach($savedIssueIDList as $savedIssueID)
+ {
+ if($issue->iid == $savedIssueID->issueID)
+ {
+ unset($gitlabIssues[$index]);
+ break;
+ }
+ }
+ }
+
+ $products = array();
+ $products[] = '';
+ foreach($productIDList as $productID)
+ {
+ $products[$productID] = $this->loadModel("product")->getByID($productID)->name;
+ }
+
+ $this->view->importable = empty($gitlabIssues) ? false : true;
+ $this->view->products = $products;
+ $this->view->gitlabID = $gitlabID;
+ $this->view->gitlabProjectID = $projectID;
+ $this->view->objectTypes = $this->config->gitlab->objectTypes;
+
+ $this->view->gitlabIssues = $gitlabIssues;
+ $this->display();
+ }
+
+ /**
+ * Ajax get executions by productID.
+ *
+ * @param int $productID
+ * @access public
+ * @return string
+ */
+ public function ajaxGetExecutionsByProduct($productID)
+ {
+ if(!$productID) $this->send(array('message' => array()));
+
+ $executions = $this->loadModel('product')->getAllExecutionPairsByProduct($productID);
+ $options = "";
+ foreach($executions as $index =>$execution)
+ {
+ $options .= "";
+ }
+ die($options);
+ }
+
+}
diff --git a/module/gitlab/js/importissue.js b/module/gitlab/js/importissue.js
new file mode 100644
index 0000000000..d48d5a6028
--- /dev/null
+++ b/module/gitlab/js/importissue.js
@@ -0,0 +1,18 @@
+$(document).ready(function()
+{
+ $('[name^=productList').change(function()
+ {
+ var execution = $(this);
+ host = execution.val();
+ if(host == '') return false;
+ executions = '';
+ url = createLink('gitlab', 'ajaxGetExecutionsByProduct', "host=" + host);
+
+ $.get(url, function(response)
+ {
+ execution = execution.parent().next().find('select');
+ execution.html('').append(response);
+ execution.chosen().trigger("chosen:updated");
+ });
+ });
+});
diff --git a/module/gitlab/lang/de.php b/module/gitlab/lang/de.php
new file mode 100644
index 0000000000..4e2c99ff52
--- /dev/null
+++ b/module/gitlab/lang/de.php
@@ -0,0 +1,19 @@
+jenkins->common = 'Jenkins';
+$lang->jenkins->browse = 'Browse Jenkins';
+$lang->jenkins->create = 'Create Jenkins';
+$lang->jenkins->edit = 'Edit Jenkins';
+$lang->jenkins->delete = 'Delete';
+$lang->jenkins->confirmDelete = 'Do you want to delete this Jenkins server?';
+
+$lang->jenkins->id = 'ID';
+$lang->jenkins->name = 'Name';
+$lang->jenkins->url = 'Service URL';
+$lang->jenkins->token = 'Token';
+$lang->jenkins->account = 'UserName';
+$lang->jenkins->password = 'Password';
+
+$lang->jenkins->lblCreate = 'Create Jenkins Server';
+$lang->jenkins->desc = 'Description';
+$lang->jenkins->tokenFirst = 'Use token if not empty.';
+$lang->jenkins->tips = 'Cancel "Prevent Cross Site Request Forgery exploits" when using password.';
diff --git a/module/gitlab/lang/en.php b/module/gitlab/lang/en.php
new file mode 100644
index 0000000000..dd8b436308
--- /dev/null
+++ b/module/gitlab/lang/en.php
@@ -0,0 +1,22 @@
+jenkins->common = 'Jenkins';
+$lang->jenkins->browse = 'Jenkins';
+$lang->jenkins->create = 'Create Jenkins';
+$lang->jenkins->edit = 'Edit Jenkins';
+$lang->jenkins->delete = 'Delete';
+$lang->jenkins->confirmDelete = 'Do you want to delete this Jenkins server?';
+
+$lang->jenkins->browseAction = 'Jenkins List';
+$lang->jenkins->deleteAction = 'Delete Jenkins';
+
+$lang->jenkins->id = 'ID';
+$lang->jenkins->name = 'Name';
+$lang->jenkins->url = 'URL';
+$lang->jenkins->token = 'Token';
+$lang->jenkins->account = 'Username';
+$lang->jenkins->password = 'Password';
+
+$lang->jenkins->lblCreate = 'Create Jenkins Server';
+$lang->jenkins->desc = 'Description';
+$lang->jenkins->tokenFirst = 'Use token if not empty.';
+$lang->jenkins->tips = 'Cancel "Prevent Cross Site Request Forgery exploits" when using password.';
diff --git a/module/gitlab/lang/fr.php b/module/gitlab/lang/fr.php
new file mode 100644
index 0000000000..d3dcf35cbd
--- /dev/null
+++ b/module/gitlab/lang/fr.php
@@ -0,0 +1,20 @@
+jenkins->common = 'Jenkins';
+$lang->jenkins->browse = 'Afficher Jenkins';
+$lang->jenkins->create = 'Cr閑r Jenkins';
+$lang->jenkins->edit = 'Editer Jenkins';
+$lang->jenkins->delete = 'Supprimer';
+$lang->jenkins->confirmDelete = 'Voulez-vous supprimer ce serveur Jenkins ?';
+
+$lang->jenkins->id = 'ID';
+$lang->jenkins->name = 'Nom';
+$lang->jenkins->url = 'Service URL';
+$lang->jenkins->token = 'Token';
+$lang->jenkins->account = 'UserName';
+$lang->jenkins->password = 'Password';
+
+$lang->jenkins->lblCreate = 'Cr閑r Serveur Jenkins';
+$lang->jenkins->desc = 'Description';
+$lang->jenkins->tokenFirst = 'Utiliser un Token si non vide.';
+$lang->jenkins->tips = 'Cancel "Prevent Cross Site Request Forgery exploits" when using password.';
+$lang->jenkins->tips = "Annuler Emp阠her les exploits de contrefa鏾n de demande intersite lors de l'utilisation du mot de passe.";
diff --git a/module/gitlab/lang/vi.php b/module/gitlab/lang/vi.php
new file mode 100644
index 0000000000..b5a5f5d46d
--- /dev/null
+++ b/module/gitlab/lang/vi.php
@@ -0,0 +1,19 @@
+jenkins->common = 'Jenkins';
+$lang->jenkins->browse = 'Jenkins';
+$lang->jenkins->create = 'Tạo Jenkins';
+$lang->jenkins->edit = 'Sửa Jenkins';
+$lang->jenkins->delete = 'Xóa';
+$lang->jenkins->confirmDelete = 'Bạn có muốn xóa máy chủ Jenkins này?';
+
+$lang->jenkins->id = 'ID';
+$lang->jenkins->name = 'Tên';
+$lang->jenkins->url = 'URL';
+$lang->jenkins->token = 'Token';
+$lang->jenkins->account = 'Người dùng';
+$lang->jenkins->password = 'Mật khẩu';
+
+$lang->jenkins->lblCreate = 'Tạo Jenkins Server';
+$lang->jenkins->desc = 'Mô tả';
+$lang->jenkins->tokenFirst = 'Sử dụng token nếu không trống.';
+$lang->jenkins->tips = 'Hủy "Ngăn chặn khai thác yêu cầu trang web giả mạo" khi sử dụng mật khẩu.';
diff --git a/module/gitlab/lang/zh-cn.php b/module/gitlab/lang/zh-cn.php
new file mode 100644
index 0000000000..923bcf7baf
--- /dev/null
+++ b/module/gitlab/lang/zh-cn.php
@@ -0,0 +1,44 @@
+gitlab = new stdclass;
+$lang->gitlab->common = 'Gitlab';
+$lang->gitlab->browse = '浏览gitlab';
+$lang->gitlab->create = '添加gitlab';
+$lang->gitlab->edit = '编辑gitlab';
+$lang->gitlab->bindUser = '绑定用户';
+$lang->gitlab->bindProduct = '关联产品';
+$lang->gitlab->importIssue = '关联issue';
+$lang->gitlab->delete = '删除';
+$lang->gitlab->confirmDelete = '确认删除该gitlab吗?';
+$lang->gitlab->gitlabAccount = 'gitlab用户';
+$lang->gitlab->zentaoAccount = '禅道用户';
+
+$lang->gitlab->browseAction = 'gitlab列表';
+$lang->gitlab->deleteAction = '删除gitlab';
+$lang->gitlab->gitlabProject = "{$lang->gitlab->common}项目";
+$lang->gitlab->gitlabIssue = "{$lang->gitlab->common}issue";
+$lang->gitlab->zentaoProduct = '禅道产品';
+$lang->gitlab->objectType = '类型'; // task, bug, story
+
+$lang->gitlab->id = 'ID';
+$lang->gitlab->name = "{$lang->gitlab->common}名称";
+$lang->gitlab->url = '服务地址';
+$lang->gitlab->token = 'Token';
+$lang->gitlab->defaultProject = '默认项目';
+$lang->gitlab->private = 'MD5验证';
+
+$lang->gitlab->lblCreate = '添加gitlab服务器';
+$lang->gitlab->desc = '描述';
+$lang->gitlab->tokenFirst = 'Token不为空时,优先使用Token。';
+$lang->gitlab->tips = '使用密码时,请在gitlab全局安全设置中禁用"防止跨站点请求伪造"选项。';
+
+$lang->gitlab->placeholder = new stdclass;
+$lang->gitlab->placeholder->name = '';
+$lang->gitlab->placeholder->url = "请填写Gitlab Server首页的访问地址,如:https://gitlab.zentao.net。";
+$lang->gitlab->placeholder->token = "请填写具有admin权限账户的access token";
+
+$lang->gitlab->noImportableIssues = "目前没有可供导入的issue。";
+$lang->gitlab->tokenError = "当前token非管理员权限。";
+$lang->gitlab->hostError = "无效的gitlab服务地址。";
+$lang->gitlab->bindUserError = "不能重复绑定用户 %s";
+$lang->gitlab->importIssueError = "未选择该issue所属的执行。";
+$lang->gitlab->importIssueWarn = "存在导入失败的issue,可再次尝试导入。";
diff --git a/module/gitlab/lang/zh-tw.php b/module/gitlab/lang/zh-tw.php
new file mode 100644
index 0000000000..e19ffbbae3
--- /dev/null
+++ b/module/gitlab/lang/zh-tw.php
@@ -0,0 +1,22 @@
+jenkins->common = 'Jenkins';
+$lang->jenkins->browse = '瀏覽Jenkins';
+$lang->jenkins->create = '添加Jenkins';
+$lang->jenkins->edit = '編輯Jenkins';
+$lang->jenkins->delete = '刪除';
+$lang->jenkins->confirmDelete = '確認刪除該Jenkins嗎?';
+
+$lang->jenkins->browseAction = 'Jenkins列表';
+$lang->jenkins->deleteAction = '刪除Jenkins';
+
+$lang->jenkins->id = 'ID';
+$lang->jenkins->name = '名稱';
+$lang->jenkins->url = '服務地址';
+$lang->jenkins->token = 'Token';
+$lang->jenkins->account = '用戶名';
+$lang->jenkins->password = '密碼';
+
+$lang->jenkins->lblCreate = '添加Jenkins伺服器';
+$lang->jenkins->desc = '描述';
+$lang->jenkins->tokenFirst = 'Token不為空時,優先使用Token。';
+$lang->jenkins->tips = '使用密碼時,請在Jenkins全局安全設置中禁用"防止跨站點請求偽造"選項。';
diff --git a/module/gitlab/model.php b/module/gitlab/model.php
new file mode 100644
index 0000000000..4a9296442f
--- /dev/null
+++ b/module/gitlab/model.php
@@ -0,0 +1,1227 @@
+
+ * @package product
+ * @version $Id: $
+ * @link http://www.zentao.net
+ */
+
+class gitlabModel extends model
+{
+ /**
+ * Get a gitlab by id.
+ *
+ * @param int $id
+ * @access public
+ * @return object
+ */
+ public function getByID($id)
+ {
+ return $this->loadModel('pipeline')->getByID($id);
+ }
+
+ /**
+ * Get gitlab list.
+ *
+ * @param string $orderBy
+ * @param object $pager
+ * @access public
+ * @return array
+ */
+ public function getList($orderBy = 'id_desc', $pager = null)
+ {
+ return $this->loadModel('pipeline')->getList('gitlab', $orderBy, $pager);
+ }
+
+ /**
+ * Get gitlab pairs
+ *
+ * @return array
+ */
+ public function getPairs()
+ {
+ return $this->loadModel('pipeline')->getPairs('gitlab');
+ }
+
+ /**
+ * Get gitlab api base url by gitlab id.
+ *
+ * @param int $id
+ * @access public
+ * @return string
+ */
+ public function getApiRoot($id)
+ {
+ $gitlab = $this->getByID($id);
+ if(!$gitlab) return '';
+ return rtrim($gitlab->url, '/').'/api/v4%s'."?private_token={$gitlab->token}";
+ }
+
+ /**
+ * Get gitlab user id zentao account pairs of one gitlab.
+ *
+ * @param int $gitlab
+ * @access public
+ * @return array
+ */
+ public function getUserIdAccountPairs($gitlab)
+ {
+ return $this->dao->select('openID,account')->from(TABLE_OAUTH)
+ ->where('providerType')->eq('gitlab')
+ ->andWhere('providerID')->eq($gitlab)
+ ->fetchPairs();
+ }
+
+ /**
+ * Get zentao account gitlab user id pairs of one gitlab.
+ *
+ * @param int $gitlab
+ * @access public
+ * @return array
+ */
+ public function getUserAccountIdPairs($gitlab)
+ {
+ return $this->dao->select('account,openID')->from(TABLE_OAUTH)
+ ->where('providerType')->eq('gitlab')
+ ->andWhere('providerID')->eq($gitlab)
+ ->fetchPairs();
+ }
+
+ /**
+ * Get project pairs of one gitlab.
+ *
+ * @param int $gitlabID
+ * @access public
+ * @return array
+ */
+ public function getProjectPairs($gitlabID)
+ {
+ $projects = $this->apiGetProjects($gitlabID);
+
+ $projectPairs = array();
+ foreach($projects as $project) $projectPairs[$project->id] = $project->name_with_namespace;
+
+ return $projectPairs;
+ }
+
+ /**
+ * Get matched gitlab users.
+ *
+ * @param array $gitlabUsers
+ * @param array $zentaoUsers
+ * @access public
+ * @return array
+ */
+ public function getMatchedUsers($gitlabID, $gitlabUsers, $zentaoUsers)
+ {
+ $matches = new stdclass;
+ foreach($gitlabUsers as $gitlabUser)
+ {
+ foreach($zentaoUsers as $zentaoUser)
+ {
+ if($gitlabUser->account == $zentaoUser->account) $matches->accounts[$gitlabUser->account][] = $zentaoUser->account;
+ if($gitlabUser->realname == $zentaoUser->realname) $matches->names[$gitlabUser->realname][] = $zentaoUser->account;
+ if($gitlabUser->email == $zentaoUser->email) $matches->emails[$gitlabUser->email][] = $zentaoUser->account;
+ }
+ }
+
+ $bindedUsers = $this->dao->select('openID,account')
+ ->from(TABLE_OAUTH)
+ ->where('providerType')->eq('gitlab')
+ ->andWhere('providerID')->eq($gitlabID)
+ ->fetchPairs();
+
+ $matchedUsers = array();
+ foreach($gitlabUsers as $gitlabUser)
+ {
+ if(isset($bindedUsers[$gitlabUser->id]))
+ {
+ $gitlabUser->zentaoAccount = $bindedUsers[$gitlabUser->id];
+ $matchedUsers[] = $gitlabUser;
+ continue;
+ }
+
+ $matchedZentaoUsers = array();
+ if(isset($matches->accounts[$gitlabUser->account])) $matchedZentaoUsers = array_merge($matchedZentaoUsers, $matches->accounts[$gitlabUser->account]);
+ if(isset($matches->emails[$gitlabUser->email])) $matchedZentaoUsers = array_merge($matchedZentaoUsers, $matches->emails[$gitlabUser->email]);
+ if(isset($matches->names[$gitlabUser->realname])) $matchedZentaoUsers = array_merge($matchedZentaoUsers, $matches->names[$gitlabUser->realname]);
+
+ $matchedZentaoUsers = array_unique($matchedZentaoUsers);
+ if(count($matchedZentaoUsers) == 1)
+ {
+ $gitlabUser->zentaoAccount = current($matchedZentaoUsers);
+ $matchedUsers[] = $gitlabUser;
+ }
+ }
+
+ return $matchedUsers;
+ }
+
+ /**
+ * Get gitlab projects by executionID.
+ *
+ * @param int $executionID
+ * @access public
+ * @return array
+ */
+ public function getProjectsByExecution($executionID)
+ {
+ $products = $this->loadModel('execution')->getProducts($executionID, false);
+ $productIdList = array_keys($products);
+
+ return $this->dao->select('AID,BID as gitlabProject')
+ ->from(TABLE_RELATION)
+ ->where('relation')->eq('interrated')
+ ->andWhere('AType')->eq('gitlab')
+ ->andWhere('BType')->eq('gitlabProject')
+ ->andWhere('product')->in($productIdList)
+ ->fetchGroup('AID');
+ }
+
+ /**
+ * Get executions by one product for gitlab module.
+ *
+ * @param int $productID
+ * @access public
+ * @return array
+ */
+ public function getExecutionsByProduct($productID)
+ {
+ return $this->dao->select('distinct execution')->from(TABLE_RELATION)
+ ->where('relation')->eq('interrated')
+ ->andWhere('AType')->eq('gitlab')
+ ->andWhere('BType')->eq('gitlabProject')
+ ->andWhere('product')->eq($productID)
+ ->fetchAll('execution');
+ }
+
+ /**
+ * Get gitlabID and projectID.
+ *
+ * @param string $objectType
+ * @param int $objectID
+ * @access public
+ * @return object
+ */
+ public function getRelationByObject($objectType, $objectID)
+ {
+ return $this->dao->select('*, extra as gitlabID, BVersion as projectID, BID as issueID')->from(TABLE_RELATION)
+ ->where('relation')->eq('gitlab')
+ ->andWhere('Atype')->eq($objectType)
+ ->andWhere('AID')->eq($objectID)
+ ->fetch();
+ }
+
+ /**
+ * Get issue id list group by obejct.
+ *
+ * @param string $objectType
+ * @param int $objectID
+ * @access public
+ * @return object
+ */
+ public function getIssueListByObjects($objectType, $objects)
+ {
+ return $this->dao->select('*, extra as gitlabID, BVersion as projectID, BID as issueID')->from(TABLE_RELATION)
+ ->where('relation')->eq('gitlab')
+ ->andWhere('Atype')->eq($objectType)
+ ->andWhere('AID')->in($objects)
+ ->fetchAll('AID');
+ }
+
+
+ /**
+ * Get gitlab userID by account.
+ *
+ * @param int $gitlabID
+ * @param string $account
+ * @access public
+ * @return arary
+ */
+ public function getGitlabUserID($gitlabID, $account)
+ {
+ return $this->dao->select('openID')->from(TABLE_OAUTH)
+ ->where('providerType')->eq('gitlab')
+ ->andWhere('providerID')->eq($gitlabID)
+ ->andWhere('account')->eq($account)
+ ->fetch('openID');
+ }
+
+ /**
+ * Create a gitlab.
+ *
+ * @access public
+ * @return bool
+ */
+ public function create()
+ {
+ return $this->loadModel('pipeline')->create('gitlab');
+ }
+
+ /**
+ * Update a gitlab.
+ *
+ * @param int $id
+ * @access public
+ * @return bool
+ */
+ public function update($id)
+ {
+ return $this->loadModel('pipeline')->update($id);
+ }
+
+ /**
+ * Send an api get request.
+ *
+ * @param int|string $host gitlab server ID | gitlab host url.
+ * @param int $api
+ * @param int $data
+ * @param int $options
+ * @access public
+ * @return object
+ */
+ public function apiGet($host, $api, $data = array(), $options = array())
+ {
+ if(is_numeric($host)) $host = $this->getApiRoot($host);
+ if(strpos($host, 'http://') !== 0 and strpos($host, 'https://') !== 0) return false;
+
+ $url = sprintf($host, $api);
+ return json_decode(commonModel::http($url, $data, $options));
+ }
+
+ /**
+ * Send an api post request.
+ *
+ * @param int|string $host gitlab server ID | gitlab host url.
+ * @param int $api
+ * @param int $data
+ * @param int $options
+ * @access public
+ * @return object
+ */
+ public function apiPost($host, $api, $data = array(), $options = array())
+ {
+ if(is_numeric($host)) $host = $this->getApiRoot($host);
+ if(strpos($host, 'http://') !== 0 and strpos($host, 'https://') !== 0) return false;
+
+ $url = sprintf($apiRoot, $api);
+ return json_decode(commonModel::http($url, $data, $options));
+ }
+
+ /**
+ * Get current user.
+ *
+ * @param string $host
+ * @param string $token
+ * @access public
+ * @return array
+ */
+ public function apiGetCurrentUser($host, $token)
+ {
+ $host = rtrim($host, '/') . "/api/v4%s?private_token=$token";
+ return $this->apiGet($host, '/user');
+ }
+
+ /**
+ * Get gitlab user list.
+ *
+ * @param string $host
+ * @param string $token
+ * @access public
+ * @return array
+ */
+ public function apiGetUsers($gitlab)
+ {
+ $response = $this->apiGet($gitlab->id, '/users');
+
+ if (!$response) return array();
+
+ $users = array();
+ foreach($response as $gitlabUser)
+ {
+ $user = new stdclass;
+ $user->id = $gitlabUser->id;
+ $user->realname = $gitlabUser->name;
+ $user->account = $gitlabUser->username;
+ $user->email = $gitlabUser->email;
+ $user->avatar = $gitlabUser->avatar_url;
+
+ $users[] = $user;
+ }
+
+ return $users;
+ }
+
+ /**
+ * Get projects of one gitlab.
+ *
+ * @param int $gitlabID
+ * @access public
+ * @return void
+ */
+ public function apiGetProjects($gitlabID)
+ {
+ $gitlab = $this->getByID($gitlabID);
+ if(!$gitlab) return array();
+ $host = rtrim($gitlab->url, '/');
+ $host .= '/api/v4/projects';
+
+ $allResults = array();
+ for($page = 1; true; $page ++)
+ {
+ $results = json_decode(commonModel::http($host . "?private_token={$gitlab->token}&simple=true&membership=true&page={$page}&per_page=100"));
+ if(empty($results) or $page > 10) break;
+ $allResults = $allResults + $results;
+ }
+ return $allResults;
+ }
+
+ /**
+ * Get hooks.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @access public
+ * @return void
+ */
+ public function apiGetHooks($gitlabID, $projectID)
+ {
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $apiPath = "/projects/{$projectID}/hooks";
+ $url = sprintf($apiRoot, $apiPath);
+ $response = json_decode(commonModel::http($url));
+ return $response;
+ }
+
+ /**
+ * Get specific hook by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param int $hookID
+ * @access public
+ * @return object
+ */
+ public function apiGetHook($gitlabID, $projectID, $hookID)
+ {
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $apiPath = "/projects/$projectID/hooks/$hookID)";
+ $url = sprintf($apiRoot, $apiPath);
+ $response = commonModel::http($url);
+ return $response;
+ }
+
+ /**
+ * Create hook by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param string $url
+ * @access public
+ * @return object
+ */
+ public function apiCreateHook($gitlabID, $projectID, $url)
+ {
+ $apiRoot = $this->getApiRoot($gitlabID);
+
+ $accessToken = $this->dao->select('private as accessToken')->from(TABLE_PIPELINE)
+ ->where('id')->eq($gitlabID)
+ ->fetch('accessToken');
+
+ $postData = new stdclass;
+ $postData->enable_ssl_verification = "false";
+ $postData->issues_events = "true";
+ $postData->merge_requests_events = "true";
+ $postData->push_events = "true";
+ $postData->tag_push_events = "true";
+ $postData->url = $url;
+ $postData->token = $accessToken;
+
+ $url = sprintf($apiRoot, "/projects/{$projectID}/hooks");
+ return commonModel::http($url, $postData);
+ }
+
+ /**
+ * Delete hook by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param int $hookID
+ * @access public
+ * @return null|object
+ */
+ public function apiDeleteHook($gitlabID, $projectID, $hookID)
+ {
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $url = sprintf($apiRoot, "/projects/{$projectID}/hooks/{$hookID}");
+
+ return commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'delete'));
+ }
+
+ /**
+ * Update hook by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param int $hookID
+ * @access public
+ * @return object
+ */
+ public function apiUpdateHook($gitlabID, $projectID, $hookID)
+ {
+ $apiRoot = $this->getApiRoot($gitlabID);
+
+ $postData = new stdclass;
+ $postData->enable_ssl_verification = "false";
+ $postData->issues_events = "true";
+ $postData->merge_requests_events = "true";
+ $postData->push_events = "true";
+ $postData->tag_push_events = "true";
+ $postData->note_events = "true";
+ $postData->url = $url;
+ $postData->token = $token;
+
+ $url = sprintf($apiRoot, "/projects/{$projectID}/hooks/{$hookID}");
+ return commonModel::http($url, $postData, $options = array(CURLOPT_CUSTOMREQUEST => 'PUT'));
+ }
+
+ /**
+ * Create Label for gitlab project.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param object $label
+ * @access public
+ * @return object
+ */
+ public function apiCreateLabel($gitlabID, $projectID, $label)
+ {
+ if(empty($label->name) or empty($label->color)) return false;
+
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $url = sprintf($apiRoot, "/projects/{$projectID}/labels/");
+ return json_decode(commonModel::http($url, $label));
+ }
+
+ /**
+ * Get labels of project by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @access public
+ * @return array
+ */
+ public function apiGetLabels($gitlabID, $projectID)
+ {
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $url = sprintf($apiRoot, "/projects/{$projectID}/labels/");
+ $response = commonModel::http($url);
+
+ return json_decode($response);
+ }
+
+ /**
+ * Delete a Label with labelName by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param string $labelName
+ * @access public
+ * @return object
+ */
+ public function apiDeleteLabel($gitlabID, $projectID, $labelName)
+ {
+ $labels = $this->apiGetLabels($gitlabID, $projectID);
+ foreach($labels as $label)
+ {
+ if($label->name == $labelName) $labelID = $label->id;
+ }
+
+ if(empty($labelID)) return false;
+
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $url = sprintf($apiRoot, "/projects/{$projectID}/labels/{$labelID}");
+
+ return json_decode(commonModel::http($url, null, $options = array(CURLOPT_CUSTOMREQUEST => 'DELETE')));
+ }
+
+ /**
+ * Get single issue by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param int $issueID
+ * @access public
+ * @return object
+ */
+ public function apiGetSingleIssue($gitlabID, $projectID, $issueID)
+ {
+ $url = sprintf($this->getApiRoot($gitlabID), "/projects/$projectID/issues/{$issueID}");
+ return json_decode(commonModel::http($url));
+ }
+
+ /**
+ * Get gitlab issues by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param string $options
+ * @access public
+ * @return object
+ */
+ public function apiGetIssues($gitlabID, $projectID, $options = null)
+ {
+ // TODO(dingguodong) not pagination yet.
+ if($options)
+ {
+ $url = sprintf($this->getApiRoot($gitlabID), "/projects/{$projectID}/issues") . '&per_page=20' . $options;
+ }
+ else
+ {
+ $url = sprintf($this->getApiRoot($gitlabID), "/projects/{$projectID}/issues") . '&per_page=20';
+ }
+ return json_decode(commonModel::http($url));
+ }
+
+ /**
+ * Create issue by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param string $objectType
+ * @param int $objectID
+ * @param object $object
+ * @access public
+ * @return object
+ */
+ public function apiCreateIssue($gitlabID, $projectID, $objectType, $objectID, $object)
+ {
+ $label = $this->createZentaoObjectLabel($gitlabID, $projectID, $objectType, $objectID);
+ if(!isset($object->id)) $object->id = $objectID;
+ $issue = $this->loadModel('gitlab')->parseObjectToIssue($gitlabID, $projectID, $objectType, $object);
+ if(isset($label->name)) $issue->labels = $label->name;
+ foreach($this->config->gitlab->skippedFields->issueCreate[$objectType] as $field)
+ {
+ if(isset($issue->$field)) unset($issue->$field);
+ }
+
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $url = sprintf($apiRoot, "/projects/{$projectID}/issues/");
+
+ $response = json_decode(commonModel::http($url, $issue));
+ if(!$response) return false;
+
+ return $this->saveIssueRelation($objectType, $object, $gitlabID, $response);
+ }
+
+ /**
+ * Update issue by gitlab API.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param int $issueID
+ * @param string $objectType
+ * @param object $object
+ * @param int $objectID
+ * @access public
+ * @return object
+ */
+ public function apiUpdateIssue($gitlabID, $projectID, $issueID, $objectType, $object, $objectID = null)
+ {
+ $oldObject = clone $object;
+ /* Get full object when desc is empty. */
+ if(!isset($object->description) || (isset($object->description) && $object->description == '')) $object = $this->loadModel($objectType)->getByID($objectID);
+ foreach($oldObject as $index => $attribute)
+ {
+ if($index != 'description') $object->$index = $attribute;
+ }
+
+ if(!isset($object->id) && !empty($objectID)) $object->id = $objectID;
+ $issue = $this->parseObjectToIssue($gitlabID, $projectID, $objectType, $object);
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $url = sprintf($apiRoot, "/projects/{$projectID}/issues/{$issueID}");
+ return json_decode(commonModel::http($url, $issue, $options = array(CURLOPT_CUSTOMREQUEST => 'PUT')));
+ }
+
+ /**
+ * Delete an issue by api.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param int $issueID
+ * @access public
+ * @return object
+ */
+ public function apiDeleteIssue($gitlabID, $projectID, $issueID)
+ {
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $url = sprintf($apiRoot, "/projects/{$projectID}/issues/{$issueID}");
+ return json_decode(commonModel::http($url, null, array(CURLOPT_CUSTOMREQUEST => 'DELETE')));
+ }
+
+ /**
+ * Check webhook token by HTTP_X_GITLAB_TOKEN.
+ *
+ * @access public
+ * @return void
+ */
+ public function webhookCheckToken()
+ {
+ $gitlab = $this->getByID($this->get->gitlab);
+ if($gitlab->private != $_SERVER["HTTP_X_GITLAB_TOKEN"]) die('Token error.');
+ }
+
+ /**
+ * Parse webhook body function.
+ *
+ * @param object $body
+ * @param int $gitlabID
+ * @access public
+ * @return object
+ */
+ public function webhookParseBody($body, $gitlabID)
+ {
+ $type = zget($body, 'object_kind', '');
+ if(!$type or !is_callable(array($this, "webhookParse{$type}"))) return false;
+ return call_user_func_array(array($this, "webhookParse{$type}"), array('body' => $body, $gitlabID));
+ }
+
+ /**
+ * Parse Webhook issue.
+ *
+ * @param object $body
+ * @param int $gitlabID
+ * @access public
+ * @return object
+ */
+ public function WebhookParseIssue($body, $gitlabID)
+ {
+ $object = $this->webhookParseObject($body->labels);
+ if(empty($object)) return null;
+
+ $issue = new stdclass;
+ $issue->action = $body->object_attributes->action . $body->object_kind;
+ $issue->issue = $body->object_attributes;
+ $issue->changes = $body->changes;
+ $issue->objectType = $object->type;
+ $issue->objectID = $object->id;
+
+ $issue->issue->objectType = $object->type;
+ $issue->issue->objectID = $object->id;
+
+ /* Parse markdown description to html. */
+ $issue->issue->description = $this->app->loadClass('hyperdown')->makeHtml($issue->issue->description);
+
+ if(!isset($this->config->gitlab->maps->{$object->type})) return false;
+ $issue->object = $this->issueToZentaoObject($issue->issue, $gitlabID, $body->changes);
+ return $issue;
+ }
+
+ /**
+ * Webhook parse note.
+ *
+ * @param object $body
+ * @access public
+ * @return void
+ */
+ public function webhookParseNote($body)
+ {
+ //@todo
+ }
+
+ /**
+ * Webhook sync issue.
+ *
+ * @param object $issue
+ * @param int $objectType
+ * @param int $objectID
+ * @access public
+ * @return void
+ */
+ public function webhookSyncIssue($gitlabID, $issue)
+ {
+ $tableName = zget($this->config->gitlab->objectTables, $issue->objectType, '');
+ if($tableName) $this->dao->update($tableName)->data($issue->object)->where('id')->eq($issue->objectID)->exec();
+ return !dao::isError();
+ }
+
+ /**
+ * Parse zentao object from labels.
+ *
+ * @param array $labels
+ * @access public
+ * @return object
+ */
+ public function webhookParseObject($labels)
+ {
+ $object = null;
+ $objectType = '';
+ foreach($labels as $label)
+ {
+ if(preg_match($this->config->gitlab->labelPattern->story, $label->title)) $objectType = 'story';
+ if(preg_match($this->config->gitlab->labelPattern->task, $label->title)) $objectType = 'task';
+ if(preg_match($this->config->gitlab->labelPattern->bug, $label->title)) $objectType = 'bug';
+
+ if($objectType)
+ {
+ list($prefix, $id) = explode('/', $label->title);
+ $object = new stdclass;
+ $object->id = $id;
+ $object->type = $objectType;
+ }
+ }
+
+ return $object;
+ }
+
+ /**
+ * Process webhook issue assign option.
+ *
+ * @param int $gitlabID
+ * @param object $issue
+ * @access public
+ * @return bool
+ */
+ public function webhookAssignIssue($issue)
+ {
+ $tableName = zget($this->config->gitlab->objectTables, $issue->objectType, '');
+ if(!$tableName) return false;
+
+ $data = $issue->object;
+ $data->assignedDate = $issue->object->lastEditedDate;
+ $data->assignedTo = $issue->object->assignedTo;
+
+ $this->dao->update($tableName)->data($data)->where('id')->eq($issue->objectID)->exec();
+ if(dao::isError()) return false;
+
+ $oldObject = $this->dao->findById($issue->objectID)->from($tableName)->fetch();
+ $changes = common::createChanges($oldObject, $data);
+ $actionID = $this->loadModel('action')->create($issue->objectType, $issue->objectID, 'Assigned', "Assigned by webhook by gitlab issue : {$issue->issue->url}", $data->assignedTo);
+ $this->action->logHistory($actionID, $changes);
+
+ return true;
+ }
+
+ /**
+ * Process issue close option.
+ *
+ * @param int $gitlabID
+ * @param object $issue
+ * @access public
+ * @return bool
+ */
+ public function webhookCloseIssue($issue)
+ {
+ $tableName = zget($this->config->gitlab->objectTables, $issue->objectType, '');
+ if(!$tableName) return false;
+
+ $data = $issue->object;
+ $data->assignedTo = 'closed';
+ $data->status = 'closed';
+ $data->closedBy = $issue->object->lastEditedBy;
+ $data->closedDate = $issue->object->lastEditedDate;
+
+ $this->dao->update($tableName)->data($data)->where('id')->eq($issue->objectID)->exec();
+ if(dao::isError()) return false;
+
+ $oldObject = $this->dao->findById($issue->objectID)->from($tableName)->fetch();
+ $changes = common::createChanges($oldObject, $data);
+ $actionID = $this->loadModel('action')->create($issue->objectType, $issue->objectID, 'Closed', "Closed by gitlab issue: {$issue->issue->url}.");
+ $this->action->logHistory($actionID, $changes);
+ return true;
+ }
+
+
+ /**
+ * Create zentao object label for gitlab project.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param string $objectType
+ * @param string $objectID
+ * @access public
+ * @return object
+ */
+ public function createZentaoObjectLabel($gitlabID, $projectID, $objectType, $objectID)
+ {
+ $label = new stdclass;
+ $label->name = sprintf($this->config->gitlab->zentaoObjectLabel->name, $objectType, $objectID);
+ $label->color = $this->config->gitlab->zentaoObjectLabel->color->$objectType;
+ $label->description = common::getSysURL() . helper::createLink($objectType, 'view', "id={$objectID}");
+
+ return $this->apiCreateLabel($gitlabID, $projectID, $label);
+ }
+
+ /**
+ * Create relationship between zentao product and gitlab project.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @access public
+ * @return void
+ */
+ public function saveProjectRelation($products, $gitlabID, $gitlabProjectID)
+ {
+ $programs = $this->dao->select('id,program')->from(TABLE_PRODUCT)->where('id')->in($products)->fetchPairs();
+
+ $relation = new stdclass;
+ $relation->execution = 0;
+ $relation->AType = 'gitlab';
+ $relation->AID = $gitlabID;
+ $relation->AVersion = '';
+ $relation->relation = 'interrated';
+ $relation->BType = 'gitlabProject';
+ $relation->BID = $gitlabProjectID;
+ $relation->BVersion = '';
+ $relation->extra = '';
+
+ foreach($products as $product)
+ {
+ $relation->project = zget($programs, $product, 0);
+ $relation->product = $product;
+ $this->dao->replace(TABLE_RELATION)->data($relation)->exec();
+ }
+ return true;
+ }
+
+ /**
+ * Delete project relation.
+ *
+ * condition: when user deleting a repo.
+ *
+ * @param int $repoID
+ * @access public
+ * @return void
+ */
+ public function deleteProjectRelation($repoID)
+ {
+ $repo = $this->dao->select('product,path as gitlabProjectID,client as gitlabID')->from(TABLE_REPO)
+ ->where('id')->eq($repoID)
+ ->andWhere('deleted')->eq(0)
+ ->fetch();
+ if(empty($repo)) return false;
+
+ $productIDList = explode(',', $repo->product);
+ foreach($productIDList as $product)
+ {
+ $this->dao->delete()->from(TABLE_RELATION)
+ ->where('product')->eq($product)
+ ->andWhere('AType')->eq('gitlab')
+ ->andWhere('BType')->eq('gitlabProject')
+ ->andWhere('relation')->eq('interrated')
+ ->andWhere('AID')->eq($repo->gitlabID)
+ ->andWhere('BID')->eq($repo->gitlabProjectID)
+ ->exec();
+ }
+ }
+
+ /**
+ * Create webhook for zentao.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @access public
+ * @return bool
+ */
+ public function initWebhooks($products, $gitlabID, $projectID)
+ {
+ $gitlab = $this->getByID($gitlabID);
+ $webhooks = $this->apiGetHooks($gitlabID, $projectID);
+ foreach($products as $index => $product)
+ {
+ $url = sprintf($this->config->gitlab->webhookURL, commonModel::getSysURL(), $product, $gitlabID);
+ foreach($webhooks as $webhook) if($webhook->url == $url) continue;
+ $response = $this->apiCreateHook($gitlabID, $projectID, $url);
+ }
+
+ return true;
+ }
+
+ /**
+ * Delete an issue from zentao and gitlab.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param string $objectType
+ * @param int $objectID
+ * @param int $issueID
+ * @access public
+ * @return void
+ */
+ public function deleteIssue($objectType, $objectID, $issueID)
+ {
+ $object = $this->loadModel($objectType)->getByID($objectID);
+ $relation = $this->getRelationByObject($objectType, $objectID);
+ if(!empty($relation)) $this->dao->delete()->from(TABLE_RELATION)->where('id')->eq($relation->id)->exec();
+ $this->apiDeleteIssue($relation->gitlabID, $relation->projectID, $issueID);
+ }
+
+ /**
+ * Save synced issue to relation table.
+ *
+ * @param string $objectType
+ * @param object $object
+ * @param int $gitlab
+ * @param object $issue
+ * @access public
+ * @return void
+ */
+ public function saveIssueRelation($objectType, $object, $gitlabID, $issue)
+ {
+ if(empty($issue->iid) or empty($issue->project_id)) return false;
+
+ $relation = new stdclass;
+ $relation->product = zget($object, 'product', 0);
+ $relation->execution = zget($object, 'execution', 0);
+ $relation->AType = $objectType;
+ $relation->AID = $object->id;
+ $relation->AVersion = '';
+ $relation->relation = 'gitlab';
+ $relation->BType = 'issue';
+ $relation->BID = $issue->iid;
+ $relation->BVersion = $issue->project_id;
+ $relation->extra = $gitlabID;
+ $this->dao->replace(TABLE_RELATION)->data($relation)->exec();
+ }
+
+ /**
+ * Save imported issue.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param string $objectType
+ * @param int $objectID
+ * @param object $issue
+ * @access public
+ * @return void
+ */
+ public function saveImportedIssue($gitlabID, $projectID, $objectType, $objectID, $issue, $object)
+ {
+ $label = $this->createZentaoObjectLabel($gitlabID, $projectID, $objectType, $objectID);
+ $data = new stdclass;
+ if(isset($label->name)) $data->labels = $label->name;
+
+ $apiRoot = $this->getApiRoot($gitlabID);
+ $url = sprintf($apiRoot, "/projects/{$projectID}/issues/{$issue->iid}");
+ commonModel::http($url, $data, $options = array(CURLOPT_CUSTOMREQUEST => 'PUT'));
+ $this->saveIssueRelation($objectType, $object, $gitlabID, $issue);
+ }
+
+ /**
+ * Parse task to issue.
+ *
+ * @param int $gitlabID
+ * @param int $gitlabProjectID
+ * @param object $task
+ * @access public
+ * @return object
+ */
+ public function taskToIssue($gitlabID, $gitlabProjectID, $task)
+ {
+ $map = $this->config->gitlab->maps->task;
+ $issue = new stdclass;
+ $gitlabUsers = $this->getUserAccountIdPairs($gitlabID);
+
+ foreach($map as $taskField => $config)
+ {
+ $value = '';
+ list($field, $optionType, $options) = explode('|', $config);
+ if($optionType == 'field') $value = $task->$taskField;
+ if($optionType == 'userPairs') $value = zget($gitlabUsers, $task->$taskField);
+ if($optionType == 'configItems')
+ {
+ $value = zget($this->config->gitlab->$options, $task->$taskField, '');
+ }
+ if($value) $issue->$field = $value;
+ }
+
+ if($isset($issue->assignee_id) and $issue->assignee_id == 'closed') unset($issue->assignee_id);
+
+ /* issue->state is null when creating it, we should put status_event when updating it. */
+ if(isset($issue->state) and $issue->state == 'closed') $issue->state_event='close';
+ if(isset($issue->state) and $issue->state == 'opened') $issue->state_event='reopen';
+
+ /* Append this object link in zentao to gitlab issue description */
+ $zentaoLink = common::getSysURL() . helper::createLink('task', 'view', "taskID={$task->id}");
+ $issue->description = $issue->description . "\n\n" . $zentaoLink;
+
+ return $issue;
+ }
+
+ /**
+ * Parse story to issue.
+ *
+ * @param int $gitlabID
+ * @param int $gitlabProjectID
+ * @param object $story
+ * @access public
+ * @return object
+ */
+ public function storyToIssue($gitlabID, $gitlabProjectID, $story)
+ {
+ $map = $this->config->gitlab->maps->story;
+ $issue = new stdclass;
+ $gitlabUsers = $this->getUserAccountIdPairs($gitlabID);
+ if(empty($gitlabUsers)) return false;
+
+ foreach($map as $storyField => $config)
+ {
+ $value = '';
+ list($field, $optionType, $options) = explode('|', $config);
+ if($optionType == 'field') $value = $story->$storyField;
+ if($optionType == 'fields') $value = $story->$storyField . "\n\n" . $story->$options;
+ if($optionType == 'userPairs')
+ {
+ $value = zget($gitlabUsers, $story->$storyField);
+ }
+ if($optionType == 'configItems')
+ {
+ $value = zget($this->config->gitlab->$options, $story->$storyField, '');
+ }
+ if($value) $issue->$field = $value;
+ }
+
+ if($issue->assignee_id == 'closed') unset($issue->assignee_id);
+
+ /* issue->state is null when creating it, we should put status_event when updating it. */
+ if(isset($issue->state) and $issue->state == 'closed') $issue->state_event='close';
+ if(isset($issue->state) and $issue->state == 'opened') $issue->state_event='reopen';
+
+ /* Append this object link in zentao to gitlab issue description */
+ $zentaoLink = common::getSysURL() . helper::createLink('story', 'view', "storyID={$story->id}");
+ $issue->description = $issue->description . "\n\n" . $zentaoLink;
+
+ return $issue;
+ }
+
+ /**
+ * Parse bug to issue.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param object $story
+ * @access public
+ * @return object
+ */
+ public function bugToIssue($gitlabID, $projectID, $bug)
+ {
+ $map = $this->config->gitlab->maps->bug;
+ $issue = new stdclass;
+ $gitlabUsers = $this->getUserAccountIdPairs($gitlabID);
+ if(empty($gitlabUsers)) return false;
+
+ foreach($map as $bugField => $config)
+ {
+ $value = '';
+ list($field, $optionType, $options) = explode('|', $config);
+ if($optionType == 'field') $value = $bug->$bugField;
+ if($optionType == 'fields') $value = $bug->$bugField . "\n\n" . $bug->$options;
+ if($optionType == 'userPairs')
+ {
+ $value = zget($gitlabUsers, $bug->$bugField);
+ }
+ if($optionType == 'configItems')
+ {
+ $value = zget($this->config->gitlab->$options, $bug->$bugField, '');
+ }
+ if($value) $issue->$field = $value;
+ }
+
+ if($issue->assignee_id == 'closed') unset($issue->assignee_id);
+
+ /* issue->state is null when creating it, we should put status_event when updating it. */
+ if(isset($issue->state) and $issue->state == 'closed') $issue->state_event='close';
+ if(isset($issue->state) and $issue->state == 'opened') $issue->state_event='reopen';
+
+ /* Append this object link in zentao to gitlab issue description */
+ $zentaoLink = common::getSysURL() . helper::createLink('bug', 'view', "bugID={$bug->id}");
+ $issue->description = $issue->description . "\n\n" . $zentaoLink;
+
+ return $issue;
+ }
+
+ /**
+ * Parse zentao object to issue. object can be task, bug and story.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param string $objectType
+ * @param object $object
+ * @access public
+ * @return object
+ */
+ public function parseObjectToIssue($gitlabID, $projectID, $objectType, $object)
+ {
+ $gitlabUsers = $this->getUserAccountIdPairs($gitlabID);
+ if(empty($gitlabUsers)) return false;
+ $issue = new stdclass;
+ $map = $this->config->gitlab->maps->$objectType;
+ foreach($map as $objectField => $config)
+ {
+ $value = '';
+ list($field, $optionType, $options) = explode('|', $config);
+ if($optionType == 'field') $value = $object->$objectField;
+ if($optionType == 'fields') $value = $object->$objectField . "\n\n" . $object->$options;
+ if($optionType == 'userPairs')
+ {
+ $value = zget($gitlabUsers, $object->$objectField);
+ }
+ if($optionType == 'configItems')
+ {
+ $value = zget($this->config->gitlab->$options, $object->$objectField, '');
+ }
+ if($value) $issue->$field = $value;
+ }
+ if(isset($issue->assignee_id) and $issue->assignee_id == 'closed') unset($issue->assignee_id);
+
+ /* issue->state is null when creating it, we should put status_event when updating it. */
+ if(isset($issue->state) and $issue->state == 'closed') $issue->state_event='close';
+ if(isset($issue->state) and $issue->state == 'opened') $issue->state_event='reopen';
+
+ /* Append this object link in zentao to gitlab issue description */
+ $zentaoLink = common::getSysURL() . helper::createLink($objectType, 'view', "id={$object->id}");
+ if(strpos($issue->description, $zentaoLink) == false) $issue->description = $issue->description . "\n\n" . $zentaoLink;
+
+ return $issue;
+ }
+
+ /**
+ * Parse issue to zentao object.
+ *
+ * @param object $issue
+ * @param int $gitlabID
+ * @access public
+ * @return object
+ */
+ public function issueToZentaoObject($issue, $gitlabID, $changes = null)
+ {
+ if(!isset($this->config->gitlab->maps->{$issue->objectType})) return null;
+
+ if(isset($changes->assignees)) $changes->assignee_id = true;
+ $maps = $this->config->gitlab->maps->{$issue->objectType};
+ $gitlabUsers = $this->getUserIdAccountPairs($gitlabID);
+
+ $object = new stdclass;
+ $object->id = $issue->objectID;
+ foreach($maps as $zentaoField => $config)
+ {
+ $value = '';
+ list($gitlabField, $optionType, $options) = explode('|', $config);
+ if(!isset($changes->$gitlabField) and $object->id != 0) continue;
+ if($optionType == 'field') $value = $issue->$gitlabField;
+ if($optionType == 'fields') $value = $issue->$gitlabField; // TODO(dingguodong) not implemented.
+ if($options == 'date') $value = $value ? date('Y-m-d', strtotime($value)) : '0000-00-00';
+ if($options == 'datetime') $value = $value ? date('Y-m-d H:i:s', strtotime($value)) : '0000-00-00 00:00:00';
+ if($optionType == 'userPairs' and isset($issue->$gitlabField)) $value = zget($gitlabUsers, $issue->$gitlabField);
+ if($optionType == 'configItems' and isset($issue->$gitlabField)) $value = array_search($issue->$gitlabField, $this->config->gitlab->$options);
+ if($value) $object->$zentaoField = $value;
+
+ if($gitlabField == "description") $object->$zentaoField .= "
" . $issue->web_url;
+ }
+ return $object;
+ }
+}
diff --git a/module/gitlab/view/binduser.html.php b/module/gitlab/view/binduser.html.php
new file mode 100644
index 0000000000..a0cf0dd17f
--- /dev/null
+++ b/module/gitlab/view/binduser.html.php
@@ -0,0 +1,63 @@
+
+ * @package story
+ * @version $Id$
+ * @link http://www.zentao.net
+ */
+?>
+
+
+
+
gitlab->bindUser;?>
+
+
+
+
diff --git a/module/gitlab/view/browse.html.php b/module/gitlab/view/browse.html.php
new file mode 100644
index 0000000000..dd3311b1dd
--- /dev/null
+++ b/module/gitlab/view/browse.html.php
@@ -0,0 +1,53 @@
+
+ * @package gitlab
+ * @version $Id$
+ * @link http://www.zentao.net
+ */
+?>
+
+
+
+ " . $lang->gitlab->create, '', "class='btn btn-primary'");?>
+
+
+
+
diff --git a/module/gitlab/view/create.html.php b/module/gitlab/view/create.html.php
new file mode 100644
index 0000000000..1675041925
--- /dev/null
+++ b/module/gitlab/view/create.html.php
@@ -0,0 +1,48 @@
+
+ * @package gitlab
+ * @version $Id$
+ * @link http://www.zentao.net
+ */
+?>
+
+
+
+
+
+
+
gitlab->lblCreate;?>
+
+
+
+
+
+
diff --git a/module/gitlab/view/edit.html.php b/module/gitlab/view/edit.html.php
new file mode 100644
index 0000000000..81d37d99f6
--- /dev/null
+++ b/module/gitlab/view/edit.html.php
@@ -0,0 +1,47 @@
+
+ * @package gitlab
+ * @version $Id$
+ * @link http://www.zentao.net
+ */
+?>
+
+
+
+
+
+
gitlab->edit; ?>
+
+
+
+
+
+
diff --git a/module/gitlab/view/importissue.html.php b/module/gitlab/view/importissue.html.php
new file mode 100644
index 0000000000..98ca018d8f
--- /dev/null
+++ b/module/gitlab/view/importissue.html.php
@@ -0,0 +1,57 @@
+
+ * @package story
+ * @version $Id$
+ * @link http://www.zentao.net
+ */
+?>
+
+
+
+
gitlab->importIssue;?>
+
+
+
+
+ gitlab->noImportableIssues; ?>
+
+
+
diff --git a/module/jenkins/control.php b/module/jenkins/control.php
index 70c030d2a4..1a77a02d27 100644
--- a/module/jenkins/control.php
+++ b/module/jenkins/control.php
@@ -111,7 +111,7 @@ class jenkins extends control
{
if($confim != 'yes') die(js::confirm($this->lang->jenkins->confirmDelete, inlink('delete', "id=$id&confirm=yes")));
- $this->jenkins->delete(TABLE_JENKINS, $id);
+ $this->jenkins->delete(TABLE_PIPELINE, $id);
die(js::reload('parent'));
}
diff --git a/module/jenkins/model.php b/module/jenkins/model.php
index 2d12469a3f..48eaa13a1b 100644
--- a/module/jenkins/model.php
+++ b/module/jenkins/model.php
@@ -21,10 +21,7 @@ class jenkinsModel extends model
*/
public function getByID($id)
{
- $jenkins = $this->dao->select('*')->from(TABLE_JENKINS)->where('id')->eq($id)->fetch();
- $jenkins->password = base64_decode($jenkins->password);
-
- return $jenkins;
+ return $this->loadModel('pipeline')->getByID($id);
}
/**
@@ -37,11 +34,7 @@ class jenkinsModel extends model
*/
public function getList($orderBy = 'id_desc', $pager = null)
{
- return $this->dao->select('*')->from(TABLE_JENKINS)
- ->where('deleted')->eq('0')
- ->orderBy($orderBy)
- ->page($pager)
- ->fetchAll('id');
+ return $this->loadModel('pipeline')->getList('jenkins', $orderBy, $pager);
}
/**
@@ -51,11 +44,7 @@ class jenkinsModel extends model
*/
public function getPairs()
{
- $jenkins = $this->dao->select('id,name')->from(TABLE_JENKINS)
- ->where('deleted')->eq('0')
- ->orderBy('id')->fetchPairs('id', 'name');
- $jenkins = array('' => '') + $jenkins;
- return $jenkins;
+ return $this->loadModel('pipeline')->getPairs('jenkins');
}
/**
@@ -94,21 +83,7 @@ class jenkinsModel extends model
*/
public function create()
{
- $jenkins = fixer::input('post')
- ->add('createdBy', $this->app->user->account)
- ->add('createdDate', helper::now())
- ->skipSpecial('url,token,account,password')
- ->get();
-
- $jenkins->password = base64_encode($jenkins->password);
-
- $this->dao->insert(TABLE_JENKINS)->data($jenkins)
- ->batchCheck($this->config->jenkins->create->requiredFields, 'notempty')
- ->batchCheck("url", 'URL')
- ->autoCheck()
- ->exec();
- if(dao::isError()) return false;
- return $this->dao->lastInsertId();
+ return $this->loadModel('pipeline')->create('jenkins');
}
/**
@@ -120,20 +95,6 @@ class jenkinsModel extends model
*/
public function update($id)
{
- $jenkins = fixer::input('post')
- ->add('editedBy', $this->app->user->account)
- ->add('editedDate', helper::now())
- ->skipSpecial('url,token,account,password')
- ->get();
-
- $jenkins->password = base64_encode($jenkins->password);
-
- $this->dao->update(TABLE_JENKINS)->data($jenkins)
- ->batchCheck($this->config->jenkins->edit->requiredFields, 'notempty')
- ->batchCheck("url", 'URL')
- ->autoCheck()
- ->where('id')->eq($id)
- ->exec();
- return !dao::isError();
+ return $this->loadModel('pipeline')->update($id);
}
}
diff --git a/module/job/model.php b/module/job/model.php
index 5d19e6c298..c948ada4e7 100644
--- a/module/job/model.php
+++ b/module/job/model.php
@@ -35,7 +35,7 @@ class jobModel extends model
{
return $this->dao->select('t1.*, t2.name as repoName, t3.name as jenkinsName')->from(TABLE_JOB)->alias('t1')
->leftJoin(TABLE_REPO)->alias('t2')->on('t1.repo=t2.id')
- ->leftJoin(TABLE_JENKINS)->alias('t3')->on('t1.jkHost=t3.id')
+ ->leftJoin(TABLE_PIPELINE)->alias('t3')->on('t1.jkHost=t3.id')
->where('t1.deleted')->eq('0')
->orderBy($orderBy)
->page($pager)
@@ -290,7 +290,7 @@ class jobModel extends model
{
$job = $this->dao->select('t1.id,t1.name,t1.product,t1.repo,t1.jkJob,t1.triggerType,t1.atTime,t1.customParam,t2.name as jenkinsName,t2.url,t2.account,t2.token,t2.password')
->from(TABLE_JOB)->alias('t1')
- ->leftJoin(TABLE_JENKINS)->alias('t2')->on('t1.jkHost=t2.id')
+ ->leftJoin(TABLE_PIPELINE)->alias('t2')->on('t1.jkHost=t2.id')
->where('t1.id')->eq($id)
->fetch();
if(!$job) return false;
diff --git a/module/pipeline/config.php b/module/pipeline/config.php
new file mode 100644
index 0000000000..29bc37b2ea
--- /dev/null
+++ b/module/pipeline/config.php
@@ -0,0 +1,5 @@
+pipeline->create = new stdclass();
+$config->pipeline->edit = new stdclass();
+$config->pipeline->create->requiredFields = 'name,url,type';
+$config->pipeline->edit->requiredFields = 'name,url,type';
diff --git a/module/pipeline/lang/zh-cn.php b/module/pipeline/lang/zh-cn.php
new file mode 100644
index 0000000000..b949e9998d
--- /dev/null
+++ b/module/pipeline/lang/zh-cn.php
@@ -0,0 +1,7 @@
+pipeline->id = 'ID';
+$lang->pipeline->name = '名称';
+$lang->pipeline->url = '服务地址';
+$lang->pipeline->token = 'Token';
+$lang->pipeline->account = '用户名';
+$lang->pipeline->password = '密码';
diff --git a/module/pipeline/model.php b/module/pipeline/model.php
new file mode 100644
index 0000000000..5e7fdba12e
--- /dev/null
+++ b/module/pipeline/model.php
@@ -0,0 +1,123 @@
+
+ * @package product
+ * @version $Id: $
+ * @link http://www.zentao.net
+ */
+
+class pipelineModel extends model
+{
+ /**
+ * Get a pipeline by id.
+ *
+ * @param int $id
+ * @access public
+ * @return object
+ */
+ public function getByID($id)
+ {
+ $pipeline = $this->dao->select('*')->from(TABLE_PIPELINE)->where('id')->eq($id)->fetch();
+ if($pipeline)
+ {
+ $pipeline->password = base64_decode($pipeline->password);
+ }
+ return $pipeline;
+ }
+
+ /**
+ * Get pipeline list.
+ *
+ * @param string $type jenkins|gitlab
+ * @param string $orderBy
+ * @param object $pager
+ * @access public
+ * @return array
+ */
+ public function getList($type = 'jenkins', $orderBy = 'id_desc', $pager = null)
+ {
+ return $this->dao->select('*')->from(TABLE_PIPELINE)
+ ->where('deleted')->eq('0')
+ ->AndWhere('type')->eq($type)
+ ->orderBy($orderBy)
+ ->page($pager)
+ ->fetchAll('id');
+ }
+
+ /**
+ * Get pipeline pairs
+ *
+ * @return array
+ */
+ public function getPairs($type)
+ {
+ $pipeline = $this->dao->select('id,name')->from(TABLE_PIPELINE)
+ ->where('deleted')->eq('0')
+ ->AndWhere('type')->eq($type)
+ ->orderBy('id')->fetchPairs('id', 'name');
+ $pipeline = array('' => '') + $pipeline;
+ return $pipeline;
+ }
+
+ /**
+ * Create a pipeline.
+ *
+ * @access public
+ * @return bool
+ */
+ public function create($type)
+ {
+ $pipeline = fixer::input('post')
+ ->add('type', $type)
+ ->add('private',md5(rand(10,113450)))
+ ->add('createdBy', $this->app->user->account)
+ ->add('createdDate', helper::now())
+ ->skipSpecial('url,token,account,password')
+ ->get();
+ if($type == 'gitlab') $pipeline->url = rtrim($pipeline->url, '/');
+
+ if(isset($pipeline->password)) $pipeline->password = base64_encode($pipeline->password);
+
+ $this->dao->insert(TABLE_PIPELINE)->data($pipeline)
+ ->batchCheck($this->config->pipeline->create->requiredFields, 'notempty')
+ ->batchCheck("url", 'URL')
+ ->autoCheck()
+ ->exec();
+ if(dao::isError()) return false;
+
+ return $this->dao->lastInsertId();
+ }
+
+ /**
+ * Update a pipeline.
+ *
+ * @param int $id
+ * @access public
+ * @return bool
+ */
+ public function update($id)
+ {
+ $pipeline = fixer::input('post')
+ ->add('editedBy', $this->app->user->account)
+ ->add('editedDate', helper::now())
+ ->skipSpecial('url,token,account,password')
+ ->get();
+
+ $type = $this->dao->select('type')->from(TABLE_PIPELINE)->where('id')->eq($id)->fetch('type');
+ if($type == 'gitlab') $pipeline->url = rtrim($pipeline->url, '/');
+ if(isset($pipeline->password)) $pipeline->password = base64_encode($pipeline->password);
+
+ $this->dao->update(TABLE_PIPELINE)->data($pipeline)
+ ->batchCheck($this->config->pipeline->edit->requiredFields, 'notempty')
+ ->batchCheck("url", 'URL')
+ ->autoCheck()
+ ->where('id')->eq($id)
+ ->exec();
+
+ return !dao::isError();
+ }
+}
diff --git a/module/repo/control.php b/module/repo/control.php
index 4a747571c8..c12c786b4e 100644
--- a/module/repo/control.php
+++ b/module/repo/control.php
@@ -129,11 +129,12 @@ class repo extends control
$this->app->loadLang('action');
- $this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->create;
- $this->view->position[] = $this->lang->repo->create;
- $this->view->groups = $this->loadModel('group')->getPairs();
- $this->view->users = $this->loadModel('user')->getPairs('noletter|noempty|nodeleted');
- $this->view->products = $this->loadModel('product')->getProductPairsByProject($objectID);
+ $this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->create;
+ $this->view->position[] = $this->lang->repo->create;
+ $this->view->groups = $this->loadModel('group')->getPairs();
+ $this->view->users = $this->loadModel('user')->getPairs('noletter|noempty|nodeleted');
+ $this->view->products = $this->loadModel('product')->getProductPairsByProject($objectID);
+ $this->view->gitlabHosts = $this->loadModel('gitlab')->getPairs();
$this->display();
}
@@ -168,21 +169,24 @@ class repo extends control
if($repo->SCM == 'Gitlab')
{
- $projects = $this->repo->getGitlabProjects($repo->client, $repo->password);
+ $projects = $this->loadModel('gitlab')->apiGetProjects($repo->gitlab, $repo->password);
+
$options = array();
- foreach($projects as $project) $options[$project->id] = $project->name . ':' . $project->http_url_to_repo;
+ foreach($projects as $project) $options[$project->id] = $project->name_with_namespace;
+
$this->view->projects = $options;
}
- $repo->repoType = $repo->id . '-' . $repo->SCM;
- $this->view->repo = $repo;
- $this->view->repoID = $repoID;
- $this->view->objectID = $objectID;
- $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->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->edit;
+ $repo->repoType = $repo->id . '-' . $repo->SCM;
+ $this->view->repo = $repo;
+ $this->view->repoID = $repoID;
+ $this->view->objectID = $objectID;
+ $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->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->edit;
$this->view->position[] = html::a(inlink('maintain'), $this->lang->repo->common);
$this->view->position[] = $this->lang->repo->edit;
@@ -205,6 +209,9 @@ class repo extends control
die(js::confirm($this->lang->repo->notice->delete, $this->repo->createLink('delete', "repoID=$repoID&objectID=$objectID&confirm=yes")));
}
+ /* Delete project relation for gitlab type. */
+ $this->loadModel('gitlab')->deleteProjectRelation($repoID);
+
$relationID = $this->dao->select('id')->from(TABLE_RELATION)->where('extra')->eq($repoID)->fetch();
if($relationID)
{
@@ -1105,25 +1112,24 @@ class repo extends control
/**
* Ajax get gitlab projects.
*
- * @param string $host
- * @param string $token
+ * @param int $host
* @access public
* @return void
*/
- public function ajaxGetGitlabProjects($host, $token)
+ public function ajaxGetGitlabProjects($host, $projectIdList = '')
{
- $host = helper::safe64Decode($host);
- $projects = $this->repo->getGitlabProjects($host, $token);
-
- if(!$projects) $this->send(array('message' => array()));
-
- $options = "";
- foreach($projects as $project)
- {
- $options .= "";
- }
- die($options);
- }
+ $projects = $this->loadModel('gitlab')->apiGetProjects($host);
+
+ if(!$projects) $this->send(array('message' => array()));
+ $projectIdList = $projectIdList ? explode(',', $projectIdList) : null;
+ $options = "";
+ foreach($projects as $project)
+ {
+ if(!empty($projectIdList) and $project and !in_array($project->id, $projectIdList)) continue;
+ $options .= "";
+ }
+ die($options);
+ }
/**
* Ajax get branch drop menu.
diff --git a/module/repo/js/create.js b/module/repo/js/create.js
index 0af0d52404..2b9bff72e5 100644
--- a/module/repo/js/create.js
+++ b/module/repo/js/create.js
@@ -7,12 +7,11 @@ $(function()
$form.css('min-height', $form.height());
})
- $('#gitlabHost, #gitlabToken').change(function()
+ $('#gitlabHost').change(function()
{
- host = Base64.encode($('#gitlabHost').val());
- token = $('#gitlabToken').val();
- url = createLink('repo', 'ajaxgetgitlabprojects', "host=" + host + '&token=' + token);
- if(host == '' || token == '') return false;
+ host = $('#gitlabHost').val();
+ url = createLink('repo', 'ajaxgetgitlabprojects', "host=" + host);
+ if(host == '') return false;
$.get(url, function(response)
{
diff --git a/module/repo/js/edit.js b/module/repo/js/edit.js
index cc79cbb2a5..e500686f04 100644
--- a/module/repo/js/edit.js
+++ b/module/repo/js/edit.js
@@ -7,12 +7,11 @@ $(function()
$form.css('min-height', $form.height());
})
- $('#gitlabHost, #gitlabToken').change(function()
+ $('#gitlabHost').change(function()
{
- host = Base64.encode($('#gitlabHost').val());
- token = $('#gitlabToken').val();
- url = createLink('repo', 'ajaxgetgitlabprojects', "host=" + host + '&token=' + token);
- if(host == '' || token == '') return false;
+ host = $('#gitlabHost').val();
+ if(host == '') return false;
+ url = createLink('repo', 'ajaxgetgitlabprojects', "host=" + host);
$.get(url, function(response)
{
@@ -28,7 +27,6 @@ $(function()
$('#name').val($option.data('name'));
$(this).chosen().trigger("chosen:updated");
});
-
});
function scmChanged(scm)
diff --git a/module/repo/lang/zh-cn.php b/module/repo/lang/zh-cn.php
index 25fbf8e9a0..8dbdf28398 100644
--- a/module/repo/lang/zh-cn.php
+++ b/module/repo/lang/zh-cn.php
@@ -178,8 +178,9 @@ $lang->repo->error->encoding = "编码可能错误,请更换编码重试
$lang->repo->error->deleted = "删除版本库失败,当前版本库有提交记录与设计关联";
$lang->repo->error->clientPath = "客户端安装目录不能有空格!";
-$lang->repo->syncTips = '请参照这里,设置版本库定时同步。';
-$lang->repo->encodingsTips = "提交日志的编码,可以用逗号连接起来的多个,比如utf-8。";
+$lang->repo->syncTips = '请参照这里,设置版本库定时同步。';
+$lang->repo->encodingsTips = "提交日志的编码,可以用逗号连接起来的多个,比如utf-8。";
+$lang->repo->pathTipsForGitlab = "GitLab 项目URL";
$lang->repo->example = new stdclass();
$lang->repo->example->client = new stdclass();
diff --git a/module/repo/model.php b/module/repo/model.php
index 864987d800..7be326bdc2 100644
--- a/module/repo/model.php
+++ b/module/repo/model.php
@@ -128,7 +128,8 @@ class repoModel extends model
$repos = $this->dao->select('*')->from(TABLE_REPO)
->where('deleted')->eq('0')
->orderBy($orderBy)
- ->page($pager)->fetchAll('id');
+ ->page($pager)
+ ->fetchAll('id');
/* Get products. */
$productIdList = $this->loadModel('product')->getProductIDByProject($projectID, false);
@@ -152,6 +153,8 @@ class repoModel extends model
if(!$hasPriv) unset($repos[$i]);
}
}
+
+ if($repo->SCM == 'Gitlab') $repo = $this->processGitlab($repo);
}
return $repos;
@@ -195,15 +198,16 @@ class repoModel extends model
if(!$this->checkConnection()) return false;
$data = fixer::input('post')
- ->setIf($this->post->SCM == 'Gitlab', 'password', $this->post->gitlabToken)
+ ->setIf($this->post->SCM == 'Gitlab', 'password', '')
+ ->setIf($this->post->SCM == 'Gitlab', 'path', $this->post->gitlabProject)
->setIf($this->post->SCM == 'Gitlab', 'client', $this->post->gitlabHost)
- ->setIf($this->post->SCM == 'Gitlab', 'extra', $this->post->gitlabProject)
->skipSpecial('path,client,account,password')
->setDefault('product', '')
->join('product', ',')
->get();
- if($this->post->SCM == 'Gitlab') $data->path = sprintf($this->config->repo->gitlab->apiPath, $data->gitlabHost, $this->post->gitlabProject);
+ /* see this file in 1783G: processGitlab::$repo->path */
+ if($this->post->SCM == 'Gitlab') $data->path = $this->post->gitlabProject;
$data->acl = empty($data->acl) ? '' : json_encode($data->acl);
@@ -226,6 +230,13 @@ class repoModel extends model
if(!dao::isError()) $this->rmClientVersionFile();
+ if($this->post->SCM == 'Gitlab')
+ {
+ $this->loadModel("gitlab")->saveProjectRelation($this->post->product, $this->post->gitlabHost, $this->post->gitlabProject);
+
+ /* create webhook for zentao */
+ $this->loadModel("gitlab")->initWebhooks($this->post->product, $this->post->gitlabHost, $this->post->gitlabProject);
+ }
return $this->dao->lastInsertID();
}
@@ -241,9 +252,9 @@ class repoModel extends model
$repo = $this->getRepoByID($id);
$data = fixer::input('post')
- ->setIf($this->post->SCM == 'Gitlab', 'password', $this->post->gitlabToken)
+ ->setIf($this->post->SCM == 'Gitlab', 'password', '')
+ ->setIf($this->post->SCM == 'Gitlab', 'path', $this->post->gitlabProject)
->setIf($this->post->SCM == 'Gitlab', 'client', $this->post->gitlabHost)
- ->setIf($this->post->SCM == 'Gitlab', 'extra', $this->post->gitlabProject)
->setDefault('client', 'svn')
->setDefault('prefix', $repo->prefix)
->setDefault('product', '')
@@ -256,6 +267,7 @@ class repoModel extends model
$data->path = sprintf($this->config->repo->gitlab->apiPath, $data->gitlabHost, $this->post->gitlabProject);
$data->prefix = '';
}
+
if($data->path != $repo->path) $data->synced = 0;
$data->acl = empty($data->acl) ? '' : json_encode($data->acl);
@@ -276,6 +288,7 @@ class repoModel extends model
if($data->client != $repo->client and !$this->checkClient()) return false;
if(!$this->checkConnection()) return false;
+
if($data->encrypt == 'base64') $data->password = base64_encode($data->password);
$this->dao->update(TABLE_REPO)->data($data, $skip = 'gitlabHost,gitlabToken,gitlabProject')
->batchCheck($this->config->repo->edit->requiredFields, 'notempty')
@@ -286,12 +299,15 @@ class repoModel extends model
$this->rmClientVersionFile();
+ if($repo->SCM == 'Gitlab') $this->loadModel("gitlab")->saveProjectRelation($this->post->product, $this->post->gitlabHost, $this->post->gitlabProject);
if($repo->path != $data->path)
{
$this->dao->delete()->from(TABLE_REPOHISTORY)->where('repo')->eq($id)->exec();
$this->dao->delete()->from(TABLE_REPOFILES)->where('repo')->eq($id)->exec();
- return false;
+ if($repo->SCM == 'Gitlab') $this->loadModel("gitlab")->initWebhooks($this->post->product, $this->post->gitlabHost, $this->post->gitlabProject);
+ return false;
}
+
return true;
}
@@ -375,6 +391,7 @@ class repoModel extends model
if(!$repo) return false;
if($repo->encrypt == 'base64') $repo->password = base64_decode($repo->password);
+ if($repo->SCM == 'Gitlab') $reps = $this->processGitlab($repo);
$repo->acl = json_decode($repo->acl);
return $repo;
}
@@ -1750,4 +1767,16 @@ class repoModel extends model
return $allResults;
}
+
+ public function processGitlab($repo)
+ {
+ $gitlab = $this->loadModel('gitlab')->getByID($repo->client);
+ if(!$gitlab) return $repo;
+ $repo->gitlab = $gitlab->id;
+ $repo->project = $repo->path;
+ $repo->path = sprintf($this->config->repo->gitlab->apiPath, $gitlab->url, $repo->path);
+ $repo->client = $gitlab->url;
+ $repo->password = $gitlab->token;
+ return $repo;
+ }
}
diff --git a/module/repo/view/create.html.php b/module/repo/view/create.html.php
index 446da06b4e..49a7a42c35 100644
--- a/module/repo/view/create.html.php
+++ b/module/repo/view/create.html.php
@@ -34,11 +34,7 @@
| repo->gitlabHost;?> |
- repo->placeholder->gitlabHost}'");?>
- |
-
- | repo->gitlabToken;?> |
-
+ | repo->placeholder->gitlabHost}'");?>
|
| repo->gitlabProject;?> |
diff --git a/module/repo/view/edit.html.php b/module/repo/view/edit.html.php
index 15f6d76846..cda78336be 100644
--- a/module/repo/view/edit.html.php
+++ b/module/repo/view/edit.html.php
@@ -34,21 +34,15 @@
| repo->type; ?> |
repo->scmList, $repo->SCM, "onchange='scmChanged(this.value)' class='form-control'"); ?> |
-
- repo->syncTips; ?>
- |
+ repo->syncTips; ?> |
| repo->gitlabHost;?> |
- client, "class='form-control' placeholder='{$lang->repo->placeholder->gitlabHost}'");?>
- |
-
- | repo->gitlabToken;?> |
- password, "class='form-control'");?>
+ | gitlab, "class='form-control' placeholder='{$lang->repo->placeholder->gitlabHost}'");?>
|
| repo->gitlabProject;?> |
- extra, "class='form-control chosen'");?>
+ | project, "class='form-control chosen'");?>
|
| repo->name; ?> |
@@ -59,8 +53,8 @@
repo->path; ?> |
path, "class='form-control'"); ?> |
- repo->example->path->git;?>
- repo->example->path->svn;?>
+ repo->example->path->git;?>
+ repo->example->path->svn;?>
|
@@ -72,8 +66,8 @@
| repo->client;?> |
client, "class='form-control'")?> |
- repo->example->client->git;?>
- repo->example->client->svn;?>
+ repo->example->client->git;?>
+ repo->example->client->svn;?>
|
diff --git a/module/repo/view/maintain.html.php b/module/repo/view/maintain.html.php
index 2cb0aced53..cd358fb3ce 100644
--- a/module/repo/view/maintain.html.php
+++ b/module/repo/view/maintain.html.php
@@ -25,7 +25,7 @@
| repo->id); ?> |
repo->type); ?> |
repo->name); ?> |
- repo->product); ?> |
+ repo->product); ?> |
repo->path; ?> |
actions; ?> |
@@ -45,10 +45,11 @@
}
?>
-
path; ?> |
+
path; ?> |
id&objectID=$objectID", '', 'list', 'edit');
+ common::printIcon('repo', 'edit', "repoID=$repo->id&objectID=$objectID", '', 'list', 'edit');
+ if(strtolower($repo->SCM) == "gitlab") common::printIcon('gitlab', 'importIssue', "repo={$repo->id}", '', 'list', 'link'); // disable button instead of hiding it.
if(common::hasPriv('repo', 'delete')) echo html::a($this->createLink('repo', 'delete', "repoID=$repo->id&objectID=$objectID"), '', 'hiddenwin', "title='{$lang->repo->delete}' class='btn'");
?>
|
diff --git a/module/story/config.php b/module/story/config.php
index 8e63ffc7d6..8150edbb1c 100644
--- a/module/story/config.php
+++ b/module/story/config.php
@@ -4,10 +4,10 @@ $config->story = new stdclass();
$config->story->batchCreate = 10;
$config->story->affectedFixedNum = 7;
$config->story->needReview = 1;
+$config->story->removeFields = 'objectTypeList,productList,executionList,gitlabID,gitlabProjectID,execution';
$config->story->batchClose = new stdclass();
$config->story->batchClose->columns = 10;
-
$config->story->create = new stdclass();
$config->story->edit = new stdclass();
$config->story->change = new stdclass();
diff --git a/module/story/control.php b/module/story/control.php
index 25754add29..54b879c137 100644
--- a/module/story/control.php
+++ b/module/story/control.php
@@ -295,6 +295,13 @@ class story extends control
$this->view->customFields = $customFields;
$this->view->showFields = $this->config->story->custom->createFields;
+ $allGitlabs = $this->loadModel('gitlab')->getPairs();
+ $executionID = $objectID;
+ $gitlabProjects = $this->loadModel('gitlab')->getProjectsByExecution($executionID);
+ foreach($allGitlabs as $id => $name) if($id and !isset($gitlabProjects[$id])) unset($allGitlabs[$id]);
+ $this->view->gitlabList = $allGitlabs;
+ $this->view->gitlabProjects = $gitlabProjects;
+
$this->view->title = $product->name . $this->lang->colon . $this->lang->story->create;
$this->view->position[] = html::a($this->createLink('product', 'browse', "product=$productID&branch=$branch"), $product->name);
$this->view->position[] = $this->lang->story->common;
@@ -983,6 +990,10 @@ class story extends control
}
else
{
+ /* Delete related issue in gitlab. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ if(!empty($relation)) $this->loadModel('gitlab')->deleteIssue('story', $storyID, $relation->issueID);
+
$this->story->delete(TABLE_STORY, $storyID);
if($story->parent > 0)
{
diff --git a/module/story/js/create.js b/module/story/js/create.js
index 473c38bd5f..4f410a918b 100644
--- a/module/story/js/create.js
+++ b/module/story/js/create.js
@@ -34,3 +34,22 @@ function refreshPlan()
$(window).unload(function(){
if(blockID) window.parent.refreshBlock($('#block' + blockID));
});
+
+$(document).ready(function()
+{
+ $('#gitlab').change(function()
+ {
+ host = $('#gitlab').val();
+ if(host == '') return false;
+ projects = '';
+ $.each(gitlabProjects[host], function(id, obj){projects = projects + ',' + obj.gitlabProject});
+ url = createLink('repo', 'ajaxgetgitlabprojects', "host=" + host + "&projects=" + projects);
+
+ $.get(url, function(response)
+ {
+ $('#gitlabProject').html('').append(response);
+ $('#gitlabProject').chosen().trigger("chosen:updated");;
+ });
+
+ });
+});
diff --git a/module/story/lang/zh-cn.php b/module/story/lang/zh-cn.php
index e4629a5735..faefd42ec8 100644
--- a/module/story/lang/zh-cn.php
+++ b/module/story/lang/zh-cn.php
@@ -406,3 +406,5 @@ $lang->story->categoryList['safe'] = '安全';
$lang->story->categoryList['experience'] = '体验';
$lang->story->categoryList['improve'] = '改进';
$lang->story->categoryList['other'] = '其他';
+
+$lang->story->sync2Gitlab = '同步到Gitlab';
diff --git a/module/story/model.php b/module/story/model.php
index 9b3143ad49..98d1cc5b20 100644
--- a/module/story/model.php
+++ b/module/story/model.php
@@ -217,7 +217,7 @@ class storyModel extends model
/* Check repeat story. */
$result = $this->loadModel('common')->removeDuplicate('story', $story, "product={$story->product}");
- if($result['stop']) return array('status' => 'exists', 'id' => $result['duplicate']);
+ if(isset($result['stop']) and $result['stop']) return array('status' => 'exists', 'id' => $result['duplicate']);
if($this->checkForceReview()) $story->status = 'draft';
if($story->status == 'draft') $story->stage = $this->post->plan > 0 ? 'planned' : 'wait';
@@ -234,7 +234,7 @@ class storyModel extends model
$requiredFields = trim($requiredFields, ',');
- $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify')->autoCheck()->batchCheck($requiredFields, 'notempty')->exec();
+ $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify,gitlab,gitlabProject')->autoCheck()->batchCheck($requiredFields, 'notempty')->exec();
if(!dao::isError())
{
$storyID = $this->dao->lastInsertID();
@@ -340,11 +340,56 @@ class storyModel extends model
/* Callback the callable method to process the related data for object that is transfered to story. */
if($from && is_callable(array($this, $this->config->story->fromObjects[$from]['callback']))) call_user_func(array($this, $this->config->story->fromObjects[$from]['callback']), $storyID);
+ /* push this story to gitlab issue */
+ $object = $this->getByID($storyID);
+ $this->loadModel('gitlab')->apiCreateIssue($this->post->gitlab, $this->post->gitlabProject, 'story', $storyID, $object);
+
return array('status' => 'created', 'id' => $storyID);
}
return false;
}
+ /**
+ * Create story from gitlab issue.
+ *
+ * @param object $story
+ * @param int $executionID
+ * @access public
+ * @return int
+ */
+ public function createStoryFromGitlabIssue($story, $executionID)
+ {
+ $story->status = 'active';
+ $story->stage = 'projected';
+ $story->openedBy = $this->app->user->account;
+ $story->version = 1;
+ $story->assignedDate = isset($story->assignedTo) ? helper::now() : 0;
+
+ if(isset($story->execution)) unset($story->execution);
+
+ $requiredFields = $this->config->story->create->requiredFields;
+ $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify,gitlab,gitlabProject')->autoCheck()->batchCheck($requiredFields, 'notempty')->exec();
+ if(!dao::isError())
+ {
+ $storyID = $this->dao->lastInsertID();
+
+ $data = new stdclass();
+ $data->story = $storyID;
+ $data->version = 1;
+ $data->title = $story->title;
+ $data->spec = $story->spec;
+ $data->verify = $story->spec;
+ $this->dao->insert(TABLE_STORYSPEC)->data($data)->exec();
+
+ /* Link story to execution. */
+ $this->linkStory($executionID, $story->product, $storyID);
+
+ return $storyID;
+ }
+
+ return false;
+ }
+
/**
* Batch create stories.
*
@@ -442,7 +487,6 @@ class storyModel extends model
dao::$errors['message'][] = sprintf($this->lang->error->notempty, $this->lang->story->$field);
return false;
}
-
$data[$i] = $story;
}
@@ -654,6 +698,11 @@ class storyModel extends model
}
$this->file->updateObjectID($this->post->uid, $storyID, 'story');
+
+ /* update story to gitlab issue. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+
return common::createChanges($oldStory, $story);
}
}
@@ -833,6 +882,10 @@ class storyModel extends model
}
}
+ /* update story to gitlab issue. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ if($relation) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+
unset($oldStory->parent);
unset($story->parent);
return common::createChanges($oldStory, $story);
@@ -1163,6 +1216,10 @@ class storyModel extends model
/* Update story sort of plan when story plan has changed. */
if($oldStory->plan != $story->plan) $this->updateStoryOrderOfPlan($storyID, $story->plan, $oldStory->plan);
+ /* update story to gitlab issue. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ if(!empty($relation)) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+
$this->executeHooks($storyID);
if($story->type == 'story') $this->batchChangeStage(array($storyID), $story->stage);
if($story->closedReason == 'done') $this->loadModel('score')->create('story', 'close');
@@ -1409,7 +1466,7 @@ class storyModel extends model
$oldStory = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch();
$now = helper::now();
$story = fixer::input('post')
- ->add('assignedTo', 'closed')
+ ->add('assignedTo', 'closed')
->add('status', 'closed')
->add('stage', 'closed')
->setDefault('lastEditedBy', $this->app->user->account)
@@ -1428,6 +1485,14 @@ class storyModel extends model
->checkIF($story->closedReason == 'duplicate', 'duplicateStory', 'notempty')
->where('id')->eq($storyID)->exec();
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+
+ if(!empty($relation))
+ {
+ $currentIssue = $this->loadModel('gitlab')->apiGetSingleIssue($relation->gitlabID, $relation->projectID, $relation->issueID);
+ if($currentIssue->state != 'closed') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+ }
+
/* Update parent story status. */
if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent);
$this->setStage($storyID);
@@ -1494,6 +1559,13 @@ class storyModel extends model
if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent);
$this->setStage($storyID);
$allChanges[$storyID] = common::createChanges($oldStory, $story);
+
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ if(!empty($relation))
+ {
+ $currentIssue = $this->loadModel('gitlab')->apiGetSingleIssue($relation->gitlabID, $relation->projectID, $relation->issueID);
+ if($currentIssue->state != 'closed') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+ }
}
else
{
@@ -1769,8 +1841,13 @@ class storyModel extends model
$story->assignedTo = $assignedTo;
$story->assignedDate = $now;
- $this->dao->update(TABLE_STORY)->data($story)->autoCheck()->where('id')->eq((int)$storyID)->exec();
- if(!dao::isError()) return common::createChanges($oldStory, $story);
+ if(!dao::isError())
+ {
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ $story->assignee_id = $this->loadModel('gitlab')->getGitlabUserID($relation->gitlabID, $story->assignedTo);
+ if($story->assignee_id != '') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+ return common::createChanges($oldStory, $story);
+ }
return false;
}
@@ -1799,7 +1876,16 @@ class storyModel extends model
$story->assignedDate = $now;
$this->dao->update(TABLE_STORY)->data($story)->autoCheck()->where('id')->eq((int)$storyID)->exec();
- if(!dao::isError()) $allChanges[$storyID] = common::createChanges($oldStory, $story);
+ if(!dao::isError())
+ {
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ $story->assignee_id = $this->loadModel('gitlab')->getGitlabUserID($relation->gitlabID, $story->assignedTo);
+ if($story->assignee_id != '') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+
+ /* Push this story to gitlab issue. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ if(!empty($relation)) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+ }
}
return $allChanges;
}
@@ -1834,6 +1920,10 @@ class storyModel extends model
/* Update parent story status. */
if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent);
+ /* Push this story to gitlab issue. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('story', $storyID);
+ if(!empty($relation)) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'story', $story, $storyID);
+
return common::createChanges($oldStory, $story);
}
diff --git a/module/story/view/create.html.php b/module/story/view/create.html.php
index 8e46723c86..f09edc694b 100644
--- a/module/story/view/create.html.php
+++ b/module/story/view/create.html.php
@@ -17,6 +17,7 @@
story->placeholder); ?>
+
@@ -251,6 +252,14 @@
+
+
+ | story->sync2Gitlab;?> |
+ |
+ |
+
+
+
diff --git a/module/task/config.php b/module/task/config.php
index 5db5eb88ae..67fe4ffabc 100644
--- a/module/task/config.php
+++ b/module/task/config.php
@@ -26,6 +26,7 @@ $config->task->editor->activate = array('id' => 'comment', 'tools' => 'simpleToo
$config->task->editor->cancel = array('id' => 'comment', 'tools' => 'simpleTools');
$config->task->editor->pause = array('id' => 'comment', 'tools' => 'simpleTools');
+$config->task->removeFields = 'objectTypeList,productList,executionList,gitlabID,gitlabProjectID,product';
$config->task->exportFields = '
id, execution, module, story,
name, desc,
diff --git a/module/task/control.php b/module/task/control.php
index 93989a71fa..31f084cbf2 100644
--- a/module/task/control.php
+++ b/module/task/control.php
@@ -92,12 +92,14 @@ class task extends control
setcookie('lastTaskModule', (int)$this->post->module, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, false);
if($this->post->execution) $executionID = (int)$this->post->execution;
+
+ /* Create task here. */
$tasksID = $this->task->create($executionID);
if(dao::isError())
{
$response['result'] = 'fail';
$response['message'] = dao::getError();
- $this->send($response);
+ return $this->send($response);
}
/* if the count of tasksID is 1 then check exists. */
@@ -108,7 +110,7 @@ class task extends control
{
$response['locate'] = $this->createLink('task', 'view', "taskID={$taskID['id']}");
$response['message'] = sprintf($this->lang->duplicate, $this->lang->task->common);
- $this->send($response);
+ return $this->send($response);
}
}
@@ -132,41 +134,41 @@ class task extends control
$this->executeHooks($taskID);
/* Return task id when call the API. */
- if($this->viewType == 'json') $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'id' => $taskID));
+ if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'id' => $taskID));
/* If link from no head then reload. */
- if(isonlybody()) $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent'));
+ if(isonlybody()) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent'));
/* Locate the browser. */
if($this->app->getViewType() == 'xhtml')
{
$taskLink = $this->createLink('task', 'view', "taskID=$taskID");
$response['locate'] = $taskLink;
- $this->send($response);
+ return $this->send($response);
}
if($this->post->after == 'continueAdding')
{
$response['message'] = $this->lang->task->successSaved . $this->lang->task->afterChoices['continueAdding'];
$response['locate'] = $this->createLink('task', 'create', "executionID=$executionID&storyID={$this->post->story}&moduleID=$moduleID");
- $this->send($response);
+ return $this->send($response);
}
elseif($this->post->after == 'toTaskList')
{
setcookie('moduleBrowseParam', 0, 0, $this->config->webRoot, '', $this->config->cookieSecure, false);
$taskLink = $this->createLink('execution', 'task', "executionID=$executionID&status=unclosed¶m=0&orderBy=id_desc");
$response['locate'] = $taskLink;
- $this->send($response);
+ return $this->send($response);
}
elseif($this->post->after == 'toStoryList')
{
$response['locate'] = $storyLink;
- $this->send($response);
+ return $this->send($response);
}
else
{
$response['locate'] = $taskLink;
- $this->send($response);
+ return $this->send($response);
}
}
@@ -215,6 +217,12 @@ class task extends control
foreach(explode(',', $this->config->task->customCreateFields) as $field) $customFields[$field] = $this->lang->task->$field;
if($execution->type == 'ops') unset($customFields['story']);
+ $allGitlabs = $this->loadModel('gitlab')->getPairs();
+ $gitlabProjects = $this->loadModel('gitlab')->getProjectsByExecution($executionID);
+ foreach($allGitlabs as $id => $name) if($id and !isset($gitlabProjects[$id])) unset($allGitlabs[$id]);
+ $this->view->gitlabList = $allGitlabs;
+ $this->view->gitlabProjects = $gitlabProjects;
+
$this->view->customFields = $customFields;
$this->view->showFields = $this->config->task->custom->createFields;
$this->view->showAllModule = $showAllModule;
@@ -230,6 +238,7 @@ class task extends control
$this->view->members = $members;
$this->view->blockID = $blockID;
$this->view->moduleOptionMenu = $moduleOptionMenu;
+
$this->display();
}
@@ -285,17 +294,17 @@ class task extends control
if(!empty($_POST))
{
$mails = $this->task->batchCreate($executionID);
- if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError()));
+ if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
$taskIDList = array();
foreach($mails as $mail) $taskIDList[] = $mail->taskID;
/* Return task id list when call the API. */
- if($this->viewType == 'json') $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $taskIDList));
+ if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $taskIDList));
/* Locate the browser. */
- if(!empty($iframe)) $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent'));
- $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $taskLink));
+ if(!empty($iframe)) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent'));
+ return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $taskLink));
}
/* Set Custom*/
@@ -372,7 +381,7 @@ class task extends control
if($comment == false)
{
$changes = $this->task->update($taskID);
- if(dao::isError()) die(js::error(dao::getError()));
+ if(dao::isError()) return print(js::error(dao::getError()));
$files = $this->loadModel('file')->saveUpload('task', $taskID);
}
@@ -399,9 +408,10 @@ class task extends control
}
}
}
+
if(defined('RUN_MODE') && RUN_MODE == 'api')
{
- die(array('status' => 'success', 'data' => $taskID));
+ return $this->send(array('status' => 'success', 'data' => $taskID));
}
else
{
@@ -552,7 +562,10 @@ class task extends control
{
$this->loadModel('action');
$changes = $this->task->assign($taskID);
+
+ if($this->viewType == 'json') return $this->send(array('result' => 'fail', 'message' => dao::getError()));
if(dao::isError()) die(js::error(dao::getError()));
+
$actionID = $this->action->create('task', $taskID, 'Assigned', $this->post->comment, $this->post->assignedTo);
$this->action->logHistory($actionID, $changes);
@@ -564,6 +577,9 @@ class task extends control
$task = $this->task->getByID($taskID);
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'task', $task, $taskID);
+
$members = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted');
/* Compute next assignedTo. */
@@ -743,7 +759,12 @@ class task extends control
{
$this->loadModel('action');
$changes = $this->task->start($taskID);
- if(dao::isError()) die(js::error(dao::getError()));
+
+ if(dao::isError())
+ {
+ if($this->viewType == 'json') return $this->send(array('result' => 'fail', 'message' => dao::getError()));
+ die(js::error(dao::getError()));
+ }
if($this->post->comment != '' or !empty($changes))
{
@@ -898,7 +919,11 @@ class task extends control
{
$this->loadModel('action');
$changes = $this->task->finish($taskID);
- if(dao::isError()) die(js::error(dao::getError()));
+ if(dao::isError())
+ {
+ if($this->viewType == 'json') return $this->send(array('result' => 'fail', 'message' => dao::getError()));
+ die(js::error(dao::getError()));
+ }
$files = $this->loadModel('file')->saveUpload('task', $taskID);
$task = $this->task->getById($taskID);
@@ -927,7 +952,7 @@ class task extends control
if(isonlybody()) die(js::closeModal('parent.parent', 'this'));
if(defined('RUN_MODE') && RUN_MODE == 'api')
{
- die(array('status' => 'success', 'data' => $taskID));
+ return $this->send(array('result' => 'success', 'data' => $taskID));
}
else
{
@@ -1264,14 +1289,18 @@ class task extends control
public function delete($executionID, $taskID, $confirm = 'no')
{
$task = $this->task->getById($taskID);
- if($task->parent < 0) die(js::alert($this->lang->task->cannotDeleteParent));
+ if($task->parent < 0) return print(js::alert($this->lang->task->cannotDeleteParent));
if($confirm == 'no')
{
- die(js::confirm($this->lang->task->confirmDelete, inlink('delete', "executionID=$executionID&taskID=$taskID&confirm=yes")));
+ return print(js::confirm($this->lang->task->confirmDelete, inlink('delete', "executionID=$executionID&taskID=$taskID&confirm=yes")));
}
else
{
+ /* Delete related issue in gitlab. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ if(!empty($relation)) $this->loadModel('gitlab')->deleteIssue('task', $taskID, $relation->issueID);
+
$this->task->delete(TABLE_TASK, $taskID);
if($task->parent > 0)
{
@@ -1283,7 +1312,7 @@ class task extends control
$this->executeHooks($taskID);
- die(js::locate($this->session->taskList, 'parent'));
+ return print(js::locate($this->session->taskList, 'parent'));
}
}
diff --git a/module/task/js/create.js b/module/task/js/create.js
index a0e2460ed1..f47496be8d 100644
--- a/module/task/js/create.js
+++ b/module/task/js/create.js
@@ -479,6 +479,23 @@ $(document).ready(function()
$('#moduleIdBox #module').val(moduleID).attr('onchange', "setStories(this.value, " + executionID + ")").chosen();
});
});
+
+ $('#gitlab').change(function()
+ {
+ host = $('#gitlab').val();
+ if(host == '') return false;
+ projects = '';
+ $.each(gitlabProjects[host], function(id, obj){projects = projects + ',' + obj.gitlabProject});
+ url = createLink('repo', 'ajaxgetgitlabprojects', "host=" + host + "&projects=" + projects);
+
+ $.get(url, function(response)
+ {
+ $('#gitlabProject').html('').append(response);
+ $('#gitlabProject').chosen().trigger("chosen:updated");;
+ });
+
+ });
+
});
$(document).on('click', '#testStory_chosen,#story_chosen', function()
diff --git a/module/task/lang/zh-cn.php b/module/task/lang/zh-cn.php
index ba81bcb2e5..a0119d0fe1 100644
--- a/module/task/lang/zh-cn.php
+++ b/module/task/lang/zh-cn.php
@@ -67,6 +67,7 @@ $lang->task->storyVersion = "{$lang->SRCommon}版本";
$lang->task->color = '标题颜色';
$lang->task->name = '任务名称';
$lang->task->type = '任务类型';
+$lang->task->sync2Gitlab = '同步到Gitlab';
$lang->task->pri = '优先级';
$lang->task->mailto = '抄送给';
$lang->task->estimate = '最初预计';
diff --git a/module/task/model.php b/module/task/model.php
index 381ec5cfc6..4d3f7b1f2b 100644
--- a/module/task/model.php
+++ b/module/task/model.php
@@ -22,6 +22,7 @@ class taskModel extends model
*/
public function create($executionID)
{
+
if($this->post->estimate < 0)
{
dao::$errors[] = $this->lang->task->error->recordMinus;
@@ -64,7 +65,7 @@ class taskModel extends model
->cleanINT('execution,story,module')
->stripTags($this->config->task->editor->create['id'], $this->config->allowedTags)
->join('mailto', ',')
- ->remove('after,files,labels,assignedTo,uid,storyEstimate,storyDesc,storyPri,team,teamEstimate,teamMember,multiple,teams,contactListMenu,selectTestStory,testStory,testPri,testEstStarted,testDeadline,testAssignedTo,testEstimate')
+ ->remove('after,files,labels,assignedTo,uid,storyEstimate,storyDesc,storyPri,team,teamEstimate,teamMember,multiple,teams,contactListMenu,selectTestStory,testStory,testPri,testEstStarted,testDeadline,testAssignedTo,testEstimate,sync')
->add('version', 1)
->get();
@@ -109,17 +110,21 @@ class taskModel extends model
/* Fix Bug #2466 */
if($this->post->multiple) $task->assignedTo = '';
- $this->dao->insert(TABLE_TASK)->data($task)
+ $this->dao->insert(TABLE_TASK)->data($task, $skip = 'gitlab,gitlabProject')
->autoCheck()
->batchCheck($requiredFields, 'notempty')
->checkIF($task->estimate != '', 'estimate', 'float')
->checkIF(!helper::isZeroDate($task->deadline), 'deadline', 'ge', $task->estStarted)
->exec();
-
+
if(dao::isError()) return false;
$taskID = $this->dao->lastInsertID();
+ /* Sync this task to gitlab issue. */
+ $object = $this->getByID($taskID);
+ $this->loadModel('gitlab')->apiCreateIssue($this->post->gitlab, $this->post->gitlabProject, 'task', $taskID, $object);
+
/* Mark design version.*/
if(isset($task->design) && !empty($task->design))
{
@@ -379,6 +384,11 @@ class taskModel extends model
if(dao::isError()) return false;
$taskID = $this->dao->lastInsertID();
+
+ /* Sync this task to gitlab issue. */
+ $object = $this->getByID($taskID);
+ $this->loadModel('gitlab')->apiCreateIssue($this->post->gitlab, $this->post->gitlabProject,'task', $taskID, $object);
+
$taskSpec = new stdClass();
$taskSpec->task = $taskID;
$taskSpec->version = $task->version;
@@ -447,6 +457,37 @@ class taskModel extends model
return $mails;
}
+ /**
+ * Create task from gitlab issue.
+ *
+ * @param object $task
+ * @param int $executionID
+ * @access public
+ * @return int
+ */
+ public function createTaskFromGitlabIssue($task, $executionID)
+ {
+ $task->version = 1;
+ $task->openedBy = $this->app->user->account;
+ $task->assignedDate = isset($task->assignedTo) ? helper::now() : 0;
+ $task->story = 0;
+ $task->module = 0;
+ $task->estimate = 0;
+ $task->estStarted = '0000-00-00';
+ $task->left = 1;
+ $task->type = 'devel';
+
+ $this->dao->insert(TABLE_TASK)->data($task, $skip = 'id,product')
+ ->autoCheck()
+ ->batchCheck($this->config->task->create->requiredFields, 'notempty')
+ ->checkIF(!helper::isZeroDate($task->deadline), 'deadline', 'ge', $task->estStarted)
+ ->exec();
+
+ if(dao::isError()) return false;
+
+ return $this->dao->lastInsertID();
+ }
+
/**
* Compute parent task working hours.
*
@@ -940,6 +981,10 @@ class taskModel extends model
->batchCheckIF($task->closedReason == 'cancel', 'finishedBy, finishedDate', 'empty')
->where('id')->eq((int)$taskID)->exec();
+ /* update task to gitlab issue. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ if(!empty($relation)) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'task', $task, $taskID);
+
if(!dao::isError())
{
/* Mark design version.*/
@@ -1173,6 +1218,14 @@ class taskModel extends model
$tasks[$taskID] = $task;
}
+ $issues = $this->loadModel('gitlab')->getIssueListByObjects('task', $taskID);
+ foreach($tasks as $taskID => $task)
+ {
+ $issue = zget($issues, $taskID, 0);
+ if(!$issue) continue;
+ $this->loadModel('gitlab')->apiUpdateIssue($issue->gitlabID, $issue->projectID, $issue->issueID, $task);
+ }
+
/* Check field not empty. */
foreach($tasks as $taskID => $task)
{
@@ -1242,6 +1295,10 @@ class taskModel extends model
if($task->status == 'done') $this->loadModel('score')->create('task', 'finish', $taskID);
if($task->status == 'closed') $this->loadModel('score')->create('task', 'close', $taskID);
$allChanges[$taskID] = common::createChanges($oldTask, $task);
+
+ /* update task to gitlab issue. */
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ if(!empty($relation)) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'task', $task, $taskID);
}
else
{
@@ -1327,6 +1384,10 @@ class taskModel extends model
->autoCheck()
->check('left', 'float')
->where('id')->eq($taskID)->exec();
+
+ $task = $this->getById($taskID);
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ if(!empty($relation)) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'task', $task, $taskID);
if(!dao::isError()) return common::createChanges($oldTask, $task);
}
@@ -1663,6 +1724,13 @@ class taskModel extends model
->where('id')->eq((int)$taskID)
->exec();
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ if(!empty($relation))
+ {
+ $currentIssue = $this->loadModel('gitlab')->apiGetSingleIssue($relation->gitlabID, $relation->projectID, $relation->issueID);
+ if($currentIssue->state != 'closed') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'task', $task, $taskID);
+ }
+
if($oldTask->parent > 0) $this->updateParentStatus($taskID);
if($oldTask->story) $this->loadModel('story')->setStage($oldTask->story);
if($task->status == 'done' && !dao::isError()) $this->loadModel('score')->create('task', 'finish', $taskID);
@@ -1719,6 +1787,13 @@ class taskModel extends model
$this->dao->update(TABLE_TASK)->data($task)->autoCheck()->where('id')->eq((int)$taskID)->exec();
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ if(!empty($relation))
+ {
+ $currentIssue = $this->loadModel('gitlab')->apiGetSingleIssue($relation->gitlabID, $relation->projectID, $relation->issueID);
+ if($currentIssue->state != 'closed') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'bug', $task, $taskID);
+ }
+
if(!dao::isError())
{
if($oldTask->parent > 0) $this->updateParentStatus($taskID);
@@ -1762,6 +1837,14 @@ class taskModel extends model
$this->dao->update(TABLE_TASK)->set('assignedTo=openedBy')->where('parent')->eq((int)$taskID)->exec();
}
if($oldTask->story) $this->loadModel('story')->setStage($oldTask->story);
+
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ if(!empty($relation))
+ {
+ $currentIssue = $this->loadModel('gitlab')->apiGetSingleIssue($relation->gitlabID, $relation->projectID, $relation->issueID);
+ if($currentIssue->state != 'closed') $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'task', $task, $taskID);
+ }
+
if(!dao::isError()) return common::createChanges($oldTask, $task);
}
@@ -1826,6 +1909,10 @@ class taskModel extends model
$this->computeWorkingHours($taskID);
}
if($oldTask->story) $this->loadModel('story')->setStage($oldTask->story);
+
+ $relation = $this->loadModel('gitlab')->getRelationByObject('task', $taskID);
+ if(!empty($relation)) $this->loadModel('gitlab')->apiUpdateIssue($relation->gitlabID, $relation->projectID, $relation->issueID, 'task', $task, $taskID);
+
if(!dao::isError()) return common::createChanges($oldTask, $task);
}
diff --git a/module/task/view/create.html.php b/module/task/view/create.html.php
index 492f20a3d4..060395a804 100644
--- a/module/task/view/create.html.php
+++ b/module/task/view/create.html.php
@@ -15,6 +15,7 @@
id));?>
+
+
+
+ | task->sync2Gitlab;?> |
+ |
+ |
+
+
| task->afterSubmit;?> |
task->afterChoices, !empty($task->id) ? 'toTaskList' : 'continueAdding');?> |
diff --git a/module/upgrade/model.php b/module/upgrade/model.php
index 6d3111ef7a..2f5d6c5489 100644
--- a/module/upgrade/model.php
+++ b/module/upgrade/model.php
@@ -5082,4 +5082,30 @@ class upgradeModel extends model
$this->dao->delete()->from(TABLE_GROUP)->where('id')->in(array_keys($projectAdmins))->exec();
return true;
}
+
+ public function processGitlabRepo()
+ {
+ $repoList = $this->dao->select('*')->from(TABLE_REPO)->where('SCM')->eq('Gitlab')->fetchAll();
+ foreach($repoList as $repo)
+ {
+ /* Create gitlab from repo. */
+ $gitlab = new stdclass;
+ $gitlab->type = 'gitlab';
+ $gitlab->name = $repo->client;
+ $gitlab->token = base64_decode($repo->password);
+ $gitlab->private = md5(uniqid());
+ $this->dao->insert(TABLE_PIPELINE)->data($gitlab)->exec();
+
+ $gitlabID = $this->dao->lastInsertID();
+
+ preg_match('/projects\/(\d+)\/repository/', $repo->path, $matches);
+ $gitlabProject = $matches[1];
+ $products = explode(',', $repo->product);
+ $this->loadModel('gitlab')->saveProjectRelation($products, $gitlabID, $gitlabProject);
+
+ $this->dao->update(TABLE_REPO)->set('client')->eq($gitlabID)->set('path')->eq($gitlabProject)->where('id')->eq($repo->id)->exec();
+
+ $this->loadModel("gitlab")->initWebhooks($products, $gitlabID, $gitlabProject);
+ }
+ }
}
diff --git a/www/api.php b/www/api.php
index 6b299d2cce..a6d46cc923 100644
--- a/www/api.php
+++ b/www/api.php
@@ -18,7 +18,8 @@ define('RUN_MODE', 'api');
ob_start();
/* Load the framework. */
-include '../framework/router.class.php';
+include '../framework/api/router.class.php';
+include '../framework/api/entry.class.php';
include '../framework/control.class.php';
include '../framework/model.class.php';
include '../framework/helper.class.php';
@@ -27,13 +28,13 @@ include '../framework/helper.class.php';
$startTime = getTime();
/* Instance the app. */
-$app = router::createApp('pms', dirname(dirname(__FILE__)), 'router');
+$app = router::createApp('pms', dirname(dirname(__FILE__)), 'api');
/* Run the app. */
$common = $app->loadCommon();
/* Check entry. */
-$common->checkEntry();
+if(!$app->version) $common->checkEntry();
$common->loadConfigFromDB();
/* Set default params. */
@@ -41,18 +42,10 @@ $config->requestType = 'GET';
$config->default->view = 'json';
$app->parseRequest();
-$common->checkPriv();
+if(!$app->version) $common->checkPriv();
$app->loadModule();
-$output = json_decode(ob_get_clean());
-$data = new stdClass();
-$data->status = isset($output->status) ? $output->status : $output->result;
-if(isset($output->message)) $data->message = $output->message;
-if(isset($output->data)) $data->data = json_decode($output->data);
-$output = json_encode($data);
-
-unset($_SESSION['ENTRY_CODE']);
-unset($_SESSION['VALID_ENTRY']);
+$output = ob_get_clean();
/* Flush the buffer. */
-echo helper::removeUTF8Bom($output);
+echo helper::removeUTF8Bom($app->formatData($output));