From 967f9cd0e5a76a68a702ef9343d7412f8bbd4350 Mon Sep 17 00:00:00 2001 From: azhi Date: Fri, 23 Aug 2013 11:21:24 +0800 Subject: [PATCH] + add git module. --- module/git/config.php | 26 ++ module/git/control.php | 98 +++++ module/git/lang/en.php | 17 + module/git/lang/zh-cn.php | 17 + module/git/model.php | 658 ++++++++++++++++++++++++++++++++++ module/git/view/cat.html.php | 16 + module/git/view/diff.html.php | 17 + 7 files changed, 849 insertions(+) create mode 100644 module/git/config.php create mode 100644 module/git/control.php create mode 100644 module/git/lang/en.php create mode 100644 module/git/lang/zh-cn.php create mode 100644 module/git/model.php create mode 100644 module/git/view/cat.html.php create mode 100644 module/git/view/diff.html.php diff --git a/module/git/config.php b/module/git/config.php new file mode 100644 index 0000000000..d1b1c57946 --- /dev/null +++ b/module/git/config.php @@ -0,0 +1,26 @@ +git->client = '/usr/bin/git'; // c:\git\git.exe + * $config->git->repos['pms']['path'] = '/home/user/repo/pms'; // c:\repo\pms + * + */ +$config->git = new stdClass(); +$config->git->encodings = 'utf-8, gbk'; +$config->git->client = '/usr/bin/git'; + +$i = 1; +$config->git->repos[$i]['path'] = '/home/z/pms'; + +/* +$i ++; +$config->git->repos[$i]['path'] = ''; +*/ diff --git a/module/git/control.php b/module/git/control.php new file mode 100644 index 0000000000..16b0885e45 --- /dev/null +++ b/module/git/control.php @@ -0,0 +1,98 @@ + + * @package git + * @version $Id$ + * @link http://www.zentao.net + */ +class git extends control +{ + /** + * Sync git. + * + * @access public + * @return void + */ + public function run() + { + $this->git->run(); + } + + /** + * Diff a file. + * + * @param string $path + * @param int $revision + * @access public + * @return void + */ + public function diff($path, $revision) + { + $url = helper::safe64Decode($path); + $this->view->url = $path; + $this->view->revision = $revision; + $this->view->diff = $this->git->diff($path, $revision); + + $this->display(); + } + + /** + * Cat a file. + * + * @param string $path + * @param int $revision + * @access public + * @return void + */ + public function cat($path, $revision) + { + $url = helper::safe64Decode($url); + $this->view->path = $path; + $this->view->revision = $revision; + $this->view->code = $this->git->cat($path, $revision); + + $this->display(); + } + + /** + * Sync from the syncer by api. + * + * @access public + * @return void + */ + public function apiSync() + { + if($this->post->logs) + { + $repoRoot = $this->post->repoRoot; + $logs = stripslashes($this->post->logs); + $logs = simplexml_load_string($logs); + foreach($logs->logentry as $entry) + { + $parsedLogs[] = $this->git->convertLog($entry); + } + $parsedObjects = array('stories' => array(), 'tasks' => array(), 'bugs' => array()); + foreach($parsedLogs as $log) + { + $objects = $this->git->parseComment($log->msg); + if($objects) + { + $this->git->saveAction2PMS($objects, $log, $repoRoot); + if($objects['stories']) $parsedObjects['stories'] = array_merge($parsedObjects['stories'], $objects['stories']); + if($objects['tasks']) $parsedObjects['tasks' ] = array_merge($parsedObjects['tasks'], $objects['tasks']); + if($objects['bugs']) $parsedObjects['bugs'] = array_merge($parsedObjects['bugs'], $objects['bugs']); + } + } + $parsedObjects['stories'] = array_unique($parsedObjects['stories']); + $parsedObjects['tasks'] = array_unique($parsedObjects['tasks']); + $parsedObjects['bugs'] = array_unique($parsedObjects['bugs']); + $this->view->parsedObjects = $parsedObjects; + $this->display(); + exit; + } + } +} diff --git a/module/git/lang/en.php b/module/git/lang/en.php new file mode 100644 index 0000000000..49a91119ea --- /dev/null +++ b/module/git/lang/en.php @@ -0,0 +1,17 @@ + + * @package git + * @version $Id$ + * @link http://www.zentao.net + */ +/* Methods. */ +$lang->git = new stdclass(); +$lang->git->common = 'Git'; +$lang->git->cat = 'View code'; +$lang->git->diff = 'Diff code'; +$lang->git->apiSync = 'API: sync git log'; diff --git a/module/git/lang/zh-cn.php b/module/git/lang/zh-cn.php new file mode 100644 index 0000000000..5575f0204e --- /dev/null +++ b/module/git/lang/zh-cn.php @@ -0,0 +1,17 @@ + + * @package git + * @version $Id$ + * @link http://www.zentao.net + */ +/* 方法列表。*/ +$lang->git = new stdclass(); +$lang->git->common = 'Git'; +$lang->git->cat = '查看源代码'; +$lang->git->diff = '比较源代码'; +$lang->git->apiSync = '接口:同步git日志'; diff --git a/module/git/model.php b/module/git/model.php new file mode 100644 index 0000000000..88b6d3616a --- /dev/null +++ b/module/git/model.php @@ -0,0 +1,658 @@ + + * @package git + * @version $Id$ + * @link http://www.zentao.net + */ +?> +loadModel('action'); + } + + /** + * Run. + * + * @access public + * @return void + */ + public function run() + { + $this->setRepos(); + $this->setLogRoot(); + $this->setRestartFile(); + + foreach($this->repos as $name => $repo) + { + $this->printLog("begin repo $name"); + $repo = (object)$repo; + $repo->name = $name; + $this->setRepo($repo); + + $savedRevision = $this->getSavedRevision(); + $this->printLog("start from revision $savedRevision"); + $logs = $this->getRepoLogs($repo, $savedRevision); + $this->printLog("get " . count($logs) . " logs"); + + $this->printLog('begin parsing logs'); + foreach($logs as $log) + { + $this->printLog("parsing log {$log->revision}"); + if($log->revision == $savedRevision) + { + $this->printLog("{$log->revision} alread parsed, ommit it"); + continue; + } + + $this->printLog("comment is\n----------\n" . trim($log->msg) . "\n----------"); + $objects = $this->parseComment($log->msg); + if($objects) + { + $this->printLog('extract' . + 'story:' . join(' ', $objects['stories']) . + ' task:' . join(' ', $objects['tasks']) . + ' bug:' . join(',', $objects['bugs'])); + + $this->saveAction2PMS($objects, $log); + } + else + { + $this->printLog('no objects found' . "\n"); + } + if($log->revision > $savedRevision) $savedRevision = $log->revision; + } + $this->saveLastRevision($savedRevision); + $this->printLog("save revision $savedRevision"); + $this->deleteRestartFile(); + $this->printLog("\n\nrepo $name finished"); + } + } + + /** + * Set the log root. + * + * @access public + * @return void + */ + public function setLogRoot() + { + $this->logRoot = $this->app->getTmpRoot() . 'git/'; + if(!is_dir($this->logRoot)) mkdir($this->logRoot); + } + + /** + * Set the restart file. + * + * @access public + * @return void + */ + public function setRestartFile() + { + $this->restartFile = dirname(__FILE__) . '/restart'; + } + + /** + * Delete the restart file. + * + * @access public + * @return void + */ + public function deleteRestartFile() + { + if(is_file($this->restartFile)) unlink($this->restartFile); + } + + /** + * Set the repos. + * + * @access public + * @return void + */ + public function setRepos() + { + if(!$this->config->git->repos) die("You must set one git repo.\n"); + $this->repos = $this->config->git->repos; + } + + /** + * Set repo. + * + * @param object $repo + * @access public + * @return void + */ + public function setRepo($repo) + { + $this->setClient($repo); + $this->setLogFile($repo->name); + $this->setRepoRoot($repo); + } + + /** + * Set the git binary client of a repo. + * + * @param object $repo + * @access public + * @return void + */ + public function setClient($repo) + { + if($this->config->git->client == '') die("You must set the git client file.\n"); + $this->client = $this->config->git->client; + } + + /** + * Set the log file of a repo. + * + * @param string $repoName + * @access public + * @return void + */ + public function setLogFile($repoName) + { + $this->logFile = $this->logRoot . $repoName; + } + + /** + * set the root path of a repo. + * + * @param object $repo + * @access public + * @return void + */ + public function setRepoRoot($repo) + { + $this->repoRoot = $repo->path; + } + + /** + * Get repo logs. + * + * @param object $repo + * @param int $fromRevision + * @access public + * @return array + */ + public function getRepoLogs($repo, $fromRevision) + { + $parsedLogs = array(); + + /* The git log command. */ + if($fromRevision) + { + $cmd = "cd $this->repoRoot; $this->client log --stat -r $fromRevision:HEAD --pretty=format:%an*_*%cd*_*%H*_*%s"; + } + else + { + $cmd = "cd $this->repoRoot; $this->client log --stat --pretty=format:%an*_*%cd*_*%H*_*%s"; + } + exec($cmd, $list); + + if(!$list) die("Some error occers: \nThe command is $cmd\n"); + + /* Process logs. */ + $logs = array(); + $i = 0; + + foreach($list as $line) + { + if(!$line) + { + $i++; + continue; + } + $logs[$i][] = $line; + } + foreach($logs as $log) + { + $parsedLogs[] = $this->convertLog($log); + } + return $parsedLogs; + } + + /** + * Convert log from xml format to object. + * + * @param object $log + * @access public + * @return ojbect + */ + public function convertLog($log) + { + + list($account, $date, $hash, $comment) = explode('*_*', $log[0]); + $parsedLog = new stdClass(); + $parsedLog->author = $account; + $parsedLog->revision = $hash; + $parsedLog->msg = $comment; + $parsedLog->date = date('Y-m-d H:i:s', strtotime($date)); + $parsedLog->files = array(); + + unset($log[0]); + foreach($log as $change) + { + if(strpos($change, '|') === false) continue; + list($entry, $modify) = explode('|', $change); + $entry = trim($entry); + $parsedLog->files['M'][] = $entry; + } + + return $parsedLog; + } + + /** + * Parse the comment of git, extract object id list from it. + * + * @param string $comment + * @access public + * @return array + */ + public function parseComment($comment) + { + $stories = array(); + $tasks = array(); + $bugs = array(); + + // bug|story|task(case insensitive) + some space + #|:|:(Chinese) + id lists(maybe join with space or ,) + // $comment = "bug # 1,2,3,4 Bug:1 2 3 4 5 story:9999,1234566 story:456,1234566"; + $commonReg = "(?:\s){0,}(?:#|:|:){0,}([0-9, ]{1,})"; + $taskReg = '/task' . $commonReg . '/i'; + $storyReg = '/story' . $commonReg . '/i'; + $bugReg = '/bug' . $commonReg . '/i'; + + if(preg_match_all($storyReg, $comment, $result)) $stories = join(' ', $result[1]); + if(preg_match_all($taskReg, $comment, $result)) $tasks = join(' ', $result[1]); + if(preg_match_all($bugReg, $comment, $result)) $bugs = join(' ', $result[1]); + + if($stories) $stories = array_unique(explode(' ', str_replace(',', ' ', $stories))); + if($tasks) $tasks = array_unique(explode(' ', str_replace(',', ' ', $tasks))); + if($bugs) $bugs = array_unique(explode(' ', str_replace(',', ' ', $bugs))); + + if(!$stories and !$tasks and !$bugs) return array(); + return array('stories' => $stories, 'tasks' => $tasks, 'bugs' => $bugs); + } + + /** + * Convert the comment to uft-8. + * + * @param string $comment + * @access public + * @return string + */ + public function iconvComment($comment) + { + /* Get encodings. */ + $encodings = str_replace(' ', '', trim($comment)); + if($encodings == '') return $comment; + $encodings = explode(',', $encodings); + + /* Try convert. */ + foreach($encodings as $encoding) + { + $result = @iconv($encoding, 'utf-8', $comment); + if($result) return $result; + } + + return $comment; + } + + /** + * Diff a url. + * + * @param string $path + * @param int $revision + * @access public + * @return string|bool + */ + public function diff($path, $revision) + { + $repo = $this->getRepoByURL($path); + if(!$repo) return false; + + $this->setClient($repo); + putenv('LC_CTYPE=en_US.UTF-8'); + + $path = str_replace('%2F', '/', urlencode($path)); + $path = str_replace('%3A', ':', $path); + + $cmd = $this->client . " diff $revision^ $revision $url"; + $diff = `$cmd`; + return $diff; + } + + /** + * Cat a url. + * + * @param string $path + * @param int $revision + * @access public + * @return string|bool + */ + public function cat($path, $revision) + { + $repo = $this->getRepoByURL($path); + if(!$repo) return false; + + $this->setClient($repo); + + putenv('LC_CTYPE=en_US.UTF-8'); + + $path = str_replace('%2F', '/', urlencode($path)); + $path = str_replace('%3A', ':', $path); + + $cmd = $this->client . " show -r $revision $path"; + $code = `$cmd`; + return $code; + } + + /** + * Get repo by url. + * + * @param string $url + * @access public + * @return object|bool + */ + public function getRepoByURL($url) + { + foreach($this->config->git->repos as $repo) if(strpos($url, $repo['path']) !== false) return (object)$repo; + return false; + } + + /** + * Save action to pms. + * + * @param array $objects + * @param object $log + * @param string $repoRoot + * @access public + * @return void + */ + public function saveAction2PMS($objects, $log, $repoRoot = '') + { + $action = new stdclass(); + $action->actor = $log->author; + $action->action = 'gitcommited'; + $action->date = $log->date; + $action->comment = $this->iconvComment($log->msg); + $action->extra = $log->revision; + + $changes = $this->createActionChanges($log, $repoRoot); + + if($objects['stories']) + { + $products = $this->getStoryProducts($objects['stories']); + foreach($objects['stories'] as $storyID) + { + $storyID = (int)$storyID; + if(!isset($products[$storyID])) continue; + + $action->objectType = 'story'; + $action->objectID = $storyID; + $action->product = $products[$storyID]; + $action->project = 0; + + $this->saveRecord($action, $changes); + } + } + + if($objects['tasks']) + { + $productsAndProjects = $this->getTaskProductsAndProjects($objects['tasks']); + foreach($objects['tasks'] as $taskID) + { + $taskID = (int)$taskID; + if(!isset($productsAndProjects[$taskID])) continue; + + $action->objectType = 'task'; + $action->objectID = $taskID; + $action->product = $productsAndProjects[$taskID]['product']; + $action->project = $productsAndProjects[$taskID]['project']; + + $this->saveRecord($action, $changes); + } + } + + if($objects['bugs']) + { + $productsAndProjects = $this->getBugProductsAndProjects($objects['bugs']); + + foreach($objects['bugs'] as $bugID) + { + $bugID = (int)$bugID; + if(!isset($productsAndProjects[$bugID])) continue; + + $action->objectType = 'bug'; + $action->objectID = $bugID; + $action->product = $productsAndProjects[$bugID]->product; + $action->project = $productsAndProjects[$bugID]->project; + + $this->saveRecord($action, $changes); + } + } + } + + /** + * Save an action to pms. + * + * @param object $action + * @param object $log + * @access public + * @return bool + */ + public function saveRecord($action, $changes) + { + $record = $this->dao->select('*')->from(TABLE_ACTION) + ->where('objectType')->eq($action->objectType) + ->andWhere('objectID')->eq($action->objectID) + ->andWhere('extra')->eq($action->extra) + ->andWhere('action')->eq('gitcommited') + ->fetch(); + if($record) + { + $this->dao->update(TABLE_ACTION)->data($action)->where('id')->eq($record->id)->exec(); + if($changes) + { + $historyID = $this->dao->findByAction($record->id)->from(TABLE_HISTORY)->fetch('id'); + $this->dao->update(TABLE_HISTORY)->data($changes)->where('id')->eq($historyID)->exec(); + } + } + else + { + $this->dao->insert(TABLE_ACTION)->data($action)->autoCheck()->exec(); + if($changes) + { + $actionID = $this->dao->lastInsertID(); + $this->action->logHistory($actionID, array($changes)); + } + } + } + + /** + * Create changes for action from a log. + * + * @param object $log + * @param string $repoRoot + * @access public + * @return array + */ + public function createActionChanges($log, $repoRoot) + { + if(!$log->files) return array(); + $diff = ''; + + $oldSelf = $this->server->PHP_SELF; + $this->server->set('PHP_SELF', $this->config->webRoot); + + if(!$repoRoot) $repoRoot = $this->repoRoot; + + foreach($log->files as $action => $actionFiles) + { + foreach($actionFiles as $file) + { + $param = array('url' => helper::safe64Encode($repoRoot . $file), 'revision' => $log->revision); + $catLink = trim(html::a(helper::createLink('git', 'cat', $param, 'html'), 'view', '', "class='gitlink'")); + $diffLink = trim(html::a(helper::createLink('git', 'diff', $param, 'html'), 'diff', '', "class='gitlink'")); + $diff .= $action . " " . $file . " $catLink "; + $diff .= $action == 'M' ? "$diffLink\n" : "\n" ; + } + } + $changes->field = 'subversion'; + $changes->old = ''; + $changes->new = ''; + $changes->diff = trim($diff); + + $this->server->set('PHP_SELF', $oldSelf); + return (array)$changes; + } + + /** + * Get products of stories. + * + * @param array $stories + * @access public + * @return array + */ + public function getStoryProducts($stories) + { + return $this->dao->select('id, product')->from(TABLE_STORY)->where('id')->in($stories)->fetchPairs(); + } + + /** + * Get products and projects of tasks. + * + * @param array $tasks + * @access public + * @return array + */ + public function getTaskProductsAndProjects($tasks) + { + $records = array(); + $products = $this->dao->select('t1.id, t2.product') + ->from(TABLE_TASK)->alias('t1') + ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') + ->where('t1.id')->in($tasks)->fetchPairs(); + + $projects = $this->dao->select('id, project')->from(TABLE_TASK)->where('id')->in($tasks)->fetchPairs(); + + foreach($projects as $taskID => $projectID) + { + $record = array(); + $record['project'] = $projectID; + $record['product'] = isset($products[$taskID]) ? $products[$taskID] : 0; + $records[$taskID] = $record; + } + return $records; + } + + /** + * Get products and projects of bugs. + * + * @param array $bugs + * @access public + * @return array + */ + public function getBugProductsAndProjects($bugs) + { + return $this->dao->select('id, project, product')->from(TABLE_BUG)->where('id')->in($bugs)->fetchAll('id'); + } + + /** + * Get the saved revision. + * + * @access public + * @return int + */ + public function getSavedRevision() + { + if(!file_exists($this->logFile)) return 0; + if(file_exists($this->restartFile)) return 0; + return (int)trim(file_get_contents($this->logFile)); + } + + /** + * Save the last revision. + * + * @param int $revision + * @access public + * @return void + */ + public function saveLastRevision($revision) + { + file_put_contents($this->logFile, $revision); + } + + /** + * Pring log. + * + * @param sting $log + * @access public + * @return void + */ + public function printLog($log) + { + echo helper::now() . " $log\n"; + } +} diff --git a/module/git/view/cat.html.php b/module/git/view/cat.html.php new file mode 100644 index 0000000000..15406f134f --- /dev/null +++ b/module/git/view/cat.html.php @@ -0,0 +1,16 @@ + + * @package file + * @version $Id$ + * @link http://www.zentao.net + */ +?> + +
+
<?php echo $code;?>
+ diff --git a/module/git/view/diff.html.php b/module/git/view/diff.html.php new file mode 100644 index 0000000000..067c43e4de --- /dev/null +++ b/module/git/view/diff.html.php @@ -0,0 +1,17 @@ + + * @package file + * @version $Id$ + * @link http://www.zentao.net + */ +?> + + +
+
<?php echo $diff;?>
+