%nTree: %T%nDate: %ai%n%n%s%n%n%b'; public function __construct($repo) { $this->repo = $repo; } /** * Test a given object hash. * * @param string Object hash. * @return mixed false if not valid or 'blob', 'tree', 'commit' */ public function testHash($hash) { $cmd = sprintf('GIT_DIR=%s git cat-file -t %s', escapeshellarg($this->repo), escapeshellarg($hash)); $ret = 0; $out = array(); exec($cmd, &$out, &$ret); if ($ret != 0) return false; return trim($out[0]); } /** * Given a commit hash returns an array of files in it. * * A file is a class with the following properties: * * 'perm', 'type', 'size', 'hash', 'file' * * @param string Commit ('HEAD') * @param string Base folder ('') * @return array */ public function filesAtCommit($commit='HEAD', $folder='') { // now we grab the info about this commit including its tree. $co = $this->getCommit($commit); if ($folder) { // As we are limiting to a given folder, we need to find // the tree corresponding to this folder. $found = false; foreach ($this->getTreeInfo($co->tree) as $file) { if ($file->type == 'tree' and $file->file == $folder) { $found = true; $tree = $file->hash; break; } } if (!$found) { throw new Exception(sprintf(__('Folder %1$s not found in commit %2$s.'), $folder, $commit)); } } else { $tree = $co->tree; } $res = array(); // get the raw log corresponding to this commit to find the // origin of each file. $rawlog = array(); $cmd = sprintf('GIT_DIR=%s git log --raw --abbrev=40 --pretty=oneline %s', escapeshellarg($this->repo), escapeshellarg($commit)); exec($cmd, &$rawlog); // We reverse the log to be able to use a fixed efficient // regex without back tracking. $rawlog = implode("\n", array_reverse($rawlog)); foreach ($this->getTreeInfo($tree, false) as $file) { // Now we grab the files in the current tree with as much // information as possible. $matches = array(); if ($file->type == 'blob' and preg_match('/^\:\d{6} \d{6} [0-9a-f]{40} '.$file->hash.' .*^([0-9a-f]{40})/msU', $rawlog, &$matches)) { $fc = $this->getCommit($matches[1]); $file->date = $fc->date; $file->log = $fc->title; } else if ($file->type == 'blob') { $file->date = $co->date; $file->log = $co->title; } $file->fullpath = ($folder) ? $folder.'/'.$file->file : $file->file; $res[] = $file; } return $res; } /** * Get the tree info. * * @param string Tree hash * @param bool Do we recurse in subtrees (true) * @return array Array of file information. */ public function getTreeInfo($tree, $recurse=true) { if ('tree' != $this->testHash($tree)) { throw new Exception(sprintf(__('Not a valid tree: %s.'), $tree)); } $cmd_tmpl = 'GIT_DIR=%s git-ls-tree%s -t -l %s'; $cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), ($recurse) ? ' -r' : '', escapeshellarg($tree)); $out = array(); $res = array(); exec($cmd, &$out); foreach ($out as $line) { list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY); $res[] = (object) array('perm' => $perm, 'type' => $type, 'size' => $size, 'hash' => $hash, 'file' => $file); } return $res; } /** * Get the file info. * * @param string File * @param string Commit ('HEAD') * @return false Information */ public function getFileInfo($totest, $commit='HEAD') { $cmd_tmpl = 'GIT_DIR=%s git-ls-tree -r -t -l %s'; $cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), escapeshellarg($commit)); $out = array(); exec($cmd, &$out); foreach ($out as $line) { list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY); if ($totest == $file) { return (object) array('perm' => $perm, 'type' => $type, 'size' => $size, 'hash' => $hash, 'file' => $file); } } return false; } /** * Get a blob. * * @param string Blob hash * @return string Raw blob */ public function getBlob($hash) { return shell_exec(sprintf('GIT_DIR=%s git-cat-file blob %s', escapeshellarg($this->repo), escapeshellarg($hash))); } /** * Get the branches. * * @return array Branches. */ public function getBranches() { $out = array(); exec(sprintf('GIT_DIR=%s git branch', escapeshellarg($this->repo)), &$out); $res = array(); foreach ($out as $b) { $res[] = substr($b, 2); } return $res; } /** * Get commit details. * * @param string Commit ('HEAD'). * @return array Changes. */ public function getCommit($commit='HEAD') { $cmd = sprintf('GIT_DIR=%s git show --date=iso --pretty=format:%s %s', escapeshellarg($this->repo), "'".$this->mediumtree_fmt."'", escapeshellarg($commit)); $out = array(); exec($cmd, &$out); $log = array(); $change = array(); $inchange = false; foreach ($out as $line) { if (!$inchange and 0 === strpos($line, 'diff --git a')) { $inchange = true; } if ($inchange) { $change[] = $line; } else { $log[] = $line; } } $out = self::parseLog($log, 4); $out[0]->changes = implode("\n", $change); return $out[0]; } /** * Get latest changes. * * @param string Commit ('HEAD'). * @param int Number of changes (10). * @return array Changes. */ public function getChangeLog($commit='HEAD', $n=10) { if ($n === null) $n = ''; else $n = ' -'.$n; $cmd = sprintf('GIT_DIR=%s git log%s --date=iso --pretty=format:\'%s\' %s', escapeshellarg($this->repo), $n, $this->mediumtree_fmt, escapeshellarg($commit)); $out = array(); exec($cmd, &$out); return self::parseLog($out, 4); } /** * Parse the log lines of a --pretty=medium log output. * * @param array Lines. * @param int Number of lines in the headers (3) * @return array Change log. */ public static function parseLog($lines, $hdrs=3) { $res = array(); $c = array(); $i = 0; $hdrs += 2; foreach ($lines as $line) { $i++; if (0 === strpos($line, 'commit')) { if (count($c) > 0) { $c['full_message'] = trim($c['full_message']); $res[] = (object) $c; } $c = array(); $c['commit'] = trim(substr($line, 7)); $c['full_message'] = ''; $i=1; continue; } if ($i == $hdrs) { $c['title'] = trim($line); continue; } $match = array(); if (preg_match('/(\S+)\s*:\s*(.*)/', $line, $match)) { $match[1] = strtolower($match[1]); $c[$match[1]] = trim($match[2]); if ($match[1] == 'date') { $c['date'] = gmdate('Y-m-d H:i:s', strtotime($match[2])); } continue; } if ($i > ($hdrs+1)) { $c['full_message'] .= trim($line)."\n"; continue; } } $c['full_message'] = trim($c['full_message']); $res[] = (object) $c; return $res; } }