Skip to content

Commit 37ea8f5

Browse files
authored
Merge branch 'main' into task/workflow-maintenance
2 parents 969990b + 690e655 commit 37ea8f5

File tree

7 files changed

+331
-0
lines changed

7 files changed

+331
-0
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ tools/
55
vendor/
66
Dockerfile
77
*.rst
8+
build

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ ENV TYPO3AZUREEDGEURIVERSION=$TYPO3AZUREEDGEURIVERSION
2020
WORKDIR /project
2121
ENTRYPOINT [ "/opt/guides/entrypoint.sh" ]
2222
CMD ["-h"]
23+
24+
RUN apk add --no-cache \
25+
git

entrypoint.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ elif [ "$1" = "configure" ]; then
7777
elif [ "$1" = "render" ]; then
7878
ENTRYPOINT="${ENTRYPOINT_DEFAULT}"
7979
shift
80+
elif [ "$1" = "create-redirects-from-git" ]; then
81+
ENTRYPOINT="${ENTRYPOINT_SYMFONY_COMMANDS} create-redirects-from-git"
82+
shift
8083
else
8184
# Default: "render"; no shifting.
8285
ENTRYPOINT="${ENTRYPOINT_DEFAULT}"

packages/typo3-guides-cli/bin/typo3-guides

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use T3Docs\GuidesCli\Command\InitCommand;
99
use T3Docs\GuidesCli\Command\MigrateSettingsCommand;
1010
use T3Docs\GuidesCli\Command\ConfigureCommand;
1111
use T3Docs\GuidesCli\Command\LintGuidesXmlCommand;
12+
use T3Docs\GuidesCli\Command\CreateRedirectsFromGitCommand;
1213

1314
use Symfony\Component\Console\Application;
1415

@@ -39,5 +40,6 @@ $application->add(new MigrateSettingsCommand());
3940
$application->add(new InitCommand());
4041
$application->add(new ConfigureCommand());
4142
$application->add(new LintGuidesXmlCommand());
43+
$application->add(new CreateRedirectsFromGitCommand());
4244

