├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG ├── LICENSE ├── README.md ├── RMT ├── autoload.php ├── command.php ├── composer.json ├── composer.lock └── src └── Liip └── RMT ├── Action ├── BaseAction.php ├── BuildPharPackageAction.php ├── ChangelogUpdateAction.php ├── CommandAction.php ├── ComposerUpdateAction.php ├── FilesUpdateAction.php ├── UpdateVersionClassAction.php ├── VcsCommitAction.php ├── VcsPublishAction.php └── VcsTagAction.php ├── Application.php ├── Changelog ├── ChangelogManager.php └── Formatter │ ├── AddTopChangelogFormatter.php │ ├── MarkdownChangelogFormatter.php │ ├── SemanticChangelogFormatter.php │ └── SimpleChangelogFormatter.php ├── Command ├── BaseCommand.php ├── ChangesCommand.php ├── ConfigCommand.php ├── CurrentCommand.php ├── InitCommand.php └── ReleaseCommand.php ├── Config ├── Exception.php ├── Handler.php └── templates │ ├── default-vcs-config.yml.tmpl │ └── no-vcs-config.yml.tmpl ├── Context.php ├── Exception.php ├── Exception └── NoReleaseFoundException.php ├── Information ├── InformationCollector.php ├── InformationRequest.php └── InteractiveQuestion.php ├── Output └── Output.php ├── Prerequisite ├── Command.php ├── ComposerDependencyStabilityCheck.php ├── ComposerJsonCheck.php ├── ComposerSecurityCheck.php ├── ComposerStabilityCheck.php ├── DisplayLastChanges.php ├── TestsCheck.php └── WorkingCopyCheck.php ├── VCS ├── BaseVCS.php ├── Git.php ├── Hg.php └── VCSInterface.php └── Version ├── Generator ├── GeneratorInterface.php ├── SemanticGenerator.php └── SimpleGenerator.php └── Persister ├── ChangelogPersister.php ├── PersisterInterface.php ├── TagValidator.php └── VcsTagPersister.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /bin export-ignore 2 | /docs export-ignore 3 | /test export-ignore 4 | 5 | .rmt.yml export-ignore 6 | box.json export-ignore 7 | 8 | .travis.yml export-ignore 9 | build.xml export-ignore 10 | phpcs.xml export-ignore 11 | phpdox.xml export-ignore 12 | phpmd.xml export-ignore 13 | phpunit.xml.dist export-ignore 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | # Test the latest stable release 17 | - php-version: '7.1' 18 | phpunit-version: 7 19 | - php-version: '7.2' 20 | phpunit-version: 8 21 | - php-version: '7.3' 22 | phpunit-version: 8 23 | - php-version: '7.4' 24 | phpunit-version: 8 25 | - php-version: '8.0' 26 | phpunit-version: 9 27 | # Test Symfony LTS versions. Read more at https://github.com/symfony/lts 28 | - php-version: '7.4' 29 | dependencies: 'symfony/lts:^3' 30 | phpunit-version: 8 31 | - php-version: '8.0' 32 | phpunit-version: 9 33 | symfony-version: '^4' 34 | - php-version: '8.0' 35 | phpunit-version: 9 36 | symfony-version: '^5' 37 | - php-version: '8.0' 38 | phpunit-version: 9 39 | symfony-version: '^6' 40 | - php-version: '8.3' 41 | phpunit-version: 9 42 | symfony-version: '^7' 43 | # Minimum supported dependencies with the oldest PHP version 44 | - php-version: '7.1' 45 | phpunit-version: 7 46 | composer-flag: '--prefer-stable --prefer-lowest' 47 | # Test latest unreleased versions 48 | - php-version: '8.3' 49 | phpunit-version: 9 50 | symfony-version: '^7' 51 | stability: 'dev' 52 | name: PHP ${{ matrix.php-version }} Test on Symfony ${{ matrix.symfony-version }} ${{ matrix.dependencies}} ${{ matrix.stability }} ${{ matrix.composer-flag }} 53 | steps: 54 | - name: Pull the code 55 | uses: actions/checkout@v2 56 | - name: Install PHP and Composer 57 | uses: shivammathur/setup-php@v2 58 | with: 59 | php-version: ${{ matrix.php-version }} 60 | tools: composer:v2, phpunit:${{ matrix.phpunit-version }} 61 | # this flag must be off so that RMT can create a phar 62 | ini-values: phar.readonly=off 63 | - name: Check PHP Version 64 | run: php -v 65 | - name: Stability 66 | run: composer config minimum-stability ${{ matrix.stability }} 67 | if: ${{ matrix.stability }} 68 | - name: Additional require 69 | run: composer require --no-update ${{ matrix.dependencies }} 70 | if: ${{ matrix.dependencies }} 71 | - name: Symfony version 72 | run: composer require --no-update symfony/flex && composer config extra.symfony.require ${{ matrix.symfony-version}} 73 | if: ${{ matrix.symfony-version }} 74 | - name: Composer update 75 | run: composer update ${{ matrix.composer-flag }} --prefer-dist --no-interaction 76 | - name: Composer validate 77 | run: composer validate --strict --no-check-lock 78 | if: ${{ matrix.stability != 'dev' }} 79 | - name: Configure git 80 | run: | 81 | git config --global user.email "test@test.com" 82 | git config --global user.name "John Doe" 83 | git config --global init.defaultBranch main 84 | 85 | - name: Run tests 86 | run: phpunit 87 | if: ${{ matrix.stability != 'dev' }} 88 | - name: Run tests allow to fail 89 | run: phpunit 90 | continue-on-error: true 91 | if: ${{ matrix.stability == 'dev' }} 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | cache.properties 3 | /build 4 | /builds 5 | bin 6 | !bin/UpdateApplicationVersionCurrentVersion.php 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2 | VERSION 1 FIRST STABLE VERSION 3 | =============================== 4 | 5 | Version 1.7 - Compatibility with Sf/Console v6 + cleanup the tests + switch to github actions 6 | 12/01/2024 15:47 1.7.4 Fix missing decalartion of class properties 7 | 0d48eab Some more PHP code cleanup 8 | 5700d07 Fix deprecation of dynamic assignment of class properties 9 | 13/12/2023 05:33 1.7.3 Compatibility with Symfony 7 10 | 38ec28d test symfony 7 11 | 16/10/2023 04:53 1.7.2 Replace vierbergenlars/php-semver by composer/semver 12 | bd9c3df Tentative to replace vierbergenlars/php-semver by composer/semver 13 | bcc9848 Tentative to replace vierbergenlars/php-semver by composer/semver 14 | 11/09/2023 03:31 1.7.1 Some fixes related to PHP 8.2 deprecation warnings 15 | 7a56946 Add symfony/flex in the composer allow-plugins list 16 | 5ad1e76 Ticket #173: prevent PHP 8.2 deprecation warnings. 17 | afbb70b Fix typo in `default-vcs-config.yml.tmpl` file 18 | 15/03/2022 07:32 1.7.0 initial release 19 | 9a1a4fb Fix the error message when the local-php-security-checker is missing 20 | 69649a0 fix lowest version build, drop tests with symfony 2 21 | 55d74d6 allow building phars and avoid git default branch name warning 22 | 3833c2f cleanup php method signatures 23 | 9e542ad switch to github actions 24 | 9b9eeac Fix errors on Symfony/Console v6.0+ 25 | 6dbe73d Cleanup the tests output + migrate the XML config 26 | db6a2be Add some doc about how to run the tests locally + replace laurent by jo 27 | 28 | Version 1.6 - Create prerequisite that checks for composer dependencies to development versions 29 | 23/11/2021 07:59 1.6.4 Add support for Symfony 6 30 | 8bba876 Update composer.lock 31 | 6a0b56d Allow Symfony 6 32 | 09/02/2021 09:31 1.6.3 Replace sensiolabs/security-checker by sensiolabs/security-checker 33 | ad38f80 Update deprecated security checker 34 | 12/01/2021 08:27 1.6.2 Adjust the test suite to support PHP 8 35 | 37aaf5a add php 8 support and drop php < 7.1 36 | 4e878d4 test with php 8 37 | 17/03/2020 08:56 1.6.1 Add support to PHP 7.4 38 | 104856b Add PHP 7.4 to Travis CI 39 | 481b928 Fix tests 40 | 85d51bc Fix the order of implode() arguments 41 | 13/12/2019 15:10 1.6.0 initial release 42 | dfc6641 Update documentation. 43 | e59ddbb Clean up constructor. 44 | 8f53fc8 Create prerequisite that checks for composer dependencies to development versions. 45 | 46 | Version 1.5 - Allow multiple files to be updated at once 47 | 13/12/2019 14:33 1.5.3 Add Symfony 5 support 48 | 31b0731 Fix security-checker compatibility and fix code usage for Symfony 5 49 | 7812528 Add TravisCI config for Symfony 5 50 | d0fff3e Add Symfony 5 support 51 | 11/03/2019 21:48 1.5.2 Improve how the --ignore-check option is handled 52 | e9fc618 Clean up 53 | 6f84449 Fix typo. 54 | 0434b6c Improve how the --ignore-check option is handled. Make better exceptions messages, referencing the allow-ignore configuration key. 55 | 11/03/2019 08:29 1.5.1 Use latest security-checker 56 | 23d397f Update composer lock 57 | 5b0f716 Use latest security-checker 58 | 20/02/2019 21:58 1.5.0 initial release 59 | f2a4ef2 Refactor Readme to have valid config 60 | da663f3 Clean up 61 | 45a97f1 Update naming 62 | 4918681 Remove debug value 63 | d382fb5 Allow to update multiple file version 64 | 8b5a0b3 Fix tests 65 | 33b5243 Add some doc 66 | c8ceeef Fix tests 67 | c80a650 DDD 68 | 7ed4e70 Remove testing for hhvm and php 5.6 69 | 70 | Version 1.4 - Markdown support for CHANGELOG formating, thanks to @ppetermann 71 | 02/01/2019 21:16 1.4.1 Extend the default command timeout to 10 minutes + make it configurable 72 | 75b322e Added timeout to actions 73 | 11/09/2018 04:11 1.4.0 initial release 74 | 8b011c7 Compatibility with PHPUnit 7.3.5 75 | 8dedf27 Fix the CHANGELOG 76 | 212a3c8 dropped name of company i haven't been working for in a while 77 | 1cea690 Added MarkdownChangelogFormatter (#138), based on the SemanticChangelogFormatter 78 | 79 | Version 1.3 - Compatibility with Symfony 4.0 and PHP 7.2 + Document Drifter install 80 | 20/12/2017 14:37 1.3.0 initial release 81 | cedd796 add branch-alias in composer.json 82 | ef072a7 update the lock file 83 | 85d993a allow symfony 4.0 (#129) 84 | 96056bc build with recent symfony versions and php 7.2 85 | 8a7b8ee have hg tests work correctly with new versions of mercurial 86 | cb13a4a Add a Drifter option in the install possibilities 87 | 110a47f Travis build tweaks 88 | 854cc5a Require supported version of phpunit and fix test with forward compatibility layer 89 | cfdce17 Update php requirement to only allow supported php versions 90 | 91 | Version 1.2 - Compatibility with Symfony 3.0 and PHP 7.0 92 | 09/05/2017 10:03 1.2.7 Last security checker version + Documentation updates 93 | d1a89e9 Fixed typo 94 | c36aaa9 Add the doc about the `{branch-name}` placeholder 95 | a6f0e70 Document persister options 96 | d7006d6 Small typo fix in the description of composer.json 97 | 375a61d Allow sensiolabs/security-checker in latest version (required by symfony 3.1.x) 98 | 07/11/2016 06:33 1.2.6 Allow to run an external command into the pre-requisite list 99 | bf179f3 Add the command action to pre-requisite doc 100 | 6cb272d Allow to run an external command into the pre-requisite list too 101 | 5d427bd Document the `command` action 102 | 09/09/2016 15:48 1.2.5 Allow to configure a timeout option for the test task 103 | 593452c Support Symfony Process 2.3 "process timed-out" exception message 104 | 8e64bf9 Support Symfony Process 2.3 "process timed-out" exception message 105 | 5a8d21d Use PHP 5.3-compatible traditional array syntax 106 | dc3c211 Make mocks added in test-check timeout tests PHPUnit 4.5-compatible 107 | 0431730 Replace usage of magic class constant with FQCN string 108 | a11982f Allow specification of tests check command timeout 109 | b19d2cd Minor fixes 110 | 08/06/2016 17:35 1.2.4 Better warning message when using --ignore-check 111 | 7ebbedf Warn the user local modifications may be included during commit action 112 | 54ded79 Small cleanup 113 | 5c46f70 Fix CHANGELOG 114 | 09/05/2016 09:25 1.2.3 Restore PHP 5.3 compatibility (was broken by 1.2.2) 115 | ee00acd Fix tests of version 1.2.2 and keep php 5.3 compatibility 116 | 09/05/2016 08:15 1.2.2 --ignore-check option must be explicitly activated for the working-copy-check prerequisite 117 | 823f40d Now --ignore-check option must be explicitly activated for the working-copy-check prerequisit 118 | 18/04/2016 15:03 1.2.1 Add support for commit and tag gpg-signing 119 | de7a350 Add a little for documentation following https://github.com/liip/RMT/pull/107 120 | a5937c9 Fix a copy/paste error for GPG sign commits 121 | 020a200 removed typo 122 | 7430de8 changed as requested 123 | 881badb add support for commit and tag gpg-signing 124 | 08/12/2015 14:31 1.2.0 initial release 125 | 719adfa Partialy Revert "add BC layer to support both Symfony 3 and 2.3", to remove the early binding between an action and the input interface 126 | c28add3 add BC layer to support both Symfony 3 and 2.3 127 | 9f7d377 make symfony deps explicit in the travis setup 128 | 0f4e3f7 bumped minimal requirement to PHP 5.3.9 and Symfony 2.3, allow Symfony 3.0 129 | 056f745 php codingstyle fixes 130 | 6c0a82b Add a small note about the specific name parameter 131 | 132 | Version 1.1 - Add support to labels in sementic versionning, thanks to @ahilles107, @acrobat, @krtek4 and @lsmith77 for the contribution 133 | 06/05/2015 22:11 1.1.9 New action build-phar-package 134 | da8d1f2 Another php 5.3 compatibility fix 135 | c26fc37 Use php 5.3 array notation 136 | 9208535 Add tests 137 | bd2b6e3 Add support for separate CLI and web usage default stubs 138 | 2597dc3 Missing docblock 139 | 521019c Add support for package-level metadata 140 | e0f8b10 Give feedback about the generated package location 141 | 7e85dd1 Do not fail on first release 142 | 69d6220 Added option to exclude specific paths from the package build 143 | ee27a39 Initial release of the BuildPharPackage action 144 | 06/05/2015 22:03 1.1.8 Various small fix and typo 145 | 9b182b5 Update of the changes command to add option --files 146 | 0baebc2 Fix the composer instruction for installing 147 | 048cae2 Minor phpcs fixes 148 | fd21b0f Small typo fixes introduced in #91 149 | 040811a Added composer global isntallation instructions 150 | 4e7d910 the running method does not exists on Phar in HHVM 151 | e6ff85b Add some badges to the README 152 | fd25b81 run tests on HHVM too 153 | 7d70c8a Fix typo 154 | 28/10/2014 11:33 1.1.7 New prerequiste [composer security check] + some CS fixes 155 | 9a20d2c cleaning up a bit 156 | 166a799 added composer security check 157 | cb6b6db phpcs fixes 158 | 4ef80de fix CS issues 159 | 23/10/2014 20:57 1.1.6 Fix info tag in title 160 | 8d6f95f PSR-2 compliant \o/ 161 | 06c2680 Only warn about line with more than 200 chars 162 | cf17ecf activate PSR-2 for PHPCS 163 | 55d2f82 Fixed info tag in title 164 | 23/10/2014 19:12 1.1.5 Only export needed file into package 165 | 25c128a fix hg branch test 166 | 91a8956 Only export needed files 167 | 11/09/2014 16:05 1.1.4 New action Command to allow to execute a cmd/script 168 | cf58130 RMT config, display the changes first 169 | e4bfed0 Finalization of the CommandAction fix #50 170 | 29c8af6 Fix typo in the README 171 | 78c1454 Prototype of a new generic command #50 172 | 28/08/2014 18:00 1.1.3 Two new prerequistes check related to Composer validation (Thanks Peter Petermann) 173 | f7ac129 PSR-2 fixes 174 | 3f06571 adding check that allows to test for composers minimum-stability setting 175 | ed329e3 confirmSuccess 176 | 2177921 typo, also option should work as in docs 177 | bc6a12a adding an option to run a composer validate within the prerequisites 178 | 28/08/2014 17:54 1.1.2 new option "exclude-merge-commits" (thanks Jeroen Thora) 179 | e2badf6 Configure RMT itself to use the new option "exclude-merge-commits" 180 | 02a4e97 Added exclude merge commits option 181 | 9207c5f Fixed minor config option typo 182 | 18/08/2014 08:50 1.1.1 Various small fixes 183 | 8c87b90 Merge pull request #79 from acrobat/phpcs-fixes 184 | 99efb63 phpcs fixes 185 | 3835237 Merge pull request #78 from liip/cleanup_travis_setup 186 | 2d65fb6 Updated .travis.yml setup to include latest symfony and php versions, no longer do an update since we need to do a require later 187 | ef1859a code style 188 | 990de63 Remove phpdox from ant build script 189 | 61e3e61 Try to make InformationRequest simpler 190 | 5bb2e83 fix coding style 191 | a5b087a Merge pull request #76 from acrobat/semver-version-fix 192 | 8e64bdb Updated semver to stable release, fixes #74 193 | d9b2aec notify slack 194 | 28/07/2014 10:56 1.1.0 initial release 195 | 6aa7120 Following the PR #73, I just add the option to activate (or not) the label management. Most of the users were currently not using the label system, so forced them to always answer 'none' could be painful. To activate it on your project (or a dedicated branch) just add 'allow-label' in your config 196 | 0ce023d Add the composer.lock to git 197 | 202a8a0 Merge pull request #73 from ahilles107/beta_releases 198 | c3d27ca simplify if/else, use rc php-semver release 199 | fa3d07e provide a way to add label for release 200 | 7771fd5 Merge pull request #70 from ppetermann/patch-1 201 | 5589eea added information on phar-composer, see #69 202 | a478fa9 Merge pull request #66 from skck/master 203 | c790587 Add option to specify a custom commit message 204 | 205 | Version 1.0 - First stable version 206 | 03/04/2014 22:45 1.0.4 Add a new command RMT config 207 | 5c2990b Add a new command RMT config 208 | f158e6e Better error handling when parsing the config 209 | 17/02/2014 11:45 1.0.3 UpdateVersionClass accept file path now (thanks ppetermann) 210 | d213ef7 Merge pull request #65 from ppetermann/master 211 | 2d755da moved changes to single file, edited readme 212 | f8b7cf9 added UpdateVersionClassFileAction 213 | 20/01/2014 06:36 1.0.2 New init option to skip creation of the RMT script 214 | 1c39f8e Merge https://github.com/liip/RMT/pull/60 215 | 12039d1 Cleanup PR60 216 | 98070aa adding support for creating phar-RMT through http://box-project.org/ 217 | 011b988 added phpstorms .idea metadata to gitignore 218 | 30f18cb Merge branch 'master' of https://github.com/liip/RMT 219 | 5df1b27 added configonly option to init command 220 | 844efba removed unused use 221 | 90b4640 adding RMT to vendor/bin/RMT 222 | e93f85e removed unused use 223 | 9bad5ef use PHP function rather than doing exec 224 | 09/01/2014 07:41 1.0.1 Bug fix #56 (Unix chmod usage) 225 | e959871 Replace a unix chmod by a php chmod, fix #61 226 | 20/12/2013 07:56 1.0.0 initial release 227 | 0532960 Add the contributors link in the README 228 | df1cfff Update the documentation related to the move on stable 229 | 12de609 Update composer config to tag it as stable 230 | 231 | VERSION 0 FIRST BETA 232 | ===================== 233 | 234 | Version 0.9 - Beta release 235 | 13/12/2013 14:11 0.9.16 Simplification of the config 236 | c8d99a3 Merge pull request #58 from jeanmonod/new-config 237 | 1ff971b Fix tests for the new config format used in the init command 238 | 4dc9190 Update the documentation and template to match the new config model ref #56 239 | dbd028e Add a LICENCE file fix #52 240 | 6d4244a Allow a new config mode with a section "_default" fix #56 241 | 5ccb00e Merge pull request #57 from liip/fix-warning 242 | c4f8b15 BaseAction has a protected options field 243 | bb4e478 Revert "Use composer's binary support for command.php" 244 | 18387cc Use composer's binary support for command.php 245 | fac130b Small fix 246 | 19/11/2013 16:05 0.9.15 Move the publish confirmation question right before publishing + fix #12 247 | f2f4b2d Display the number of questions in the interactive session 248 | e524ee6 Do not set a default value for input option when an interactive question is planned fix #12 249 | 89f56f4 Move the publish confirmation question right before publishing fix #47 250 | 1fe370d Move all the write* methods from the BaseCommand to the Output class 251 | 18/11/2013 06:24 0.9.14 Add a new TestCheck prerequisite 252 | 98ae014 Add a test check prerequisite and use it for RMT fix #51 253 | 9f86275 Update README.md 254 | 12/11/2013 07:49 0.9.13 Better init command + various refactoring 255 | e14203d Rename the rmt config file 256 | 47b8c1e Method renaming in tests 257 | 003d0b3 Handle more gracefully errors related to VCS 258 | ab28225 Init command enhancement: * based on commented yml templates * default config file is now .rmt.yml * remove unused JSONHelper 259 | 7cecfb5 Strict comparison for remote, in case a remote is named "0"... 260 | 06/11/2013 08:23 0.9.12 Hidden response, new class updater, new 'changes' command + some bug fixes 261 | 90ef2c2 Fix a bug when publishing with no remote fix #53 262 | 05d91aa Better YML handling fix #54 263 | 337125e Merge pull request #45 from liip/add-handlers 264 | e8264f2 adding documentation 265 | c26b27a Commit an example of the current changelog formatter 266 | e3828aa Add a new command that display the list of changes since the last release 267 | d78b796 Merge pull request #48 from richardfullmer/hidden-response 268 | 10e325f adding optional pattern for version string 269 | 17d7757 Add support for "hidden" responses 270 | a47d0c3 adding a version class updater and another changelog formatter 271 | 01/11/2013 00:49 0.9.11 Better publish action 272 | a8b875b Add 3 configuration options on the publish action: ask-confirmation, remote-name, ask-remote-name 273 | 7a71338 Merge branch 'pr/49' 274 | c4b6d10 Add support for the name of the remote to push changes to 275 | 31/10/2013 23:52 0.9.10 YAML syntax + various bug fixes 276 | c141283 Fix #38 when using the dump-commit option on a first release 277 | a223ec3 Small cleanup 278 | a2ddebb Merge pull request #46 from liip/improve-error 279 | 65452f5 improve exception message when git command fails 280 | 156f007 try to reduce complexity 281 | 6179cf7 phpdoc and supress PHPMD warning for necessary exit 282 | 7cbaacd add a title formatting style, phpdoc 283 | 6875a0c move the custom styles to the Output class 284 | cbbd341 phpdoc cleanup, call parent methods 285 | aa78a9d fix build to generate phpmd log file 286 | 7c97bd0 Merge pull request #41 from jeanmonod/option-merge 287 | 73ad1e7 Merge pull request #42 from jeanmonod/yaml-config 288 | f2b619e Auto create the CHANGELOG file if absent fix #40 289 | 0f4222f Change the init command to use yaml by default and adapt test 290 | 6793372 Convert the documentation from JSON to YML 291 | 872f336 Allow to override only options for branch specific 292 | bd05cfb Accept YML as config syntax 293 | 26/09/2013 23:51 0.9.9 Fix compatibility accross all symfony:console version 294 | 387e342 Configure travis to build over all version of the symfony:console dependency 295 | b3a014b Put back a test removed by @bonndan by fix it by redirecting errors on the standard output 296 | 0f64aa5 Update the Application::asText() signature to be compatible with symfony:console 2.1 297 | 26/09/2013 23:07 0.9.8 Various fix, including the PR from @bonndan 298 | a05865d Execute the current test on no-ansi mode, to avoid errors on some environments 299 | cd778fc Revert "changed current command test to expected a formatted string on the console" 300 | c0adab0 Merge remote-tracking branch 'bonndan/master' 301 | 479daea RMT command clean-up 302 | 393d32b correct brace position and indentation 303 | 5dbc1d4 prepare RMT for jenkins 304 | 0e6e279 #30 maintains compatibility to symfony/console 2.0* 305 | 37e1ce4 exception thrown in working copy check has code greather than zero, causing exit code to be non-zero as well 306 | b2d1c87 fixed typo 307 | 142d602 separated working copy checkout tests 308 | 952d360 testWorkingCopyCheck sends the console output as message to phpunit 309 | 5ee8164 fixes a failing test with localized git console output 310 | 9a9a88a changed current command test to expected a formatted string on the console 311 | 7a9058d Changed symfony console version 312 | 02/09/2013 11:01 0.9.7 cleanup and better VCA-Commit action 313 | 1385543 Ignore commit action if no locale modifications are found 314 | e64812d use indentation size as JSON format parameter, default to a size 4 315 | 5967701 Update installation steps and correct typos 316 | 9dabc50 Merge pull request #33 from gildegoma/travis-patches 317 | 07969aa Travis CI: Add php 5.5 and use more defaults 318 | be4cb0f Merge pull request #34 from gildegoma/support-git-1.8.x 319 | 66dbf4b Handle output of `git branch` with git v1.8+ 320 | a956b2d Merge pull request #32 from cordoval/patch-1 321 | 9ca003b standards 322 | eb5a9c6 Update README.md 323 | 17/05/2013 23:32 0.9.6 Lot of small fixes 324 | 7159a29 Review the application output fix #23 325 | 3e56e54 Amelioration of the Changelog formatter fix #27 326 | 3654a60 Add a docbloc related to the github issue #29 327 | c8d2a0b Add a new option tag-pattern for a tag persister 328 | 64c3f82 Merge pull request #28 from liip/minor_tweaks 329 | 802854a min requirement is php 5.3.3, some ws/typo fixes 330 | d5c7f7c fix: set up default git username and email for travis tests 331 | e466114 Add a getModifiedFilesSince to VCS 332 | 13/02/2013 21:58 0.9.5 Mercurial support (thanks to krtek4) 333 | d46189c Small formatting fix in the README.md 334 | 652dc65 Merge pull request #26 from liip/mercurial-support 335 | 3352378 Update tests so we give some default username for systems without global hgrc 336 | 300f4dd Add HG to the init options 337 | 04a2287 Add mercurial support 338 | 07/02/2013 07:07 0.9.4 Various bug fixes: #11, #12 and #24 + Functionnal test enhancement 339 | cdf11f9 Use the init command to setup functionnal tests 340 | b82bed0 Generate a relative path in the init command fix #24 341 | 2c1c27d Update composer command 342 | 3f5474d Add possibility to configure the action CHANGELOG update 343 | d939469 Fixing DisplayLastChanges when no release yet fix #11 344 | d73ef3b Indent the rmt.json file when running the init function fix #13 345 | 14/12/2012 07:57 0.9.3 Rename all from RD to RMT + Various bugs fixes 346 | 34bde25 Master renaming from RD to RMT fix #21 347 | 1e7456f Fix the init command: fix #10 348 | 7f623c0 first cleaning round 349 | 3283c31 anchor to config 350 | 1c89b38 update readme 351 | 6abb145 New option --vcs-tag on the command RD current 352 | d938629 Test output cleanup 353 | 01/12/2012 22:45 0.9.2 Refactoring of the Context and bug fixes 354 | 95fefbf New shortcut methods Context::get() and Context::getParam() 355 | 895ea0c Fix for the sorting issue fix #18 356 | c7f180d Replace the context class by a singleton 357 | ffe58a6 Allow to dump commit message in the changelog 358 | ada96f3 Add new tests for command RMT init and RMT current ref #10 359 | 2eb6fae Documentation review 360 | 25/11/2012 16:31 0.9.1 Setup for composer publication 361 | 08/11/2012 23:59 0.9.0 First beta release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Liip AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RMT - Release Management Tool 2 | ============================= 3 | 4 | [![Build Status](https://github.com/liip/rmt/actions/workflows/ci.yml/badge.svg)](https://github.com/liip/rmt/actions/workflows/ci.yml) 5 | [![Latest Stable Version](https://poser.pugx.org/liip/RMT/version.png)](https://packagist.org/packages/liip/RMT) 6 | [![Total Downloads](https://poser.pugx.org/liip/RMT/d/total.png)](https://packagist.org/packages/liip/RMT) 7 | [![License](https://poser.pugx.org/liip/rmt/license.svg)](https://packagist.org/packages/liip/rmt) 8 | 9 | RMT is a handy tool to help releasing new versions of your software. You can define the type 10 | of version generator you want to use (e.g. semantic versioning), where you want to store 11 | the version (e.g. in a changelog file or as a VCS tag) and a list of actions that should be 12 | executed before or after the release of a new version. 13 | 14 | Installation 15 | ------------ 16 | ### Option 1: As a development dependency of your project 17 | In order to use RMT in your project you should use [Composer](http://getcomposer.org/) to install it 18 | as a dev-dependency. Just go to your project's root directory and execute: 19 | 20 | composer require --dev liip/rmt 21 | 22 | Then you must initialize RMT by running the following command: 23 | 24 | php vendor/liip/rmt/command.php init 25 | 26 | This command will create a `.rmt.yml` config file and a `RMT` executable script in your project's 27 | root folder. You can now start using RMT by executing: 28 | 29 | ./RMT 30 | 31 | Once there, your best option is to pick one of the [configuration examples](#configuration-examples) below 32 | and adapt it to your needs. 33 | 34 | If you are using a versioning tool, we recommend to add both Composer files (`composer.json` 35 | and `composer.lock`), the RMT configuration file(`.rmt.yml`) and the `RMT` executable script 36 | to it. The `vendor` directory should be ignored as it is populated when running 37 | `composer install`. 38 | 39 | ### Option 2: As a global Composer installation 40 | 41 | You can add RMT to your global composer.json and have it available globally for all your projects. Therefor 42 | just run the following command: 43 | 44 | composer global require liip/rmt 45 | 46 | Make sure you have `~/.composer/vendor/bin/` in your $PATH. 47 | 48 | ### Option 3: As a Phar file 49 | RMT can be installed through [phar-composer](https://github.com/clue/phar-composer/), which needs to be [installed](https://github.com/clue/phar-composer/#install) therefor. This useful tool allows you to create runnable Phar files from Composer packages. 50 | 51 | If you have phar-composer installed, you can run: 52 | 53 | sudo phar-composer install liip/RMT 54 | 55 | and have phar-composer build and install the Phar file to your $PATH, which then allows you to run it simply as `rmt` from the command line or you can run 56 | 57 | phar-composer build liip/RMT 58 | 59 | and copy the resulting Phar file manually to where you need it (either make the Phar file executable via `chmod +x rmt.phar` and execute it 60 | directly `./rmt.phar` or run it by invoking it through PHP via `php rmt.phar`. 61 | 62 | For the usage substitute RMT with whatever variant you have decided to use. 63 | 64 | ### Option 4: As Drifter role 65 | If your are using https://github.com/liip/drifter for your project, you just need three step 66 | * Activate the `rmt` role 67 | * Re-run the provisionning `vagrant provision` 68 | * Init RMT for your project `php /home/vagrant/.config/composer/vendor/liip/rmt/RMT` 69 | 70 | Usage 71 | ----- 72 | Using RMT is very straightforward, just run the command: 73 | 74 | ./RMT release 75 | 76 | RMT will then execute the following tasks: 77 | 78 | 1. Execute the prerequisites checks 79 | 2. Ask the user to answers potentials questions 80 | 3. Execute the pre-release actions 81 | 4. Release 82 | * Generate a new version number 83 | * Persist the new version number 84 | 5. Execute the post-release actions 85 | 86 | Here is an example output: 87 | 88 | ![screenshot](https://github.com/liip/RMT/raw/master/docs/output-example.png "First stable for RMT") 89 | 90 | 91 | ### Additional commands 92 | 93 | The `release` command provides the main behavior of the tool, additional some extra commands are available: 94 | 95 | * `current` will show your project current version number (alias version) 96 | * `changes` display the changes that will be incorporated in the next release 97 | * `config` display the current config (already merged) 98 | * `init` create (or reset) the .rmt.yml config file 99 | 100 | 101 | Configuration 102 | ------------- 103 | 104 | All RMT configurations have to be done in `.rmt.yml`. The file is divided in six root elements: 105 | 106 | * `vcs`: The type of VCS you are using, can be `git`, `svn` or `none` 107 | * For `git` VCS you can use the two following options `sign-tag` and `sign-commit` if you want to GPG sign your release 108 | * `prerequisites`: A list `[]` of prerequisites that must be matched before starting the release process 109 | * `pre-release-actions`: A list `[]` of actions that will be executed before the release process 110 | * `version-generator`: The generator to use to create a new version (mandatory) 111 | * `version-persister`: The persister to use to store the versions (mandatory) 112 | * `post-release-actions`: A list `[]` of actions that will be executed after the release 113 | 114 | All entries of this config work the same. You have to specify the class you want to handle the action. Example: 115 | 116 | version-generator: "simple"` 117 | version-persister: 118 | vcs-tag: 119 | tag-prefix: "v_" 120 | 121 | RMT also support JSON configs, but we recommend using YAML. 122 | 123 | ### Branch specific config 124 | 125 | Sometimes you want to use a different release strategy according to the VCS branch, e.g. you want to add CHANGELOG entries only in the `master` branch. To do so, you have to place your default config into a root element named `_default`, then you can override parts of this default config for the 126 | branch `master`. Example: 127 | 128 | _default: 129 | version-generator: "simple" 130 | version-persister: "vcs-tag" 131 | master: 132 | pre-release-actions: [changelog-update] 133 | 134 | You can use the command `RMT config` to see the merge result between _default and your current branch. 135 | 136 | ### Version generator 137 | 138 | Build-in version number generation strategies. 139 | 140 | * simple: This generator is doing a simple increment (1,2,3...) 141 | * semantic: A generator which implements [Semantic versioning](http://semver.org) 142 | * Option `allow-label` (boolean): To allow adding a label on a version (such as -beta, -rcXX) (default: *false*) 143 | * Option `type`: to force the version type 144 | * Option `label`: to force the label 145 | 146 | The two forced option could be very useful if you decide that a given branch is dedicated to the next beta of a 147 | given version. So just force the label to beta and all release are going to be beta increments. 148 | 149 | ### Version persister 150 | 151 | Class in charge of saving/retrieving the version number. 152 | 153 | * vcs-tag: Save the version as a VCS tag 154 | * Option `tag-pattern`: Allow to provide a regex that all tag must match. This allow for example to release a version 1.X.X in a specific branch and to release a 2.X.X in a separate branch 155 | * Option `tag-prefix`: Allow to prefix all VCS tag with a string. You can have a numeric versionning but generation tags such as `v_2.3.4`. As a bonus you can use a specific placeholder: `{branch-name}` that will automatically inject the current branch name in the tag. So use, simple generation and `tag-prefix: "{branch-name}_"` and it will generate tag like `featureXY_1`, `featureXY_2`, etc... 156 | 157 | * changelog: Save the version in the changelog file 158 | * Option `location`: Changelog file name an location (default: *CHANGELOG*) 159 | 160 | ### Prerequisite actions 161 | 162 | Prerequisite actions are executed before the interactive part. 163 | 164 | * `working-copy-check`: check that you don't have any VCS local changes 165 | * Option `allow-ignore`: allow the user to skip the check when doing a release with `--ignore-check` 166 | * `display-last-changes`: display your last changes 167 | * `tests-check`: run the project test suite 168 | * Option `command`: command to run (default: *phpunit*) 169 | * Option `timeout`: the number of seconds after which the command times out (default: *60.0*) 170 | * Option `expected_exit_code`: expected return code (default: *0*) 171 | * `composer-json-check`: run a validate on the composer.json 172 | * Option `composer`: how to run composer (default: *php composer.phar*) 173 | * `composer-stability-check`: will check if the composer.json is set to the right minimum-stability 174 | * Option `stability`: the stability that should be set in the minimum-stability field (default: *stable*) 175 | * `composer-security-check`: run the composer.lock against https://github.com/fabpot/local-php-security-checker to check for known vulnerabilities in the dependencies. ⚠️ The local-php-security-checker binary must be installed globally. 176 | * `composer-dependency-stability-check`: test if only allowed dependencies are using development versions 177 | * Option `ignore-require` and `ignore-require-dev`: don't check dependencies in `require` or `require-dev` section 178 | * Option `whitelist`: allow specific dependencies to use development version 179 | * `command`: Execute a system command 180 | * Option `cmd` The command to execute 181 | * Option `live_output` boolean, do we display the command output? (default: *true*) 182 | * Option `timeout` integer, limits the time for the command. (default: *600*) 183 | * Option `stop_on_error` boolean, do we break the release process on error? (default: *true*) 184 | 185 | ### Actions 186 | 187 | Actions can be used for pre or post release parts. 188 | 189 | * `changelog-update`: Update a changelog file. This action is further configured 190 | to use a specific formatter. 191 | * Option `format`: *simple*, *semantic*, *markdown* or *addTop* (default: *simple*) 192 | * Option `file`: path from .rmt.yml to changelog file (default: *CHANGELOG*) 193 | * Option `dump-commits`: write all commit messages since the last release into the 194 | changelog file (default: *false*) 195 | * Option `insert-at`: only for addTop formatter: Number of lines to skip from the 196 | top of the changelog file before adding the release number (default: *0*) 197 | * Option `exclude-merge-commits`: Exclude merge commits from the changelog (default: *false*) 198 | * `vcs-commit`: commit all files of the working copy (only use it with the 199 | `working-copy-check` prerequisite) 200 | * Option `commit-message`: specify a custom commit message. %version% will be replaced by the current / next version strings. 201 | * `vcs-tag`: Tag the last commit 202 | * `vcs-publish`: Publish the changes (commits and tags) 203 | * `composer-update`: Update the version number in a composer file (note that when using packagist.org, it is recommended to not have a tag in composer.json as the version is handle by version control tags) 204 | * `files-update`: Update the version in one or multiple files. For each file to update, please provide an array with 205 | * Option `file`: path to the file to update 206 | * Option `pattern`: optional, use to specify the string replacement pattern in your file. For example: 207 | `const VERSION = '%version%';` 208 | * `build-phar-package`: Builds a Phar package of the current project whose filename depends on the 'package-name' option and the deployed version: [package-name]-[version].phar 209 | * Option `package-name`: the name of the generate package 210 | * Option `destination`: the destination directory to build the package into. If prefixed with a slash, is considered absolute, otherwise relative to the project root. 211 | * Option `excluded-paths`: a regex of excluded paths, directly passed to the [Phar::buildFromDirectory](http://php.net/manual/en/phar.buildfromdirectory.php) method. Ex: `/^(?!.*cookbooks|.*\.vagrant|.*\.idea).*$/im` 212 | * Option `metadata`: an array of metadata describing the package. Ex author, project. Note: the release version is added by default but can be overridden here. 213 | * Option `default-stub-cli`: the default stub for CLI usage of the package. 214 | * Option `default-stub-web`: the default stub for web application usage of the package. 215 | * `command`: Execute a system command 216 | * Option `cmd` The command to execute 217 | * Option `live_output` boolean, do we display the command output? (default: *true*) 218 | * Option `timeout` integer, limits the time for the command. (default: *600*) 219 | * Option `stop_on_error` boolean, do we break the release process on error? (default: *true*) 220 | * `update-version-class`: Update the version constant in a class file. DEPRECATED, use `files-update` instead 221 | * Option `class`: path to class to be updated, or fully qualified class name of the class containing the version constant 222 | * Option `pattern`: optional, use to specify the string replacement pattern in your 223 | version class. %version% will be replaced by the current / next version strings. 224 | For example you could use `const VERSION = '%version%';`. If you do not specify 225 | this option, every occurrence of the version string in the file will be replaced. 226 | 227 | 228 | Extend it 229 | --------- 230 | 231 | RMT is providing some existing actions, generators, and persisters. If needed you can add your own by creating a PHP script in your project, and referencing it in the configuration via it's relative path: 232 | 233 | version-generator: "bin/myOwnGenerator.php" 234 | 235 | Example with injected parameters: 236 | 237 | version-persister: 238 | name: "bin/myOwnGenerator.php" 239 | parameter1: value1 240 | 241 | As an example, you can look at the script [/bin/UpdateApplicationVersionCurrentVersion.php](https://github.com/liip/RMT/blob/master/bin/UpdateApplicationVersionCurrentVersion.php) configured [here](https://github.com/liip/RMT/blob/master/.rmt.yml#L9). 242 | 243 | *WARNING:* As the key `name` is used to define the name of the object, you cannot have a parameter named `name`. 244 | 245 | 246 | Configuration examples 247 | ---------------------- 248 | Most of the time, it will be easier for you to pick up an example below and adapt it to your needs. 249 | 250 | ### No VCS, changelog updater only 251 | 252 | version-generator: semantic 253 | version-persister: changelog 254 | 255 | ### Using Git tags, simple versioning and prerequisites 256 | 257 | vcs: git 258 | version-generator: simple 259 | version-persister: vcs-tag 260 | prerequisites: [working-copy-check, display-last-changes] 261 | 262 | ### Using Git tags, simple versioning and composer-prerequisites 263 | 264 | vcs: git 265 | version-generator: simple 266 | version-persister: vcs-tag 267 | prerequisites: 268 | - composer-json-check 269 | - composer-stability-check: 270 | stability: beta 271 | - composer-dependency-stability-check: 272 | whitelist: 273 | - [symfony/console] 274 | - [phpunit/phpunit, require-dev] 275 | 276 | ### Using Git tags, simple versioning and prerequisites, and gpg sign commit and tags 277 | 278 | vcs: 279 | name: git 280 | sign-tag: true 281 | sign-commit: true 282 | version-generator: simple 283 | version-persister: vcs-tag 284 | prerequisites: [working-copy-check, display-last-changes] 285 | 286 | 287 | ### Using Git tags with prefix, semantic versioning, updating two files and pushing automatically 288 | 289 | vcs: git 290 | version-generator: semantic 291 | version-persister: 292 | name: vcs-tag 293 | tag-prefix : "v_" 294 | pre-release-actions: 295 | files-update: 296 | - [config.yml] 297 | - [app.ini, 'dynamic-version: %version%'] 298 | post-release-actions: [vcs-publish] 299 | 300 | ### Using semantic versioning on master and simple versioning on topic branches, markdown formatting for changelog 301 | 302 | _default: 303 | vcs: git 304 | prerequisites: [working-copy-check] 305 | version-generator: simple 306 | version-persister: 307 | name: vcs-tag 308 | tag-prefix: "{branch-name}_" 309 | post-release-actions: [vcs-publish] 310 | 311 | # This entry allow to override some parameters for the master branch 312 | master: 313 | prerequisites: [working-copy-check, display-last-changes] 314 | pre-release-actions: 315 | changelog-update: 316 | format: markdown 317 | file: CHANGELOG.md 318 | dump-commits: true 319 | update-version-class: 320 | class: Doctrine\ODM\PHPCR\Version 321 | pattern: const VERSION = '%version%'; 322 | vcs-commit: ~ 323 | version-generator: semantic 324 | version-persister: vcs-tag 325 | 326 | Contributing 327 | ------------ 328 | If you would like to help, by submitting one of your action scripts, generators, or persisters. Or just by reporting a 329 | bug just go to the project page [https://github.com/liip/RMT](https://github.com/liip/RMT). 330 | 331 | If you provide a PR, try to associate it some unit or functional tests. See next section 332 | 333 | Tests 334 | ----- 335 | 336 | ### Requirements 337 | 338 | To be able to run the tests locally, you need: 339 | * phpunit 340 | * git 341 | * mercurial 342 | 343 | You can install all of them with Brew: 344 | 345 | > brew install phpunit git hg 346 | 347 | The tests are also testing the creation of the RMT phar. So you 348 | have to allow this in your php.ini, by uncommenting this line: 349 | 350 | phar.readonly = Off 351 | 352 | Finally, to run the tests, just launch PHPUnit 353 | 354 | > phpunit 355 | 356 | ### Functional tests 357 | 358 | The functional tests are fully functional temporary RMT setup. Each time you run functional test, it creates a temporary 359 | folder with a RMT project. Then the test suite is running RMT commands on it, and check the results. That's why you need 360 | to have Git and Mercurial installed. 361 | 362 | #### Debug 363 | 364 | To debug RMT functional tests, the best is to go into this temporary folder and manually explore the project. To do so, 365 | just add a small `$this->manualDebug();` into the test suite. This will break the test with the following output: 366 | 367 | MANUAL DEBUG Go to: 368 | > cd /private/var/folders/hl/gnj5dcj55gbc93pcgrjxbb0w0000gn/T/ceN2Mf 369 | 370 | Then you just have to go into the mentioned folder and start debugging 371 | 372 | Authors 373 | ------- 374 | 375 | * Jonathan Macheret, Liip SA 376 | * David Jeanmonod Liip SA 377 | * and [others contributors](https://github.com/liip/RMT/graphs/contributors) 378 | 379 | License 380 | ------- 381 | 382 | RMT is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 383 | -------------------------------------------------------------------------------- /RMT: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add('Liip\RMT\Tests', __DIR__ . '/test'); 22 | $loader->add('Liip', __DIR__ . '/src'); 23 | } elseif (file_exists($file = __DIR__ . '/vendor/autoload.php')) { 24 | 25 | // Composer when on RMT standalone install (used in travis.ci) 26 | $loader = require $file; 27 | $loader->add('Liip\RMT\Tests', __DIR__.'/test'); 28 | $loader->add('Liip', __DIR__.'/src'); 29 | } elseif (file_exists($file = __DIR__ . '/../symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php')) { 30 | 31 | // Symfony 2.0 32 | require_once $file; 33 | $loader = new \Symfony\Component\ClassLoader\UniversalClassLoader(); 34 | $loader->registerNamespaces(array( 35 | 'Liip' => array(__DIR__ . '/src', __DIR__ . '/test'), 36 | 'Symfony' => __DIR__ . '/../symfony/src', 37 | )); 38 | $loader->register(); 39 | } else { 40 | throw new \Exception("Unable to find the an autoloader"); 41 | } 42 | -------------------------------------------------------------------------------- /command.php: -------------------------------------------------------------------------------- 1 | run(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liip/rmt", 3 | "description": "Release Management Tool: a handy tool to help releasing new version of your software", 4 | "keywords": ["release", "version", "semantic versioning", "vcs tag", "pre-release", "post-release"], 5 | "homepage": "http://github.com/liip/RMT", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Jonathan Macheret", 10 | "email": "jonathan.macheret@liip.ch", 11 | "role": "Developer" 12 | }, 13 | { 14 | "name": "David Jeanmonod", 15 | "email": "david.jeanmonod@liip.ch", 16 | "role": "Developer" 17 | } 18 | ], 19 | "support": { 20 | "issues": "http://github.com/liip/RMT/issues" 21 | }, 22 | "require": { 23 | "ext-json": "*", 24 | "php": "^7.1|^8.0", 25 | "symfony/console": "^3.4|^4.0|^5.0|^6.0|^7.0", 26 | "symfony/yaml": "^3.4|^4.0|^5.0|^6.0|^7.0", 27 | "symfony/process": "^3.4|^4.0|^5.0|^6.0|^7.0", 28 | "composer/semver": "^3.4" 29 | }, 30 | "autoload": { 31 | "psr-0": { 32 | "Liip": "src" 33 | } 34 | }, 35 | "bin": ["RMT"], 36 | "config" : { 37 | "bin-dir" : "bin", 38 | "allow-plugins": { 39 | "symfony/flex": true 40 | } 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "1.3-dev" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/BaseAction.php: -------------------------------------------------------------------------------- 1 | options = $options; 24 | } 25 | 26 | /** 27 | * Main part of the action 28 | */ 29 | abstract public function execute(); 30 | 31 | /** 32 | * Return the name of the action as it will be display to the user 33 | * 34 | * @return string 35 | */ 36 | public function getTitle() 37 | { 38 | $nsAndclass = explode('\\', get_class($this)); 39 | 40 | return preg_replace('/(?!^)[[:upper:]][[:lower:]]/', ' $0', preg_replace('/(?!^)[[:upper:]]+/', '$0', end($nsAndclass))); 41 | } 42 | 43 | /** 44 | * Return an array of options that can be 45 | * * Liip\RMT\Option\Option A new option specific to this prerequiste 46 | * * string The name of a standarmt option (comment, type, author...) 47 | * 48 | * @return array 49 | */ 50 | public function getInformationRequests() 51 | { 52 | return array(); 53 | } 54 | 55 | /** 56 | * A common method to confirm success to the user 57 | */ 58 | public function confirmSuccess() 59 | { 60 | Context::get('output')->writeln('OK'); 61 | } 62 | 63 | /** 64 | * Execute a command and render the output through the classical indented output 65 | * @param string $cmd 66 | * @param float|null $timeout 67 | * @return Process 68 | */ 69 | public function executeCommandInProcess($cmd, $timeout = null) 70 | { 71 | Context::get('output')->write("$cmd\n\n"); 72 | $process = method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline($cmd) : new Process($cmd); 73 | 74 | if ($timeout !== null) { 75 | $process->setTimeout($timeout); 76 | } 77 | 78 | $process->run(function ($type, $buffer) { 79 | Context::get('output')->write($buffer); 80 | }); 81 | return $process; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/BuildPharPackageAction.php: -------------------------------------------------------------------------------- 1 | options = array_merge(array( 28 | 'package-name' => 'rmt-package', 29 | 'destination' => '/tmp/', 30 | 'excluded-paths' => '', 31 | 'metadata' => array(), 32 | 'default-stub-cli' => '', 33 | 'default-stub-web' => '', 34 | ), $options); 35 | } 36 | 37 | public function execute() 38 | { 39 | $packagePath = $this->create(); 40 | 41 | $this->confirmSuccess(); 42 | 43 | Context::get('output')->writeln('The package has been successfully created in: ' . $packagePath); 44 | } 45 | 46 | /** 47 | * Handles the creation of the package. 48 | */ 49 | protected function create() 50 | { 51 | $this->setReleaseVersion(); 52 | 53 | $output = $this->getDestination() . '/' . $this->getFilename(); 54 | 55 | $phar = new Phar($output, FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME); 56 | $phar->buildFromDirectory(Context::getParam('project-root'), $this->options['excluded-paths']); 57 | $phar->setMetadata(array_merge(array('version' => $this->releaseVersion), $this->options['metadata'])); 58 | $phar->setDefaultStub($this->options['default-stub-cli'], $this->options['default-stub-web']); 59 | 60 | return $output; 61 | } 62 | 63 | /** 64 | * Determines the package filename based on the next version and the 'package-name' option. 65 | * 66 | * @return string 67 | */ 68 | protected function getFilename() 69 | { 70 | return $this->options['package-name'] . '-' . $this->releaseVersion . '.phar'; 71 | } 72 | 73 | /** 74 | * Checks if the path is relative. 75 | * 76 | * @param $path string The path to check 77 | * 78 | * @return bool 79 | */ 80 | protected function isRelativePath($path) 81 | { 82 | return strpos($path, '/') !== 0; 83 | } 84 | 85 | /** 86 | * Get the destination directory to build the package into. 87 | * 88 | * @return string The destination 89 | */ 90 | protected function getDestination() 91 | { 92 | $destination = $this->options['destination']; 93 | 94 | if ($this->isRelativePath($destination)) { 95 | return Context::getParam('project-root') . '/' . $destination; 96 | } 97 | 98 | return $destination; 99 | } 100 | 101 | /** 102 | * Determine and set the next release version. 103 | */ 104 | protected function setReleaseVersion() 105 | { 106 | try { 107 | $currentVersion = Context::get('version-persister')->getCurrentVersion(); 108 | } catch (\Exception $e) { 109 | $currentVersion = Context::get('version-generator')->getInitialVersion(); 110 | } 111 | 112 | $this->releaseVersion = Context::get('version-generator')->generateNextVersion($currentVersion); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/ChangelogUpdateAction.php: -------------------------------------------------------------------------------- 1 | options = array_merge(array( 26 | 'dump-commits' => false, 27 | 'exclude-merge-commits' => false, 28 | 'format' => 'simple', 29 | 'file' => 'CHANGELOG', 30 | ), $options); 31 | } 32 | 33 | public function execute() 34 | { 35 | // Handle the commits dump 36 | if ($this->options['dump-commits'] == true) { 37 | try { 38 | $extraLines = Context::get('vcs')->getAllModificationsSince( 39 | Context::get('version-persister')->getCurrentVersionTag(), 40 | false, 41 | $this->options['exclude-merge-commits'] 42 | ); 43 | $this->options['extra-lines'] = $extraLines; 44 | } catch (NoReleaseFoundException $e) { 45 | Context::get('output')->writeln('No commits dumped as this is the first release'); 46 | } 47 | unset($this->options['dump-commits']); 48 | } 49 | 50 | $manager = new ChangelogManager($this->options['file'], $this->options['format']); 51 | $manager->update( 52 | Context::getParam('new-version'), 53 | Context::get('information-collector')->getValueFor('comment'), 54 | array_merge( 55 | array('type' => Context::get('information-collector')->getValueFor('type', null)), 56 | $this->options 57 | ) 58 | ); 59 | $this->confirmSuccess(); 60 | } 61 | 62 | public function getInformationRequests() 63 | { 64 | return array('comment'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/CommandAction.php: -------------------------------------------------------------------------------- 1 | options = array_merge(array( 25 | 'cmd' => null, 26 | 'live_output' => true, 27 | 'stop_on_error' => true, 28 | 'timeout' => 600, 29 | ), $options); 30 | 31 | if ($this->options['cmd'] == null) { 32 | throw new \RuntimeException('Missing [cmd] option'); 33 | } 34 | } 35 | 36 | public function execute() 37 | { 38 | $command = $this->options['cmd']; 39 | Context::get('output')->write("$command\n\n"); 40 | 41 | // Prepare a callback for live output 42 | $callback = null; 43 | if ($this->options['live_output']) { 44 | $callback = function ($type, $buffer) { 45 | $decorator = array('',''); 46 | if ($type == Process::ERR) { 47 | $decorator = array('',''); 48 | } 49 | Context::get('output')->write($decorator[0] . $buffer.$decorator[1]); 50 | }; 51 | } 52 | 53 | // Run the process 54 | $process = method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline($command) : new Process($command); 55 | 56 | if (null !== $timeout = $this->options['timeout']) { 57 | $process->setTimeout($timeout); 58 | } 59 | 60 | $process->run($callback); 61 | 62 | // Break up if the result is not good 63 | if ($this->options['stop_on_error'] && $process->getExitCode() !== 0) { 64 | throw new \RuntimeException("Command [$command] exit with code " . $process->getExitCode()); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/ComposerUpdateAction.php: -------------------------------------------------------------------------------- 1 | confirmSuccess(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/FilesUpdateAction.php: -------------------------------------------------------------------------------- 1 | options) === 0) { 27 | throw new ConfigException('You must specify at least one file'); 28 | } 29 | 30 | foreach ($this->options as $option) { 31 | $file = $option[0]; 32 | $pattern = $option[1] ?? null; 33 | 34 | if (! file_exists($file)) { 35 | $versionClass = new ReflectionClass($file); 36 | $file = $versionClass->getFileName(); 37 | } 38 | 39 | if (! @file_get_contents($file)) { 40 | throw new ConfigException("Could not get the content of $file"); 41 | } 42 | 43 | $this->updateFile($file, $pattern); 44 | } 45 | 46 | $this->confirmSuccess(); 47 | } 48 | 49 | /** 50 | * will update a given filename with the current version 51 | * 52 | * @param string $filename 53 | * @param null $pattern 54 | * @throws Exception 55 | */ 56 | protected function updateFile($filename, $pattern = null) 57 | { 58 | $current = Context::getParam('current-version'); 59 | $next = Context::getParam('new-version'); 60 | 61 | $content = file_get_contents($filename); 62 | if (false === strpos($content, $current)) { 63 | throw new Exception("The version file $filename does not contain the current version $current"); 64 | } 65 | if ($pattern) { 66 | $current = str_replace('%version%', $current, $pattern); 67 | $next = str_replace('%version%', $next, $pattern); 68 | } 69 | 70 | $content = str_replace($current, $next, $content); 71 | 72 | if (false === strpos($content, (string)$next)) { 73 | throw new Exception("The version file $filename could not be updated with version $next"); 74 | } 75 | file_put_contents($filename, $content); 76 | } 77 | } -------------------------------------------------------------------------------- /src/Liip/RMT/Action/UpdateVersionClassAction.php: -------------------------------------------------------------------------------- 1 | 32 | * @deprecated Please use FileUpdateAction instead 33 | */ 34 | class UpdateVersionClassAction extends BaseAction 35 | { 36 | public function __construct($options) 37 | { 38 | parent::__construct($options); 39 | } 40 | 41 | public function execute() 42 | { 43 | if (!isset($this->options['class'])) { 44 | throw new ConfigException('You must specify the class or file to update'); 45 | } 46 | 47 | if (file_exists($this->options['class'])) { 48 | $filename = $this->options['class']; 49 | } else { 50 | $versionClass = new \ReflectionClass($this->options['class']); 51 | $filename = $versionClass->getFileName(); 52 | } 53 | 54 | $this->updateFile($filename); 55 | $this->confirmSuccess(); 56 | } 57 | 58 | /** 59 | * will update a given filename with the current version 60 | * 61 | * @param string $filename 62 | * 63 | * @throws \Liip\RMT\Exception 64 | */ 65 | protected function updateFile($filename) 66 | { 67 | $current = Context::getParam('current-version'); 68 | $next = Context::getParam('new-version'); 69 | 70 | $content = file_get_contents($filename); 71 | if (false === strpos($content, $current)) { 72 | throw new Exception('The version class ' . $filename . " does not contain the current version $current"); 73 | } 74 | if (isset($this->options['pattern'])) { 75 | $current = str_replace('%version%', $current, $this->options['pattern']); 76 | $next = str_replace('%version%', $next, $this->options['pattern']); 77 | } 78 | $content = str_replace($current, $next, $content); 79 | if (false === strpos($content, $next)) { 80 | throw new Exception('The version class ' . $filename . " could not be updated with version $next"); 81 | } 82 | file_put_contents($filename, $content); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/VcsCommitAction.php: -------------------------------------------------------------------------------- 1 | options = array_merge( 25 | array( 26 | 'commit-message' => 'Release of new version %version%', 27 | ), 28 | $options 29 | ); 30 | } 31 | 32 | public function execute() 33 | { 34 | /** @var VCSInterface $vcs */ 35 | $vcs = Context::get('vcs'); 36 | if (count($vcs->getLocalModifications()) == 0) { 37 | Context::get('output')->writeln('No modification found, aborting commit'); 38 | 39 | return; 40 | } 41 | $vcs->saveWorkingCopy( 42 | str_replace('%version%', Context::getParam('new-version'), $this->options['commit-message']) 43 | ); 44 | $this->confirmSuccess(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/VcsPublishAction.php: -------------------------------------------------------------------------------- 1 | true, 28 | 'remote-name' => null, 29 | 'ask-remote-name' => false, 30 | ), $options)); 31 | } 32 | 33 | public function execute() 34 | { 35 | if ($this->options['ask-confirmation']) { 36 | 37 | // Ask the question if there is no confirmation yet 38 | $ic = Context::get('information-collector'); 39 | if (!$ic->hasValueFor(self::AUTO_PUBLISH_OPTION)) { 40 | $answer = Context::get('output')->askConfirmation('Do you want to publish your release (default: y): ', Context::get('input')); 41 | $ic->setValueFor(self::AUTO_PUBLISH_OPTION, $answer == true ? 'y' : 'n'); 42 | } 43 | 44 | // Skip if the user didn't ask for publishing 45 | if ($ic->getValueFor(self::AUTO_PUBLISH_OPTION) !== 'y') { 46 | Context::get('output')->writeln('requested to be ignored'); 47 | 48 | return; 49 | } 50 | } 51 | 52 | Context::get('vcs')->publishChanges($this->getRemote()); 53 | Context::get('vcs')->publishTag( 54 | Context::get('version-persister')->getTagFromVersion( 55 | Context::getParam('new-version') 56 | ), 57 | $this->getRemote() 58 | ); 59 | 60 | $this->confirmSuccess(); 61 | } 62 | 63 | public function getInformationRequests() 64 | { 65 | $requests = array(); 66 | if ($this->options['ask-confirmation']) { 67 | $requests[] = new InformationRequest(self::AUTO_PUBLISH_OPTION, array( 68 | 'description' => 'Changes will be published automatically', 69 | 'type' => 'yes-no', 70 | 'interactive' => false, 71 | )); 72 | } 73 | if ($this->options['ask-remote-name']) { 74 | $requests[] = new InformationRequest('remote', array( 75 | 'description' => 'Remote to push changes', 76 | 'type' => 'text', 77 | 'default' => 'origin', 78 | )); 79 | } 80 | 81 | return $requests; 82 | } 83 | 84 | /** 85 | * Return the remote name where to publish or null if not defined 86 | * 87 | * @return string|null 88 | */ 89 | protected function getRemote(): ?string 90 | { 91 | if ($this->options['ask-remote-name']) { 92 | return Context::get('information-collector')->getValueFor('remote'); 93 | } 94 | if ($this->options['remote-name'] !== null) { 95 | return $this->options['remote-name']; 96 | } 97 | 98 | return null; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Liip/RMT/Action/VcsTagAction.php: -------------------------------------------------------------------------------- 1 | createTag( 24 | Context::get('vcs')->getTagFromVersion( 25 | Context::getParam('new-version') 26 | ) 27 | ); 28 | $this->confirmSuccess(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Liip/RMT/Application.php: -------------------------------------------------------------------------------- 1 | myproject/RMT release 45 | chdir($this->getProjectRootDir()); 46 | 47 | // Add all command, in a controlled way and render exception if any 48 | try { 49 | // Add the default command 50 | $this->add(new InitCommand()); 51 | // Add command that require the config file 52 | if (file_exists($this->getConfigFilePath())) { 53 | $this->add(new ReleaseCommand()); 54 | $this->add(new CurrentCommand()); 55 | $this->add(new ChangesCommand()); 56 | $this->add(new ConfigCommand()); 57 | } 58 | } catch (\Exception $e) { 59 | $output = new Output(); 60 | $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); 61 | 62 | if (method_exists($this, 'renderThrowable')) { 63 | $this->renderThrowable($e, $output); 64 | } else { 65 | $this->renderException($e, $output); 66 | } 67 | 68 | exit(1); 69 | } 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function run(InputInterface $input = null, OutputInterface $output = null): int 76 | { 77 | return parent::run($input, new Output()); 78 | } 79 | 80 | public function getProjectRootDir() 81 | { 82 | if (defined('RMT_ROOT_DIR')) { 83 | return RMT_ROOT_DIR; 84 | } 85 | 86 | return getcwd(); 87 | } 88 | 89 | public function getConfigFilePath() 90 | { 91 | $validConfigFileName = array('.rmt.yml', '.rmt.json', 'rmt.yml', 'rmt.json'); 92 | foreach ($validConfigFileName as $filename) { 93 | if (file_exists($path = $this->getProjectRootDir().DIRECTORY_SEPARATOR.$filename)) { 94 | return $path; 95 | } 96 | } 97 | } 98 | 99 | public function getConfig() 100 | { 101 | $configFile = $this->getConfigFilePath(); 102 | if (!is_file($configFile)) { 103 | throw new \Exception( 104 | "Impossible to locate the config file rmt.xxx at $configFile. If it's the first time you ". 105 | 'are using this tool, you setup your project using the [RMT init] command' 106 | ); 107 | } 108 | 109 | if (pathinfo($configFile, PATHINFO_EXTENSION) == 'json') { 110 | $config = json_decode(file_get_contents($configFile), true); 111 | if (!is_array($config)) { 112 | throw new \Exception("Impossible to parse your config file ($configFile), you probably have an error in the JSON syntax"); 113 | } 114 | } else { 115 | try { 116 | $config = Yaml::parse(file_get_contents($configFile), true); 117 | } catch (\Exception $e) { 118 | throw new \Exception( 119 | "Impossible to parse your config file ($configFile), ". 120 | 'you probably have an error in the YML syntax: '.$e->getMessage() 121 | ); 122 | } 123 | } 124 | 125 | return $config; 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function asText($namespace = null, $raw = false) 132 | { 133 | $messages = array(); 134 | 135 | // Title 136 | $title = 'RMT '.$this->getLongVersion(); 137 | $messages[] = ''; 138 | $messages[] = $title; 139 | $messages[] = str_pad('', 41, '-'); // strlen is not working here... 140 | $messages[] = ''; 141 | 142 | // Usage 143 | $messages[] = 'Usage:'; 144 | $messages[] = ' RMT command [arguments] [options]'; 145 | $messages[] = ''; 146 | 147 | // Commands 148 | $messages[] = 'Available commands:'; 149 | $commands = $this->all(); 150 | $width = 0; 151 | foreach ($commands as $command) { 152 | $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; 153 | } 154 | $width += 2; 155 | foreach ($commands as $name => $command) { 156 | if (in_array($name, array('list', 'help'))) { 157 | continue; 158 | } 159 | $messages[] = sprintf(" %-{$width}s %s", $name, $command->getDescription()); 160 | } 161 | $messages[] = ''; 162 | 163 | // Options 164 | $messages[] = 'Common options:'; 165 | foreach ($this->getDefinition()->getOptions() as $option) { 166 | if (in_array($option->getName(), array('help', 'ansi', 'no-ansi', 'no-interaction', 'version'))) { 167 | continue; 168 | } 169 | $messages[] = sprintf( 170 | ' %-29s %s %s', 171 | '--'.$option->getName().'', 172 | $option->getShortcut() ? '-'.$option->getShortcut().'' : ' ', 173 | $option->getDescription() 174 | ); 175 | } 176 | $messages[] = ''; 177 | 178 | // Help 179 | $messages[] = 'Help:'; 180 | $messages[] = ' To get more information about a given command, you can use the help option:'; 181 | $messages[] = sprintf(' %-26s %s %s', '--help', '-h', 'Provide help for the given command'); 182 | $messages[] = ''; 183 | 184 | return implode(PHP_EOL, $messages); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Liip/RMT/Changelog/ChangelogManager.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 33 | 34 | // Store the formatter 35 | $this->format = $format; 36 | $formatterClass = 'Liip\\RMT\\Changelog\\Formatter\\'.ucfirst($format).'ChangelogFormatter'; 37 | if (!class_exists($formatterClass)) { 38 | throw new \Exception("There is no formatter for [$format]"); 39 | } 40 | $this->formatter = new $formatterClass(); 41 | } 42 | 43 | public function update($version, $comment, $options = array()) 44 | { 45 | $lines = file($this->filePath, FILE_IGNORE_NEW_LINES); 46 | $lines = $this->formatter->updateExistingLines($lines, $version, $comment, $options); 47 | file_put_contents($this->filePath, implode("\n", $lines)); 48 | } 49 | 50 | public function getCurrentVersion() 51 | { 52 | $changelog = file_get_contents($this->filePath); 53 | $result = preg_match($this->formatter->getLastVersionRegex(), $changelog, $match); 54 | if ($result === 1) { 55 | return $match[1]; 56 | } 57 | throw new \Liip\RMT\Exception\NoReleaseFoundException( 58 | 'There is a format error in the CHANGELOG file, impossible to read the last version number' 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Liip/RMT/Changelog/Formatter/AddTopChangelogFormatter.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | class AddTopChangelogFormatter 41 | { 42 | public function updateExistingLines($lines, $version, $comment, $options) 43 | { 44 | $pos = isset($options['insert-at']) ? $options['insert-at'] : 0; 45 | 46 | if (!empty($comment)) { 47 | array_splice($lines, $pos, 0, array($comment, '')); 48 | } 49 | if (isset($options['extra-lines'])) { 50 | array_splice($lines, $pos, 0, $options['extra-lines']); 51 | } 52 | 53 | array_splice($lines, $pos, 0, array($version, str_repeat('-', strlen($version)), '')); 54 | 55 | return $lines; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Liip/RMT/Changelog/Formatter/MarkdownChangelogFormatter.php: -------------------------------------------------------------------------------- 1 | getNewLines('minor', $version, $comment) 64 | ); 65 | } elseif ($type == 'minor') { 66 | return array_merge( 67 | array( 68 | '', 69 | " * Version **$major.$minor** - $comment", 70 | ), 71 | $this->getNewLines('patch', $version, 'initial release') 72 | ); 73 | } else { //patch 74 | $date = $this->getFormattedDate(); 75 | 76 | return array( 77 | " * $date **$version** $comment", 78 | ); 79 | } 80 | } 81 | 82 | /** 83 | * Return the position where to insert new lines according to the type of insertion 84 | * 85 | * @param array $lines Existing lines 86 | * @param string $type Release type 87 | * 88 | * @return int The position where to insert 89 | * 90 | * @throws \Liip\RMT\Exception 91 | */ 92 | protected function findPositionToInsert($lines, $type) 93 | { 94 | // Major are always inserted at the top 95 | if ($type == 'major') { 96 | return 0; 97 | } 98 | 99 | // Minor must be inserted one line above the first major section 100 | if ($type == 'minor') { 101 | foreach ($lines as $pos => $line) { 102 | if (preg_match('/^##\ +/', $line)) { 103 | return $pos + 1; 104 | } 105 | } 106 | } 107 | 108 | // Patch should go directly after the first minor 109 | if ($type == 'patch') { 110 | foreach ($lines as $pos => $line) { 111 | if (preg_match('/\ \*\ Version\s\*\*\d+\.\d+\*\*\s\-/', $line)) { 112 | return $pos + 1; 113 | } 114 | } 115 | } 116 | 117 | throw new \Liip\RMT\Exception('Invalid changelog formatting'); 118 | } 119 | 120 | protected function getFormattedDate() 121 | { 122 | return date('Y-m-d H:i'); 123 | } 124 | 125 | public function getLastVersionRegex() 126 | { 127 | return '#\s+\d+/\d+/\d+\s\d+:\d+\s+([^\s]+)#'; 128 | } 129 | 130 | /** 131 | * format extra lines (such as commit details) 132 | * @param array $lines 133 | * @return array 134 | */ 135 | protected function formatExtraLines($lines) 136 | { 137 | foreach ($lines as $pos => $line) { 138 | $lines[$pos] = ' * '.$line; 139 | } 140 | return $lines; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Liip/RMT/Changelog/Formatter/SemanticChangelogFormatter.php: -------------------------------------------------------------------------------- 1 | findPositionToInsert($lines, $type), 0, $this->getNewLines($type, $version, $comment)); 63 | 64 | // Insert extra lines (like commits details) 65 | if (isset($options['extra-lines'])) { 66 | $extraLines = $this->formatExtraLines($options['extra-lines']); 67 | array_splice($lines, $this->findPositionToInsert($lines, 'patch') + 1, 0, $extraLines); 68 | } 69 | 70 | return $lines; 71 | } 72 | 73 | /** 74 | * format extra lines (such as commit details) 75 | * @param array $lines 76 | * @return array 77 | */ 78 | protected function formatExtraLines($lines) 79 | { 80 | foreach ($lines as $pos => $line) { 81 | $lines[$pos] = ' '.$line; 82 | } 83 | return $lines; 84 | } 85 | 86 | /** 87 | * Return the new formatted lines for the given variables 88 | * 89 | * @param string $type The version type, could be major, minor, patch 90 | * @param string $version The new version number 91 | * @param string $comment The user comment 92 | * 93 | * @return array An array of new lines 94 | */ 95 | protected function getNewLines($type, $version, $comment) 96 | { 97 | list($major, $minor, $patch) = explode('.', $version); 98 | if ($type == 'major') { 99 | $title = "version $major $comment"; 100 | 101 | return array_merge( 102 | array( 103 | '', 104 | strtoupper($title), 105 | str_pad('', strlen($title), '='), 106 | ), 107 | $this->getNewLines('minor', $version, $comment) 108 | ); 109 | } elseif ($type == 'minor') { 110 | return array_merge( 111 | array( 112 | '', 113 | " Version $major.$minor - $comment", 114 | ), 115 | $this->getNewLines('patch', $version, 'initial release') 116 | ); 117 | } else { //patch 118 | $date = $this->getFormattedDate(); 119 | 120 | return array( 121 | " $date $version $comment", 122 | ); 123 | } 124 | } 125 | 126 | /** 127 | * Return the position where to insert new lines according to the type of insertion 128 | * 129 | * @param array $lines Existing lines 130 | * @param string $type Release type 131 | * 132 | * @return int The position where to insert 133 | * 134 | * @throws \Liip\RMT\Exception 135 | */ 136 | protected function findPositionToInsert($lines, $type) 137 | { 138 | // Major are always inserted at the top 139 | if ($type == 'major') { 140 | return 0; 141 | } 142 | 143 | // Minor must be inserted one line above the first major section 144 | if ($type == 'minor') { 145 | foreach ($lines as $pos => $line) { 146 | if (preg_match('/^=======/', $line)) { 147 | return $pos + 1; 148 | } 149 | } 150 | } 151 | 152 | // Patch should go directly after the first minor 153 | if ($type == 'patch') { 154 | foreach ($lines as $pos => $line) { 155 | if (preg_match('/Version\s\d+\.\d+\s\-/', $line)) { 156 | return $pos + 1; 157 | } 158 | } 159 | } 160 | 161 | throw new \Liip\RMT\Exception('Invalid changelog formatting'); 162 | } 163 | 164 | protected function getFormattedDate() 165 | { 166 | return date('d/m/Y H:i'); 167 | } 168 | 169 | public function getLastVersionRegex() 170 | { 171 | return '#\s+\d+/\d+/\d+\s\d+:\d+\s+([^\s]+)#'; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Liip/RMT/Changelog/Formatter/SimpleChangelogFormatter.php: -------------------------------------------------------------------------------- 1 | getFormattedDate(); 19 | array_splice($lines, 0, 0, array("$date $version $comment")); 20 | 21 | if (isset($options['extra-lines'])) { 22 | array_splice($lines, 1, 0, $options['extra-lines']); 23 | } 24 | 25 | return $lines; 26 | } 27 | 28 | protected function getFormattedDate() 29 | { 30 | return date('d/m/Y H:i'); 31 | } 32 | 33 | public function getLastVersionRegex() 34 | { 35 | return '#\d+/\d+/\d+\s\d+:\d+\s\s([^\s]+)#'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Liip/RMT/Command/BaseCommand.php: -------------------------------------------------------------------------------- 1 | input = $input; 46 | if (!$output instanceof Output) { 47 | throw new \InvalidArgumentException('Not the expected output type'); 48 | } 49 | $this->output = $output; 50 | $dialogHelper = class_exists(QuestionHelper::class) 51 | ? $this->getHelperSet()->get('question') 52 | : $this->getHelperSet()->get('dialog') 53 | ; 54 | $this->output->setDialogHelper($dialogHelper); 55 | $this->output->setFormatterHelper($this->getHelperSet()->get('formatter')); 56 | Context::getInstance()->setService('input', $this->input); 57 | Context::getInstance()->setService('output', $this->output); 58 | 59 | return parent::run($input, $output); 60 | } 61 | 62 | public function getInput(): InputInterface 63 | { 64 | return $this->input; 65 | } 66 | 67 | public function getOutput(): Output 68 | { 69 | return $this->output; 70 | } 71 | 72 | public function loadContext(): void 73 | { 74 | $configHandler = new Handler($this->getApplication()->getConfig(), $this->getApplication()->getProjectRootDir()); 75 | $config = $configHandler->getBaseConfig(); 76 | 77 | // Select a branch specific config if a VCS is in use 78 | if (isset($config['vcs'])) { 79 | Context::getInstance()->setService('vcs', $config['vcs']['class'], $config['vcs']['options']); 80 | /** @var VCSInterface $vcs */ 81 | $vcs = Context::get('vcs'); 82 | try { 83 | $branch = $vcs->getCurrentBranch(); 84 | } catch (\Exception $e) { 85 | echo "\033[31mImpossible to read the branch name\033[37m"; 86 | } 87 | if (isset($branch)) { 88 | $config = $configHandler->getConfigForBranch($branch); 89 | } 90 | } 91 | 92 | // Store the config for latter usage 93 | Context::getInstance()->setParameter('config', $config); 94 | 95 | // Populate the context 96 | foreach (array('version-generator', 'version-persister') as $service) { 97 | Context::getInstance()->setService($service, $config[$service]['class'], $config[$service]['options']); 98 | } 99 | foreach (array('prerequisites', 'pre-release-actions', 'post-release-actions') as $listName) { 100 | Context::getInstance()->createEmptyList($listName); 101 | foreach ($config[$listName] as $service) { 102 | Context::getInstance()->addToList($listName, $service['class'], $service['options']); 103 | } 104 | } 105 | 106 | // Provide the root dir as a context parameter 107 | Context::getInstance()->setParameter('project-root', $this->getApplication()->getProjectRootDir()); 108 | } 109 | 110 | public function getApplication(): Application 111 | { 112 | return Application::$instance; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Liip/RMT/Command/ChangesCommand.php: -------------------------------------------------------------------------------- 1 | setName('changes'); 27 | $this->setDescription('Shows the list of changes since last release'); 28 | $this->setHelp('The changes command is used to list the changes since last release.'); 29 | $this->addOption('exclude-merge-commits', null, InputOption::VALUE_NONE, 'Exclude merge commits'); 30 | $this->addOption('files', null, InputOption::VALUE_NONE, 'Display the list of modified files'); 31 | } 32 | 33 | protected function execute(InputInterface $input, OutputInterface $output): int 34 | { 35 | $lastVersion = Context::get('version-persister')->getCurrentVersionTag(); 36 | $noMerges = $input->getOption('exclude-merge-commits'); 37 | 38 | if ($input->getOption('files')) { 39 | $output->writeln("Here is the list of files changed since $lastVersion:"); 40 | $output->indent(); 41 | $output->writeln(array_keys(Context::get('vcs')->getModifiedFilesSince($lastVersion))); 42 | 43 | return 0; 44 | } 45 | 46 | $output->writeln("Here is the list of changes since $lastVersion:"); 47 | $output->indent(); 48 | $output->writeln(Context::get('vcs')->getAllModificationsSince($lastVersion, false, $noMerges)); 49 | 50 | return 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Liip/RMT/Command/ConfigCommand.php: -------------------------------------------------------------------------------- 1 | setName('config'); 27 | $this->setDescription('Show the current parsed config (according to your branch)'); 28 | $this->setHelp('The config command can be used to see the current config.'); 29 | } 30 | 31 | protected function execute(InputInterface $input, OutputInterface $output): int 32 | { 33 | $this->loadContext(); 34 | $output->writeln('Current configuration is:'); 35 | $output->writeln(Yaml::dump(Context::getInstance()->getParam('config'))); 36 | 37 | return 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Liip/RMT/Command/CurrentCommand.php: -------------------------------------------------------------------------------- 1 | setName('current'); 27 | $this->setDescription('Display information about the current release'); 28 | $this->setHelp('The current task can be used to display information on the current release'); 29 | $this->addOption('raw', null, InputOption::VALUE_NONE, 'display only the version name'); 30 | $this->addOption('vcs-tag', null, InputOption::VALUE_NONE, 'display the associated vcs-tag'); 31 | } 32 | 33 | protected function execute(InputInterface $input, OutputInterface $output): int 34 | { 35 | $this->loadContext(); 36 | $version = Context::get('version-persister')->getCurrentVersion(); 37 | if ($input->getOption('vcs-tag')) { 38 | $vcsTag = Context::get('version-persister')->getCurrentVersionTag(); 39 | } 40 | if ($input->getOption('raw')) { 41 | $output->writeln($input->getOption('vcs-tag') ? $vcsTag : $version); 42 | } else { 43 | $msg = "Current release is: $version"; 44 | if ($input->getOption('vcs-tag')) { 45 | $msg .= " (VCS tag: $vcsTag)"; 46 | } 47 | $output->writeln($msg); 48 | } 49 | 50 | return 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Liip/RMT/Command/InitCommand.php: -------------------------------------------------------------------------------- 1 | getApplication()->getProjectRootDir(); 34 | $this->executablePath = $projectDir.'/RMT'; 35 | $this->configPath = $configPath == null ? $projectDir.'/.rmt.yml' : $configPath; 36 | $this->commandPath = realpath(__DIR__.'/../../../../command.php'); 37 | 38 | // If possible try to generate a relative link for the command if RMT is installed inside the project 39 | if (strpos($this->commandPath, $projectDir) === 0) { 40 | $this->commandPath = substr($this->commandPath, strlen($projectDir) + 1); 41 | } 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | protected function configure(): void 48 | { 49 | $this->setName('init'); 50 | $this->setDescription('Setup a new project configuration in the current directory'); 51 | $this->setHelp('The init interactive task can be used to setup a new project'); 52 | 53 | // Add an option to force re-creation of the config file 54 | $this->getDefinition()->addOption(new InputOption('force', null, InputOption::VALUE_NONE, 'Force update of the config file')); 55 | 56 | // Create an information collector and configure the different information request 57 | $this->informationCollector = new InformationCollector(); 58 | $this->informationCollector->registerRequests(array( 59 | new InformationRequest('configonly', array( 60 | 'description' => 'if you want to skip creation of the RMT convenience script', 61 | 'type' => 'yes-no', 62 | 'command_argument' => true, 63 | 'interactive' => true, 64 | 'default' => 'n', 65 | )), 66 | new InformationRequest('vcs', array( 67 | 'description' => 'The VCS system to use', 68 | 'type' => 'choice', 69 | 'choices' => array('git', 'hg', 'none'), 70 | 'choices_shortcuts' => array('g' => 'git', 'h' => 'hg', 'n' => 'none'), 71 | 'default' => 'none', 72 | )), 73 | new InformationRequest('generator', array( 74 | 'description' => 'The generator to use for version incrementing', 75 | 'type' => 'choice', 76 | 'choices' => array('semantic-versioning', 'basic-increment'), 77 | 'choices_shortcuts' => array('s' => 'semantic-versioning', 'b' => 'basic-increment'), 78 | )), 79 | new InformationRequest('persister', array( 80 | 'description' => 'The strategy to use to persist the current version value', 81 | 'type' => 'choice', 82 | 'choices' => array('vcs-tag', 'changelog'), 83 | 'choices_shortcuts' => array('t' => 'vcs-tag', 'c' => 'changelog'), 84 | 'command_argument' => true, 85 | 'interactive' => true, 86 | )), 87 | )); 88 | foreach ($this->informationCollector->getCommandOptions() as $option) { 89 | $this->getDefinition()->addOption($option); 90 | } 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | protected function initialize(InputInterface $input, OutputInterface $output): void 97 | { 98 | parent::initialize($input, $output); 99 | 100 | $this->informationCollector->handleCommandInput($input); 101 | $this->getOutput()->writeBigTitle('Welcome to Release Management Tool initialization'); 102 | $this->getOutput()->writeEmptyLine(); 103 | 104 | // Security check for the config 105 | $configPath = $this->getApplication()->getConfigFilePath(); 106 | if ($configPath !== null && file_exists($configPath) && $input->getOption('force') !== true) { 107 | throw new \Exception("A config file already exist ($configPath), if you want to regenerate it, use the --force option"); 108 | } 109 | 110 | // Guessing elements path 111 | $this->buildPaths($configPath); 112 | 113 | // disable the creation of the conveniance script when within a phar 114 | if (extension_loaded('phar') && \Phar::running()) { 115 | $this->informationCollector->setValueFor('configonly', 'y'); 116 | } 117 | } 118 | 119 | /** 120 | * {@inheritdoc} 121 | */ 122 | protected function interact(InputInterface $input, OutputInterface $output): void 123 | { 124 | parent::interact($input, $output); 125 | 126 | // Fill up questions 127 | if ($this->informationCollector->hasMissingInformation()) { 128 | foreach ($this->informationCollector->getInteractiveQuestions() as $name => $question) { 129 | $answer = $this->getOutput()->askQuestion($question, null, $this->input); 130 | $this->informationCollector->setValueFor($name, $answer); 131 | $this->getOutput()->writeEmptyLine(); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * {@inheritdoc} 138 | */ 139 | protected function execute(InputInterface $input, OutputInterface $output): int 140 | { 141 | if ($this->informationCollector->getValueFor('configonly') == 'n') { 142 | // Create the executable task inside the project home 143 | $this->getOutput()->writeln("Creation of the new executable {$this->executablePath}"); 144 | file_put_contents( 145 | $this->executablePath, 146 | "#!/usr/bin/env php\n". 147 | "commandPath}';\n" 150 | ); 151 | chmod('RMT', 0755); 152 | } 153 | 154 | // Create the config file from a template 155 | $this->getOutput()->writeln("Creation of the config file {$this->configPath}"); 156 | $template = $this->informationCollector->getValueFor('vcs') == 'none' ? 157 | __DIR__.'/../Config/templates/no-vcs-config.yml.tmpl' : 158 | __DIR__.'/../Config/templates/default-vcs-config.yml.tmpl' 159 | ; 160 | $config = file_get_contents($template); 161 | $generator = $this->informationCollector->getValueFor('generator'); 162 | foreach (array( 163 | 'generator' => $generator == 'semantic-versioning' ? 164 | 'semantic # More complex versionning (semantic)' : 'simple # Same simple versionning', 165 | 'vcs' => $this->informationCollector->getValueFor('vcs'), 166 | 'persister' => $this->informationCollector->getValueFor('persister'), 167 | 'changelog-format' => $generator == 'semantic-versioning' ? 'semantic' : 'simple', 168 | ) as $key => $value) { 169 | $config = str_replace("%%$key%%", $value, $config); 170 | } 171 | file_put_contents($this->configPath, $config); 172 | 173 | // Confirmation 174 | $this->getOutput()->writeBigTitle('Success, you can start using RMT by calling "RMT release"'); 175 | $this->getOutput()->writeEmptyLine(); 176 | 177 | return 0; 178 | } 179 | 180 | public function getConfigData(): array 181 | { 182 | $config = array(); 183 | 184 | $vcs = $this->informationCollector->getValueFor('vcs'); 185 | if ($vcs !== 'none') { 186 | $config['vcs'] = $vcs; 187 | } 188 | 189 | $generator = $this->informationCollector->getValueFor('generator'); 190 | 191 | $config['version-persister'] = $this->informationCollector->getValueFor('persister'); 192 | 193 | return $config; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Liip/RMT/Command/ReleaseCommand.php: -------------------------------------------------------------------------------- 1 | setName('release'); 31 | $this->setDescription('Release a new version of the project'); 32 | $this->setHelp('The release interactive task must be used to create a new version of a project'); 33 | 34 | $this->loadContext(); 35 | $this->loadInformationCollector(); 36 | 37 | // Register the command option 38 | foreach (Context::get('information-collector')->getCommandOptions() as $option) { 39 | $this->getDefinition()->addOption($option); 40 | } 41 | } 42 | 43 | protected function loadInformationCollector(): void 44 | { 45 | $ic = new InformationCollector(); 46 | 47 | // Add a specific option if it's the first release 48 | try { 49 | Context::get('version-persister')->getCurrentVersion(); 50 | } catch (\Liip\RMT\Exception\NoReleaseFoundException $e) { 51 | $ic->registerRequest( 52 | new InformationRequest('confirm-first', array( 53 | 'description' => 'This is the first release for the current branch', 54 | 'type' => 'confirmation', 55 | )) 56 | ); 57 | } catch (\Exception $e) { 58 | echo 'Error while trying to read the current version'; 59 | } 60 | 61 | // Register options of the release tasks 62 | $ic->registerRequests(Context::get('version-generator')->getInformationRequests()); 63 | $ic->registerRequests(Context::get('version-persister')->getInformationRequests()); 64 | 65 | // Register options of all lists (prerequistes and actions) 66 | foreach (array('prerequisites', 'pre-release-actions', 'post-release-actions') as $listName) { 67 | foreach (Context::getInstance()->getList($listName) as $listItem) { 68 | $ic->registerRequests($listItem->getInformationRequests()); 69 | } 70 | } 71 | 72 | Context::getInstance()->setService('information-collector', $ic); 73 | } 74 | 75 | /** 76 | * Always executed 77 | * 78 | * {@inheritdoc} 79 | */ 80 | protected function initialize(InputInterface $input, OutputInterface $output): void 81 | { 82 | parent::initialize($input, $output); 83 | 84 | Context::get('information-collector')->handleCommandInput($input); 85 | 86 | $this->getOutput()->writeBigTitle('Welcome to Release Management Tool'); 87 | 88 | $this->executeActionListIfExist('prerequisites'); 89 | } 90 | 91 | /** 92 | * Executed only when we are in interactive mode 93 | * 94 | * {@inheritdoc} 95 | */ 96 | protected function interact(InputInterface $input, OutputInterface $output): void 97 | { 98 | parent::interact($input, $output); 99 | 100 | // Fill up questions 101 | if (Context::get('information-collector')->hasMissingInformation()) { 102 | $questions = Context::get('information-collector')->getInteractiveQuestions(); 103 | $this->getOutput()->writeSmallTitle('Information collect ('.count($questions).' questions)'); 104 | $this->getOutput()->indent(); 105 | $count = 1; 106 | foreach ($questions as $name => $question) { 107 | $answer = $this->getOutput()->askQuestion($question, $count++, $this->input); 108 | Context::get('information-collector')->setValueFor($name, $answer); 109 | $this->getOutput()->writeEmptyLine(); 110 | } 111 | $this->getOutput()->unIndent(); 112 | } 113 | } 114 | 115 | /** 116 | * Always executed, but first initialize and interact have already been called 117 | * 118 | * {@inheritdoc} 119 | */ 120 | protected function execute(InputInterface $input, OutputInterface $output): int 121 | { 122 | // Get the current version or generate a new one if the user has confirm that this is required 123 | try { 124 | $currentVersion = Context::get('version-persister')->getCurrentVersion(); 125 | } catch (\Liip\RMT\Exception\NoReleaseFoundException $e) { 126 | if (Context::get('information-collector')->getValueFor('confirm-first') == false) { 127 | throw $e; 128 | } 129 | $currentVersion = Context::get('version-generator')->getInitialVersion(); 130 | } 131 | Context::getInstance()->setParameter('current-version', $currentVersion); 132 | 133 | // Generate and save the new version number 134 | $newVersion = Context::get('version-generator')->generateNextVersion( 135 | Context::getParam('current-version') 136 | ); 137 | Context::getInstance()->setParameter('new-version', $newVersion); 138 | 139 | $this->executeActionListIfExist('pre-release-actions'); 140 | 141 | $this->getOutput()->writeSmallTitle('Release process'); 142 | $this->getOutput()->indent(); 143 | 144 | $this->getOutput()->writeln("A new version named [$newVersion] is going to be released"); 145 | Context::get('version-persister')->save($newVersion); 146 | $this->getOutput()->writeln('Release: Success'); 147 | 148 | $this->getOutput()->unIndent(); 149 | 150 | $this->executeActionListIfExist('post-release-actions'); 151 | 152 | return 0; 153 | } 154 | 155 | protected function executeActionListIfExist($name, $title = null): void 156 | { 157 | $actions = Context::getInstance()->getList($name); 158 | if (count($actions) > 0) { 159 | $this->getOutput()->writeSmallTitle($title ?: ucfirst($name)); 160 | $this->getOutput()->indent(); 161 | foreach ($actions as $num => $action) { 162 | $this->getOutput()->write(++$num.') '.$action->getTitle().' : '); 163 | $this->getOutput()->indent(); 164 | $action->execute(); 165 | $this->getOutput()->writeEmptyLine(); 166 | $this->getOutput()->unIndent(); 167 | } 168 | $this->getOutput()->unIndent(); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Liip/RMT/Config/Exception.php: -------------------------------------------------------------------------------- 1 | rawConfig = $rawConfig; 26 | $this->projectRoot = $projectRoot; 27 | } 28 | 29 | public function getDefaultConfig() 30 | { 31 | return array( 32 | 'vcs' => null, 33 | 'prerequisites' => array(), 34 | 'pre-release-actions' => array(), 35 | 'version-generator' => null, 36 | 'version-persister' => null, 37 | 'post-release-actions' => array(), 38 | 'branch-specific' => array(), 39 | ); 40 | } 41 | 42 | public function getConfigForBranch($branchName) 43 | { 44 | return $this->prepareConfigFor($branchName); 45 | } 46 | 47 | public function getBaseConfig() 48 | { 49 | return $this->prepareConfigFor(null); 50 | } 51 | 52 | protected function prepareConfigFor($branch) 53 | { 54 | $config = $this->mergeConfig($branch); 55 | $config = $this->normalize($config); 56 | 57 | return $config; 58 | } 59 | 60 | protected function mergeConfig($branchName = null) 61 | { 62 | // Handling the two different config mode (with 'branch-specific' or with '_default' section) 63 | // See https://github.com/liip/RMT/issues/56 for more info 64 | if (array_key_exists('_default', $this->rawConfig)) { 65 | $baseConfig = array_merge($this->getDefaultConfig(), $this->rawConfig['_default']); 66 | unset($baseConfig['branch-specific']); 67 | $branchesConfig = $this->rawConfig; 68 | unset($branchesConfig['_default']); 69 | } else { 70 | $baseConfig = array_merge($this->getDefaultConfig(), $this->rawConfig); 71 | $branchesConfig = $baseConfig['branch-specific']; 72 | unset($baseConfig['branch-specific']); 73 | } 74 | 75 | // Return custom branch config 76 | if (isset($branchName) && isset($branchesConfig[$branchName])) { 77 | return array_replace_recursive($baseConfig, $branchesConfig[$branchName]); 78 | } 79 | 80 | return $baseConfig; 81 | } 82 | 83 | /** 84 | * Normalize all config entry to be a normalize class entry: array("class"=>XXX, "options"=>YYY) 85 | */ 86 | protected function normalize($config) 87 | { 88 | // Validate the config entry 89 | $this->validateRootElements($config); 90 | 91 | // For single value elements, normalize all class name and options, remove null entry 92 | foreach (array('vcs', 'version-generator', 'version-persister') as $configKey) { 93 | $value = $config[$configKey]; 94 | if ($value == null) { 95 | unset($config[$configKey]); 96 | continue; 97 | } 98 | $config[$configKey] = $this->getClassAndOptions($value, $configKey); 99 | } 100 | 101 | // Same process but for list value elements 102 | foreach (array('prerequisites', 'pre-release-actions', 'post-release-actions') as $configKey) { 103 | foreach ($config[$configKey] as $key => $item) { 104 | 105 | // Accept the element to be define by key or by value 106 | if (!is_numeric($key)) { 107 | if ($item == null) { 108 | $item = array(); 109 | } 110 | $item['name'] = $key; 111 | } 112 | 113 | $config[$configKey][$key] = $this->getClassAndOptions($item, $configKey.'_'.$key); 114 | } 115 | } 116 | 117 | return $config; 118 | } 119 | 120 | protected function validateRootElements($config) 121 | { 122 | // Check for extra keys 123 | $extraKeys = array_diff(array_keys($config), array_keys($this->getDefaultConfig())); 124 | if (count($extraKeys) > 0) { 125 | $extraKeys = implode(', ', $extraKeys); 126 | $validKeys = implode(', ', array_keys($this->getDefaultConfig())); 127 | throw new Exception('key(s) ['.$extraKeys.'] are invalid, must be ['.$validKeys.']'); 128 | } 129 | 130 | // Check for missing keys 131 | foreach (array('version-generator', 'version-persister') as $mandatoryParam) { 132 | if ($config[$mandatoryParam] == null) { 133 | throw new Exception("[$mandatoryParam] should be defined"); 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Sub part of the normalize() 140 | */ 141 | protected function getClassAndOptions($rawConfig, $sectionName) 142 | { 143 | if (is_string($rawConfig)) { 144 | $class = $this->findClass($rawConfig, $sectionName); 145 | $options = array(); 146 | } elseif (is_array($rawConfig)) { 147 | 148 | // Handling Yml corner case (see https://github.com/liip/RMT/issues/54) 149 | if (count($rawConfig) == 1 && key($rawConfig) !== 'name') { 150 | $name = key($rawConfig); 151 | $rawConfig = is_array(reset($rawConfig)) ? reset($rawConfig) : array(); 152 | $rawConfig['name'] = $name; 153 | } 154 | 155 | if (!isset($rawConfig['name'])) { 156 | throw new Exception("Missing information for [$sectionName], you must provide a [name] value"); 157 | } 158 | 159 | $class = $this->findClass($rawConfig['name'], $sectionName); 160 | unset($rawConfig['name']); 161 | 162 | $options = $rawConfig; 163 | } else { 164 | throw new Exception("Invalid configuration for [$sectionName] should be a object name or an array with name and options"); 165 | } 166 | 167 | return array('class' => $class, 'options' => $options); 168 | } 169 | 170 | /** 171 | * Sub part of the normalize() 172 | */ 173 | protected function findClass($name, $sectionName) 174 | { 175 | $file = $this->projectRoot.DIRECTORY_SEPARATOR.$name; 176 | if (strpos($file, '.php') > 0) { 177 | if (file_exists($file)) { 178 | require_once $file; 179 | $parts = explode(DIRECTORY_SEPARATOR, $file); 180 | $lastPart = array_pop($parts); 181 | 182 | return str_replace('.php', '', $lastPart); 183 | } else { 184 | throw new \Liip\RMT\Exception("Impossible to open [$file] please review your config"); 185 | } 186 | } 187 | 188 | return $this->findInternalClass($name, $sectionName); 189 | } 190 | 191 | /** 192 | * Sub part of the normalize() 193 | */ 194 | protected function findInternalClass($name, $sectionName) 195 | { 196 | // Remove list id like xxx_3 197 | $classType = $sectionName; 198 | if (strpos($classType, '_') !== false) { 199 | $classType = substr($classType, 0, strpos($classType, '_')); 200 | } 201 | 202 | // Guess the namespace 203 | $namespacesByType = [ 204 | 'vcs' => 'Liip\RMT\VCS', 205 | 'prerequisites' => 'Liip\RMT\Prerequisite', 206 | 'pre-release-actions' => 'Liip\RMT\Action', 207 | 'post-release-actions' => 'Liip\RMT\Action', 208 | 'version-generator' => 'Liip\RMT\Version\Generator', 209 | 'version-persister' => 'Liip\RMT\Version\Persister', 210 | ]; 211 | $nameSpace = $namespacesByType[$classType]; 212 | 213 | // Guess the class name 214 | // Convert from xxx-yyy-zzz to XxxYyyZzz and append suffix 215 | $suffixByType = [ 216 | 'vcs' => '', 217 | 'prerequisites' => '', 218 | 'pre-release-actions' => 'Action', 219 | 'post-release-actions' => 'Action', 220 | 'version-generator' => 'Generator', 221 | 'version-persister' => 'Persister', 222 | ]; 223 | $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $name))).$suffixByType[$classType]; 224 | 225 | return $nameSpace.'\\'.$className; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/Liip/RMT/Config/templates/default-vcs-config.yml.tmpl: -------------------------------------------------------------------------------- 1 | _default: 2 | 3 | # VCS CONFIG 4 | vcs: %%vcs%% 5 | 6 | # PREREQUISITES 7 | # Actions executed before any questions get asked to the user. 8 | # Custom action can be added by provided a relative path to the php script. Example: 9 | # - relative/path/to/your-own-sript.php 10 | prerequisites: 11 | - working-copy-check 12 | - display-last-changes 13 | 14 | # GENERAL CONFIG 15 | # Apply to all branches except the one from the 'branch-specific' section 16 | # Like prerequisites, you can add your own script. Example: 17 | # - relative/path/to/your-own-sript.php 18 | version-generator: simple # Simple versionning 19 | version-persister: 20 | vcs-tag: # Release with VCS tag 21 | tag-prefix: "{branch-name}_" # Prefix any tag with the VCS branch name 22 | post-release-actions: 23 | vcs-publish: # Publish the release to the VCS 24 | ask-confirmation: true 25 | 26 | # BRANCH SPECIFIC CONFIG 27 | # On master, we override the general config 28 | master: 29 | version-generator: %%generator%% 30 | version-persister: 31 | vcs-tag: 32 | tag-prefix: '' # No more prefix for tags 33 | pre-release-actions: 34 | changelog-update: # Update a CHANGELOG file before the release 35 | format: %%changelog-format%% 36 | vcs-commit: ~ # Commit the CHANGELOG 37 | -------------------------------------------------------------------------------- /src/Liip/RMT/Config/templates/no-vcs-config.yml.tmpl: -------------------------------------------------------------------------------- 1 | version-generator: %%generator%% 2 | version-persister: %%persister%% -------------------------------------------------------------------------------- /src/Liip/RMT/Context.php: -------------------------------------------------------------------------------- 1 | services[$id] = $classOrObject; 42 | } elseif (is_string($classOrObject)) { 43 | $this->validateClass($classOrObject); 44 | $this->services[$id] = array($classOrObject, $options); 45 | } else { 46 | throw new \InvalidArgumentException('setService() only accept an object or a valid class name'); 47 | } 48 | } 49 | 50 | public function getService($id) 51 | { 52 | if (!isset($this->services[$id])) { 53 | throw new \InvalidArgumentException("There is no service defined with id [$id]"); 54 | } 55 | if (is_array($this->services[$id])) { 56 | $this->services[$id] = $this->instanciateObject($this->services[$id]); 57 | } 58 | 59 | return $this->services[$id]; 60 | } 61 | 62 | public function setParameter($id, $value) 63 | { 64 | $this->params[$id] = $value; 65 | } 66 | 67 | public function getParameter($id) 68 | { 69 | if (!isset($this->params[$id])) { 70 | throw new \InvalidArgumentException("There is no param defined with id [$id]"); 71 | } 72 | 73 | return $this->params[$id]; 74 | } 75 | 76 | public function createEmptyList($id) 77 | { 78 | $this->lists[$id] = array(); 79 | } 80 | 81 | public function addToList($id, $class, $options = null) 82 | { 83 | $this->validateClass($class); 84 | if (!isset($this->lists[$id])) { 85 | $this->createEmptyList($id); 86 | } 87 | $this->lists[$id][] = array($class, $options); 88 | } 89 | 90 | public function getList($id) 91 | { 92 | if (!isset($this->lists[$id])) { 93 | throw new \InvalidArgumentException("There is no list defined with id [$id]"); 94 | } 95 | foreach ($this->lists[$id] as $pos => $object) { 96 | if (is_array($object)) { 97 | $this->lists[$id][$pos] = $this->instanciateObject($object); 98 | } 99 | } 100 | 101 | return $this->lists[$id]; 102 | } 103 | 104 | protected function instanciateObject($objectDefinition) 105 | { 106 | list($className, $options) = $objectDefinition; 107 | 108 | return new $className($options); 109 | } 110 | 111 | protected function validateClass($className) 112 | { 113 | if (!class_exists($className)) { 114 | throw new \InvalidArgumentException("The class [$className] does not exist"); 115 | } 116 | } 117 | 118 | /** 119 | * Shortcut to retried a service 120 | * 121 | * @param string $serviceName 122 | * 123 | * @return mixed 124 | */ 125 | public static function get($serviceName) 126 | { 127 | return self::getInstance()->getService($serviceName); 128 | } 129 | 130 | /** 131 | * Shortcut to retried a parameter 132 | * 133 | * @param string $name 134 | * 135 | * @return mixed 136 | */ 137 | public static function getParam($name) 138 | { 139 | return self::getInstance()->getParameter($name); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Liip/RMT/Exception.php: -------------------------------------------------------------------------------- 1 | array( 23 | 'description' => 'Comment associated with the release', 24 | 'type' => 'text', 25 | ), 26 | 'type' => array( 27 | 'description' => 'Release type, can be major, minor or patch', 28 | 'type' => 'choice', 29 | 'choices' => array('major', 'minor', 'patch'), 30 | 'choices_shortcuts' => array('m' => 'major', 'i' => 'minor', 'p' => 'patch'), 31 | 'default' => 'patch', 32 | ), 33 | 'label' => array( 34 | 'description' => 'Release label, can be rc, beta, alpha or none', 35 | 'type' => 'choice', 36 | 'choices' => array('rc', 'beta', 'alpha', 'none'), 37 | 'choices_shortcuts' => array('rc' => 'rc', 'b' => 'beta', 'a' => 'alpha', 'n' => 'none'), 38 | 'default' => 'none', 39 | ), 40 | ); 41 | 42 | protected $requests = array(); 43 | protected $values = array(); 44 | 45 | public function registerRequest($request) 46 | { 47 | $name = $request->getName(); 48 | if (in_array($name, static::$standardRequests)) { 49 | throw new \Exception("Request [$name] is reserved as a standard request name, choose an other name please"); 50 | } 51 | 52 | if ($this->hasRequest($name)) { 53 | throw new \Exception("Request [$name] already registered"); 54 | } 55 | 56 | $this->requests[$name] = $request; 57 | } 58 | 59 | public function registerRequests($list) 60 | { 61 | foreach ($list as $request) { 62 | if (is_string($request)) { 63 | $this->registerStandardRequest($request); 64 | } elseif ($request instanceof InformationRequest) { 65 | $this->registerRequest($request); 66 | } else { 67 | throw new \Exception('Invalid request, must a Request class or a string for standard requests'); 68 | } 69 | } 70 | } 71 | 72 | public function registerStandardRequest($name) 73 | { 74 | if (!in_array($name, array_keys(static::$standardRequests))) { 75 | throw new \Exception("There is no standard request named [$name]"); 76 | } 77 | if (!isset($this->requests[$name])) { 78 | $this->requests[$name] = new InformationRequest($name, static::$standardRequests[$name]); 79 | } 80 | } 81 | 82 | /** 83 | * @param string $name 84 | * 85 | * @return InformationRequest 86 | */ 87 | public function getRequest($name) 88 | { 89 | if (!$this->hasRequest($name)) { 90 | throw new \InvalidArgumentException("There is no information request named [$name]"); 91 | } 92 | 93 | return $this->requests[$name]; 94 | } 95 | 96 | public function hasRequest($name) 97 | { 98 | return array_key_exists($name, $this->requests); 99 | } 100 | 101 | /** 102 | * Return a set of command request, converted from the Base Request 103 | * 104 | * @return InputOption[] 105 | */ 106 | public function getCommandOptions() 107 | { 108 | $consoleOptions = array(); 109 | foreach ($this->requests as $name => $request) { 110 | if ($request->isAvailableAsCommandOption()) { 111 | $consoleOptions[$name] = $request->convertToCommandOption(); 112 | } 113 | } 114 | 115 | return $consoleOptions; 116 | } 117 | 118 | public function hasMissingInformation() 119 | { 120 | foreach ($this->requests as $request) { 121 | if (!$request->hasValue()) { 122 | return true; 123 | } 124 | } 125 | 126 | return false; 127 | } 128 | 129 | public function getInteractiveQuestions() 130 | { 131 | $questions = array(); 132 | foreach ($this->requests as $name => $request) { 133 | if ($request->isAvailableForInteractive() && !$request->hasValue()) { 134 | $questions[$name] = $request->convertToInteractiveQuestion(); 135 | } 136 | } 137 | 138 | return $questions; 139 | } 140 | 141 | public function handleCommandInput(InputInterface $input) 142 | { 143 | foreach ($input->getOptions() as $name => $value) { 144 | if ($this->hasRequest($name) && ($value !== null && $value !== false)) { 145 | $this->getRequest($name)->setValue($value); 146 | } 147 | } 148 | } 149 | 150 | public function setValueFor($requestName, $value) 151 | { 152 | return $this->getRequest($requestName)->setValue($value); 153 | } 154 | 155 | public function hasValueFor($requestName) 156 | { 157 | return $this->getRequest($requestName)->hasValue(); 158 | } 159 | 160 | public function getValueFor($requestName, $default = null) 161 | { 162 | if ($this->hasRequest($requestName)) { 163 | return $this->getRequest($requestName)->getValue(); 164 | } else { 165 | if (func_num_args() == 2) { 166 | return $default; 167 | } 168 | throw new \Exception("No request named $requestName"); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Liip/RMT/Information/InformationRequest.php: -------------------------------------------------------------------------------- 1 | '', 24 | 'type' => 'text', 25 | 'choices' => array(), 26 | 'choices_shortcuts' => array(), 27 | 'command_argument' => true, 28 | 'command_shortcut' => null, 29 | 'interactive' => true, 30 | 'default' => null, 31 | 'interactive_help' => '', 32 | 'interactive_help_shortcut' => 'h', 33 | 'hidden_answer' => false, 34 | ); 35 | 36 | protected $name; 37 | protected $options; 38 | protected $value; 39 | protected $hasValue = false; 40 | 41 | public function __construct($name, $options = array()) 42 | { 43 | $this->name = $name; 44 | 45 | // Check for invalid option 46 | $invalidOptions = array_diff(array_keys($options), array_keys(self::$defaults)); 47 | if (count($invalidOptions) > 0) { 48 | throw new \Exception('Invalid config option(s) ['.implode(', ', $invalidOptions).']'); 49 | } 50 | 51 | // Set a default false for confirmation 52 | if (isset($options['type']) && $options['type'] == 'confirmation') { 53 | $options['default'] = false; 54 | } 55 | 56 | // Merging with defaults 57 | $this->options = array_merge(self::$defaults, $options); 58 | 59 | // Type validation 60 | if (!in_array($this->options['type'], self::$validTypes)) { 61 | throw new \Exception('Invalid option type ['.$this->options['type'].']'); 62 | } 63 | } 64 | 65 | public function getName() 66 | { 67 | return $this->name; 68 | } 69 | 70 | public function getOption($name) 71 | { 72 | return $this->options[$name]; 73 | } 74 | 75 | public function isAvailableAsCommandOption() 76 | { 77 | return $this->options['command_argument']; 78 | } 79 | 80 | public function isAvailableForInteractive() 81 | { 82 | return $this->options['interactive']; 83 | } 84 | 85 | public function convertToCommandOption() 86 | { 87 | $mode = $this->options['type'] == 'boolean' || $this->options['type'] == 'confirmation' ? 88 | InputOption::VALUE_NONE : 89 | InputOption::VALUE_REQUIRED 90 | ; 91 | 92 | return new InputOption( 93 | $this->name, 94 | $this->options['command_shortcut'], 95 | $mode, 96 | $this->options['description'], 97 | (!$this->isAvailableForInteractive() && $this->getOption('type') !== 'confirmation') ? $this->options['default'] : null 98 | ); 99 | } 100 | 101 | public function convertToInteractiveQuestion() 102 | { 103 | $questionOptions = array(); 104 | foreach (array('choices', 'choices_shortcuts', 'interactive_help', 'interactive_help_shortcut') as $optionName) { 105 | $questionOptions[$optionName] = $this->options[$optionName]; 106 | } 107 | 108 | return new \Liip\RMT\Information\InteractiveQuestion($this); 109 | } 110 | 111 | public function setValue($value) 112 | { 113 | try { 114 | $value = $this->validate($value); 115 | } catch (\Exception $e) { 116 | throw new \InvalidArgumentException('Validation error for ['.$this->getName().']: '.$e->getMessage()); 117 | } 118 | $this->value = $value; 119 | $this->hasValue = true; 120 | } 121 | 122 | private function validateValue($parameters, $callback, $message) 123 | { 124 | if (!is_array($parameters)) { 125 | $parameters = array($parameters); 126 | } 127 | 128 | if (!call_user_func_array($callback, $parameters)) { 129 | throw new \InvalidArgumentException($message); 130 | } 131 | } 132 | 133 | public function validate($value) 134 | { 135 | switch ($this->options['type']) { 136 | case 'boolean': 137 | $this->validateValue($value, 'is_bool', 'Must be a boolean'); 138 | break; 139 | case 'choice': 140 | $this->validateValue(array($value, $this->options['choices']), function ($v, $choices) { 141 | return in_array($v, $choices); 142 | }, 'Must be one of '.json_encode($this->options['choices'])); 143 | break; 144 | case 'text': 145 | $this->validateValue($value, function ($v) { 146 | return is_string($v) && strlen($v) > 0; 147 | }, 'Text must be provided'); 148 | break; 149 | case 'yes-no': 150 | $value = lcfirst($value[0]); 151 | $this->validateValue($value, function ($v) { 152 | return $v === 'y' || $v === 'n'; 153 | }, "Must be 'y' or 'n'"); 154 | break; 155 | } 156 | 157 | return $value; 158 | } 159 | 160 | public function getValue() 161 | { 162 | if (!$this->hasValue() && $this->options['default'] === null) { 163 | throw new \Liip\RMT\Exception("No value [{$this->name}] available"); 164 | } 165 | 166 | return $this->hasValue() ? $this->value : $this->options['default']; 167 | } 168 | 169 | public function hasValue() 170 | { 171 | return $this->hasValue; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Liip/RMT/Information/InteractiveQuestion.php: -------------------------------------------------------------------------------- 1 | informationRequest = $ir; 24 | } 25 | 26 | public function getFormatedText() 27 | { 28 | if ($this->informationRequest->getOption('type') == 'confirmation') { 29 | $text = 'Please confirm that '; 30 | } else { 31 | $text = 'Please provide '; 32 | } 33 | 34 | $text .= strtolower($this->informationRequest->getOption('description')); 35 | 36 | if ($this->informationRequest->getOption('type') == 'choice') { 37 | $text .= "\n". $this->formatChoices( 38 | $this->informationRequest->getOption('choices'), 39 | $this->informationRequest->getOption('choices_shortcuts') 40 | ); 41 | } 42 | 43 | // print the default if exist 44 | if ($this->hasDefault()) { 45 | $defaultVal = $this->getDefault(); 46 | if (is_bool($defaultVal)) { 47 | $defaultVal = $defaultVal === true ? 'true' : 'false'; 48 | } 49 | $text .= ' (default: '.$defaultVal.')'; 50 | } 51 | 52 | return $text . ': '; 53 | } 54 | 55 | public function formatChoices($choices, $shortcuts) 56 | { 57 | if (count($shortcuts) > 0) { 58 | $shortcuts = array_flip($shortcuts); 59 | foreach ($shortcuts as $choice => $shortcut) { 60 | $shortcuts[$choice] = ''.$shortcut.''; 61 | } 62 | foreach ($choices as $pos => $choice) { 63 | $choices[$pos] = '['.$shortcuts[$choice].'] '. $choice; 64 | } 65 | } 66 | $text = ' '.implode(PHP_EOL.' ', $choices); 67 | 68 | return $text."\nYour choice"; 69 | } 70 | 71 | public function hasDefault() 72 | { 73 | return $this->informationRequest->getOption('default') !== null; 74 | } 75 | 76 | public function getDefault() 77 | { 78 | $default = $this->informationRequest->getOption('default'); 79 | if (count($shortcuts = $this->informationRequest->getOption('choices_shortcuts')) > 0) { 80 | foreach ($shortcuts as $shortcut => $value) { 81 | if ($default == $value) { 82 | return $shortcut; 83 | } 84 | } 85 | } 86 | 87 | return $default; 88 | } 89 | 90 | public function isHiddenAnswer() 91 | { 92 | return $this->informationRequest->getOption('hidden_answer'); 93 | } 94 | 95 | public function getValidator() 96 | { 97 | return array($this, 'validate'); 98 | } 99 | 100 | public function validate($value) 101 | { 102 | // Replace potential shortcuts 103 | if (count($shortcuts = $this->informationRequest->getOption('choices_shortcuts')) > 0) { 104 | if (in_array($value, array_keys($shortcuts))) { 105 | $value = $shortcuts[$value]; 106 | } else { 107 | throw new \Exception('Please select a value in '.json_encode(array_keys($shortcuts))); 108 | } 109 | } 110 | 111 | // Validation 112 | return $this->informationRequest->validate($value); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Liip/RMT/Output/Output.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('error', new OutputFormatterStyle('white', 'red')); 45 | $this->getFormatter()->setStyle('green', new OutputFormatterStyle('green')); 46 | $this->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow')); 47 | $this->getFormatter()->setStyle('question', new OutputFormatterStyle('black', 'cyan')); 48 | $this->getFormatter()->setStyle('title', new OutputFormatterStyle('white', 'blue')); 49 | } 50 | 51 | public function doWrite($message, $newline): void 52 | { 53 | // In case the $message is multi lines 54 | $message = str_replace(PHP_EOL, PHP_EOL.$this->getIndentPadding(), $message); 55 | 56 | if ($this->positionIsALineStart) { 57 | $message = $this->getIndentPadding().$message; 58 | } 59 | 60 | $this->positionIsALineStart = $newline; 61 | parent::doWrite($message, $newline); 62 | } 63 | 64 | public function indent($repeat = 1) 65 | { 66 | $this->indentationLevel += $repeat; 67 | } 68 | 69 | public function unIndent($repeat = 1) 70 | { 71 | $this->indentationLevel -= $repeat; 72 | } 73 | 74 | public function resetIndentation() 75 | { 76 | $this->indentationLevel = 0; 77 | } 78 | 79 | protected function getIndentPadding() 80 | { 81 | return str_pad('', $this->indentationLevel * $this->indentationSize); 82 | } 83 | 84 | public function setDialogHelper($dh) 85 | { 86 | $this->dialogHelper = $dh; 87 | } 88 | 89 | public function setFormatterHelper($fh) 90 | { 91 | $this->formatterHelper = $fh; 92 | } 93 | 94 | public function writeTitle($title, $large = true) 95 | { 96 | $this->writeEmptyLine(); 97 | $this->writeln($this->formatterHelper->formatBlock($title, 'title', $large)); 98 | } 99 | 100 | public function writeBigTitle($title) 101 | { 102 | $this->writeTitle($title, true); 103 | } 104 | 105 | public function writeSmallTitle($title) 106 | { 107 | $this->writeTitle($title, false); 108 | $this->writeEmptyLine(); 109 | } 110 | 111 | public function writeEmptyLine($repeat = 1) 112 | { 113 | $this->writeln(array_fill(0, $repeat, '')); 114 | } 115 | 116 | // when we drop symfony 2.3 support, we should switch to the new QuestionHelper (since 2.5) and see if we need these methods at all anymore 117 | // QuestionHelper does about the same as we do here. 118 | public function askQuestion(InteractiveQuestion $question, $position = null, InputInterface $input = null) 119 | { 120 | $text = ($position !== null ? $position .') ' : null) . $question->getFormatedText(); 121 | 122 | if ($this->dialogHelper instanceof QuestionHelper) { 123 | if (!$input) { 124 | throw new \InvalidArgumentException('With symfony 3, the input stream may not be null'); 125 | } 126 | $q = new Question($text, $question->getDefault()); 127 | $q->setValidator($question->getValidator()); 128 | if ($question->isHiddenAnswer()) { 129 | $q->setHidden(true); 130 | } 131 | 132 | return $this->dialogHelper->ask($input, $this, $q); 133 | } 134 | 135 | if ($this->dialogHelper instanceof DialogHelper) { 136 | 137 | if ($question->isHiddenAnswer()) { 138 | return $this->dialogHelper->askHiddenResponseAndValidate($this, $text, $question->getValidator(), false); 139 | } 140 | 141 | return $this->dialogHelper->askAndValidate($this, $text, $question->getValidator(), false, $question->getDefault()); 142 | } 143 | 144 | throw new \RuntimeException("Invalid dialogHelper"); 145 | } 146 | 147 | // when we drop symfony 2.3 support, we should switch to the QuestionHelper (since 2.5) and drop this method as it adds no value 148 | public function askConfirmation($text, InputInterface $input = null) 149 | { 150 | if ($this->dialogHelper instanceof QuestionHelper) { 151 | if (!$input) { 152 | throw new \InvalidArgumentException('With symfony 3, the input stream may not be null'); 153 | } 154 | return $this->dialogHelper->ask($input, $this, new ConfirmationQuestion($text)); 155 | } 156 | 157 | if ($this->dialogHelper instanceof DialogHelper) { 158 | return $this->dialogHelper->askConfirmation($this, $text); 159 | } 160 | 161 | throw new \RuntimeException("Invalid dialogHelper"); 162 | 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Liip/RMT/Prerequisite/Command.php: -------------------------------------------------------------------------------- 1 | whitelist = array(); 35 | $this->dependencyListWhitelists = array(); 36 | 37 | if (isset($this->options['whitelist'])) { 38 | $this->createWhitelists($this->options['whitelist']); 39 | } 40 | } 41 | 42 | private function createWhitelists($whitelistConfig) 43 | { 44 | foreach ($whitelistConfig as $listing) { 45 | if (isset($listing[1])) { 46 | if (!in_array($listing[1], self::DEPENDENCY_LISTS)) { 47 | throw new \Exception("configuration error: " 48 | . $listing[1] . " is no valid composer dependency section"); 49 | } 50 | if (!isset($this->dependencyListWhitelists[$listing[1]])) { 51 | $this->dependencyListWhitelists[$listing[1]] = array(); 52 | } 53 | $this->dependencyListWhitelists[$listing[1]][] = $listing[0]; 54 | } else { 55 | $this->whitelist[] = $listing[0]; 56 | } 57 | } 58 | } 59 | 60 | public function execute() 61 | { 62 | if (Context::get('information-collector')->getValueFor(self::SKIP_OPTION)) { 63 | Context::get('output')->writeln('composer dependency-stability check skipped'); 64 | return; 65 | } 66 | 67 | if (!file_exists('composer.json')) { 68 | Context::get('output')->writeln('composer.json does not exist, skipping check'); 69 | return; 70 | } 71 | 72 | if (!is_readable('composer.json')) { 73 | throw new \Exception( 74 | 'composer.json can not be read (permissions?), (you can force a release with option --' 75 | . self::SKIP_OPTION.')' 76 | ); 77 | } 78 | 79 | $contents = json_decode(file_get_contents('composer.json'), true); 80 | 81 | foreach (self::DEPENDENCY_LISTS as $dependencyList) { 82 | if (!$this->isListIgnored($dependencyList) && $this->listExists($contents, $dependencyList)) { 83 | $specificWhitelist = $this->generateListSpecificWhitelist($dependencyList); 84 | $this->checkDependencies($contents[$dependencyList], $specificWhitelist); 85 | } 86 | } 87 | 88 | $this->confirmSuccess(); 89 | } 90 | 91 | /** 92 | * @param $dependencyList 93 | * @return mixed 94 | */ 95 | private function isListIgnored($dependencyList) 96 | { 97 | return isset($this->options['ignore-' . $dependencyList]) && $this->options['ignore-' . $dependencyList] === true; 98 | } 99 | 100 | /** 101 | * @param $contents 102 | * @param $dependencyList 103 | * @return bool 104 | */ 105 | private function listExists($contents, $dependencyList) 106 | { 107 | return isset($contents[$dependencyList]); 108 | } 109 | 110 | /** 111 | * @param $dependencyList 112 | * @return array 113 | */ 114 | private function generateListSpecificWhitelist($dependencyList) 115 | { 116 | if (isset($this->dependencyListWhitelists[$dependencyList])) { 117 | return array_merge($this->whitelist, $this->dependencyListWhitelists[$dependencyList]); 118 | } else { 119 | return $this->whitelist; 120 | } 121 | } 122 | 123 | /** 124 | * check every element inside this array for composer version strings and throw an exception if the dependency is 125 | * not stable 126 | * 127 | * @param $dependencyList array 128 | * @param $whitelist array 129 | * @throws \Exception 130 | */ 131 | private function checkDependencies($dependencyList, $whitelist = array()) { 132 | foreach ($dependencyList as $dependency => $version) { 133 | if (($this->startsWith($version, 'dev-') || $this->endsWith($version, '@dev')) 134 | && !in_array($dependency, $whitelist)) { 135 | throw new \Exception( 136 | $dependency 137 | . ' uses dev-version but is not listed on whitelist ' 138 | . ' (you can force a release with option --'.self::SKIP_OPTION.')' 139 | ); 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * @param $haystack string 146 | * @param $needle string 147 | * @return bool 148 | */ 149 | private function startsWith($haystack, $needle) 150 | { 151 | return $haystack[0] === $needle[0] 152 | ? strncmp($haystack, $needle, strlen($needle)) === 0 153 | : false; 154 | } 155 | 156 | /** 157 | * @param $haystack string 158 | * @param $needle string 159 | * @return bool 160 | */ 161 | private function endsWith($haystack, $needle) { 162 | return $needle === '' || substr_compare($haystack, $needle, -strlen($needle)) === 0; 163 | } 164 | 165 | public function getInformationRequests() 166 | { 167 | return array( 168 | new InformationRequest( 169 | self::SKIP_OPTION, 170 | array( 171 | 'description' => 'Do not check composer.json for minimum-stability before the release', 172 | 'type' => 'confirmation', 173 | 'interactive' => false, 174 | ) 175 | ), 176 | ); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Liip/RMT/Prerequisite/ComposerJsonCheck.php: -------------------------------------------------------------------------------- 1 | options = array_merge(array( 28 | 'composer' => 'php composer.phar', 29 | ), $options); 30 | } 31 | 32 | public function execute() 33 | { 34 | // Handle the skip option 35 | if (Context::get('information-collector')->getValueFor(self::SKIP_OPTION)) { 36 | Context::get('output')->writeln('composer.json validation skipped'); 37 | 38 | return; 39 | } 40 | 41 | // Run the validation and live output with the standard output class 42 | $process = $this->executeCommandInProcess($this->options['composer'] . ' validate'); 43 | 44 | // Break up if the result is not good 45 | if ($process->getExitCode() !== 0) { 46 | throw new \Exception('composer.json invalid (you can force a release with option --'.self::SKIP_OPTION.')'); 47 | } 48 | 49 | $this->confirmSuccess(); 50 | } 51 | 52 | public function getInformationRequests() 53 | { 54 | return array( 55 | new InformationRequest(self::SKIP_OPTION, array( 56 | 'description' => 'Do not validate composer.json before the release', 57 | 'type' => 'confirmation', 58 | 'interactive' => false, 59 | )), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Liip/RMT/Prerequisite/ComposerSecurityCheck.php: -------------------------------------------------------------------------------- 1 | getValueFor(self::SKIP_OPTION)) { 30 | Context::get('output')->writeln('composer security check skipped'); 31 | 32 | return; 33 | } 34 | 35 | Context::get('output')->writeln('running composer security check'); 36 | 37 | // Run the actual security check 38 | $process = new Process(['local-php-security-checker', '--format', 'json']); 39 | $process->run(); 40 | 41 | $alerts = json_decode($process->getOutput(), true); 42 | 43 | if ($process->isSuccessful() && count($alerts) === 0) { 44 | $this->confirmSuccess(); 45 | return; 46 | } 47 | 48 | if ($alerts === null) { 49 | throw new \RuntimeException('Error while trying to execute `local-php-security-checker` command. Are you sure the binary is installed globally in your system?'); 50 | } 51 | 52 | // print out the advisories if available 53 | foreach ($alerts as $package => $alert) { 54 | Context::get('output')->writeln("{$package} {$alert['version']}"); 55 | foreach ($alert['advisories'] as $data) { 56 | Context::get('output')->writeln(''); 57 | Context::get('output')->writeln($data['title']); 58 | Context::get('output')->writeln($data['link']); 59 | Context::get('output')->writeln(''); 60 | } 61 | } 62 | 63 | // throw exception to have check fail 64 | throw new \Exception( 65 | 'composer.lock contains insecure packages (you can force a release with option --'.self::SKIP_OPTION.')' 66 | ); 67 | } 68 | 69 | public function getInformationRequests() 70 | { 71 | return array( 72 | new InformationRequest( 73 | self::SKIP_OPTION, 74 | array( 75 | 'description' => 'Do not run composer security check before the release', 76 | 'type' => 'confirmation', 77 | 'interactive' => false, 78 | ) 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Liip/RMT/Prerequisite/ComposerStabilityCheck.php: -------------------------------------------------------------------------------- 1 | options = array_merge( 29 | array( 30 | 'stability' => 'stable', 31 | ), 32 | $options 33 | ); 34 | } 35 | 36 | public function execute() 37 | { 38 | // Handle the skip option 39 | if (Context::get('information-collector')->getValueFor(self::SKIP_OPTION)) { 40 | Context::get('output')->writeln('composer minimum-stability check skipped'); 41 | 42 | return; 43 | } 44 | 45 | // file exists? 46 | if (!file_exists('composer.json')) { 47 | Context::get('output')->writeln('composer.json does not exist, skipping check'); 48 | 49 | return; 50 | } 51 | 52 | // if file is not readable, we can't perform our check 53 | if (!is_readable('composer.json')) { 54 | throw new \Exception( 55 | 'composer.json can not be read (permissions?), (you can force a release with option --' 56 | . self::SKIP_OPTION.')' 57 | ); 58 | } 59 | 60 | $contents = json_decode(file_get_contents('composer.json'), true); 61 | 62 | // fail if the composer config falls back to default, and this check has something else but default set 63 | if (!isset($contents['minimum-stability']) && $this->options['stability'] != 'stable') { 64 | throw new \Exception( 65 | 'minimum-stability is not set, but RMT config requires: ' 66 | . $this->options['stability'].' (you can force a release with option --' 67 | . self::SKIP_OPTION.')' 68 | ); 69 | } 70 | 71 | // fail if stability is set and not the one expected 72 | if (isset($contents['minimum-stability']) && $contents['minimum-stability'] != $this->options['stability']) { 73 | throw new \Exception( 74 | 'minimum-stability is set to: ' 75 | . $contents['minimum-stability'] 76 | . ', but RMT config requires: ' 77 | . $this->options['stability'] 78 | . ' (you can force a release with option --'.self::SKIP_OPTION.')' 79 | ); 80 | } 81 | 82 | $this->confirmSuccess(); 83 | } 84 | 85 | public function getInformationRequests() 86 | { 87 | return array( 88 | new InformationRequest( 89 | self::SKIP_OPTION, 90 | array( 91 | 'description' => 'Do not check composer.json for minimum-stability before the release', 92 | 'type' => 'confirmation', 93 | 'interactive' => false, 94 | ) 95 | ), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Liip/RMT/Prerequisite/DisplayLastChanges.php: -------------------------------------------------------------------------------- 1 | writeEmptyLine(); 28 | Context::get('output')->writeln( 29 | Context::get('vcs')->getAllModificationsSince( 30 | Context::get('version-persister')->getCurrentVersionTag() 31 | ) 32 | ); 33 | } catch (\Exception $e) { 34 | Context::get('output')->writeln('No modification found: '.$e->getMessage().''); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Liip/RMT/Prerequisite/TestsCheck.php: -------------------------------------------------------------------------------- 1 | options = array_merge(array( 28 | 'command' => 'phpunit --stop-on-failure', 29 | 'expected_exit_code' => 0, 30 | ), $options); 31 | } 32 | 33 | public function execute() 34 | { 35 | // Handle the skip option 36 | if (Context::get('information-collector')->getValueFor(self::SKIP_OPTION)) { 37 | Context::get('output')->writeln('tests skipped'); 38 | 39 | return; 40 | } 41 | 42 | // Run the tests and live output with the standard output class 43 | $timeout = $this->options['timeout'] ?? null; 44 | $process = $this->executeCommandInProcess($this->options['command'], $timeout); 45 | 46 | // Break up if the result is not good 47 | if ($process->getExitCode() !== $this->options['expected_exit_code']) { 48 | throw new \Exception('Tests fails (you can force a release with option --'.self::SKIP_OPTION.')'); 49 | } 50 | } 51 | 52 | public function getInformationRequests() 53 | { 54 | return array( 55 | new InformationRequest(self::SKIP_OPTION, array( 56 | 'description' => 'Do not run the tests before the release', 57 | 'type' => 'confirmation', 58 | 'interactive' => false, 59 | )), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Liip/RMT/Prerequisite/WorkingCopyCheck.php: -------------------------------------------------------------------------------- 1 | false), $options)); 35 | } 36 | 37 | public function getTitle() 38 | { 39 | return 'Check that your working copy is clean'; 40 | } 41 | 42 | public function execute() 43 | { 44 | // Allow to be skipped when explicitly activated from the config 45 | if (Context::get('information-collector')->getValueFor($this->ignoreCheckOptionName)) { 46 | if ($this->options['allow-ignore']) { 47 | Context::get('output')->writeln('requested to be ignored'); 48 | return; 49 | } 50 | 51 | throw new \Exception( 52 | 'The option "' . $this->ignoreCheckOptionName . '" only works if the "allow-ignore" configuration ' . 53 | 'key is set to true.' 54 | ); 55 | } 56 | 57 | $modCount = count(Context::get('vcs')->getLocalModifications()); 58 | if ($modCount > 0) { 59 | throw new \Exception( 60 | 'Your working directory contains ' . $modCount . ' local modification' . ($modCount > 1 ? 's' : '') . 61 | '. Use the --' . $this->ignoreCheckOptionName . ' option (along with the "allow-ignore" ' . 62 | 'configuration key set to true) to bypass this check.' . "\n" . 'WARNING, if your release task ' . 63 | 'include a commit action, the pending changes are going to be included in the release.', 64 | self::EXCEPTION_CODE 65 | ); 66 | } 67 | 68 | $this->confirmSuccess(); 69 | } 70 | 71 | public function getInformationRequests() 72 | { 73 | return array( 74 | new InformationRequest($this->ignoreCheckOptionName, array( 75 | 'description' => 'Do not process the check for a clean VCS working copy (if "allow-ignore" ' . 76 | 'configuration key is set to true)', 77 | 'type' => 'confirmation', 78 | 'interactive' => false, 79 | )) 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Liip/RMT/VCS/BaseVCS.php: -------------------------------------------------------------------------------- 1 | options = $options; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Liip/RMT/VCS/Git.php: -------------------------------------------------------------------------------- 1 | executeGitCommand("log --oneline $tag..HEAD $color $noMergeCommits"); 24 | } 25 | 26 | public function getModifiedFilesSince($tag) 27 | { 28 | $data = $this->executeGitCommand("diff --name-status $tag..HEAD"); 29 | $files = array(); 30 | foreach ($data as $d) { 31 | $parts = explode("\t", $d); 32 | $files[$parts[1]] = $parts[0]; 33 | } 34 | 35 | return $files; 36 | } 37 | 38 | public function getLocalModifications() 39 | { 40 | return $this->executeGitCommand('status -s'); 41 | } 42 | 43 | public function getTags() 44 | { 45 | return $this->executeGitCommand('tag'); 46 | } 47 | 48 | public function createTag($tagName) 49 | { 50 | // this requires git and gpg configured 51 | $signOption = (isset($this->options['sign-tag']) && $this->options['sign-tag']) ? '-s' : ''; 52 | 53 | return $this->executeGitCommand("tag $signOption $tagName -m $tagName"); 54 | } 55 | 56 | public function publishTag($tagName, $remote = null) 57 | { 58 | $remote = $remote == null ? 'origin' : $remote; 59 | $this->executeGitCommand("push $remote $tagName"); 60 | } 61 | 62 | public function publishChanges($remote = null) 63 | { 64 | $remote = $remote === null ? 'origin' : $remote; 65 | $this->executeGitCommand("push $remote ".$this->getCurrentBranch()); 66 | } 67 | 68 | public function saveWorkingCopy($commitMsg = '') 69 | { 70 | $this->executeGitCommand('add --all'); 71 | 72 | // this requires git and gpg configured 73 | $signOption = (isset($this->options['sign-commit']) && $this->options['sign-commit']) ? '-S' : ''; 74 | 75 | $this->executeGitCommand("commit $signOption -m \"$commitMsg\""); 76 | } 77 | 78 | public function getCurrentBranch() 79 | { 80 | $branches = $this->executeGitCommand('branch'); 81 | foreach ($branches as $branch) { 82 | if (strpos($branch, '* ') === 0 && !preg_match('/^\*\s\(.*\)$/', $branch)) { 83 | return substr($branch, 2); 84 | } 85 | } 86 | throw new \Liip\RMT\Exception('Not currently on any branch'); 87 | } 88 | 89 | protected function executeGitCommand($cmd) 90 | { 91 | // Avoid using some commands in dry mode 92 | if ($this->dryRun) { 93 | if ($cmd !== 'tag') { 94 | $cmdWords = explode(' ', $cmd); 95 | if (in_array($cmdWords[0], array('tag', 'push', 'add', 'commit'))) { 96 | return; 97 | } 98 | } 99 | } 100 | 101 | // Execute 102 | $cmd = 'git ' . $cmd; 103 | exec($cmd, $result, $exitCode); 104 | if ($exitCode !== 0) { 105 | throw new \Liip\RMT\Exception('Error while executing git command: ' . $cmd . "\n" . implode("\n", $result)); 106 | } 107 | 108 | return $result; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Liip/RMT/VCS/Hg.php: -------------------------------------------------------------------------------- 1 | executeHgCommand("log --template '{node|short} {desc}\n' -r tip:$tag $noMergeCommits"); 22 | array_pop($modifications); // remove the last commit since it is the one described by the tag 23 | 24 | return $modifications; 25 | } 26 | 27 | public function getModifiedFilesSince($tag) 28 | { 29 | $data = $this->executeHgCommand("status --rev $tag:tip"); 30 | $files = array(); 31 | foreach ($data as $d) { 32 | $parts = explode(' ', $d); 33 | $files[$parts[1]] = $parts[0]; 34 | } 35 | 36 | return $files; 37 | } 38 | 39 | public function getLocalModifications() 40 | { 41 | return $this->executeHgCommand('status'); 42 | } 43 | 44 | public function getTags() 45 | { 46 | $tags = $this->executeHgCommand('tags'); 47 | $tags = array_map(function ($t) { 48 | $parts = explode(' ', $t); 49 | 50 | return $parts[0]; 51 | }, $tags); 52 | 53 | return $tags; 54 | } 55 | 56 | public function createTag($tagName) 57 | { 58 | return $this->executeHgCommand("tag $tagName"); 59 | } 60 | 61 | public function publishTag($tagName, $remote = null) 62 | { 63 | // nothing to do, tags are published with other changes 64 | } 65 | 66 | public function publishChanges($remote = null) 67 | { 68 | $remote = $remote === null ? 'default' : $remote; 69 | $this->executeHgCommand("push $remote"); 70 | } 71 | 72 | public function saveWorkingCopy($commitMsg = '') 73 | { 74 | $this->executeHgCommand('addremove'); 75 | $this->executeHgCommand("commit -m \"$commitMsg\""); 76 | } 77 | 78 | public function getCurrentBranch() 79 | { 80 | $data = $this->executeHgCommand('branch'); 81 | 82 | return $data[0]; 83 | } 84 | 85 | protected function executeHgCommand($cmd) 86 | { 87 | if ($this->dryRun) { 88 | $binary = 'hg --dry-run '; 89 | } else { 90 | $binary = 'hg '; 91 | } 92 | 93 | // Execute 94 | $cmd = $binary.$cmd; 95 | exec($cmd, $result, $exitCode); 96 | 97 | if ($exitCode !== 0) { 98 | throw new \Liip\RMT\Exception('Error while executing hg command: '.$cmd); 99 | } 100 | 101 | return $result; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Liip/RMT/VCS/VCSInterface.php: -------------------------------------------------------------------------------- 1 | options = $options; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | * 36 | * @throws \InvalidArgumentException 37 | */ 38 | public function generateNextVersion($currentVersion) 39 | { 40 | $type = $this->options['type'] ?? Context::get('information-collector')->getValueFor('type'); 41 | 42 | $label = 'none'; 43 | if (isset($this->options['allow-label']) && $this->options['allow-label']) { 44 | $label = $this->options['label'] ?? Context::get('information-collector')->getValueFor('label'); 45 | } 46 | 47 | // Type validation 48 | $validTypes = array('patch', 'minor', 'major'); 49 | if (!in_array($type, $validTypes)) { 50 | throw new \InvalidArgumentException( 51 | 'The option [type] must be one of: {'.implode(', ', $validTypes)."}, \"$type\" given" 52 | ); 53 | } 54 | 55 | if (!preg_match('#^'.$this->getValidationRegex().'$#', $currentVersion)) { 56 | throw new \Exception('Current version format is invalid (' . $currentVersion . '). It should be major.minor.patch'); 57 | } 58 | 59 | $matches = null; 60 | preg_match('$(?:(\d+\.\d+\.\d+)(?:(-)([a-zA-Z]+)(\d+)?)?)$', $currentVersion, $matches); 61 | // if last version is with label 62 | if (count($matches) > 3) { 63 | list($major, $minor, $patch) = explode('.', $currentVersion); 64 | $patch = substr($patch, 0, strpos($patch, '-')); 65 | 66 | if ($label != 'none') { 67 | // increment label 68 | if (array_key_exists(3, $matches)) { 69 | $oldLabel = $matches[3]; 70 | $labelVersion = 2; 71 | 72 | // if label is new clear version 73 | if ($label !== $oldLabel) { 74 | $labelVersion = false; 75 | } elseif (array_key_exists(4, $matches)) { 76 | // if version exists increment it 77 | $labelVersion = intval($matches[4]) + 1; 78 | } 79 | } 80 | 81 | return implode('.', array($major, $minor, $patch)).'-'.$label.$labelVersion; 82 | } 83 | 84 | return implode('.', array($major, $minor, $patch)); 85 | } 86 | 87 | list($major, $minor, $patch) = explode('.', $currentVersion); 88 | // Increment 89 | switch ($type) { 90 | case 'major': 91 | $major += 1; 92 | $patch = $minor = 0; 93 | break; 94 | case 'minor': 95 | $minor += 1; 96 | $patch = 0; 97 | break; 98 | default: 99 | $patch += 1; 100 | break; 101 | } 102 | 103 | // new label 104 | if ($label != 'none') { 105 | return implode('.', array($major, $minor, $patch)).'-'.$label; 106 | } 107 | 108 | return implode('.', array($major, $minor, $patch)); 109 | } 110 | 111 | public function getInformationRequests() 112 | { 113 | $ir = array(); 114 | 115 | // Ask the type if it's not forced 116 | if (!isset($this->options['type'])) { 117 | $ir[] = 'type'; 118 | } 119 | 120 | // Ask the label if it's allow and not forced 121 | if (isset($this->options['allow-label']) && $this->options['allow-label'] == true && !isset($this->options['label'])) { 122 | $ir[] = 'label'; 123 | } 124 | 125 | return $ir; 126 | } 127 | 128 | public function getValidationRegex() 129 | { 130 | return '(?:(\d+\.\d+\.\d+)(?:(-)([a-zA-Z]+)(\d+)?)?)'; 131 | } 132 | 133 | public function getInitialVersion() 134 | { 135 | return '0.0.0'; 136 | } 137 | 138 | public function compareTwoVersions($a, $b) 139 | { 140 | if (Comparator::equalTo($a, $b)) { 141 | return 0; 142 | } 143 | if (Comparator::greaterThan($a, $b)) { 144 | return 1; 145 | } 146 | 147 | return -1; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Liip/RMT/Version/Generator/SimpleGenerator.php: -------------------------------------------------------------------------------- 1 | changelogManager = new ChangelogManager( 35 | Context::getParam('project-root').'/' . $options['location'], 36 | $format 37 | ); 38 | } 39 | 40 | public function getCurrentVersion() 41 | { 42 | return $this->changelogManager->getCurrentVersion(); 43 | } 44 | 45 | public function save($versionNumber) 46 | { 47 | $comment = Context::get('information-collector')->getValueFor('comment'); 48 | $type = Context::get('information-collector')->getValueFor('type', null); 49 | $this->changelogManager->update($versionNumber, $comment, array('type' => $type)); 50 | } 51 | 52 | public function getInformationRequests() 53 | { 54 | return array('comment'); 55 | } 56 | 57 | public function init() 58 | { 59 | // TODO: Implement init() method. 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Liip/RMT/Version/Persister/PersisterInterface.php: -------------------------------------------------------------------------------- 1 | regex = $regex; 22 | $this->tagPrefix = $tagPrefix; 23 | } 24 | 25 | /** 26 | * Check if a tag is valid 27 | * 28 | * @param string $tag 29 | * 30 | * @return bool 31 | */ 32 | public function isValid($tag) 33 | { 34 | if (strlen($this->tagPrefix) > 0 && strpos($tag, $this->tagPrefix) !== 0) { 35 | return false; 36 | } 37 | 38 | return preg_match('/^' . $this->regex . '$/', substr($tag, strlen($this->tagPrefix))) == 1; 39 | } 40 | 41 | /** 42 | * Remove all invalid tags from a list 43 | * 44 | * @param array $tags 45 | * 46 | * @return array 47 | */ 48 | public function filtrateList($tags) 49 | { 50 | $validTags = array(); 51 | foreach ($tags as $tag) { 52 | if ($this->isValid($tag)) { 53 | $validTags[] = $tag; 54 | } 55 | } 56 | 57 | return $validTags; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Liip/RMT/Version/Persister/VcsTagPersister.php: -------------------------------------------------------------------------------- 1 | options = $options; 25 | $this->vcs = Context::get('vcs'); 26 | $this->versionRegex = Context::get('version-generator')->getValidationRegex(); 27 | if (isset($options['tag-pattern'])) { 28 | $this->versionRegex = $options['tag-pattern']; 29 | } 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function getCurrentVersion() 36 | { 37 | $tags = $this->getValidVersionTags($this->versionRegex); 38 | if (count($tags) === 0) { 39 | throw new \Liip\RMT\Exception\NoReleaseFoundException('No VCS tag matching the regex [' . $this->getTagPrefix() . $this->versionRegex . ']'); 40 | } 41 | 42 | // Extract versions from tags and sort them 43 | $versions = $this->getVersionFromTags($tags); 44 | usort($versions, array(Context::get('version-generator'), 'compareTwoVersions')); 45 | 46 | return array_pop($versions); 47 | } 48 | 49 | public function save($versionNumber) 50 | { 51 | $tagName = $this->getTagFromVersion($versionNumber); 52 | Context::get('output')->writeln("Creation of a new VCS tag [$tagName]"); 53 | $this->vcs->createTag($tagName); 54 | } 55 | 56 | public function init() 57 | { 58 | } 59 | 60 | public function getInformationRequests() 61 | { 62 | return array(); 63 | } 64 | 65 | public function getTagPrefix() 66 | { 67 | return $this->generatePrefix(isset($this->options['tag-prefix']) ? $this->options['tag-prefix'] : ''); 68 | } 69 | 70 | public function getTagFromVersion($versionName) 71 | { 72 | return $this->getTagPrefix().$versionName; 73 | } 74 | 75 | public function getVersionFromTag($tagName) 76 | { 77 | return substr($tagName, strlen($this->getTagPrefix())); 78 | } 79 | 80 | public function getVersionFromTags($tags) 81 | { 82 | $versions = array(); 83 | foreach ($tags as $tag) { 84 | $versions[] = $this->getVersionFromTag($tag); 85 | } 86 | 87 | return $versions; 88 | } 89 | 90 | public function getCurrentVersionTag() 91 | { 92 | return $this->getTagFromVersion($this->getCurrentVersion()); 93 | } 94 | 95 | /** 96 | * Return all tags matching the versionRegex and prefix 97 | * 98 | * @param string $versionRegex 99 | * 100 | * @return array 101 | */ 102 | public function getValidVersionTags($versionRegex) 103 | { 104 | $validator = new TagValidator($versionRegex, $this->getTagPrefix()); 105 | 106 | return $validator->filtrateList($this->vcs->getTags()); 107 | } 108 | 109 | protected function generatePrefix($userTag) 110 | { 111 | preg_match_all('/\{([^\}]*)\}/', $userTag, $placeHolders); 112 | foreach ($placeHolders[1] as $pos => $placeHolder) { 113 | if ($placeHolder == 'branch-name') { 114 | $replacement = $this->vcs->getCurrentBranch(); 115 | } elseif ($placeHolder == 'date') { 116 | $replacement = date('Y-m-d'); 117 | } else { 118 | throw new \Liip\RMT\Exception("There is no rules to process the prefix placeholder [$placeHolder]"); 119 | } 120 | $userTag = str_replace($placeHolders[0][$pos], $replacement, $userTag); 121 | } 122 | 123 | return $userTag; 124 | } 125 | } 126 | --------------------------------------------------------------------------------