From d7d7433ea5541f4507d69fa170b25d186b9269d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Mon, 7 Sep 2026 18:08:26 +0200 Subject: [PATCH] Add Repository::runProcess() to expose stderr on successful commands Repository::run() only ever returns stdout, even on success. Some git commands (push, in particular) write their meaningful output to stderr even when they succeed, so callers had no way to read it (#205). runProcess() returns the full, already-run Process instead, giving access to getErrorOutput(). run() now delegates to it and keeps its exact original behavior. --- phpstan-baseline.neon | 6 ++++ src/Gitonomy/Git/Repository.php | 33 +++++++++++++++++---- tests/Gitonomy/Git/Tests/RepositoryTest.php | 21 +++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8b1e721..8a95150 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -246,6 +246,12 @@ parameters: count: 1 path: src/Gitonomy/Git/Repository.php + - + message: '#^Method Gitonomy\\Git\\Repository\:\:runProcess\(\) has parameter \$args with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Gitonomy/Git/Repository.php + - message: '#^Method Gitonomy\\Git\\Repository\:\:shell\(\) has parameter \$env with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue diff --git a/src/Gitonomy/Git/Repository.php b/src/Gitonomy/Git/Repository.php index 3e49988..ac294e6 100644 --- a/src/Gitonomy/Git/Repository.php +++ b/src/Gitonomy/Git/Repository.php @@ -470,6 +470,11 @@ public function setDescription(string $description): static * This command is a facility command. You can run any command * directly on git repository. * + * Note that this only returns the standard output of the process. Some + * git commands (`push`, for instance) write their meaningful output to + * stderr even when they succeed: use {@see runProcess()} instead if you + * need access to it. + * * @param string $command Git command to run (checkout, branch, tag) * @param array $args Arguments of git command * @@ -478,6 +483,26 @@ public function setDescription(string $description): static * @throws RuntimeException Error while executing git command (debug-mode only) */ public function run(string $command, array $args = []): ?string + { + $process = $this->runProcess($command, $args); + + return $process->isSuccessful() ? $process->getOutput() : null; + } + + /** + * Same as {@see run()}, but returns the full, already-run process + * instead of only its standard output on success. + * + * This is useful for commands like `push`, which write their + * meaningful output to stderr even when they succeed, so it can't be + * read through run(). + * + * @param string $command Git command to run (checkout, branch, tag) + * @param array $args Arguments of git command + * + * @throws RuntimeException Error while executing git command (debug-mode only) + */ + public function runProcess(string $command, array $args = []): Process { $process = $this->getProcess($command, $args); @@ -488,13 +513,11 @@ public function run(string $command, array $args = []): ?string $process->run(); - $output = $process->getOutput(); - if ($this->logger && $this->debug) { $duration = microtime(true) - $before; $this->logger->debug(\sprintf('last command (%s) duration: %sms', $command, \sprintf('%.2f', $duration * 1000))); $this->logger->debug(\sprintf('last command (%s) return code: %s', $command, $process->getExitCode())); - $this->logger->debug(\sprintf('last command (%s) output: %s', $command, $output)); + $this->logger->debug(\sprintf('last command (%s) output: %s', $command, $process->getOutput())); } if (!$process->isSuccessful()) { @@ -507,11 +530,9 @@ public function run(string $command, array $args = []): ?string if ($this->debug) { throw new ProcessException($process); } - - return null; } - return $output; + return $process; } /** diff --git a/tests/Gitonomy/Git/Tests/RepositoryTest.php b/tests/Gitonomy/Git/Tests/RepositoryTest.php index e7bc480..e92b2b0 100644 --- a/tests/Gitonomy/Git/Tests/RepositoryTest.php +++ b/tests/Gitonomy/Git/Tests/RepositoryTest.php @@ -30,6 +30,27 @@ public function testRunReturnsNullInsteadOfThrowingWhenDebugIsFalse(): void $this->assertNull($repository->run('not-a-command')); } + public function testRunProcessGivesAccessToStderrOnSuccess(): void + { + $repository = self::createFoobarRepository(false); + + // `git checkout` reports the switched branch on stderr, even on success. + $process = $repository->runProcess('checkout', ['master']); + + $this->assertTrue($process->isSuccessful()); + $this->assertStringContainsString('master', $process->getErrorOutput()); + } + + public function testRunProcessReturnsFailedProcessInsteadOfThrowingWhenDebugIsFalse(): void + { + $repository = self::createFoobarRepository(true); + $repository = new Repository($repository->getPath(), array_merge(self::getOptions(), ['debug' => false])); + + $process = $repository->runProcess('not-a-command'); + + $this->assertFalse($process->isSuccessful()); + } + public function testGetShortHashThrowsCleanExceptionWhenDebugIsFalse(): void { $repository = self::createFoobarRepository(true);