diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31bbd67..fd5faba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: - name: Install dependencies # Do a clone here to make sure the repository exists on install # to avoid missing repository error in composer. - run: composer tdk:clone && composer install --no-progress + run: composer install --no-progress - name: phpstan run: ./vendor/bin/phpstan analyse -c .phpstan.neon --no-progress diff --git a/.gitignore b/.gitignore index fa0c5db..f13be9b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,13 @@ var/ public/ composer.lock .idea -typo3-core packages/* !packages/.gitkeep +!packages/tdk-composer-plugin .ddev/ /config/ test-acceptance-tdk/ /tests/_support/_generated -/.vscode \ No newline at end of file +/.vscode +/.php-cs-fixer.cache +/typo3-core \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml index b4af360..055aea7 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -4,6 +4,7 @@ # 8.0 https://gitpod.io/#TDK_PHP_VERSION=8.0,TDK_BRANCH=main,TDK_PATCH_REF=refs%2Fchanges%2F43%2F70643%2F36,TDK_USERNAME=ochorocho,TDK_PATCH_ID=70643/https://github.com/ochorocho/tdk/tree/main # Detect php: https://gitpod.io/#TDK_BRANCH=11.5,TDK_PATCH_REF=refs%2Fchanges%2F43%2F70643%2F36,TDK_USERNAME=ochorocho,TDK_PATCH_ID=70643/https://github.com/ochorocho/tdk/tree/main # https://gitpod.io/#TDK_BRANCH=11.5,TDK_PATCH_REF=refs%2Fchanges%2F43%2F70643%2F36,TDK_USERNAME=ochorocho,TDK_PATCH_ID=70643/https://github.com/ochorocho/tdk/tree/feature/add-ssh-command +# Composer Plugin: https://gitpod.io/#TDK_BRANCH=main,TDK_PATCH_REF=refs%2Fchanges%2F43%2F70643%2F36,TDK_USERNAME=ochorocho,TDK_PATCH_ID=70643/https://github.com/ochorocho/tdk/tree/feature/scripts-to-composer-plugin image: ochorocho/gitpod-tdk:latest @@ -14,14 +15,13 @@ tasks: cp -Rp .gitpod/phpstorm .idea tdk php "$(php .gitpod/php/version.php)" --no-reload gp open .gitpod/info.md - composer tdk:clone - composer tdk:checkout composer install - composer tdk:set-git-config - composer tdk:enable-hooks -- --force - composer tdk:set-commit-template -- --file=./.gitmessage.txt + composer tdk:git checkout + composer tdk:git config + composer tdk:hooks create --force + composer tdk:git template --file=./.gitmessage.txt tdk ssh-add - composer tdk:apply-patch + composer tdk:git apply composer install mkdir -p public/typo3conf touch public/FIRST_INSTALL @@ -34,9 +34,9 @@ tasks: sudo service cron start sleep 5 tdk db create - composer tdk:help + composer tdk:help summary + composer tdk:help done tdk preview fe - gp sync-done tdk-done vscode: extensions: diff --git a/.gitpod/php/version.php b/.gitpod/php/version.php index 80e4768..db94846 100644 --- a/.gitpod/php/version.php +++ b/.gitpod/php/version.php @@ -1,9 +1,8 @@ getFinder()->in([__DIR__ . '/Scripts', __DIR__ . '/tests/Acceptance']); +$config->getFinder()->in([__DIR__ . '/packages/tdk-composer-plugin', __DIR__ . '/tests/Acceptance']); return $config; diff --git a/.phpstan.neon b/.phpstan.neon index fa75e28..d39bb5d 100644 --- a/.phpstan.neon +++ b/.phpstan.neon @@ -1,4 +1,4 @@ parameters: level: 3 paths: - - Scripts + - packages/tdk-composer-plugin diff --git a/README.md b/README.md index b576b41..bf84758 100644 --- a/README.md +++ b/README.md @@ -25,21 +25,22 @@ composer based TYPO3 CoreDev environment. └── typo3-core # TYPO3 repository (master branch) git@github.com:TYPO3/typo3.git ``` -## Additional Composer commands/scripts +## Additional Composer commands `composer ` -* `tdk:setup`: Setup everything to run a Composer based CoreDev Setup -* `tdk:clear`: Delete all files and folder -* `tdk:remove-hooks`: Delete created hooks in `.git/hooks` -* `tdk:enable-hooks`: Create hooks copied from the TYPO3 repository -* `tdk:set-commit-template`: Configure TYPO3 repository to use `.gitmessage.txt` as commit message template +* `tdk:cleanup`: Delete all files and folder including the `typo3-core` repository +* `tdk:hooks `: Create/delete created hooks in `.git/hooks` +* `tdk:git ` + * `config`: Set git name, email and pushurl + * `template`: Configure TYPO3 repository to use `.gitmessage.txt` as commit message template + * `apply`: Apply Gerrit patch e.g. `composer tdk:git apply --ref=refs/changes/60/69360/6` + * `clone`: Download and store the repository in `./typo3-core` * `tdk:set-push-url`: Set Gerrit as remote to push patches to -* `tdk:ddev-config`: Create a basic ddev configuration -* `tdk:help`: Show summary with links to the TYPO3 Contribution Guide -* `tdk:doctor`: Show potential issues -* `typo3`: Shortcut to run TYPO3 Commands -* `tdk:apply-patch`: Apply Gerrit patch e.g. `composer tdk:apply-patch -- --ref=refs/changes/75/72275/17` +* `tdk:ddev`: Create a basic ddev configuration +* `tdk:composer `: Require or remove all TYPO3 Core extensions +* `tdk:help `: Show informational text +* `tdk:doctor`: Show potential issues ## Demo run diff --git a/Scripts/BaseScript.php b/Scripts/BaseScript.php deleted file mode 100644 index 0754b09..0000000 --- a/Scripts/BaseScript.php +++ /dev/null @@ -1,23 +0,0 @@ -getArguments())['project-name'] ?? getenv('TDK_CREATE_DDEV_PROJECT_NAME') ?? false; - if (!$ddevProjectName) { - $skip = isset(self::getArguments($event->getArguments())['no']) ?? false; - if ($skip) { - $createConfig = false; - } else { - $createConfig = $event->getIO()->askConfirmation('Create a basic ddev config [y/n] ?'); - } - - if (!$createConfig) { - $event->getIO()->write('Aborted! No ddev config created.'); - return 0; - } - } - - $validator = ValidatorScript::projectName(); - - if (!$ddevProjectName) { - $defaultProjectName = basename(getcwd()); - $ddevProjectName = $event->getIO()->askAndValidate('Choose a ddev project name [default: ' . $defaultProjectName . '] :', $validator, 2, $defaultProjectName); - } else { - try { - $ddevProjectName = $validator($ddevProjectName); - } catch (\UnexpectedValueException $e) { - $event->getIO()->write('' . $e->getMessage() . ''); - return 1; - } - } - - $phpVersion = self::getPhpVersion(); - $ddevCommand = 'ddev config --docroot public --project-name ' . $ddevProjectName . ' --web-environment-add TYPO3_CONTEXT=Development --project-type typo3 --php-version ' . $phpVersion . ' --create-docroot 1> /dev/null'; - exec($ddevCommand, $output, $statusCode); - - return $statusCode; - } - - return 0; - } - - public static function removeFilesAndFolders(Event $event): void - { - $filesToDelete = [ - 'composer.lock', - 'public/index.php', - 'public/typo3', - self::$coreDevFolder, - 'var', - ]; - - $force = self::getArguments($event->getArguments())['force'] ?? false; - - if ($force) { - $answer = true; - } else { - $answer = $event->getIO()->askConfirmation('Really want to delete ' . implode(', ', $filesToDelete) . '? [y/n] ', false); - } - - if ($answer) { - $filesystem = new Filesystem(); - $filesystem->remove($filesToDelete); - $event->getIO()->write('Done deleting files.'); - } - } - - /** - * Determine php version: - * 1. From env (TDK_PHP_VERSION) - * 2. composer.json of current branch - * 3. Default: 8.1 - * - * @param string $jsonPath - * @return string - * @throws \JsonException - */ - public static function getPhpVersion(string $jsonPath = ''): string - { - if ($version = getenv('TDK_PHP_VERSION')) { - return $version; - } - - if ($jsonPath === '') { - $jsonPath = self::$coreDevFolder . '/composer.json'; - } - - if ($fileContent = file_get_contents($jsonPath)) { - $json = json_decode($fileContent, true, 512, JSON_THROW_ON_ERROR); - preg_match_all('/[0-9].[0-9]/', $json['require']['php'], $versions); - return $versions[0][0]; - } - - return '8.1'; - } - - public static function doctor(Event $event): void - { - $filesystem = new Filesystem(); - - // Test for existing repository - if ($filesystem->exists(self::$coreDevFolder . '/.git')) { - $event->getIO()->write('✔ Repository exists.'); - } else { - $event->getIO()->write('✘ TYPO3 Repository not in place, please run "composer tdk:clone"'); - } - - // Test if hooks are set up - if ($filesystem->exists([ - self::$coreDevFolder . '/.git/hooks/pre-commit', - self::$coreDevFolder . '/.git/hooks/commit-msg', - ])) { - $event->getIO()->write('✔ All hooks are in place.'); - } else { - $event->getIO()->write('✘ Hooks are missing please run "composer tdk:enable-hooks".'); - } - - // Test git push url - $process = new ProcessExecutor(); - $command = 'git config --get remote.origin.pushurl'; - $process->execute($command, $output, self::$coreDevFolder); - - preg_match('/^ssh:\/\/(.*)@review\.typo3\.org/', $output, $matches); - if (!empty($matches)) { - $event->getIO()->write('✔ Git "remote.origin.pushurl" seems correct.'); - } else { - $event->getIO()->write('✘ Git "remote.origin.pushurl" not set correctly, please run "composer tdk:set-git-config".'); - } - - // Test commit template - $commandTemplate = 'git config --get commit.template'; - $process->execute($commandTemplate, $outputTemplate, self::$coreDevFolder); - - if (!empty($outputTemplate) && $filesystem->exists(trim($outputTemplate))) { - $event->getIO()->write('✔ Git "commit.template" is set to ' . trim($outputTemplate) . '.'); - } else { - $event->getIO()->write('✘ Git "commit.template" not set or file does not exist, please run "composer tdk:set-commit-template"'); - } - - // Test vendor folder - if ($filesystem->exists('vendor')) { - $event->getIO()->write('✔ Vendor folder exists.'); - } else { - $event->getIO()->write('✘ Vendor folder is missing, please run "composer install"'); - } - } -} diff --git a/Scripts/GitScript.php b/Scripts/GitScript.php deleted file mode 100644 index 455bb3b..0000000 --- a/Scripts/GitScript.php +++ /dev/null @@ -1,132 +0,0 @@ -getArguments()); - $validator = ValidatorScript::username($event); - - $username = $arguments['username'] ?? getenv('TDK_USERNAME') ?? false; - if ($username === 'none') { - return 0; - } - - if ($username) { - $userData = $validator($username); - } else { - $userData = $event->getIO()->askAndValidate('What is your TYPO3/Gerrit Account Username? ', $validator, 2); - } - - $pushUrl = 'ssh://' . $userData['username'] . '@review.typo3.org:29418/Packages/TYPO3.CMS.git'; - self::setGitConfigValue($event, 'remote.origin.pushurl', $pushUrl); - self::setGitConfigValue($event, 'user.name', $userData['display_name'] ?? $userData['name'] ?? $userData['username']); - self::setGitConfigValue($event, 'user.email', $userData['email']); - - return 0; - } - - public static function setCommitTemplate(Event $event) - { - $arguments = self::getArguments($event->getArguments()); - $validator = ValidatorScript::filePath(); - - if ($arguments['file'] ?? false) { - $file = $validator($arguments['file']); - } else { - $file = $event->getIO()->askAndValidate('Set TYPO3 commit message template [default: .gitmessage.txt] ?', $validator, 3, '.gitmessage.txt'); - } - - $process = new ProcessExecutor(); - $template = realpath($file); - $status = $process->execute('git config commit.template ' . $template, $output, self::$coreDevFolder); - - if ($status) { - $event->getIO()->writeError('Could not enable Git Commit Template!'); - } else { - $event->getIO()->write('Set "commit.template" to ' . $template . ' '); - } - } - - public static function applyPatch(Event $event) - { - $ref = self::getArguments($event->getArguments())['ref'] ?? getenv('TDK_PATCH_REF') ?? false; - if (empty($ref)) { - $event->getIO()->write('No patch ref given'); - return 1; - } - - $filesystem = new Filesystem(); - if ($filesystem->exists(self::$coreDevFolder)) { - $process = new ProcessExecutor(); - $command = 'git fetch https://review.typo3.org/Packages/TYPO3.CMS ' . $ref . ' && git cherry-pick FETCH_HEAD'; - $event->getIO()->write('Apply patch ' . $ref . ''); - $status = $process->executeTty($command, self::$coreDevFolder); - - if ($status) { - $event->getIO()->write('Could not apply patch ' . $ref . ' '); - } - } else { - $event->getIO()->write('Could not apply patch, repository does not exist. Please run "composer tdk:clone"'); - } - - return 0; - } - - public static function cloneRepository(Event $event): void - { - $filesystem = new Filesystem(); - if (!$filesystem->exists(self::$coreDevFolder)) { - $process = new ProcessExecutor(); - $gitRemoteUrl = 'https://github.com/TYPO3/typo3.git'; - $command = sprintf('git clone %s %s', ProcessExecutor::escape($gitRemoteUrl), ProcessExecutor::escape(self::$coreDevFolder)); - $event->getIO()->write('Cloning TYPO3 repository. This may take a while depending on your internet connection!'); - $status = $process->executeTty($command); - - if ($status) { - $event->getIO()->write('Could not download git repository ' . $gitRemoteUrl . ' '); - } - } else { - $event->getIO()->write('Repository exists! Therefore no download required.'); - } - } - - public static function checkoutBranch(Event $event) - { - $branch = self::getArguments($event->getArguments())['branch'] ?? getenv('TDK_BRANCH') ?? false; - if (empty($branch)) { - $event->getIO()->write('No branch name given'); - return 1; - } - - $process = new ProcessExecutor(); - $command = sprintf('git checkout %s', ProcessExecutor::escape($branch)); - $event->getIO()->write('Checking out branch "' . $branch . '"!'); - $status = $process->executeTty($command, self::$coreDevFolder); - if ($status) { - $event->getIO()->write('Could not checkout branch ' . $branch . ' '); - } - - return 0; - } - - private static function setGitConfigValue(Event $event, string $config, string $value): void - { - $process = new ProcessExecutor(); - $command = 'git config ' . $config . ' "' . $value . '"'; - $status = $process->execute($command, $output, self::$coreDevFolder); - if ($status > 0) { - $event->getIO()->writeError('Could not set "' . $config . '" to "' . $value . '"'); - } else { - $event->getIO()->write('Set "' . $config . '" to "' . $value . '"'); - } - } -} diff --git a/Scripts/HookScript.php b/Scripts/HookScript.php deleted file mode 100644 index e363430..0000000 --- a/Scripts/HookScript.php +++ /dev/null @@ -1,89 +0,0 @@ - 'enableCommitMessage', - 'message' => 'Setup Commit Message Hook? [y/n] ', - 'default' => true - ], - [ - 'method' => 'enablePreCommit', - 'message' => 'Setup Pre Commit Hook? [y/n] ', - 'default' => true - ], - ]; - - $force = (bool)(GitScript::getArguments($event->getArguments())['force'] ?? getenv('TDK_HOOK_FORCE_CREATE') ?? false); - foreach ($questions as $question) { - if ($force) { - $answer = true; - } else { - $answer = $event->getIO()->askConfirmation($question['message'], $question['default']); - } - - if ($answer) { - $method = $question['method']; - static::$method($event); - } - } - } - - public static function remove(Event $event) - { - $filesystem = new Filesystem(); - $filesystem->remove([ - self::$coreDevFolder . '/.git/hooks/pre-commit', - self::$coreDevFolder . '/.git/hooks/commit-msg', - ]); - } - - private static function enableCommitMessage(Event $event) - { - $filesystem = new Filesystem(); - - try { - $targetCommitMsg = self::$coreDevFolder . '/.git/hooks/commit-msg'; - $filesystem->copy(self::$coreDevFolder . '/Build/git-hooks/commit-msg', $targetCommitMsg); - - if (!is_executable($targetCommitMsg)) { - $filesystem->chmod($targetCommitMsg, 0755); - } - - $event->getIO()->write('Created Commit Message Hook'); - } catch (IOException $e) { - $event->getIO()->writeError('Exception:enableCommitMessageHook:' . $e->getMessage() . ''); - } - } - - private static function enablePreCommit(Event $event) - { - if (DIRECTORY_SEPARATOR === '\\') { - return; - } - $filesystem = new Filesystem(); - try { - $targetPreCommit = self::$coreDevFolder . '/.git/hooks/pre-commit'; - $filesystem->copy(self::$coreDevFolder . '/Build/git-hooks/unix+mac/pre-commit', $targetPreCommit); - - if (!is_executable($targetPreCommit)) { - $filesystem->chmod($targetPreCommit, 0755); - } - - $event->getIO()->write('Created Pre Commit Hook'); - } catch (IOException $e) { - $event->getIO()->writeError('Exception:enablePreCommitHook:' . $e->getMessage() . ''); - } - } -} diff --git a/Scripts/MessageScript.php b/Scripts/MessageScript.php deleted file mode 100644 index bd27453..0000000 --- a/Scripts/MessageScript.php +++ /dev/null @@ -1,38 +0,0 @@ -To be able to push to Gerrit, you need to add your public key, see https://review.typo3.org/settings/#SSHKeys -EOF; - - $event->getIO()->write($summary); - } - - public static function done(Event $event): void - { - $event->getIO()->write('🎉 Happy days ... TYPO3 Composer CoreDev Setup done!'); - } -} diff --git a/Scripts/ValidatorScript.php b/Scripts/ValidatorScript.php deleted file mode 100644 index 0cd11f7..0000000 --- a/Scripts/ValidatorScript.php +++ /dev/null @@ -1,73 +0,0 @@ -getIO(), $event->getComposer()->getConfig()); - $json = $request->get('https://review.typo3.org/accounts/' . urlencode($username) . '/?pp=0'); - - // Gerrit does not return valid JSON using their JSON API - // therefore we need to chop off the first line - // Sounds weird? See why https://gerrit-review.googlesource.com/Documentation/rest-api.html#output - $validJson = str_replace(')]}\'', '', $json->getBody()); - - return json_decode($validJson, true, 512, JSON_THROW_ON_ERROR); - } -} diff --git a/composer.json b/composer.json index 63f0366..5ed225e 100644 --- a/composer.json +++ b/composer.json @@ -10,30 +10,7 @@ } ], "scripts": { - "pre-install-cmd": "@tdk:clone", - "post-root-package-install": "@tdk:setup", - "post-create-project-cmd": [ - "@tdk:help", - "Ochorocho\\Tdk\\Scripts\\MessageScript::done" - ], - "tdk:setup": [ - "@tdk:clone", - "@tdk:set-git-config", - "@tdk:enable-hooks", - "@tdk:ddev-config", - "@tdk:set-commit-template" - ], - "tdk:clone": "Ochorocho\\Tdk\\Scripts\\GitScript::cloneRepository", - "tdk:clear": "Ochorocho\\Tdk\\Scripts\\CommonScript::removeFilesAndFolders", - "tdk:doctor": "Ochorocho\\Tdk\\Scripts\\CommonScript::doctor", - "tdk:remove-hooks": "Ochorocho\\Tdk\\Scripts\\HookScript::remove", - "tdk:enable-hooks": "Ochorocho\\Tdk\\Scripts\\HookScript::enable", - "tdk:ddev-config": "Ochorocho\\Tdk\\Scripts\\CommonScript::createDdevConfig", - "tdk:help": "Ochorocho\\Tdk\\Scripts\\MessageScript::summary", - "tdk:set-commit-template": "Ochorocho\\Tdk\\Scripts\\GitScript::setCommitTemplate", - "tdk:set-git-config": "Ochorocho\\Tdk\\Scripts\\GitScript::setGitConfig", - "tdk:apply-patch": "Ochorocho\\Tdk\\Scripts\\GitScript::applyPatch", - "tdk:checkout": "Ochorocho\\Tdk\\Scripts\\GitScript::checkoutBranch" + "pre-command-run": "[ -d typo3-core/typo3/sysext ] || git clone https://github.com/TYPO3/typo3.git typo3-core" }, "repositories": { "typo3-core-packages": { @@ -45,48 +22,45 @@ "url": "packages/*" } }, - "autoload-dev": { - "psr-4": { - "Ochorocho\\Tdk\\Scripts\\": "Scripts/" - } - }, "require": { "ext-json": "*", - "typo3/cms-adminpanel": "@dev", - "typo3/cms-backend": "@dev", - "typo3/cms-belog": "@dev", - "typo3/cms-beuser": "@dev", - "typo3/cms-core": "@dev", - "typo3/cms-dashboard": "@dev", + "typo3/cms-composer-installers": "^3", + "ochorocho/tdk-composer-plugin": "@dev", "typo3/cms-extbase": "@dev", - "typo3/cms-extensionmanager": "@dev", - "typo3/cms-felogin": "@dev", - "typo3/cms-filelist": "@dev", - "typo3/cms-filemetadata": "@dev", - "typo3/cms-fluid": "@dev", - "typo3/cms-fluid-styled-content": "@dev", + "typo3/cms-belog": "@dev", + "typo3/cms-adminpanel": "@dev", "typo3/cms-form": "@dev", + "typo3/cms-install": "@dev", + "typo3/cms-filemetadata": "@dev", + "typo3/cms-core": "@dev", "typo3/cms-frontend": "@dev", + "typo3/cms-felogin": "@dev", + "typo3/cms-linkvalidator": "@dev", + "typo3/cms-setup": "@dev", "typo3/cms-impexp": "@dev", - "typo3/cms-indexed-search": "@dev", + "typo3/cms-fluid-styled-content": "@dev", + "typo3/cms-scheduler": "@dev", + "typo3/cms-backend": "@dev", + "typo3/cms-workspaces": "@dev", + "typo3/cms-fluid": "@dev", + "typo3/cms-tstemplate": "@dev", "typo3/cms-info": "@dev", - "typo3/cms-install": "@dev", - "typo3/cms-linkvalidator": "@dev", - "typo3/cms-lowlevel": "@dev", - "typo3/cms-opendocs": "@dev", - "typo3/cms-recordlist": "@dev", + "typo3/cms-dashboard": "@dev", "typo3/cms-recycler": "@dev", "typo3/cms-redirects": "@dev", - "typo3/cms-reports": "@dev", + "typo3/cms-extensionmanager": "@dev", + "typo3/cms-filelist": "@dev", + "typo3/cms-t3editor": "@dev", + "typo3/cms-lowlevel": "@dev", + "typo3/cms-beuser": "@dev", "typo3/cms-rte-ckeditor": "@dev", - "typo3/cms-scheduler": "@dev", "typo3/cms-seo": "@dev", - "typo3/cms-setup": "@dev", - "typo3/cms-sys-note": "@dev", - "typo3/cms-t3editor": "@dev", - "typo3/cms-tstemplate": "@dev", "typo3/cms-viewpage": "@dev", - "typo3/cms-workspaces": "@dev" + "typo3/cms-opendocs": "@dev", + "typo3/cms-sys-note": "@dev", + "typo3/cms-indexed-search": "@dev", + "typo3/cms-reports": "@dev", + "typo3/cms-recordlist": "@dev" }, "require-dev": { "codeception/codeception": "*", @@ -101,7 +75,8 @@ "config": { "allow-plugins": { "typo3/class-alias-loader": true, - "typo3/cms-composer-installers": true + "typo3/cms-composer-installers": true, + "ochorocho/tdk-composer-plugin": true } } } diff --git a/packages/.gitkeep b/packages/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/tdk-composer-plugin/.gitignore b/packages/tdk-composer-plugin/.gitignore new file mode 100644 index 0000000..ff72e2d --- /dev/null +++ b/packages/tdk-composer-plugin/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor diff --git a/packages/tdk-composer-plugin/composer.json b/packages/tdk-composer-plugin/composer.json new file mode 100644 index 0000000..39cf55e --- /dev/null +++ b/packages/tdk-composer-plugin/composer.json @@ -0,0 +1,20 @@ +{ + "name": "ochorocho/tdk-composer-plugin", + "description": "TYPO3 Development Kit additional Composer commands", + "license": "GPL-2.0-or-later", + "type": "composer-plugin", + "require": { + "composer/composer": "*", + "composer-plugin-api": "*" + }, + "autoload": { + "psr-4": { + "Ochorocho\\TdkComposer\\": "src/" + } + }, + "extra": { + "class": "Ochorocho\\TdkComposer\\Plugin", + "plugin-modifies-install-path": true, + "plugin-modifies-downloads": true + } +} diff --git a/packages/tdk-composer-plugin/src/Command/CleanupCommand.php b/packages/tdk-composer-plugin/src/Command/CleanupCommand.php new file mode 100644 index 0000000..8b12c84 --- /dev/null +++ b/packages/tdk-composer-plugin/src/Command/CleanupCommand.php @@ -0,0 +1,59 @@ +setName('tdk:cleanup') + ->setDescription('Delete TYPO3 installation in this TDK (files and folders only)') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to run delete without confirmation') + ->setHelp( + <<getOption('force'); + + if ($force) { + $answer = true; + } else { + $answer = $this->getIO()->askConfirmation('Really want to delete ' . implode(', ', $filesToDelete) . '? [y/n] ', false); + } + + if ($answer) { + $filesystem = new Filesystem(); + $filesystem->remove($filesToDelete); + $this->getIO()->write(BaseService::ICON_SUCCESS . 'Done deleting files.'); + } + + return Command::SUCCESS; + } +} diff --git a/packages/tdk-composer-plugin/src/Command/CommandProvider.php b/packages/tdk-composer-plugin/src/Command/CommandProvider.php new file mode 100644 index 0000000..591fa8d --- /dev/null +++ b/packages/tdk-composer-plugin/src/Command/CommandProvider.php @@ -0,0 +1,23 @@ +composerService = new ComposerService(); + + parent::initialize($input, $output); + } + + protected function configure() + { + $this + ->setName('tdk:composer') + ->setDescription('Manage TYPO3 Core packages with composer.') + ->addArgument('action', InputArgument::OPTIONAL, 'Require/remove all TYPO3 system extensions') + ->setHelp( + <<getArgument('action'); + + switch ($action) { + case 'require': + $this->composerService->requireAllCoreExtensions(); + break; + case 'remove': + $this->composerService->removeAllCoreExtensions(); + break; + default: + $this->getIO()->write($this->getHelp()); + } + + return Command::SUCCESS; + } +} diff --git a/packages/tdk-composer-plugin/src/Command/DdevConfigCommand.php b/packages/tdk-composer-plugin/src/Command/DdevConfigCommand.php new file mode 100644 index 0000000..c6518bc --- /dev/null +++ b/packages/tdk-composer-plugin/src/Command/DdevConfigCommand.php @@ -0,0 +1,81 @@ +setName('tdk:ddev') + ->setDescription('Delete TYPO3 installation in this TDK (files and folders only)') + ->addOption('project-name', null, InputOption::VALUE_OPTIONAL, 'Set project name') + ->addOption('no', null, InputOption::VALUE_NONE, 'Set all to no') + ->setHelp( + <<getOption('project-name') ?? getenv('TDK_CREATE_DDEV_PROJECT_NAME') ?? false; + if (!$ddevProjectName) { + $skip = $input->getOption('no') ?? false; + if ($skip) { + $createConfig = false; + } else { + $createConfig = $this->getIO()->askConfirmation('Create a basic ddev config [y/n]? '); + } + + if (!$createConfig) { + $this->getIO()->write('Aborted! No ddev config created.'); + return Command::SUCCESS; + } + } + + $validationService = new ValidationService($this->getIO(), $this->requireComposer()); + $validator = $validationService->projectName(); + + if (!$ddevProjectName) { + $defaultProjectName = basename(getcwd()); + $ddevProjectName = $this->getIO()->askAndValidate('Choose a ddev project name [default: ' . $defaultProjectName . '] :', $validator, 2, $defaultProjectName); + } else { + try { + $ddevProjectName = $validator($ddevProjectName); + } catch (\UnexpectedValueException $e) { + $this->getIO()->write('' . $e->getMessage() . ''); + return Command::FAILURE; + } + } + + $phpVersion = BaseService::getPhpVersion(); + $ddevCommand = 'ddev config --docroot public --database mariadb:10.3 --project-name ' . $ddevProjectName . ' --web-environment-add TYPO3_CONTEXT=Development --project-type typo3 --php-version ' . $phpVersion . ' --create-docroot'; + $this->getIO()->write($ddevCommand); + return (new ProcessExecutor())->executeTty($ddevCommand); + } + + $this->getIO()->write('No ddev binary found.'); + + return Command::SUCCESS; + } +} diff --git a/packages/tdk-composer-plugin/src/Command/DoctorCommand.php b/packages/tdk-composer-plugin/src/Command/DoctorCommand.php new file mode 100644 index 0000000..2de0e63 --- /dev/null +++ b/packages/tdk-composer-plugin/src/Command/DoctorCommand.php @@ -0,0 +1,162 @@ +filesystem = new Filesystem(); + $this->code = Command::SUCCESS; + $this->composerService = new ComposerService(); + $this->process = new ProcessExecutor(); + + parent::initialize($input, $output); + } + + protected function configure() + { + $this + ->setName('tdk:doctor') + ->setDescription('Test TYPO3 Development Kit setup') + ->setHelp( + <<testGitRepository(); + $this->testHooks(); + $this->testGitPushUrl(); + $this->testCommitTemplate(); + $this->testCoreExtensionSymlinked(); + $this->testVendor(); + + return $this->code; + } + + /** + * @return void + */ + protected function testVendor(): void + { + if ($this->filesystem->exists('vendor')) { + $this->getIO()->write(BaseService::ICON_SUCCESS . 'Vendor folder exists.'); + } else { + $this->getIO()->write(BaseService::ICON_FAILED . 'Vendor folder is missing, please run "composer install"'); + $this->code = Command::FAILURE; + } + } + + protected function testCoreExtensionSymlinked(): void + { + try { + $coreExtensionFolders = $this->composerService->getCoreExtensionsFolder(); + } catch (DirectoryNotFoundException $exception) { + $this->getIO()->write($exception->getMessage()); + return; + } + + $extensionTest = []; + foreach ($coreExtensionFolders as $folder) { + $path = 'public/typo3/sysext/' . $folder->getFileName(); + + $symlink = $this->filesystem->readlink($path, true); + + if ($symlink === null) { + $extensionTest['failed'][] = $folder->getFileName(); + $this->code = Command::FAILURE; + } else { + $extensionTest['success'][] = $folder->getFileName(); + } + } + + if ($extensionTest['failed'] ?? false) { + $this->getIO()->write(BaseService::ICON_FAILED . 'Following extensions are not symlinked: ' . implode( + ', ', + $extensionTest['failed'] + )); + } + + if ($extensionTest['success'] ?? false) { + $this->getIO()->write(BaseService::ICON_SUCCESS . 'Following extensions are symlinked: ' . implode( + ', ', + $extensionTest['success'] + )); + } + } + + protected function testCommitTemplate(): void + { + $commandTemplate = 'git config --get commit.template'; + $this->process->execute($commandTemplate, $outputTemplate, BaseService::CORE_DEV_FOLDER); + + if (!empty($outputTemplate) && $this->filesystem->exists(trim($outputTemplate))) { + $this->getIO()->write(BaseService::ICON_SUCCESS . 'Git "commit.template" is set to ' . trim($outputTemplate) . '.'); + } else { + $this->getIO()->write(BaseService::ICON_FAILED . 'Git "commit.template" not set or file does not exist, please run "composer tdk:git template"'); + $this->code = Command::FAILURE; + } + } + + protected function testGitPushUrl(): void + { + $command = 'git config --get remote.origin.pushurl'; + $this->process->execute($command, $commandOutput, BaseService::CORE_DEV_FOLDER); + + preg_match('/^ssh:\/\/(.*)@review\.typo3\.org/', (string)$commandOutput, $matches); + if (!empty($matches)) { + $this->getIO()->write(BaseService::ICON_SUCCESS . 'Git "remote.origin.pushurl" seems correct.'); + } else { + $this->getIO()->write(BaseService::ICON_FAILED . 'Git "remote.origin.pushurl" not set correctly, please run "composer tdk:git config".'); + $this->code = Command::FAILURE; + } + } + + protected function testHooks(): void + { + if ($this->filesystem->exists([ + BaseService::CORE_DEV_FOLDER . '/.git/hooks/pre-commit', + BaseService::CORE_DEV_FOLDER . '/.git/hooks/commit-msg', + ])) { + $this->getIO()->write(BaseService::ICON_SUCCESS . 'All hooks are in place.'); + } else { + $this->getIO()->write(BaseService::ICON_FAILED . 'Hooks are missing please run "composer tdk:hooks create".'); + $this->code = Command::FAILURE; + } + } + + protected function testGitRepository(): void + { + if ($this->filesystem->exists(BaseService::CORE_DEV_FOLDER . '/.git')) { + $gitService = new GitService(); + $commit = $gitService->latestCommit(); + $this->getIO()->write(BaseService::ICON_SUCCESS . 'Repository exists on commit ' . $commit); + } else { + $this->getIO()->write(BaseService::ICON_FAILED . 'Repository not in place, please run "composer tdk:git clone"'); + $this->code = Command::FAILURE; + } + } +} diff --git a/packages/tdk-composer-plugin/src/Command/GitCommand.php b/packages/tdk-composer-plugin/src/Command/GitCommand.php new file mode 100644 index 0000000..ebaba7a --- /dev/null +++ b/packages/tdk-composer-plugin/src/Command/GitCommand.php @@ -0,0 +1,207 @@ +gitService = new GitService(); + $this->validationService = new ValidationService($this->getIO(), $this->requireComposer()); + parent::initialize($input, $output); + } + + protected function configure() + { + $this + ->setName('tdk:git') + ->setDescription('Do some git operations') + ->addArgument('action', InputArgument::OPTIONAL, 'Manage git related files.') + ->addOption('username', 'u', InputOption::VALUE_OPTIONAL, 'Gerrit/TYPO3 account username') + ->addOption('file', 'f', InputOption::VALUE_OPTIONAL, 'Relative path to your git commit template.') + ->addOption('ref', null, InputOption::VALUE_OPTIONAL, 'Relative path to your git commit template.') + ->addOption('branch', null, InputOption::VALUE_OPTIONAL, 'Checkout a certain git branch.') + ->setHelp( + << - Apply patch to core from Gerrit + * clone - clone TYPO3 Core repository + * checkout --branch - Checkout branch +EOT + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + $action = $input->getArgument('action'); + + switch ($action) { + case 'config': + $this->setConfig(); + break; + case 'template': + $this->setCommitTemplate(); + break; + case 'apply': + $this->applyPatch(); + break; + case 'clone': + $this->cloneRepository(); + break; + case 'checkout': + $this->checkout(); + break; + default: + $this->getIO()->write($this->getHelp()); + } + + return Command::SUCCESS; + } + + protected function setConfig() + { + $username = $this->input->getOption('username') ?? getenv('TDK_USERNAME') ?? false; + if ($username === 'none') { + return Command::SUCCESS; + } + + if ($username) { + $userData = $this->validationService->user()($username); + } else { + $userData = $this->getIO()->askAndValidate('What is your TYPO3/Gerrit Account Username? ', $this->validationService->user(), 3); + } + + $pushUrl = 'ssh://' . $userData['username'] . '@review.typo3.org:29418/Packages/TYPO3.CMS.git'; + + $gitConfigValues = [ + 'remote.origin.pushurl' => $pushUrl, + 'user.name' => $userData['display_name'] ?? $userData['name'] ?? $userData['username'], + 'user.email' => $userData['email'], + ]; + + $code = Command::SUCCESS; + + foreach ($gitConfigValues as $key => $value) { + try { + $this->gitService->setGitConfigValue($key, $value); + $this->getIO()->write('Set "' . $key . '" to "' . $value . '"'); + } catch (IOException $exception) { + $this->getIO()->writeError('' .$exception->getMessage() . '"'); + $code = Command::FAILURE; + } + } + + return $code; + } + + protected function setCommitTemplate(): int + { + $filePath = $this->input->getOption('file'); + + if ($filePath ?? false) { + $file = $this->validationService->filePath()($filePath); + } else { + $file = $this->getIO()->askAndValidate('Set TYPO3 commit message template [default: .gitmessage.txt]? ', $this->validationService->filePath(), 3, '.gitmessage.txt'); + } + + $status = $this->gitService->setCommitTemplate($file); + + if ($status) { + $this->getIO()->writeError('Could not enable Git Commit Template!'); + return Command::FAILURE; + } + + $template = realpath($file); + $this->getIO()->write('Set "commit.template" to ' . $template . ' '); + + return Command::SUCCESS; + } + + public function applyPatch() + { + $ref = $this->input->getOption('ref') ?? getenv('TDK_PATCH_REF') ?? false; + if (empty($ref)) { + $this->getIO()->write('No patch ref given'); + return Command::FAILURE; + } + + if (!$this->gitService->repositoryExists()) { + $this->getIO()->write('Could not apply patch, repository does not exist. Please run "composer tdk:clone"'); + return Command::FAILURE; + } + + if ($this->gitService->applyPatch($ref)) { + $this->getIO()->write('Could not apply patch ' . $ref . ' '); + return Command::FAILURE; + } else { + $this->getIO()->write('Apply patch ' . $ref . ''); + return Command::SUCCESS; + } + } + + public function cloneRepository(): int + { + if ($this->gitService->repositoryExists()) { + $this->getIO()->write('Repository exists! Therefore no download required.'); + return Command::SUCCESS; + } + + $this->getIO()->overwrite('Cloning TYPO3 repository. This may take a while depending on your internet connection!'); + + $gitRemoteUrl = 'https://github.com/TYPO3/typo3.git'; + // $gitRemoteUrl = '/Users/jochen/Development/typo3-dev'; + if ($this->gitService->cloneRepository($gitRemoteUrl)) { + $this->getIO()->write('Could not download git repository ' . $gitRemoteUrl . ' '); + return Command::FAILURE; + } + + return Command::SUCCESS; + } + + public function checkout() + { + $branch = $this->input->getOption('branch') ?? getenv('TDK_BRANCH') ?? false; + if (empty($branch)) { + $branch = 'main'; + } + + $this->getIO()->write('Checking out branch "' . $branch . '"!'); + if ($this->gitService->checkout($branch)) { + $this->getIO()->write('Could not checkout branch ' . $branch . ' '); + } + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('action')) { + $suggestions->suggestValues(['config', 'template', 'apply', 'clone', 'checkout']); + } + } +} diff --git a/packages/tdk-composer-plugin/src/Command/HelpCommand.php b/packages/tdk-composer-plugin/src/Command/HelpCommand.php new file mode 100644 index 0000000..d5a91e1 --- /dev/null +++ b/packages/tdk-composer-plugin/src/Command/HelpCommand.php @@ -0,0 +1,47 @@ +setName('tdk:help') + ->setDescription('Show details to get more information about contributing') + ->addArgument('type', InputArgument::OPTIONAL, 'Which help text show.') + ->setHelp( + <<getArgument('type'); + $baseService = new GitService(); + + switch ($action) { + case 'summary': + $output->writeln($baseService->summary()); + break; + case 'done': + $output->writeln($baseService->done()); + break; + } + + return Command::SUCCESS; + } +} diff --git a/packages/tdk-composer-plugin/src/Command/HookCommand.php b/packages/tdk-composer-plugin/src/Command/HookCommand.php new file mode 100644 index 0000000..c6db88d --- /dev/null +++ b/packages/tdk-composer-plugin/src/Command/HookCommand.php @@ -0,0 +1,125 @@ +setName('tdk:hooks') + ->setDescription('Enable hooks') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to overwrite hooks') + ->addArgument('action', InputArgument::OPTIONAL, 'Create/delete hooks') + ->setHelp( + <<output = $output; + $this->hookService = new HookService(); + + $hooks = ['commit-msg', 'pre-commit']; + $action = $input->getArgument('action'); + $force = (bool)($input->getOption('force') ?? getenv('TDK_HOOK_FORCE_CREATE') ?? false); + $helper = $this->getHelper('question'); + + switch ($action) { + case 'create': + $actionLabel = 'Create'; + break; + case 'delete': + $actionLabel = 'Delete'; + break; + default: + return $this->info($hooks); + } + + $code = Command::SUCCESS; + foreach ($hooks as $file) { + if ($force) { + $answer = true; + } else { + $message = $actionLabel . ' "' . $file . '" Hook? [y/n] '; + $question = new ConfirmationQuestion($message, true); + $answer = $helper->ask($input, $output, $question); + } + + if ($answer) { + switch ($action) { + case 'create': + $code = $this->create($file); + break; + case 'delete': + $code = $this->delete($file); + break; + } + } + } + + return $code; + } + + protected function create(string $file): int + { + try { + $this->hookService->create($file); + $this->output->writeln('Created "' . $file . '" hook'); + return Command::SUCCESS; + } catch (IOException $e) { + $this->output->writeln('Failed to create "' . $file . '" hook:' . $e->getMessage() . ''); + return Command::FAILURE; + } + } + + protected function delete(string $file): int + { + try { + $this->hookService->delete((array)$file); + $this->output->writeln('Deleted "' . $file . '" hook'); + return Command::SUCCESS; + } catch (IOException $e) { + $this->output->writeln('Failed to delete "' . $file . '" hook:' . $e->getMessage() . ''); + return Command::FAILURE; + } + } + + protected function info(array $hooks): int + { + $code = Command::SUCCESS; + foreach ($hooks as $file) { + if ($this->hookService->exists($file)) { + $this->output->writeln('' . BaseService::ICON_SUCCESS . 'Hook "' . $file . '" exists'); + } else { + $this->output->writeln(BaseService::ICON_FAILED . 'Hook "' . $file . '" does not exist'); + $code = Command::FAILURE; + } + } + + if ($code !== Command::SUCCESS) { + $this->output->writeln('You may run "composer tdk:hooks create"'); + } + + return $code; + } +} diff --git a/packages/tdk-composer-plugin/src/Plugin.php b/packages/tdk-composer-plugin/src/Plugin.php new file mode 100644 index 0000000..3de5101 --- /dev/null +++ b/packages/tdk-composer-plugin/src/Plugin.php @@ -0,0 +1,131 @@ +application = new Application(); + $this->application->setAutoExit(false); + $this->composerService = new ComposerService(); + } + + public static function getSubscribedEvents() + { + return [ +// ScriptEvents::POST_ROOT_PACKAGE_INSTALL => [ +// ['cloneRepository', 0] +// ], + ScriptEvents::POST_CREATE_PROJECT_CMD => [ + ['gitConfig', 0], + ['createHooks', 0], + ['ddevConfig', 0], + ['commitTemplate', 0], + ['showInformation', 0] + ] + ]; + } + + public function getCapabilities(): array + { + return [ + CommandProviderCapability::class => CommandProvider::class + ]; + } + + public function deactivate(Composer $composer, IOInterface $io): void + { + // TODO: Implement deactivate() method. + } + + public function uninstall(Composer $composer, IOInterface $io): void + { + // TODO: Implement uninstall() method. + } + + public function cloneRepository(PackageEvent $event): int + { + $operation = $event->getOperation(); + if ($operation instanceof InstallOperation) { + $package = $operation->getPackage()->getName(); + + if ($package === self::PACKAGE_NAME) { + + $input = new ArrayInput(array('command' => 'tdk:git', 'action' => 'clone')); + $this->application->run($input); + + $event->getComposer()->getEventDispatcher()->dispatchScript('typo3-require-done', true); + } + } + + return Command::SUCCESS; + } + + public function gitConfig(Event $event): int + { + $input = new ArrayInput(array('command' => 'tdk:git', 'action' => 'config')); + $this->application->run($input); + + return Command::SUCCESS; + } + + public function createHooks(Event $event): int + { + $input = new ArrayInput(array('command' => 'tdk:hooks', 'action' => 'create')); + $this->application->run($input); + + return Command::SUCCESS; + } + + public function ddevConfig(Event $event): int + { + $input = new ArrayInput(array('command' => 'tdk:ddev')); + $this->application->run($input); + + return Command::SUCCESS; + } + + public function commitTemplate(Event $event): int + { + $input = new ArrayInput(array('command' => 'tdk:git', 'action' => 'template')); + $this->application->run($input); + + return Command::SUCCESS; + } + + public function showInformation(Event $event): int + { + $input = new ArrayInput(array('command' => 'tdk:help', 'type' => 'summary')); + $this->application->run($input); + + $input = new ArrayInput(array('command' => 'tdk:help', 'type' => 'done')); + $this->application->run($input); + + return Command::SUCCESS; + } +} diff --git a/packages/tdk-composer-plugin/src/Service/BaseService.php b/packages/tdk-composer-plugin/src/Service/BaseService.php new file mode 100644 index 0000000..f618329 --- /dev/null +++ b/packages/tdk-composer-plugin/src/Service/BaseService.php @@ -0,0 +1,67 @@ +✔ '; + public const ICON_FAILED = '✘ '; + + protected Filesystem $filesystem; + + public function __construct() + { + $this->filesystem = new Filesystem(); + } + + public function summary(): string + { + $coreFolder = self::CORE_DEV_FOLDER; + return <<To be able to push to Gerrit, you need to add your public key, see https://review.typo3.org/settings/#SSHKeys +EOF; + } + + public function done(): string + { + return '🎉 Happy days ... TYPO3 Composer CoreDev Setup done!'; + } + + public static function getPhpVersion(string $jsonPath = ''): string + { + if ($version = getenv('TDK_PHP_VERSION')) { + return $version; + } + + // @todo: check after patch applied, because a patch may change the version + if ($jsonPath === '') { + $jsonPath = self::CORE_DEV_FOLDER . '/composer.json'; + } + + try { + $fileContent = file_get_contents($jsonPath); + $json = json_decode($fileContent, true, 512, JSON_THROW_ON_ERROR); + preg_match_all('/[0-9].[0-9]/', $json['require']['php'], $versions); + + return trim($versions[0][0]); + } catch (\Exception $exception) { + return '8.1'; + } + } +} diff --git a/packages/tdk-composer-plugin/src/Service/ComposerService.php b/packages/tdk-composer-plugin/src/Service/ComposerService.php new file mode 100644 index 0000000..f0cfa78 --- /dev/null +++ b/packages/tdk-composer-plugin/src/Service/ComposerService.php @@ -0,0 +1,68 @@ +application = new Application(); + $this->finder = new Finder(); + + parent::__construct(); + } + + public function requireAllCoreExtensions(): int + { + $coreExtensions = $this->getCoreExtensions(); + foreach ($coreExtensions as $key => $extension) { + $coreExtensions[$key] = $extension . ':@dev'; + } + + if (count($coreExtensions)) { + $input = new ArrayInput(array('command' => 'require', 'packages' => $coreExtensions)); + $this->application->run($input); + } + + return Command::SUCCESS; + } + + public function removeAllCoreExtensions(): int + { + $coreExtensions = $this->getCoreExtensions(); + if (count($coreExtensions)) { + $input = new ArrayInput(array('command' => 'remove', 'packages' => $coreExtensions)); + return $this->application->run($input); + } + + return Command::SUCCESS; + } + + public function getCoreExtensionsFolder(string $path = 'public/typo3/sysext'): Finder + { + return $this->finder->in($path)->depth(0)->directories(); + } + + public function getCoreExtensions(string $path = BaseService::CORE_DEV_FOLDER . '/typo3/sysext/'): array + { + $files = $this->finder->name('composer.json')->in($path)->depth(1)->files(); + + $coreExtensions = []; + foreach ($files as $file) { + $json = json_decode($file->getContents(), true, 512, JSON_THROW_ON_ERROR); + $coreExtensions[] = $json['name']; + } + + return $coreExtensions; + } +} diff --git a/packages/tdk-composer-plugin/src/Service/GitService.php b/packages/tdk-composer-plugin/src/Service/GitService.php new file mode 100644 index 0000000..61f86ae --- /dev/null +++ b/packages/tdk-composer-plugin/src/Service/GitService.php @@ -0,0 +1,66 @@ +execute('git config commit.template ' . $template, $output, BaseService::CORE_DEV_FOLDER); + } + + public function setGitConfigValue(string $config, string $value): void + { + $process = new ProcessExecutor(); + $command = 'git config ' . $config . ' "' . $value . '"'; + $status = $process->execute($command, $output, BaseService::CORE_DEV_FOLDER); + if ($status > 0) { + throw new IOException('Could not set Git "' . $config . '" to "' . $value); + } + } + + public function applyPatch($ref) + { + $process = new ProcessExecutor(); + $command = 'git fetch https://review.typo3.org/Packages/TYPO3.CMS ' . $ref . ' && git cherry-pick FETCH_HEAD'; + + return $process->executeTty($command, BaseService::CORE_DEV_FOLDER); + } + + public function cloneRepository($url): int + { + $process = new ProcessExecutor(); + $command = sprintf('git clone %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape(BaseService::CORE_DEV_FOLDER)); + + return $process->executeTty($command); + } + + public function checkout(string $branch) + { + $process = new ProcessExecutor(); + $command = sprintf('git checkout %s', ProcessExecutor::escape($branch)); + return $process->executeTty($command, BaseService::CORE_DEV_FOLDER); + } + + public function repositoryExists(): bool + { + return $this->filesystem->exists(BaseService::CORE_DEV_FOLDER . '/.git'); + } + + public function latestCommit(): string + { + $process = new ProcessExecutor(); + $command = 'git log -n 1 --pretty=\'format:%C(auto)%h (%s, %ad)\''; + $process->execute($command, $output, BaseService::CORE_DEV_FOLDER); + + return $output; + } +} diff --git a/packages/tdk-composer-plugin/src/Service/HookService.php b/packages/tdk-composer-plugin/src/Service/HookService.php new file mode 100644 index 0000000..ed1c779 --- /dev/null +++ b/packages/tdk-composer-plugin/src/Service/HookService.php @@ -0,0 +1,39 @@ +filesystem->remove($filePaths); + } + + public function create(string $fileName): void + { + $finder = new Finder(); + $hookTarget = BaseService::CORE_DEV_FOLDER . '/.git/hooks/' . $fileName; + $files = $finder->name($fileName)->in(BaseService::CORE_DEV_FOLDER . '/Build/git-hooks/')->files(); + foreach ($files as $file) { + $this->filesystem->copy($file->getPath() . '/' . $file->getFilename(), $hookTarget); + } + + if (!is_executable($hookTarget)) { + $this->filesystem->chmod($hookTarget, 0755); + } + } + + public function exists($hook): bool + { + $hookTarget = BaseService::CORE_DEV_FOLDER . '/.git/hooks/' . $hook; + return $this->filesystem->exists($hookTarget); + } +} diff --git a/packages/tdk-composer-plugin/src/Service/ValidationService.php b/packages/tdk-composer-plugin/src/Service/ValidationService.php new file mode 100644 index 0000000..9bc816e --- /dev/null +++ b/packages/tdk-composer-plugin/src/Service/ValidationService.php @@ -0,0 +1,65 @@ +io = $io; + $this->composer = $composer; + } + + public function projectName(): \Closure + { + return function ($value) { + if (!preg_match('/^[a-zA-Z0-9_-]*$/', trim($value))) { + throw new \UnexpectedValueException('Invalid ddev project name "' . $value . '"'); + } + + return trim($value); + }; + } + + public function filePath(): \Closure + { + return function ($value) { + if (!is_file($value)) { + throw new \UnexpectedValueException('Invalid file path "' . $value . '"'); + } + + return $value; + }; + } + + public function user(): \Closure + { + return function ($username) { + try { + $request = new HttpDownloader($this->io, $this->composer->getConfig()); + $json = $request->get('https://review.typo3.org/accounts/' . urlencode($username ?? '') . '/?pp=0'); + + // Gerrit does not return valid JSON using their JSON API + // therefore we need to chop off the first line + // Sounds weird? See why https://gerrit-review.googlesource.com/Documentation/rest-api.html#output + $validJson = str_replace(')]}\'', '', $json->getBody()); + + $userData = json_decode($validJson, true, 512, JSON_THROW_ON_ERROR); + } catch (TransportException $exception) { + throw new \UnexpectedValueException('Username "' . $username . '" not found in TYPO3 Gerrit.'); + } + + return $userData; + }; + } +} diff --git a/tests/Acceptance/TdkCest.php b/tests/Acceptance/TdkCest.php index 08c9192..dcd56b1 100644 --- a/tests/Acceptance/TdkCest.php +++ b/tests/Acceptance/TdkCest.php @@ -4,14 +4,17 @@ namespace Acceptance; -use AcceptanceTester as AcceptanceTester; +use AcceptanceTester; +use Codeception\Example; +use Symfony\Component\Filesystem\Filesystem; class TdkCest { private static string $coreDevFolder = 'typo3-core/'; + private static string $extensionFolder = 'public/typo3/sysext/'; private static string $testFolder = __DIR__ . '/../../test-acceptance-tdk/'; - public function _before(AcceptanceTester $I) + public function _before(AcceptanceTester $I): void { chdir(self::$testFolder); } @@ -20,21 +23,54 @@ public function clone(AcceptanceTester $I): void { // Use "composer install" because it triggers tdk:clone $I->runShellCommand('composer install'); - $I->seeResultCodeIs(0); + $I->seeFileFound('config', self::$testFolder . self::$coreDevFolder . '.git/'); + $I->seeInShellOutput('Cloning TYPO3 repository. This may take a while depending on your internet connection!'); $I->seeInShellOutput('Cloning into'); - $I->runShellCommand('composer tdk:clone'); - $I->seeResultCodeIs(0); + $I->runShellCommand('composer tdk:git clone'); $I->seeInShellOutput('Repository exists! Therefore no download required.'); } - public function help(AcceptanceTester $I): void + /** + * @param AcceptanceTester $I + * @param Example $testData + * @return void + * @dataProvider extensionsDataProvider + */ + public function extensionIsSymlink(AcceptanceTester $I, Example $testData): void { - $I->runShellCommand('composer tdk:help'); + $filesystem = new Filesystem(); + $symlink = $filesystem->readlink(static::$extensionFolder . $testData['ext'], true); + $I->assertNotNull($symlink); + } - $I->seeResultCodeIs(0); + public function composerCommand(AcceptanceTester $I): void + { + $I->amGoingTo('Remove all core extensions'); + $I->runShellCommand('composer tdk:composer remove'); + $composerJson = $I->loadRootComposerJsonToArray(); + $exampleJsonRemove = $I->loadExampleComposerJsonToArray('composer-without-core-packages.json'); + $I->assertEquals($exampleJsonRemove['require'], $composerJson['require']); + + $I->amGoingTo('Require all core extensions'); + $I->runShellCommand('composer tdk:composer require'); + $composerJson = $I->loadRootComposerJsonToArray(); + $exampleJsonRequire = $I->loadExampleComposerJsonToArray('composer-core-packages.json'); + $I->assertEquals($exampleJsonRequire['require'], $composerJson['require']); + + $I->amGoingTo('See expected scripts are in place to create the initial repository folder'); + $composerJson = $I->loadRootComposerJsonToArray(); + $I->assertEquals($composerJson['scripts'], ['command' => '[ -d typo3-core/typo3/sysext ] || mkdir -p typo3-core/typo3/sysext']); + } + + public function help(AcceptanceTester $I): void + { + $I->runShellCommand('composer tdk:help summary'); $I->seeInShellOutput('For more Details read the docs:', 'To be able to push to Gerrit, you need to add your public key'); + + $I->runShellCommand('composer tdk:help done'); + $I->seeInShellOutput('TYPO3 Composer CoreDev Setup done'); } /** @@ -42,7 +78,7 @@ public function help(AcceptanceTester $I): void */ public function gitConfig(AcceptanceTester $I): void { - $I->runShellCommand('composer tdk:set-git-config -- --username=ochorocho'); + $I->runShellCommand('composer tdk:git config --username=ochorocho'); $I->amGoingTo('See expected response text of command'); $I->seeInShellOutput('Set "remote.origin.pushurl" to "ssh://ochorocho@review.typo3.org:29418/Packages/TYPO3.CMS.git"'); $I->seeInShellOutput('Set "user.email" to "rothjochen@gmail.com"'); @@ -66,7 +102,7 @@ public function gitConfig(AcceptanceTester $I): void */ public function commitTemplate(AcceptanceTester $I): void { - $I->runShellCommand('composer tdk:set-commit-template -- --file=./.gitmessage.txt'); + $I->runShellCommand('composer tdk:git template --file=./.gitmessage.txt'); $I->seeInShellOutput('Set "commit.template" to '); $I->runShellCommand('git -C ' . self::$coreDevFolder . ' config --get commit.template'); @@ -81,22 +117,21 @@ public function enableHooks(AcceptanceTester $I): void $hooksFolder = self::$testFolder . self::$coreDevFolder . '.git/hooks/'; $I->amGoingTo('Enable the hooks'); - $I->runShellCommand('composer tdk:enable-hooks -- --force'); + $I->runShellCommand('composer tdk:hooks create --force'); - $I->seeResultCodeIs(0); $I->seeFileFound('commit-msg', $hooksFolder); $I->seeFileFound('pre-commit', $hooksFolder); } /** - * @todo: Find a more generic way to test the tdk:apply-patch command + * @todo: Find a more generic way to test the tdk:git apply command * * @param AcceptanceTester $I */ public function applyPatch(AcceptanceTester $I): void { // @todo: Create a dedicated patch to test against, currently this breaks as soon as the patch gets merged - $I->runShellCommand('composer tdk:apply-patch -- --ref=refs/changes/60/69360/6'); + $I->runShellCommand('composer tdk:git apply --ref=refs/changes/60/69360/6'); $I->seeInShellOutput('Apply patch refs/changes/60/69360/6'); $I->runShellCommand('git -C ' . self::$coreDevFolder . ' log -1 --oneline'); @@ -109,19 +144,19 @@ public function applyPatch(AcceptanceTester $I): void public function ddevConfig(AcceptanceTester $I): void { $I->amGoingTo('use a invalid project name'); - $I->runShellCommand('composer tdk:ddev-config -- --project-name="typo3 invalid"'); - $I->seeInShellOutput('Invalid ddev project name'); + $I->runShellCommand('composer tdk:ddev --project-name="typo3 invalid"', false); + $I->seeInShellOutput('Invalid ddev project name "typo3 invalid"'); $I->dontSeeFileFound('.ddev', 'test-acceptance-tdk/'); + $I->seeResultCodeIs(1); $I->amGoingTo('abort configuration'); - $I->runShellCommand('composer tdk:ddev-config -- --no'); + $I->runShellCommand('composer tdk:ddev --no'); $I->seeInShellOutput('Aborted! No ddev config created'); $I->dontSeeFileFound('.ddev', 'test-acceptance-tdk/'); $I->amGoingTo('create a ddev config'); - $I->runShellCommand('composer tdk:ddev-config -- --project-name="typo3-dev-tdk"'); + $I->runShellCommand('composer tdk:ddev --project-name="typo3-dev-tdk"'); $I->seeFileFound('config.yaml', 'test-acceptance-tdk/.ddev/'); - $I->seeResultCodeIs(0); } /** @@ -129,7 +164,7 @@ public function ddevConfig(AcceptanceTester $I): void */ public function checkoutBranch(AcceptanceTester $I): void { - $I->runShellCommand('composer tdk:checkout -- --branch=main'); + $I->runShellCommand('composer tdk:git checkout --branch=main'); $I->seeInShellOutput('Checking out branch "main"!'); $I->runShellCommand('git -C ' . self::$coreDevFolder . ' branch --show-current'); @@ -166,7 +201,7 @@ public function removeHooks(AcceptanceTester $I) $hooksFolder = self::$testFolder . self::$coreDevFolder . '.git/hooks/'; $I->amGoingTo('Delete the hooks'); - $I->runShellCommand('composer tdk:remove-hooks'); + $I->runShellCommand('composer tdk:hooks delete --force'); $I->seeResultCodeIs(0); $I->dontSeeFileFound('commit-msg', $hooksFolder); @@ -178,7 +213,7 @@ public function removeHooks(AcceptanceTester $I) */ public function clear(AcceptanceTester $I): void { - $I->runShellCommand('composer tdk:clear -- --force'); + $I->runShellCommand('composer tdk:cleanup --force'); $I->seeResultCodeIs(0); // Foreach is needed here, as we don't want to @@ -198,4 +233,44 @@ protected function clearDataProvider(): array 'var', ]; } + + protected function extensionsDataProvider(): array + { + return [ + ['ext' => 'adminpanel'], + ['ext' => 'backend'], + ['ext' => 'belog'], + ['ext' => 'beuser'], + ['ext' => 'core'], + ['ext' => 'dashboard'], + ['ext' => 'extbase'], + ['ext' => 'extensionmanager'], + ['ext' => 'felogin'], + ['ext' => 'filelist'], + ['ext' => 'filemetadata'], + ['ext' => 'fluid'], + ['ext' => 'fluid_styled_content'], + ['ext' => 'form'], + ['ext' => 'frontend'], + ['ext' => 'impexp'], + ['ext' => 'indexed_search'], + ['ext' => 'info'], + ['ext' => 'install'], + ['ext' => 'linkvalidator'], + ['ext' => 'lowlevel'], + ['ext' => 'opendocs'], + ['ext' => 'recycler'], + ['ext' => 'redirects'], + ['ext' => 'reports'], + ['ext' => 'rte_ckeditor'], + ['ext' => 'scheduler'], + ['ext' => 'seo'], + ['ext' => 'setup'], + ['ext' => 'sys_note'], + ['ext' => 't3editor'], + ['ext' => 'tstemplate'], + ['ext' => 'viewpage'], + ['ext' => 'workspaces'] + ]; + } } diff --git a/tests/_data/composer-core-packages.json b/tests/_data/composer-core-packages.json new file mode 100644 index 0000000..dc32917 --- /dev/null +++ b/tests/_data/composer-core-packages.json @@ -0,0 +1,82 @@ +{ + "name": "ochorocho/tdk", + "description": "TYPO3 Composer Development Kit", + "type": "project", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Jochen Roth", + "email": "jochen.roth@b13.com" + } + ], + "scripts": { + "command": "[ -d typo3-core/typo3/sysext ] || mkdir -p typo3-core/typo3/sysext" + }, + "repositories": { + "typo3-core-packages": { + "type": "path", + "url": "typo3-core/typo3/sysext/*" + }, + "local-packages": { + "type": "path", + "url": "packages/*" + } + }, + "require": { + "ext-json": "*", + "typo3/cms-composer-installers": "^3", + "ochorocho/tdk-composer-plugin": "@dev", + "typo3/cms-extbase": "@dev", + "typo3/cms-belog": "@dev", + "typo3/cms-adminpanel": "@dev", + "typo3/cms-form": "@dev", + "typo3/cms-install": "@dev", + "typo3/cms-filemetadata": "@dev", + "typo3/cms-core": "@dev", + "typo3/cms-frontend": "@dev", + "typo3/cms-felogin": "@dev", + "typo3/cms-linkvalidator": "@dev", + "typo3/cms-setup": "@dev", + "typo3/cms-impexp": "@dev", + "typo3/cms-fluid-styled-content": "@dev", + "typo3/cms-scheduler": "@dev", + "typo3/cms-backend": "@dev", + "typo3/cms-workspaces": "@dev", + "typo3/cms-fluid": "@dev", + "typo3/cms-tstemplate": "@dev", + "typo3/cms-info": "@dev", + "typo3/cms-dashboard": "@dev", + "typo3/cms-recycler": "@dev", + "typo3/cms-redirects": "@dev", + "typo3/cms-extensionmanager": "@dev", + "typo3/cms-filelist": "@dev", + "typo3/cms-t3editor": "@dev", + "typo3/cms-lowlevel": "@dev", + "typo3/cms-beuser": "@dev", + "typo3/cms-rte-ckeditor": "@dev", + "typo3/cms-seo": "@dev", + "typo3/cms-viewpage": "@dev", + "typo3/cms-opendocs": "@dev", + "typo3/cms-sys-note": "@dev", + "typo3/cms-indexed-search": "@dev", + "typo3/cms-reports": "@dev", + "typo3/cms-recordlist": "@dev" + }, + "require-dev": { + "codeception/codeception": "*", + "codeception/module-cli": "*", + "codeception/module-webdriver": "*", + "phpstan/phpstan": "*", + "composer/composer": "*", + "friendsofphp/php-cs-fixer": "*", + "codeception/module-asserts": "*", + "codeception/module-filesystem": "*" + }, + "config": { + "allow-plugins": { + "typo3/class-alias-loader": true, + "typo3/cms-composer-installers": true, + "ochorocho/tdk-composer-plugin": true + } + } +} diff --git a/tests/_data/composer-without-core-packages.json b/tests/_data/composer-without-core-packages.json new file mode 100644 index 0000000..0c91107 --- /dev/null +++ b/tests/_data/composer-without-core-packages.json @@ -0,0 +1,47 @@ +{ + "name": "ochorocho/tdk", + "description": "TYPO3 Composer Development Kit", + "type": "project", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Jochen Roth", + "email": "jochen.roth@b13.com" + } + ], + "scripts": { + "command": "[ -d typo3-core/typo3/sysext ] || mkdir -p typo3-core/typo3/sysext" + }, + "repositories": { + "typo3-core-packages": { + "type": "path", + "url": "typo3-core/typo3/sysext/*" + }, + "local-packages": { + "type": "path", + "url": "packages/*" + } + }, + "require": { + "ext-json": "*", + "typo3/cms-composer-installers": "^3", + "ochorocho/tdk-composer-plugin": "@dev" + }, + "require-dev": { + "codeception/codeception": "*", + "codeception/module-cli": "*", + "codeception/module-webdriver": "*", + "phpstan/phpstan": "*", + "composer/composer": "*", + "friendsofphp/php-cs-fixer": "*", + "codeception/module-asserts": "*", + "codeception/module-filesystem": "*" + }, + "config": { + "allow-plugins": { + "typo3/class-alias-loader": true, + "typo3/cms-composer-installers": true, + "ochorocho/tdk-composer-plugin": true + } + } +} diff --git a/tests/_support/AcceptanceTester.php b/tests/_support/AcceptanceTester.php index a7d7dac..9703639 100644 --- a/tests/_support/AcceptanceTester.php +++ b/tests/_support/AcceptanceTester.php @@ -2,7 +2,6 @@ declare(strict_types=1); - /** * Inherited Methods * @method void wantToTest($text) @@ -24,4 +23,13 @@ class AcceptanceTester extends \Codeception\Actor /** * Define custom actions here */ + public function loadRootComposerJsonToArray(): array + { + return json_decode(file_get_contents('composer.json'), true, 512, JSON_THROW_ON_ERROR); + } + + public function loadExampleComposerJsonToArray(string $path) + { + return json_decode(file_get_contents(codecept_data_dir($path)), true, 512, JSON_THROW_ON_ERROR); + } }