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";
+foreach ($repo->getTags() as $tag => $hash) {
+ echo "- ", htmlspecialchars($tag), " →
", sha1_hex($hash), " \n";
+}
+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);
-