Skip to content
Draft
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
54 changes: 54 additions & 0 deletions app/Git/FakeGit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Git;

final class FakeGit implements Git
{
public array $commands = [];
private string $branch = 'test';

public function __construct(
private readonly string $path,
) {}

public function isInitialized(): bool
{
return true;
}

public function tryInit(string $remote): void
{
$this->commands[] = 'git init';
}

public function checkoutBranch(string $branch): void
{
$this->branch = $branch;
$this->commands[] = 'git checkout -b ' . $branch;
}

public function pull(): void
{
$this->commands[] = 'git pull origin ' . $this->getCurrentBranch();
}

public function push(): void
{
$this->commands[] = 'git push origin ' . $this->getCurrentBranch();
}

public function getCurrentBranch(): string
{
return 'test';
}

public function status(): string
{
return '';
}

public function commit(string $message): void
{
$this->commands[] = 'git add . && git commit -m "' . $message . '"';
}
}
101 changes: 101 additions & 0 deletions app/Git/GenericGit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace App\Git;

use Tempest\Container\Autowire;
use function Tempest\Support\path;
use Tempest\Support\Filesystem;

#[Autowire]
final readonly class GenericGit implements Git
{
public function __construct(
private string $path,
) {}

public function isInitialized(): bool
{
return Filesystem\exists(path($this->path . '/.git/'));
}

public function tryInit(string $remote): void
{
if ($this->isInitialized()) {
return;
}

exec("cd {$this->path}; git init && git remote add origin {$remote};", result_code: $result);

if ($result !== 0) {
throw new GitOperationFailed('Failed to initialize git repository.');
}
}

public function checkoutBranch(string $branch): void
{
exec("cd {$this->path}; git checkout -b {$branch} &> /dev/null", result_code: $result);

if ($result === 0) {
return;
}

exec("cd {$this->path}; git checkout &> {$branch}", result_code: $result);

if ($result !== 0) {
throw new GitOperationFailed('Failed to checkout branch.');
}
}

public function pull(): void
{
exec("cd {$this->path}; git pull origin {$this->getCurrentBranch()} &> /dev/null", result_code: $result);

if ($result !== 0) {
throw new GitOperationFailed('Failed to pull changes from remote.');
}
}

public function push(): void
{
exec("cd {$this->path}; git push origin {$this->getCurrentBranch()} &> /dev/null", result_code: $result);

if ($result !== 0) {
throw new GitOperationFailed('Failed to push changes to remote.');
}
}

public function getCurrentBranch(): string
{
exec("cd {$this->path}; git branch --show-current", output: $output, result_code: $result);

if ($result !== 0 || $output === []) {
throw new GitOperationFailed('Failed get current branch.');
}

return $output[0];
}

public function status(): string
{
exec("cd {$this->path}; git status" , output: $output, result_code: $result);

if ($result !== 0) {
throw new GitOperationFailed('Failed to get git status.');
}

return implode(PHP_EOL, $output);
}

public function commit(string $message): void
{
if (str_contains($this->status(), 'nothing to commit')) {
return;
}

exec("cd {$this->path}; git add . && git commit -m \"{$message}\"" , result_code: $result);

if ($result !== 0) {
throw new GitOperationFailed('Failed to commit changes.');
}
}
}
22 changes: 22 additions & 0 deletions app/Git/Git.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace App\Git;

interface Git
{
public function isInitialized(): bool;

public function tryInit(string $remote): void;

public function checkoutBranch(string $branch): void;

public function pull(): void;

public function push(): void;

public function getCurrentBranch(): string;

public function status(): string;

public function commit(string $message): void;
}
10 changes: 10 additions & 0 deletions app/Git/GitOperationFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Git;

use Exception;

final class GitOperationFailed extends Exception
{

}
41 changes: 41 additions & 0 deletions app/Subsplit/Package.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace App\Subsplit;

use Tempest\Support\Arr\ImmutableArray;
use function Tempest\Support\arr;

final class Package
{
public function __construct(
public readonly string $path,
public readonly string $name,
) {}

public string $remote {
get => "git@github.com:tempestphp/tempest-{$this->name}.git";
}

public string $buildPath {
get => __DIR__ . '/../../build/packages/' . $this->name . '/';
}

public string $sourcePath {
get => __DIR__ . '/../../packages/' . $this->name . '/';
}

/** @return ImmutableArray<array-key, self> */
public static function all(): ImmutableArray
{
return arr(glob(__DIR__ . '/../../packages/*'))
->map(fn (string $path) => self::fromPath($path));
}

public static function fromPath(string $path): self
{
return new self(
path: $path,
name: pathinfo($path, PATHINFO_FILENAME),
);
}
}
86 changes: 86 additions & 0 deletions app/Subsplit/TempestSubsplitCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace App\Subsplit;