4345
$application->run();
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace T3Docs\GuidesCli\Command;
6+
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Input\InputInterface;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
use Symfony\Component\Console\Style\SymfonyStyle;
11+
use Symfony\Component\Console\Input\InputOption;
12+
use T3Docs\GuidesCli\Git\GitChangeDetector;
13+
use T3Docs\GuidesCli\Redirect\RedirectCreator;
14+
15+
final class CreateRedirectsFromGitCommand extends Command
16+
{
17+
protected static $defaultName = 'create-redirects-from-git';
18+
19+
private GitChangeDetector $gitChangeDetector;
20+
private RedirectCreator $redirectCreator;
21+
22+
public function __construct(
23+
?GitChangeDetector $gitChangeDetector = null,
24+
?RedirectCreator $redirectCreator = null
25+
) {
26+
parent::__construct();
27+
$this->gitChangeDetector = $gitChangeDetector ?? new GitChangeDetector();
28+
$this->redirectCreator = $redirectCreator ?? new RedirectCreator();
29+
}
30+
31+
protected function configure(): void
32+
{
33+
$this->setDescription('Creates nginx redirects for files moved in a GitHub pull request.');
34+
$this->setHelp(
35+
<<<'EOT'
36+
The <info>%command.name%</info> command analyzes git history to detect moved files
37+
in the current branch/PR and creates appropriate nginx redirects for them.
38+
39+
<info>$ php %command.name% [options]</info>
40+
41+
EOT
42+
);
43+
44+
$this->addOption(
45+
'base-branch',
46+
'b',
47+
InputOption::VALUE_REQUIRED,
48+
'The base branch to compare changes against (default: main)',
49+
'main'
50+
);
51+
52+
$this->addOption(
53+
'docs-path',
54+
'd',
55+
InputOption::VALUE_REQUIRED,
56+
'Path to the Documentation directory',
57+
'Documentation'
58+
);
59+
60+
$this->addOption(
61+
'output-file',
62+
'o',
63+
InputOption::VALUE_REQUIRED,
64+
'Path to the nginx redirect configuration output file',
65+
'redirects.nginx.conf'
66+
);
67+
$this->addOption(
68+
'versions',
69+
'r',
70+
InputOption::VALUE_REQUIRED,
71+
'Regex of versions to include',
72+
'(main|13.4|12.4)'
73+
);
74+
$this->addOption(
75+
'path',
76+
'p',
77+
InputOption::VALUE_REQUIRED,
78+
'Path, for example /m/typo3/reference-coreapi/',
79+
'/'
80+
);
81+
}
82+
83+
protected function execute(InputInterface $input, OutputInterface $output): int
84+
{
85+
$io = new SymfonyStyle($input, $output);
86+
87+
$baseBranch = $input->getOption('base-branch');
88+
$docsPath = $input->getOption('docs-path');
89+
$outputFile = $input->getOption('output-file');
90+
$versions = $input->getOption('versions');
91+
$path = $input->getOption('path');
92+
93+
if (!is_string($baseBranch)) {
94+
$io->error('Base branch must be a string.');
95+
return Command::FAILURE;
96+
}
97+
98+
if (!is_string($docsPath)) {
99+
$io->error('Documentation path must be a string.');
100+
return Command::FAILURE;
101+
}
102+
103+
if (!is_string($outputFile)) {
104+
$io->error('Output file must be a string.');
105+
return Command::FAILURE;
106+
}
107+
108+
if (!is_string($versions) || preg_match($versions, '') === false) {
109+
$io->error('Versions must be valid regex.');
110+
return Command::FAILURE;
111+
}
112+
113+
if (!is_string($path)) {
114+
$io->error('Path must be a string.');
115+
return Command::FAILURE;
116+
}
117+
118+
$io->title('Creating nginx redirects from git history');
119+
$io->text("Base branch: {$baseBranch}");
120+
$io->text("Documentation path: {$docsPath}");
121+
$io->text("Output file: {$outputFile}");
122+
$io->text("Versions regex: {$versions}");
123+
$io->text("Path: {$path}");
124+
125+
try {
126+
$movedFiles = $this->gitChangeDetector->detectMovedFiles($baseBranch, $docsPath);
127+
128+
if (empty($movedFiles)) {
129+
$io->success('No moved files detected in this PR.');
130+
return Command::SUCCESS;
131+
}
132+
133+
$io->section('Detected moved files:');
134+
foreach ($movedFiles as $oldPath => $newPath) {
135+
$io->text("- <info>{$oldPath}</info> → <info>{$newPath}</info>");
136+
}
137+
138+
$this->redirectCreator->setNginxRedirectFile($outputFile);
139+
140+
141+
$createdRedirects = $this->redirectCreator->createRedirects($movedFiles, $docsPath, $versions, $path);
142+
143+
$io->section('Created nginx redirects:');
144+
foreach ($createdRedirects as $source => $target) {
145+
$sourceUrl = str_replace($docsPath . '/', '', $source);
146+
$sourceUrl = preg_replace('/\.(rst|md)$/', '', $sourceUrl);
147+
148+
$targetUrl = str_replace($docsPath . '/', '', $target);
149+
$targetUrl = preg_replace('/\.(rst|md)$/', '', $targetUrl);
150+
if (is_string($targetUrl) === false) {
151+
$io->error('Target construct failed');
152+
return Command::FAILURE;
153+
}
154+
155+
$io->text("- <info>/{$sourceUrl}</info> → <info>/{$targetUrl}</info>");
156+
}
157+
158+
$io->success(sprintf('Nginx redirects created successfully in %s!', $outputFile));
159+
return Command::SUCCESS;
160+
} catch (\Exception $e) {
161+
$io->error($e->getMessage());
162+
return Command::FAILURE;
163+
}
164+
}
165+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace T3Docs\GuidesCli\Git;
6+
7+
/**
8+
* Detects file changes in git, specifically focusing on moved files
9+
*/
10+
class GitChangeDetector
11+
{
12+
/** @return array<string, string> */
13+
public function detectMovedFiles(string $baseBranch, string $docsPath): array
14+
{
15+
$movedFiles = [];
16+
17+
// Get the common ancestor commit between current branch and base branch
18+
$mergeBase = trim($this->executeGitCommand("merge-base {$baseBranch} HEAD"));
19+
20+
if (empty($mergeBase)) {
21+
throw new \RuntimeException('Could not determine merge base with the specified branch.');
22+
}
23+
24+
// Use git diff to find renamed files
25+
// --diff-filter=R shows only renamed files
26+
// -M detects renames
27+
// --name-status shows the status and filenames
28+
$command = "diff {$mergeBase} HEAD --diff-filter=R -M --name-status";
29+
$output = $this->executeGitCommand($command);
30+
31+
// Parse the output to extract renamed files
32+
$lines = explode("\n", $output);
33+
foreach ($lines as $line) {
34+
if (empty($line)) {
35+
continue;
36+
}
37+
38+
// Format is: R<score>\t<old-file>\t<new-file>
39+
$parts = preg_split('/\s+/', $line, 3);
40+
if ($parts === false) {
41+
continue;
42+
}
43+
44+
if (count($parts) !== 3 || !str_starts_with($parts[0], 'R')) {
45+
continue;
46+
}
47+
48+
$oldPath = trim($parts[1]);
49+
$newPath = trim($parts[2]);
50+
51+
if ($this->isDocumentationFile($oldPath, $docsPath) && $this->isDocumentationFile($newPath, $docsPath)) {
52+
$movedFiles[$oldPath] = $newPath;
53+
}
54+
}
55+
56+
return $movedFiles;
57+
}
58+
59+
private function isDocumentationFile(string $filePath, string $docsPath): bool
60+
{
61+
return str_starts_with($filePath, $docsPath)
62+
&& (str_ends_with($filePath, '.rst') || str_ends_with($filePath, '.md'));
63+
}
64+
65+
private function executeGitCommand(string $command): string
66+
{
67+
$fullCommand = "git {$command} 2>&1";
68+
$output = [];
69+
$returnCode = 0;
70+
71+
exec($fullCommand, $output, $returnCode);
72+
73+
if ($returnCode !== 0) {
74+
throw new \RuntimeException('Git command failed: ' . implode("\n", $output));
75+
}
76+
77+
return implode("\n", $output);
78+
}
79+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace T3Docs\GuidesCli\Redirect;
6+
7+
/**
8+
* Creates nginx redirect configurations for moved documentation files
9+
*/
10+
class RedirectCreator
11+
{
12+
private string $nginxRedirectFile = 'redirects.nginx.conf';
13+
14+
/**
15+
* @param array<string, string> $movedFiles
16+
* @return array<string, string>
17+
*/
18+
public function createRedirects(array $movedFiles, string $docsPath, string $versions, string $path): array
19+
{
20+
$createdRedirects = [];
21+
$nginxRedirects = [];
22+
23+
foreach ($movedFiles as $oldPath => $newPath) {
24+
$oldRelativePath = $this->stripDocsPathPrefix($oldPath, $docsPath);
25+
$newRelativePath = $this->stripDocsPathPrefix($newPath, $docsPath);
26+
27+
$oldUrlPath = $this->convertToUrlPath($oldRelativePath);
28+
$newUrlPath = $this->convertToUrlPath($newRelativePath);
29+
30+
$nginxRedirects[] = sprintf("location = ^%s%s/en-us/%s { return 301 %s$1/en-us/%s; }", $path, $versions, $oldUrlPath, $path, $newUrlPath);
31+
32+
$createdRedirects[$oldPath] = $newPath;
33+
}
34+
35+
if (!empty($nginxRedirects)) {
36+
$nginxConfig = "# Nginx redirects for moved files in Documentation\n";
37+
$nginxConfig .= "# Generated on: " . date('Y-m-d H:i:s') . "\n\n";
38+
$nginxConfig .= implode("\n", $nginxRedirects) . "\n";
39+
40+
file_put_contents($this->nginxRedirectFile, $nginxConfig);
41+
}
42+
43+
return $createdRedirects;
44+
}
45+
46+
/**
47+
* Set a custom path for the nginx redirect configuration file
48+
*/
49+
public function setNginxRedirectFile(string $filePath): void
50+
{
51+
$this->nginxRedirectFile = $filePath;
52+
}
53+
54+
private function stripDocsPathPrefix(string $path, string $docsPath): string
55+
{
56+
if (str_starts_with($path, $docsPath . '/')) {
57+
return substr($path, strlen($docsPath) + 1);
58+
}
59+
return $path;
60+
}
61+
62+
private function convertToUrlPath(string $path): string
63+
{
64+
$path = preg_replace('/\.(rst|md)$/', '.html', $path);
65+
if (is_string($path) === false) {
66+
throw new \RuntimeException('Failed to convert path to URL format');
67+
}
68+
69+
if (basename($path) === 'Index') {
70+
$path = dirname($path);
71+
if ($path === '.') {
72+
$path = '';
73+
}
74+
}
75+
76+
return ltrim($path, '/');
77+
}
78+
}

0 commit comments

Comments
 (0)