Modernize Git to upstream GitRepos, mainly command handling.
authorEdward Z. Yang <edwardzyang@thewritingpot.com>
Sun, 20 Jul 2008 01:34:05 +0000 (19 21:34 -0400)
committerEdward Z. Yang <edwardzyang@thewritingpot.com>
Sun, 20 Jul 2008 01:34:05 +0000 (19 21:34 -0400)
* Convert to use proc_open and COM for command interfacing. This allows
  us to retrieve stdout seperate from stderr, as well as use stdin.
* Implement extended output options. Might be a little buggy, so needs
  testing.
* Remove --git-dir; directory information is transmitted via execution.
* Fix E_NOTICE in Git_Commit
* Add Git_Exception_Command
* Revamp Git_Repo->__construct to recursively search upwards for Git. This
  greatly improves the flexibility of paths we can pass.
* Fix bug with path arrays in Git_Repo->log()
* Turn on error reporting in the sample.php file
* Smoosh single character args, so -s our becomes -sour

Signed-off-by: Edward Z. Yang <edwardzyang@thewritingpot.com>
library/Git.php
library/Git/Commit.php
library/Git/Exception.php
library/Git/Repo.php
library/Git/Tree.php
sample.php
tests/GitTest.php

index 2f098f4..a776342 100644 (file)
@@ -17,8 +17,8 @@ class Git
     const S = DIRECTORY_SEPARATOR;
 
     static protected $executeKwargs = array(
-        'istream', 'with_keep_cwd', 'with_extended_output',
-        'with_exceptions', 'with_raw_output'
+        'istream', 'withKeepCwd', 'withExtendedOutput',
+        'withExceptions', 'withRawOutput'
     );
 
     /**
@@ -45,11 +45,103 @@ class Git
      */
     public function getDir() {return $this->dir;}
 
+    // GitPython appears to have an implementation of getting
+    // attributes which also calls process. We're only going to support
+    // pure method calls... for now.
+    // public function __get($name)
+
     /**
      * Executes a command on shell and returns output.
+     * @warning $istream is a STRING not a HANDLE, as it is in the Python 
+     *      implementation. We might want to change this some time.
+     * @param $command Command argument list to handle.
+     * @param $istream Stdin string passed to subprocess.
+     * @param $options Lookup array of options. These options are:
+     *      'keepCwd' => Whether to use current working directory from
+     *          getcwd() or the Git directory in $this->dir
+     *      'extendedOutput' => Whether to return array(status, stdout, stderr)
+     *      'exceptions' => Whether to raise an exception if Git returns
+     *          a non-zero exit status
+     *      'rawOutput' => Whether to avoid stripping off trailing whitespace
+     * @return String stdout output when withExtendedOutput is false, see above
+     *      if true.
      */
