Skip to content

Commit 3f177c4

Browse files
authored
Add a Console-based tool to regenerate the fixture bundle (#242)
* Add a Symfony Console script to regenerate the fixture bundle tests/fixtures/generate-bundle.php replaces the manual git recipe for adding a new fixture scenario: `extract` clones the current bundle to a working directory with every branch checked out locally, `build` rebuilds tests/fixtures/foobar.bundle from it. It only ever extends the existing history rather than starting from scratch, so existing commit SHAs (and the GPG-signed one, which we couldn't reproduce without the original private key) stay valid. The ref list it bundles is now shared with verify-bundle.sh via tests/fixtures/bundle-refs.txt, so the two can no longer drift apart. Requires symfony/console (^8.1, require-dev only), which the script's Application/Command classes use for argument parsing and output. * Document generate-bundle.php in CONTRIBUTING.md Replaces the raw git recipe for adding a fixture scenario with the extract/build commands, and points at bundle-refs.txt for adding a new branch or tag.
1 parent 94cf24e commit 3f177c4

5 files changed

Lines changed: 159 additions & 26 deletions

File tree

.github/CONTRIBUTING.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,26 +48,27 @@ a local git bundle. Using a bundle instead of a network clone keeps the tests fa
4848
fully offline.
4949

5050
If you need a new fixture scenario (a specific merge, encoding, or signed-commit shape,
51-
for example), regenerate the bundle locally, entirely from the one already in the repo:
51+
for example), regenerate the bundle with `tests/fixtures/generate-bundle.php`, entirely
52+
from the one already in the repo:
5253

5354
```bash
54-
$ git clone tests/fixtures/foobar.bundle /tmp/foobar-fixture && cd /tmp/foobar-fixture
55-
$ for b in $(git branch -r | grep -v HEAD | sed 's#origin/##'); do
56-
$ git branch --track "$b" "origin/$b"
57-
$ done
58-
# ... add your commits, branches or tags ...
59-
$ git bundle create foobar.bundle \
60-
HEAD refs/heads/master refs/heads/new-feature refs/heads/diff-features \
61-
refs/heads/pagination refs/heads/path-resolving refs/tags/0.1 refs/tags/annotated
62-
$ cp foobar.bundle /path/to/gitlib/tests/fixtures/foobar.bundle
55+
$ php tests/fixtures/generate-bundle.php extract
56+
# ... add your commits, branches or tags in the printed directory ...
57+
$ php tests/fixtures/generate-bundle.php build /path/printed/above
6358
```
6459

60+
`extract` clones the current bundle to a working directory with every branch checked out
61+
locally, ready to receive new commits. `build` rebuilds `tests/fixtures/foobar.bundle`
62+
from that directory, restricted to the refs listed in `tests/fixtures/bundle-refs.txt`
63+
add your new branch or tag there first if you introduced one.
64+
6565
Then update the commit SHA constants in `AbstractTestCase` to match, and run
6666
`tests/fixtures/verify-bundle.sh`. It checks the bundle's integrity, its ref list against
67-
an allow-list, and its size, since GitHub renders any change to this binary file as an
67+
`bundle-refs.txt`, and its size, since GitHub renders any change to this binary file as an
6868
opaque diff. If your change intentionally adds a ref or grows the file, update
69-
`ALLOWED_REFS` or `MAX_SIZE_KB` in that script as part of the same pull request, so the
70-
reason for the change is explicit and reviewable rather than a silent binary diff.
69+
`tests/fixtures/bundle-refs.txt` or `MAX_SIZE_KB` in that script as part of the same pull
70+
request, so the reason for the change is explicit and reviewable rather than a silent
71+
binary diff.
7172

7273
## Standard code
7374

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"require-dev": {
4444
"ext-fileinfo": "*",
4545
"phpunit/phpunit": "^12.0",
46-
"psr/log": "^3.0"
46+
"psr/log": "^3.0",
47+
"symfony/console": "^8.1"
4748
},
4849
"config": {
4950
"preferred-install": "dist",

tests/fixtures/bundle-refs.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
HEAD
2+
refs/heads/master
3+
refs/heads/new-feature
4+
refs/heads/diff-features
5+
refs/heads/pagination
6+
refs/heads/path-resolving
7+
refs/tags/0.1
8+
refs/tags/annotated

tests/fixtures/generate-bundle.php

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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 dirname(__DIR__, 2).'/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 BUNDLE_PATH = __DIR__.'/foobar.bundle';
26+
const REFS_PATH = __DIR__.'/bundle-refs.txt';
27+
28+
function git(array $args, ?string $cwd = null): Process
29+
{
30+
$process = new Process(['git', ...$args], $cwd);
31+
$process->mustRun();
32+
33+
return $process;
34+
}
35+
36+
function bundleRefs(): array
37+
{
38+
return array_values(array_filter(explode("\n", trim(file_get_contents(REFS_PATH)))));
39+
}
40+
41+
#[AsCommand(name: 'extract', description: 'Clone the fixture bundle to a working directory, ready for new commits, branches or tags')]
42+
final class ExtractCommand extends Command
43+
{
44+
protected function configure(): void
45+
{
46+
$this->addArgument('path', InputArgument::OPTIONAL, 'Where to clone the fixture', sys_get_temp_dir().'/foobar-fixture-'.bin2hex(random_bytes(4)));
47+
}
48+
49+
protected function execute(InputInterface $input, OutputInterface $output): int
50+
{
51+
$io = new SymfonyStyle($input, $output);
52+
$dest = $input->getArgument('path');
53+
54+
if (file_exists($dest)) {
55+
$io->error(sprintf('Destination "%s" already exists.', $dest));
56+
57+
return Command::FAILURE;
58+
}
59+
60+
git(['clone', '--quiet', BUNDLE_PATH, $dest]);
61+
62+
$localBranches = explode("\n", trim(git(['branch', '--format=%(refname:short)'], $dest)->getOutput()));
63+
64+
// Full ref names, not `--format=%(refname:short)`: git shortens the symbolic
65+
// refs/remotes/origin/HEAD pointer to a bare "origin" on some git versions, which
66+
// would otherwise be mistaken for a real branch called "origin".
67+
$refs = trim(git(['for-each-ref', '--format=%(refname)', 'refs/remotes/origin'], $dest)->getOutput());
68+
foreach (explode("\n", $refs) as $ref) {
69+
if ('' === $ref) {
70+
continue;
71+
}
72+
$local = preg_replace('#^refs/remotes/origin/#', '', $ref);
73+
if ('HEAD' === $local || in_array($local, $localBranches, true)) {
74+
// Symbolic HEAD pointer, or already checked out as the clone's default branch.
75+
continue;
76+
}
77+
git(['branch', '--track', $local, "origin/{$local}"], $dest);
78+
}
79+
80+
$io->success('Fixture extracted.');
81+
$io->writeln([
82+
sprintf('Path: %s', $dest),
83+
'',
84+
'Make your changes there (commits, branches, tags), then run:',
85+
sprintf(' %s build %s', $_SERVER['argv'][0], $dest),
86+
]);
87+
88+
return Command::SUCCESS;
89+
}
90+
}
91+
92+
#[AsCommand(name: 'build', description: 'Rebuild tests/fixtures/foobar.bundle from a working directory produced by "extract"')]
93+
final class BuildCommand extends Command
94+
{
95+
protected function configure(): void
96+
{
97+
$this->addArgument('path', InputArgument::REQUIRED, 'The working directory produced by "extract"');
98+
}
99+
100+
protected function execute(InputInterface $input, OutputInterface $output): int
101+
{
102+
$io = new SymfonyStyle($input, $output);
103+
$src = $input->getArgument('path');
104+
105+
if (!is_dir($src)) {
106+
$io->error(sprintf('Source "%s" does not exist. Run "extract" first.', $src));
107+
108+
return Command::FAILURE;
109+
}
110+
111+
$tmpBundle = tempnam(sys_get_temp_dir(), 'foobar_bundle_');
112+
git(['bundle', 'create', $tmpBundle, ...bundleRefs()], $src);
113+
git(['bundle', 'verify', $tmpBundle]);
114+
115+
copy($tmpBundle, BUNDLE_PATH);
116+
unlink($tmpBundle);
117+
118+
$io->success('Bundle rebuilt.');
119+
$io->writeln([
120+
sprintf('Path: %s', BUNDLE_PATH),
121+
'',
122+
sprintf('If you added a ref not in %s, update that file first and re-run build.', REFS_PATH),
123+
'Otherwise, update the commit constants in AbstractTestCase if needed, then run:',
124+
' tests/fixtures/verify-bundle.sh',
125+
]);
126+
127+
return Command::SUCCESS;
128+
}
129+
}
130+
131+
$app = new Application('gitlib fixture bundle tool');
132+
$app->addCommands([new ExtractCommand(), new BuildCommand()]);
133+
exit($app->run());

tests/fixtures/verify-bundle.sh

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,7 @@ cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null
1515

1616
BUNDLE="foobar.bundle"
1717
MAX_SIZE_KB=200
18-
19-
ALLOWED_REFS="
20-
HEAD
21-
refs/heads/diff-features
22-
refs/heads/master
23-
refs/heads/new-feature
24-
refs/heads/pagination
25-
refs/heads/path-resolving
26-
refs/tags/0.1
27-
refs/tags/annotated
28-
"
18+
ALLOWED_REFS="$(cat bundle-refs.txt)"
2919

3020
echo "== Verifying $BUNDLE =="
3121

@@ -46,7 +36,7 @@ UNEXPECTED_REFS="$(comm -23 <(echo "$ACTUAL_REFS") <(sort -u <<< "$ALLOWED_REFS"
4636
if [ -n "$UNEXPECTED_REFS" ]; then
4737
echo "ERROR: $BUNDLE contains refs that are not in the allow-list:" >&2
4838
echo "$UNEXPECTED_REFS" >&2
49-
echo "If this is expected, update ALLOWED_REFS in $0 as part of the same PR." >&2
39+
echo "If this is expected, update tests/fixtures/bundle-refs.txt as part of the same PR." >&2
5040
exit 1
5141
fi
5242

0 commit comments

Comments
 (0)