├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bc-break.md
│ ├── bug-report.md
│ ├── feature-request.md
│ └── support-question.md
├── PULL_REQUEST_TEMPLATE.md
├── SUPPORT.md
└── workflows
│ ├── buildAndDeployPhar.yml
│ └── continuous-integration.yml
├── CHANGELOG.md
├── KeepAChangelogSigning.asc.gpg
├── LICENSE.md
├── README.md
├── bin
├── keep-a-changelog
└── keep-a-changelog.dist
├── box.json.dist
├── captainhook.json
├── composer.json
├── scoper.inc.php
└── src
├── Bump
├── BumpChangelogVersionEvent.php
├── BumpChangelogVersionListener.php
├── BumpCommand.php
├── BumpToVersionCommand.php
└── ChangelogBump.php
├── Changelog
├── CreateNewChangelogEvent.php
├── CreateNewChangelogListener.php
├── EditChangelogLinksEvent.php
├── EditChangelogLinksListener.php
├── EditLinksCommand.php
├── FindChangelogLinksListener.php
└── NewCommand.php
├── Common
├── AbstractEvent.php
├── ArrayMergeRecursiveTrait.php
├── ChangelogAwareEventInterface.php
├── ChangelogEditSpawnerTrait.php
├── ChangelogEditor.php
├── ChangelogEntry.php
├── ChangelogEntryAwareEventInterface.php
├── ChangelogEntryDiscoveryTrait.php
├── ChangelogFormatter.php
├── ChangelogParser.php
├── ChangelogProviderTrait.php
├── CommonOptionsTrait.php
├── CreateMilestoneOptionsTrait.php
├── DiscoverChangelogEntryListener.php
├── DiscoverEditorListener.php
├── EditSpawnerTrait.php
├── Editor.php
├── EditorAwareEventInterface.php
├── EditorProviderTrait.php
├── EventInterface.php
├── FormatChangelogListener.php
├── IOInterface.php
├── IOTrait.php
├── IniReadWriteTrait.php
├── IsChangelogReadableListener.php
├── ParseChangelogListener.php
├── ValidateVersionListener.php
├── VersionAwareEventInterface.php
└── VersionValidationTrait.php
├── Config.php
├── Config
├── AbstractConfigListener.php
├── AbstractDiscoverPackageFromFileListener.php
├── CommonConfigOptionsTrait.php
├── ConfigDiscovery.php
├── ConfigListener.php
├── DiscoverPackageFromComposerListener.php
├── DiscoverPackageFromGitRemoteListener.php
├── DiscoverPackageFromNpmPackageListener.php
├── DiscoverRemoteFromGitRemotesListener.php
├── Exception
│ ├── ExceptionInterface.php
│ └── InvalidProviderException.php
├── LocateGlobalConfigTrait.php
├── PackageNameDiscovery.php
├── PromptForGitRemoteListener.php
├── RemoteNameDiscovery.php
├── RetrieveGlobalConfigListener.php
├── RetrieveInputOptionsListener.php
└── RetrieveLocalConfigListener.php
├── ConfigCommand
├── AbstractCreateConfigListener.php
├── AbstractEditConfigListener.php
├── AbstractRemoveConfigListener.php
├── AbstractShowConfigListener.php
├── CreateCommand.php
├── CreateConfigEvent.php
├── CreateGlobalConfigListener.php
├── CreateLocalConfigListener.php
├── EditCommand.php
├── EditConfigEvent.php
├── EditGlobalConfigListener.php
├── EditLocalConfigListener.php
├── MaskProviderTokensTrait.php
├── RemoveCommand.php
├── RemoveConfigEvent.php
├── RemoveGlobalConfigListener.php
├── RemoveLocalConfigListener.php
├── ShowCommand.php
├── ShowConfigEvent.php
├── ShowGlobalConfigListener.php
├── ShowLocalConfigListener.php
├── ShowMergedConfigListener.php
├── VerifyEditOptionsListener.php
└── VerifyRemoveOptionsListener.php
├── Entry
├── AbstractPrependLinkListener.php
├── AddChangelogEntryEvent.php
├── AddChangelogEntryListener.php
├── ConfigListener.php
├── EntryCommand.php
├── EntryTypes.php
├── InjectionIndex.php
├── IsEntryArgumentEmptyListener.php
├── NotifyPreparingEntryListener.php
├── PrependIssueLinkListener.php
└── PrependPatchLinkListener.php
├── EventDispatcher.php
├── Exception
├── ChangelogEntriesNotFoundException.php
├── ChangelogMissingDateException.php
├── ChangelogNotFoundException.php
├── ExceptionInterface.php
├── FileNotReadableException.php
├── IniParsingFailedException.php
├── InvalidBumpTypeException.php
├── InvalidChangelogBumpCriteriaException.php
├── InvalidChangelogFormatException.php
├── InvalidIniSectionDataException.php
├── InvalidIniValueException.php
├── InvalidNoteTypeException.php
├── InvalidProviderException.php
└── MissingTagException.php
├── ListenerProvider.php
├── Milestone
├── AbstractMilestoneProviderEvent.php
├── CloseCommand.php
├── CloseMilestoneEvent.php
├── CloseMilestoneListener.php
├── CommandConfigListener.php
├── CreateCommand.php
├── CreateMilestoneEvent.php
├── CreateMilestoneListener.php
├── ListCommand.php
├── ListMilestonesEvent.php
├── ListMilestonesListener.php
└── VerifyProviderListener.php
├── Provider
├── Exception
│ ├── ExceptionInterface.php
│ ├── InvalidPackageNameException.php
│ ├── InvalidUrlException.php
│ ├── MissingPackageNameException.php
│ ├── MissingTagException.php
│ └── MissingTokenException.php
├── GitHub.php
├── GitLab.php
├── Milestone.php
├── MilestoneAwareProviderInterface.php
├── ProviderInterface.php
├── ProviderList.php
└── ProviderSpec.php
├── Unreleased
├── PromoteCommand.php
├── PromoteEvent.php
├── PromoteUnreleasedToNewVersionListener.php
└── ValidateDateToUseListener.php
└── Version
├── CheckTreeForChangesListener.php
├── CreateReleaseNameListener.php
├── DiscoverVersionEventTrait.php
├── DiscoverVersionListener.php
├── DiscoverableVersionEventInterface.php
├── EditChangelogVersionEvent.php
├── EditChangelogVersionListener.php
├── EditCommand.php
├── ListCommand.php
├── ListVersionsEvent.php
├── ListVersionsListener.php
├── PromptForRemovalConfirmationListener.php
├── PushReleaseToProviderListener.php
├── PushTagToRemoteListener.php
├── ReadyCommand.php
├── ReadyLatestChangelogEvent.php
├── ReleaseCommand.php
├── ReleaseCommandConfigListener.php
├── ReleaseEvent.php
├── RemoveChangelogVersionEvent.php
├── RemoveChangelogVersionListener.php
├── RemoveCommand.php
├── SetDateForChangelogReleaseListener.php
├── ShowCommand.php
├── ShowVersionEvent.php
├── ShowVersionListener.php
├── TagCommand.php
├── TagCommandConfigListener.php
├── TagReleaseEvent.php
├── TagReleaseListener.php
├── ValidateVersionToUseListener.php
├── VerifyProviderCanReleaseListener.php
├── VerifyTagExistsListener.php
└── VerifyVersionHasReleaseDateListener.php
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | This project adheres to [The Code Manifesto](http://codemanifesto.com)
4 | as its guidelines for contributor interactions.
5 |
6 | ## The Code Manifesto
7 |
8 | We want to work in an ecosystem that empowers developers to reach their
9 | potential — one that encourages growth and effective collaboration. A space that
10 | is safe for all.
11 |
12 | A space such as this benefits everyone that participates in it. It encourages
13 | new developers to enter our field. It is through discussion and collaboration
14 | that we grow, and through growth that we improve.
15 |
16 | In the effort to create such a place, we hold to these values:
17 |
18 | 1. **Discrimination limits us.** This includes discrimination on the basis of
19 | race, gender, sexual orientation, gender identity, age, nationality, technology
20 | and any other arbitrary exclusion of a group of people.
21 | 2. **Boundaries honor us.** Your comfort levels are not everyone’s comfort
22 | levels. Remember that, and if brought to your attention, heed it.
23 | 3. **We are our biggest assets.** None of us were born masters of our trade.
24 | Each of us has been helped along the way. Return that favor, when and where
25 | you can.
26 | 4. **We are resources for the future.** As an extension of #3, share what you
27 | know. Make yourself a resource to help those that come after you.
28 | 5. **Respect defines us.** Treat others as you wish to be treated. Make your
29 | discussions, criticisms and debates from a position of respectfulness. Ask
30 | yourself, is it true? Is it necessary? Is it constructive? Anything less is
31 | unacceptable.
32 | 6. **Reactions require grace.** Angry responses are valid, but abusive language
33 | and vindictive actions are toxic. When something happens that offends you,
34 | handle it assertively, but be respectful. Escalate reasonably, and try to
35 | allow the offender an opportunity to explain themselves, and possibly correct
36 | the issue.
37 | 7. **Opinions are just that: opinions.** Each and every one of us, due to our
38 | background and upbringing, have varying opinions. The fact of the matter, is
39 | that is perfectly acceptable. Remember this: if you respect your own
40 | opinions, you should respect the opinions of others.
41 | 8. **To err is human.** You might not intend it, but mistakes do happen and
42 | contribute to build experience. Tolerate honest mistakes, and don't hesitate
43 | to apologize if you make one yourself.
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bc-break.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: BC Break
3 | about: Have you encountered an issue during an upgrade?
4 | title: ''
5 | labels: BC Break
6 | assignees: ''
7 |
8 | ---
9 |
10 |
13 |
14 | ### BC Break Report
15 |
16 |
17 |
18 | | Q | A
19 | |------------ | ------
20 | | BC Break | yes
21 | | Version | x.y.z
22 |
23 | #### Summary
24 |
25 |
26 |
27 | #### Previous behaviour
28 |
29 |
30 |
31 | #### Current behavior
32 |
33 |
34 |
35 | #### How to reproduce
36 |
37 |
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Something is broken?
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Bug Report
11 |
12 |
13 |
14 | | Q | A
15 | |------------ | ------
16 | | BC Break | yes/no
17 | | Version | x.y.z
18 |
19 | #### Summary
20 |
21 |
22 |
23 | #### Current behaviour
24 |
25 |
26 |
27 | #### How to reproduce
28 |
29 |
33 |
34 | #### Expected behaviour
35 |
36 |
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: You have a neat idea that should be implemented?
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Feature Request
11 |
12 |
13 |
14 | | Q | A
15 | |------------ | ------
16 | | New Feature | yes
17 | | BC Break | yes/no
18 |
19 | #### Summary
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support-question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Support Question
3 | about: Have a problem that you can't figure out?
4 | title: ''
5 | labels: question
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 | | Q | A
13 | |------------ | -----
14 | | Version | x.y.z
15 |
16 | ### Support Question
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
32 |
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Getting Support
2 |
3 | This project offers the following support channels:
4 |
5 | - To report issues, use this repository's
6 | [issue tracker](https://github.com/phly/keep-a-changelog/issues/new)
7 |
8 | Feel free to ask questions via the issue tracker; however, please include the
9 | veribiage `[QUESTION]` in the issue title to ensure we resolve it as such.
10 |
11 | When reporting an issue, please include the following details:
12 |
13 | - A narrative description of what you are trying to accomplish.
14 | - The minimum code necessary to reproduce the issue.
15 | - The expected results of exercising that code.
16 | - The actual results received.
17 |
18 | We may ask for additional details: what version of the library you are using,
19 | and what PHP version was used to reproduce the issue.
20 |
21 | You may also submit a failing test case as a pull request.
22 |
--------------------------------------------------------------------------------
/.github/workflows/buildAndDeployPhar.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions
2 |
3 | name: Build and deploy PHAR
4 | on:
5 | push:
6 | tags:
7 | - "*"
8 | jobs:
9 | build:
10 | name: Build PHAR-File
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@main
15 |
16 | - name: Setup PHP
17 | uses: shivammathur/setup-php@v2
18 | with:
19 | php-version: 8.3
20 | extensions: mbstring, intl, readline
21 | tools: composer:v2
22 |
23 | - name: Prepare binary with version
24 | run: |
25 | VERSION=$(echo ${GITHUB_REF} | cut -d / -f 3)
26 | cat bin/keep-a-changelog.dist | sed -e "s/%VERSION%/${VERSION}/" > bin/keep-a-changelog
27 |
28 | - name: Build Phar
29 | run: composer buildphar
30 |
31 | - name: Sign Phar
32 | env:
33 | DECRYPT_KEY: ${{ secrets.DECRYPT_KEY }}
34 | SIGN_KEY: ${{ secrets.SIGN_KEY }}
35 | run: |
36 | export GPG_TTY=$(tty)
37 | echo $DECRYPT_KEY
38 | echo Decrypting key
39 | gpg --batch --yes --passphrase $DECRYPT_KEY KeepAChangelogSigning.asc.gpg && gpg --batch --yes --import KeepAChangelogSigning.asc
40 | echo Signing Artifact
41 | gpg --batch --yes --pinentry-mode loopback --passphrase $SIGN_KEY -u 9A2577FF9A688FAF --armor --detach-sig build/keep-a-changelog.phar
42 |
43 | - name: Prepare Release-Info
44 | run: |
45 | ./bin/keep-a-changelog version:show $(echo $GITHUB_REF | cut -d / -f 3) > release.note
46 |
47 | - name: Create release
48 | uses: ncipollo/release-action@v1
49 | with:
50 | artifacts: "build/*"
51 | bodyfile: release.note
52 | name: "keep-a-changelog ${{ github.event.release.tag_name }}"
53 | token: ${{ secrets.GITHUB_TOKEN }}
54 |
--------------------------------------------------------------------------------
/.github/workflows/continuous-integration.yml:
--------------------------------------------------------------------------------
1 | name: "Continuous Integration"
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - '[0-9]+.[0-9]+.x'
8 | - 'refs/pull/*'
9 | tags:
10 |
11 | jobs:
12 | matrix:
13 | name: Generate job matrix
14 | runs-on: ubuntu-latest
15 | outputs:
16 | matrix: ${{ steps.matrix.outputs.matrix }}
17 | steps:
18 | - name: Gather CI configuration
19 | id: matrix
20 | uses: laminas/laminas-ci-matrix-action@v1
21 |
22 | qa:
23 | name: QA Checks
24 | needs: [matrix]
25 | runs-on: ${{ matrix.operatingSystem }}
26 | strategy:
27 | fail-fast: false
28 | matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
29 | steps:
30 | - name: ${{ matrix.name }}
31 | uses: laminas/laminas-continuous-integration-action@v1
32 | with:
33 | job: ${{ matrix.job }}
34 |
--------------------------------------------------------------------------------
/KeepAChangelogSigning.asc.gpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phly/keep-a-changelog/ab87917662e62e852610853416778c7212db1dea/KeepAChangelogSigning.asc.gpg
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018, Matthew Weier O'Phinney
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | - Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | - Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/box.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "output": "build/keep-a-changelog.phar",
3 | "compactors": ["KevinGH\\Box\\Compactor\\PhpScoper"],
4 | "php-scoper": "scoper.inc.php"
5 | }
6 |
--------------------------------------------------------------------------------
/captainhook.json:
--------------------------------------------------------------------------------
1 | {
2 | "commit-msg": {
3 | "enabled": true,
4 | "actions": [
5 | {
6 | "action": "\\CaptainHook\\App\\Hook\\Message\\Action\\Regex",
7 | "options": {
8 | "regex": "#^(Merge branch|(?:feat|fix|docs|qa|refactor):) \\S+.*?(\n\n.*)?#s",
9 | "error": "Commit message must have a subject line of the format '(feat|fix|docs|qa|refactor): {description}' or 'Merge branch ...'; any additional content must have one empty line following.",
10 | "success": "Expected commit message format detected!"
11 | },
12 | "conditions": []
13 | }
14 | ]
15 | },
16 | "pre-push": {
17 | "enabled": true,
18 | "actions": [
19 | {
20 | "action": "\\CaptainHook\\App\\Hook\\PHP\\Action\\Linting",
21 | "options": [],
22 | "conditions": []
23 | },
24 | {
25 | "action": "composer check",
26 | "options": [],
27 | "conditions": []
28 | }
29 | ]
30 | },
31 | "pre-commit": {
32 | "enabled": false,
33 | "actions": [
34 | ]
35 | },
36 | "prepare-commit-msg": {
37 | "enabled": false,
38 | "actions": []
39 | },
40 | "post-commit": {
41 | "enabled": false,
42 | "actions": []
43 | },
44 | "post-merge": {
45 | "enabled": false,
46 | "actions": []
47 | },
48 | "post-checkout": {
49 | "enabled": true,
50 | "actions": [
51 | {
52 | "action": "composer install",
53 | "options": [],
54 | "conditions": []
55 | }
56 | ]
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phly/keep-a-changelog",
3 | "description": "Tag and release packages on GitHub using Keep A Changelog; add new version entries to your changelog.",
4 | "license": "BSD-2-Clause",
5 | "keywords": [
6 | "keepachangelog"
7 | ],
8 | "support": {
9 | "issues": "https://github.com/phly/keep-a-changelog/issues",
10 | "source": "https://github.com/phly/keep-a-changelog",
11 | "rss": "https://github.com/phly/keep-a-changelog/releases.atom"
12 | },
13 | "require": {
14 | "php": "^8.1",
15 | "composer-runtime-api": "^2.0",
16 | "knplabs/github-api": "^3.4",
17 | "laminas/laminas-diactoros": "^3.3",
18 | "m4tthumphrey/php-gitlab-api": "^11.0",
19 | "php-http/guzzle7-adapter": "^1.0",
20 | "psr/event-dispatcher": "^1.0",
21 | "symfony/console": "^5.2.1 || ^6.0 || ^7.0"
22 | },
23 | "require-dev": {
24 | "captainhook/captainhook": "^5.18.3",
25 | "captainhook/plugin-composer": "^5.3.3",
26 | "laminas/laminas-coding-standard": "^2.5.0",
27 | "phpunit/phpunit": "^9.6.21"
28 | },
29 | "replace": {
30 | "laminas/laminas-zendframework-bridge": "*"
31 | },
32 | "autoload": {
33 | "psr-4": {
34 | "Phly\\KeepAChangelog\\": "src/"
35 | }
36 | },
37 | "autoload-dev": {
38 | "psr-4": {
39 | "PhlyTest\\KeepAChangelog\\": "test/"
40 | }
41 | },
42 | "config": {
43 | "sort-packages": true,
44 | "platform": {
45 | "php": "8.1.99"
46 | },
47 | "allow-plugins": {
48 | "captainhook/plugin-composer": true,
49 | "dealerdirect/phpcodesniffer-composer-installer": true,
50 | "php-http/discovery": false
51 | }
52 | },
53 | "bin": [
54 | "bin/keep-a-changelog"
55 | ],
56 | "scripts": {
57 | "check": [
58 | "@cs-check",
59 | "@test"
60 | ],
61 | "cs-check": "phpcs -p",
62 | "cs-fix": "phpcbf -p",
63 | "test": "phpunit --colors=always",
64 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml",
65 | "buildphar" : [
66 | "rm -rf vendor",
67 | "composer install --no-dev --prefer-dist",
68 | "curl -o box -L https://api.getlatestassets.com/github/humbug/box/box.phar",
69 | "chmod 755 box",
70 | "mkdir -p ./build",
71 | "chmod 777 ./build",
72 | "php -d phar.readonly=0 ./box compile"
73 | ]
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/scoper.inc.php:
--------------------------------------------------------------------------------
1 | [
7 | // Expose providers and provider interfaces
8 | 'Phly\KeepAChangelog\Provider\GitHub',
9 | 'Phly\KeepAChangelog\Provider\GitLab',
10 | 'Phly\KeepAChangelog\Provider\MilestoneAwareProviderInterface',
11 | 'Phly\KeepAChangelog\Provider\ProviderInterface',
12 | ],
13 | // Necessary for allowing polyfill classes
14 | 'expose-global-classes' => false,
15 | ];
16 |
--------------------------------------------------------------------------------
/src/Bump/BumpChangelogVersionEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
63 | $this->output = $output;
64 | $this->dispatcher = $dispatcher;
65 | $this->bumpMethod = $bumpMethod;
66 | $this->version = $version;
67 | }
68 |
69 | public function isPropagationStopped(): bool
70 | {
71 | return $this->failed;
72 | }
73 |
74 | public function bumpMethod(): ?string
75 | {
76 | return $this->bumpMethod;
77 | }
78 |
79 | public function version(): ?string
80 | {
81 | return $this->version;
82 | }
83 |
84 | public function bumpedChangelog(string $version): void
85 | {
86 | $this->version = $version;
87 | $this->output()->writeln(sprintf(
88 | 'Bumped changelog version to %s',
89 | $version
90 | ));
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Bump/BumpChangelogVersionListener.php:
--------------------------------------------------------------------------------
1 | config()->changelogFile());
16 | $version = $event->version() ?: $this->lookupLatestVersionInChangelog($bumper, $event->bumpMethod());
17 | $bumper->updateChangelog($version);
18 | $event->bumpedChangelog($version);
19 | }
20 |
21 | private function lookupLatestVersionInChangelog(ChangelogBump $bumper, string $method): string
22 | {
23 | $latest = $bumper->findLatestVersion();
24 | return $bumper->$method($latest);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Changelog/CreateNewChangelogEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
35 | $this->output = $output;
36 | $this->dispatcher = $dispatcher;
37 | $this->version = $initialVersion;
38 | $this->overwrite = $overwrite;
39 | }
40 |
41 | public function isPropagationStopped(): bool
42 | {
43 | return $this->failed;
44 | }
45 |
46 | public function overwrite(): bool
47 | {
48 | return $this->overwrite;
49 | }
50 |
51 | public function changelogExists(): void
52 | {
53 | $this->failed = true;
54 | $this->output->writeln(sprintf(
55 | 'Cannot create changelog file "%s"; file exists.',
56 | $this->config()->changelogFile()
57 | ));
58 | $this->output->writeln('If you want to overwrite the file, use the --overwrite|-o option');
59 | }
60 |
61 | public function createdChangelog(): void
62 | {
63 | $this->output->writeln(sprintf(
64 | 'Created new changelog in file "%s" using initial version "%s".',
65 | $this->config()->changelogFile(),
66 | $this->version
67 | ));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Changelog/CreateNewChangelogListener.php:
--------------------------------------------------------------------------------
1 | config()->changelogFile();
49 | if (! $event->overwrite() && file_exists($changelogFile)) {
50 | $event->changelogExists($changelogFile);
51 | return;
52 | }
53 |
54 | file_put_contents(
55 | $changelogFile,
56 | sprintf(self::TEMPLATE, $event->version())
57 | );
58 |
59 | $event->createdChangelog();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Changelog/EditChangelogLinksEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
46 | $this->output = $output;
47 | $this->dispatcher = $dispatcher;
48 | }
49 |
50 | public function isPropagationStopped(): bool
51 | {
52 | return $this->failed;
53 | }
54 |
55 | public function appendLinksToChangelogFile(): bool
56 | {
57 | return $this->appendLinksToChangelogFile;
58 | }
59 |
60 | public function links(): ?ChangelogEntry
61 | {
62 | return $this->links;
63 | }
64 |
65 | public function discoveredLinks(ChangelogEntry $links): void
66 | {
67 | $this->links = $links;
68 | }
69 |
70 | public function noLinksDiscovered(): void
71 | {
72 | $this->appendLinksToChangelogFile = true;
73 | }
74 |
75 | public function editComplete(string $changelogFile): void
76 | {
77 | $this->output->writeln(sprintf(
78 | 'Completed editing links for file %s',
79 | $changelogFile
80 | ));
81 | }
82 |
83 | public function editFailed(string $changelogFile): void
84 | {
85 | $this->failed = true;
86 | $this->output->writeln(sprintf(
87 | 'Editing links for file %s failed',
88 | $changelogFile
89 | ));
90 | $this->output->writeln('Review the output above for potential errors.');
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Changelog/EditChangelogLinksListener.php:
--------------------------------------------------------------------------------
1 | config()->changelogFile();
25 | $links = $event->links();
26 | $contents = $links instanceof ChangelogEntry ? $links->contents() : '';
27 | $tempFile = $this->createTempFileWithContents($contents);
28 |
29 | $status = $this->getEditor()->spawnEditor(
30 | $event->output(),
31 | $event->editor(),
32 | $tempFile
33 | );
34 |
35 | if (0 !== $status) {
36 | $this->unlinkTempFile($tempFile);
37 | $event->editFailed($changelog);
38 | return;
39 | }
40 |
41 | $linkContents = file_get_contents($tempFile);
42 | $editor = $this->getChangelogEditor();
43 |
44 | $links instanceof ChangelogEntry
45 | ? $editor->update($changelog, $linkContents, $links)
46 | : $editor->append($changelog, $linkContents);
47 |
48 | $this->unlinkTempFile($tempFile);
49 | $event->editComplete($changelog);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Changelog/EditLinksCommand.php:
--------------------------------------------------------------------------------
1 |
25 |
26 | (Indentation is for documentation purposes; omit it in actual files.)
27 |
28 | This command will spawn your $EDITOR with any discovered links, and then ensure
29 | they are written back to the file on completion of any edits.
30 | EOH;
31 |
32 | /** @var EventDispatcherInterface */
33 | private $dispatcher;
34 |
35 | public function __construct(EventDispatcherInterface $dispatcher, ?string $name = null)
36 | {
37 | $this->dispatcher = $dispatcher;
38 | parent::__construct($name);
39 | }
40 |
41 | protected function configure(): void
42 | {
43 | $this->setDescription(self::DESCRIPTION);
44 | $this->setHelp(self::HELP);
45 | }
46 |
47 | protected function execute(InputInterface $input, OutputInterface $output): int
48 | {
49 | return $this->dispatcher
50 | ->dispatch(new EditChangelogLinksEvent(
51 | $input,
52 | $output,
53 | $this->dispatcher
54 | ))
55 | ->failed()
56 | ? 1
57 | : 0;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Changelog/FindChangelogLinksListener.php:
--------------------------------------------------------------------------------
1 | config()->changelogFile();
18 | $links = (new ChangelogParser())->findLinks($changelog);
19 |
20 | if (null === $links->index) {
21 | $event->noLinksDiscovered();
22 | return;
23 | }
24 |
25 | $event->discoveredLinks($links);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Changelog/NewCommand.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
34 | parent::__construct($name);
35 | }
36 |
37 | protected function configure(): void
38 | {
39 | $this->setDescription(self::DESCRIPTION);
40 | $this->setHelp(self::HELP);
41 | $this->addOption(
42 | 'initial-version',
43 | '-i',
44 | InputOption::VALUE_REQUIRED,
45 | 'Initial version to provide in new changelog file; defaults to 0.1.0.'
46 | );
47 | $this->addOption(
48 | 'overwrite',
49 | '-o',
50 | InputOption::VALUE_NONE,
51 | 'Overwrite the changelog file, if exists'
52 | );
53 | }
54 |
55 | protected function execute(InputInterface $input, OutputInterface $output): int
56 | {
57 | return $this->dispatcher
58 | ->dispatch(new CreateNewChangelogEvent(
59 | $input,
60 | $output,
61 | $this->dispatcher,
62 | $input->getOption('initial-version') ?: '0.1.0',
63 | $input->getOption('overwrite') ?: false
64 | ))
65 | ->failed()
66 | ? 1
67 | : 0;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Common/AbstractEvent.php:
--------------------------------------------------------------------------------
1 | failed;
36 | }
37 |
38 | public function changelogFileIsUnreadable(string $changelogFile): void
39 | {
40 | $this->failed = true;
41 | $this->output()->writeln(sprintf(
42 | 'Changelog file "%s" is unreadable.',
43 | $changelogFile
44 | ));
45 | }
46 |
47 | public function configurationIncomplete(): void
48 | {
49 | $this->failed = true;
50 | }
51 |
52 | public function missingConfiguration(): bool
53 | {
54 | return null === $this->config;
55 | }
56 |
57 | /**
58 | * Update the event with the discovered configuration instance.
59 | */
60 | public function discoveredConfiguration(Config $config): void
61 | {
62 | $this->config = $config;
63 | }
64 |
65 | /**
66 | * Return the configuration instance, if available.
67 | */
68 | public function config(): ?Config
69 | {
70 | return $this->config;
71 | }
72 |
73 | /**
74 | * Configurable events should be passed the event dispatcher, so that the
75 | * configuration listener can dispatch its internal events in order to
76 | * aggregate configuration.
77 | */
78 | public function dispatcher(): EventDispatcherInterface
79 | {
80 | return $this->dispatcher;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Common/ArrayMergeRecursiveTrait.php:
--------------------------------------------------------------------------------
1 | $value) {
29 | if (! isset($a[$key]) && ! array_key_exists($key, $a)) {
30 | $a[$key] = $value;
31 | continue;
32 | }
33 |
34 | if (! $preserveNumericKeys && is_int($key)) {
35 | $a[] = $value;
36 | continue;
37 | }
38 |
39 | if (is_array($value) && is_array($a[$key])) {
40 | $a[$key] = $this->arrayMergeRecursive($a[$key], $value, $preserveNumericKeys);
41 | continue;
42 | }
43 |
44 | $a[$key] = $value;
45 | }
46 |
47 | return $a;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Common/ChangelogAwareEventInterface.php:
--------------------------------------------------------------------------------
1 | changelogEditor instanceof ChangelogEditor) {
22 | return $this->changelogEditor;
23 | }
24 |
25 | return new ChangelogEditor();
26 | }
27 |
28 | /**
29 | * Provide a ChangelogEditor instance to use.
30 | *
31 | * For testing purposes only.
32 | *
33 | * @internal
34 | *
35 | * @var null|ChangelogEditor
36 | */
37 | public $changelogEditor;
38 | }
39 |
--------------------------------------------------------------------------------
/src/Common/ChangelogEditor.php:
--------------------------------------------------------------------------------
1 | index, $entry->length, $replacement);
24 | file_put_contents($filename, implode('', $contents));
25 | }
26 |
27 | public function append(string $filename, string $contentsToAppend): void
28 | {
29 | $contents = file_get_contents($filename);
30 | file_put_contents(
31 | $filename,
32 | sprintf("%s\n%s", $contents, $contentsToAppend)
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Common/ChangelogEntry.php:
--------------------------------------------------------------------------------
1 | contents;
34 | case 'index':
35 | return $this->index;
36 | case 'length':
37 | return $this->length;
38 | default:
39 | throw new UnexpectedValueException(sprintf(
40 | 'The property "%s" does not exist for class "%s"',
41 | $name,
42 | static::class
43 | ));
44 | }
45 | }
46 |
47 | /**
48 | * @param mixed $value
49 | */
50 | public function __set(string $name, $value)
51 | {
52 | switch ($name) {
53 | case 'contents':
54 | $this->setContents($value);
55 | break;
56 | case 'index':
57 | $this->setIndex($value);
58 | break;
59 | case 'length':
60 | $this->setLength($value);
61 | break;
62 | default:
63 | throw new UnexpectedValueException(sprintf(
64 | 'The property "%s" does not exist for class "%s"',
65 | $name,
66 | static::class
67 | ));
68 | }
69 | }
70 |
71 | public function contents(): string
72 | {
73 | return $this->contents;
74 | }
75 |
76 | public function index(): int
77 | {
78 | return $this->index;
79 | }
80 |
81 | public function length(): int
82 | {
83 | return $this->length;
84 | }
85 |
86 | private function setContents(string $value): void
87 | {
88 | $this->contents = $value;
89 | }
90 |
91 | private function setIndex(?int $value): void
92 | {
93 | $this->index = $value ?: 0;
94 | }
95 |
96 | private function setLength(int $value): void
97 | {
98 | $this->length = $value;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Common/ChangelogEntryAwareEventInterface.php:
--------------------------------------------------------------------------------
1 | failed = true;
27 | $this->output->writeln(sprintf(
28 | 'Could not locate version %s in changelog file %s;'
29 | . ' please verify the version and/or changelog file.',
30 | $this->version ?: '"latest"',
31 | $changelogFile
32 | ));
33 | }
34 |
35 | public function discoveredChangelogEntry(ChangelogEntry $entry): void
36 | {
37 | $this->changelogEntry = $entry;
38 | }
39 |
40 | public function changelogEntry(): ?ChangelogEntry
41 | {
42 | return $this->changelogEntry;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Common/ChangelogFormatter.php:
--------------------------------------------------------------------------------
1 | Added|Changed|Deprecated|Removed|Fixed)/m',
22 | static function (array $matches) {
23 | return sprintf(
24 | "%s\n%s",
25 | $matches['heading'],
26 | str_repeat('-', strlen($matches['heading']))
27 | );
28 | },
29 | $changelog
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Common/ChangelogProviderTrait.php:
--------------------------------------------------------------------------------
1 | changelog;
26 | }
27 |
28 | public function updateChangelog(string $changelog): void
29 | {
30 | $this->changelog = $changelog;
31 | }
32 |
33 | public function errorParsingChangelog(string $changelogFile, Throwable $e)
34 | {
35 | $this->failed = true;
36 | $output = $this->output();
37 | $output->writeln(sprintf(
38 | 'An error occurred parsing the changelog file "%s" for the release "%s":',
39 | $changelogFile,
40 | $this->version()
41 | ));
42 | $output->writeln($e->getMessage());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Common/CommonOptionsTrait.php:
--------------------------------------------------------------------------------
1 | addOption(
19 | 'editor',
20 | '-e',
21 | InputOption::VALUE_REQUIRED,
22 | 'Provide the name of the editor program to use.'
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Common/CreateMilestoneOptionsTrait.php:
--------------------------------------------------------------------------------
1 | addOption(
26 | 'create-milestone',
27 | 'm',
28 | InputOption::VALUE_NONE,
29 | 'Create a milestone with your provider named after the new version'
30 | );
31 |
32 | $command->addOption(
33 | 'create-milestone-with-name',
34 | null,
35 | InputOption::VALUE_REQUIRED,
36 | 'Create a milestone with your provider using the provided name (instead of the version)'
37 | );
38 | }
39 |
40 | private function isMilestoneCreationRequested(InputInterface $input): bool
41 | {
42 | return $input->getOption('create-milestone')
43 | || $input->getOption('create-milestone-with-name');
44 | }
45 |
46 | private function getMilestoneName(InputInterface $input, string $default): string
47 | {
48 | return $input->getOption('create-milestone-with-name') ?: $default;
49 | }
50 |
51 | private function triggerCreateMilestoneEvent(
52 | string $name,
53 | OutputInterface $output,
54 | EventDispatcherInterface $dispatcher
55 | ): CreateMilestoneEvent {
56 | $input = new ArrayInput(
57 | ['title' => $name],
58 | new InputDefinition([
59 | new InputArgument('title', InputArgument::REQUIRED),
60 | new InputArgument('description', InputArgument::OPTIONAL, '', ''),
61 | ])
62 | );
63 |
64 | return $dispatcher
65 | ->dispatch(new CreateMilestoneEvent(
66 | $input,
67 | $output,
68 | $dispatcher
69 | ));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Common/DiscoverChangelogEntryListener.php:
--------------------------------------------------------------------------------
1 | config()->changelogFile();
31 | $version = $event->version();
32 | $contents = file($filename) ?: [];
33 | $entry = new ChangelogEntry();
34 | $boundaryRegex = '/^(?:## (?:\d+\.\d+\.\d+|\[\d+\.\d+\.\d+\]|unreleased|\[unreleased\])|\[.*?\]:\s*\S+)/i';
35 | $regex = $version
36 | ? sprintf(
37 | '/^## (?:%1$s|\[%1$s\])/i',
38 | preg_quote($version)
39 | )
40 | : $boundaryRegex;
41 |
42 | foreach ($contents as $index => $line) {
43 | if ($entry->index && preg_match($boundaryRegex, $line)) {
44 | break;
45 | }
46 |
47 | if (preg_match($regex, $line)) {
48 | $entry->contents = $line;
49 | $entry->index = $index;
50 | $entry->length = 1;
51 | continue;
52 | }
53 |
54 | if (! $entry->index) {
55 | continue;
56 | }
57 |
58 | $entry->contents .= $line;
59 | $entry->length += 1;
60 | }
61 |
62 | if ($entry->index === null) {
63 | $event->changelogEntryNotFound($filename, $version);
64 | return;
65 | }
66 |
67 | $event->discoveredChangelogEntry($entry);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Common/DiscoverEditorListener.php:
--------------------------------------------------------------------------------
1 | editor()) {
19 | // Passed as an argument; nothing to do
20 | return;
21 | }
22 |
23 | $editor = getenv('EDITOR');
24 |
25 | if ($editor) {
26 | $event->discoverEditor($editor);
27 | return;
28 | }
29 |
30 | $editor = isset($_SERVER['OS']) && false !== strpos($_SERVER['OS'], 'indows')
31 | ? 'notepad'
32 | : 'vi';
33 | $event->discoverEditor($editor);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Common/EditSpawnerTrait.php:
--------------------------------------------------------------------------------
1 | editor instanceof Editor) {
42 | return $this->editor;
43 | }
44 |
45 | return new Editor();
46 | }
47 |
48 | /**
49 | * Creates a temporary file with the changelog contents.
50 | */
51 | private function createTempFileWithContents(string $contents): string
52 | {
53 | if (is_string($this->mockTempFile)) {
54 | return $this->mockTempFile;
55 | }
56 |
57 | $filename = sprintf('%s.md', uniqid('KAC', true));
58 | $path = sprintf('%s/%s', sys_get_temp_dir(), $filename);
59 |
60 | file_put_contents($path, $contents);
61 |
62 | return $path;
63 | }
64 |
65 | /**
66 | * Removes the temp file generated by createTempFileWithContents
67 | */
68 | private function unlinkTempFile(string $filename): void
69 | {
70 | if (is_string($this->mockTempFile) || ! file_exists($filename)) {
71 | return;
72 | }
73 |
74 | unlink($filename);
75 | }
76 |
77 | /**
78 | * Provide an Editor instance to use.
79 | *
80 | * For testing purposes only.
81 | *
82 | * @internal
83 | *
84 | * @var null|Editor
85 | */
86 | public $editor;
87 |
88 | /**
89 | * Provide a mock temporary filename.
90 | *
91 | * For testing purposes only.
92 | *
93 | * @internal
94 | *
95 | * @var null|string
96 | */
97 | public $mockTempFile;
98 | }
99 |
--------------------------------------------------------------------------------
/src/Common/Editor.php:
--------------------------------------------------------------------------------
1 | writeln(sprintf('Executing "%s"', $command));
31 |
32 | $pipes = [];
33 | $process = ($this->procOpen)($command, $descriptorspec, $pipes);
34 | return ($this->procClose)($process);
35 | }
36 |
37 | /**
38 | * Specify a callback for opening a new process.
39 | *
40 | * For testing purposes only.
41 | *
42 | * @internal
43 | *
44 | * @var callable
45 | */
46 | public $procOpen = 'proc_open';
47 |
48 | /**
49 | * Specify a callback for closing an open process.
50 | *
51 | * For testing purposes only.
52 | *
53 | * @internal
54 | *
55 | * @var callable
56 | */
57 | public $procClose = 'proc_close';
58 | }
59 |
--------------------------------------------------------------------------------
/src/Common/EditorAwareEventInterface.php:
--------------------------------------------------------------------------------
1 | editor;
22 | }
23 |
24 | public function discoverEditor(string $editor): void
25 | {
26 | $this->editor = $editor;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Common/EventInterface.php:
--------------------------------------------------------------------------------
1 | updateChangelog(
17 | $formatter->format($event->changelog())
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Common/IOInterface.php:
--------------------------------------------------------------------------------
1 | input;
33 | }
34 |
35 | public function output(): OutputInterface
36 | {
37 | return $this->output;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Common/IsChangelogReadableListener.php:
--------------------------------------------------------------------------------
1 | config()->changelogFile();
18 |
19 | if (! is_readable($changelogFile)) {
20 | $event->changelogFileIsUnreadable($changelogFile);
21 | return;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Common/ParseChangelogListener.php:
--------------------------------------------------------------------------------
1 | config()->changelogFile();
20 | $parser = new ChangelogParser();
21 | try {
22 | $changelog = $parser->findChangelogForVersion(
23 | file_get_contents($changelogFile),
24 | $event->version()
25 | );
26 | } catch (Exception\ExceptionInterface $e) {
27 | $event->errorParsingChangelog($changelogFile, $e);
28 | return;
29 | }
30 |
31 | $event->updateChangelog($changelog);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Common/ValidateVersionListener.php:
--------------------------------------------------------------------------------
1 | version() ?: '';
21 | $pattern = sprintf('/^(\d+\.\d+\.\d+(%s)?|unreleased)$/i', self::PRE_RELEASE_REGEX);
22 | if (! preg_match($pattern, $version)) {
23 | $event->versionIsInvalid($version);
24 | return;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Common/VersionAwareEventInterface.php:
--------------------------------------------------------------------------------
1 | version;
27 | }
28 |
29 | public function versionIsInvalid(string $version): void
30 | {
31 | $this->failed = true;
32 | $this->output()->writeln(sprintf(
33 | 'Invalid version "%s"; must follow semantic versioning rules',
34 | $version
35 | ));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Config.php:
--------------------------------------------------------------------------------
1 | changelogFile = realpath(getcwd()) . '/CHANGELOG.md';
37 |
38 | $githubSpec = new Provider\ProviderSpec('github');
39 | $githubSpec->setClassName(Provider\GitHub::class);
40 |
41 | $gitlabSpec = new Provider\ProviderSpec('gitlab');
42 | $gitlabSpec->setClassName(Provider\GitLab::class);
43 |
44 | $this->providers = new Provider\ProviderList();
45 | $this->providers->add($githubSpec);
46 | $this->providers->add($gitlabSpec);
47 | }
48 |
49 | public function changelogFile(): string
50 | {
51 | return $this->changelogFile;
52 | }
53 |
54 | public function package(): ?string
55 | {
56 | return $this->package;
57 | }
58 |
59 | public function provider(): Provider\ProviderSpec
60 | {
61 | if (! $this->provider) {
62 | return $this->providers->get($this->providerName);
63 | }
64 |
65 | return $this->provider;
66 | }
67 |
68 | public function providers(): Provider\ProviderList
69 | {
70 | return $this->providers;
71 | }
72 |
73 | public function remote(): ?string
74 | {
75 | return $this->remote;
76 | }
77 |
78 | public function setChangelogFile(string $file): void
79 | {
80 | $this->changelogFile = $file;
81 | }
82 |
83 | public function setPackage(string $package): void
84 | {
85 | $this->package = $package;
86 | $this->provider()->setPackage($package);
87 | }
88 |
89 | public function setProviderName(string $providerName): void
90 | {
91 | $this->providerName = $providerName;
92 | $this->provider = $this->providers->get($providerName);
93 | if ($this->package) {
94 | $this->provider->setPackage($this->package);
95 | }
96 | }
97 |
98 | public function setRemote(string $remote): void
99 | {
100 | $this->remote = $remote;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Config/AbstractDiscoverPackageFromFileListener.php:
--------------------------------------------------------------------------------
1 | packageWasFound()) {
34 | // Already discovered
35 | return;
36 | }
37 |
38 | $packageFile = $this->getFileName();
39 | if (! is_readable($packageFile)) {
40 | // No package file present nothing to do.
41 | return;
42 | }
43 |
44 | $package = json_decode(file_get_contents($packageFile));
45 | if (! isset($package->name)) {
46 | return;
47 | }
48 |
49 | $event->foundPackage($package->name);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Config/CommonConfigOptionsTrait.php:
--------------------------------------------------------------------------------
1 | addOption(
25 | 'package',
26 | 'p',
27 | InputOption::VALUE_REQUIRED,
28 | 'Package to release; must be in valid for the provider you use.'
29 | );
30 | }
31 |
32 | private function injectRemoteOption(Command $command): void
33 | {
34 | $command->addOption(
35 | 'remote',
36 | 'r',
37 | InputOption::VALUE_REQUIRED,
38 | 'Git remote to push tag to; defaults to first Git remote matching provider and package'
39 | );
40 | }
41 |
42 | private function injectProviderOptions(Command $command): void
43 | {
44 | $command->addOption(
45 | 'provider',
46 | null,
47 | InputOption::VALUE_REQUIRED,
48 | 'Named repository provider, based on configuration file definitions;'
49 | . ' "github" and "gitlab" are always available; default is "github"'
50 | );
51 |
52 | $command->addOption(
53 | 'provider-class',
54 | null,
55 | InputOption::VALUE_REQUIRED,
56 | sprintf(
57 | 'Name of a resolvable PHP class that implements %s for use as your'
58 | . ' repository provider; overrides the --provider option when used',
59 | ProviderInterface::class
60 | )
61 | );
62 |
63 | $command->addOption(
64 | 'provider-url',
65 | null,
66 | InputOption::VALUE_REQUIRED,
67 | 'Custom base URL for use with your selected repository provider;'
68 | . ' primarily applies to enterprise github or self-hosted gitlab'
69 | );
70 |
71 | $command->addOption(
72 | 'provider-token',
73 | null,
74 | InputOption::VALUE_REQUIRED,
75 | 'Personal access/OAuth2 token to use for authenticating with your provider;'
76 | . ' this value is REQUIRED in order to create releases.'
77 | );
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Config/ConfigDiscovery.php:
--------------------------------------------------------------------------------
1 | input = $input;
27 | $this->output = $output;
28 | $this->config = new Config();
29 | }
30 |
31 | public function config(): Config
32 | {
33 | return $this->config;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Config/DiscoverPackageFromComposerListener.php:
--------------------------------------------------------------------------------
1 | packageDir ?: realpath(getcwd());
19 | return $path . '/composer.json';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Config/DiscoverPackageFromNpmPackageListener.php:
--------------------------------------------------------------------------------
1 | packageDir ?: realpath(getcwd());
19 | return $path . '/package.json';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Config/Exception/ExceptionInterface.php:
--------------------------------------------------------------------------------
1 | configRoot) {
35 | return $this->configRoot;
36 | }
37 |
38 | $configRoot = getenv('XDG_CONFIG_HOME');
39 | if ($configRoot) {
40 | return $this->normalizePath($configRoot);
41 | }
42 |
43 | $configRoot = getenv('HOME');
44 | if (! $configRoot) {
45 | throw new RuntimeException(
46 | 'keep-a-changelog requires either the XDG_CONFIG_HOME or HOME'
47 | . ' env variables be set in order to operate.'
48 | );
49 | }
50 |
51 | return sprintf('%s/.config', $this->normalizePath($configRoot));
52 | }
53 |
54 | private function normalizePath(string $path): string
55 | {
56 | return rtrim(strtr($path, '\\', '/'), '/');
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Config/PackageNameDiscovery.php:
--------------------------------------------------------------------------------
1 | input = $input;
31 | $this->output = $output;
32 | $this->config = $config;
33 | }
34 |
35 | public function isPropagationStopped(): bool
36 | {
37 | return $this->packageFound
38 | || null !== $this->config->package();
39 | }
40 |
41 | public function config(): Config
42 | {
43 | return $this->config;
44 | }
45 |
46 | public function packageWasFound(): bool
47 | {
48 | return $this->isPropagationStopped();
49 | }
50 |
51 | public function foundPackage(string $package): void
52 | {
53 | $this->config->setPackage($package);
54 | $this->packageFound = true;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Config/PromptForGitRemoteListener.php:
--------------------------------------------------------------------------------
1 | remoteWasFound()) {
20 | return;
21 | }
22 |
23 | $choices = array_merge($event->remotes(), ['abort' => 'Abort release']);
24 |
25 | $helper = $event->questionHelper();
26 | $question = new ChoiceQuestion(
27 | 'More than one valid remote was found; which one should I use?',
28 | $choices
29 | );
30 |
31 | $remote = $helper->ask($event->input(), $event->output(), $question);
32 |
33 | if ('abort' === $remote) {
34 | $event->abort();
35 | return;
36 | }
37 |
38 | $event->foundRemote($choices[$remote]);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Config/RetrieveGlobalConfigListener.php:
--------------------------------------------------------------------------------
1 |
19 | * [defaults]
20 | * changelog_file = changelog.md
21 | * provider = custom
22 | * remote = upstream
23 | *
24 | * [providers]
25 | * github[class] = Phly\KeepAChangelog\Provider\GitHub
26 | * github[url] = https://github.mwop.net
27 | * github[token] = this-is-a-token
28 | * custom[class] = Mwop\Git\Provider
29 | * custom[url] = https://git.mwop.net
30 | * custom[token] = this-is-a-token
31 | * gitlab[class] = Phly\KeepAChangelog\Provider\GitHub
32 | * gitlab[token] = this-is-a-token
33 | *
34 | */
35 | class RetrieveGlobalConfigListener extends AbstractConfigListener
36 | {
37 | use LocateGlobalConfigTrait;
38 |
39 | protected function getConfigFile(): string
40 | {
41 | return sprintf('%s/keep-a-changelog.ini', $this->getConfigRoot());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Config/RetrieveLocalConfigListener.php:
--------------------------------------------------------------------------------
1 |
22 | * [defaults]
23 | * ; Note: the following item is the only one that differs from global config
24 | * package = some/package
25 | * changelog_file = changelog.md
26 | * provider = custom
27 | * remote = upstream
28 | *
29 | * [providers]
30 | * github[class] = Phly\KeepAChangelog\Provider\GitHub
31 | * github[url] = https://github.mwop.net
32 | * github[token] = this-is-a-token
33 | * custom[class] = Mwop\Git\Provider
34 | * custom[url] = https://git.mwop.net
35 | * custom[token] = this-is-a-token
36 | * gitlab[class] = Phly\KeepAChangelog\Provider\GitHub
37 | * gitlab[token] = this-is-a-token
38 | *
39 | */
40 | class RetrieveLocalConfigListener extends AbstractConfigListener
41 | {
42 | /** @var string */
43 | protected $configType = 'local config file';
44 | /** @var bool */
45 | protected $consumeProviderTokens = false;
46 |
47 | protected function getConfigFile(): string
48 | {
49 | return realpath(getcwd()) . '/.keep-a-changelog.ini';
50 | }
51 |
52 | protected function processDefaults(array $defaults, Config $config): void
53 | {
54 | parent::processDefaults($defaults, $config);
55 |
56 | if (! isset($defaults['package'])) {
57 | return;
58 | }
59 |
60 | $config->setPackage($defaults['package']);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/ConfigCommand/AbstractCreateConfigListener.php:
--------------------------------------------------------------------------------
1 | configCreateRequested($event)) {
35 | return;
36 | }
37 |
38 | $configFile = $this->getConfigFileName();
39 |
40 | if (file_exists($configFile)) {
41 | $event->fileExists($configFile);
42 | return;
43 | }
44 |
45 | $success = file_put_contents($configFile, sprintf(
46 | $this->getConfigTemplate(),
47 | $event->customChangelog() ?: 'CHANGELOG.md'
48 | ));
49 |
50 | if (false === $success) {
51 | $event->creationFailed($configFile);
52 | return;
53 | }
54 |
55 | $event->createdConfigFile($configFile);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/ConfigCommand/AbstractEditConfigListener.php:
--------------------------------------------------------------------------------
1 | configEditRequested($event)) {
26 | return;
27 | }
28 |
29 | $configFile = $this->getConfigFile();
30 |
31 | if (! file_exists($configFile)) {
32 | $event->configFileNotFound($configFile);
33 | return;
34 | }
35 |
36 | $status = $this->getEditor()->spawnEditor(
37 | $event->output(),
38 | $event->editor(),
39 | $configFile
40 | );
41 |
42 | if (0 !== $status) {
43 | $event->editFailed($configFile);
44 | return;
45 | }
46 |
47 | $event->editComplete($configFile);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/ConfigCommand/AbstractRemoveConfigListener.php:
--------------------------------------------------------------------------------
1 | configRemovalRequested($event)) {
26 | return;
27 | }
28 |
29 | $configFile = $this->getConfigFile();
30 |
31 | if (! file_exists($configFile)) {
32 | $event->configFileNotFound($configFile);
33 | return;
34 | }
35 |
36 | $output = $event->output();
37 |
38 | $output->writeln(sprintf(
39 | 'Found the following configuration file: %s',
40 | $configFile
41 | ));
42 |
43 | $question = new ConfirmationQuestion('Do you really want to delete this file? ', false);
44 |
45 | if (! $this->getQuestionHelper()->ask($event->input(), $output, $question)) {
46 | $event->abort($configFile);
47 | return;
48 | }
49 |
50 | if (false === ($this->unlink)($configFile)) {
51 | $event->errorRemovingConfig($configFile);
52 | return;
53 | }
54 |
55 | $event->deletedConfigFile($configFile);
56 | }
57 |
58 | public function getQuestionHelper(): QuestionHelper
59 | {
60 | if ($this->questionHelper instanceof QuestionHelper) {
61 | return $this->questionHelper;
62 | }
63 | return new QuestionHelper();
64 | }
65 |
66 | /**
67 | * Provide a QuestionHelper instance for use in prompting the user for
68 | * confirmation.
69 | *
70 | * For testing purposes only.
71 | *
72 | * @internal
73 | *
74 | * @var null|QuestionHelper
75 | */
76 | public $questionHelper;
77 |
78 | /**
79 | * Provide a callable for removing a configuration file.
80 | *
81 | * For testing purposes only.
82 | *
83 | * @internal
84 | *
85 | * @var callable
86 | */
87 | public $unlink = 'unlink';
88 | }
89 |
--------------------------------------------------------------------------------
/src/ConfigCommand/AbstractShowConfigListener.php:
--------------------------------------------------------------------------------
1 | shouldShowConfig($event)) {
26 | return;
27 | }
28 |
29 | $configFile = $this->getConfigFile();
30 | if (! is_readable($configFile)) {
31 | $event->configIsNotReadable($configFile, $this->getConfigType());
32 | return;
33 | }
34 |
35 | $this->displayConfig($event, $configFile);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/ConfigCommand/CreateConfigEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
45 | $this->output = $output;
46 | $this->createLocal = $createLocal;
47 | $this->createGlobal = $createGlobal;
48 | $this->customChangelog = $customChangelog;
49 | }
50 |
51 | public function isPropagationStopped(): bool
52 | {
53 | return $this->failed;
54 | }
55 |
56 | public function failed(): bool
57 | {
58 | return $this->failed;
59 | }
60 |
61 | public function createGlobal(): bool
62 | {
63 | return $this->createGlobal;
64 | }
65 |
66 | public function createLocal(): bool
67 | {
68 | return $this->createLocal;
69 | }
70 |
71 | public function customChangelog(): ?string
72 | {
73 | return $this->customChangelog;
74 | }
75 |
76 | public function fileExists(string $filename): void
77 | {
78 | $this->output->writeln(sprintf(
79 | 'Config file already exists at %s; skipping',
80 | $filename
81 | ));
82 | }
83 |
84 | public function createdConfigFile(string $configFile): void
85 | {
86 | $this->output->writeln(sprintf('Created %s', $configFile));
87 | }
88 |
89 | public function creationFailed(string $filename): void
90 | {
91 | $this->failed = true;
92 | $this->output->writeln(sprintf('Failed creating config file %s', $filename));
93 | $this->output->writeln('Verify you have permission to create the file, and try again.');
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/ConfigCommand/CreateGlobalConfigListener.php:
--------------------------------------------------------------------------------
1 | createGlobal();
36 | }
37 |
38 | public function getConfigFileName(): string
39 | {
40 | return sprintf('%s/keep-a-changelog.ini', $this->getConfigRoot());
41 | }
42 |
43 | public function getConfigTemplate(): string
44 | {
45 | return self::TEMPLATE;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/ConfigCommand/CreateLocalConfigListener.php:
--------------------------------------------------------------------------------
1 | createLocal();
20 | }
21 |
22 | public function getConfigFileName(): string
23 | {
24 | return sprintf('%s/.keep-a-changelog.ini', $this->configRoot ?: getcwd());
25 | }
26 |
27 | public function getConfigTemplate(): string
28 | {
29 | // Done this way due to issues with PHAR creation
30 | return implode("\n", [
31 | '[defaults]',
32 | 'changelog_file = %s',
33 | 'provider = github',
34 | 'remote = origin',
35 | '',
36 | '[providers]',
37 | 'github[class] = Phly\KeepAChangelog\Provider\GitHub',
38 | 'gitlab[class] = Phly\KeepAChangelog\Provider\GitLab',
39 | '',
40 | ]);
41 | }
42 |
43 | /**
44 | * Set a specific directory in which to look for the local config file.
45 | *
46 | * For testing purposes only.
47 | *
48 | * @internal
49 | *
50 | * @var null|string
51 | */
52 | public $configRoot;
53 | }
54 |
--------------------------------------------------------------------------------
/src/ConfigCommand/EditCommand.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
38 | parent::__construct($name);
39 | }
40 |
41 | protected function configure()
42 | {
43 | $this->setDescription(self::DESCRIPTION);
44 | $this->setHelp(self::HELP);
45 | $this->addOption(
46 | 'global',
47 | 'g',
48 | InputOption::VALUE_NONE,
49 | 'Edit the global configuration file ($XDG_CONFIG_HOME/keep-a-changelog.ini)'
50 | );
51 | $this->addOption(
52 | 'local',
53 | 'l',
54 | InputOption::VALUE_NONE,
55 | 'Edit the local configuration file (./.keep-a-changelog.ini)'
56 | );
57 |
58 | $this->injectEditorOption($this);
59 | }
60 |
61 | protected function execute(InputInterface $input, OutputInterface $output): int
62 | {
63 | return $this->dispatcher
64 | ->dispatch(new EditConfigEvent(
65 | $input,
66 | $output,
67 | $input->getOption('local') ?: false,
68 | $input->getOption('global') ?: false,
69 | $input->getOption('editor') ?: null
70 | ))
71 | ->failed()
72 | ? 1
73 | : 0;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/ConfigCommand/EditGlobalConfigListener.php:
--------------------------------------------------------------------------------
1 | editGlobal();
22 | }
23 |
24 | public function getConfigFile(): string
25 | {
26 | return sprintf('%s/keep-a-changelog.ini', $this->getConfigRoot());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/ConfigCommand/EditLocalConfigListener.php:
--------------------------------------------------------------------------------
1 | editLocal();
19 | }
20 |
21 | public function getConfigFile(): string
22 | {
23 | return sprintf('%s/.keep-a-changelog.ini', $this->configRoot ?: getcwd());
24 | }
25 |
26 | /**
27 | * Set a specific directory in which to look for the local config file.
28 | *
29 | * For testing purposes only.
30 | *
31 | * @internal
32 | *
33 | * @var null|string
34 | */
35 | public $configRoot;
36 | }
37 |
--------------------------------------------------------------------------------
/src/ConfigCommand/MaskProviderTokensTrait.php:
--------------------------------------------------------------------------------
1 | $data) {
20 | $config['providers'][$name]['token'] = isset($data['token']) ? 'Yes' : 'No';
21 | }
22 |
23 | return $config;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/ConfigCommand/RemoveCommand.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
34 | parent::__construct($name);
35 | }
36 |
37 | protected function configure()
38 | {
39 | $this->setDescription(self::DESCRIPTION);
40 | $this->setHelp(self::HELP);
41 | $this->addOption(
42 | 'global',
43 | 'g',
44 | InputOption::VALUE_NONE,
45 | 'Edit the global configuration file ($XDG_CONFIG_HOME/keep-a-changelog.ini)'
46 | );
47 | $this->addOption(
48 | 'local',
49 | 'l',
50 | InputOption::VALUE_NONE,
51 | 'Edit the local configuration file (./.keep-a-changelog.ini)'
52 | );
53 | }
54 |
55 | protected function execute(InputInterface $input, OutputInterface $output): int
56 | {
57 | return $this->dispatcher
58 | ->dispatch(new RemoveConfigEvent(
59 | $input,
60 | $output,
61 | $input->getOption('local') ?: false,
62 | $input->getOption('global') ?: false
63 | ))
64 | ->failed()
65 | ? 1
66 | : 0;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/ConfigCommand/RemoveGlobalConfigListener.php:
--------------------------------------------------------------------------------
1 | removeGlobal();
22 | }
23 |
24 | public function getConfigFile(): string
25 | {
26 | return sprintf('%s/keep-a-changelog.ini', $this->getConfigRoot());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/ConfigCommand/RemoveLocalConfigListener.php:
--------------------------------------------------------------------------------
1 | removeLocal();
19 | }
20 |
21 | public function getConfigFile(): string
22 | {
23 | return sprintf('%s/.keep-a-changelog.ini', $this->configRoot ?: getcwd());
24 | }
25 |
26 | /**
27 | * Set a specific directory in which to look for the local config file.
28 | *
29 | * For testing purposes only.
30 | *
31 | * @internal
32 | *
33 | * @var null|string
34 | */
35 | public $configRoot;
36 | }
37 |
--------------------------------------------------------------------------------
/src/ConfigCommand/ShowCommand.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
37 | parent::__construct($name);
38 | }
39 |
40 | protected function configure()
41 | {
42 | $this->setDescription(self::DESCRIPTION);
43 | $this->setHelp(self::HELP);
44 | $this->addOption(
45 | 'global',
46 | 'g',
47 | InputOption::VALUE_NONE,
48 | 'Show configuration from the global configuration file ($XDG_CONFIG_HOME/keep-a-changelog.ini)'
49 | );
50 | $this->addOption(
51 | 'local',
52 | 'l',
53 | InputOption::VALUE_NONE,
54 | 'Show configuration from the local configuration file (./.keep-a-changelog.ini)'
55 | );
56 | }
57 |
58 | protected function execute(InputInterface $input, OutputInterface $output): int
59 | {
60 | return $this->dispatcher
61 | ->dispatch(new ShowConfigEvent(
62 | $input,
63 | $output,
64 | $input->getOption('local') ?: false,
65 | $input->getOption('global') ?: false
66 | ))
67 | ->failed()
68 | ? 1
69 | : 0;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/ConfigCommand/ShowConfigEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
38 | $this->output = $output;
39 | $this->showLocal = $showLocal;
40 | $this->showGlobal = $showGlobal;
41 | $this->showMerged = ($showLocal && $showGlobal) || ! ($showGlobal || $showLocal);
42 | }
43 |
44 | public function isPropagationStopped(): bool
45 | {
46 | return $this->finished || $this->failed;
47 | }
48 |
49 | public function showGlobal(): bool
50 | {
51 | return $this->showGlobal;
52 | }
53 |
54 | public function showLocal(): bool
55 | {
56 | return $this->showLocal;
57 | }
58 |
59 | public function showMerged(): bool
60 | {
61 | return $this->showMerged;
62 | }
63 |
64 | public function displayConfig(string $config, string $type, string $location): void
65 | {
66 | $this->finished = true;
67 | $this->output->writeln(sprintf(
68 | 'Showing %s configuration (%s)',
69 | $type,
70 | $location
71 | ));
72 | $this->output->writeln($config);
73 | $this->output->writeln('');
74 | }
75 |
76 | public function displayMergedConfig(string $config): void
77 | {
78 | $this->finished = true;
79 | $this->output->writeln('Showing merged configuration');
80 | $this->output->writeln($config);
81 | $this->output->writeln('');
82 | }
83 |
84 | public function configIsNotReadable(string $configFile, string $type): void
85 | {
86 | $this->failed = true;
87 | $this->output->writeln('Unable to read configuration');
88 | $this->output->writeln(sprintf(
89 | 'The %s configuration file "%s" either does not exist, or is not readable.',
90 | $type,
91 | $configFile
92 | ));
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/ConfigCommand/ShowGlobalConfigListener.php:
--------------------------------------------------------------------------------
1 | showGlobal() && ! $event->showMerged();
25 | }
26 |
27 | public function getConfigFile(): string
28 | {
29 | return sprintf('%s/keep-a-changelog.ini', $this->getConfigRoot());
30 | }
31 |
32 | public function getConfigType(): string
33 | {
34 | return 'global';
35 | }
36 |
37 | public function displayConfig(ShowConfigEvent $event, string $configFile): void
38 | {
39 | $event->displayConfig(
40 | $this->filterConfiguration($configFile),
41 | 'global',
42 | $configFile
43 | );
44 | }
45 |
46 | private function filterConfiguration(string $configFile): string
47 | {
48 | $config = $this->readIniFile($configFile);
49 | return $this->arrayToIniString($this->maskProviderTokens($config));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/ConfigCommand/ShowLocalConfigListener.php:
--------------------------------------------------------------------------------
1 | showLocal() && ! $event->showMerged();
20 | }
21 |
22 | public function getConfigFile(): string
23 | {
24 | return sprintf('%s/.keep-a-changelog.ini', $this->configRoot ?: getcwd());
25 | }
26 |
27 | public function getConfigType(): string
28 | {
29 | return 'local';
30 | }
31 |
32 | public function displayConfig(ShowConfigEvent $event, string $configFile): void
33 | {
34 | $event->displayConfig(
35 | file_get_contents($configFile),
36 | 'local',
37 | $configFile
38 | );
39 | }
40 |
41 | /**
42 | * Set a specific directory in which to look for the local config file.
43 | *
44 | * For testing purposes only.
45 | *
46 | * @internal
47 | *
48 | * @var null|string
49 | */
50 | public $configRoot;
51 | }
52 |
--------------------------------------------------------------------------------
/src/ConfigCommand/ShowMergedConfigListener.php:
--------------------------------------------------------------------------------
1 | showMerged()) {
29 | return;
30 | }
31 |
32 | $globalConfigFile = sprintf('%s/keep-a-changelog.ini', $this->getConfigRoot());
33 | if (! is_readable($globalConfigFile)) {
34 | $event->configIsNotReadable($globalConfigFile, 'global');
35 | return;
36 | }
37 |
38 | $localConfigFile = sprintf('%s/.keep-a-changelog.ini', $this->localConfigRoot ?: getcwd());
39 | if (! is_readable($localConfigFile)) {
40 | $event->configIsNotReadable($localConfigFile, 'local');
41 | return;
42 | }
43 |
44 | $config = $this->arrayMergeRecursive(
45 | $this->readIniFile($globalConfigFile),
46 | $this->readIniFile($localConfigFile)
47 | );
48 |
49 | $event->displayMergedConfig(
50 | $this->arrayToIniString($this->maskProviderTokens($config))
51 | );
52 | }
53 |
54 | /**
55 | * Set a specific directory in which to look for the local config file.
56 | *
57 | * For testing purposes only.
58 | *
59 | * @internal
60 | *
61 | * @var null|string
62 | */
63 | public $localConfigRoot;
64 | }
65 |
--------------------------------------------------------------------------------
/src/ConfigCommand/VerifyEditOptionsListener.php:
--------------------------------------------------------------------------------
1 | editLocal() && $event->editGlobal()) {
16 | $event->tooManyOptions();
17 | return;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/ConfigCommand/VerifyRemoveOptionsListener.php:
--------------------------------------------------------------------------------
1 | removeLocal() && ! $event->removeGlobal()) {
16 | $event->missingOptions();
17 | return;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Entry/ConfigListener.php:
--------------------------------------------------------------------------------
1 | input()->getOption('pr')
20 | || $event->input()->getOption('issue')
21 | ) {
22 | $this->requiresPackageName = true;
23 | }
24 | parent::__invoke($event);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Entry/EntryTypes.php:
--------------------------------------------------------------------------------
1 | index;
43 | case 'type':
44 | return $this->type;
45 | default:
46 | throw new UnexpectedValueException(sprintf(
47 | 'The property "%s" does not exist for class "%s"',
48 | $name,
49 | self::class
50 | ));
51 | }
52 | }
53 |
54 | /**
55 | * @param mixed $value
56 | */
57 | public function __set(string $name, $value)
58 | {
59 | switch ($name) {
60 | case 'index':
61 | $this->setIndex($value);
62 | break;
63 | case 'type':
64 | $this->setType($value);
65 | break;
66 | default:
67 | throw new UnexpectedValueException(sprintf(
68 | 'The property "%s" does not exist for class "%s"',
69 | $name,
70 | self::class
71 | ));
72 | }
73 | }
74 |
75 | private function setIndex(int $value): void
76 | {
77 | $this->index = $value;
78 | }
79 |
80 | private function setType(string $value): void
81 | {
82 | if (! in_array($value, self::ACTIONS, true)) {
83 | throw new TypeError(
84 | 'Property type expects one of the constants ACTION_INJECT, ACTION_REPLACE, or ACTION_NOT_FOUND'
85 | );
86 | }
87 | $this->type = $value;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Entry/IsEntryArgumentEmptyListener.php:
--------------------------------------------------------------------------------
1 | entry()) {
16 | $event->entryIsEmpty();
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Entry/NotifyPreparingEntryListener.php:
--------------------------------------------------------------------------------
1 | output()->writeln(sprintf(
19 | 'Preparing entry for %s section',
20 | ucwords($event->entryType())
21 | ));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Entry/PrependIssueLinkListener.php:
--------------------------------------------------------------------------------
1 | issueNumber();
18 | }
19 |
20 | public function generateLink(ProviderInterface $provider, int $identifier): string
21 | {
22 | return $provider->generateIssueLink($identifier);
23 | }
24 |
25 | public function reportInvalidIdentifier(AddChangelogEntryEvent $event, int $identifier): void
26 | {
27 | $event->issueNumberIsInvalid($identifier);
28 | }
29 |
30 | public function reportInvalidLink(AddChangelogEntryEvent $event, string $link): void
31 | {
32 | $event->issueLinkIsInvalid($link);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Entry/PrependPatchLinkListener.php:
--------------------------------------------------------------------------------
1 | patchNumber();
18 | }
19 |
20 | public function generateLink(ProviderInterface $provider, int $identifier): string
21 | {
22 | return $provider->generatePatchLink($identifier);
23 | }
24 |
25 | public function reportInvalidIdentifier(AddChangelogEntryEvent $event, int $identifier): void
26 | {
27 | $event->patchNumberIsInvalid($identifier);
28 | }
29 |
30 | public function reportInvalidLink(AddChangelogEntryEvent $event, string $link): void
31 | {
32 | $event->patchLinkIsInvalid($link);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/EventDispatcher.php:
--------------------------------------------------------------------------------
1 | listenerProvider = $listenerProvider;
26 | }
27 |
28 | public function dispatch(object $event): object
29 | {
30 | $stoppable = $event instanceof StoppableEventInterface;
31 |
32 | if ($stoppable && $event->isPropagationStopped()) {
33 | return $event;
34 | }
35 |
36 | foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) {
37 | $listener($event);
38 |
39 | if ($stoppable && $event->isPropagationStopped()) {
40 | break;
41 | }
42 | }
43 |
44 | return $event;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Exception/ChangelogEntriesNotFoundException.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
39 | parent::__construct($name);
40 | }
41 |
42 | protected function configure(): void
43 | {
44 | $this->setDescription('Close a milestone for this package via your provider');
45 | $this->setHelp(self::HELP);
46 |
47 | $this->addArgument('id', InputArgument::REQUIRED, 'Milestone ID');
48 |
49 | $this->injectPackageOption($this);
50 | $this->injectRemoteOption($this);
51 | $this->injectProviderOptions($this);
52 | }
53 |
54 | protected function execute(InputInterface $input, OutputInterface $output): int
55 | {
56 | return $this->dispatcher
57 | ->dispatch(new CloseMilestoneEvent(
58 | $input,
59 | $output,
60 | $this->dispatcher
61 | ))
62 | ->failed()
63 | ? 1
64 | : 0;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Milestone/CloseMilestoneEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
29 | $this->output = $output;
30 | $this->dispatcher = $dispatcher;
31 | $this->id = (int) $input->getArgument('id');
32 | }
33 |
34 | public function isPropagationStopped(): bool
35 | {
36 | return $this->failed;
37 | }
38 |
39 | public function id(): int
40 | {
41 | return $this->id;
42 | }
43 |
44 | public function milestoneClosed(): void
45 | {
46 | $this->output()->writeln(sprintf(
47 | 'Closed milestone %d',
48 | $this->id()
49 | ));
50 | }
51 |
52 | public function errorClosingMilestone(Throwable $e): void
53 | {
54 | $this->failed = true;
55 |
56 | if ((int) $e->getCode() === 401) {
57 | $this->reportAuthenticationException($e);
58 | return;
59 | }
60 |
61 | $this->reportStandardException($e);
62 | }
63 |
64 | private function reportStandardException(Throwable $e): void
65 | {
66 | $output = $this->output();
67 |
68 | $output->writeln('Error closing milestone!');
69 | $output->writeln('An error occurred when attempting to close the milestone:');
70 | $output->writeln('');
71 | $output->writeln('Error Message: ' . $e->getMessage());
72 | }
73 |
74 | private function reportAuthenticationException(Throwable $e): void
75 | {
76 | $output = $this->output();
77 |
78 | $output->writeln('Invalid credentials');
79 | $output->writeln('The credentials associated with your Git provider are invalid.');
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Milestone/CloseMilestoneListener.php:
--------------------------------------------------------------------------------
1 | id();
20 | $provider = $event->provider();
21 |
22 | $event->output()->writeln(sprintf(
23 | 'Closing milestone %d',
24 | $id
25 | ));
26 |
27 | try {
28 | $status = $provider->closeMilestone($id);
29 | } catch (Throwable $e) {
30 | $event->errorClosingMilestone($e);
31 | return;
32 | }
33 |
34 | $event->milestoneClosed();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Milestone/CommandConfigListener.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
37 | parent::__construct($name);
38 | }
39 |
40 | protected function configure(): void
41 | {
42 | $this->setDescription('Create a new milestone for this package via your provider');
43 | $this->setHelp(self::HELP);
44 |
45 | $this->addArgument('title', InputArgument::REQUIRED, 'Title/name of milestone');
46 | $this->addArgument('description', InputArgument::OPTIONAL, 'Milestone description');
47 |
48 | $this->injectPackageOption($this);
49 | $this->injectRemoteOption($this);
50 | $this->injectProviderOptions($this);
51 | }
52 |
53 | protected function execute(InputInterface $input, OutputInterface $output): int
54 | {
55 | return $this->dispatcher
56 | ->dispatch(new CreateMilestoneEvent(
57 | $input,
58 | $output,
59 | $this->dispatcher
60 | ))
61 | ->failed()
62 | ? 1
63 | : 0;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Milestone/CreateMilestoneListener.php:
--------------------------------------------------------------------------------
1 | title();
22 | $description = $event->description();
23 | /** @var ProviderInterface|MilestoneAwareProviderInterface $provider */
24 | $provider = $event->provider();
25 |
26 | $event->output()->writeln(sprintf(
27 | 'Creating milestone "%s" (%s)',
28 | $title,
29 | $description
30 | ));
31 |
32 | try {
33 | $milestone = $provider->createMilestone($title, $description);
34 | } catch (Throwable $e) {
35 | $event->errorCreatingMilestone($e);
36 | return;
37 | }
38 |
39 | $event->milestoneCreated($milestone);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Milestone/ListCommand.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
35 | parent::__construct($name);
36 | }
37 |
38 | protected function configure(): void
39 | {
40 | $this->setDescription('List milestones for this package via your provider');
41 | $this->setHelp(self::HELP);
42 |
43 | $this->injectPackageOption($this);
44 | $this->injectRemoteOption($this);
45 | $this->injectProviderOptions($this);
46 | }
47 |
48 | protected function execute(InputInterface $input, OutputInterface $output): int
49 | {
50 | return $this->dispatcher
51 | ->dispatch(new ListMilestonesEvent(
52 | $input,
53 | $output,
54 | $this->dispatcher
55 | ))
56 | ->failed()
57 | ? 1
58 | : 0;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Milestone/ListMilestonesListener.php:
--------------------------------------------------------------------------------
1 | provider();
21 |
22 | $event->output()->writeln('Fetching milestones...');
23 |
24 | try {
25 | $milestones = $provider->listMilestones();
26 | } catch (Throwable $e) {
27 | $event->errorListingMilestones($e);
28 | return;
29 | }
30 |
31 | $event->milestonesRetrieved($milestones);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Milestone/VerifyProviderListener.php:
--------------------------------------------------------------------------------
1 | config();
18 | $providerSpec = $config->provider();
19 |
20 | if (! $providerSpec->isComplete()) {
21 | $event->providerIsIncomplete();
22 | return;
23 | }
24 |
25 | $provider = $providerSpec->createProvider();
26 |
27 | if (! $provider instanceof MilestoneAwareProviderInterface) {
28 | $event->providerIncapableOfMilestones();
29 | return;
30 | }
31 |
32 | $event->discoveredProvider($provider);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Provider/Exception/ExceptionInterface.php:
--------------------------------------------------------------------------------
1 | getCode(), $e);
26 | }
27 |
28 | public static function forTagOnGithub(string $package, string $version, Throwable $e): self
29 | {
30 | return new self(sprintf(
31 | 'When verifying that the tag %s for package %s is present on GitHub,'
32 | . ' an error occurred fetching tag details: %s',
33 | $version,
34 | $package,
35 | $e->getMessage()
36 | ), $e->getCode(), $e);
37 | }
38 |
39 | public static function forUnverifiedTagOnGithub(string $package, string $version): self
40 | {
41 | return new self(sprintf(
42 | 'When verifying that the tag %s for package %s is present on GitHub,'
43 | . ' the tag found was unsigned. Please recreate the tag using the'
44 | . ' -s flag.',
45 | $version,
46 | $package
47 | ));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Provider/Exception/MissingTokenException.php:
--------------------------------------------------------------------------------
1 | id = $id;
25 | $this->title = $title;
26 | $this->description = $description;
27 | }
28 |
29 | public function id(): int
30 | {
31 | return $this->id;
32 | }
33 |
34 | public function title(): string
35 | {
36 | return $this->title;
37 | }
38 |
39 | public function description(): string
40 | {
41 | return $this->description;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Provider/MilestoneAwareProviderInterface.php:
--------------------------------------------------------------------------------
1 |
43 | * [#17](https://github.com/not-an-org/not-a-repo/issues/17)
44 | *
45 | *
46 | * @throws Exception\MissingPackageNameException
47 | */
48 | public function generateIssueLink(int $issueIdentifier): string;
49 |
50 | /**
51 | * This method should generate the full markdown link to a patch.
52 | *
53 | * As an example of something it could generate:
54 | *
55 | *
56 | * [#17](https://github.com/not-an-org/not-a-repo/pull/17)
57 | *
58 | *
59 | * @throws Exception\MissingPackageNameException
60 | */
61 | public function generatePatchLink(int $patchIdentifier): string;
62 |
63 | /**
64 | * Set the package name to use in links and when creating the release name.
65 | */
66 | public function setPackageName(string $package): void;
67 |
68 | /**
69 | * Set the authentication token to use for API calls to the provider.
70 | */
71 | public function setToken(string $token): void;
72 |
73 | /**
74 | * Set the base URL to use for API calls to the provider.
75 | *
76 | * Generally, this should only be the scheme + authority.
77 | */
78 | public function setUrl(string $url): void;
79 | }
80 |
--------------------------------------------------------------------------------
/src/Provider/ProviderList.php:
--------------------------------------------------------------------------------
1 | */
16 | private $providers = [];
17 |
18 | public function has(string $name): bool
19 | {
20 | return isset($this->providers[$name]);
21 | }
22 |
23 | public function get(string $name): ?ProviderSpec
24 | {
25 | return $this->providers[$name] ?? null;
26 | }
27 |
28 | public function add(ProviderSpec $provider): void
29 | {
30 | $this->providers[$provider->name()] = $provider;
31 | }
32 |
33 | public function listKnownTypes(): array
34 | {
35 | return array_keys($this->providers);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Provider/ProviderSpec.php:
--------------------------------------------------------------------------------
1 | name = $name;
39 | }
40 |
41 | public function isComplete(): bool
42 | {
43 | return $this->className
44 | && class_exists($this->className)
45 | && in_array(ProviderInterface::class, class_implements($this->className), true);
46 | }
47 |
48 | public function createProvider(): ProviderInterface
49 | {
50 | $class = $this->className;
51 | $provider = new $class();
52 |
53 | if ($this->package) {
54 | $provider->setPackageName($this->package);
55 | }
56 |
57 | if ($this->token) {
58 | $provider->setToken($this->token);
59 | }
60 |
61 | if ($this->url) {
62 | $provider->setUrl($this->url);
63 | }
64 |
65 | return $provider;
66 | }
67 |
68 | public function name(): string
69 | {
70 | return $this->name;
71 | }
72 |
73 | public function url(): ?string
74 | {
75 | return $this->url;
76 | }
77 |
78 | public function setClassName(string $className): void
79 | {
80 | $this->className = $className;
81 | }
82 |
83 | public function setPackage(string $package): void
84 | {
85 | $this->package = $package;
86 | }
87 |
88 | public function setToken(string $token): void
89 | {
90 | $this->token = $token;
91 | }
92 |
93 | public function setUrl(string $url): void
94 | {
95 | $this->url = $url;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/Unreleased/PromoteEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
38 | $this->output = $output;
39 | $this->dispatcher = $dispatcher;
40 | $this->newVersion = $version;
41 | $this->releaseDate = $releaseDate;
42 | }
43 |
44 | public function isPropagationStopped(): bool
45 | {
46 | return $this->failed;
47 | }
48 |
49 | public function newVersion(): string
50 | {
51 | return $this->newVersion;
52 | }
53 |
54 | public function releaseDate(): string
55 | {
56 | return $this->releaseDate;
57 | }
58 |
59 | public function version(): string
60 | {
61 | return 'unreleased';
62 | }
63 |
64 | public function versionIsInvalid(string $version): void
65 | {
66 | // intentional no-op; never should get called
67 | }
68 |
69 | public function didNotPromote(): void
70 | {
71 | $this->failed = true;
72 | $this->output()->writeln('Invalid date provided for release; must be in Y-m-d format');
73 | }
74 |
75 | public function changelogReady(): void
76 | {
77 | $this->output->writeln(sprintf(
78 | 'Renamed Unreleased entry to "%s" with release date of "%s"',
79 | $this->newVersion,
80 | $this->releaseDate
81 | ));
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Unreleased/PromoteUnreleasedToNewVersionListener.php:
--------------------------------------------------------------------------------
1 | changelogEntry();
24 | $lines = explode("\n", $entry->contents);
25 | $lines[0] = sprintf('## %s - %s', $event->newVersion(), $event->releaseDate());
26 |
27 | $this->getChangelogEditor()->update(
28 | $event->config()->changelogFile(),
29 | implode("\n", $lines),
30 | $entry
31 | );
32 |
33 | $event->changelogReady();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Unreleased/ValidateDateToUseListener.php:
--------------------------------------------------------------------------------
1 | releaseDate())) {
18 | return;
19 | }
20 |
21 | $event->didNotPromote();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Version/CheckTreeForChangesListener.php:
--------------------------------------------------------------------------------
1 |
21 | * function(string $command[, array &$output[, int &$return]])
22 | *
23 | *
24 | * @var callable
25 | */
26 | public $exec = 'exec';
27 |
28 | public function __invoke(TagReleaseEvent $event): void
29 | {
30 | if ($event->input()->getOption('force')) {
31 | return;
32 | }
33 |
34 | $command = 'git status -s';
35 | $output = [];
36 | $status = 0;
37 | $exec = $this->exec;
38 |
39 | $exec($command, $output, $status);
40 |
41 | if ($status !== 0 || count($output) > 0) {
42 | $event->unversionedChangesPresent();
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Version/CreateReleaseNameListener.php:
--------------------------------------------------------------------------------
1 | input()->getOption('name');
20 | if ($name) {
21 | $event->setReleaseName($name);
22 | return;
23 | }
24 |
25 | $package = $event->config()->package();
26 | $version = $event->version();
27 | $lastSeparator = strrpos($package, '/');
28 | $repo = substr($package, $lastSeparator + 1);
29 | $event->setReleaseName(sprintf('%s %s', $repo, $version));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Version/DiscoverVersionEventTrait.php:
--------------------------------------------------------------------------------
1 | version = $version;
16 | $this->tagName = $this->tagName ?: $version;
17 | }
18 |
19 | public function versionNotAccepted(): void
20 | {
21 | $this->failed = true;
22 | $this->output->writeln('No version specified');
23 | $this->output->writeln('Please specify a version via the argument.');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Version/DiscoverVersionListener.php:
--------------------------------------------------------------------------------
1 | version();
37 | if (is_string($version) && ! empty($version)) {
38 | // Version was provided already
39 | return;
40 | }
41 |
42 | $readyVersions = $this->getReadyVersions(
43 | (new ChangelogParser())
44 | ->findAllVersions($event->config()->changelogFile())
45 | );
46 |
47 | if (0 === count($readyVersions)) {
48 | // No versions found; let version validator flag it as invalid
49 | return;
50 | }
51 |
52 | $versionToTag = array_shift($readyVersions);
53 | $question = new ConfirmationQuestion(sprintf(
54 | "Most recent version in changelog file is %s; use this version? [Y/n]" . PHP_EOL . "> ",
55 | $versionToTag
56 | ));
57 |
58 | $questionHelper = $this->questionHelper ?: new QuestionHelper();
59 | if (! $questionHelper->ask($event->input(), $event->output(), $question)) {
60 | $event->versionNotAccepted();
61 | return;
62 | }
63 |
64 | $event->foundVersion($versionToTag);
65 | }
66 |
67 | private function getReadyVersions(iterable $versions): array
68 | {
69 | $readyVersions = [];
70 |
71 | foreach ($versions as $version => $date) {
72 | if (! preg_match('/^\d{4}-\d{2}\-\d{2}$/', $date)) {
73 | continue;
74 | }
75 |
76 | $readyVersions[] = $version;
77 | }
78 |
79 | return $readyVersions;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Version/DiscoverableVersionEventInterface.php:
--------------------------------------------------------------------------------
1 | input = $input;
39 | $this->output = $output;
40 | $this->dispatcher = $dispatcher;
41 | $this->version = $version;
42 | $this->editor = $editor;
43 | }
44 |
45 | public function isPropagationStopped(): bool
46 | {
47 | return $this->failed;
48 | }
49 |
50 | public function editorFailed(): void
51 | {
52 | $this->failed = true;
53 | $this->output->writeln(sprintf(
54 | 'Could not edit %s; please check the output for details.',
55 | $this->config()->changelogFile()
56 | ));
57 | }
58 |
59 | public function editComplete(): void
60 | {
61 | $message = $this->version
62 | ? sprintf(
63 | 'Edited change for version %s in %s',
64 | $this->version,
65 | $this->config()->changelogFile()
66 | )
67 | : sprintf(
68 | 'Edited most recent changelog in %s',
69 | $this->config()->changelogFile()
70 | );
71 | $this->output->writeln($message);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Version/EditChangelogVersionListener.php:
--------------------------------------------------------------------------------
1 | changelogEntry();
24 | $tempFile = $this->createTempFileWithContents(
25 | $changelogEntry->contents
26 | );
27 |
28 | $status = $this->getEditor()->spawnEditor(
29 | $event->output(),
30 | $event->editor(),
31 | $tempFile
32 | );
33 |
34 | if (0 !== $status) {
35 | $this->unlinkTempFile($tempFile);
36 | $event->editorFailed();
37 | return;
38 | }
39 |
40 | $this->getChangelogEditor()->update(
41 | $event->config()->changelogFile(),
42 | file_get_contents($tempFile),
43 | $changelogEntry
44 | );
45 |
46 | $this->unlinkTempFile($tempFile);
47 | $event->editComplete();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Version/EditCommand.php:
--------------------------------------------------------------------------------
1 | is provided, assumes the most recent changelog should be used.
29 |
30 | By default, the command will edit CHANGELOG.md in the current directory, unless
31 | a different file is specified via the --file option.
32 | EOH;
33 |
34 | /** @var EventDispatcherInterface */
35 | private $dispatcher;
36 |
37 | public function __construct(EventDispatcherInterface $dispatcher, ?string $name = null)
38 | {
39 | $this->dispatcher = $dispatcher;
40 | parent::__construct($name);
41 | }
42 |
43 | protected function configure(): void
44 | {
45 | $this->setDescription(self::DESCRIPTION);
46 | $this->setHelp(self::HELP);
47 |
48 | $this->addArgument(
49 | 'version',
50 | InputArgument::OPTIONAL,
51 | 'A specific changelog version to edit; defaults to latest.'
52 | );
53 |
54 | $this->injectEditorOption($this);
55 | }
56 |
57 | protected function execute(InputInterface $input, OutputInterface $output): int
58 | {
59 | return $this->dispatcher
60 | ->dispatch(new EditChangelogVersionEvent(
61 | $input,
62 | $output,
63 | $this->dispatcher,
64 | $input->getArgument('version') ?: null,
65 | $input->getOption('editor') ?: null
66 | ))
67 | ->failed()
68 | ? 1
69 | : 0;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Version/ListCommand.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
31 | parent::__construct($name);
32 | }
33 |
34 | protected function configure(): void
35 | {
36 | $this->setDescription(self::DESCRIPTION);
37 | $this->setHelp(self::HELP);
38 | }
39 |
40 | protected function execute(InputInterface $input, OutputInterface $output): int
41 | {
42 | return $this->dispatcher
43 | ->dispatch(new ListVersionsEvent($input, $output, $this->dispatcher))
44 | ->failed()
45 | ? 1
46 | : 0;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Version/ListVersionsEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
24 | $this->output = $output;
25 | $this->dispatcher = $dispatcher;
26 | }
27 |
28 | public function isPropagationStopped(): bool
29 | {
30 | return $this->failed;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Version/ListVersionsListener.php:
--------------------------------------------------------------------------------
1 | output();
22 |
23 | $output->writeln('Found the following versions:');
24 | foreach ((new ChangelogParser())->findAllVersions($event->config()->changelogFile()) as $version => $date) {
25 | $output->writeln(sprintf(
26 | '- %s%s(release date: %s)',
27 | $version,
28 | str_repeat("\t", strlen($version) < 8 ? 2 : 1),
29 | $date
30 | ));
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Version/PromptForRemovalConfirmationListener.php:
--------------------------------------------------------------------------------
1 | input();
19 | if ($input->hasOption('force-removal') && $input->getOption('force-removal')) {
20 | // No need to prompt
21 | return;
22 | }
23 |
24 | $entry = $event->changelogEntry();
25 | $output = $event->output();
26 |
27 | $output->writeln('Found the following entry:');
28 | $output->writeln($entry->contents);
29 |
30 | $question = new ConfirmationQuestion('Do you really want to delete this version ([y]es/[n]o)? ', false);
31 |
32 | if (! $this->getQuestionHelper()->ask($input, $output, $question)) {
33 | $event->abort();
34 | return;
35 | }
36 | }
37 |
38 | private function getQuestionHelper(): QuestionHelper
39 | {
40 | if ($this->questionHelper instanceof QuestionHelper) {
41 | return $this->questionHelper;
42 | }
43 | return new QuestionHelper();
44 | }
45 |
46 | /**
47 | * Provide an alternative question helper for use in prompting.
48 | *
49 | * For testing purposes only.
50 | *
51 | * @internal
52 | *
53 | * @var null|QuestionHelper
54 | */
55 | public $questionHelper;
56 | }
57 |
--------------------------------------------------------------------------------
/src/Version/PushReleaseToProviderListener.php:
--------------------------------------------------------------------------------
1 | releaseName();
20 | $provider = $event->provider();
21 |
22 | $event->output()->writeln(sprintf(
23 | 'Creating release "%s"',
24 | $releaseName
25 | ));
26 |
27 | try {
28 | $release = $provider->createRelease(
29 | $releaseName,
30 | $event->tagName(),
31 | $event->changelog()
32 | );
33 | } catch (Throwable $e) {
34 | $event->errorCreatingRelease($e);
35 | return;
36 | }
37 |
38 | if (! $release) {
39 | $event->unexpectedProviderResult();
40 | return;
41 | }
42 |
43 | $event->releaseCreated($release);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Version/PushTagToRemoteListener.php:
--------------------------------------------------------------------------------
1 |
22 | * function (string $command[, array &$output[, int &$exitStatus]]) : void
23 | *
24 | *
25 | * @internal
26 | *
27 | * @var callable
28 | */
29 | public $exec = 'exec';
30 |
31 | public function __invoke(ReleaseEvent $event): void
32 | {
33 | $tagName = $event->tagName();
34 | $remote = $event->config()->remote();
35 |
36 | $event->output()->writeln(sprintf(
37 | 'Pushing tag %s to %s',
38 | $tagName,
39 | $remote
40 | ));
41 |
42 | $command = sprintf('git push %s %s', $remote, $tagName);
43 | $exec = $this->exec;
44 | $output = [];
45 | $return = 0;
46 |
47 | $exec($command, $output, $return);
48 |
49 | if (0 !== $return) {
50 | $event->taggingFailed();
51 | return;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Version/ReadyCommand.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
37 | parent::__construct($name);
38 | }
39 |
40 | protected function configure(): void
41 | {
42 | $this->setDescription(self::DESCRIPTION);
43 | $this->setHelp(self::HELP);
44 | $this->addOption(
45 | 'date',
46 | '-d',
47 | InputOption::VALUE_REQUIRED,
48 | 'Specific date string to use; use this if the date is other than today,'
49 | . ' or if you wish to use a different date format.'
50 | );
51 | $this->addOption(
52 | 'date',
53 | '-d',
54 | InputOption::VALUE_REQUIRED,
55 | 'Specific date string to use; use this if the date is other than today,'
56 | . ' or if you wish to use a different date format.'
57 | );
58 | $this->addOption(
59 | 'release-version',
60 | 'r',
61 | InputOption::VALUE_REQUIRED,
62 | 'A specific changelog version to ready for release.'
63 | );
64 | }
65 |
66 | protected function execute(InputInterface $input, OutputInterface $output): int
67 | {
68 | return $this->dispatcher
69 | ->dispatch(new ReadyLatestChangelogEvent(
70 | $input,
71 | $output,
72 | $this->dispatcher,
73 | $input->getOption('date') ?: date('Y-m-d'),
74 | $input->getOption('release-version') ?: null
75 | ))
76 | ->failed()
77 | ? 1
78 | : 0;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Version/ReadyLatestChangelogEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
37 | $this->output = $output;
38 | $this->dispatcher = $dispatcher;
39 | $this->releaseDate = $releaseDate;
40 | $this->version = $version;
41 | }
42 |
43 | public function isPropagationStopped(): bool
44 | {
45 | return $this->failed;
46 | }
47 |
48 | public function releaseDate(): string
49 | {
50 | return $this->releaseDate;
51 | }
52 |
53 | public function malformedReleaseLine(string $versionLine): void
54 | {
55 | $this->failed = true;
56 | $this->output->writeln(
57 | 'Unable to set release date; most recent release has a malformed release line.'
58 | );
59 | $this->output->writeln('Must be in the following format (minus initial indentation):');
60 | $this->output->writeln(' ## - TBD');
61 | $this->output->writeln('where follows semantic versioning rules.');
62 | $this->output->writeln('');
63 | $this->output->writeln('Discovered:');
64 | $this->output->writeln(sprintf(' %s', $versionLine));
65 | }
66 |
67 | public function changelogReady(): void
68 | {
69 | $versionString = $this->version
70 | ? sprintf('changelog version %s', $this->version)
71 | : 'most recent changelog';
72 | $this->output->writeln(sprintf(
73 | 'Set release date of %s to "%s"',
74 | $versionString,
75 | $this->releaseDate
76 | ));
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Version/ReleaseCommandConfigListener.php:
--------------------------------------------------------------------------------
1 | input = $input;
37 | $this->output = $output;
38 | $this->dispatcher = $dispatcher;
39 | $this->version = $version;
40 | }
41 |
42 | public function isPropagationStopped(): bool
43 | {
44 | return $this->aborted || $this->failed;
45 | }
46 |
47 | public function abort()
48 | {
49 | $this->aborted = true;
50 | $this->output->writeln('Aborting at user request');
51 | }
52 |
53 | public function versionRemoved()
54 | {
55 | $this->output->writeln(sprintf(
56 | 'Removed changelog version %s from file %s.',
57 | $this->version,
58 | $this->config()->changelogFile()
59 | ));
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Version/RemoveChangelogVersionListener.php:
--------------------------------------------------------------------------------
1 | config()->changelogFile();
20 | $entry = $event->changelogEntry();
21 |
22 | $this->getChangelogEditor()->update($changelog, '', $entry);
23 |
24 | $event->versionRemoved();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Version/RemoveCommand.php:
--------------------------------------------------------------------------------
1 | provided. The command will provide a preview, and prompt for
25 | confirmation before doing so (unless using the --force-removal flag).
26 | EOH;
27 |
28 | /** @var EventDispatcherInterface */
29 | private $dispatcher;
30 |
31 | public function __construct(EventDispatcherInterface $dispatcher, ?string $name = null)
32 | {
33 | $this->dispatcher = $dispatcher;
34 | parent::__construct($name);
35 | }
36 |
37 | protected function configure(): void
38 | {
39 | $this->setDescription(self::DESCRIPTION);
40 | $this->setHelp(self::HELP);
41 | $this->addArgument(
42 | 'version',
43 | InputArgument::REQUIRED,
44 | 'The changelog version to remove.'
45 | );
46 | $this->addOption(
47 | 'force-removal',
48 | 'r',
49 | InputOption::VALUE_NONE,
50 | 'Do not prompt for confirmation.'
51 | );
52 | }
53 |
54 | protected function execute(InputInterface $input, OutputInterface $output): int
55 | {
56 | return $this->dispatcher
57 | ->dispatch(new RemoveChangelogVersionEvent(
58 | $input,
59 | $output,
60 | $this->dispatcher,
61 | $input->getArgument('version')
62 | ))
63 | ->failed()
64 | ? 1
65 | : 0;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Version/SetDateForChangelogReleaseListener.php:
--------------------------------------------------------------------------------
1 | changelogEntry();
25 | $lines = explode("\n", $entry->contents);
26 | $versionLine = $lines[0];
27 |
28 | if (null === ($versionLine = $this->injectDate($versionLine, $event->releaseDate()))) {
29 | $event->malformedReleaseLine($lines[0]);
30 | return;
31 | }
32 |
33 | $lines[0] = $versionLine;
34 |
35 | $this->getChangelogEditor()->update(
36 | $event->config()->changelogFile(),
37 | implode("\n", $lines),
38 | $entry
39 | );
40 |
41 | $event->changelogReady();
42 | }
43 |
44 | private function injectDate(string $versionLine, string $date): ?string
45 | {
46 | // @phpcs:disable
47 | $regex = '/^(?P## \d+\.\d+\.\d+(?:(alpha|beta|rc|dev|patch|pl|a|b|p)\d+)?)\s+-\s+(?:(?!\d{4}-\d{2}-\d{2}).*)/i';
48 | // @phpcs:enable
49 |
50 | if (! preg_match($regex, $versionLine, $matches)) {
51 | return null;
52 | }
53 |
54 | return sprintf(
55 | '%s - %s',
56 | $matches['prefix'],
57 | $date
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Version/ShowCommand.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
31 | parent::__construct($name);
32 | }
33 |
34 | protected function configure(): void
35 | {
36 | $this->setDescription(self::DESCRIPTION);
37 | $this->setHelp(self::HELP);
38 | $this->addArgument(
39 | 'version',
40 | InputArgument::OPTIONAL,
41 | 'Which version do you wish to display? (Defaults to latest)'
42 | );
43 | }
44 |
45 | protected function execute(InputInterface $input, OutputInterface $output): int
46 | {
47 | return $this->dispatcher
48 | ->dispatch(new ShowVersionEvent(
49 | $input,
50 | $output,
51 | $this->dispatcher,
52 | $input->getArgument('version') ?: null
53 | ))
54 | ->failed()
55 | ? 1
56 | : 0;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Version/ShowVersionEvent.php:
--------------------------------------------------------------------------------
1 | input = $input;
31 | $this->output = $output;
32 | $this->dispatcher = $dispatcher;
33 | $this->version = $version;
34 | }
35 |
36 | public function isPropagationStopped(): bool
37 | {
38 | return $this->failed;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Version/ShowVersionListener.php:
--------------------------------------------------------------------------------
1 | changelogEntry();
16 | $event->output()->writeln($changelogEntry->contents);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Version/TagCommandConfigListener.php:
--------------------------------------------------------------------------------
1 | version();
23 |
24 | $event->output()->writeln(sprintf('Preparing to tag version %s', $version));
25 |
26 | if (
27 | ! $this->tagWithChangelog(
28 | $event->tagName(),
29 | $event->package(),
30 | $version,
31 | $event->changelog()
32 | )
33 | ) {
34 | $event->tagOperationFailed();
35 | return;
36 | }
37 |
38 | $event->taggingComplete();
39 | }
40 |
41 | private function tagWithChangelog(string $tagName, string $package, string $version, string $changelog): bool
42 | {
43 | $tempFile = tempnam(sys_get_temp_dir(), 'KAC');
44 | file_put_contents($tempFile, sprintf("%s %s\n\n%s", $package, $version, $changelog));
45 |
46 | $command = sprintf('git tag -s -F %s %s', $tempFile, $tagName);
47 | system($command, $return);
48 |
49 | unlink($tempFile);
50 |
51 | return 0 === $return;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Version/ValidateVersionToUseListener.php:
--------------------------------------------------------------------------------
1 | version()) {
19 | // null is a valid version for this workflow; equates to "most recent"
20 | return;
21 | }
22 |
23 | parent::__invoke($event);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Version/VerifyProviderCanReleaseListener.php:
--------------------------------------------------------------------------------
1 | config();
16 | $providerSpec = $config->provider();
17 |
18 | if (! $providerSpec->isComplete()) {
19 | $event->providerIsIncomplete();
20 | return;
21 | }
22 |
23 | $provider = $providerSpec->createProvider();
24 |
25 | if (! $provider->canCreateRelease()) {
26 | $event->providerIsIncomplete();
27 | return;
28 | }
29 |
30 | $event->discoveredProvider($provider);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Version/VerifyTagExistsListener.php:
--------------------------------------------------------------------------------
1 |
21 | * function(string $command[, array &$output[, int &$return]])
22 | *
23 | *
24 | * @var callable
25 | */
26 | public $exec = 'exec';
27 |
28 | public function __invoke(ReleaseEvent $event): void
29 | {
30 | $command = sprintf('git show %s', $event->tagName());
31 | $exec = $this->exec;
32 | $exec($command, $output, $return);
33 | if (0 !== $return) {
34 | $event->couldNotFindTag();
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Version/VerifyVersionHasReleaseDateListener.php:
--------------------------------------------------------------------------------
1 | findReleaseDateForVersion(
24 | file_get_contents($event->config()->changelogFile()),
25 | $event->version(),
26 | true
27 | );
28 | } catch (ChangelogMissingDateException $e) {
29 | $event->changelogMissingDate();
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------