client = $client; $this->root = rtrim($root, DIRECTORY_SEPARATOR); if(!realpath($this->root) and !empty($repo)) { global $app; $project = $app->control->loadModel('gitea')->apiGetSingleProject($repo->serviceHost, $repo->serviceProject); if(isset($project->tokenCloneUrl)) { $cmd = 'git clone --progress -v "' . $project->tokenCloneUrl . '" "' . $this->root . '" > "' . $app->getTmpRoot() . "log/clone.progress." . strtolower($repo->SCM) . ".{$repo->name}.log\" 2>&1 &"; if(PHP_OS == 'WINNT') $cmd = "start /b $cmd"; exec($cmd); return $app->control->locate($app->control->createLink('repo', 'showSyncCommit', "repoID={$repo->id}")); } } $branch = isset($_COOKIE['repoBranch']) ? $_COOKIE['repoBranch'] : ''; if($branch) { $branches = $this->branch(); if(isset($branches[$branch])) $branch = "origin/$branch"; } $this->branch = $branch; $this->repo = $repo; chdir($this->root); exec("{$this->client} config core.quotepath false"); } /** * List files. * * @param string $path * @param string $revision * @access public * @return array */ public function ls($path, $revision = 'HEAD') { if(!scm::checkRevision($revision)) return array(); $path = ltrim($path, DIRECTORY_SEPARATOR); $sub = ''; chdir($this->root); if(!empty($path)) $sub = ":$path"; if(!empty($this->branch))$revision = $this->branch; execCmd(escapeCmd("$this->client pull")); $cmd = escapeCmd("$this->client ls-tree -l $revision$sub"); $list = execCmd($cmd . ' 2>&1', 'array', $result); if($result) return array(); $infos = array(); foreach($list as $entry) { list($mod, $kind, $revision, $size, $name) = preg_split('/[\t ]+/', $entry); /* Get commit info. */ $pathName = ltrim($path . DIRECTORY_SEPARATOR . $name, DIRECTORY_SEPARATOR); $cmd = escapeCmd("$this->client log -1 $this->branch -- $pathName"); $commit = execCmd($cmd, 'array'); $logs = $this->parseLog($commit); if($size > 1024 * 1024) { $size = round($size / (1024 * 1024), 2) . 'MB'; } else if($size > 1024) { $size = round($size / 1024, 2) . 'KB'; } else { $size .= 'Bytes'; } $info = new stdClass(); $info->name = $name; $info->kind = $kind == 'tree' ? 'dir' : 'file'; $info->revision = $logs ? $logs[0]->revision : $revision; $info->size = $size; $info->account = $logs ? $logs[0]->committer : ''; $info->date = $logs ? $logs[0]->time : ''; $info->comment = $logs ? $logs[0]->comment : ''; $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 tags * * @param string $path * @param string $revision * @access public * @return array */ public function tags($path, $revision = 'HEAD') { if(!scm::checkRevision($revision)) return array(); chdir($this->root); $cmd = escapeCmd("$this->client tag --sort=taggerdate"); $list = execCmd($cmd . ' 2>&1', 'array', $result); if($result) return array(); foreach($list as $key => $tag) { if(!$tag) unset($list[$key]); } return $list; } /** * Get branch. * * @access public * @return array */ public function branch() { chdir($this->root); /* Get local branch. */ $cmd = escapeCmd("$this->client branch -a"); $list = execCmd($cmd . ' 2>&1', 'array', $result); if($result) return array(); /* Get default branch. */ $defaultBranch = execCmd("$this->client symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"); $defaultBranch = trim($defaultBranch); $branches = array(); foreach($list as $localBranch) { $localBranch = trim($localBranch); if(substr($localBranch, 0, 19) == 'remotes/origin/HEAD') continue; if(substr($localBranch, 0, 1) == '*') continue; if(substr($localBranch, 0, 15) == 'remotes/origin/') $localBranch = substr($localBranch, 15); $localBranch = trim($localBranch); if(empty($localBranch))continue; if($localBranch != $defaultBranch) $branches[$localBranch] = $localBranch; } asort($branches); if($defaultBranch) $branches = array($defaultBranch => $defaultBranch) + $branches; return $branches; } /** * Get last log. * * @param string $path * @param int $count * @access public * @return array */ public function getLastLog($path, $count = 10) { $path = ltrim($path, DIRECTORY_SEPARATOR); $revision = $this->branch ? $this->branch : 'HEAD'; chdir($this->root); $list = execCmd(escapeCmd("$this->client log -10 $revision -- $path"), 'array'); $logs = $this->parseLog($list); return $logs; } /** * 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(); execCmd(escapeCmd("$this->client pull")); $path = ltrim($path, DIRECTORY_SEPARATOR); $count = $count == 0 ? '' : "-n $count"; /* compatible with svn. */ if($fromRevision === 'HEAD' and $this->branch) $fromRevision = $this->branch; if($toRevision === 'HEAD' and $this->branch) $toRevision = $this->branch; if($fromRevision === $toRevision) { $logs = array(); chdir($this->root); $list = execCmd(escapeCmd("$this->client log --stat=1024 --name-status --stat-name-width=1000 -1 $fromRevision -- $path"), 'array'); $logs = $this->parseLog($list); return $logs; } if(!$fromRevision) { $revisions = " $toRevision"; } else { $revisions = "$fromRevision..$toRevision"; } chdir($this->root); $list = execCmd(escapeCmd("$this->client log --stat=1024 --name-status --stat-name-width=1000 $count $revisions -- $path"), 'array'); $logs = $this->parseLog($list); return $logs; } /** * 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); chdir($this->root); $list = execCmd(escapeCmd("$this->client blame -l $revision -- $path"), 'array'); $blames = array(); $revLine = 0; $revision = ''; foreach($list as $line) { if(empty($line)) continue; if($line[0] == '^') $line = substr($line, 1); preg_match('/^([0-9a-f]{39,40})\s.*\((\S+)\s+([\d-]+)\s(.*)\s(\d+)\)(.*)$/U', $line, $matches); if(isset($matches[1]) and $matches[1] != $revision) { $blame = array(); $blame['revision'] = $matches[1]; $blame['committer'] = $matches[2]; $blame['time'] = $matches[3]; $blame['line'] = $matches[5]; $blame['lines'] = 1; $blame['content'] = strpos($matches[6], ' ') === false ? $matches[6] : substr($matches[6], 1); $log = $this->log('', '', '', 1); $blame['message'] = $log[0]->comment; $revision = $matches[1]; $revLine = $matches[5]; $blames[$revLine] = $blame; } elseif(isset($matches[5])) { $blame = array(); $blame['line'] = $matches[5]; $blame['content'] = strpos($matches[6], ' ') === false ? $matches[6] : substr($matches[6], 1); $blames[$matches[5]] = $blame; $blames[$revLine]['lines'] ++; } } return $blames; } /** * Diff file. * * @param string $path * @param string $fromRevision * @param string $toRevision * @param string $extra * @access public * @return array */ public function diff($path, $fromRevision, $toRevision, $extra = '') { if(!scm::checkRevision($fromRevision) and $extra != 'isBranchOrTag') return array(); if(!scm::checkRevision($toRevision) and $extra != 'isBranchOrTag') return array(); $path = ltrim($path, DIRECTORY_SEPARATOR); chdir($this->root); if($toRevision == 'HEAD' and $this->branch) $toRevision = $this->branch; if($fromRevision == '^') $fromRevision = $toRevision . '^'; if(strpos($fromRevision, '^') !== false) { $list = execCmd(escapeCmd("$this->client log -2 $toRevision --pretty=format:%H -- $path"), 'array'); if(isset($list[1])) $fromRevision = $list[1]; } $lines = execCmd(escapeCmd("$this->client diff $fromRevision $toRevision -- $path"), 'array'); 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; chdir($this->root); if($revision == 'HEAD' and $this->branch) $revision = $this->branch; $cmd = escapeCmd("$this->client show $revision:$entry"); $content = execCmd($cmd); if(is_array($content)) $content = implode("\n", $content); return $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; chdir($this->root); if($revision == 'HEAD' and $this->branch) $revision = $this->branch; $path = ltrim($entry, DIRECTORY_SEPARATOR); $cmd = escapeCmd("$this->client ls-tree $revision -- $path"); $result = execCmd($cmd); $kind = ''; if($result) { $results = explode("\n", trim($result)); if(count($results) >= 2) { $kind = 'dir'; } else { list($mode, $type) = explode(' ', $results[0]); $kind = $type == 'tree' ? 'dir' : 'file'; } } $list = execCmd(escapeCmd("$this->client log -1 $revision --pretty=format:%H -- $path"), 'array'); $revision = $list[0]; $info = new stdclass(); $info->kind = $kind; $info->path = $entry; $info->revision = $revision; $info->root = $this->root; return $info; } /** * Exec git cmd. * * @param string $cmd * @access public * @return array */ public function exec($cmd) { chdir($this->root); 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 $rversion * @param int $count * @param string $branch * @access public * @return array */ public function getCommits($revision = '', $count = 0, $branch = '') { if(!scm::checkRevision($revision)) return array(); if($revision == 'HEAD' and $branch) { $revision = $branch; } elseif(is_numeric($revision)) { $revision = "--skip=$revision $branch"; } $count = $count == 0 ? '' : "-n $count"; chdir($this->root); if($branch) { execCmd(escapeCmd("$this->client checkout $branch")); execCmd(escapeCmd("$this->client pull")); } $list = execCmd(escapeCmd("$this->client log $count $revision -- ./"), 'array'); $commits = $this->parseLog($list); $logs = array(); foreach($commits as $commit) { $hash = $commit->revision; $log = new stdClass(); $log->committer = $commit->committer; $log->revision = $commit->revision; $log->comment = $commit->comment; $log->time = $commit->time; $logs['commits'][$hash] = $log; $logs['files'][$hash] = array(); } if(empty($logs)) return $logs; $hash = ''; $files = execCmd(escapeCmd("$this->client whatchanged $count $revision --pretty=format:%an@_@%cd@_@%H@_@%s -- ./"), 'array'); foreach($files as $commit) { $commit = trim($commit); if(empty($commit)) continue; $parsedCommit = explode('@_@', $commit); if(count($parsedCommit) == 4) { list($account, $date, $hash, $comment) = $parsedCommit; } else { $file = explode(' ', $commit); $file = explode("\t", end($file)); if(!isset($file[1])) $file[1] = ''; list($action, $path) = $file; $parsedFile = new stdclass(); $parsedFile->revision = $hash; $parsedFile->path = '/' . trim($path); $parsedFile->oldPath = isset($file[2]) ? '/' . trim($file[2]) : ''; $parsedFile->type = 'file'; $parsedFile->action = $action; $logs['files'][$hash][] = $parsedFile; } } return $logs; } /** * Get clone url. * * @access public * @return string */ public function getCloneUrl() { $url = new stdclass(); $remote = execCmd(escapeCmd("$this->client remote -v"), 'array'); $pregHttp = '/http(s)?:\/\/(www\.)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:\d+)*(\/\w+)*\.git/'; $pregSSH = '/ssh:\/\/git@[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:\d+)*(\/\w+)*\.git/'; if(preg_match($pregHttp, $remote[0], $matches)) $url->http = $matches[0]; if(preg_match($pregSSH, $remote[0], $matches)) $url->ssh = $matches[0]; return $url; } /** * Parse log. * * @param array $logs * @access public * @return array */ public function parseLog($logs) { $parsedLogs = array(); $i = 0; foreach($logs as $line) { if(strpos($line, 'commit ') === 0) { if(isset($log)) { $log->comment = trim($comment); $log->change = $changes; $parsedLogs[$i] = $log; $i++; } $log = new stdclass(); $comment = ''; $changes = array(); $log->revision = trim(preg_replace('/^commit/', '', $line)); } elseif(strpos($line, 'Author:') === 0) { $account = preg_replace('/^Author:/', '', $line); $log->committer = trim(preg_replace('/<[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9_\-\.]+>/', '', $account)); } elseif(strpos($line, 'Date:') === 0) { $date = trim(preg_replace('/^Date:/', '', $line)); $log->time = date('Y-m-d H:i:s', strtotime($date)); } elseif(preg_match('/^\s{2,}/', $line)) { $comment .= $line; } elseif(strpos($line, "\t") !== false) { $lineList = explode("\t", $line); list($action, $entry) = $lineList; $entry = '/' . trim($entry); $pathInfo = array(); $pathInfo['action'] = $action; $pathInfo['kind'] = 'file'; $pathInfo['oldPath'] = isset($lineList[2]) ? '/' . trim($lineList[2]) : ''; $changes[$entry] = $pathInfo; } } if(isset($log)) { $log->comment = trim($comment); $log->change = $changes; $parsedLogs[$i] = $log; } 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') { global $app; $api = $app->control->loadModel('gitea')->getApiRoot($this->repo->serviceHost); return sprintf($api, "/repos/{$this->repo->serviceProject}/archive/{$branch}.zip"); } /** * 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(); $path = ltrim($path, DIRECTORY_SEPARATOR); $sub = ''; chdir($this->root); if(!empty($path)) $sub = ":$path"; if(!empty($this->branch))$revision = $this->branch; $cmd = escapeCmd("$this->client ls-tree -l $revision$sub"); $list = execCmd($cmd . ' 2>&1', 'array', $result); if($result) return array(); foreach($list as $entry) { list($mod, $kind, $revision, $size, $name) = preg_split('/[\t ]+/', $entry); /* Get commit info. */ $pathName = ltrim($path . DIRECTORY_SEPARATOR . $name, DIRECTORY_SEPARATOR); if($kind == 'tree') { $this->getAllFiles($pathName, $revision, $lists); } else { $lists[] = rtrim($pathName, DIRECTORY_SEPARATOR); } } return $lists; } }