diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..da7151c --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "frozen-doe.net/glip", + "description": "Fork of Git Library In PHP", + "license": "GPL-2.0+", + "authors": [ + { + "name": "Patrik Fimml" + }, { + "name": "Josef Kufner", + "email": "jk@frozen-doe.net" + } + ], + "require": { + "php": ">=5.1" + }, + "autoload": { + "classmap": [ "lib/" ] + } +} + diff --git a/doc/mainpage.doxygen b/doc/mainpage.doxygen index 5e301be..1a9d31e 100644 --- a/doc/mainpage.doxygen +++ b/doc/mainpage.doxygen @@ -63,15 +63,65 @@ $master_name = $repo->getTip('master'); $master = $repo->getObject($master_name); @endcode -@todo It should be possible to look up any ref, not only branches. - In a common git repository, this will give you a GitCommit instance in $master, referring to the last commit made to the @em master branch. +You can also lookup tag or any ref: + +@code +$tag_name = $repo->getTip('v1.0'); +$tag = $repo->getObject($tag_name); +@endcode + +@code +$origin_master_name = $repo->getTip('refs/remotes/origin/master'); +$origin_master = $repo->getObject($tag_name); +@endcode + +When you get bored with branches and tags, try Git::getHead(): + +@code +$head_name = $repo->getHead(); +$last_commit = $repo->getObject($head_name); + +echo $repo->getHead(false); // false means 'Do not resolve ref, give me human readable name instead.' +@endcode + GitObject%s hold all related data in public attributes. For example, GitCommit::$summary contains the commit summary of a particular commit. Utility functions are provided to make it easier to perform common functions. +@subsection tags_refs Working with tags and refs + +Tags and refs can be listed using Git::getTags() and Git::getRefs(): + +@code +echo "\n"; +@endcode + +Usage of Git::getRefs is same, but it returns 'refs/tags/v1.0' instead of 'v1.0'. + +@subsection describe Git Describe + +There is partial reimplementation of handy 'git describe' command in Git::describe(): + +@code +$master_name = $repo->getTip('master'); +$master = $repo->getObject($master_name); // This is optional, Git::describe() can do this for you. + +$describe = $repo->describe($master); // You can use $master_name here also. +echo htmlspecialchars($describe); +@endcode + +@note Current implementation is way more stupid than original, but it should be +enough for usual use. + +@attention Hash digits included in result string may not uniquely describe commit. There is no check for ambiguous abbrevations. + @section writing Writing to a git repository To change things in a repository, you currently have to directly make changes diff --git a/lib/git.class.php b/lib/git.class.php index 8828fa8..1226b01 100644 --- a/lib/git.class.php +++ b/lib/git.class.php @@ -1,6 +1,7 @@ * * This file is part of glip. * @@ -18,13 +19,6 @@ * along with glip. If not, see . */ -require_once('binary.class.php'); -require_once('git_object.class.php'); -require_once('git_blob.class.php'); -require_once('git_commit.class.php'); -require_once('git_commit_stamp.class.php'); -require_once('git_tree.class.php'); - /** * @relates Git * @brief Convert a SHA-1 hash from hexadecimal to binary representation. @@ -90,7 +84,27 @@ static public function getTypeName($type) public function __construct($dir) { $this->dir = realpath($dir); - if ($this->dir === FALSE || !@is_dir($this->dir)) + if ($this->dir === FALSE) + throw new Exception(sprintf('repository not found: %s', $dir)); + + if (is_file($this->dir)) { + $repo = file_get_contents($this->dir); + if (preg_match('/^gitdir: (.*)/', $repo, $m)) { + if ($m[1][0] == '.') { + if (basename($this->dir) == '.git') { + $this->dir = rtrim(dirname($this->dir), '/').'/'.$m[1]; + } else { + $this->dir = rtrim($this->dir, '/').'/'.$m[1]; + } + } else { + $this->dir = $m[1]; + } + } else { + throw new Exception(sprintf('unknown repository format: %s', $this->dir)); + } + } + + if (!@is_dir($this->dir)) throw new Exception(sprintf('not a directory: %s', $dir)); $this->packs = array(); @@ -403,36 +417,215 @@ public function getObject($name) } /** - * @brief Look up a branch. + * @brief Look up a branch or tag. * - * @param $branch (string) The branch to look up, defaulting to @em master. + * @param $branch (string) The branch or tag to look up, defaulting to @em master. * @returns (string) The tip of the branch (binary sha1). */ public function getTip($branch='master') { - $subpath = sprintf('refs/heads/%s', $branch); - $path = sprintf('%s/%s', $this->dir, $subpath); - if (file_exists($path)) - return sha1_bin(file_get_contents($path)); - $path = sprintf('%s/packed-refs', $this->dir); - if (file_exists($path)) - { - $head = NULL; - $f = fopen($path, 'rb'); - flock($f, LOCK_SH); - while ($head === NULL && ($line = fgets($f)) !== FALSE) - { - if ($line{0} == '#') - continue; - $parts = explode(' ', trim($line)); - if (count($parts) == 2 && $parts[1] == $subpath) - $head = sha1_bin($parts[0]); - } - fclose($f); - if ($head !== NULL) - return $head; - } - throw new Exception(sprintf('no such branch: %s', $branch)); + if (strchr($branch, '/') !== FALSE) + { + $subpath = array($branch); + } + else + { + $subpath = array( + sprintf('refs/heads/%s', $branch), + sprintf('refs/tags/%s', $branch) + ); + } + foreach ($subpath as $sp) + { + $path = sprintf('%s/%s', $this->dir, $sp); + if (file_exists($path)) + return sha1_bin(file_get_contents($path)); + } + $path = sprintf('%s/packed-refs', $this->dir); + if (file_exists($path)) + { + $head = NULL; + $f = fopen($path, 'rb'); + flock($f, LOCK_SH); + while ($head === NULL && ($line = fgets($f)) !== FALSE) + { + if ($line{0} == '#') + continue; + $parts = explode(' ', trim($line)); + if (count($parts) == 2 && in_array($parts[1], $subpath)) + $head = sha1_bin($parts[0]); + } + fclose($f); + if ($head !== NULL) + return $head; + } + throw new Exception(sprintf('no such branch: %s', $branch)); + } + + + /** + * @brief Get current head + * + * @param $resolve (bool) It true, returned value is binary sha1, otherwise readable string is returned. + * @returns (string) Current head as readable string or binary sha1 (depends on $resolve arg.). On error, FALSE is returned. + */ + public function getHead($resolve = true) + { + $content = file_get_contents($this->dir.'/HEAD'); + + if ($content === FALSE) + { + return FALSE; + } + else if (sscanf($content, 'ref: %s', $head) == 1) + { + return $resolve ? $this->getTip($head) : $head; + } + else if (sscanf($content, '%[0-9a-fA-F]s', $hash) == 1) + { + return $resolve ? sha1_bin($hash) : $hash; + } + else + { + return FALSE; + } + } + + + /** + * @brief List all known refs + * + * @returns (array) List of all known refs (ref -> binary sha1). + */ + public function getRefs() + { + $refs = array(); + + // refs in files + foreach (array('refs/heads', 'refs/tags') as $subpath) + { + $path = sprintf('%s/%s', $this->dir, $subpath); + foreach(scandir($path) as $f) + { + $file = $path.'/'.$f; + if ($f[0] != '.' && is_file($file)) { + $refs[$subpath.'/'.$f] = sha1_bin(file_get_contents($file)); + } + } + } + + // packed refs + $path = sprintf('%s/packed-refs', $this->dir); + if (file_exists($path)) + { + $head = NULL; + $f = fopen($path, 'rb'); + flock($f, LOCK_SH); + while ($head === NULL && ($line = fgets($f)) !== FALSE) + { + if ($line{0} == '#') + continue; + $parts = explode(' ', trim($line)); + if (count($parts) == 2) + $refs[$parts[1]] = sha1_bin($parts[0]); + } + fclose($f); + if ($head !== NULL) + return $head; + } + + return $refs; + } + + + /** + * @brief List all tags + * + * @returns (array) List of all tags (tag name -> binary sha1). + */ + public function getTags() + { + $tags = array(); + foreach($this->getRefs() as $ref => $hash) + { + if (strncmp($ref, 'refs/tags/', 10) == 0) + { + $tags[substr($ref, 10)] = $hash; + } + } + return $tags; + } + + /** + * @brief Implementation of 'git describe' (partial) + * + * @param $commit (GitCommit) Commit to describe or binary sha1 of commit. + * @param $abbrev (int) Hash digits count. Hash may not be uniqe, no checks done. + * @returns (string) 'git describe' string + */ + public function describe($commit, $abbrev = 7) + { + if (is_string($commit)) + { + $commit = $this->getObject($commit); + } + + if ($commit->getType() == Git::OBJ_TAG) + { + return $commit->tag; + } + + // Load tags and get their objects + $tags = array(); + foreach($this->getTags() as $tag_name => $hash) + { + $t = $this->getObject($hash); + if ($t->getType() == Git::OBJ_TAG) + { + foreach($t->getObjects() as $obj) + { + $tags[$obj] = $t->tag; + } + } + } + + $queue = array(array($commit, 0)); + $tag_name = false; + $tag_depth = false; + + // DFS (simplified for DAG) + while (!empty($queue)) + { + list($tag_object, $tag_depth) = array_shift($queue); + $commit_name = $tag_object->getName(); + if (array_key_exists($commit_name, $tags)) + { + // Tag found + $tag_name = $tags[$commit_name]; + break; + } + + // enqueue ancestors + foreach($tag_object->parents as $parent) + { + $parent_object = $this->getObject($parent); + array_push($queue, array($parent_object, $tag_depth + 1)); + } + } + + // Format result + if ($tag_name === false) + { + return substr(sha1_hex($commit->getName()), 0, $abbrev); + } + if ($tag_depth == 0) + { + return $tag_name; + } + else + { + return sprintf('%s-%d-g%s', $tag_name, $tag_depth, substr(sha1_hex($commit->getName()), 0, $abbrev)); + } } } diff --git a/lib/git_blob.class.php b/lib/git_blob.class.php index da6983e..81a6bf9 100644 --- a/lib/git_blob.class.php +++ b/lib/git_blob.class.php @@ -18,8 +18,6 @@ * along with glip. If not, see . */ -require_once('git_object.class.php'); - class GitBlob extends GitObject { /** diff --git a/lib/git_commit.class.php b/lib/git_commit.class.php index 0252a36..d3d0bd5 100644 --- a/lib/git_commit.class.php +++ b/lib/git_commit.class.php @@ -18,9 +18,6 @@ * along with glip. If not, see . */ -require_once('git_object.class.php'); -require_once('git_commit_stamp.class.php'); - class GitCommit extends GitObject { /** diff --git a/lib/git_object.class.php b/lib/git_object.class.php index c4c67c3..b4dd3ef 100644 --- a/lib/git_object.class.php +++ b/lib/git_object.class.php @@ -56,6 +56,8 @@ static public function create($repo, $type) return new GitCommit($repo); if ($type == Git::OBJ_TREE) return new GitTree($repo); + if ($type == Git::OBJ_TAG) + return new GitTag($repo); if ($type == Git::OBJ_BLOB) return new GitBlob($repo); throw new Exception(sprintf('unhandled object type %d', $type)); diff --git a/lib/git_tag.class.php b/lib/git_tag.class.php new file mode 100644 index 0000000..ea73b6e --- /dev/null +++ b/lib/git_tag.class.php @@ -0,0 +1,101 @@ + + * + * This file is part of glip. + * + * glip is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + + * glip is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with glip. If not, see . + */ + +class GitTag extends GitObject +{ + /** + * @brief (string) Tag name + */ + public $tag; + + /** + * @brief (array of GitObject) Targets the tag is pointing to. + * strings. + */ + public $objects; + + /** + * @brief (GitCommitStamp) The committer of this commit. + */ + public $tagger; + + /** + * @brief (string) Commit summary, i.e. the first line of the commit message. + */ + public $summary; + + /** + * @brief (string) Everything after the first line of the commit message. + */ + public $detail; + + public function __construct($repo) + { + parent::__construct($repo, Git::OBJ_TAG); + } + + public function _unserialize($data) + { + $lines = explode("\n", $data); + unset($data); + $meta = array('object' => array()); + while (($line = array_shift($lines)) != '') + { + $parts = explode(' ', $line, 2); + if (!isset($meta[$parts[0]])) + $meta[$parts[0]] = array($parts[1]); + else + $meta[$parts[0]][] = $parts[1]; + } + + $this->tag = $meta['tag'][0]; + $this->objects = array_map('sha1_bin', $meta['object']); + $this->tagger = new GitCommitStamp; + $this->tagger->unserialize($meta['tagger'][0]); + + $this->summary = array_shift($lines); + $this->detail = implode("\n", $lines); + + $this->history = NULL; + } + + public function _serialize() + { + $s = ''; + foreach ($this->objects as $object) + $s .= sprintf("object %s\n", sha1_hex($object)); + $s .= sprintf("tag %s\n", $this->tag); + $s .= sprintf("tagger %s\n", $this->tagger->serialize()); + $s .= "\n".$this->summary."\n".$this->detail; + return $s; + } + + /** + * @brief Get commit the tag is pointing to. + * + * @returns (array of GitCommit) + */ + public function getObjects() + { + return $this->objects; + } + +} + diff --git a/lib/git_tree.class.php b/lib/git_tree.class.php index f7a0495..a60e4d4 100644 --- a/lib/git_tree.class.php +++ b/lib/git_tree.class.php @@ -21,8 +21,6 @@ class GitTreeError extends Exception {} class GitTreeInvalidPathError extends GitTreeError {} -require_once('git_object.class.php'); - class GitTree extends GitObject { public $nodes = array(); diff --git a/lib/glip.php b/lib/glip.php deleted file mode 100644 index a414f36..0000000 --- a/lib/glip.php +++ /dev/null @@ -1,24 +0,0 @@ -. - */ - -$old_include_path = set_include_path(dirname(__FILE__)); -require_once('git.class.php'); -set_include_path($old_include_path); -