From bc6e8f8df2b65e97232cbebffb695fd48a10252c Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 13 Mar 2024 14:34:13 +0100 Subject: [PATCH] TASK: Add force strategy to the rebase workspace command --- .../WorkspaceBasedContentPublishing.feature | 67 +++++++++++++++++++ .../Feature/WorkspaceCommandHandler.php | 7 +- .../Command/RebaseWorkspace.php | 16 ++++- .../Dto/RebaseErrorHandlingStrategy.php | 44 ++++++++++++ .../Bootstrap/Features/WorkspaceCreation.php | 4 ++ .../Command/WorkspaceCommandController.php | 16 +++-- 6 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Dto/RebaseErrorHandlingStrategy.php diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/WorkspacePublishing/WorkspaceBasedContentPublishing.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/WorkspacePublishing/WorkspaceBasedContentPublishing.feature index c7bc3d1531d..f075fc91e83 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/WorkspacePublishing/WorkspaceBasedContentPublishing.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/WorkspacePublishing/WorkspaceBasedContentPublishing.feature @@ -320,3 +320,70 @@ Feature: Workspace based content publishing Then workspace user-ws-two has status OUTDATED + Scenario: Conflicting changes lead to OUTDATED_CONFLICT which can be recovered from via forced rebase + + When the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-ws-one" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-one" | + | workspaceOwner | "owner-identifier" | + And the graph projection is fully up to date + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-ws-two" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-two" | + | workspaceOwner | "owner-identifier" | + And the graph projection is fully up to date + + When the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeVariantSelectionStrategy | "allVariants" | + | coveredDimensionSpacePoint | {} | + | contentStreamId | "user-cs-one" | + And the graph projection is fully up to date + + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "user-cs-two" | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {} | + | propertyValues | {"text": "Modified"} | + And the graph projection is fully up to date + + And the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "noderus-secundus" | + | nodeTypeName | "Neos.ContentRepository.Testing:Content" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {} | + | contentStreamId | "user-cs-two" | + And the graph projection is fully up to date + + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "user-cs-two" | + | nodeAggregateId | "noderus-secundus" | + | originDimensionSpacePoint | {} | + | propertyValues | {"text": "The other node"} | + And the graph projection is fully up to date + + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-ws-one" | + And the graph projection is fully up to date + + Then workspace user-ws-two has status OUTDATED + + When the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-ws-two" | + | rebasedContentStreamId | "user-cs-two-rebased" | + | rebaseErrorHandlingStrategy | "force" | + And the graph projection is fully up to date + + Then workspace user-ws-two has status UP_TO_DATE + And I expect a node identified by user-cs-two-rebased;noderus-secundus;{} to exist in the content graph + diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 4d144cdad7b..18551980299 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -27,6 +27,7 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardIndividualNodesFromWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\WorkspaceRebaseStatistics; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Command\CreateContentStream; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream; @@ -354,7 +355,7 @@ private function publishContentStream( */ private function handleRebaseWorkspace( RebaseWorkspace $command, - ContentRepository $contentRepository, + ContentRepository $contentRepository ): EventsToPublish { $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); @@ -417,7 +418,7 @@ private function handleRebaseWorkspace( $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); // if we got so far without an Exception, we can switch the Workspace's active Content stream. - if (!$rebaseStatistics->hasErrors()) { + if ($command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FORCE || $rebaseStatistics->hasErrors() === false) { $events = Events::with( new WorkspaceWasRebased( $command->workspaceName, @@ -426,6 +427,8 @@ private function handleRebaseWorkspace( ), ); + //@todo persist errors in case force is used $somehow + return new EventsToPublish( $streamName, $events, diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Command/RebaseWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Command/RebaseWorkspace.php index dc3a67382f4..daf481df59a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Command/RebaseWorkspace.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Command/RebaseWorkspace.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -31,13 +32,14 @@ final class RebaseWorkspace implements CommandInterface */ private function __construct( public readonly WorkspaceName $workspaceName, - public readonly ContentStreamId $rebasedContentStreamId + public readonly ContentStreamId $rebasedContentStreamId, + public readonly RebaseErrorHandlingStrategy $rebaseErrorHandlingStrategy ) { } public static function create(WorkspaceName $workspaceName): self { - return new self($workspaceName, ContentStreamId::create()); + return new self($workspaceName, ContentStreamId::create(), RebaseErrorHandlingStrategy::STRATEGY_FAIL); } /** @@ -45,6 +47,14 @@ public static function create(WorkspaceName $workspaceName): self */ public function withRebasedContentStreamId(ContentStreamId $newContentStreamId): self { - return new self($this->workspaceName, $newContentStreamId); + return new self($this->workspaceName, $newContentStreamId, $this->rebaseErrorHandlingStrategy); + } + + /** + * Call this method if you want to run this command with a specific error handling strategy like force + */ + public function withErrorHandlingStrategy(RebaseErrorHandlingStrategy $rebaseErrorHandlingStrategy): self + { + return new self($this->workspaceName, $this->rebasedContentStreamId, $rebaseErrorHandlingStrategy); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Dto/RebaseErrorHandlingStrategy.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Dto/RebaseErrorHandlingStrategy.php new file mode 100644 index 00000000000..4656abea441 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceRebase/Dto/RebaseErrorHandlingStrategy.php @@ -0,0 +1,44 @@ +value; + } +} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php index e28b6d7ad9e..c3aa1be6764 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features; use Behat\Gherkin\Node\TableNode; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; @@ -105,6 +106,9 @@ public function theCommandRebaseWorkspaceIsExecutedWithPayload(TableNode $payloa if (isset($commandArguments['rebasedContentStreamId'])) { $command = $command->withRebasedContentStreamId(ContentStreamId::fromString($commandArguments['rebasedContentStreamId'])); } + if (isset($commandArguments['rebaseErrorHandlingStrategy'])) { + $command = $command->withErrorHandlingStrategy(RebaseErrorHandlingStrategy::from($commandArguments['rebaseErrorHandlingStrategy'])); + } $this->lastCommandOrEventResult = $this->currentContentRepository->handle($command); } diff --git a/Neos.Neos/Classes/Command/WorkspaceCommandController.php b/Neos.Neos/Classes/Command/WorkspaceCommandController.php index c152dc952d9..8b90e7d7101 100644 --- a/Neos.Neos/Classes/Command/WorkspaceCommandController.php +++ b/Neos.Neos/Classes/Command/WorkspaceCommandController.php @@ -23,6 +23,7 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\ContentRepository\Core\Service\WorkspaceMaintenanceServiceFactory; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; @@ -112,19 +113,22 @@ public function discardCommand(string $workspace, string $contentRepositoryIdent * * @param string $workspace Name of the workspace, for example "user-john" * @param string $contentRepositoryIdentifier + * @param bool $force Rebase all events that do not conflict * @throws StopCommandException */ - public function rebaseCommand(string $workspace, string $contentRepositoryIdentifier = 'default'): void + public function rebaseCommand(string $workspace, string $contentRepositoryIdentifier = 'default', bool $force = false): void { $contentRepositoryId = ContentRepositoryId::fromString($contentRepositoryIdentifier); $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); try { - $contentRepository->handle( - RebaseWorkspace::create( - WorkspaceName::fromString($workspace), - ) - )->block(); + $rebaseCommand = RebaseWorkspace::create( + WorkspaceName::fromString($workspace), + ); + if ($force) { + $rebaseCommand = $rebaseCommand->withErrorHandlingStrategy(RebaseErrorHandlingStrategy::STRATEGY_FORCE); + } + $contentRepository->handle($rebaseCommand)->block(); } catch (WorkspaceDoesNotExist $exception) { $this->outputLine('Workspace "%s" does not exist', [$workspace]); $this->quit(1);