├── .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 | [](https://github.com/liip/rmt/actions/workflows/ci.yml)
5 | [](https://packagist.org/packages/liip/RMT)
6 | [](https://packagist.org/packages/liip/RMT)
7 | [](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 | 
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 |
--------------------------------------------------------------------------------