From 772bac4b4352e77da4bacac7ad7244277756fd24 Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Fri, 18 Jul 2008 22:40:19 -0400 Subject: [PATCH] Initial commit; implement enough code to get Git_Repo->log() semi-working. Signed-off-by: Edward Z. Yang --- library/Git.php | 113 ++++++++++++++++++++++++++++++++++++++++++++ library/Git/Actor.php | 37 +++++++++++++++ library/Git/Commit.php | 116 ++++++++++++++++++++++++++++++++++++++++++++++ library/Git/Exception.php | 13 ++++++ library/Git/Repo.php | 90 +++++++++++++++++++++++++++++++++++ tests/GitTest.php | 40 ++++++++++++++++ tests/index.php | 19 ++++++++ 7 files changed, 428 insertions(+) create mode 100644 library/Git.php create mode 100644 library/Git/Actor.php create mode 100644 library/Git/Commit.php create mode 100644 library/Git/Exception.php create mode 100644 library/Git/Repo.php create mode 100644 tests/GitTest.php create mode 100644 tests/index.php diff --git a/library/Git.php b/library/Git.php new file mode 100644 index 0000000..5571f2e --- /dev/null +++ b/library/Git.php @@ -0,0 +1,113 @@ +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 index 0000000..de3504a --- /dev/null +++ b/library/Git/Actor.php @@ -0,0 +1,37 @@ +name = $name; + $this->email = $email; + } + public function __toString() { + return $this->name; + } + + /** + * Parses a string from John Doe 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 index 0000000..1bd8fcf --- /dev/null +++ b/library/Git/Commit.php @@ -0,0 +1,116 @@ + 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 ', 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 index 0000000..b6ab19c --- /dev/null +++ b/library/Git/Exception.php @@ -0,0 +1,13 @@ +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 index 0000000..a6a0cc1 --- /dev/null +++ b/tests/GitTest.php @@ -0,0 +1,40 @@ +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 index 0000000..9ba843b --- /dev/null +++ b/tests/index.php @@ -0,0 +1,19 @@ +createSuiteFromClasses('PHPGit Tests', array( + 'GitTest' +)); +$result = $suite->run(new DefaultReporter()); +if (SimpleReporter::inCli()) { + exit($result ? 0 : 1); +} -- 2.11.4.GIT