X-Git-Url: https://git.cweiske.de/phorkie.git/blobdiff_plain/2b4b34a76f42841e964a549fc64c02ba4f60a3f4..0e362c3a8a13e7e2ae7d5c1a5d2e5eaa163d153f:/src/phorkie/Repository.php diff --git a/src/phorkie/Repository.php b/src/phorkie/Repository.php index fef5656..9de2e1a 100644 --- a/src/phorkie/Repository.php +++ b/src/phorkie/Repository.php @@ -12,11 +12,33 @@ class Repository public $id; /** - * Full path to the git repository + * Full path to the .git repository * * @var string */ - public $repoDir; + public $gitDir; + + /** + * Full path to the work tree directory + * + * @var string + */ + public $workDir; + + /** + * Revision of the repository that shall be shown + * + * @var string + */ + public $hash; + + /** + * Commit message of the last (or current) revision + * + * @var string + */ + public $message; + /** * Load Repository data from GET-Request @@ -33,13 +55,14 @@ class Repository if (!is_numeric($_GET['id'])) { throw new Exception_Input('Paste ID not numeric'); } - $this->id = (int)$_GET['id']; - - $repoDir = $GLOBALS['phorkie']['cfg']['repos'] . '/' . $this->id; - if (!is_dir($repoDir)) { - throw new Exception_NotFound('Paste not found'); + if (isset($_GET['rev'])) { + $this->hash = $_GET['rev']; } - $this->repoDir = $repoDir; + + $this->id = (int)$_GET['id']; + $this->loadDirs(); + $this->loadHash(); + $this->loadMessage(); } public function loadById($id) @@ -48,48 +71,161 @@ class Repository throw new Exception_Input('Paste ID not numeric'); } $this->id = (int)$id; + $this->loadDirs(); + $this->loadHash(); + } + + protected function loadDirs() + { + $gitDir = $GLOBALS['phorkie']['cfg']['gitdir'] . '/' . $this->id . '.git'; + if (!is_dir($gitDir)) { + throw new Exception_NotFound( + sprintf('Paste %d .git dir not found', $this->id) + ); + } + $this->gitDir = $gitDir; - $repoDir = $GLOBALS['phorkie']['cfg']['repos'] . '/' . $this->id; - if (!is_dir($repoDir)) { - throw new Exception_NotFound('Paste not found'); + $workDir = $GLOBALS['phorkie']['cfg']['workdir'] . '/' . $this->id; + if (!is_dir($workDir)) { + throw new Exception_NotFound( + sprintf('Paste %d work dir not found', $this->id) + ); + } + $this->workDir = $workDir; + } + + public function loadHash() + { + return; + if ($this->hash !== null) { + return; + } + + $output = $this->getVc()->getCommand('log') + ->setOption('pretty', 'format:%H') + ->setOption('max-count', 1) + ->execute(); + $output = trim($output); + if (strlen($output) !== 40) { + throw new Exception( + 'Loading commit hash failed: ' . $output + ); + } + $this->hash = $output; + } + + /** + * Populates $this->message + * + * @return void + */ + public function loadMessage() + { + $rev = (isset($this->hash)) ? $this->hash : 'HEAD'; + $output = $this->getVc()->getCommand('log') + ->setOption('oneline') + ->addArgument('-1') + ->addArgument($rev) + ->execute(); + $output = trim($output); + if (strpos($output, ' ') > 0) { + $output = substr($output, strpos($output, ' '), strlen($output)); + $this->message = trim($output); + } else { + $this->message = "This commit message intentionally left blank."; } - $this->repoDir = $repoDir; } public function getVc() { - return new \VersionControl_Git($this->repoDir); + return new \VersionControl_Git($this->gitDir); } /** * Loads the list of files in this repository * - * @return File[] Array of files + * @return File[] Array of file objects */ public function getFiles() { - $files = glob($this->repoDir . '/*'); + $files = $this->getFilePaths(); $arFiles = array(); - foreach ($files as $path) { - $arFiles[] = new File($path, $this); + foreach ($files as $name) { + $arFiles[] = new File($name, $this); } return $arFiles; } - public function getFileByName($name, $bHasToExist = true) + /** + * Decodes unicode characters in git filenames + * They begin and end with double quote characters, and may contain + * backslash + 3 letter octal code numbers representing the character. + * + * For example, + * > "t\303\244st.txt" + * means + * > täst.txt + * + * On the shell, you can pipe them into "printf" and have them decoded. + * + * @param string Encoded git file name + * + * @return string Decoded file name + */ + protected function decodeFileName($name) + { + $name = substr($name, 1, -1); + $name = str_replace('\"', '"', $name); + $name = preg_replace_callback( + '#\\\\[0-7]{3}#', + function ($ar) { + return chr(octdec(substr($ar[0], 1))); + }, + $name + ); + return $name; + } + + /** + * Return array with all file paths in this repository + * + * @return array + */ + protected function getFilePaths() { - $base = basename($name); - if ($base != $name) { - throw new Exception('No directories supported for now'); + if ($this->hash === null) { + $hash = 'HEAD'; + } else { + $hash = $this->hash; } + $output = $this->getVc()->getCommand('ls-tree') + ->setOption('r') + ->setOption('name-only') + ->addArgument($hash) + ->execute(); + $files = explode("\n", trim($output)); + foreach ($files as &$file) { + if ($file{0} == '"') { + $file = $this->decodeFileName($file); + } + } + return $files; + } + + public function getFileByName($name, $bHasToExist = true) + { + $name = Tools::sanitizeFilename($name); if ($name == '') { throw new Exception_Input('Empty file name given'); } - $path = $this->repoDir . '/' . $base; - if ($bHasToExist && !is_readable($path)) { - throw new Exception_Input('File does not exist'); + + if ($bHasToExist) { + $files = $this->getFilePaths(); + if (array_search($name, $files) === false) { + throw new Exception_Input('File does not exist'); + } } - return new File($path, $this); + return new File($name, $this); } public function hasFile($name) @@ -110,48 +246,177 @@ class Repository */ public function delete() { - return Tools::recursiveDelete($this->repoDir); + $db = new Database(); + $db->getIndexer()->deleteRepo($this); + + $bOk = Tools::recursiveDelete($this->workDir) + && Tools::recursiveDelete($this->gitDir); + + $not = new Notificator(); + $not->delete($this); + + return $bOk; + } + + public function getTitle() + { + $desc = $this->getDescription(); + if (trim($desc) != '') { + return $desc; + } + + return 'paste #' . $this->id; } public function getDescription() { - if (!is_readable($this->repoDir . '/.git/description')) { + if (!is_readable($this->gitDir . '/description')) { return null; } - return file_get_contents($this->repoDir . '/.git/description'); + return file_get_contents($this->gitDir . '/description'); } public function setDescription($description) { - file_put_contents($this->repoDir . '/.git/description', $description); + file_put_contents($this->gitDir . '/description', $description); + } + + /** + * @return array Array with keys "email" and "name" + */ + public function getOwner() + { + try { + $name = $this->getVc()->getCommand('config') + ->addArgument('owner.name')->execute(); + } catch (\VersionControl_Git_Exception $e) { + $name = $GLOBALS['phorkie']['auth']['anonymousName']; + } + try { + $email = $this->getVc()->getCommand('config') + ->addArgument('owner.email')->execute(); + } catch (\VersionControl_Git_Exception $e) { + $email = $GLOBALS['phorkie']['auth']['anonymousEmail']; + } + + return array('name' => trim($name), 'email' => trim($email)); } /** * Get a link to the repository * - * @param string $type Link type. Supported are: - * - "edit" - * - "display" - * - "fork" + * @param string $type Link type. Supported are: + * - "edit" + * - "delete" + * - "delete-confirm" + * - "display" + * - "fork" + * - "revision" + * @param string $option Additional link option, e.g. revision number + * @param boolean $full Return full URL or normal relative * * @return string */ - public function getLink($type) + public function getLink($type, $option = null, $full = false) { if ($type == 'edit') { - return '/' . $this->id . '/edit'; + $link = $this->id . '/edit'; } else if ($type == 'display') { - return '/' . $this->id; + $link = $this->id; } else if ($type == 'fork') { - return '/' . $this->id . '/fork'; + $link = $this->id . '/fork'; + } else if ($type == 'doap') { + $link = $this->id . '/doap'; } else if ($type == 'delete') { - return '/' . $this->id . '/delete'; + $link = $this->id . '/delete'; } else if ($type == 'delete-confirm') { - return '/' . $this->id . '/delete/confirm'; + $link = $this->id . '/delete/confirm'; + } else if ($type == 'remotefork') { + return 'web+fork:' . $this->getLink('display', null, true); + } else if ($type == 'revision') { + $link = $this->id . '/rev/' . $option; + } else if ($type == 'linkback') { + $link = $this->id . '/linkback'; + } else { + throw new Exception('Unknown link type'); + } + + if ($full) { + $link = Tools::fullUrl($link); + } + return $link; + } + + public function getCloneURL($public = true) + { + $var = $public ? 'public' : 'private'; + if (isset($GLOBALS['phorkie']['cfg']['git'][$var])) { + return $GLOBALS['phorkie']['cfg']['git'][$var] . $this->id . '.git'; + } + return null; + } + + /** + * Returns the history of the repository. + * We don't use VersionControl_Git's rev list fetcher since it does not + * give us separate email addresses and names, and it does not give us + * the amount of changed (added/deleted) lines. + * + * @return array Array of history objects + */ + public function getHistory() + { + $output = $this->getVc()->getCommand('log') + ->setOption('pretty', 'format:commit %H%n%at%n%an%n%ae') + ->setOption('max-count', 10) + ->setOption('shortstat') + ->execute(); + + $arCommits = array(); + $arOutput = explode("\n", $output); + $lines = count($arOutput); + $current = 0; + while ($current < $lines) { + $commit = new Repository_Commit(); + list($name,$commit->hash) = explode(' ', $arOutput[$current]); + if ($name !== 'commit') { + throw new Exception( + 'Git log output format not as expected: ' . $arOutput[$current] + ); + } + $commit->committerTime = $arOutput[$current + 1]; + $commit->committerName = $arOutput[$current + 2]; + $commit->committerEmail = $arOutput[$current + 3]; + + if (substr($arOutput[$current + 4], 0, 1) != ' ') { + //commit without changed lines + $arCommits[] = $commit; + $current += 4; + continue; + } + + $arLineParts = explode(' ', trim($arOutput[$current + 4])); + $commit->filesChanged = $arLineParts[0]; + $commit->linesAdded = $arLineParts[3]; + if (isset($arLineParts[5])) { + $commit->linesDeleted = $arLineParts[5]; + } + + $current += 6; + + $arCommits[] = $commit; } - throw new Exception('Unknown link type'); + + return $arCommits; } + /** + * @return Repository_ConnectionInfo + */ + public function getConnectionInfo() + { + return new Repository_ConnectionInfo($this); + } } ?>