Skip to content

Commit 9763476

Browse files
committed
feature #845 Merge-powered recipe update system (weaverryan)
This PR was squashed before being merged into the 1.x branch. Discussion ---------- Merge-powered recipe update system Hi! tl;dr; Updating recipes was a pain. Now its easy: the new `recipes:update` command performs a smart "patch" to your project of what actually changed in the recipe. [![asciicast](https://asciinema.org/a/456707.svg)](https://asciinema.org/a/456707) If you run simply `composer recipes:update`, then it will ask you what to update, from a list of only the out-dated recipes (thanks to @shadowc for the idea!): <img width="696" alt="Screen Shot 2021-12-17 at 11 18 09 AM" src="https://user-images.githubusercontent.com/121003/146575617-8ca7c6c8-1897-4fb4-be33-cdd5bf6894c0.png"> As you can see, if there are conflicts, you fix them like normal git conflicts. In addition to the unit tests, I've tried this on a fairly out-dated project and it worked *brilliantly*. This was NOT possible until @nicolas-grekas open sourced the Flex server - so a HUGE thanks 🎆 ### How It Works A) Whenever `symfony/recipes` (or contrib) has a new commit, a GitHub action *already* stores each recipe as JSON, which becomes the "recipe API": https://github.com/symfony/recipes/tree/flex/main. A change to that action (symfony/recipes#1037) and the recipes-checker (symfony-tools/recipes-checker#2) will add an "archived/" directory that contains EVERY version of every recipe. You can see an example in my fork: https://github.com/weaverryan/recipes/tree/flex/main/archived B) When you run `recipes:update <package/name>`, we fetch the "original" recipe and "new" recipe and then pass both to each "configurator". Its job is to say what files would exist and what they would look if the "original" recipe were installed now and if the "new" recipe were installed now. C) We use these "original files" and "new files" to generate a patch file. D) We apply that patch file to the user's actual project. There are a few tricks here, like temporarily inserting the git "blob" for what the "original" version of the file would look like. That allows for a 3-way merge. So, while it was a bit of work, it's a fairly straightforward process. It makes keeping your recipes up to date both easy and rewarding. ## TODOS * [x] Merge recipes-checker PR symfony-tools/recipes-checker#2 * [x] Merge symfony/recipes PR symfony/recipes#1037 * [x] Update `Downloader` in this PR to remove 2x TODOs that the above PR's will enable * [x] Make sure the `archived/` directory is populated for both `recipes` and `recipes-contrib` - see symfony-tools/recipes-checker#3 and symfony/recipes#1038 * [x] Document that this now exists symfony/symfony-docs#16301 Cheers! Commits ------- 3ee7719 Merge-powered recipe update system
2 parents e7f3f34 + 3ee7719 commit 9763476

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+4141
-287
lines changed

.php_cs.dist renamed to .php-cs-fixer.dist.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
$finder = PhpCsFixer\Finder::create()->in(__DIR__);
44

5-
return PhpCsFixer\Config::create()
5+
return (new PhpCsFixer\Config())
66
->setFinder($finder)
77
->setRules(array(
88
'@Symfony' => true,

src/Command/InstallRecipesCommand.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@
1313

1414
use Composer\Command\BaseCommand;
1515
use Composer\DependencyResolver\Operation\InstallOperation;
16-
use Composer\Factory;
1716
use Symfony\Component\Console\Exception\RuntimeException;
1817
use Symfony\Component\Console\Input\InputArgument;
1918
use Symfony\Component\Console\Input\InputInterface;
2019
use Symfony\Component\Console\Input\InputOption;
2120
use Symfony\Component\Console\Output\OutputInterface;
2221
use Symfony\Flex\Event\UpdateEvent;
23-
use Symfony\Flex\Lock;
22+
use Symfony\Flex\Flex;
2423

2524
class InstallRecipesCommand extends BaseCommand
2625
{
26+
/** @var Flex */
2727
private $flex;
2828
private $rootDir;
2929

@@ -55,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
5555
throw new RuntimeException('Cannot run "sync-recipes --force": git not found.');
5656
}
5757

58-
$symfonyLock = new Lock(getenv('SYMFONY_LOCKFILE') ?: str_replace('composer.json', 'symfony.lock', Factory::getComposerFile()));
58+
$symfonyLock = $this->flex->getLock();
5959
$composer = $this->getComposer();
6060
$locker = $composer->getLocker();
6161
$lockData = $locker->getLockData();

src/Command/RecipesCommand.php

Lines changed: 8 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313

1414
use Composer\Command\BaseCommand;
1515
use Composer\Downloader\TransportException;
16-
use Composer\Util\HttpDownloader;
1716
use Symfony\Component\Console\Input\InputArgument;
1817
use Symfony\Component\Console\Input\InputInterface;
1918
use Symfony\Component\Console\Input\InputOption;
2019
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Flex\GithubApi;
2121
use Symfony\Flex\InformationOperation;
2222
use Symfony\Flex\Lock;
2323
use Symfony\Flex\Recipe;
@@ -31,13 +31,13 @@ class RecipesCommand extends BaseCommand
3131
private $flex;
3232

3333
private $symfonyLock;
34-
private $downloader;
34+
private $githubApi;
3535

3636
public function __construct(/* cannot be type-hinted */ $flex, Lock $symfonyLock, $downloader)
3737
{
3838
$this->flex = $flex;
3939
$this->symfonyLock = $symfonyLock;
40-
$this->downloader = $downloader;
40+
$this->githubApi = new GithubApi($downloader);
4141

4242
parent::__construct();
4343
}
@@ -136,7 +136,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
136136
'',
137137
'Run:',
138138
' * <info>composer recipes vendor/package</info> to see details about a recipe.',
139-
' * <info>composer recipes:install vendor/package --force -v</info> to update that recipe.',
139+
' * <info>composer recipes:update vendor/package</info> to update that recipe.',
140140
'',
141141
]));
142142

@@ -171,13 +171,15 @@ private function displayPackageInformation(Recipe $recipe)
171171
$commitDate = null;
172172
if (null !== $lockRef && null !== $lockRepo) {
173173
try {
174-
list($gitSha, $commitDate) = $this->findRecipeCommitDataFromTreeRef(
174+
$recipeCommitData = $this->githubApi->findRecipeCommitDataFromTreeRef(
175175
$recipe->getName(),
176176
$lockRepo,
177177
$lockBranch ?? '',
178178
$lockVersion,
179179
$lockRef
180180
);
181+
$gitSha = $recipeCommitData ? $recipeCommitData['commit'] : null;
182+
$commitDate = $recipeCommitData ? $recipeCommitData['date'] : null;
181183
} catch (TransportException $exception) {
182184
$io->writeError('Error downloading exact git sha for installed recipe.');
183185
}
@@ -232,7 +234,7 @@ private function displayPackageInformation(Recipe $recipe)
232234
$io->write([
233235
'',
234236
'Update this recipe by running:',
235-
sprintf('<info>composer recipes:install %s --force -v</info>', $recipe->getName()),
237+
sprintf('<info>composer recipes:update %s</info>', $recipe->getName()),
236238
]);
237239
}
238240
}
@@ -324,63 +326,4 @@ private function writeTreeLine($line)
324326

325327
$io->write($line);
326328
}
327-
328-
/**
329-
* Attempts to find the original git sha when the recipe was installed.
330-
*/
331-
private function findRecipeCommitDataFromTreeRef(string $package, string $repo, string $branch, string $version, string $lockRef)
332-
{
333-
// only supports public repository placement
334-
if (0 !== strpos($repo, 'github.com')) {
335-
return [null, null];
336-
}
337-
338-
$parts = explode('/', $repo);
339-
if (3 !== \count($parts)) {
340-
return [null, null];
341-
}
342-
343-
$recipePath = sprintf('%s/%s', $package, $version);
344-
$commitsData = $this->requestGitHubApi(sprintf(
345-
'https://api.github.com/repos/%s/%s/commits?path=%s&sha=%s',
346-
$parts[1],
347-
$parts[2],
348-
$recipePath,
349-
$branch
350-
));
351-
352-
foreach ($commitsData as $commitData) {
353-
// go back the commits one-by-one
354-
$treeUrl = $commitData['commit']['tree']['url'].'?recursive=true';
355-
356-
// fetch the full tree, then look for the tree for the package path
357-
$treeData = $this->requestGitHubApi($treeUrl);
358-
foreach ($treeData['tree'] as $treeItem) {
359-
if ($treeItem['path'] !== $recipePath) {
360-
continue;
361-
}
362-
363-
if ($treeItem['sha'] === $lockRef) {
364-
// shorten for brevity
365-
return [
366-
substr($commitData['sha'], 0, 7),
367-
$commitData['commit']['committer']['date'],
368-
];
369-
}
370-
}
371-
}
372-
373-
return [null, null];
374-
}
375-
376-
private function requestGitHubApi(string $path)
377-
{
378-
if ($this->downloader instanceof HttpDownloader) {
379-
$contents = $this->downloader->get($path)->getBody();
380-
} else {
381-
$contents = $this->downloader->getContents('api.github.com', $path, false);
382-
}
383-
384-
return json_decode($contents, true);
385-
}
386329
}

0 commit comments

Comments
 (0)