client = $client; $this->root = rtrim($root, '/') . '/'; $this->token = $password; $this->branch = isset($_COOKIE['repoBranch']) ? $_COOKIE['repoBranch'] : 'HEAD'; } /** * List files. * * @param string $path * @param string $revision * @access public * @return array */ public function ls($path, $revision = 'HEAD') { if(!scm::checkRevision($revision)) return array(); $api = "tree"; $param = new stdclass(); $param->path = ltrim($path, '/'); $param->ref = $revision; $param->recursive = 0; if(!empty($this->branch)) $param->ref = $this->branch; $list = $this->fetch($api, $param, true); if(empty($list)) return array(); $infos = array(); foreach($list as $file) { if(!isset($file->type)) continue; $info = new stdClass(); $info->name = $file->name; $info->kind = $file->type == 'blob' ? 'file' : 'dir'; if($file->type == 'blob') { $file = $this->files($file->path, $this->branch); $info->revision = zget($file, 'revision', ''); $info->comment = zget($file, 'comment', ''); $info->account = zget($file, 'committer', ''); $info->date = zget($file, 'date', ''); $info->size = zget($file, 'size', ''); } else { $commits = $this->getCommitsByPath($file->path, '', '', 1); if(empty($commits)) continue; $commit = $commits[0]; $info->revision = $commit->id; $info->comment = $commit->message; $info->account = $commit->committer_name; $info->date = date('Y-m-d H:i:s', strtotime($commit->committed_date)); $info->size = 0; } $infos[] = $info; unset($info); } /* Sort by kind */ foreach($infos as $key => $info) $kinds[$key] = $info->kind; if($infos) array_multisort($kinds, SORT_ASC, $infos); return $infos; } /** * Get files info. * * The API path requested is: "GET /projects/:id/repository/files/:file_path". * Known issue of GitLab API: if a '%' in 'file_path', GitLab API will show a error 'file_path should be a valid file path'. * * @param string $path * @param string $ref * @access public * @return object * @doc https://docs.gitlab.com/ee/api/repository_files.html */ public function files($path, $ref = 'master') { $path = urlencode($path); $api = "files/$path"; $param = new stdclass(); $param->ref = $ref; $file = $this->fetch($api, $param); if(!isset($file->file_name)) return false; $commits = $this->getCommitsByPath($path, '', '', 1); $file->revision = $file->commit_id; $file->size = $this->formatBytes($file->size); if(!empty($commits)) { $commit = $commits[0]; $file->revision = $commit->id; $file->committer = $commit->committer_name; $file->comment = $commit->message; $file->date = date('Y-m-d H:i:s', strtotime($commit->committed_date)); } return $file; } /** * Get tags * * @param string $path * @param string $revision * @access public * @return array */ public function tags($path, $revision = 'HEAD') { $api = "tags"; $tags = array(); $params = array(); $params['per_page'] = '100'; $params['order_by'] = 'updated'; $params['sort'] = 'asc'; for($page = 1; true; $page ++) { $params['page'] = $page; $list = $this->fetch($api, $params); if(empty($list)) break; foreach($list as $tag) $tags[] = $tag->name; if(count($list) < $params['per_page']) break; } return $tags; } /** * Get branches. * * @access public * @return array */ public function branch() { /* Max size of per_page in gitlab API is 100. */ $params = array(); $params['per_page'] = '100'; $branches = array(); $default = array(); for($page = 1; true; $page ++) { $params['page'] = $page; $branchList = $this->fetch("branches", $params); if(empty($branchList)) break; foreach($branchList as $branch) { if(!isset($branch->name)) continue; if($branch->default) { $default[$branch->name] = $branch->name; } else { $branches[$branch->name] = $branch->name; } } /* Last page. */ if(count($branchList) < $params['per_page']) break; } if(empty($branches) and empty($default)) $branches['master'] = 'master'; asort($branches); $branches = $default + $branches; return $branches; } /** * Get last log. * * @param string $path * @param int $count * @access public * @return array */ public function getLastLog($path, $count = 10) { return $this->log($path); } /** * Get logs. * * @param string $path * @param string $fromRevision * @param string $toRevision * @param int $count * @access public * @return array */ public function log($path, $fromRevision = 0, $toRevision = 'HEAD', $count = 0) { if(!scm::checkRevision($fromRevision)) return array(); if(!scm::checkRevision($toRevision)) return array(); $path = ltrim($path, DIRECTORY_SEPARATOR); $count = $count == 0 ? '' : "-n $count"; $list = $this->getCommitsByPath($path, $fromRevision, $toRevision); foreach($list as $commit) { if(isset($commit->id)) $commit->diffs = $this->getFilesByCommit($commit->id); } return $this->parseLog($list); } /** * Blame file * * @param string $path * @param string $revision * @access public * @return array */ public function blame($path, $revision) { if(!scm::checkRevision($revision)) return array(); $path = ltrim($path, DIRECTORY_SEPARATOR); $path = urlencode($path); $api = "files/$path/blame"; $param = new stdclass; $param->ref = ($revision and $revision != 'HEAD') ? $revision : $this->branch; $results = $this->fetch($api, $param); if(empty($results) or isset($results->message)) return array(); $blames = array(); $revLine = 0; $revision = ''; $lineNumber = 1; foreach($results as $blame) { $line = array(); $line['revision'] = $blame->commit->id; $line['committer'] = $blame->commit->committer_name; $line['message'] = $blame->commit->message; $line['time'] = date('Y-m-d H:i:s', strtotime($blame->commit->committed_date)); $line['line'] = $lineNumber; $line['lines'] = count($blame->lines); $line['content'] = array_shift($blame->lines); $blames[$lineNumber] = $line; $lineNumber ++; foreach($blame->lines as $line) { $blames[$lineNumber] = array('line' => $lineNumber, 'content' => $line); $lineNumber ++; } } return $blames; } /** * Diff file. * * @param string $path * @param string $fromRevision * @param string $toRevision * @param string $fromProject * @param string $extra * @access public * @return array */ public function diff($path, $fromRevision, $toRevision, $fromProject = '', $extra = '') { if(!scm::checkRevision($fromRevision) and $extra != 'isBranchOrTag') return array(); if(!scm::checkRevision($toRevision) and $extra != 'isBranchOrTag') return array(); $api = "compare"; $params = array('from' => $fromRevision, 'to' => $toRevision); if($fromProject) $params['from_project_id'] = $fromProject; if($toRevision == 'HEAD' and $this->branch) $params['to'] = $this->branch; $results = $this->fetch($api, $params); if(!isset($results->diffs)) return array(); foreach($results->diffs as $key => $diff) { if($path != '' and strpos($diff->new_path, $path) === false) unset($results->diffs[$key]); } $diffs = $results->diffs; $lines = array(); foreach($diffs as $diff) { $lines[] = sprintf("diff --git a/%s b/%s", $diff->old_path, $diff->new_path); $lines[] = sprintf("index %s ... %s %s ", $fromRevision, $toRevision, $diff->b_mode); $lines[] = sprintf("--a/%s", $diff->old_path); $lines[] = sprintf("--b/%s", $diff->new_path); $diffLines = explode("\n", $diff->diff); foreach($diffLines as $diffLine) $lines[] = $diffLine; } return $lines; } /** * Cat file. * * @param string $entry * @param string $revision * @access public * @return string */ public function cat($entry, $revision = 'HEAD') { if(!scm::checkRevision($revision)) return false; if($revision == 'HEAD' and $this->branch) $revision = $this->branch; $file = $this->files($entry, $revision); return isset($file->content) ? base64_decode($file->content) : ''; } /** * Get info. * * @param string $entry * @param string $revision * @access public * @return object */ public function info($entry, $revision = 'HEAD') { if(!scm::checkRevision($revision)) return false; $info = new stdclass(); $info->kind = 'dir'; $info->path = $entry; $info->revision = $revision; $info->root = ''; if($revision == 'HEAD' and $this->branch) $info->revision = $this->branch; if($entry) { $parent = dirname($entry); if($parent == '.') $parent = '/'; if($parent == '') $parent = '/'; $list = $this->tree($parent, 0); $file = new stdclass(); foreach($list as $node) if($node->path == $entry) $file = $node; $commits = $this->getCommitsByPath($entry); if(!empty($commits)) $file->revision = zget($commits[0], 'id', ''); $info->kind = (isset($file->type) and $file->type == 'tree') ? 'dir' : 'file'; } return $info; } /** * Exec git cmd. * * @param string $cmd * @access public * @todo Exec commands by gitlab api. * @return array */ public function exec($cmd) { return execCmd(escapeCmd("$this->client $cmd"), 'array'); } /** * Parse diff. * * @param array $lines * @access public * @return array */ public function parseDiff($lines) { if(empty($lines)) return array(); $diffs = array(); $num = count($lines); $endLine = end($lines); if(strpos($endLine, '\ No newline at end of file') === 0) $num -= 1; $newFile = false; $allFiles = array(); for($i = 0; $i < $num; $i ++) { $diffFile = new stdclass(); if(strpos($lines[$i], "diff --git ") === 0) { $fileInfo = explode(' ',$lines[$i]); $fileName = substr($fileInfo[2], strpos($fileInfo[2], '/') + 1); /* Prevent duplicate display of files. */ if(in_array($fileName, $allFiles)) continue; $allFiles[] = $fileName; $diffFile->fileName = $fileName; for($i++; $i < $num; $i ++) { $diff = new stdclass(); /* Fix bug #1757. */ if($lines[$i] == '+++ /dev/null') $newFile = true; if(strpos($lines[$i], '+++', 0) !== false) continue; if(strpos($lines[$i], '---', 0) !== false) continue; if(strpos($lines[$i], '======', 0) !== false) continue; if(preg_match('/^@@ -(\\d+)(,(\\d+))?\\s+\\+(\\d+)(,(\\d+))?\\s+@@/A', $lines[$i])) { $startLines = trim(str_replace(array('@', '+', '-'), '', $lines[$i])); list($oldStartLine, $newStartLine) = explode(' ', $startLines); list($diff->oldStartLine) = explode(',', $oldStartLine); list($diff->newStartLine) = explode(',', $newStartLine); $oldCurrentLine = $diff->oldStartLine; $newCurrentLine = $diff->newStartLine; if($newFile) { $oldCurrentLine = $diff->newStartLine; $newCurrentLine = $diff->oldStartLine; } $newLines = array(); for($i++; $i < $num; $i ++) { if(preg_match('/^@@ -(\\d+)(,(\\d+))?\\s+\\+(\\d+)(,(\\d+))?\\s+@@/A', $lines[$i])) { $i --; break; } if(strpos($lines[$i], "diff --git ") === 0) break; $line = $lines[$i]; if(strpos($line, '\ No newline at end of file') === 0)continue; $sign = empty($line) ? '' : $line[0]; if($sign == '-' and $newFile) $sign = '+'; $type = $sign != '-' ? $sign == '+' ? 'new' : 'all' : 'old'; if($sign == '-' || $sign == '+') { $line = substr_replace($line, ' ', 1, 0); if($newFile) $line = preg_replace('/^\-/', '+', $line); } $newLine = new stdclass(); $newLine->type = $type; $newLine->oldlc = $type != 'new' ? $oldCurrentLine : ''; $newLine->newlc = $type != 'old' ? $newCurrentLine : ''; $newLine->line = htmlSpecialString($line); if($type != 'new') $oldCurrentLine++; if($type != 'old') $newCurrentLine++; $newLines[] = $newLine; } $diff->lines = $newLines; $diffFile->contents[] = $diff; } if(isset($lines[$i]) and strpos($lines[$i], "diff --git ") === 0) { $i --; $newFile = false; break; } } $diffs[] = $diffFile; } } return $diffs; } /** * Get commit count. * * @param int $commits * @param string $lastVersion * @access public * @return int */ public function getCommitCount($commits = 0, $lastVersion = '') { if(!scm::checkRevision($lastVersion)) return false; chdir($this->root); $revision = $this->branch ? $this->branch : 'HEAD'; return execCmd(escapeCmd("$this->client rev-list --count $revision -- ./"), 'string'); } /** * Get first revision. * * @access public * @return string */ public function getFirstRevision() { chdir($this->root); $list = execCmd(escapeCmd("$this->client rev-list --reverse HEAD -- ./"), 'array'); return $list[0]; } /** * Get latest revision * * @access public * @return string */ public function getLatestRevision() { chdir($this->root); $revision = $this->branch ? $this->branch : 'HEAD'; $list = execCmd(escapeCmd("$this->client rev-list -1 $revision -- ./"), 'array'); return $list[0]; } /** * Get commits. * * @param string $version * @param int $count * @param string $branch * @param bool $getFile * @access public * @return array */ public function getCommits($version = '', $count = 0, $branch = '', $getFile = false) { if(!scm::checkRevision($version)) return array(); $api = "commits"; $commits = array(); $files = array(); if(empty($count)) $count = 10; if(!empty($version) and $count == 1) { $api .= '/' . $version; $commit = $this->fetch($api); if(isset($commit->id)) { $log = new stdclass; $log->committer = $commit->committer_name; $log->revision = $commit->id; $log->comment = $commit->message; $log->time = date('Y-m-d H:i:s', strtotime($commit->created_at)); $commits[$commit->id] = $log; $files[$commit->id] = $this->getFilesByCommit($log->revision); return array('commits' => $commits, 'files' => $files); } } $params = array(); $params['ref_name'] = $branch; $params['per_page'] = $count; $params['all'] = 0; if($version and $version != 'HEAD') { /* Get since param. */ if(substr($version, 0, 5) == 'since') { $since = true; $version = substr($version, 5); } $committedDate = $this->getCommittedDate($version); if(!$committedDate) return array('commits' => array(), 'files' => array()); if(!empty($since)) { $params['since'] = $committedDate; } else { $params['until'] = $committedDate; } } $list = $this->fetch($api, $params); foreach($list as $commit) { if(!is_object($commit)) continue; $log = new stdclass; $log->committer = $commit->committer_name; $log->revision = $commit->id; $log->comment = $commit->message; $log->time = date('Y-m-d H:i:s', strtotime($commit->created_at)); $commits[$commit->id] = $log; if($getFile) $files[$commit->id] = $this->getFilesByCommit($log->revision); } return array('commits' => $commits, 'files' => $files); } /** * getCommit * * @param int $sha * @access public * @return void */ public function getCommittedDate($sha) { if(!scm::checkRevision($sha)) return null; if(!$sha or $sha == 'HEAD') return date('c'); global $dao; $time = $dao->select('time')->from(TABLE_REPOHISTORY)->where('revision')->eq($sha)->fetch('time'); if($time) return date('c', strtotime($time)); $result = $this->fetch("commits/$sha"); return (isset($result->committed_date)) ? $result->committed_date : false; } /** * Get commits by path. * * @param string $path * @param string $fromRevision * @param string $toRevision * @param int $perPage * @param int $page * @param bool $getUrl * @access public * @return array */ public function getCommitsByPath($path, $fromRevision = '', $toRevision = '', $perPage = 0, $page = 1, $getUrl = false) { $path = ltrim($path, DIRECTORY_SEPARATOR); $api = "commits"; $param = new stdclass(); $param->path = urldecode($path); $param->ref_name = ($toRevision != 'HEAD' and $toRevision) ? $toRevision : $this->branch; $fromDate = $this->getCommittedDate($fromRevision); $toDate = $this->getCommittedDate($toRevision); $since = ''; $until = ''; if($fromRevision and $toRevision) { $since = min($fromDate, $toDate); $until = max($fromDate, $toDate); } elseif($fromRevision) { $since = $fromDate; } if($since) $param->since = $since; if($until) $param->until = $until; if($perPage) $param->per_page = $perPage; if($page) $param->page = $page; if($getUrl) { $params = (array) $param; $params['private_token'] = $this->token; $params['per_page'] = isset($params['per_page']) ? $params['per_page'] : 100; $api = ltrim($api, '/'); $api = $this->root . $api . '?' . http_build_query($params); return $api; } return $this->fetch($api, $param); } /** * Get files by commit. * * @param string $commit * @access public * @return void */ public function getFilesByCommit($revision) { if(!scm::checkRevision($revision)) return array(); $api = "commits/{$revision}/diff"; $params = new stdclass; $params->page = 1; $params->per_page = 100; $allResults = array(); while(true) { $results = $this->fetch($api, $params); $params->page ++; if(!is_array($results)) $results = array(); $allResults = $allResults + $results; if(count($results) < 100) break; } $files = array(); foreach($allResults as $row) { $file = new stdclass(); $file->revision = $revision; $file->path = '/' . $row->new_path; $file->type = 'file'; $file->oldPath = '/' . $row->old_path; $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; } /** * Repository/tree api. * * @param string $path * @param bool $recursive * @param bool $loop * @access public * @return mixed */ public function tree($path, $recursive = 1, $loop = false) { $api = "tree"; $params = array(); $params['path'] = ltrim($path, '/'); $params['ref'] = $this->branch; $params['recursive'] = (int) $recursive; return $this->fetch($api, $params, $loop, $loop ? true : false); } /** * Fetch data from gitlab api. * * @param string $api * @param array $params * @param bool $needToLoop * @param bool $multi * @access public * @return mixed */ public function fetch($api, $params = array(), $needToLoop = false, $multi = false) { $params = (array) $params; $params['private_token'] = $this->token; $params['per_page'] = isset($params['per_page']) ? $params['per_page'] : 100; $api = ltrim($api, '/'); $api = $this->root . $api . '?' . http_build_query($params); if($needToLoop) { $allResults = array(); if($multi) { $results = commonModel::httpWithHeader($api . "&page=1"); if(empty($results['header']['X-Total-Pages'])) return array(); $totalPages = $results['header']['X-Total-Pages']; if($totalPages == 1) { $allResults = json_decode($results['body']); } else { $requests = array(); for($page = 1; $page <= $totalPages; $page++) { $requests[$page]['url'] = $api . "&page={$page}"; } $results = requests::request_multiple($requests, array('timeout' => 60)); foreach($results as $result) { if(empty($result->body)) continue; $data = json_decode($result->body); $allResults = array_merge($allResults, $data); } } } else { for($page = 1; true; $page++) { $results = json_decode(commonModel::http($api . "&page={$page}", null, array(), array(), 'data', 'POST', 30, true, false)); if(!is_array($results)) break; if(!empty($results)) $allResults = array_merge($allResults, $results); if(count($results) < 100) break; } } return $allResults; } else { list($response, $httpCode) = commonModel::http($api, null, array(), array(), 'data', 'POST', 30, true, false); if(!empty(commonModel::$requestErrors)) { commonModel::$requestErrors = array(); return array(); } if($httpCode == 500) return array(); return json_decode($response); } } /** * Format bytes shown. * * @param int $size * @static * @access public * @return string */ public static function formatBytes($size) { if($size < 1024) return $size . 'Bytes'; if(round($size / (1024 * 1024), 2) > 1) return round($size / (1024 * 1024), 2) . 'G'; if(round($size / 1024, 2) > 1) return round($size / 1024, 2) . 'M'; return round($size, 2) . 'KB'; } /** * Parse log. * * @param array $logs * @access public * @return array */ public function parseLog($logs) { $parsedLogs = array(); $i = 0; foreach($logs as $commit) { if(!isset($commit->id)) continue; $parsedLog = new stdclass(); $parsedLog->revision = $commit->id; $parsedLog->committer = $commit->committer_name; $parsedLog->time = date('Y-m-d H:i:s', strtotime($commit->committed_date)); $parsedLog->comment = $commit->message; $parsedLog->change = array(); if(!empty($commit->diffs)) { foreach($commit->diffs as $diff) { $parsedLog->change[$diff->path] = array(); $parsedLog->change[$diff->path]['action'] = $diff->action; $parsedLog->change[$diff->path]['kind'] = $diff->type; $parsedLog->change[$diff->path]['oldPath'] = $diff->oldPath; } } $parsedLogs[] = $parsedLog; } return $parsedLogs; } /** * Get download url. * * @param string $branch * @param string $savePath * @param string $ext * @access public * @return string */ public function getDownloadUrl($branch = 'master', $savePath = '', $ext = 'zip') { $params = (array) $params; $params['private_token'] = $this->token; $params['sha'] = $branch; return "{$this->root}archive.{$ext}" . '?' . http_build_query($params); } /** * List all files. * * @param string $path * @param string $revision * @param array $lists * @access public * @return array */ public function getAllFiles($path = '', $revision = 'HEAD', &$lists = array()) { if(!scm::checkRevision($revision)) return array(); $api = "tree"; $param = new stdclass(); $param->path = ltrim($path, '/'); $param->ref = $revision; $param->recursive = 0; if(!empty($this->branch)) $param->ref = $this->branch; $list = $this->fetch($api, $param, true); if(empty($list)) return array(); $infos = array(); foreach($list as $file) { if(!isset($file->type)) continue; if($file->type == 'blob') { $lists[] = $file->path; } else { $this->getAllFiles($file->path, $revision, $lists); } } return $lists; } }