use App\Git\Git;
use App\Git\GitOperationFailed;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\HasConsole;
use Tempest\Console\Middleware\ForceMiddleware;
use Tempest\Container\Container;
use Tempest\Support\Filesystem;
use App\Git\GenericGit;
use function Tempest\root_path;

final class TempestSubsplitCommand
{
use HasConsole;

public function __construct(
private Container $container,
) {}

#[ConsoleCommand(middleware: [ForceMiddleware::class])]
public function __invoke(?string $package = null): void
{
$packages = Package::all();

if ($package) {
$packages = $packages->filter(fn (Package $search) => $search->name === $package)->values();
}

if ($packages->isEmpty()) {
$this->error('No packages found');
return;
}

$total = $packages->count();

$currentBranch = $this->container->get(Git::class, path: root_path())->getCurrentBranch();

$this->confirm("Subsplitting on `{$currentBranch}`, continue?");

foreach ($packages as $i => $package) {
$this->info("{$i}/{$total} <em>tempest/{$package->name}</em>");

try {
$git = $this->container->get(Git::class, path: $package->buildPath);

Filesystem\ensure_directory_exists($package->buildPath);

if ( ! $git->isInitialized()) {
$git->tryInit($package->remote);
$this->writeln(' - New remote initialized');
}

$this->writeln(" - Checking out <em>{$currentBranch}</em> branch");
$git->checkoutBranch($currentBranch);

$this->writeln(' - Pulling latest changes');

try {
$git->pull();
} catch (GitOperationFailed) {
// Remote branch does not exist yet
}

// Don't we have a recursive directory copy function somewhere??
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do now: #1909

However, this is not the right approach. We need to use actual git subtree split (or an equivalent alternative) to keep the history and tags in sync with the main monorepo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to keep the history and tags in sync with the main monorepo

Ah, I was under the impression the release script took care of keeping tags in sync, actually, but that was probably a wrong assumption

When it comes to history: I really don't care about keeping it in sync. However, tags are important indeed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to care about history. This will break Packagist if you don't.

exec("cp -R {$package->sourcePath} {$package->buildPath}");
$this->writeln(" - Copied updated contents from <em>{$package->sourcePath}</em> to <em>{$package->buildPath}</em>");

// TODO: Validate composer, LICENSE, README, and what else?
$this->writeln(" - Committing changes");
$git->commit("chore: subsplit");

$this->writeln(" - Pushing changes");
$git->push();

$this->success('Done');
} catch (GitOperationFailed $e) {
$this->error($e->getMessage());
continue;
}
}
}
}
19 changes: 19 additions & 0 deletions app/TempestReleaseCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App;

use Tempest\Console\ConsoleCommand;
use Tempest\Console\HasConsole;

final class TempestReleaseCommand
{
use HasConsole;

#[ConsoleCommand]
public function __invoke(): void
{
// Move all logic of the `bin/release` script into here

$this->info('Todo');
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
},
"autoload-dev": {
"psr-4": {
"App\\": "app/",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely need to use another directory than app 😬

Something like steward/, release/, cd/, or something more specific like that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I used app as a placeholder. Any preferences?

"Tempest\\Auth\\Tests\\": "packages/auth/tests",
"Tempest\\Cache\\Tests\\": "packages/cache/tests",
"Tempest\\Clock\\Tests\\": "packages/clock/tests",
Expand Down
10 changes: 5 additions & 5 deletions packages/container/src/AutowireDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ public function discover(DiscoveryLocation $location, ClassReflector $class): vo
return;
}

$this->discoveryItems->add($location, [$class, $autowire]);
$this->discoveryItems->add($location, [$class, $autowire, $class->getAttribute(Singleton::class)]);
}

public function apply(): void
{
foreach ($this->discoveryItems as [$class, $autowire]) {
if ($autowire !== null) {
foreach ($this->discoveryItems as [$class, $autowire, $singleton]) {
if ($singleton !== null) {
$this->discoverAsSingleton($class);
} else {
$this->discoverAsDefinition($class);
Expand All @@ -49,7 +49,7 @@ private function discoverAsSingleton(ClassReflector $class): void
foreach ($interfaces as $interface) {
$this->container->singleton(
$interface,
static fn (Container $container) => $container->get($class->getName()),
static fn (Container $container, mixed ...$params) => $container->get($class->getName(), ...$params),
);
}
}
Expand All @@ -61,7 +61,7 @@ private function discoverAsDefinition(ClassReflector $class): void
foreach ($interfaces as $interface) {
$this->container->register(
$interface,
static fn (Container $container) => $container->get($class->getName()),
static fn (Container $container, mixed ...$params) => $container->get($class->getName(), ...$params),
);
}
}
Expand Down
Loading
Loading