-    public function execute($command) {
-        return shell_exec($command);
+    public function execute($command, $istream = null, $options = array()) {
+        if (is_array($command)) $command = implode(' ', $command);
+        $options = array_merge(array(
+            'keepCwd' => false,
+            'extendedOutput' => false,
+            'exceptions' => true,
+            'rawOutput' => false,
+        ), $options);
+        if ($options['keepCwd'] || is_null($this->dir)) {
+            $cwd = getcwd();
+        } else {
+            $cwd = $this->dir;
+        }
+        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
+            // Windows dependent code, stolen from PHPT
+            $com = new COM('WScript.Shell');
+            $old = getcwd();
+            chdir($cwd); // :HACK: Determine the proper way to pass cwd to COM
+            $proc = $com->Exec($command);
+            if (!is_null($istream)) $proc->StdIn->Write($istream);
+            $stdout = $proc->StdOut->ReadAll();
+            $stderr = $proc->StdErr->ReadAll();
+            $status = $proc->ExitCode;
+            chdir($old);
+        } else {
+            // untested! Also stolen from PHPT
+            // this seems needlessly complicated
+            $pipes_template = array(
+                0 => array('pipe', 'r'),
+                1 => array('pipe', 'w'),
+                2 => array('pipe', 'w'),
+                3 => array('pipe', 'w'), // pipe to write exit code to
+            );
+            $pipes = array();
+            $proc = proc_open($command, $pipes_template, $pipes);
+            fwrite($pipes[0], $this->istream);
+            fclose($pipes[0]);
+            while (true) {
+                $read = array($this->_pipes[1]);
+                $except = $write = null;
+                $n = stream_select($read, $write, $except, 30);
+                $stderr = $stdout = '';
+                if ($n === 0) {
+                    throw new Git_Exception('Process timed out');
+                } elseif ($n > 0) {
+                    $errLine = fread($this->_pipes[2], 8192);
+                    $outLine = fread($this->_pipes[1], 8192);
+                    $stderr .= $errLine;
+                    $stdout .= $outLine;
+                    if ($errLine === '' && $outLine === '') {
+                        fclose($pipes[1]);
+                        fclose($pipes[2]);
+                        break;
+                    }
+                }
+            }
+            $status = trim(fread($pipes[3], 5));
+            fclose($pipes[3]);
+            $close = proc_close($proc);
+            if (empty($code)) $status = $close;
+        }
+        // We want $stderr, $stdout and $status
+        if (!$options['rawOutput']) {
+            // This feels buggy to me, especially with git cat-file
+            $stdout = rtrim($stdout);
+            $stderr = rtrim($stderr);
+        }
+        if ($options['exceptions'] && $status !== 0) {
+            throw new Git_Exception_Command($command, $status, $stderr);
+        }
+        // Trace code omitted
+        if ($options['extendedOutput']) {
+            return array($status, $stdout, $stderr);
+        } else {
+            return $stdout;
+        }
     }
 
     /**
@@ -64,7 +156,7 @@ class Git
         foreach ($kwargs as $k => $v) {
             if (strlen($k) == 1) {
                 if ($v === true) $args[] = "-$k";
-                else $args[] = "-$k $v";
+                else $args[] = "-$k$v";
             } else {
                 // $k = dashify($k);
                 if ($v === true) $args[] = "--$k";
@@ -89,7 +181,7 @@ class Git
         $args = array();
         $kwargs = array();
         foreach ($raw_args as $raw) {
-            if (is_array($raw)) $kwargs = $raw;
+            if (is_array($raw)) $kwargs = $raw; // only one assoc array allowed!
             else $args[] = $raw;
         }
         for ($i = 0; isset($kwargs[$i]); $i++) {
@@ -113,7 +205,7 @@ class Git
             $c = $method[$i];
             $command .= ctype_upper($c) ? '-' . strtolower($c) : $c;
         }
-        $call = self::$git . " --git-dir={$this->dir} $command " . implode(' ', $args);
+        $call = self::$git . " $command " . implode(' ', $args);
         // var_dump($call);
         $result = $this->execute($call);
         return $result;
index 3029b46..a559b7a 100644 (file)
@@ -60,6 +60,7 @@ class Git_Commit {
         foreach ($lines as $k => $v) if (trim($v) === '') unset($lines[$k]);
         $lines = array_values($lines);
 
+        $commits = array();
         for ($i = 0, $c = count($lines); $i < $c;) {
 
             $id   = self::_l($lines, $i, true);
index b6ab19c..74fa29f 100644 (file)
@@ -11,3 +11,10 @@ class Git_Exception_InvalidRepository extends Git_Exception
         parent::__construct("Git repository $path does not exist");
     }
 }
+
+class Git_Exception_Command extends Git_Exception
+{
+    public function __construct($command, $status, $stderr) {
+        parent::__construct("Command `$command` returned error code [$status] with message \"$stderr\"");
+    }
+}
index 69181ae..0429af2 100644 (file)
@@ -24,20 +24,49 @@ class Git_Repo {
     public $git;
 
     /**
+     * Current working directory.
+     */
+    public $wd;
+
+    /**
      * @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();
+    public function __construct($path = null) {
+        $path = $path ? $path : getcwd();
+        $path = realpath($path); // I believe PHP automatically expands ~
+        if ($path === false) {
+            throw new Git_Exception("No such path '$path'");
+        }
+        $curpath = $path;
+        while ($curpath) {
+            if ($this->isGitDir($curpath)) {
+                $this->bare = true;
+                $this->path = $this->wd = $curpath;
+            }
+            $gitpath = "$curpath/.git";
+            if ($this->isGitDir($gitpath)) {
+                $this->bare = false;
+                $this->path = realpath($gitpath);
+                $this->wd = $curpath;
+                break;
+            }
+            $temp = dirname($curpath);
+            if (!$temp || $temp == $curpath) break;
         }
-        $this->git = new Git($this->path);
+        if (is_null($this->path)) throw new Git_Exception_InvalidRepository($path);
+        $this->git = new Git($this->wd);
+    }
+
+    /**
+     * Determines whether or not this is a Git directory.
+     * @note Taken from setup.c:is_git_directory
+     */
+    protected function isGitDir($d) {
+        if (is_dir($d) && is_dir("$d/objects") && is_dir("$d/refs")) {
+            $headref = "$d/HEAD";
+            return is_file($headref) || 
+                (is_link($headref) && strncmp(readlink($headref), 'refs', 4));
+        } else return false;
     }
 
     // * means will implement soon
@@ -69,14 +98,17 @@ class Git_Repo {
      * @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()) {
+    public function log($commit = 'master', $path = array(), $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);
+        // This call is kinda weird, I know, but otherwise we have
+        // to do user_func_call_array. Perhaps we should patch __call
+        // to work with numeric arrays? A thought...
+        $commits = $this->git->__call('log', array_merge($path, array($options)));
         return Git_Commit::listFromString($this, $commits);
     }
 
index dbe2a38..483bde9 100644 (file)
@@ -32,6 +32,7 @@ class Git_Tree extends Git_Lazy {
      * Constructs and fully initializes a tree.
      */
     public static function construct($repo, $treeish, $paths = array()) {
+        // suspect:
         $output = $repo->git->lsTree($treeish, $paths);
         $tree = new Git_Tree($repo, array('id' => $treeish));
         $tree->constructInitialize($repo, $treeish, $output);
index 9b834e0..b8e8b25 100644 (file)
@@ -5,7 +5,9 @@
  * Sample script. Assumes that this directory is a Git checkout.
  */
 
+error_reporting(E_ALL | E_STRICT);
 require_once 'library/Git.php';
 
 $repo = new Git_Repo('.');
+//$repo->log();
 var_dump($repo->log());
index 605a157..bacda4f 100644 (file)
@@ -16,7 +16,7 @@ class GitTest extends UnitTestCase
             'message' => 'test'
         ));
         $expect = array(
-            '-v', '-s ours', '--no-commit', '--message=test',
+            '-v', '-sours', '--no-commit', '--message=test',
         );
         $this->assertIdentical($result, $expect);
     }
@@ -31,7 +31,7 @@ class GitTest extends UnitTestCase
     function testCall() {
         $git = new GitPartialMock();
         $git->__construct('dir');
-        $git->expectOnce('execute', array('git --git-dir=dir cherry-pick -s f533ebca --no-commit'));
+        $git->expectOnce('execute', array('git cherry-pick -s f533ebca --no-commit'));
         $git->setReturnValue('execute', $expect = 'Result');
         $result = $git->cherryPick('f533ebca', array('--no-commit', 's' => true));
         $this->assertIdentical($result, $expect);