Initial commit; implement enough code to get Git_Repo->log() semi-working.
authorEdward Z. Yang <edwardzyang@thewritingpot.com>
Sat, 19 Jul 2008 02:40:19 +0000 (18 22:40 -0400)
committerEdward Z. Yang <edwardzyang@thewritingpot.com>
Sat, 19 Jul 2008 02:40:19 +0000 (18 22:40 -0400)
Signed-off-by: Edward Z. Yang <edwardzyang@thewritingpot.com>
library/Git.php [new file with mode: 0644]
library/Git/Actor.php [new file with mode: 0644]
library/Git/Commit.php [new file with mode: 0644]
library/Git/Exception.php [new file with mode: 0644]
library/Git/Repo.php [new file with mode: 0644]
tests/GitTest.php [new file with mode: 0644]
tests/index.php [new file with mode: 0644]

diff --git a/library/Git.php b/library/Git.php
new file mode 100644 (file)
index 0000000..5571f2e
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+
+require_once 'Git/Actor.php';
+require_once 'Git/Commit.php';
+require_once 'Git/Exception.php';
+require_once 'Git/Repo.php';
+
+/**
+ * Wrapper for Git executable. Lowest level interface.
+ */
+class Git
+{
+
+    const S = DIRECTORY_SEPARATOR;
+
+    /**
+     * Git executable to invoke. You can replace this with a full path
+     * to your Git executable/wrapper script, but be sure to escape
+     * everything properly!
+     */
+    static public $git = 'git';
+
+    /**
+     * Current working directory.
+     */
+    protected $dir;
+
+    /**
+     * @param $dir Current working directory
+     */
+    public function __construct($dir) {
+        $this->dir = $dir;
+    }
+
+    /**
+     * Executes a command on shell and returns output.
+     */
+    public function execute($command) {
+        return shell_exec($command);
+    }
+
+    /**
+     * Transforms an associative array of arguments to command line options.
+     * Arguments can be like 'r' or 'no-commit'.
+     * @note Original Python version kwargs used 'no_commit'
+     *       form due to Python conventions. We decided to use a more direct
+     *       approach because our associative array allow them.
+     */
+    public function transformArgs($kwargs) {
+        $args = array();
+        foreach ($kwargs as $k => $v) {
+            if (strlen($k) == 1) {
+                if ($v === true) $args[] = "-$k";
+                else $args[] = "-$k $v";
+            } else {
+                // $k = dashify($k);
+                if ($v === true) $args[] = "--$k";
+                else $args[] = "--$k=$v";
+            }
+        }
+        return $args;
+    }
+
+    /**
+     * Runs a given Git command with the specified arguments and return
+     * the result as a string.
+     * @param $method Name of command, but camelCased instead of dash-ified.
+     * @param $args Array of arguments. There is actually only one argument,
+     *        which is an array of associative and numerically indexed
+     *        parameters.
+     * @return String output.
+     */
+    public function __call($method, $raw_args) {
+        // split out "kwargs" (to be converted to options) from regular
+        // "args" (which get inserted normally)
+        $kwargs = $raw_args[0];
+        $args = array();
+        for ($i = 0; isset($kwargs[$i]); $i++) {
+            $args[] = $kwargs[$i];
+            unset($kwargs[$i]);
+        }
+        // $args and $kwargs are interesting
+        $opt_args = $this->transformArgs($kwargs);
+        // parse through $args again to determine which ones are actually kwargs
+        $ext_args = array();
+        foreach ($args as $v) {
+            if ($v == '--') $ext_args[] = $v;
+            else $ext_args[] = $this->escape($v);
+        }
+        // Full arguments
+        $args = array_merge($opt_args, $ext_args);
+        // Convert methodName to method-name (our equivalent of dashify).
+        // This is kind of inefficient.
+        $command = '';
+        for ($i = 0, $max = strlen($method); $i < $max; $i++) {
+            $c = $method[$i];
+            $command .= ctype_upper($c) ? '-' . strtolower($c) : $c;
+        }
+        $call = self::$git . " --git-dir={$this->dir} $command " . implode(' ', $args);
+        // var_dump($call);
+        $result = $this->execute($call);
+        return $result;
+    }
+
+    /**
+     * Escape argument for shell. I don't think this actually works properly
+     * on Windows.
+     */
+    public function escape($v) {
+        return str_replace("'", "\\\\'", $v);
+    }
+
+}
diff --git a/library/Git/Actor.php b/library/Git/Actor.php
new file mode 100644 (file)
index 0000000..de3504a
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * An actor, either a committer or an author.
+ */
+class Git_Actor {
+
+    /**
+     * Name of actor.
+     */
+    public $name;
+
+    /**
+     * Email of actor.
+     */
+    public $email;
+
+    public function __construct($name, $email = null) {
+        $this->name = $name;
+        $this->email = $email;
+    }
+    public function __toString() {
+        return $this->name;
+    }
+
+    /**
+     * Parses a string from John Doe <jdoe@example.com> to this object.
+     */
+    public static function fromString($string) {
+        if (preg_match('/(.*) <(.+?)>/', $string, $matches)) {
+            list($x, $name, $email) = $matches;
+            return new Git_Actor($name, $email);
+        } else {
+            return new Git_Actor($string);
+        }
+    }
+}
diff --git a/library/Git/Commit.php b/library/Git/Commit.php
new file mode 100644 (file)
index 0000000..1bd8fcf
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * Represents a commit in the Git repository.
+ */
+class Git_Commit {
+
+    public $repo, $id, $tree, $author, $authoredDate, $committer,
+        $committedDate, $message, $parents = array();
+
+    /**
+     * @param $repo Git_Repo of this commit.
+     * @param $kwargs Hash of info about this commit, specifically:
+     *      'id' => Id of commit
+     *      'parents' => List of commit IDs (converted to Git_Commit objects)
+     *      'tree' => Tree ID (converted to Git_Tree object)
+     *      'author' => Author string
+     *      'authoredDate' => Authored DateTime
+     *      'committer' => Committer string
+     *      'committedDate' => Committed DateTime
+     *      'message' => First line of commit message
+     */
+    public function __construct($repo, $kwargs) {
+        $this->repo = $repo;
+        foreach ($kwargs as $k => $v) {
+            if ($k == 'parents') {
+                foreach ($v as $id) {
+                    $this->parents[] = new Git_Commit($repo, array('id' => $id));
+                }
+                continue;
+            } elseif ($k == 'tree') {
+                //$this->tree = new Git_Tree($repo, array('id' => $v));
+                // :TODO: Implement Git_Tree
+                $this->tree = $v;
+                continue;
+            }
+            $this->$k = $v;
+        }
+    }
+
+    // __bake__
+
+    /**
+     * Return a shortened representation of Git's commit ID.
+     */
+    public function idAbbrev() {
+        return substr($this->id, 0, 7);
+    }
+
+    // public static(?) function count() {
+    // public static function findAll($repo, $ref, $kwargs) {
+
+    /**
+     * Parses out commit information from git log --pretty raw into an
+     * array of Commit objects.
+     * @param $repo Git_Repo
+     * @param $text Text from command
+     * @return Array of Git_Commit objects
+     */
+    public static function listFromString($repo, $text) {
+        $lines = explode("\n", $text);
+        for ($i = 0, $c = count($lines); $i < $c; $i++) {
+
+            $id   = self::_l($lines, $i, true);
+            $tree = self::_l($lines, $i, true);
+
+            $parents = array();
+            while ($lines && strncmp($lines[$i], 'parent', 6) === 0) {
+                $parents[] = self::_l($lines, $i, true);
+            }
+            list($author, $authoredDate) = self::actor(self::_l($lines, $i));
+            list($committer, $committedDate) = self::actor(self::_l($lines, $i));
+
+            $messages = array();
+            while ($lines && strncmp($lines[$i], '    ', 4) === 0) {
+                $messages[] = trim(self::_l($lines, $i));
+            }
+
+            $message = $messages ? $messages[0] : '';
+
+            $commits[] = new Git_Commit($repo, compact(
+                'id', 'parents', 'tree', 'author', 'authoredDate',
+                'committer', 'committedDate', 'message'
+            ));
+        }
+        return $commits;
+    }
+
+    /**
+     * Grabs the current line, advances the index forward, and parses
+     * out the last bit.
+     * @param $lines Array of lines
+     * @param &$i Index in $lines array
+     * @param $grab_last Whether or not to retrieve the span of text after
+     *        the last whitespace.
+     */
+    private static function _l($lines, &$i, $grab_last = false) {
+        $line = $lines[$i++];
+        if ($grab_last) {
+            $line = trim($line);
+            $line = substr($line, strrpos($line, ' ') + 1);
+        }
+        return $line;
+    }
+
+    /**
+     * Parse out actor (author/committer) information.
+     * @returns array('Actor name <email>', timestamp)
+     */
+    public static function actor($line) {
+        preg_match('/^.+? (.*) (\d+) .*$/', $line, $matches);
+        list($x, $actor, $epoch) = $matches;
+        return array(Git_Actor::fromString($actor), new DateTime($epoch));
+    }
+
+}
diff --git a/library/Git/Exception.php b/library/Git/Exception.php
new file mode 100644 (file)
index 0000000..b6ab19c
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+
+class Git_Exception extends Exception
+{
+    
+}
+
+class Git_Exception_InvalidRepository extends Git_Exception
+{
+    public function __construct($path) {
+        parent::__construct("Git repository $path does not exist");
+    }
+}
diff --git a/library/Git/Repo.php b/library/Git/Repo.php
new file mode 100644 (file)
index 0000000..9f60088
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * Represents a Git repository, and allows access to history, snapshots,
+ * commits, et cetera.
+ */
+class Git_Repo {
+
+    const DAEMON_EXPORT_FILE = 'git-daemon-export-ok';
+
+    /**
+     * Path to this repository.
+     */
+    protected $path;
+
+    /**
+     * Whether or not this is a bare repository.
+     */
+    protected $bare;
+
+    /**
+     * Interface to Git itself.
+     */
+    protected $git;
+
+    /**
+     * @param $path Path to the repository (either working copy or bare)
+     */
+    public function __construct($path) {
+        $path = realpath($path);
+        if (file_exists($p = $path . Git::S . '.git')) {
+            $this->path = $p;
+            $this->bare = false;
+        } elseif (file_exists($path) && strlen($path) >= 4 && substr($path, -4) == '.git') {
+            $this->path = $path;
+            $this->bare = true;
+        } elseif (!file_exists($path)) {
+            throw new Git_Exception_InvalidRepository();
+        }
+        $this->git = new Git($this->path);
+    }
+
+    // * means will implement soon
+
+    // public function description() {
+    // * public function heads() {
+    // * public function branches() {return $this->heads();}
+    // public function tags() {
+    // * public function commits($start, $max_count, $skip) {
+    // public function commitsBetween($frm, $to) {
+    // public function commitsSince($start = 'master', $since) {
+    // public function commitCount($start = 'master') {
+    // * public function commit($id) {
+    // public function commitDeltasFrom($other_repo, $ref = 'master', $other_ref = 'master') {
+    // * public function tree($treeish = 'master', $paths = array()) {
+    // * public function blob($id) {
+
+    /**
+     * Returns the commit log for a treeish entity.
+     * @param $commit Commit to get log of, or commit range like begin..end
+     * @param $path Path (or paths) to get logs for
+     * @param $kwargs Extra arguments, see Git->transformArgs()
+     */
+    public function log($commit = 'master', $path = null, $kwargs = array()) {
+        $options = array_merge(array('pretty' => 'raw'), $kwargs);
+        if ($path) {
+            $arg = array($commit, '--', $path);
+        } else {
+            $arg = array($commit);
+        }
+        $commits = $this->git->log($path, $options);
+        return Commit::listFromString($commits);
+    }
+
+    // public function diff($a, $b, $paths = array()) {
+    // public function commitDiff($commit) {
+    // public static function initBare($path, $kwargs) {
+    // public static function forkBare($path, $kwargs) {
+    // public function archiveTar($treeish = 'master', $prefix = null) {
+    // public function archiveTarGz($treeish = 'master', $prefix = null) {
+    // public function enableDaemonServe() {
+    // public function disableDaemonServe() {
+    // ? private function _getAlternates() {
+    // ? private function _setAlternates() {
+
+    public function __toString() {
+        return '(Git_Repo "'. $this->path .'")';
+    }
+
+}
diff --git a/tests/GitTest.php b/tests/GitTest.php
new file mode 100644 (file)
index 0000000..a6a0cc1
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+Mock::generatePartial(
+    'Git',
+    'GitPartialMock',
+    array('execute'));
+
+class GitTest extends UnitTestCase
+{
+    function testTransformArgs() {
+        $git = new Git('..');
+        $result = $git->transformArgs(array(
+            'v' => true,
+            's' => 'ours',
+            'no-commit' => true,
+            'message' => 'test'
+        ));
+        $expect = array(
+            '-v', '-s ours', '--no-commit', '--message=test',
+        );
+        $this->assertIdentical($result, $expect);
+    }
+
+    function testEscape() {
+        $git = new Git('..');
+        $result = $git->escape("foo's");
+        $expect = "foo\\\\'s";
+        $this->assertIdentical($result, $expect);
+    }
+
+    function testCall() {
+        $git = new GitPartialMock();
+        $git->__construct('dir');
+        $git->expectOnce('execute', array('git --git-dir=dir cherry-pick -s --no-commit f533ebca'));
+        $git->setReturnValue('execute', $expect = 'Result');
+        $result = $git->cherryPick(array('--no-commit', 's' => true, 'f533ebca'));
+        $this->assertIdentical($result, $expect);
+    }
+
+}
diff --git a/tests/index.php b/tests/index.php
new file mode 100644 (file)
index 0000000..9ba843b
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+chdir(dirname(__FILE__));
+$simpletest = '..';
+require '../test-settings.php';
+require_once $simpletest . '/unit_tester.php';
+require_once $simpletest . '/mock_objects.php';
+
+require_once '../library/Git.php';
+require_once 'GitTest.php';
+
+$loader = new SimpleFileLoader();
+$suite = $loader->createSuiteFromClasses('PHPGit Tests', array(
+    'GitTest'
+));
+$result = $suite->run(new DefaultReporter());
+if (SimpleReporter::inCli()) {
+    exit($result ? 0 : 1);
+}