Skip to content

Commit 36114f4

Browse files
authored
Rewrite test-git-versions.sh in PHP and update the tested git versions (#251)
Port the git-version build/test tool to PHP with Symfony Console and Process, matching the style of tests/fixtures/generate-bundle.php, and split it into a "build" and a "test" command. Update the curated version list to versions picked for how long and widely each shipped as the default git on a major distro: v1.7.1 (RHEL/CentOS 6), v1.8.3.1 (RHEL/CentOS 7), v2.17.1 (Ubuntu 18.04), v2.25.1 (20.04), v2.34.1 (22.04), v2.43.0 (24.04).
1 parent c756967 commit 36114f4

4 files changed

Lines changed: 212 additions & 75 deletions

File tree

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Then run [PHPUnit](https://phpunit.de/):
3737
$ vendor/bin/phpunit
3838
```
3939

40-
* A script `test-git-versions.sh` is available in repository to test gitlib against many git versions.
40+
* A script `test-git-versions.php` is available in repository to test gitlib against many git versions: `php test-git-versions.php build` compiles them, then `php test-git-versions.php test` runs the suite against each.
4141
* The tests will be automatically run by [GitHub Actions](https://github.com/features/actions) against pull requests.
4242
* Tests run fully offline: no network access is required.
4343

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/phpunit.xml
66
/var/
77
/vendor
8+
/git-builds/

test-git-versions.php

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
* This file is part of Gitonomy.
6+
*
7+
* (c) Alexandre Salomé <alexandre.salome@gmail.com>
8+
* (c) Julien DIDIER <genzo.wm@gmail.com>
9+
*
10+
* This source file is subject to the MIT license that is bundled
11+
* with this source code in the file LICENSE.
12+
*/
13+
14+
require __DIR__.'/vendor/autoload.php';
15+
16+
use Symfony\Component\Console\Application;
17+
use Symfony\Component\Console\Attribute\AsCommand;
18+
use Symfony\Component\Console\Command\Command;
19+
use Symfony\Component\Console\Input\InputArgument;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Output\OutputInterface;
22+
use Symfony\Component\Console\Style\SymfonyStyle;
23+
use Symfony\Component\Process\Process;
24+
25+
const GIT_URL = 'https://github.com/git/git.git';
26+
const BUILDS_PATH = __DIR__.'/git-builds';
27+
28+
// One v1.x and several v2.y releases, picked for how long and how widely each
29+
// shipped as the default git on a major distro, rather than for a spread across
30+
// git's history: v1.7.1 (RHEL/CentOS 6), v1.8.3.1 (RHEL/CentOS 7), v2.17.1 (Ubuntu
31+
// 18.04), v2.25.1 (Ubuntu 20.04), v2.34.1 (Ubuntu 22.04), v2.43.0 (Ubuntu 24.04).
32+
const DEFAULT_VERSIONS = ['v1.7.1', 'v1.8.3.1', 'v2.17.1', 'v2.25.1', 'v2.34.1', 'v2.43.0'];
33+
34+
function sh(array $args, ?string $cwd = null, ?string $logfile = null): Process
35+
{
36+
$process = new Process($args, $cwd, timeout: null);
37+
38+
if (null !== $logfile) {
39+
file_put_contents($logfile, '$ '.implode(' ', $args)."\n", \FILE_APPEND);
40+
$process->run(static function (string $type, string $data) use ($logfile): void {
41+
file_put_contents($logfile, $data, \FILE_APPEND);
42+
});
43+
if (!$process->isSuccessful()) {
44+
throw new RuntimeException(sprintf('Command "%s" failed, see %s', implode(' ', $args), $logfile));
45+
}
46+
} else {
47+
$process->mustRun();
48+
}
49+
50+
return $process;
51+
}
52+
53+
function cachePath(): string
54+
{
55+
return BUILDS_PATH.'/cache';
56+
}
57+
58+
function versionPath(string $version): string
59+
{
60+
return BUILDS_PATH.'/'.$version;
61+
}
62+
63+
function gitBinary(string $version): string
64+
{
65+
return versionPath($version).'/build/bin/git';
66+
}
67+
68+
#[AsCommand(name: 'build', description: 'Build one or more git versions from source, ready to be used by the "test" command')]
69+
final class BuildCommand extends Command
70+
{
71+
protected function configure(): void
72+
{
73+
$this
74+
->addArgument('versions', InputArgument::IS_ARRAY, 'git tags to build (defaults to the curated list)', DEFAULT_VERSIONS)
75+
;
76+
}
77+
78+
protected function execute(InputInterface $input, OutputInterface $output): int
79+
{
80+
$io = new SymfonyStyle($input, $output);
81+
$versions = $input->getArgument('versions');
82+
83+
if (!is_dir(BUILDS_PATH)) {
84+
mkdir(BUILDS_PATH);
85+
}
86+
87+
if (is_dir(cachePath())) {
88+
$io->section('Updating cache from GitHub');
89+
sh(['git', 'fetch', '--quiet'], cachePath());
90+
} else {
91+
$io->section('Cloning git/git from GitHub');
92+
sh(['git', 'clone', '--quiet', GIT_URL, cachePath()]);
93+
}
94+
95+
foreach ($versions as $version) {
96+
$io->section(sprintf('Building %s', $version));
97+
98+
$path = versionPath($version);
99+
$lockfile = BUILDS_PATH.'/'.$version.'.lock';
100+
$logfile = BUILDS_PATH.'/'.$version.'.log';
101+
102+
if (is_file($lockfile)) {
103+
$io->warning('A previous build was interrupted, rebuilding from scratch.');
104+
new Process(['rm', '-rf', $path])->mustRun();
105+
unlink($lockfile);
106+
}
107+
108+
if (is_dir($path)) {
109+
$io->writeln('Already built, skipping.');
110+
111+
continue;
112+
}
113+
114+
touch($lockfile);
115+
mkdir($path, recursive: true);
116+
file_put_contents($logfile, '');
117+
118+
$io->writeln(sprintf('Log: %s', $logfile));
119+
120+
$source = $path.'/source';
121+
sh(['git', 'clone', '--shared', '--quiet', cachePath(), $source]);
122+
sh(['git', 'checkout', '--quiet', $version], $source);
123+
124+
$prefix = $path.'/build';
125+
mkdir($prefix);
126+
127+
$io->writeln('Compiling (autoconf, configure, make, make install)...');
128+
sh(['autoconf'], $source, $logfile);
129+
sh(['./configure', '--prefix='.$prefix], $source, $logfile);
130+
// gitlib only ever shells out to plumbing/porcelain in the main "git" binary,
131+
// so building the GUI/language extras is both unnecessary and, for a build
132+
// this old, unreliable on a modern machine:
133+
// - -std=gnu17: recent git releases declare an "unreachable" identifier that
134+
// collides with the C23 unreachable() macro glibc/gcc now define by default.
135+
// - NO_OPENSSL: older git-imap-send.c uses OpenSSL 1.0 APIs (HMAC_CTX as a
136+
// stack struct) removed in OpenSSL 3; the test suite is fully offline and
137+
// never needs imap-send.
138+
// - NO_PYTHON, NO_TCLTK: skip the Python remote-helper bridge and git-gui/gitk,
139+
// which need toolchains (Python 2, Tcl/Tk) this machine doesn't have.
140+
// All flags are passed to both invocations: git's Makefile forces a full
141+
// rebuild whenever they change between "make all" and "make install".
142+
$makeFlags = ['CFLAGS=-std=gnu17', 'NO_OPENSSL=1', 'NO_PYTHON=1', 'NO_TCLTK=1'];
143+
sh(['make', 'all', ...$makeFlags], $source, $logfile);
144+
sh(['make', 'install', ...$makeFlags], $source, $logfile);
145+
146+
unlink($lockfile);
147+
$io->success(sprintf('%s built at %s', $version, gitBinary($version)));
148+
}
149+
150+
return Command::SUCCESS;
151+
}
152+
}
153+
154+
#[AsCommand(name: 'test', description: 'Run the test suite against one or more already-built git versions')]
155+
final class TestCommand extends Command
156+
{
157+
protected function configure(): void
158+
{
159+
$this
160+
->addArgument('versions', InputArgument::IS_ARRAY, 'git tags to test against (defaults to the curated list)', DEFAULT_VERSIONS)
161+
;
162+
}
163+
164+
protected function execute(InputInterface $input, OutputInterface $output): int
165+
{
166+
$io = new SymfonyStyle($input, $output);
167+
$versions = $input->getArgument('versions');
168+
169+
// Old git binaries choke on config values introduced after their release (e.g.
170+
// "merge.conflictstyle = zdiff3", added in 2.35, is a fatal "unknown style" error
171+
// on anything older) if they pick up the invoking user's own ~/.gitconfig. Give
172+
// each git binary an empty, isolated HOME so only gitlib's own fixtures apply.
173+
$isolatedHome = sys_get_temp_dir().'/gitlib-test-git-versions-home-'.bin2hex(random_bytes(4));
174+
mkdir($isolatedHome);
175+
176+
$results = [];
177+
178+
foreach ($versions as $version) {
179+
$binary = gitBinary($version);
180+
181+
if (!is_file($binary)) {
182+
$io->error(sprintf('%s is not built yet. Run the "build" command first.', $version));
183+
$results[] = [$version, 'not built'];
184+
185+
continue;
186+
}
187+
188+
$io->section(sprintf('Testing against %s', $version));
189+
$io->writeln(sprintf('Command: %s', $binary));
190+
191+
$env = ['GIT_COMMAND' => $binary, 'HOME' => $isolatedHome, 'XDG_CONFIG_HOME' => $isolatedHome];
192+
$process = new Process(['vendor/bin/phpunit'], __DIR__, $env, timeout: null);
193+
$process->run(static function (string $type, string $data) use ($output): void {
194+
$output->write($data);
195+
});
196+
197+
$results[] = [$version, $process->isSuccessful() ? 'ok' : 'FAILED'];
198+
}
199+
200+
$io->table(['Version', 'Result'], $results);
201+
202+
$failed = array_filter($results, static fn (array $result) => 'ok' !== $result[1]);
203+
204+
return [] === $failed ? Command::SUCCESS : Command::FAILURE;
205+
}
206+
}
207+
208+
$app = new Application('gitlib multi-version git test tool');
209+
$app->addCommands([new BuildCommand(), new TestCommand()]);
210+
exit($app->run());

test-git-versions.sh

Lines changed: 0 additions & 74 deletions
This file was deleted.

0 commit comments

Comments
 (0)