From 8979283bc2986ac53ba8499ccfe9375e89072902 Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Sat, 19 Jul 2008 21:34:05 -0400 Subject: [PATCH] Modernize Git to upstream GitRepos, mainly command handling. * 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 --- library/Git.php | 106 +++++++++++++++++++++++++++++++++++++++++++--- library/Git/Commit.php | 1 + library/Git/Exception.php | 7 +++ library/Git/Repo.php | 58 +++++++++++++++++++------ library/Git/Tree.php | 1 + sample.php | 2 + tests/GitTest.php | 4 +- 7 files changed, 157 insertions(+), 22 deletions(-) diff --git a/library/Git.php b/library/Git.php index 2f098f4..a776342 100644 --- a/library/Git.php +++ b/library/Git.php @@ -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; diff --git a/library/Git/Commit.php b/library/Git/Commit.php index 3029b46..a559b7a 100644 --- a/library/Git/Commit.php +++ b/library/Git/Commit.php @@ -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); diff --git a/library/Git/Exception.php b/library/Git/Exception.php index b6ab19c..74fa29f 100644 --- a/library/Git/Exception.php +++ b/library/Git/Exception.php @@ -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\""); + } +} diff --git a/library/Git/Repo.php b/library/Git/Repo.php index 69181ae..0429af2 100644 --- a/library/Git/Repo.php +++ b/library/Git/Repo.php @@ -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); } diff --git a/library/Git/Tree.php b/library/Git/Tree.php index dbe2a38..483bde9 100644 --- a/library/Git/Tree.php +++ b/library/Git/Tree.php @@ -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); diff --git a/sample.php b/sample.php index 9b834e0..b8e8b25 100644 --- a/sample.php +++ b/sample.php @@ -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()); diff --git a/tests/GitTest.php b/tests/GitTest.php index 605a157..bacda4f 100644 --- a/tests/GitTest.php +++ b/tests/GitTest.php @@ -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); -- 2.11.4.GIT