├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpspec.yml ├── readmegen ├── readmegen.yml ├── spec ├── Config │ └── LoaderSpec.php ├── Input │ └── ParserSpec.php ├── Log │ ├── DecoratorSpec.php │ └── ExtractorSpec.php ├── Output │ ├── Format │ │ └── MdSpec.php │ └── WriterSpec.php ├── ReadmeGenSpec.php └── Vcs │ ├── ParserSpec.php │ └── Type │ └── GitSpec.php └── src ├── Bootstrap.php ├── Config └── Loader.php ├── Input └── Parser.php ├── Log ├── Decorator.php └── Extractor.php ├── Output ├── Format │ ├── FormatInterface.php │ └── Md.php └── Writer.php ├── ReadmeGen.php ├── Shell.php └── Vcs ├── Parser.php └── Type ├── AbstractType.php ├── Git.php └── TypeInterface.php /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/private/ 2 | vendor/ 3 | bin/ 4 | nbproject/ 5 | composer.lock 6 | .idea/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | dist: trusty 3 | 4 | php: 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - hhvm 9 | 10 | before_script: 11 | - composer self-update 12 | - composer install --prefer-source --no-interaction --dev 13 | 14 | script: 15 | - vendor/phpspec/phpspec/bin/phpspec run -f dot 16 | 17 | matrix: 18 | include: 19 | - php: 5.3 20 | dist: precise 21 | allow_failures: 22 | - php: hhvm 23 | fast_finish: true 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 fojuth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReadmeGen 2 | 3 | [![Build Status](https://travis-ci.org/fojuth/readmegen.svg)](https://travis-ci.org/fojuth/readmegen) 4 | 5 | ## MAINTAINER NEEDED 6 | 7 | Sadly, I don't have the time to develop **readmegen**. It's great to see it's being used by many people! It would be sad if this project would die. Maybe you can help? There's not much work to do, since the community is very helpful and provides PRs. 8 | 9 | If you're willing to push **readmegen** further on, let me know. 10 | 11 | --- 12 | 13 | Generate your project's log using VCS commit messages. 14 | 15 | ReadmeGen is a PHP package that scans the VCS's log searching for messages with specific pattern. These messages are extracted, grouped and prepended to the changelog file (e.g. readme.md). The package can be instructed to fetch messages between specific tags (or commits). This way, whenever you're tagging a new release, you can run ReadmeGen to generate the changelog automatically. 16 | 17 | **Notice**: The package currently supports only GIT and the *.md output files. You can provide support for othe VCS's or output formats? Help welcome :) 18 | 19 | ### Installation 20 | #### Global installation (recommended) 21 | ``` 22 | composer global require fojuth/readmegen:@stable 23 | ``` 24 | 25 | You can read more about global installation in the [composer docs](https://getcomposer.org/doc/03-cli.md#global). 26 | 27 | #### Local installation 28 | ``` 29 | composer require fojuth/readmegen:1.* 30 | ``` 31 | 32 | #### Windows installation 33 | Make sure the Windows `PATH` variable contains the path to the composer bin dir: 34 | ``` 35 | C:\Users\{USER}\AppData\Roaming\Composer\vendor\bin 36 | ``` 37 | Restart any shell terminal you want to use for changes to take effect. 38 | 39 | ### Usage 40 | This package is intended to be used as an executable script, not a library you would include in your project. Assuming you installed ReadmeGen globally, to update your changelog file, simply run: 41 | 42 | ``` 43 | readmegen --from TAG --to TAG --release RELEASE_NUMBER --break BREAKPOINT 44 | ``` 45 | 46 | For example: 47 | ``` 48 | readmegen --from 1.12.0 --to 1.13.0 --release 1.13.0 --break *Changelog* 49 | ``` 50 | 51 | This tells the script to generate a changelod update named `1.13.0` and that it should scan the log since tag `1.12.0` up to `1.13.0`. No earlier (or latter) commits will be taken into consideration. ReadmeGen will inject the generated log *after* the `*Changelog*` line. 52 | 53 | If you want to generate the changelog from a specific tag (or commit checksum) up to the latest commit (`HEAD`) just omit the `--to` argument: 54 | ``` 55 | readmegen --from a04cf99 --release 1.13.0 --break *Changelog* 56 | ``` 57 | 58 | You can also specify the breakpoint in the `readmegen.yml` config file so the command will be even cleaner: 59 | ``` 60 | readmegen --from a04cf99 --release 1.13.0 61 | ``` 62 | 63 | ### Message format 64 | ReadmeGen will search for messages that start with a specific keyword. These keywords tell the script to which group the commit should be appended. The message groups can be overwritten. 65 | 66 | For example - the default configuration supports four types of commits: Features, Bugfixes, Documentation and Refactoring. The commit will be appended to a certain group only if it starts with a specific word. The default config allows two keywords for bugfixes: `bugfix` and `fix`. This means, that for a message to be appended to the Bugfix group it has to start with either `bugfix: blabla` or `Fix: foo bar` (notice the colon `:` sign - it has to be right after the keyword). The keywords are case insensitive. 67 | 68 | All commits that do not fit into any of the groups will be ignored (we don't want merges and stuff like that in the changelog). 69 | 70 | ### Grouping commits 71 | Each commit that fits into a group will be grouped (yeah, that sounds silly). Groups will be printed out in the order they appear in the config file, so if you have `Features` and `Bugfixes`, this is the order they will appear in the changelog: 72 | ``` 73 | Features 74 | - feature 1 75 | - feature 2 76 | 77 | Bugfixes 78 | - fix 1 79 | ``` 80 | 81 | You can override the groups in your custom config file (details below). 82 | 83 | ### Link patterns 84 | ReadmeGen can link issues to a issue tracker - all numbers starting with `#` will be linked to a website defined in the config under the `issue_tracker_pattern` key. If a commit message has a string `#1234` in it, it will be converted to a link targeting the issue tracker. 85 | 86 | ### Local config 87 | The default config holds the definitions of commit groups and the issue link pattern. It also specifies which VCS to use and the type of the output file. You can override these settings (project-wide) by creating a `readmegen.yml` file in the root dir of your project. When ReadmeGen will be run it will check if this file exists and merge the settings accordingly. 88 | 89 | The default `readmegen.yml` config looks like this: 90 | ``` 91 | vcs: git 92 | format: md 93 | issue_tracker_pattern: http://some.issue.tracker.com/\1 94 | break: "## Changelog" 95 | output_file_name: "README.md" 96 | message_groups: 97 | Features: 98 | - feature 99 | - feat 100 | Bugfixes: 101 | - fix 102 | - bugfix 103 | Documentation: 104 | - docs 105 | Refactoring: 106 | - refactoring 107 | ``` 108 | 109 | Each of the `message_groups` key is the name of the group that will be put in the changelog. The values inside the group are the keywords the commit must start with (followed by the colon `:` sign) to be appended to that group. 110 | 111 | ### Release number 112 | ReadmeGen requires a release number (`--release`) to be provided. This will be the title of the generated changelog. 113 | 114 | ### Breakpoint 115 | By default the changes will go onto the beginning of the changelog file. You can though specify a "breakpoint" beneath which these changes should be appended. Usually, you'll have some "intro" in you changelog, and the changes listed below. You don't want the script to push the changes on top of the file, but below a certain line. You can specify this line in the `readmegen.yml` config file or using the `--break` argument. 116 | 117 | For example: 118 | ``` 119 | readmegen --from 1.12.0 --to 1.13.0 --release 1.3.3 --break *Changelog* 120 | ``` 121 | The script will append the changes *below* the line that contains the `*Changelog*` phrase. This should be the only phrase in this line. If you use the CLI argument method (`--break`), the breakpoint **must not contain spaces**. Thus you are encouraged to use the config method - you can use spaces there, as shown in the default config. 122 | 123 | ReadmeGen will search for the `## Changelog` breakpoint by default. If the breakpoint phrase is not found, the output will go onto the beginning of the changelog file. 124 | 125 | ### Example commits 126 | Here are some example commit messages that will be grabbed by ReadmeGen (with the default config): 127 | ``` 128 | feature: Added some cool stuff (#1234) 129 | fix: #4245, regarding client login bug 130 | docs: Updated the transaction section of the docs 131 | feat: Some more cool stuff 132 | feat(username): Here is more some cool stuffs done by username 133 | ``` 134 | 135 | ## Changelog 136 | ## 1.2.0 137 | *(2019-02-13)* 138 | 139 | #### Fixed 140 | * Use symfony yaml to 3.2 141 | * Use phpspec version ^2.5 ((thanks to [murrant](https://github.com/murrant))) 142 | 143 | #### Features 144 | * Handled username next to keyword ((thanks to [ingluife](https://github.com/ingluife))) 145 | 146 | #### Bugfixes 147 | * Use symfony yaml to 3.2 148 | * Use phpspec version ^2.5 149 | 150 | --- 151 | 152 | ## 1.1.3 153 | *(2018-08-16)* 154 | 155 | #### Added 156 | * Added message groups according to https://keepachangelog.com/ 157 | 158 | --- 159 | 160 | ## 1.1.2 161 | *(2015-07-12)* 162 | 163 | #### Features 164 | * Change output file name (thanks to [reva2](https://github.com/reva2)) 165 | 166 | #### Bugfixes 167 | * Added missing new line character in example usage message (thanks to [reva2](https://github.com/reva2)) 168 | 169 | --- 170 | 171 | ## 1.1.1 172 | *(2015-01-04)* 173 | 174 | #### Features 175 | * Added .travis.yml 176 | 177 | --- 178 | 179 | ## 1.1.0 180 | *(2014-12-30)* 181 | 182 | #### Features 183 | * Added "break" to the readmegen.yml default config file. It has a default value set and can be overwritten locally. 184 | 185 | --- 186 | 187 | ## 1.0.2 188 | *(2014-12-30)* 189 | 190 | #### Bugfixes 191 | * The release date is extracted from the --to commit. 192 | 193 | --- 194 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fojuth/readmegen", 3 | "description": "Readme file / doc generator. It uses VCS logs as a source of information.", 4 | "keywords": ["readme", "generator", "log parser", "vcs"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Kamil Fojuth", 9 | "email": "fojuth@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "symfony/yaml": "^3.2 || ~2.1", 14 | "ulrichsg/getopt-php": "2.*" 15 | }, 16 | "require-dev": { 17 | "phpspec/phpspec": "^2.5" 18 | }, 19 | "config": { 20 | "bin-dir": "bin" 21 | }, 22 | "bin": [ 23 | "readmegen" 24 | ], 25 | "autoload": { 26 | "psr-4": { 27 | "ReadmeGen\\": "src/" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpspec.yml: -------------------------------------------------------------------------------- 1 | suites: 2 | readmegen: 3 | namespace: ReadmeGen 4 | psr4_prefix: ReadmeGen -------------------------------------------------------------------------------- /readmegen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | count($argv)) { 22 | die("Example usage: php generate.php --from --release \n"); 23 | } 24 | 25 | new \ReadmeGen\Bootstrap($argv); -------------------------------------------------------------------------------- /readmegen.yml: -------------------------------------------------------------------------------- 1 | vcs: git 2 | format: md 3 | issue_tracker_pattern: http://some.issue.tracker.com/\1 4 | break: "## Changelog" 5 | output_file_name: "README.md" 6 | message_groups: 7 | Added: 8 | - added 9 | - add 10 | Changed: 11 | - changed 12 | - change 13 | Deprecated: 14 | - deprecated 15 | - deprecate 16 | Removed: 17 | - removed 18 | - remove 19 | Fixed: 20 | - fixed 21 | - fix 22 | Security: 23 | - security 24 | Features: 25 | - feature 26 | - feat 27 | Bugfixes: 28 | - fix 29 | - bugfix 30 | Documentation: 31 | - docs 32 | Refactoring: 33 | - refactoring -------------------------------------------------------------------------------- /spec/Config/LoaderSpec.php: -------------------------------------------------------------------------------- 1 | dummyConfigFile, "vcs: git\nfoo: bar"); 16 | file_put_contents($this->badConfigFile, "badly:\tformed\n\tfile"); 17 | } 18 | 19 | function letgo() 20 | { 21 | unlink($this->dummyConfigFile); 22 | unlink($this->badConfigFile); 23 | } 24 | 25 | function it_should_throw_exception_when_default_config_doesnt_exist() 26 | { 27 | $this->shouldThrow('\InvalidArgumentException')->during('get', array('foobar.yml')); 28 | } 29 | 30 | function it_should_throw_exception_when_the_config_file_is_malformed() 31 | { 32 | $this->shouldThrow('\Symfony\Component\Yaml\Exception\ParseException')->during('get', array($this->badConfigFile)); 33 | } 34 | 35 | function it_loads_the_default_config() 36 | { 37 | $this->get($this->dummyConfigFile)->shouldBeArray(); 38 | } 39 | 40 | function it_should_have_specific_values_loaded() 41 | { 42 | $this->get($this->dummyConfigFile)->shouldHaveKey('vcs'); 43 | } 44 | 45 | function getMatchers() 46 | { 47 | return array( 48 | 'haveKey' => function($subject, $key) { 49 | return array_key_exists($key, $subject); 50 | }, 51 | ); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /spec/Input/ParserSpec.php: -------------------------------------------------------------------------------- 1 | setInput('someDummyContent --f=foo --to=bar --release=4.5.6'); 12 | 13 | $result = $this->parse(); 14 | 15 | $result['from']->shouldReturn('foo'); 16 | $result['f']->shouldReturn('foo'); 17 | $result['to']->shouldReturn('bar'); 18 | $result['t']->shouldReturn('bar'); 19 | $result['release']->shouldReturn('4.5.6'); 20 | $result['r']->shouldReturn('4.5.6'); 21 | } 22 | 23 | function it_should_check_for_required_arguments() 24 | { 25 | $this->setInput('someDummyContent --from=1.2'); 26 | $this->shouldThrow('\BadMethodCallException')->during('parse'); 27 | 28 | $this->setInput('someDummyContent --release=1.2'); 29 | $this->shouldThrow('\BadMethodCallException')->during('parse'); 30 | 31 | $this->setInput('someDummyContent --release=1.2 --from=2.3'); 32 | $result = $this->parse(); 33 | 34 | $result['release']->shouldBe('1.2'); 35 | $result['from']->shouldBe('2.3'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spec/Log/DecoratorSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($formatter); 15 | } 16 | 17 | function it_should_set_the_correct_output_class(FormatInterface $formatter) 18 | { 19 | $formatter->setLog(array())->shouldBeCalled(); 20 | $formatter->setIssueTrackerUrlPattern('')->shouldBeCalled(); 21 | $formatter->decorate()->willReturn('foo'); 22 | 23 | $this->setLog(array()); 24 | $this->setIssueTrackerUrlPattern(''); 25 | $this->decorate()->shouldReturn('foo'); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /spec/Log/ExtractorSpec.php: -------------------------------------------------------------------------------- 1 | array('feature', 'feat'), 25 | 'Bugfixes' => array('bugfix', 'fix'), 26 | 'Docs' => array('docs'), 27 | ); 28 | 29 | $result = array( 30 | 'Features' => array( 31 | 'bar baz', 32 | 'dummy feature', 33 | 'lol', 34 | ), 35 | 'Bugfixes' => array( 36 | 'some bugfix', 37 | ), 38 | ); 39 | 40 | $this->setLog($log); 41 | $this->setMessageGroups($messageGroups); 42 | 43 | $this->extract()->shouldReturn($result); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /spec/Output/Format/MdSpec.php: -------------------------------------------------------------------------------- 1 | array( 14 | 'bar #123 baz', 15 | 'dummy feature', 16 | ), 17 | 'Bugfixes' => array( 18 | 'some bugfix (#890)', 19 | ), 20 | ); 21 | 22 | function let() { 23 | $this->setLog($this->log); 24 | } 25 | 26 | function it_should_add_links_to_the_issue_tracker() 27 | { 28 | $result = array( 29 | 'Features' => array( 30 | "bar [#123]({$this->issueTrackerUrl}123) baz", 31 | 'dummy feature', 32 | ), 33 | 'Bugfixes' => array( 34 | "some bugfix ([#890]({$this->issueTrackerUrl}890))", 35 | ), 36 | ); 37 | 38 | $this->setIssueTrackerUrlPattern($this->issueTrackerPattern); 39 | $this->decorate()->shouldReturn($result); 40 | } 41 | 42 | function it_should_generate_a_write_ready_output() { 43 | $this->setRelease('4.5.6') 44 | ->setDate(new \DateTime('2014-12-21')); 45 | 46 | $result = array( 47 | "## 4.5.6", 48 | "*(2014-12-21)*", 49 | "\n#### Features", 50 | '* bar #123 baz', 51 | '* dummy feature', 52 | "\n#### Bugfixes", 53 | '* some bugfix (#890)', 54 | "\n---\n", 55 | ); 56 | 57 | $this->generate()->shouldReturn($result); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /spec/Output/WriterSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($formatter); 16 | file_put_contents($this->fileNameWithBreakpoint, "line one\n{$this->break}\nline two\n##{$this->break}\nline three"); 17 | } 18 | 19 | function letgo() 20 | { 21 | @ unlink($this->fileName); 22 | @ unlink($this->fileNameWithBreakpoint); 23 | } 24 | 25 | function it_should_write_log_output_to_a_file(FormatInterface $formatter) 26 | { 27 | $logContent = array( 28 | 'Features:', 29 | '- foo', 30 | '- bar', 31 | ); 32 | 33 | $formatter->generate()->willReturn($logContent); 34 | $formatter->getFileName()->willReturn($this->fileName); 35 | 36 | $this->write(); 37 | 38 | if (false === file_exists($this->fileName)) { 39 | throw new \Exception(sprintf('File %s has not been created.', $this->fileName)); 40 | } 41 | 42 | $content = file_get_contents($this->fileName); 43 | 44 | if (true === empty($content)) { 45 | throw new \Exception(sprintf('File %s is empty.', $this->fileName)); 46 | } 47 | 48 | if (trim($content) !== join("\n", $logContent)) { 49 | throw new \Exception('File content differs from expectations.'); 50 | } 51 | } 52 | 53 | function it_should_add_content_after_breakpoint(FormatInterface $formatter) 54 | { 55 | $logContent = array( 56 | 'Features:', 57 | '- foo', 58 | '- bar', 59 | ); 60 | 61 | $resultContent = array( 62 | 'line one', 63 | $this->break, 64 | 'Features:', 65 | '- foo', 66 | '- bar', 67 | '', 68 | 'line two', 69 | '##'.$this->break, 70 | 'line three', 71 | ); 72 | 73 | $formatter->generate()->willReturn($logContent); 74 | $formatter->getFileName()->willReturn($this->fileNameWithBreakpoint); 75 | 76 | $this->setBreak($this->break); 77 | $this->write(); 78 | 79 | $content = file_get_contents($this->fileNameWithBreakpoint); 80 | 81 | if (trim($content) !== join("\n", $resultContent)) { 82 | throw new \Exception('File content differs from expectations.'); 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /spec/ReadmeGenSpec.php: -------------------------------------------------------------------------------- 1 | 'dummyvcs', 20 | 'foo' => 'bar', 21 | 'message_groups' => array( 22 | 'Features' => array( 23 | 'feat', 'feature' 24 | ), 25 | 'Bugfixes' => array( 26 | 'fix', 'bugfix' 27 | ), 28 | ), 29 | ); 30 | protected $badConfigFile = 'bad_config.yaml'; 31 | protected $badConfig = "vcs: nope\nfoo: bar"; 32 | protected $gitConfigFile = 'git_config.yaml'; 33 | protected $gitConfig = "vcs: git\nmessage_groups:\n Features:\n - feat\n - feature\n Bugfixes:\n - fix\n - bugfix\nformat: md\nissue_tracker_pattern: http://issue.tracker.com/\\1"; 34 | protected $outputFile = 'dummy.md'; 35 | 36 | function let() 37 | { 38 | file_put_contents($this->dummyConfigFile, $this->dummyConfig); 39 | file_put_contents($this->badConfigFile, $this->badConfig); 40 | 41 | $this->beConstructedWith(new ConfigLoader, $this->dummyConfigFile, true); 42 | } 43 | 44 | function letgo() 45 | { 46 | unlink($this->dummyConfigFile); 47 | unlink($this->badConfigFile); 48 | @ unlink($this->gitConfigFile); 49 | @ unlink($this->outputFile); 50 | } 51 | 52 | function it_should_load_default_config() 53 | { 54 | $this->getConfig()->shouldBe($this->dummyConfigArray); 55 | } 56 | 57 | function it_loads_the_correct_vcs_parser() 58 | { 59 | $config = $this->getConfig(); 60 | 61 | $config['vcs']->shouldBe('dummyvcs'); 62 | 63 | $this->getParser()->shouldHaveType('\ReadmeGen\Vcs\Parser'); 64 | $this->getParser()->getVcsParser()->shouldHaveType('\ReadmeGen\Vcs\Type\Dummyvcs'); 65 | } 66 | 67 | function it_throws_exception_when_trying_to_load_nonexisting_vcs_parser() 68 | { 69 | $this->beConstructedWith(new ConfigLoader, $this->badConfigFile, true); 70 | $this->shouldThrow('\InvalidArgumentException')->during('getParser'); 71 | } 72 | 73 | function it_runs_the_whole_process(Shell $shell) 74 | { 75 | file_put_contents($this->gitConfigFile, $this->gitConfig); 76 | 77 | $shell->beADoubleOf('\ReadmeGen\Shell'); 78 | $shell->run(sprintf('git log --pretty=format:"%%s%s%%b" 1.2.3..4.0.0', Git::MSG_SEPARATOR))->willReturn($this->getLogAsString()); 79 | 80 | $this->beConstructedWith(new ConfigLoader, $this->gitConfigFile, true); 81 | 82 | $this->getParser()->getVcsParser()->shouldHaveType('\ReadmeGen\Vcs\Type\Git'); 83 | $this->getParser()->setArguments(array( 84 | 'from' => '1.2.3', 85 | 'to' => '4.0.0', 86 | )); 87 | $this->getParser()->setShellRunner($shell); 88 | 89 | $log = $this->getParser()->parse(); 90 | 91 | $this->setExtractor(new Extractor()); 92 | $logGrouped = $this->extractMessages($log)->shouldReturn(array( 93 | 'Features' => array( 94 | 'bar baz #123', 95 | 'dummy feature', 96 | 'lol', 97 | ), 98 | 'Bugfixes' => array( 99 | 'some bugfix', 100 | 'fixed by foobar' 101 | ) 102 | )); 103 | 104 | $formatter = new Md(); 105 | $formatter->setFileName($this->outputFile) 106 | ->setRelease('4.5.6') 107 | ->setDate(new \DateTime(2014-12-12)); 108 | 109 | $this->setDecorator(new Decorator($formatter)); 110 | $this->getDecoratedMessages($logGrouped)->shouldReturn(array( 111 | 'Features' => array( 112 | 'bar baz [#123](http://issue.tracker.com/123)', 113 | 'dummy feature', 114 | 'lol', 115 | ), 116 | 'Bugfixes' => array( 117 | 'some bugfix', 118 | 'fixed by foobar' 119 | ) 120 | )); 121 | 122 | $outputWriter = new Writer($formatter); 123 | 124 | $this->setOutputWriter($outputWriter); 125 | $this->writeOutput()->shouldReturn(true); 126 | } 127 | 128 | protected function getLogAsString() 129 | { 130 | $log = array( 131 | 'foo', 132 | 'feature: bar baz #123', 133 | 'nope', 134 | 'feature: dummy feature', 135 | 'feat: lol', 136 | 'also nope', 137 | 'fix: some bugfix', 138 | 'bugfix(foobar): fixed by foobar' 139 | ); 140 | 141 | return join(Git::MSG_SEPARATOR."\n", $log).Git::MSG_SEPARATOR."\n"; 142 | } 143 | 144 | } 145 | 146 | } 147 | 148 | /** 149 | * Dummy VCS type class used by ReadmeGen during tests. 150 | */ 151 | namespace ReadmeGen\Vcs\Type { 152 | 153 | class Dummyvcs extends \ReadmeGen\Vcs\Type\AbstractType 154 | { 155 | 156 | public function parse() 157 | { 158 | return array(); 159 | } 160 | 161 | public function getToDate(){ 162 | 163 | } 164 | 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /spec/Vcs/ParserSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($vcs); 13 | } 14 | 15 | function it_should_parse_the_vcs_log_into_an_array(TypeInterface $vcs) 16 | { 17 | $returnData = array( 18 | 'foo bar', 19 | 'baz', 20 | ); 21 | 22 | $vcs->parse()->willReturn($returnData); 23 | 24 | $this->parse()->shouldBe($returnData); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/Vcs/Type/GitSpec.php: -------------------------------------------------------------------------------- 1 | setArguments(array('from' => '1.0')); 15 | 16 | $log = sprintf("Foo bar.%s\nDummy message.%s\n\n", Git::MSG_SEPARATOR, Git::MSG_SEPARATOR); 17 | $shell->run($this->getCommand())->willReturn($log); 18 | 19 | $this->setShellRunner($shell); 20 | 21 | $this->parse()->shouldReturn(array( 22 | 'Foo bar.', 23 | 'Dummy message.', 24 | )); 25 | } 26 | 27 | function it_has_input_options_and_arguments() 28 | { 29 | $this->setOptions(array('a')); 30 | $this->setArguments(array('foo' => 'bar')); 31 | 32 | $this->hasOption('z')->shouldReturn(false); 33 | $this->hasOption('a')->shouldReturn(true); 34 | 35 | $this->hasArgument('wat')->shouldReturn(false); 36 | $this->hasArgument('foo')->shouldReturn(true); 37 | $this->getArgument('foo')->shouldReturn('bar'); 38 | } 39 | 40 | function it_should_add_options_and_arguments_to_the_command(Shell $shell) 41 | { 42 | $log = sprintf("Foo bar.%s\nDummy message.%s\n\n", Git::MSG_SEPARATOR, Git::MSG_SEPARATOR); 43 | $shell->run(sprintf('git log --pretty=format:"%%s%s%%b"', Git::MSG_SEPARATOR))->willReturn($log); 44 | 45 | $this->setShellRunner($shell); 46 | 47 | $this->setOptions(array('x', 'y')); 48 | $this->setArguments(array('foo' => 'bar', 'baz' => 'wat', 'from' => '1.0')); 49 | 50 | $this->getCommand()->shouldReturn('git log --pretty=format:"%s'.Git::MSG_SEPARATOR.'%b" 1.0..HEAD --x --y'); 51 | } 52 | 53 | function it_should_properly_include_the_from_and_to_arguments() { 54 | $this->setOptions(array('x', 'y')); 55 | 56 | $this->setArguments(array('from' => '3.4.5', 'foo' => 'bar')); 57 | $this->getCommand()->shouldReturn('git log --pretty=format:"%s'.Git::MSG_SEPARATOR.'%b" 3.4.5..HEAD --x --y'); 58 | 59 | $this->setArguments(array('from' => '3.4.5', 'foo' => 'bar', 'to' => '4.0')); 60 | $this->getCommand()->shouldReturn('git log --pretty=format:"%s'.Git::MSG_SEPARATOR.'%b" 3.4.5..4.0 --x --y'); 61 | } 62 | 63 | function it_returns_the_date_of_the_commit(Shell $shell) { 64 | $shell->run('git log -1 -s --format=%ci 3f04264')->willReturn('2014-11-28 01:01:58 +0100'); 65 | 66 | $this->setShellRunner($shell); 67 | 68 | $this->setArguments(array('from' => '1.0', 'to' => '3f04264')); 69 | $this->getToDate()->shouldReturn('2014-11-28'); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Bootstrap.php: -------------------------------------------------------------------------------- 1 | setInput(join(' ', $input)); 18 | 19 | // Parse the input 20 | try { 21 | $input = $inputParser->parse(); 22 | } catch (\BadMethodCallException $e) { 23 | die($e->getMessage()); 24 | } 25 | 26 | // Run the whole process 27 | $this->run($input->getOptions()); 28 | } 29 | 30 | /** 31 | * Generates the output file. 32 | * 33 | * @param array $options 34 | */ 35 | public function run(array $options) 36 | { 37 | $this->generator = new ReadmeGen(new ConfigLoader()); 38 | 39 | $this->setupParser($options); 40 | 41 | // Extract useful log entries 42 | $logGrouped = $this->generator->setExtractor(new Extractor()) 43 | ->extractMessages($this->getLog()); 44 | 45 | $config = $this->generator->getConfig(); 46 | 47 | $formatterClassName = '\ReadmeGen\Output\Format\\' . ucfirst($config['format']); 48 | 49 | // Create the output formatter 50 | $formatter = new $formatterClassName; 51 | 52 | $formatter 53 | ->setRelease($options['release']) 54 | ->setFileName($config['output_file_name']) 55 | ->setDate($this->getToDate()); 56 | 57 | // Pass decorated log entries to the generator 58 | $this->generator->setDecorator(new Decorator($formatter)) 59 | ->getDecoratedMessages($logGrouped); 60 | 61 | $writer = new Writer($formatter); 62 | 63 | // If present, respect the breakpoint in the existing output file 64 | $break = $this->getBreak($options, $config); 65 | 66 | if (false === empty($break)) { 67 | $writer->setBreak($break); 68 | } 69 | 70 | // Write the output 71 | $this->generator->setOutputWriter($writer) 72 | ->writeOutput(); 73 | } 74 | 75 | /** 76 | * Returns the parsed log. 77 | * 78 | * @return mixed 79 | */ 80 | public function getLog() 81 | { 82 | return $this->generator->getParser() 83 | ->parse(); 84 | } 85 | 86 | /** 87 | * Returns the date of the latter commit (--to). 88 | * 89 | * @return string 90 | */ 91 | protected function getToDate() 92 | { 93 | $date = $this->generator->getParser() 94 | ->getToDate(); 95 | 96 | return new \DateTime($date); 97 | } 98 | 99 | /** 100 | * Sets the parser. 101 | * 102 | * @param array $options 103 | */ 104 | protected function setupParser(array $options) 105 | { 106 | $this->generator->getParser() 107 | ->setArguments($options) 108 | ->setShellRunner(new Shell); 109 | } 110 | 111 | /** 112 | * Returns the breakpoint if set, null otherwise. 113 | * 114 | * @param array $options 115 | * @param array $config 116 | * @return null|string 117 | */ 118 | protected function getBreak(array $options, array $config){ 119 | if (true === isset($options['break'])) { 120 | return $options['break']; 121 | } 122 | 123 | if (true === isset($config['break'])) { 124 | return $config['break']; 125 | } 126 | 127 | return null; 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/Config/Loader.php: -------------------------------------------------------------------------------- 1 | getFileContent($path)); 22 | 23 | if (false === empty($sourceConfig)) { 24 | return array_replace_recursive($sourceConfig, $config); 25 | } 26 | 27 | return $config; 28 | } 29 | 30 | /** 31 | * Returns the file's contents. 32 | * 33 | * @param string $path Path to file. 34 | * @return string 35 | * @throws \InvalidArgumentException When the file does not exist. 36 | */ 37 | protected function getFileContent($path) 38 | { 39 | if (false === file_exists($path)) { 40 | throw new \InvalidArgumentException(sprintf('File "%s" does not exist.', $path)); 41 | } 42 | 43 | return file_get_contents($path); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Input/Parser.php: -------------------------------------------------------------------------------- 1 | handler = new Getopt(array( 32 | new Option('r', 'release', Getopt::REQUIRED_ARGUMENT), 33 | new Option('f', 'from', Getopt::REQUIRED_ARGUMENT), 34 | new Option('t', 'to', Getopt::OPTIONAL_ARGUMENT), 35 | new Option('b', 'break', Getopt::OPTIONAL_ARGUMENT), 36 | )); 37 | } 38 | 39 | /** 40 | * Set the input. 41 | * 42 | * @param $input string 43 | */ 44 | public function setInput($input) 45 | { 46 | $inputArray = explode(' ', $input); 47 | 48 | array_shift($inputArray); 49 | 50 | $this->input = join(' ', $inputArray); 51 | } 52 | 53 | /** 54 | * Parses the input and returns the Getopt handler. 55 | * 56 | * @return Getopt 57 | */ 58 | public function parse() 59 | { 60 | $this->handler->parse($this->input); 61 | 62 | $output = $this->handler->getOptions(); 63 | 64 | if (false === isset($output['from'])) { 65 | throw new \BadMethodCallException('The --from argument is required.'); 66 | } 67 | 68 | if (false === isset($output['release'])) { 69 | throw new \BadMethodCallException('The --release argument is required.'); 70 | } 71 | 72 | return $this->handler; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Log/Decorator.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 23 | } 24 | 25 | /** 26 | * Log setter. 27 | * 28 | * @param array $log 29 | * @return $this 30 | */ 31 | public function setLog(array $log) 32 | { 33 | $this->formatter->setLog($log); 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * Issue tracker pattern setter. 40 | * 41 | * @param string $pattern 42 | * @return $this 43 | */ 44 | public function setIssueTrackerUrlPattern($pattern) 45 | { 46 | $this->formatter->setIssueTrackerUrlPattern($pattern); 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Returns the decorated log. 53 | * 54 | * @return FormatInterface 55 | */ 56 | public function decorate() 57 | { 58 | return $this->formatter->decorate(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Log/Extractor.php: -------------------------------------------------------------------------------- 1 | log = $log; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Message groups setter. 53 | * 54 | * @param array $messageGroups 55 | * @return $this 56 | */ 57 | public function setMessageGroups(array $messageGroups) 58 | { 59 | $this->messageGroups = $messageGroups; 60 | 61 | // Set the joined message groups as well 62 | foreach ($this->messageGroups as $header => $keywords) { 63 | $this->messageGroupsJoined[$header] = join('|', $keywords); 64 | } 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Groups messages and returns them. 71 | * 72 | * @return array 73 | */ 74 | public function extract() { 75 | foreach ($this->log as $line) { 76 | foreach ($this->messageGroupsJoined as $header => $keywords) { 77 | $pattern = $this->getPattern($keywords); 78 | 79 | if (preg_match($pattern, $line)) { 80 | $this->appendToGroup($header, $line, $pattern); 81 | } 82 | } 83 | } 84 | 85 | // Remove empty groups 86 | foreach (array_keys($this->messageGroups) as $groupKey) { 87 | if (true === empty($this->groups[$groupKey])) { 88 | unset($this->messageGroups[$groupKey]); 89 | } 90 | } 91 | 92 | // The array_merge sorts $messageGroups basing on $groups 93 | return array_merge($this->messageGroups, $this->groups); 94 | } 95 | 96 | /** 97 | * Appends a message to a group 98 | * 99 | * @param string $groupHeader 100 | * @param string $text 101 | * @param string $pattern 102 | */ 103 | protected function appendToGroup($groupHeader, $text, $pattern) { 104 | $this->groups[$groupHeader][] = trim(preg_replace($pattern, '', $text)); 105 | } 106 | 107 | /** 108 | * Returns the regexp pattern used to determine the log entry's group. 109 | * 110 | * @param string $keywords 111 | * @return string 112 | */ 113 | protected function getPattern($keywords) { 114 | return '/(^('.$keywords.')?\([^()]*[^()]*\):)|(^('.$keywords.'):)/i'; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Output/Format/FormatInterface.php: -------------------------------------------------------------------------------- 1 | log = $log; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Issue tracker patter setter. 57 | * 58 | * @param $pattern 59 | * @return mixed 60 | */ 61 | public function setIssueTrackerUrlPattern($pattern) 62 | { 63 | $this->pattern = $pattern; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Decorates the output (e.g. adds linkgs to the issue tracker) 70 | * 71 | * @return self 72 | */ 73 | public function decorate() 74 | { 75 | foreach ($this->log as &$entries) { 76 | array_walk($entries, array($this, 'injectLinks')); 77 | } 78 | 79 | return $this->log; 80 | } 81 | 82 | /** 83 | * Injects issue tracker links into the log. 84 | * 85 | * @param string $entry Log entry. 86 | */ 87 | protected function injectLinks(&$entry) 88 | { 89 | $entry = preg_replace('/#(\d+)/', "[#\\1]({$this->pattern})", $entry); 90 | } 91 | 92 | /** 93 | * Returns a write-ready log. 94 | * 95 | * @return array 96 | */ 97 | public function generate() 98 | { 99 | if (true === empty($this->log)) { 100 | return array(); 101 | } 102 | 103 | $log = array(); 104 | 105 | // Iterate over grouped entries 106 | foreach ($this->log as $header => &$entries) { 107 | 108 | // Add a group header (e.g. Bugfixes) 109 | $log[] = sprintf("\n#### %s", $header); 110 | 111 | // Iterate over entries 112 | foreach ($entries as &$line) { 113 | $message = explode(VCS::MSG_SEPARATOR, $line); 114 | 115 | $log[] = sprintf("* %s", trim($message[0])); 116 | 117 | // Include multi-line entries 118 | if (true === isset($message[1])) { 119 | $log[] = sprintf("\n %s", trim($message[1])); 120 | } 121 | } 122 | } 123 | 124 | // Return a write-ready log 125 | return array_merge(array("## {$this->release}", "*({$this->date->format('Y-m-d')})*"), $log, array("\n---\n")); 126 | } 127 | 128 | /** 129 | * Returns the output filename. 130 | * 131 | * @return string 132 | */ 133 | public function getFileName() 134 | { 135 | return $this->fileName; 136 | } 137 | 138 | /** 139 | * Output filename setter. 140 | * 141 | * @param $fileName 142 | * @return mixed 143 | */ 144 | public function setFileName($fileName) 145 | { 146 | $this->fileName = $fileName; 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * Release number setter. 153 | * 154 | * @param $release 155 | * @return mixed 156 | */ 157 | public function setRelease($release) { 158 | $this->release = $release; 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * Creation date setter. 165 | * 166 | * @param \DateTime $date 167 | * @return mixed 168 | */ 169 | public function setDate(\DateTime $date) { 170 | $this->date = $date; 171 | 172 | return $this; 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/Output/Writer.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 30 | } 31 | 32 | /** 33 | * Writes the output to a file. 34 | * 35 | * @return bool 36 | */ 37 | public function write() 38 | { 39 | // Crete the file if it does not exist 40 | $this->makeFile($this->formatter->getFileName()); 41 | 42 | // Contents of the original file 43 | $fileContent = file_get_contents($this->formatter->getFileName()); 44 | 45 | // Final log 46 | $log = join("\n", (array) $this->formatter->generate())."\n"; 47 | 48 | // Include the breakpoint 49 | if (false === empty($this->break) && 1 === preg_match("/^{$this->break}/m", $fileContent)) { 50 | $splitFileContent = preg_split("/^{$this->break}/m", $fileContent); 51 | 52 | file_put_contents($this->formatter->getFileName(), $splitFileContent[0].$this->break."\n".$log.$splitFileContent[1]); 53 | 54 | return true; 55 | } 56 | 57 | file_put_contents($this->formatter->getFileName(), $log.$fileContent); 58 | 59 | return true; 60 | } 61 | 62 | /** 63 | * Create the file if it does not exist. 64 | * 65 | * @param string $fileName 66 | */ 67 | protected function makeFile($fileName){ 68 | if (file_exists($fileName)) { 69 | return; 70 | } 71 | 72 | touch($fileName); 73 | } 74 | 75 | /** 76 | * Breakpoint setter. 77 | * 78 | * @param null|string $break 79 | * @return $this 80 | */ 81 | public function setBreak($break = null) 82 | { 83 | if (false === empty($break)) { 84 | $this->break = $break; 85 | } 86 | 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/ReadmeGen.php: -------------------------------------------------------------------------------- 1 | defaultConfigPath); 66 | 67 | // Overriding the root config 68 | $configPath = (false === empty($defaultConfigPath) ? $defaultConfigPath : $rootConfigPath); 69 | 70 | $this->configLoader = $configLoader; 71 | $this->defaultConfig = $this->configLoader->get($configPath); 72 | 73 | $localConfigPath = realpath($this->defaultConfigPath); 74 | 75 | // Merging local config 76 | if (file_exists($localConfigPath) && false === $ignoreLocalConfig) { 77 | $this->config = $this->configLoader->get($localConfigPath, $this->defaultConfig); 78 | } 79 | else { 80 | $this->config = $this->defaultConfig; 81 | } 82 | } 83 | 84 | /** 85 | * Returns the config. 86 | * 87 | * @return array 88 | */ 89 | public function getConfig() 90 | { 91 | return $this->config; 92 | } 93 | 94 | /** 95 | * Returns the parser. 96 | * 97 | * @return Parser 98 | * @throws \InvalidArgumentException When the VCS parser class does not exist. 99 | */ 100 | public function getParser() 101 | { 102 | if (true === empty($this->parser)) { 103 | $typeParserClassName = sprintf('\ReadmeGen\Vcs\Type\%s', ucfirst($this->config['vcs'])); 104 | 105 | if (false === class_exists($typeParserClassName)) { 106 | throw new \InvalidArgumentException(sprintf('Class "%s" does not exist', $typeParserClassName)); 107 | } 108 | 109 | $this->parser = new Parser(new $typeParserClassName()); 110 | } 111 | 112 | return $this->parser; 113 | } 114 | 115 | public function setExtractor(Extractor $extractor) 116 | { 117 | $this->extractor = $extractor; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Returns messages extracted from the log. 124 | * 125 | * @param array $log 126 | * @return array 127 | */ 128 | public function extractMessages(array $log = null) 129 | { 130 | if (true === empty($log)) { 131 | return array(); 132 | } 133 | 134 | $this->extractor->setMessageGroups($this->config['message_groups']); 135 | 136 | return $this->extractor->setLog($log) 137 | ->extract(); 138 | } 139 | 140 | /** 141 | * 142 | * phpspec failed to properly resolve the aliased version of this interface. 143 | * 144 | * @param \ReadmeGen\Log\Decorator $decorator 145 | * @return $this 146 | */ 147 | public function setDecorator(Decorator $decorator) 148 | { 149 | $this->decorator = $decorator; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Returns decorated log messages. 156 | * 157 | * @param array $log 158 | * @return array|Output\Format\FormatInterface 159 | */ 160 | public function getDecoratedMessages(array $log = null) 161 | { 162 | if (true === empty($log)) { 163 | return array(); 164 | } 165 | 166 | return $this->decorator->setLog($log) 167 | ->setIssueTrackerUrlPattern($this->config['issue_tracker_pattern']) 168 | ->decorate(); 169 | } 170 | 171 | public function setOutputWriter(OutputWriter $output) 172 | { 173 | $this->output = $output; 174 | 175 | return $this; 176 | } 177 | 178 | /** 179 | * Writes the ready output to the file. 180 | * 181 | * @return mixed 182 | */ 183 | public function writeOutput() 184 | { 185 | return $this->output->write(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Shell.php: -------------------------------------------------------------------------------- 1 | vcs = $vcs; 23 | } 24 | 25 | /** 26 | * Returns the parsed log. 27 | * 28 | * @return array 29 | */ 30 | public function parse() 31 | { 32 | return $this->vcs->parse(); 33 | } 34 | 35 | /** 36 | * Returns the VCS parser. 37 | * 38 | * @return TypeInterface 39 | */ 40 | public function getVcsParser() 41 | { 42 | return $this->vcs; 43 | } 44 | 45 | /** 46 | * Shell runner setter. 47 | * 48 | * @param Shell $shell 49 | * @return $this 50 | */ 51 | public function setShellRunner(Shell $shell) 52 | { 53 | $this->vcs->setShellRunner($shell); 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * Sets input options. 60 | * 61 | * @param array $options 62 | * @return $this 63 | */ 64 | public function setOptions(array $options = null) 65 | { 66 | $this->vcs->setOptions($options); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Sets input arguments. 73 | * 74 | * @param array $arguments 75 | * @return $this 76 | */ 77 | public function setArguments(array $arguments = null) 78 | { 79 | $this->vcs->setArguments($arguments); 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Returns the date of the latter (--to) commit, in the format YYYY-MM-DD. 86 | * 87 | * @return string 88 | */ 89 | public function getToDate(){ 90 | return $this->vcs->getToDate(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Vcs/Type/AbstractType.php: -------------------------------------------------------------------------------- 1 | shell = $shell; 38 | } 39 | 40 | /** 41 | * Runs the shell command and returns the result. 42 | * 43 | * @param $command 44 | * @return string 45 | */ 46 | protected function runCommand($command) 47 | { 48 | return $this->shell->run($command); 49 | } 50 | 51 | /** 52 | * Input option setter. 53 | * 54 | * @param array $options 55 | * @return mixed 56 | */ 57 | public function setOptions(array $options = null) 58 | { 59 | $this->options = $options; 60 | } 61 | 62 | /** 63 | * Input argument setter. 64 | * 65 | * @param array $arguments 66 | * @return mixed 67 | */ 68 | public function setArguments(array $arguments = null) 69 | { 70 | $this->arguments = $arguments; 71 | } 72 | 73 | /** 74 | * Returns true if an option exists. 75 | * 76 | * @param $option 77 | * @return mixed 78 | */ 79 | public function hasOption($option) 80 | { 81 | return in_array($option, $this->options); 82 | } 83 | 84 | /** 85 | * Returns all options. 86 | * 87 | * @return mixed 88 | */ 89 | public function getOptions() 90 | { 91 | return $this->options; 92 | } 93 | 94 | /** 95 | * Returns true if an argument exists. 96 | * 97 | * @param $argument 98 | * @return mixed 99 | */ 100 | public function hasArgument($argument) 101 | { 102 | return isset($this->arguments[$argument]); 103 | } 104 | 105 | /** 106 | * Returns the argument's value. 107 | * 108 | * @param $argument 109 | * @return mixed 110 | */ 111 | public function getArgument($argument) 112 | { 113 | return $this->arguments[$argument]; 114 | } 115 | 116 | /** 117 | * Return all arguments. 118 | * 119 | * @return mixed 120 | */ 121 | public function getArguments() 122 | { 123 | return $this->arguments; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Vcs/Type/Git.php: -------------------------------------------------------------------------------- 1 | getLog()))); 13 | } 14 | 15 | /** 16 | * Returns the base VCS log command. 17 | * 18 | * @return string 19 | */ 20 | protected function getBaseCommand() 21 | { 22 | return 'git log --pretty=format:"%s{{MSG_SEPARATOR}}"'; 23 | } 24 | 25 | /** 26 | * Returns the compiled VCS log command. 27 | * 28 | * @return string 29 | */ 30 | public function getCommand() 31 | { 32 | $options = $this->getOptions(); 33 | $arguments = $this->getArguments(); 34 | 35 | $to = null; 36 | $from = $arguments['from']; 37 | 38 | if (true === isset($arguments['to'])) { 39 | $to = $arguments['to']; 40 | } 41 | 42 | array_walk($options, function (&$option) { 43 | $option = '--' . $option; 44 | }); 45 | 46 | array_walk($arguments, function (&$value, $argument) { 47 | $value = '--' . $argument . '=' . $value; 48 | }); 49 | 50 | return trim(sprintf('%s %s %s', $this->getBaseCommand(), $this->getRange($from, $to), join(' ', $options))); 51 | } 52 | 53 | protected function getLog() 54 | { 55 | return $this->runCommand($this->getCommand()); 56 | } 57 | 58 | protected function getRange($from, $to = null) 59 | { 60 | $range = $from . '..'; 61 | 62 | return $range . (($to) ?: 'HEAD'); 63 | } 64 | 65 | 66 | public function getToDate() 67 | { 68 | $arguments = $this->getArguments(); 69 | 70 | $to = (true === isset($arguments['to'])) ? $arguments['to'] : 'HEAD'; 71 | 72 | $fullDate = $this->runCommand(sprintf('git log -1 -s --format=%%ci %s', $to)); 73 | $date = explode(' ', $fullDate); 74 | 75 | return $date[0]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Vcs/Type/TypeInterface.php: -------------------------------------------------------------------------------- 1 |