Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,29 @@ vendor/bin/monitor matrix
```


---

## Merge many projects to one Monorepo

Automate micro-services merge to one macro project, to make coding saint again.


<br>

### Use

See how the monorepo project and the merge one are different. Use this knowledge to fill gaps in monorepo project. Only then create a final merge pull-request.

```bash
vendor/bin/monitor diff-projects ../monorepo-project ../project-to-be-merged
```

That's it! This command will check:

* differences in dependencies
* differences in autoload
*

<br>

Happy coding!
20 changes: 11 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
{
"name": "rector/monitor",
"description": "Monitor code quality for all your projects and packages in one place, with same PHP version and eliminating package conflicts.",
"description": "Monitor code quality for all your projects/packages in one place. Keep same PHP version, packages and even PHPStan extensions. Helps with merging multiple repositories to one.",
"license": "proprietary",
"bin": [
"bin/monitor"
],
"require": {
"php": ">=8.2",
"composer/semver": "^3.4",
"illuminate/container": "^12.12",
"illuminate/container": "12.40.*",
"nette/neon": "^3.4",
"nette/utils": "^4.1",
"symfony/console": "^6.4",
"symfony/filesystem": "^7.4",
"symfony/finder": "^7.4",
"symfony/process": "^7.4",
"webmozart/assert": "^1.11"
"webmozart/assert": "^1.12"
},
"require-dev": {
"phpecs/phpecs": "^2.1",
"phpecs/phpecs": "^2.2",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpunit/phpunit": "^11.0",
"rector/rector": "^2.0.14",
"phpunit/phpunit": "^11.5",
"rector/jack": "^0.4.0",
"rector/rector": "^2.2",
"shipmonk/composer-dependency-analyser": "^1.8",
"symplify/phpstan-extensions": "^12.0",
"symplify/phpstan-rules": "^14.6",
"tomasvotruba/class-leak": "^2.0",
"tracy/tracy": "^2.10"
"symplify/phpstan-rules": "^14.9",
"tomasvotruba/class-leak": "^2.1",
"tracy/tracy": "^2.11"
},
"autoload": {
"psr-4": {
Expand Down
76 changes: 76 additions & 0 deletions src/Macro/Command/CompareProjectsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Rector\Monitor\Macro\Command;

use Rector\Monitor\Macro\Comparator\ComposerAutoloadComparator;
use Rector\Monitor\Macro\Comparator\ConfigFilesComparator;
use Rector\Monitor\Macro\Comparator\MutuallyMissingPackagesComparator;
use Rector\Monitor\Macro\Comparator\PHPStanExtensionsComparator;
use Rector\Monitor\Macro\Comparator\PHPStanPathsComparator;
use Rector\Monitor\Macro\Contract\ComparatorInterface;
use Rector\Monitor\Macro\ValueObject\ProjectMetadata;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

final class CompareProjectsCommand extends Command
{
/**
* @var ComparatorInterface[]
*/
private readonly array $comparators;

public function __construct(
private readonly SymfonyStyle $symfonyStyle,
ComposerAutoloadComparator $composerComparator,
MutuallyMissingPackagesComparator $mutuallyMissingPackagesComparator,
ConfigFilesComparator $configFilesComparator,
PHPStanExtensionsComparator $phpStanExtensionsComparator,
PHPStanPathsComparator $phpStanPathsComparator
) {
parent::__construct();

$this->comparators = [
$composerComparator,
$mutuallyMissingPackagesComparator,
$configFilesComparator,
$phpStanExtensionsComparator,
$phpStanPathsComparator,
];
}

protected function configure(): void
{
$this->setName('compare-projects');

$this->setDescription(
'Compare two projects and show the differences. First is the macro, monorepo project we want to merge into. Other is the project to merge merge and dismantle later.'
);

$this->addArgument('monorepo-directory', InputArgument::REQUIRED, 'Path to the monorepo directory');
$this->addOption('merge-project', null, InputOption::VALUE_REQUIRED);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$monorepoDirectory = $input->getArgument('monorepo-directory');
$monorepoProjectMetadata = new ProjectMetadata($monorepoDirectory);

$mergeDirectory = $input->getOption('merge-project');
$mergeProjectMetadata = new ProjectMetadata($mergeDirectory);

$this->symfonyStyle->writeln('<fg=green>Both directories found. Comparing 2 projects</>');

foreach ($this->comparators as $key => $comparator) {
$comparator->compare($key + 1, $monorepoProjectMetadata, $mergeProjectMetadata);
$this->symfonyStyle->newLine();
}

return self::SUCCESS;
}
}
75 changes: 75 additions & 0 deletions src/Macro/Comparator/ComposerAutoloadComparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Rector\Monitor\Macro\Comparator;

use Nette\Utils\Json;
use Rector\Monitor\Macro\Contract\ComparatorInterface;
use Rector\Monitor\Macro\Utils\ArrayUtils;
use Rector\Monitor\Macro\ValueObject\ProjectMetadata;
use Symfony\Component\Console\Style\SymfonyStyle;

final readonly class ComposerAutoloadComparator implements ComparatorInterface
{
/**
* @var string[]
*/
private const AUTOLOAD_KEYS = ['autoload', 'autoload-dev'];

public function __construct(
private SymfonyStyle $symfonyStyle
) {
}

public function compare(
int $step,
ProjectMetadata $monorepoProjectMetadata,
ProjectMetadata $mergeProjectMetadata
): void {
$this->symfonyStyle->title(sprintf('%d) PSR-4 autoload differences', $step));

$hasDifference = false;

foreach (self::AUTOLOAD_KEYS as $autoloadKey) {
$monorepoComposerJson = $monorepoProjectMetadata->getComposerJson();
$mergeComposerJson = $mergeProjectMetadata->getComposerJson();

$autoloadDiff = ArrayUtils::diff(
$mergeComposerJson[$autoloadKey] ?? [],
$monorepoComposerJson[$autoloadKey] ?? []
);

if ($autoloadDiff !== []) {
$hasDifference = true;
$this->symfonyStyle->writeln(sprintf(
'<fg=yellow>Monorepo project and project have different "%s":</>',
$autoloadKey
));

$this->symfonyStyle->newLine();
$this->symfonyStyle->writeln(
sprintf('<options=underscore>Monorepo project ("%s")</>', $monorepoProjectMetadata->getName())
);

$this->symfonyStyle->newLine();
$this->symfonyStyle->writeln(Json::encode($monorepoComposerJson[$autoloadKey] ?? [], true));

$this->symfonyStyle->newLine();
$this->symfonyStyle->writeln(sprintf(
'<options=underscore>Merge project ("%s")</>',
$mergeProjectMetadata->getName()
));

$this->symfonyStyle->newLine();
$this->symfonyStyle->writeln(Json::encode($mergeComposerJson[$autoloadKey] ?? [], true));

$this->symfonyStyle->newLine();
}
}

if ($hasDifference === false) {
$this->symfonyStyle->success('Autoloads are identical, nothing spotted');
}
}
}
73 changes: 73 additions & 0 deletions src/Macro/Comparator/ConfigFilesComparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace Rector\Monitor\Macro\Comparator;

use Rector\Monitor\Macro\Contract\ComparatorInterface;
use Rector\Monitor\Macro\ValueObject\ProjectMetadata;
use Symfony\Component\Console\Style\SymfonyStyle;

final readonly class ConfigFilesComparator implements ComparatorInterface
{
public function __construct(
private SymfonyStyle $symfonyStyle
) {
}

public function compare(
int $step,
ProjectMetadata $monorepoProjectMetadata,
ProjectMetadata $mergeProjectMetadata
): void {
// show different config structures
$monorepoConfigDirectory = $monorepoProjectMetadata->getConfigDirectory();
$mergeConfigDirectory = $mergeProjectMetadata->getConfigDirectory();

$this->symfonyStyle->title(sprintf('%d. Comparing config directories', $step));

if ($monorepoConfigDirectory !== $mergeConfigDirectory) {
$this->symfonyStyle->warning('Projects have different /config directory location');

$this->symfonyStyle->writeln(sprintf(
'%s: %s',
$monorepoProjectMetadata->getName(),
$monorepoProjectMetadata->getConfigDirectory(),
));

$this->symfonyStyle->writeln(sprintf(
'%s: %s',
$mergeProjectMetadata->getName(),
$mergeProjectMetadata->getConfigDirectory(),
));
}

// render config structures
if (is_string($monorepoConfigDirectory) && is_string($mergeConfigDirectory)) {
$commonConfigFiles = array_intersect(
$monorepoProjectMetadata->getConfigFiles(),
$mergeProjectMetadata->getConfigFiles()
);

// find shared items
$extraMonorepoConfigFiles = array_diff($monorepoProjectMetadata->getConfigFiles(), $commonConfigFiles);

$extraMergeConfigFiles = array_diff($mergeProjectMetadata->getConfigFiles(), $commonConfigFiles);

$this->symfonyStyle->writeln(sprintf(
'<fg=yellow>Extra monorepo project config files ("%s"):</>',
$monorepoProjectMetadata->getName()
));
$this->symfonyStyle->newLine();
$this->symfonyStyle->listing($extraMonorepoConfigFiles);
$this->symfonyStyle->newLine();

$this->symfonyStyle->writeln(sprintf(
'<fg=yellow>Extra merge project config files ("%s"):</>',
$mergeProjectMetadata->getName()
));
$this->symfonyStyle->newLine();
$this->symfonyStyle->listing($extraMergeConfigFiles);
}
}
}
69 changes: 69 additions & 0 deletions src/Macro/Comparator/MutuallyMissingPackagesComparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Rector\Monitor\Macro\Comparator;

use Rector\Monitor\Macro\Contract\ComparatorInterface;
use Rector\Monitor\Macro\ValueObject\ProjectMetadata;
use Symfony\Component\Console\Style\SymfonyStyle;

final readonly class MutuallyMissingPackagesComparator implements ComparatorInterface
{
/**
* @var string[]
*/
private const REQUIRE_KEYS = ['require', 'require-dev'];

public function __construct(
private SymfonyStyle $symfonyStyle
) {
}

public function compare(
int $step,
ProjectMetadata $monorepoProjectMetadata,
ProjectMetadata $mergeProjectMetadata
): void {
$this->symfonyStyle->title(sprintf('%d. Composer dependencies', $step));

$hasDifference = false;

foreach (self::REQUIRE_KEYS as $requireKey) {
$monorepoComposerJson = $monorepoProjectMetadata->getComposerJson();
$mergeComposerJson = $mergeProjectMetadata->getComposerJson();

$monorepoRequiredPackages = array_keys($monorepoComposerJson[$requireKey] ?? []);
$mergeRequiredPackages = array_keys($mergeComposerJson[$requireKey] ?? []);

$missingPackages = array_diff($mergeRequiredPackages, $monorepoRequiredPackages);
sort($missingPackages);

if ($missingPackages === []) {
continue;
}

$hasDifference = true;

$this->symfonyStyle->writeln(sprintf(
'<fg=yellow>Monorepo project missing couple dependencies in "%s":</>',
$requireKey
));

$this->symfonyStyle->newLine();
$this->symfonyStyle->listing($missingPackages);
$this->symfonyStyle->writeln('<fg=green>Add them via this command to the monorepo project:</>');
$this->symfonyStyle->writeln(sprintf(
'composer require %s %s',
implode(' ', $missingPackages),
$requireKey === 'require-dev' ? '--dev' : ''
));

$this->symfonyStyle->newLine();
}

if ($hasDifference === false) {
$this->symfonyStyle->success('Monorepo project has all required dependencies from the merge project');
}
}
}
Loading