├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── pronto.yml │ ├── push_gem.yml │ └── specs.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── pronto ├── lib ├── pronto.rb └── pronto │ ├── bitbucket.rb │ ├── bitbucket_server.rb │ ├── cli.rb │ ├── client.rb │ ├── clients │ ├── bitbucket_client.rb │ └── bitbucket_server_client.rb │ ├── comment.rb │ ├── config.rb │ ├── config_file.rb │ ├── error.rb │ ├── formatter │ ├── base.rb │ ├── bitbucket_formatter.rb │ ├── bitbucket_pull_request_formatter.rb │ ├── bitbucket_server_pull_request_formatter.rb │ ├── checkstyle_formatter.rb │ ├── colorizable.rb │ ├── commit_formatter.rb │ ├── formatter.rb │ ├── git_formatter.rb │ ├── github_combined_status_formatter.rb │ ├── github_formatter.rb │ ├── github_pull_request_formatter.rb │ ├── github_pull_request_review_formatter.rb │ ├── github_status_formatter.rb │ ├── github_status_formatter │ │ ├── sentence.rb │ │ └── status_builder.rb │ ├── gitlab_formatter.rb │ ├── gitlab_merge_request_review_formatter.rb │ ├── json_formatter.rb │ ├── null_formatter.rb │ ├── pull_request_formatter.rb │ ├── text_formatter.rb │ └── text_message_decorator.rb │ ├── gem_names.rb │ ├── git │ ├── line.rb │ ├── patch.rb │ ├── patches.rb │ └── repository.rb │ ├── github.rb │ ├── github_pull.rb │ ├── gitlab.rb │ ├── logger.rb │ ├── message.rb │ ├── plugin.rb │ ├── rake_task │ └── travis_pull_request.rb │ ├── runner.rb │ ├── runners.rb │ ├── status.rb │ └── version.rb ├── pronto.gemspec ├── pronto.gif └── spec ├── fixtures ├── message_with_path.xml ├── message_without_line.xml ├── message_without_path.xml ├── new_file.txt ├── renamed-file.git │ ├── HEAD │ ├── config │ ├── index │ ├── logs │ │ ├── HEAD │ │ └── refs │ │ │ └── heads │ │ │ ├── master │ │ │ └── new_branch │ ├── objects │ │ ├── 20 │ │ │ └── 6ab76efefefb5b457107f9e841ccf06c3db4eb │ │ ├── 81 │ │ │ └── d7ad66f39011d0eedaa70e33bae27b67c3c519 │ │ ├── 93 │ │ │ └── 657bccfddbce46b58c23960fea84103d90d37d │ │ ├── 97 │ │ │ └── 422ff3a9482718dae96ab9afc6b60c16450076 │ │ ├── 3a │ │ │ └── 57a3eff4ae575cedf9643a36e61422fe9be74b │ │ ├── 3e │ │ │ └── 2cbd49d2d9d33b6c9eea4371eb67c947920f71 │ │ ├── a1 │ │ │ └── 69cecd3261a284b1c3bfd84ad4eb2c7cdcd7e4 │ │ └── fd │ │ │ └── 497adbc2fec28a3a13f49693b7a03dbfe8f542 │ └── refs │ │ └── heads │ │ ├── master │ │ └── new_branch └── test.git │ ├── HEAD │ ├── config │ ├── index │ ├── logs │ ├── HEAD │ └── refs │ │ └── heads │ │ └── master │ ├── objects │ ├── 14 │ │ └── cf9b86fa9661e967c8a526374384a4f10fb5ea │ ├── 22 │ │ └── 51181228419719f7d926b5326d744ad871ad4a │ ├── 56 │ │ └── 0c7d57b1ce19e1852c5193e826b62c25f7db4b │ ├── 57 │ │ └── 7afa184c9bc82a66c40047d0809e5fcc43489f │ ├── 64 │ │ └── dadfdb7c7437476782e8eb024085862e6287d6 │ ├── 92 │ │ └── 8676bf506a52bf693b82a29f45db812e4f18d6 │ ├── 97 │ │ ├── 1783b0dbb66e03b18379a8604d2e1bb64cbb01 │ │ └── 8cbcccb92bdfa34587dd175f183a2678f0c9b3 │ ├── 07 │ │ └── b7623cc56ccfc25ef45a65e4723b3d720504b6 │ ├── 3e │ │ └── 0e3ab0a436fc2a9c05253a439dc6084699b7d5 │ ├── 3f │ │ └── c0911fde02260ba650ca3bd428f2bb5f88e93f │ ├── 5a │ │ └── 6e1531cd6d79810356e7ea7c77485514db28c8 │ ├── 7b │ │ └── 21c8f4dfb0b8aa39739fc16678c5934877a414 │ ├── 8f │ │ └── a4900507f731bb4c827c606facdf8c76aa29ab │ ├── ac │ │ └── 86326d7231ad77dab94e2c4f6f61245a2d9bec │ ├── af │ │ └── eef802b84cab684cf1b99fdbd444fe57c41141 │ ├── d0 │ │ └── e5047bf0930d6cfa151e65bc0aaaa4a7e7de0d │ ├── d6 │ │ └── d56582ebfd0c6c3263ea4c4e2d727048370124 │ ├── e6 │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ ├── ec │ │ └── 05bab7d263d5e01be99f2c4e10a5974e24e6de │ └── ef │ │ └── 4cc92872c2e34ea4b508e024df2b80a2268f1b │ └── refs │ └── heads │ └── master ├── pronto ├── bitbucket_server_spec.rb ├── bitbucket_spec.rb ├── clients │ ├── bitbucket_client_spec.rb │ └── bitbucket_server_client_spec.rb ├── comment_spec.rb ├── config_file_spec.rb ├── config_spec.rb ├── formatter │ ├── bitbucket_formatter_spec.rb │ ├── bitbucket_pull_request_formatter_spec.rb │ ├── bitbucket_server_pull_request_formatter_spec.rb │ ├── checkstyle_formatter_spec.rb │ ├── colorizable_spec.rb │ ├── formatter_spec.rb │ ├── github_combined_status_formatter_spec.rb │ ├── github_formatter_spec.rb │ ├── github_pull_request_formatter_spec.rb │ ├── github_pull_request_review_formatter_spec.rb │ ├── github_status_formatter │ │ └── sentence_spec.rb │ ├── github_status_formatter_spec.rb │ ├── gitlab_formatter_spec.rb │ ├── gitlab_merge_request_review_formatter_spec.rb │ ├── json_formatter_spec.rb │ ├── null_formatter_spec.rb │ └── text_formatter_spec.rb ├── gem_names_spec.rb ├── git │ ├── line_spec.rb │ ├── patch_spec.rb │ ├── patches_spec.rb │ └── repository_spec.rb ├── github_pull_spec.rb ├── github_spec.rb ├── gitlab_spec.rb ├── logger_spec.rb ├── message_spec.rb ├── runner_spec.rb ├── runners_spec.rb └── status_spec.rb ├── pronto_spec.rb ├── spec_helper.rb └── support └── coverage.rb /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Order is important. The last matching pattern takes the most precedence. 2 | # Default owners for everything in the repo. 3 | * @prontolabs/core 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/pronto.yml: -------------------------------------------------------------------------------- 1 | name: Pronto 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | pronto: 9 | runs-on: ubuntu-24.04 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Set up Ruby 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: 2.5 18 | bundler-cache: true 19 | - name: Run Pronto 20 | run: bundle exec pronto run --exit-code -c origin/${{ github.base_ref }} 21 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'prontolabs/pronto' 14 | runs-on: ubuntu-24.04 15 | 16 | permissions: 17 | contents: write 18 | id-token: write 19 | 20 | steps: 21 | - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 22 | with: 23 | egress-policy: audit 24 | 25 | - uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb # v4.2.2 26 | 27 | - uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0 28 | with: 29 | bundler-cache: true 30 | ruby-version: '3.4' 31 | 32 | - uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 33 | -------------------------------------------------------------------------------- /.github/workflows/specs.yml: -------------------------------------------------------------------------------- 1 | name: Specs 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-24.04 12 | strategy: 13 | matrix: 14 | ruby: ['2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4'] 15 | exclude: 16 | - ruby: "2.3" # Rugged uses the wrong openssl version on CI and segfaults (similar to https://github.com/libgit2/rugged/issues/718) 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use specific gitlab gem version (if required) 20 | if: matrix.ruby == '2.4' 21 | run: echo "gem 'gitlab', '< 4.14.1'" >> Gemfile.local 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | bundler-cache: true 27 | - name: Test & publish code coverage 28 | uses: paambaati/codeclimate-action@f429536ee076d758a24705203199548125a28ca7 # v9.0.0 29 | env: 30 | CC_TEST_REPORTER_ID: 3d676246ffa66d3fdef6253a9870431b1a2da04e9ecb25486c08a38823c37b6a 31 | COVERAGE: true 32 | with: 33 | coverageCommand: bundle exec rspec 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | .DS_Store 5 | Gemfile.lock 6 | coverage 7 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --require spec_helper 3 | --profile 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | 4 | Documentation: 5 | Enabled: false 6 | 7 | SignalException: 8 | EnforcedStyle: only_raise 9 | 10 | MultilineOperationIndentation: 11 | EnforcedStyle: indented 12 | 13 | MultilineMethodCallIndentation: 14 | EnforcedStyle: indented 15 | 16 | Metrics/AbcSize: 17 | Max: 25 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 0.11.4 6 | 7 | ### Changes 8 | 9 | * [#477](https://github.com/prontolabs/pronto/pull/477) fix uninitialized constant error with BitBucket integration 10 | * [#479](https://github.com/prontolabs/pronto/pull/479) relax octokit dependency to allow 10.x releases 11 | 12 | ## 0.11.3 13 | 14 | ### Changes 15 | 16 | * [#455](https://github.com/prontolabs/pronto/pull/455) compatibility fixes for supporting octokit 8.x 17 | * [#460](https://github.com/prontolabs/pronto/pull/460) improve documentation for Gitlab CI integration 18 | * [#462](https://github.com/prontolabs/pronto/pull/462) more doc improvements for Gitlab CI integration 19 | * [#466](https://github.com/prontolabs/pronto/pull/466) relax octokit dependency to allow 9.x releases 20 | 21 | ## 0.11.2 22 | 23 | ### Changes 24 | 25 | * [#449](https://github.com/prontolabs/pronto/pull/449) relax octokit version dependency to allow releases up to 8.0 26 | * [#450](https://github.com/prontolabs/pronto/pull/450) introduce Pronto::Formatter.register for adding custom formatters 27 | 28 | ## 0.11.1 29 | 30 | ### New features 31 | 32 | * [#371](https://github.com/prontolabs/pronto/pull/371) allow to filter runners via config 33 | * [#402](https://github.com/prontolabs/pronto/pull/402) add --workdir option 34 | * [#410](https://github.com/prontolabs/pronto/pull/410) allow the default commit run against to be configured 35 | * [#435](https://github.com/prontolabs/pronto/pull/435) allow override of config file path via PRONTO_CONFIG_FILE 36 | 37 | ### Bugs fixed 38 | 39 | * [#422](https://github.com/prontolabs/pronto/pull/422) fix Gitlab#slug_regex when URL has different host 40 | * [#423](https://github.com/prontolabs/pronto/pull/423) show existing message count when reporting posted messages 41 | 42 | ### Changes 43 | 44 | * [#397](https://github.com/prontolabs/pronto/pull/397) migrate CI to GitHub Actions 45 | * [#398](https://github.com/prontolabs/pronto/pull/398) fix duplicated runs on GitHub Actions 46 | * [#403](https://github.com/prontolabs/pronto/pull/403) run pronto on GitHub Actions without any reporting 47 | * [#408](https://github.com/prontolabs/pronto/pull/408) require rexml ~> 3.2.5 due to CVE-2021-28965 48 | * [#409](https://github.com/prontolabs/pronto/pull/409) add article about GitHub Actions to README.md 49 | * [#414](https://github.com/prontolabs/pronto/pull/414) fix typo in README.md 50 | * [#415](https://github.com/prontolabs/pronto/pull/415) remove deprecated pronto-bundler_audit from README.md 51 | * [#417](https://github.com/prontolabs/pronto/pull/417) relax rugged dependency to allow releases up to 1.2.0 52 | * [#424](https://github.com/prontolabs/pronto/pull/424) add Ruby 3.1 to the test matrix 53 | * [#431](https://github.com/prontolabs/pronto/pull/431) relax rugged dependency to allow releases up to 2.0 54 | * [#436](https://github.com/prontolabs/pronto/pull/436) update dead links in README.md 55 | * [#438](https://github.com/prontolabs/pronto/pull/438) add Ruby 3.2 to the test matrix 56 | * [#439](https://github.com/prontolabs/pronto/pull/439) relax dependencies to allow currently released major versions 57 | 58 | ## 0.11.0 59 | 60 | ### New features 61 | 62 | * [#304](https://github.com/prontolabs/pronto/pull/304) add option to limit comments per PR review 63 | * [#333](https://github.com/prontolabs/pronto/pull/333) add github_combined_status formatter 64 | * [#334](https://github.com/prontolabs/pronto/pull/334) add configurable review_type for GitHub (with REQUEST_CHANGES as default) 65 | * [#351](https://github.com/prontolabs/pronto/pull/351) add gitLab_mr formatter 66 | * [#369](https://github.com/prontolabs/pronto/pull/369) make Pronto::Git::Patch#new_file_path public 67 | * update to the BitBucket 2.0 API (_as the 1.0 API was deprecated_) via [#347](https://github.com/prontolabs/pronto/pull/347), [#348](https://github.com/prontolabs/pronto/pull/348), [#352](https://github.com/prontolabs/pronto/pull/352) and [#354](https://github.com/prontolabs/pronto/pull/354) 68 | 69 | ### Bugs fixed 70 | 71 | * [#344](https://github.com/prontolabs/pronto/pull/344) treat Gemfile and .gemspecs as Ruby 72 | * [#380](https://github.com/prontolabs/pronto/pull/380) fix compatibility with rugged >= 0.99 73 | * [#387](https://github.com/prontolabs/pronto/pull/387) fix running pronto inside git submodules 74 | 75 | ### Changes 76 | 77 | * [#370](https://github.com/prontolabs/pronto/pull/370) allow thor 1.x gem versions 78 | * [#379](https://github.com/prontolabs/pronto/pull/379) allow rugged 1.0.x gem versions 79 | * [#386](https://github.com/prontolabs/pronto/pull/386) add ruby 2.7 to CI 80 | * [#390](https://github.com/prontolabs/pronto/pull/390) fix issue with generating Sorbet RBI 81 | * [#396](https://github.com/prontolabs/pronto/pull/396) add support for Ruby 3.0 82 | * document/improve GitHub Actions integration in README.md via [#360](https://github.com/prontolabs/pronto/pull/360), [#378](https://github.com/prontolabs/pronto/pull/378) and [#389](https://github.com/prontolabs/pronto/pull/389) 83 | * add links to additional pronto runners in README.md 84 | 85 | ## 0.10.0 86 | 87 | ### New features 88 | 89 | * [#301](https://github.com/prontolabs/pronto/pull/301): add ability to auto approve Bitbucket pull requests. 90 | * [#331](https://github.com/prontolabs/pronto/pull/331): allow to specify PATH in "run" command. 91 | 92 | ### Bugs fixed 93 | 94 | * [#258](https://github.com/prontolabs/pronto/pull/258): fix blame returning nil when file does not exist in the git tree. 95 | * [#270](https://github.com/prontolabs/pronto/pull/270): fix ${line} in text format to mean line number. 96 | * [#282](https://github.com/prontolabs/pronto/issues/282): relax rainbow dependency. 97 | * [#329](https://github.com/prontolabs/pronto/pull/329): correctly handle renamed-only files. 98 | 99 | ### Changes 100 | 101 | * Depend on thor `0.20.*`. 102 | * [#298](https://github.com/prontolabs/pronto/pull/298): change default GitLab API endpoint to v4. 103 | * [#332](https://github.com/prontolabs/pronto/pull/332): remove support for Ruby older than 2.3.0. 104 | 105 | ## 0.9.5 106 | 107 | ### Bugs fixed 108 | 109 | * [#253](https://github.com/prontolabs/pronto/pull/253): fix an infinite loop when Bitbucket Server sends a paginated response. 110 | 111 | ### Changes 112 | 113 | * [#250](https://github.com/prontolabs/pronto/issues/250): allow HTTParty `0.15.*`. 114 | 115 | ## 0.9.4 116 | 117 | ### Changes 118 | 119 | * [#227](https://github.com/prontolabs/pronto/issues/227): the repository was converted from an individual one (mmozuras/pronto) to an org (prontolabs/pronto). 120 | * [#247](https://github.com/prontolabs/pronto/pull/247): try to find GitHub pull request by sha when HEAD is detached. 121 | 122 | ### Bugs fixed 123 | 124 | * [#235](https://github.com/prontolabs/pronto/pull/235): do not submit empty pull request reviews to GitHub. 125 | 126 | ## 0.9.3 127 | 128 | ### Bugs fixed 129 | 130 | * [#234](https://github.com/prontolabs/pronto/pull/234): text formatter was not working, require delegate.rb in text_message_decorator.rb to fix. 131 | 132 | ## 0.9.2 133 | 134 | ### Bugs fixed 135 | 136 | * [#231](https://github.com/prontolabs/pronto/pull/231): GitHub pull request review formatter was not working in some cases without Accept header. 137 | 138 | ## 0.9.1 139 | 140 | ### Bugs fixed 141 | 142 | * Poper and some other runners were not working correctly. When using staged/unstaged flags, pass a string instead of Rugged::Commit as commit parameter for runners. 143 | 144 | ## 0.9.0 145 | 146 | ### New features 147 | 148 | * [#206](https://github.com/prontolabs/pronto/pull/216): add Bitbucket Server pull request formatter. 149 | * [#204](https://github.com/prontolabs/pronto/pull/204): add ability configure message format for each formatter. 150 | * [#111](https://github.com/prontolabs/pronto/issues/111): add `--staged` option for `pronto run` to analyze staged changes. 151 | * [#217](https://github.com/prontolabs/pronto/issues/217): add GitHub pull request review formatter. 152 | 153 | ### Changes 154 | 155 | * [#193](https://github.com/prontolabs/pronto/issues/193): rename `pronto run --index` option to `--unstaged`. 156 | * [#49](https://github.com/prontolabs/pronto/issues/49): handle nonexistence of GitHub pull requests gracefully. 157 | * [#217](https://github.com/prontolabs/pronto/issues/217): depend on `octokit >= 4.7.0`. 158 | * [#224](https://github.com/prontolabs/pronto/issues/224): depend on `gitlab >= 4.0.0`. 159 | * [#222](https://github.com/prontolabs/pronto/pull/184): prefix PULL_REQUEST_ID env variable with `PRONTO_`. 160 | 161 | ### Bugs fixed 162 | 163 | * [#215](https://github.com/prontolabs/pronto/pull/215): an exclusion of files for single runner led to those files being excluded for all runners. 164 | 165 | ## 0.8.2 166 | 167 | ### Bugs fixed 168 | 169 | * [#203](https://github.com/prontolabs/pronto/pull/203): fix unintentional class conversion that led to exclude config option not working. 170 | 171 | ## 0.8.1 172 | 173 | ### Changes 174 | 175 | * Add a default GitLab API endpoint: https://gitlab.com/api/v3. 176 | 177 | ### Bugs fixed 178 | 179 | * [#125](https://github.com/prontolabs/pronto/issues/125): check whether message has a line before posting to GitLab. 180 | * Post on commit comments on correct commit: use message.commit_sha to set comment.sha instead of head. 181 | * [#201](https://github.com/prontolabs/pronto/issues/201): allow messages without line positions or paths. 182 | 183 | ## 0.8.0 184 | 185 | ### New features 186 | 187 | * [#199](https://github.com/prontolabs/pronto/pull/199): add support for Ruby 2.4.0. 188 | 189 | ### Changes 190 | 191 | * [#181](https://github.com/prontolabs/pronto/pull/181): add ENV variables for all configuration options. 192 | * [#184](https://github.com/prontolabs/pronto/pull/184): prefix all ENV variables with `PRONTO_`. 193 | * [#185](https://github.com/prontolabs/pronto/pull/185): allow excluding files to lint for single runner. 194 | 195 | ### Bugs fixed 196 | 197 | * [#179](https://github.com/prontolabs/pronto/pull/179): correctly select branch name for fix Bitbucket pull request formatter. 198 | * [#187](https://github.com/prontolabs/pronto/pull/187): correctly handle nil/false with consolidate_comments config option. 199 | * [#189](https://github.com/prontolabs/pronto/pull/189): do not post anything when all consolidated comments already exist. 200 | * [#195](https://github.com/prontolabs/pronto/pull/195): fix warning for default formatters value. 201 | 202 | ## 0.7.1 203 | 204 | ### Changes 205 | 206 | * Remove support for Ruby 1.9.3. 207 | 208 | ### Bugs fixed 209 | 210 | * [#149](https://github.com/prontolabs/pronto/issues/149): use patches to correctly find line position for GitHub pull request formatter. 211 | 212 | ## 0.7.0 213 | 214 | ### New features 215 | 216 | * [#135](https://github.com/prontolabs/pronto/pull/135): add Bitbucket formatter. 217 | * [#135](https://github.com/prontolabs/pronto/pull/135): add Bitbucket pull request formatter. 218 | * [#134](https://github.com/prontolabs/pronto/pull/134): colorize text formatter. 219 | * [#144](https://github.com/prontolabs/pronto/pull/144): add GitHub status formatter. 220 | * [#157](https://github.com/prontolabs/pronto/pull/157): ability to run pronto CLI from within subdirectories of a git repository. 221 | * [#154](https://github.com/prontolabs/pronto/pull/154): add an option to consolidate pull request comments. 222 | 223 | ### Changes 224 | 225 | * [#162](https://github.com/prontolabs/pronto/pull/162): don't count info messages for error exit code. 226 | 227 | ### Bugs fixed 228 | 229 | * [#153](https://github.com/prontolabs/pronto/pull/153): correctly get repo_path. 230 | 231 | ## 0.6.0 232 | 233 | ### New features 234 | 235 | * Add `-V/--verbose-version` option that displays Ruby version. 236 | * [#127](https://github.com/prontolabs/pronto/pull/127): ability to specify `max_warnings` via configuration or environment variable. 237 | * [#18](https://github.com/prontolabs/pronto/issues/18): ability to specify `verbose` via configuration, which can provide more output for debugging purposes. 238 | * [#83](https://github.com/prontolabs/pronto/issues/83): support multiple formatters as an option to `pronto run`. 239 | 240 | ### Changes 241 | 242 | * `--version` only displays the version itself without any additional text. 243 | * Replace `Pronto.gem_names` with `Pronto::GemNames.new.to_a`. 244 | * [#116](https://github.com/prontolabs/pronto/pull/116): improve GitHub formatter error output. 245 | * [#123](https://github.com/prontolabs/pronto/pull/126): add runner attribute to message initialization. 246 | * Runner expects to receive patches/commit via `initialize(patches, commit)`, instead of `run(patches, commit)`. 247 | 248 | ### Bugs fixed 249 | 250 | * [#122](https://github.com/prontolabs/pronto/pull/122): ignore symlink directories. 251 | 252 | ## 0.5.3 253 | 254 | ### Bugs fixed 255 | 256 | * Remove pronto.gif from gem, accidently included since `0.5.0`. 257 | 258 | ## 0.5.2 259 | 260 | ### Bugs fixed 261 | 262 | * GithubPullRequestFormatter was working incorrectly when `PULL_REQUEST_ID` is not specified. Introduced in `0.5.1`. 263 | 264 | ## 0.5.1 265 | 266 | ### Changes 267 | 268 | * Try to retrieve commit sha for GitHub PR via GitHub API instead of trusting local sha. 269 | 270 | ## 0.5.0 271 | 272 | ### New features 273 | 274 | * [#104](https://github.com/prontolabs/pronto/pull/104): configure via .pronto.yml file. 275 | * [#86](https://github.com/prontolabs/pronto/pull/86): ability to specify GitHub slug via configuration or environment variable. 276 | * [#77](https://github.com/prontolabs/pronto/pull/77): ability to specify GitHub endpoints via configuration or environment variable. 277 | * [#108](https://github.com/prontolabs/pronto/pull/108): ability to specify excluded files via configuration. 278 | 279 | ### Changes 280 | 281 | * [#82](https://github.com/prontolabs/pronto/pull/82): treat Rake files as Ruby files. 282 | * [#107](https://github.com/prontolabs/pronto/pull/107): use desc: instead of banner: for CLI options descriptions. 283 | 284 | ### Bugs fixed 285 | 286 | * [#87](https://github.com/prontolabs/pronto/pull/87): handle github remote urls without .git suffix. 287 | * [#91](https://github.com/prontolabs/pronto/pull/91): find position in full diff and fix how commit id is used in GithubPullRequestFormatter. 288 | * [#92](https://github.com/prontolabs/pronto/pull/92): ignore failed pull request comments. 289 | * [#93](https://github.com/prontolabs/pronto/pull/93): comments didn't have position when outdated. 290 | * [#94](https://github.com/prontolabs/pronto/pull/94): duplicate comment detection was failing for large GitHub pull requests. 291 | * [poper#4](https://github.com/prontolabs/pronto-poper/issues/4): handle message uniqueness when they're without line numbers. 292 | * [#101](https://github.com/prontolabs/pronto/pull/101): make GitLab work with ssh port urls. 293 | 294 | ## 0.4.3 295 | 296 | ### Changes 297 | 298 | * Depend on `rugged ~> 0.23.0` and `octokit ~> 4.1.0`. 299 | 300 | ## 0.4.2 301 | 302 | ### New features 303 | 304 | * New formatter: NullFormatter. Discards data without writing it anywhere. 305 | 306 | ## 0.4.1 307 | 308 | ### Bugs fixed 309 | 310 | * [#58](https://github.com/prontolabs/pronto/pull/58): GitlabFormatter uses a high +per_page+ value to avoid pagination (and thus duplicate comments). 311 | 312 | ## 0.4.0 313 | 314 | ### New features 315 | 316 | * Try to detect pull request id automatically, if `PULL_REQUEST_ID` is not specified. Inspired by @willnet/prid. 317 | * [#40](https://github.com/prontolabs/pronto/issues/40): add '--index' option for 'pronto run'. Pronto analyzes changes before committing. 318 | * [#50](https://github.com/prontolabs/pronto/pull/50): add GitLab formatter 319 | * [#52](https://github.com/prontolabs/pronto/pull/52): allow specifying a path for 'pronto run'. 320 | 321 | ### Changes 322 | 323 | * GitHub and GitHub pull request formatters now filter out duplicate offenses on the same line to avoid spamming with redundant comments. 324 | 325 | ## 0.3.3 326 | 327 | ### Bugs fixed 328 | 329 | * GithubPullRequestFormatter was working incorrectly with merge commits. 330 | 331 | ## 0.3.2 332 | 333 | ### Bugs fixed 334 | 335 | * GithubPullRequestFormatter had an off-by-one positioning error. 336 | 337 | ## 0.3.1 338 | 339 | ### Bugs fixed 340 | 341 | * Git::Patches#repo was always returning nil. 342 | 343 | ## 0.3.0 344 | 345 | ### New features 346 | 347 | * [#27](https://github.com/prontolabs/pronto/issues/27): '--exit-code' option for 'pronto run'. Pronto exits with non-zero code if there were any warnings/errors. 348 | * [#16](https://github.com/prontolabs/pronto/issues/16): new formatter: GithubPullRequestFormatter. Writes review comments on GitHub pull requests. 349 | 350 | ### Changes 351 | 352 | * [#29](https://github.com/prontolabs/pronto/issues/29): be compatible and depend on rugged '0.21.0'. 353 | * Performance improvement: use Rugged::Blame instead of one provided by Grit. 354 | * Performance improvement: cache comments retrieved from GitHub. 355 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you discover issues, have ideas for improvements or new features, 4 | please report them to the [issue tracker][1] of the repository or 5 | submit a pull request. Please, try to follow these guidelines when you 6 | do so. 7 | 8 | ## Issue reporting 9 | 10 | * Check that the issue has not already been reported. 11 | * Check that the issue has not already been fixed in the latest code 12 | (a.k.a. `master`). 13 | * Be clear, concise and precise in your description of the problem. 14 | * Open an issue with a descriptive title and a summary in grammatically correct, 15 | complete sentences. 16 | * Include any relevant code or traces in the issue summary. 17 | 18 | ## Pull requests 19 | 20 | * Read [how to properly contribute to open source projects on Github][2]. 21 | * Fork the project. 22 | * Use a topic/feature branch to easily amend a pull request later, if necessary. 23 | * Write [good commit messages][3]. 24 | * Use the same coding conventions as the rest of the project. 25 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. You can also test your version of pronto locally with the help of `gem build` and `gem install`. 26 | * Commit and push until you are happy with your contribution. 27 | * Open a [pull request][4] that relates to *only* one subject with a clear title 28 | and description in grammatically correct, complete sentences. 29 | 30 | [1]: https://github.com/prontolabs/pronto/issues 31 | [2]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request 32 | [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 33 | [4]: https://help.github.com/articles/using-pull-requests 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | local_gemfile = File.expand_path('Gemfile.local', __dir__) 6 | eval_gemfile local_gemfile if File.exist?(local_gemfile) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Mindaugas Mozūras 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require 'rubygems' 3 | require 'bundler' 4 | require 'bundler/gem_tasks' 5 | 6 | begin 7 | Bundler.setup(:default, :development) 8 | rescue Bundler::BundlerError => e 9 | $stderr.puts e.message 10 | $stderr.puts 'Run `bundle install` to install missing gems' 11 | exit e.status_code 12 | end 13 | 14 | require 'rake' 15 | require 'rspec/core/rake_task' 16 | 17 | RSpec::Core::RakeTask.new(:spec) 18 | 19 | desc 'Run RSpec with code coverage' 20 | task :coverage do 21 | ENV['COVERAGE'] = 'true' 22 | Rake::Task['spec'].execute 23 | end 24 | 25 | task(:default).clear 26 | task default: [:spec] 27 | -------------------------------------------------------------------------------- /bin/pronto: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pronto' 4 | require 'pronto/cli' 5 | 6 | Pronto::CLI.start 7 | -------------------------------------------------------------------------------- /lib/pronto.rb: -------------------------------------------------------------------------------- 1 | require 'rugged' 2 | require 'octokit' 3 | require 'gitlab' 4 | require 'forwardable' 5 | require 'httparty' 6 | require 'rainbow' 7 | 8 | require 'pronto/error' 9 | 10 | require 'pronto/gem_names' 11 | 12 | require 'pronto/logger' 13 | require 'pronto/config_file' 14 | require 'pronto/config' 15 | 16 | require 'pronto/clients/bitbucket_client' 17 | require 'pronto/clients/bitbucket_server_client' 18 | 19 | require 'pronto/git/repository' 20 | require 'pronto/git/patches' 21 | require 'pronto/git/patch' 22 | require 'pronto/git/line' 23 | 24 | require 'pronto/plugin' 25 | require 'pronto/message' 26 | require 'pronto/comment' 27 | require 'pronto/status' 28 | require 'pronto/runner' 29 | require 'pronto/runners' 30 | require 'pronto/client' 31 | require 'pronto/github' 32 | require 'pronto/gitlab' 33 | require 'pronto/bitbucket' 34 | require 'pronto/bitbucket_server' 35 | 36 | require 'pronto/formatter/colorizable' 37 | require 'pronto/formatter/base' 38 | require 'pronto/formatter/formatter' 39 | require 'pronto/formatter/text_formatter' 40 | require 'pronto/formatter/json_formatter' 41 | require 'pronto/formatter/git_formatter' 42 | require 'pronto/formatter/commit_formatter' 43 | require 'pronto/formatter/pull_request_formatter' 44 | require 'pronto/formatter/github_formatter' 45 | require 'pronto/formatter/github_status_formatter' 46 | require 'pronto/formatter/github_combined_status_formatter' 47 | require 'pronto/formatter/github_pull_request_formatter' 48 | require 'pronto/formatter/github_pull_request_review_formatter' 49 | require 'pronto/formatter/gitlab_formatter' 50 | require 'pronto/formatter/gitlab_merge_request_review_formatter' 51 | require 'pronto/formatter/bitbucket_formatter' 52 | require 'pronto/formatter/bitbucket_pull_request_formatter' 53 | require 'pronto/formatter/bitbucket_server_pull_request_formatter' 54 | require 'pronto/formatter/checkstyle_formatter' 55 | require 'pronto/formatter/null_formatter' 56 | 57 | module Pronto 58 | def self.run(commit = nil, repo_path = '.', 59 | formatters = [Formatter::TextFormatter.new], file = nil) 60 | commit ||= default_commit 61 | 62 | repo = Git::Repository.new(repo_path) 63 | options = { paths: [file] } if file 64 | patches = repo.diff(commit, options) 65 | 66 | result = Runners.new.run(patches) 67 | 68 | Array(formatters).each do |formatter| 69 | formatted = formatter.format(result, repo, patches) 70 | puts formatted if formatted 71 | end 72 | 73 | result 74 | end 75 | 76 | def self.default_commit 77 | Config.new.default_commit 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/pronto/bitbucket.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Bitbucket < Client 3 | def pull_comments(sha) 4 | @comment_cache["#{pull_id}/#{sha}"] ||= begin 5 | client.pull_comments(slug, pull_id).map do |comment| 6 | Comment.new(sha, comment.content, comment.filename, comment.line_to) 7 | end 8 | end 9 | end 10 | 11 | def commit_comments(sha) 12 | @comment_cache[sha.to_s] ||= begin 13 | client.commit_comments(slug, sha).map do |comment| 14 | Comment.new(sha, comment.content, comment.filename, comment.line_to) 15 | end 16 | end 17 | end 18 | 19 | def create_commit_comment(comment) 20 | @config.logger.log("Creating commit comment on #{comment.sha}") 21 | client.create_commit_comment(slug, comment.sha, comment.body, 22 | comment.path, comment.position) 23 | end 24 | 25 | def create_pull_comment(comment) 26 | if comment.path && comment.position 27 | @config.logger.log("Creating pull request comment on #{pull_id}") 28 | client.create_pull_comment(slug, pull_id, comment.body, 29 | comment.path, comment.position) 30 | else 31 | create_commit_comment(comment) 32 | end 33 | end 34 | 35 | def approve_pull_request 36 | client.approve_pull_request(slug, pull_id) 37 | end 38 | 39 | def unapprove_pull_request 40 | client.unapprove_pull_request(slug, pull_id) 41 | end 42 | private 43 | 44 | def slug 45 | return @config.bitbucket_slug if @config.bitbucket_slug 46 | @slug ||= begin 47 | @repo.remote_urls.map do |url| 48 | hostname = Regexp.escape(@config.bitbucket_hostname) 49 | match = %r{.*#{hostname}(:|\/)(?.*?)(?:\.git)?\z}.match(url) 50 | match[:slug] if match 51 | end.compact.first 52 | end 53 | end 54 | 55 | def client 56 | @client ||= BitbucketClient.new(@config.bitbucket_username, 57 | @config.bitbucket_password) 58 | end 59 | 60 | def pull_id 61 | pull ? pull.id.to_i : env_pull_id 62 | end 63 | 64 | def pull 65 | @pull ||= if env_pull_id 66 | pull_requests.find { |pr| pr.id.to_i == env_pull_id } 67 | elsif @repo.branch 68 | pull_requests.find do |pr| 69 | pr.source['branch']['name'] == @repo.branch 70 | end 71 | end 72 | end 73 | 74 | def pull_requests 75 | @pull_requests ||= client.pull_requests(slug) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/pronto/bitbucket_server.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class BitbucketServer < Bitbucket 3 | def pull_comments(sha) 4 | @comment_cache["#{pull_id}/#{sha}"] ||= begin 5 | client.pull_comments(slug, pull_id).map do |comment| 6 | anchor = comment['commentAnchor'] 7 | if anchor 8 | Comment.new(sha, comment['comment']['text'], 9 | anchor['path'], anchor['line']) 10 | end 11 | end.compact 12 | end 13 | end 14 | 15 | private 16 | 17 | def client 18 | @client ||= BitbucketServerClient.new(@config.bitbucket_username, 19 | @config.bitbucket_password, 20 | @config.bitbucket_api_endpoint) 21 | end 22 | 23 | def pull 24 | @pull ||= if env_pull_id 25 | pull_requests.find { |pr| pr.id.to_i == env_pull_id } 26 | elsif @repo.branch 27 | pull_requests.find do |pr| 28 | pr['fromRef']['displayId'] == @repo.branch 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/pronto/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | 3 | module Pronto 4 | class CLI < Thor 5 | require 'pronto' 6 | require 'pronto/version' 7 | 8 | class << self 9 | def is_thor_reserved_word?(word, type) 10 | return false if word == 'run' 11 | super 12 | end 13 | end 14 | 15 | desc 'run [PATH]', 'Run Pronto' 16 | 17 | method_option :'exit-code', 18 | type: :boolean, 19 | desc: 'Exits with non-zero code if there were any warnings/errors.' 20 | 21 | method_option :commit, 22 | type: :string, 23 | aliases: '-c', 24 | desc: 'Commit for the diff' 25 | 26 | method_option :unstaged, 27 | type: :boolean, 28 | aliases: ['-i', '--index'], 29 | desc: 'Analyze changes made, but not in git staging area' 30 | 31 | method_option :staged, 32 | type: :boolean, 33 | desc: 'Analyze changes in git staging area' 34 | 35 | method_option :workdir, 36 | type: :boolean, 37 | aliases: ['-w'], 38 | desc: 'Analyze both staged and unstaged changes' 39 | 40 | method_option :runner, 41 | type: :array, 42 | default: [], 43 | aliases: '-r', 44 | desc: 'Run only the passed runners' 45 | 46 | method_option :formatters, 47 | type: :array, 48 | default: ['text'], 49 | aliases: ['formatter', '-f'], 50 | desc: "Pick output formatters. Available: #{::Pronto::Formatter.names.join(', ')}" 51 | 52 | def run(path = '.') 53 | path = File.expand_path(path) 54 | 55 | gem_names = options[:runner].any? ? options[:runner] : ::Pronto::GemNames.new.to_a 56 | gem_names.each do |gem_name| 57 | require "pronto/#{gem_name}" 58 | end 59 | 60 | formatters = ::Pronto::Formatter.get(options[:formatters]) 61 | 62 | commit_options = %i[workdir staged unstaged index] 63 | commit = commit_options.find { |o| options[o] } || options[:commit] 64 | 65 | repo_workdir = ::Rugged::Repository.discover(path).workdir 66 | relative = path.sub(repo_workdir, '') 67 | 68 | messages = Dir.chdir(repo_workdir) do 69 | file = relative.length != path.length ? relative : nil 70 | ::Pronto.run(commit, '.', formatters, file) 71 | end 72 | if options[:'exit-code'] 73 | error_messages_count = messages.count { |m| m.level != :info } 74 | exit(error_messages_count) 75 | end 76 | rescue Rugged::RepositoryError 77 | puts '"pronto" must be run from within a git repository or must be supplied the path to a git repository' 78 | rescue Pronto::Error => e 79 | $stderr.puts "Pronto errored: #{e.message}" 80 | end 81 | 82 | desc 'list', 'Lists pronto runners that are available to be used' 83 | 84 | def list 85 | puts ::Pronto::GemNames.new.to_a 86 | end 87 | 88 | desc 'version', 'Display version' 89 | map %w[-v --version] => :version 90 | 91 | def version 92 | puts Version::STRING 93 | end 94 | 95 | desc 'verbose-version', 'Display verbose version' 96 | map %w[-V --verbose-version] => :verbose_version 97 | 98 | def verbose_version 99 | puts Version.verbose 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/pronto/client.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Client 3 | def initialize(repo) 4 | @repo = repo 5 | @config = Config.new 6 | @comment_cache = {} 7 | @pull_id_cache = {} 8 | end 9 | 10 | def env_pull_id 11 | if (pull_request = ENV['PULL_REQUEST_ID']) 12 | warn "[DEPRECATION] `PULL_REQUEST_ID` is deprecated. Please use `PRONTO_PULL_REQUEST_ID` instead." 13 | end 14 | 15 | pull_request ||= ENV['PRONTO_PULL_REQUEST_ID'] 16 | pull_request.to_i if pull_request 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/pronto/clients/bitbucket_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ostruct' 4 | 5 | class BitbucketClient 6 | include HTTParty 7 | base_uri 'https://api.bitbucket.org/2.0/repositories' 8 | 9 | def initialize(username, password) 10 | self.class.basic_auth(username, password) 11 | end 12 | 13 | def commit_comments(slug, sha) 14 | response = get("/#{slug}/commit/#{sha}/comments?pagelen=100") 15 | result = parse_comments(openstruct(response)) 16 | while (response['next']) 17 | response = get response['next'] 18 | result.concat(parse_comments(openstruct(response))) 19 | end 20 | result 21 | end 22 | 23 | def create_commit_comment(slug, sha, body, path, position) 24 | post("/#{slug}/commit/#{sha}/comments", body, path, position) 25 | end 26 | 27 | def pull_comments(slug, pull_id) 28 | response = get("/#{slug}/pullrequests/#{pull_id}/comments?pagelen=100") 29 | parse_comments(openstruct(response)) 30 | result = parse_comments(openstruct(response)) 31 | while (response['next']) 32 | response = get response['next'] 33 | result.concat(parse_comments(openstruct(response))) 34 | end 35 | result 36 | end 37 | 38 | def pull_requests(slug) 39 | response = get("/#{slug}/pullrequests?state=OPEN") 40 | openstruct(response) 41 | end 42 | 43 | def create_pull_comment(slug, pull_id, body, path, position) 44 | post("/#{slug}/pullrequests/#{pull_id}/comments", body, path, position) 45 | end 46 | 47 | def approve_pull_request(slug, pull_id) 48 | self.class.post("/#{slug}/pullrequests/#{pull_id}/approve") 49 | end 50 | 51 | def unapprove_pull_request(slug, pull_id) 52 | self.class.delete("/#{slug}/pullrequests/#{pull_id}/approve") 53 | end 54 | 55 | private 56 | 57 | def openstruct(response) 58 | if response['values'] 59 | response['values'].map { |r| OpenStruct.new(r) } 60 | else 61 | p response 62 | raise 'BitBucket response invalid' 63 | end 64 | end 65 | 66 | def parse_comments(values) 67 | values.each do |value| 68 | value.content = value.content['raw'] 69 | value.line_to = value.inline ? value.inline['to'] : 0 70 | value.filename = value.inline ? value.inline['path'] : '' 71 | end 72 | values 73 | end 74 | 75 | def post(url, body, path, position) 76 | options = { 77 | body: { 78 | content: { 79 | raw: body 80 | }, 81 | inline: { 82 | to: position, 83 | path: path 84 | } 85 | }.to_json, 86 | headers: { 87 | 'Content-Type': 'application/json' 88 | } 89 | } 90 | self.class.post(url, options) 91 | end 92 | 93 | def get(url) 94 | self.class.get(url).parsed_response 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/pronto/clients/bitbucket_server_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ostruct' 4 | 5 | class BitbucketServerClient 6 | include HTTParty 7 | 8 | def initialize(username, password, endpoint) 9 | self.class.base_uri(endpoint) 10 | self.class.basic_auth(username, password) 11 | @headers = { 'Content-Type' => 'application/json' } 12 | end 13 | 14 | def pull_comments(slug, pull_id) 15 | url = "#{pull_requests_url(slug)}/#{pull_id}/activities" 16 | response = paged_request(url) 17 | response.select { |activity| activity.action == 'COMMENTED' } 18 | end 19 | 20 | def pull_requests(slug) 21 | paged_request(pull_requests_url(slug), state: 'OPEN') 22 | end 23 | 24 | def create_pull_comment(slug, pull_id, body, path, position) 25 | url = "#{pull_requests_url(slug)}/#{pull_id}/comments" 26 | post(url, body, path, position) 27 | end 28 | 29 | private 30 | 31 | def pull_requests_url(slug) 32 | project_key, repository_key = slug.split('/') 33 | "/projects/#{project_key}/repos/#{repository_key}/pull-requests" 34 | end 35 | 36 | def openstruct(response) 37 | response.map { |r| OpenStruct.new(r) } 38 | end 39 | 40 | def paged_request(url, query = {}) 41 | Enumerator.new do |yielder| 42 | next_page_start = 0 43 | loop do 44 | response = get(url, query.merge(start: next_page_start)) 45 | break if response['values'].nil? 46 | 47 | response['values'].each { |item| yielder << OpenStruct.new(item) } 48 | 49 | next_page_start = response['nextPageStart'] 50 | break unless next_page_start 51 | end 52 | end 53 | end 54 | 55 | def post(url, body, path, position) 56 | body = { 57 | text: body, 58 | anchor: { 59 | line: position, 60 | lineType: 'ADDED', 61 | path: path, 62 | srcPath: path 63 | } 64 | } 65 | self.class.post(url, body: body.to_json, headers: @headers) 66 | end 67 | 68 | def get(url, query) 69 | self.class.get(url, query: query).parsed_response 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/pronto/comment.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | Comment = Struct.new(:sha, :body, :path, :position) do 3 | def ==(other) 4 | position == other.position && 5 | path == other.path && 6 | body == other.body 7 | end 8 | 9 | def to_s 10 | "[#{sha}] #{path}:#{position} - #{body}" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/pronto/config.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Config 3 | def initialize(config_hash = ConfigFile.new.to_h) 4 | @config_hash = config_hash 5 | end 6 | 7 | %w[github gitlab bitbucket].each do |service| 8 | ConfigFile::EMPTY[service].each do |key, _| 9 | name = "#{service}_#{key}" 10 | define_method(name) { ENV["PRONTO_#{name.upcase}"] || @config_hash[service][key] } 11 | end 12 | end 13 | 14 | def default_commit 15 | default_commit = 16 | ENV['PRONTO_DEFAULT_COMMIT'] || 17 | @config_hash.fetch('default_commit', 'master') 18 | default_commit 19 | end 20 | 21 | def consolidate_comments? 22 | consolidated = 23 | ENV['PRONTO_CONSOLIDATE_COMMENTS'] || 24 | @config_hash.fetch('consolidate_comments', false) 25 | consolidated 26 | end 27 | 28 | def github_review_type 29 | review_type = 30 | ENV['PRONTO_GITHUB_REVIEW_TYPE'] || 31 | @config_hash.fetch('github_review_type', false) 32 | 33 | if review_type == 'request_changes' 34 | 'REQUEST_CHANGES' 35 | else 36 | 'COMMENT' 37 | end 38 | end 39 | 40 | def excluded_files(runner) 41 | files = 42 | if runner == 'all' 43 | ENV['PRONTO_EXCLUDE'] || @config_hash['all']['exclude'] 44 | else 45 | @config_hash.fetch(runner, {})['exclude'] 46 | end 47 | 48 | Array(files) 49 | .flat_map { |path| Dir[path.to_s] } 50 | .map { |path| File.expand_path(path) } 51 | end 52 | 53 | def github_hostname 54 | URI.parse(github_web_endpoint).host 55 | end 56 | 57 | def bitbucket_hostname 58 | URI.parse(bitbucket_web_endpoint).host 59 | end 60 | 61 | def warnings_per_review 62 | fetch_integer('warnings_per_review') 63 | end 64 | 65 | def max_warnings 66 | fetch_integer('max_warnings') 67 | end 68 | 69 | def message_format(formatter) 70 | formatter_config = @config_hash[formatter] 71 | if formatter_config && formatter_config.key?('format') 72 | formatter_config['format'] 73 | else 74 | fetch_value('format') 75 | end 76 | end 77 | 78 | def skip_runners 79 | fetch_list('skip_runners') 80 | end 81 | 82 | def runners 83 | fetch_list('runners') 84 | end 85 | 86 | def logger 87 | @logger ||= begin 88 | verbose = fetch_value('verbose') 89 | verbose ? Logger.new($stdout) : Logger.silent 90 | end 91 | end 92 | 93 | private 94 | 95 | def fetch_integer(key) 96 | full_key = env_key(key) 97 | 98 | (ENV[full_key] && Integer(ENV[full_key])) || @config_hash[key] 99 | end 100 | 101 | def fetch_value(key) 102 | ENV[env_key(key)] || @config_hash[key] 103 | end 104 | 105 | def env_key(key) 106 | "PRONTO_#{key.upcase}" 107 | end 108 | 109 | def fetch_list(key) 110 | Array(fetch_value(key)).flat_map do |runners| 111 | runners.split(',') 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/pronto/config_file.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class ConfigFile 3 | DEFAULT_MESSAGE_FORMAT = '%{msg}'.freeze 4 | DEFAULT_WARNINGS_PER_REVIEW = 30 5 | 6 | EMPTY = { 7 | 'all' => { 8 | 'exclude' => [], 9 | 'include' => [] 10 | }, 11 | 'github' => { 12 | 'slug' => nil, 13 | 'access_token' => nil, 14 | 'api_endpoint' => 'https://api.github.com/', 15 | 'web_endpoint' => 'https://github.com/', 16 | 'review_type' => 'request_changes' 17 | }, 18 | 'gitlab' => { 19 | 'slug' => nil, 20 | 'api_private_token' => nil, 21 | 'api_endpoint' => 'https://gitlab.com/api/v4' 22 | }, 23 | 'bitbucket' => { 24 | 'slug' => nil, 25 | 'username' => nil, 26 | 'password' => nil, 27 | 'api_endpoint' => nil, 28 | 'auto_approve' => false, 29 | 'web_endpoint' => 'https://bitbucket.org/' 30 | }, 31 | 'text' => { 32 | 'format' => '%{color_location} %{color_level}: %{msg}' 33 | }, 34 | 'default_commit' => 'master', 35 | 'runners' => [], 36 | 'formatters' => [], 37 | 'max_warnings' => nil, 38 | 'warnings_per_review' => DEFAULT_WARNINGS_PER_REVIEW, 39 | 'verbose' => false, 40 | 'format' => DEFAULT_MESSAGE_FORMAT 41 | }.freeze 42 | 43 | attr_reader :path 44 | 45 | def initialize(path = ENV.fetch('PRONTO_CONFIG_FILE', '.pronto.yml')) 46 | @path = path 47 | end 48 | 49 | def to_h 50 | hash = File.exist?(@path) ? YAML.load_file(@path) : {} 51 | deep_merge(hash) 52 | end 53 | 54 | private 55 | 56 | def deep_merge(hash) 57 | merger = proc do |_, oldval, newval| 58 | if oldval.is_a?(Hash) && newval.is_a?(Hash) 59 | oldval.merge(newval, &merger) 60 | else 61 | oldval.nil? ? newval : oldval 62 | end 63 | end 64 | 65 | hash.merge(EMPTY, &merger) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/pronto/error.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Error < StandardError; end 3 | end 4 | -------------------------------------------------------------------------------- /lib/pronto/formatter/base.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class Base 4 | def self.name 5 | raise NoMethodError, 'Must be implemented in subclasses.' 6 | end 7 | 8 | def config 9 | @config ||= Config.new 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/pronto/formatter/bitbucket_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class BitbucketFormatter < CommitFormatter 4 | def self.name 5 | 'bitbucket' 6 | end 7 | 8 | def client_module 9 | Bitbucket 10 | end 11 | 12 | def pretty_name 13 | 'BitBucket' 14 | end 15 | 16 | def line_number(message, _) 17 | message.line.new_lineno if message.line 18 | end 19 | end 20 | end 21 | end 22 | 23 | Pronto::Formatter.register(Pronto::Formatter::BitbucketFormatter) 24 | -------------------------------------------------------------------------------- /lib/pronto/formatter/bitbucket_pull_request_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class BitbucketPullRequestFormatter < PullRequestFormatter 4 | def self.name 5 | 'bitbucket_pr' 6 | end 7 | 8 | def client_module 9 | Bitbucket 10 | end 11 | 12 | def pretty_name 13 | 'BitBucket' 14 | end 15 | 16 | def line_number(message, _) 17 | message.line.line.new_lineno if message.line 18 | end 19 | 20 | def approve_pull_request(comments_count, additions_count, client) 21 | return if config.bitbucket_auto_approve == false 22 | 23 | if comments_count > 0 && additions_count > 0 24 | client.unapprove_pull_request 25 | elsif comments_count == 0 26 | client.approve_pull_request 27 | end 28 | end 29 | end 30 | end 31 | end 32 | 33 | Pronto::Formatter.register(Pronto::Formatter::BitbucketPullRequestFormatter) 34 | -------------------------------------------------------------------------------- /lib/pronto/formatter/bitbucket_server_pull_request_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class BitbucketServerPullRequestFormatter < PullRequestFormatter 4 | def self.name 5 | 'bitbucket_server_pr' 6 | end 7 | 8 | def client_module 9 | BitbucketServer 10 | end 11 | 12 | def pretty_name 13 | 'BitBucket Server' 14 | end 15 | 16 | def line_number(message, _) 17 | message.line.line.new_lineno if message.line 18 | end 19 | end 20 | end 21 | end 22 | 23 | Pronto::Formatter.register(Pronto::Formatter::BitbucketServerPullRequestFormatter) 24 | -------------------------------------------------------------------------------- /lib/pronto/formatter/checkstyle_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'rexml/document' 2 | 3 | module Pronto 4 | module Formatter 5 | class CheckstyleFormatter < Base 6 | def self.name 7 | 'checkstyle' 8 | end 9 | 10 | def initialize 11 | @output = '' 12 | end 13 | 14 | def format(messages, _repo, _patches) 15 | open_xml 16 | process_messages(messages) 17 | close_xml 18 | 19 | @output 20 | end 21 | 22 | private 23 | 24 | def open_xml 25 | @document = REXML::Document.new.tap do |d| 26 | d << REXML::XMLDecl.new 27 | end 28 | @checkstyle = REXML::Element.new('checkstyle', @document) 29 | end 30 | 31 | def process_messages(messages) 32 | messages.group_by(&:path).map do |path, path_messages| 33 | REXML::Element.new('file', @checkstyle).tap do |file| 34 | file.attributes['name'] = path 35 | add_file_messages(path_messages, file) 36 | end 37 | end 38 | end 39 | 40 | def add_file_messages(path_messages, file) 41 | path_messages.each do |message| 42 | REXML::Element.new('error', file).tap do |e| 43 | e.attributes['line'] = message.line.new_lineno if message.line 44 | e.attributes['severity'] = to_checkstyle_severity(message.level) 45 | e.attributes['message'] = message.msg 46 | e.attributes['source'] = 'com.puppycrawl.tools.checkstyle.pronto' 47 | end 48 | end 49 | end 50 | 51 | def close_xml 52 | @document.write(@output, 2) 53 | end 54 | 55 | def to_checkstyle_severity(pronto_level) 56 | case pronto_level 57 | when :error, :fatal then 'error' 58 | else pronto_level.to_s 59 | end 60 | end 61 | end 62 | end 63 | end 64 | 65 | Pronto::Formatter.register(Pronto::Formatter::CheckstyleFormatter) 66 | -------------------------------------------------------------------------------- /lib/pronto/formatter/colorizable.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | module Colorizable 4 | def colorize(string, color) 5 | rainbow.wrap(string).color(color) 6 | end 7 | 8 | private 9 | 10 | def rainbow 11 | @rainbow ||= Rainbow.new.tap do |rainbow| 12 | rainbow.enabled = $stdout.tty? 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/pronto/formatter/commit_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class CommitFormatter < GitFormatter 4 | def existing_comments(messages, client, _) 5 | shas = messages.map(&:commit_sha) 6 | comments = shas.flat_map { |sha| client.commit_comments(sha) } 7 | grouped_comments(comments) 8 | end 9 | 10 | def submit_comments(client, comments) 11 | comments.each { |comment| client.create_commit_comment(comment) } 12 | rescue Octokit::UnprocessableEntity, HTTParty::Error => e 13 | $stderr.puts "Failed to post: #{e.message}" 14 | $stderr.puts e.inspect 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/pronto/formatter/formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class << self 4 | def register(formatter_klass) 5 | unless formatter_klass.method_defined?(:format) 6 | raise NoMethodError, "format method is not declared in the #{formatter_klass.name} class." 7 | end 8 | 9 | base = Pronto::Formatter::Base 10 | raise "#{formatter_klass.name} is not a #{base}" unless formatter_klass.ancestors.include?(base) 11 | 12 | @formatters ||= {} 13 | @formatters[formatter_klass.name] = formatter_klass 14 | end 15 | 16 | def get(names) 17 | names ||= 'text' 18 | Array(names).map { |name| @formatters[name.to_s] || TextFormatter } 19 | .uniq.map(&:new) 20 | end 21 | 22 | def names 23 | @formatters.keys 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/pronto/formatter/git_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class GitFormatter < Base 4 | def format(messages, repo, patches) 5 | client = client_module.new(repo) 6 | existing = existing_comments(messages, client, repo) 7 | comments = new_comments(messages, patches) 8 | additions = remove_duplicate_comments(existing, comments) 9 | submit_comments(client, additions) 10 | 11 | approve_pull_request(comments.count, additions.count, client) if defined?(self.approve_pull_request) 12 | 13 | "#{additions.count} Pronto messages posted to #{pretty_name} (#{existing.count} existing)" 14 | end 15 | 16 | def client_module 17 | raise NotImplementedError 18 | end 19 | 20 | def pretty_name 21 | raise NotImplementedError 22 | end 23 | 24 | protected 25 | 26 | def existing_comments(*) 27 | raise NotImplementedError 28 | end 29 | 30 | def line_number(*) 31 | raise NotImplementedError 32 | end 33 | 34 | def submit_comments(*) 35 | raise NotImplementedError 36 | end 37 | 38 | private 39 | 40 | def grouped_comments(comments) 41 | comments.group_by { |comment| [comment.path, comment.position] } 42 | end 43 | 44 | def consolidate_comments(comments) 45 | comment = comments.first 46 | if comments.length > 1 47 | joined_body = join_comments(comments) 48 | Comment.new(comment.sha, joined_body, comment.path, comment.position) 49 | else 50 | comment 51 | end 52 | end 53 | 54 | def dedupe_comments(existing, comments) 55 | body = existing.map(&:body).join(' ') 56 | comments.reject { |comment| body.include?(comment.body) } 57 | end 58 | 59 | def join_comments(comments) 60 | comments.map { |comment| "- #{comment.body}" }.join("\n") 61 | end 62 | 63 | def new_comment(message, patches) 64 | config.logger.log("Creating a comment from message: #{message.inspect}") 65 | sha = message.commit_sha 66 | 67 | body = config.message_format(self.class.name) % message.to_h 68 | 69 | path = message.path 70 | lineno = line_number(message, patches) if message.line 71 | Comment.new(sha, body, path, lineno) 72 | end 73 | 74 | def new_comments(messages, patches) 75 | comments = messages 76 | .uniq 77 | .map { |message| new_comment(message, patches) } 78 | grouped_comments(comments) 79 | end 80 | 81 | def remove_duplicate_comments(old_comments, new_comments) 82 | new_comments.each_with_object([]) do |(key, comments), memo| 83 | existing = old_comments[key] 84 | comments = dedupe_comments(existing, comments) if existing 85 | 86 | if config.consolidate_comments? && !comments.empty? 87 | comment = consolidate_comments(comments) 88 | memo.push(comment) 89 | else 90 | memo.concat(comments) 91 | end 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/pronto/formatter/github_combined_status_formatter.rb: -------------------------------------------------------------------------------- 1 | require_relative 'github_status_formatter/status_builder' 2 | 3 | module Pronto 4 | module Formatter 5 | class GithubCombinedStatusFormatter < Base 6 | def self.name 7 | 'github_combined_status' 8 | end 9 | 10 | def format(messages, repo, _) 11 | client = Github.new(repo) 12 | head = repo.head_commit_sha 13 | 14 | create_status(client, head, messages.uniq || []) 15 | end 16 | 17 | private 18 | 19 | def create_status(client, sha, messages) 20 | builder = GithubStatusFormatter::StatusBuilder.new(nil, messages) 21 | status = Status.new(sha, builder.state, 22 | 'pronto', builder.description) 23 | 24 | client.create_commit_status(status) 25 | end 26 | end 27 | end 28 | end 29 | 30 | Pronto::Formatter.register(Pronto::Formatter::GithubCombinedStatusFormatter) 31 | -------------------------------------------------------------------------------- /lib/pronto/formatter/github_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class GithubFormatter < CommitFormatter 4 | def self.name 5 | 'github' 6 | end 7 | 8 | def client_module 9 | Github 10 | end 11 | 12 | def pretty_name 13 | 'GitHub' 14 | end 15 | 16 | def line_number(message, _) 17 | message.line.commit_line.position if message.line 18 | end 19 | end 20 | end 21 | end 22 | 23 | Pronto::Formatter.register(Pronto::Formatter::GithubFormatter) 24 | -------------------------------------------------------------------------------- /lib/pronto/formatter/github_pull_request_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class GithubPullRequestFormatter < PullRequestFormatter 4 | def self.name 5 | 'github_pr' 6 | end 7 | 8 | def client_module 9 | Github 10 | end 11 | 12 | def pretty_name 13 | 'GitHub' 14 | end 15 | 16 | def line_number(message, _) 17 | message.line&.new_lineno 18 | end 19 | end 20 | end 21 | end 22 | 23 | Pronto::Formatter.register(Pronto::Formatter::GithubPullRequestFormatter) 24 | -------------------------------------------------------------------------------- /lib/pronto/formatter/github_pull_request_review_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class GithubPullRequestReviewFormatter < PullRequestFormatter 4 | def self.name 5 | 'github_pr_review' 6 | end 7 | 8 | def client_module 9 | Github 10 | end 11 | 12 | def pretty_name 13 | 'GitHub' 14 | end 15 | 16 | def submit_comments(client, comments) 17 | client.publish_pull_request_comments(comments) 18 | rescue Octokit::UnprocessableEntity, HTTParty::Error => e 19 | $stderr.puts "Failed to post: #{e.message}" 20 | end 21 | 22 | def line_number(message, _) 23 | message.line&.new_lineno 24 | end 25 | end 26 | end 27 | end 28 | 29 | Pronto::Formatter.register(Pronto::Formatter::GithubPullRequestReviewFormatter) 30 | -------------------------------------------------------------------------------- /lib/pronto/formatter/github_status_formatter.rb: -------------------------------------------------------------------------------- 1 | require_relative 'github_status_formatter/status_builder' 2 | 3 | module Pronto 4 | module Formatter 5 | class GithubStatusFormatter < Base 6 | def self.name 7 | 'github_status' 8 | end 9 | 10 | def format(messages, repo, _) 11 | client = Github.new(repo) 12 | head = repo.head_commit_sha 13 | 14 | messages_by_runner = messages.uniq.group_by(&:runner) 15 | 16 | Runner.runners.each do |runner| 17 | create_status(client, head, runner, messages_by_runner[runner] || []) 18 | end 19 | end 20 | 21 | private 22 | 23 | def create_status(client, sha, runner, messages) 24 | builder = StatusBuilder.new(runner, messages) 25 | status = Status.new(sha, builder.state, 26 | builder.context, builder.description) 27 | 28 | client.create_commit_status(status) 29 | end 30 | end 31 | end 32 | end 33 | 34 | Pronto::Formatter.register(Pronto::Formatter::GithubStatusFormatter) 35 | -------------------------------------------------------------------------------- /lib/pronto/formatter/github_status_formatter/sentence.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class GithubStatusFormatter < Base 4 | class Sentence 5 | def initialize(words) 6 | @words = words 7 | end 8 | 9 | def to_s 10 | case words.size 11 | when 0 12 | '' 13 | when 1 14 | words[0].to_s.dup 15 | when 2 16 | "#{words[0]}#{WORD_CONNECTORS[:two_words_connector]}#{words[1]}" 17 | else 18 | to_oxford_comma_sentence 19 | end 20 | end 21 | 22 | private 23 | 24 | attr_reader :words 25 | 26 | WORD_CONNECTORS = { 27 | words_connector: ', ', 28 | two_words_connector: ' and ', 29 | last_word_connector: ', and ' 30 | }.freeze 31 | 32 | private_constant :WORD_CONNECTORS 33 | 34 | def to_oxford_comma_sentence 35 | "#{words[0...-1].join(WORD_CONNECTORS[:words_connector])}"\ 36 | "#{WORD_CONNECTORS[:last_word_connector]}"\ 37 | "#{words[-1]}" 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/pronto/formatter/github_status_formatter/status_builder.rb: -------------------------------------------------------------------------------- 1 | require_relative 'sentence' 2 | 3 | module Pronto 4 | module Formatter 5 | class GithubStatusFormatter < Base 6 | class StatusBuilder 7 | def initialize(runner, messages) 8 | @runner = runner 9 | @messages = messages 10 | end 11 | 12 | def description 13 | desc = map_description 14 | desc.empty? ? NO_ISSUES_DESCRIPTION : "Found #{desc}." 15 | end 16 | 17 | def state 18 | failure? ? :failure : :success 19 | end 20 | 21 | def context 22 | "pronto/#{@runner.title}" 23 | end 24 | 25 | private 26 | 27 | def failure? 28 | @messages.any? { |message| failure_message?(message) } 29 | end 30 | 31 | def failure_message?(message) 32 | message_state(message) == :failure 33 | end 34 | 35 | def message_state(message) 36 | DEFAULT_LEVEL_TO_STATE_MAPPING[message.level] 37 | end 38 | 39 | def map_description 40 | words = count_issue_types.map do |issue_type, issue_count| 41 | pluralize(issue_count, issue_type) 42 | end 43 | 44 | Sentence.new(words).to_s 45 | end 46 | 47 | def count_issue_types 48 | counts = @messages.each_with_object(Hash.new(0)) do |message, r| 49 | r[message.level] += 1 50 | end 51 | order_by_severity(counts) 52 | end 53 | 54 | def order_by_severity(counts) 55 | Hash[counts.sort_by { |k, _v| Pronto::Message::LEVELS.index(k) }] 56 | end 57 | 58 | def pluralize(count, word) 59 | "#{count} #{word}#{count > 1 ? 's' : ''}" 60 | end 61 | 62 | DEFAULT_LEVEL_TO_STATE_MAPPING = { 63 | info: :success, 64 | warning: :failure, 65 | error: :failure, 66 | fatal: :failure 67 | }.freeze 68 | 69 | NO_ISSUES_DESCRIPTION = 'Coast is clear!'.freeze 70 | 71 | private_constant :DEFAULT_LEVEL_TO_STATE_MAPPING, :NO_ISSUES_DESCRIPTION 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/pronto/formatter/gitlab_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class GitlabFormatter < CommitFormatter 4 | def self.name 5 | 'gitlab' 6 | end 7 | 8 | def client_module 9 | Gitlab 10 | end 11 | 12 | def pretty_name 13 | 'GitLab' 14 | end 15 | 16 | def line_number(message, _) 17 | message.line.commit_line.new_lineno if message.line 18 | end 19 | end 20 | end 21 | end 22 | 23 | Pronto::Formatter.register(Pronto::Formatter::GitlabFormatter) 24 | -------------------------------------------------------------------------------- /lib/pronto/formatter/gitlab_merge_request_review_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class GitlabMergeRequestReviewFormatter < PullRequestFormatter 4 | def self.name 5 | 'gitlab_mr' 6 | end 7 | 8 | def client_module 9 | Gitlab 10 | end 11 | 12 | def pretty_name 13 | 'Gitlab' 14 | end 15 | 16 | def existing_comments(_, client, repo) 17 | sha = repo.head_commit_sha 18 | comments = client.pull_comments(sha) 19 | grouped_comments(comments) 20 | end 21 | 22 | def submit_comments(client, comments) 23 | client.create_pull_request_review(comments) 24 | rescue => e 25 | $stderr.puts "Failed to post: #{e.message}" 26 | end 27 | 28 | def line_number(message, _) 29 | message.line.line.new_lineno if message.line 30 | end 31 | end 32 | end 33 | end 34 | 35 | Pronto::Formatter.register(Pronto::Formatter::GitlabMergeRequestReviewFormatter) 36 | -------------------------------------------------------------------------------- /lib/pronto/formatter/json_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Pronto 4 | module Formatter 5 | class JsonFormatter < Base 6 | def self.name 7 | 'json' 8 | end 9 | 10 | def format(messages, _repo, _patches) 11 | messages.map do |message| 12 | lineno = message.line.new_lineno if message.line 13 | 14 | result = { level: message.level[0].upcase, message: message.msg } 15 | result[:path] = message.path if message.path 16 | result[:line] = lineno if lineno 17 | result[:commit_sha] = message.commit_sha if message.commit_sha 18 | result[:runner] = message.runner if message.runner 19 | result 20 | end.to_json 21 | end 22 | end 23 | end 24 | end 25 | 26 | Pronto::Formatter.register(Pronto::Formatter::JsonFormatter) 27 | -------------------------------------------------------------------------------- /lib/pronto/formatter/null_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class NullFormatter < Base 4 | def self.name 5 | 'null' 6 | end 7 | 8 | def format(_messages, _repo, _patches); end 9 | end 10 | end 11 | end 12 | 13 | Pronto::Formatter.register(Pronto::Formatter::NullFormatter) 14 | -------------------------------------------------------------------------------- /lib/pronto/formatter/pull_request_formatter.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | class PullRequestFormatter < GitFormatter 4 | def existing_comments(_, client, repo) 5 | sha = repo.head_commit_sha 6 | comments = client.pull_comments(sha) 7 | grouped_comments(comments) 8 | end 9 | 10 | def submit_comments(client, comments) 11 | comments.each { |comment| client.create_pull_comment(comment) } 12 | rescue Octokit::UnprocessableEntity, HTTParty::Error => e 13 | $stderr.puts "Failed to post: #{e.message}" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/pronto/formatter/text_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'pronto/formatter/text_message_decorator' 2 | 3 | module Pronto 4 | module Formatter 5 | class TextFormatter < Base 6 | def self.name 7 | 'text' 8 | end 9 | 10 | def format(messages, _repo, _patches) 11 | messages.map do |message| 12 | message_format = config.message_format(self.class.name) 13 | message_data = TextMessageDecorator.new(message).to_h 14 | (message_format % message_data).strip 15 | end 16 | end 17 | end 18 | end 19 | end 20 | 21 | Pronto::Formatter.register(Pronto::Formatter::TextFormatter) 22 | -------------------------------------------------------------------------------- /lib/pronto/formatter/text_message_decorator.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module Pronto 4 | module Formatter 5 | class TextMessageDecorator < SimpleDelegator 6 | include Colorizable 7 | 8 | LOCATION_COLOR = :cyan 9 | 10 | LEVEL_COLORS = { 11 | info: :yellow, 12 | warning: :magenta, 13 | error: :red, 14 | fatal: :red 15 | }.freeze 16 | 17 | def to_h 18 | original = __getobj__.to_h 19 | original[:line] = __getobj__.line.new_lineno if __getobj__.line 20 | original[:color_level] = format_level(__getobj__) 21 | original[:color_location] = format_location(__getobj__) 22 | original 23 | end 24 | 25 | private 26 | 27 | def format_location(message) 28 | line = message.line 29 | lineno = line.new_lineno if line 30 | path = message.path 31 | commit_sha = message.commit_sha 32 | 33 | if path || lineno 34 | path = colorize(path, LOCATION_COLOR) if path 35 | "#{path}:#{lineno}" 36 | elsif commit_sha 37 | colorize(commit_sha[0..6], LOCATION_COLOR) 38 | end 39 | end 40 | 41 | def format_level(message) 42 | level = message.level 43 | color = LEVEL_COLORS.fetch(level) 44 | 45 | colorize(level[0].upcase, color) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/pronto/gem_names.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class GemNames 3 | def to_a 4 | gems.map { |gem| gem.name.sub(/^pronto-/, '') }.uniq.sort 5 | end 6 | 7 | private 8 | 9 | def gems 10 | Gem::Specification.find_all.select do |gem| 11 | if gem.name =~ /^pronto-/ 12 | true 13 | elsif gem.name != 'pronto' 14 | runner_path = File.join(gem.full_gem_path, 15 | "lib/pronto/#{gem.name}.rb") 16 | File.exist?(runner_path) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/pronto/git/line.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Git 3 | Line = Struct.new(:line, :patch, :hunk) do 4 | extend Forwardable 5 | 6 | def_delegators :line, :addition?, :deletion?, :content, :new_lineno, 7 | :old_lineno, :line_origin 8 | 9 | COMPARISON_ATTRIBUTES = %i[content line_origin 10 | old_lineno new_lineno].freeze 11 | 12 | def position 13 | hunk_index = patch.hunks.find_index { |h| h.header == hunk.header } 14 | line_index = patch.lines.find_index(line) 15 | 16 | line_index + hunk_index + 1 17 | end 18 | 19 | def commit_sha 20 | blame[:final_commit_id] if blame 21 | end 22 | 23 | def commit_line 24 | @commit_line ||= begin 25 | patches = patch.repo.show_commit(commit_sha) 26 | 27 | result = patches.find_line(patch.new_file_full_path, 28 | blame[:orig_start_line_number]) 29 | result || self # no commit_line means that it was just added 30 | end 31 | end 32 | 33 | def ==(other) 34 | return false if other.nil? 35 | return true if line.nil? && other.line.nil? 36 | 37 | COMPARISON_ATTRIBUTES.all? do |attribute| 38 | send(attribute) == other.send(attribute) 39 | end 40 | end 41 | 42 | private 43 | 44 | def blame 45 | @blame ||= patch.blame(new_lineno) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/pronto/git/patch.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Git 3 | Patch = Struct.new(:patch, :repo) do 4 | extend Forwardable 5 | 6 | def_delegators :patch, :delta, :hunks, :stat 7 | 8 | def additions 9 | stat[0] 10 | end 11 | 12 | def deletions 13 | stat[1] 14 | end 15 | 16 | def blame(lineno) 17 | repo.blame(new_file_path, lineno) 18 | end 19 | 20 | def lines 21 | @lines ||= begin 22 | hunks.flat_map do |hunk| 23 | hunk.lines.map { |line| Line.new(line, self, hunk) } 24 | end 25 | end 26 | end 27 | 28 | def added_lines 29 | lines.select(&:addition?) 30 | end 31 | 32 | def deleted_lines 33 | lines.select(&:deletion?) 34 | end 35 | 36 | def new_file_full_path 37 | repo.path.join(new_file_path) 38 | end 39 | 40 | def new_file_path 41 | delta.new_file[:path] 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/pronto/git/patches.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Git 3 | class Patches 4 | include Enumerable 5 | 6 | attr_reader :commit, :repo 7 | 8 | def initialize(repo, commit, patches) 9 | @repo = repo 10 | @commit = commit 11 | @patches = patches.map { |patch| Git::Patch.new(patch, repo) } 12 | end 13 | 14 | def each(&block) 15 | @patches.each(&block) 16 | end 17 | 18 | def reject(&block) 19 | Pronto::Git::Patches.new(repo, commit, @patches.reject(&block)) 20 | end 21 | 22 | def find_line(path, line) 23 | patch = find { |p| p.new_file_full_path == path } 24 | lines = patch ? patch.lines : [] 25 | lines.find { |l| l.new_lineno == line } 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/pronto/git/repository.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module Pronto 4 | module Git 5 | class Repository 6 | def initialize(path) 7 | @repo = Rugged::Repository.new(path) 8 | end 9 | 10 | def diff(commit, options = nil) 11 | target, patches = case commit 12 | when :unstaged, :index 13 | [head_commit_sha, @repo.index.diff(options)] 14 | when :staged 15 | [head_commit_sha, head.diff(@repo.index, options)] 16 | when :workdir 17 | [ 18 | head_commit_sha, 19 | @repo.diff_workdir( 20 | head, 21 | { 22 | include_untracked: true, 23 | include_untracked_content: true, 24 | recurse_untracked_dirs: true 25 | }.merge(options || {}) 26 | ) 27 | ] 28 | else 29 | merge_base = merge_base(commit) 30 | patches = @repo.diff(merge_base, head, options) 31 | [merge_base, patches] 32 | end 33 | 34 | patches.find_similar!(renames: true) 35 | Patches.new(self, target, patches) 36 | end 37 | 38 | def show_commit(sha) 39 | return empty_patches(sha) unless sha 40 | 41 | commit = @repo.lookup(sha) 42 | return empty_patches(sha) if commit.parents.count != 1 43 | 44 | # TODO: Rugged does not seem to support diffing against multiple parents 45 | diff = commit.diff(reverse: true) 46 | return empty_patches(sha) if diff.nil? 47 | 48 | Patches.new(self, sha, diff.patches) 49 | end 50 | 51 | def commits_until(sha) 52 | result = [] 53 | @repo.walk(head_commit_sha, Rugged::SORT_TOPO).take_while do |commit| 54 | result << commit.oid 55 | !commit.oid.start_with?(sha) 56 | end 57 | result 58 | end 59 | 60 | def path 61 | Pathname.new(@repo.workdir).cleanpath 62 | end 63 | 64 | def blame(path, lineno) 65 | return if new_file?(path) 66 | 67 | Rugged::Blame.new(@repo, path, min_line: lineno, max_line: lineno, 68 | track_copies_same_file: true, 69 | track_copies_any_commit_copies: true)[0] 70 | end 71 | 72 | def branch 73 | @repo.head.name.sub('refs/heads/', '') if @repo.head.branch? 74 | end 75 | 76 | def remote_urls 77 | @repo.remotes.map(&:url) 78 | end 79 | 80 | def head_commit_sha 81 | head.oid 82 | end 83 | 84 | def head_detached? 85 | @repo.head_detached? 86 | end 87 | 88 | private 89 | 90 | def new_file?(path) 91 | @repo.status(path).include?(:index_new) 92 | end 93 | 94 | def empty_patches(sha) 95 | Patches.new(self, sha, []) 96 | end 97 | 98 | def merge_base(commit) 99 | @repo.merge_base(commit, head) 100 | end 101 | 102 | def head 103 | @repo.head.target 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/pronto/github.rb: -------------------------------------------------------------------------------- 1 | require 'pronto/github_pull' 2 | 3 | module Pronto 4 | class Github < Client 5 | def initialize(repo) 6 | super(repo) 7 | @github_pull = Pronto::GithubPull.new(client, slug) 8 | end 9 | 10 | def pull_comments(sha) 11 | @comment_cache["#{pull_id}/#{sha}"] ||= begin 12 | client.pull_comments(slug, pull_id).map do |comment| 13 | Comment.new( 14 | sha, comment.body, comment.path, comment.line || comment.original_line 15 | ) 16 | end 17 | end 18 | rescue Octokit::NotFound => e 19 | @config.logger.log("Error raised and rescued: #{e}") 20 | msg = "Pull request for sha #{sha} with id #{pull_id} was not found." 21 | raise Pronto::Error, msg 22 | end 23 | 24 | def commit_comments(sha) 25 | @comment_cache[sha.to_s] ||= begin 26 | client.commit_comments(slug, sha).map do |comment| 27 | Comment.new(sha, comment.body, comment.path, comment.line) 28 | end 29 | end 30 | end 31 | 32 | def create_commit_comment(comment) 33 | @config.logger.log("Creating commit comment on #{comment.sha}") 34 | client.create_commit_comment(slug, comment.sha, comment.body, 35 | comment.path, nil, comment.position) 36 | end 37 | 38 | def create_pull_comment(comment) 39 | if comment.path && comment.position 40 | @config.logger.log("Creating pull request comment on #{pull_id}") 41 | client.create_pull_comment( 42 | # Depending on the Octokit version the 6th argument can be either postion or line. We'll 43 | # provide the `line` as this argument and also provide the line in the options argument. 44 | # The API uses `line` and ignores position when `line` is provided. 45 | slug, pull_id, comment.body, pull_sha || comment.sha, 46 | comment.path, comment.position, { line: comment.position } 47 | ) 48 | else 49 | create_commit_comment(comment) 50 | end 51 | end 52 | 53 | def publish_pull_request_comments(comments) 54 | comments_left = comments.clone 55 | while comments_left.any? 56 | comments_to_publish = comments_left.slice!(0, warnings_per_review) 57 | create_pull_request_review(comments_to_publish) 58 | end 59 | end 60 | 61 | def create_commit_status(status) 62 | sha = pull_sha || status.sha 63 | @config.logger.log("Creating comment status on #{sha}") 64 | client.create_status(slug, sha, status.state, 65 | context: status.context, 66 | description: status.description) 67 | end 68 | 69 | private 70 | 71 | def create_pull_request_review(comments) 72 | options = { 73 | event: @config.github_review_type, 74 | comments: comments.map do |comment| 75 | { 76 | path: comment.path, 77 | line: comment.position, 78 | body: comment.body 79 | } 80 | end 81 | } 82 | client.create_pull_request_review(slug, pull_id, options) 83 | end 84 | 85 | def slug 86 | return @config.github_slug if @config.github_slug 87 | @slug ||= begin 88 | @repo.remote_urls.map do |url| 89 | hostname = Regexp.escape(@config.github_hostname) 90 | match = %r{.*#{hostname}(:|\/)(?.*?)(?:\.git)?\z}.match(url) 91 | match[:slug] if match 92 | end.compact.first 93 | end 94 | end 95 | 96 | def client 97 | @client ||= Octokit::Client.new(api_endpoint: @config.github_api_endpoint, 98 | web_endpoint: @config.github_web_endpoint, 99 | access_token: @config.github_access_token, 100 | auto_paginate: true) 101 | end 102 | 103 | def pull_id 104 | env_pull_id || pull[:number].to_i 105 | end 106 | 107 | def pull_sha 108 | pull[:head][:sha] if pull 109 | end 110 | 111 | def pull 112 | @pull ||= if env_pull_id 113 | @github_pull.pull_by_id(env_pull_id) 114 | elsif @repo.branch 115 | @github_pull.pull_by_branch(@repo.branch) 116 | elsif @repo.head_detached? 117 | @github_pull.pull_by_commit(@repo.head_commit_sha) 118 | end 119 | end 120 | 121 | def warnings_per_review 122 | @warnings_per_review ||= @config.warnings_per_review 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/pronto/github_pull.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | # Provides strategies for finding corresponding PR on GitHub 3 | class GithubPull 4 | def initialize(client, slug) 5 | @client = client 6 | @slug = slug 7 | end 8 | 9 | def pull_requests 10 | @pull_requests ||= @client.pull_requests(@slug) 11 | end 12 | 13 | def pull_by_id(pull_id) 14 | result = pull_requests.find { |pr| pr[:number].to_i == pull_id } 15 | unless result 16 | message = "Pull request ##{pull_id} was not found in #{@slug}." 17 | raise Pronto::Error, message 18 | end 19 | result 20 | end 21 | 22 | def pull_by_branch(branch) 23 | result = pull_requests.find { |pr| pr[:head][:ref] == branch } 24 | unless result 25 | raise Pronto::Error, "Pull request for branch #{branch} " \ 26 | "was not found in #{@slug}." 27 | end 28 | result 29 | end 30 | 31 | def pull_by_commit(sha) 32 | result = pull_requests.find do |pr| 33 | pr[:head][:sha] == sha 34 | end 35 | unless result 36 | message = "Pull request with head #{sha} " \ 37 | "was not found in #{@slug}." 38 | raise Pronto::Error, message 39 | end 40 | result 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/pronto/gitlab.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Gitlab < Client 3 | def commit_comments(sha) 4 | @comment_cache[sha.to_s] ||= begin 5 | client.commit_comments(slug, sha).auto_paginate.map do |comment| 6 | Comment.new(sha, comment.note, comment.path, comment.line) 7 | end 8 | end 9 | end 10 | 11 | def pull_comments(sha) 12 | @comment_cache["#{slug}/#{pull_id}"] ||= begin 13 | arr = [] 14 | client.merge_request_discussions(slug, pull_id).auto_paginate.each do |comment| 15 | comment.notes.each do |note| 16 | next unless note['position'] 17 | 18 | arr << Comment.new( 19 | sha, 20 | note['body'], 21 | note['position']['new_path'], 22 | note['position']['new_line'] 23 | ) 24 | end 25 | end 26 | arr 27 | end 28 | end 29 | 30 | def create_pull_request_review(comments) 31 | return if comments.empty? 32 | 33 | comments.each do |comment| 34 | options = { 35 | body: comment.body, 36 | position: position_sha.dup.merge( 37 | new_path: comment.path, 38 | position_type: 'text', 39 | new_line: comment.position, 40 | old_line: nil, 41 | ) 42 | } 43 | 44 | client.create_merge_request_discussion(slug, pull_id, options) 45 | end 46 | end 47 | 48 | def create_commit_comment(comment) 49 | @config.logger.log("Creating commit comment on #{comment.sha}") 50 | client.create_commit_comment(slug, comment.sha, comment.body, 51 | path: comment.path, line: comment.position, 52 | line_type: 'new') 53 | end 54 | 55 | private 56 | 57 | def position_sha 58 | # Better to get those informations from Gitlab API directly than trying to look for them here. 59 | # (FYI you can't use `pull` method because index api does not contains those informations) 60 | @position_sha ||= begin 61 | data = client.merge_request(slug, pull_id) 62 | data.diff_refs.to_h 63 | end 64 | end 65 | 66 | def slug 67 | return @config.gitlab_slug if @config.gitlab_slug 68 | @slug ||= begin 69 | @repo.remote_urls.map do |url| 70 | match = slug_regex(url).match(url) 71 | match[:slug] if match 72 | end.compact.first 73 | end 74 | end 75 | 76 | def pull_id 77 | env_pull_id || raise(Pronto::Error, "Unable to determine merge request id. Specify either `PRONTO_PULL_REQUEST_ID` or `CI_MERGE_REQUEST_IID`.") 78 | end 79 | 80 | def env_pull_id 81 | pull_request = super 82 | 83 | pull_request ||= ENV['CI_MERGE_REQUEST_IID'] 84 | pull_request.to_i if pull_request 85 | end 86 | 87 | def slug_regex(url) 88 | if url =~ %r{^ssh:\/\/} 89 | %r{.*#{host}(:[0-9]+)?(:|\/)(?.*).git} 90 | elsif url =~ /#{host}/ 91 | %r{.*#{host}(:|\/)(?.*).git} 92 | else 93 | %r{\/\/.*?(\/)(?.*).git} 94 | end 95 | end 96 | 97 | def host 98 | @host ||= URI.split(gitlab_api_endpoint)[2, 2].compact.join(':') 99 | end 100 | 101 | def client 102 | @client ||= ::Gitlab.client(endpoint: gitlab_api_endpoint, 103 | private_token: gitlab_api_private_token) 104 | end 105 | 106 | def gitlab_api_private_token 107 | @config.gitlab_api_private_token 108 | end 109 | 110 | def gitlab_api_endpoint 111 | @config.gitlab_api_endpoint 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/pronto/logger.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Logger 3 | def self.silent 4 | null = File.open(File::NULL, 'w') 5 | new(null) 6 | end 7 | 8 | def initialize(out) 9 | @out = out 10 | end 11 | 12 | def log(*args) 13 | @out.puts(*args) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/pronto/message.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Message 3 | attr_reader :path, :line, :level, :msg, :commit_sha, :runner 4 | 5 | LEVELS = %i[info warning error fatal].freeze 6 | 7 | def initialize(path, line, level, msg, commit_sha = nil, runner = nil) 8 | unless LEVELS.include?(level) 9 | raise ::ArgumentError, "level should be set to one of #{LEVELS}" 10 | end 11 | 12 | @path = path 13 | @line = line 14 | @level = level 15 | @msg = msg 16 | @runner = runner 17 | @commit_sha = commit_sha 18 | @commit_sha ||= line.commit_sha if line 19 | end 20 | 21 | def full_path 22 | repo.path.join(path) if repo 23 | end 24 | 25 | def repo 26 | line.patch.repo if line 27 | end 28 | 29 | def ==(other) 30 | comparison_attributes.all? do |attribute| 31 | send(attribute) == other.send(attribute) 32 | end 33 | end 34 | 35 | alias eql? == 36 | 37 | def hash 38 | comparison_attributes.reduce(0) do |hash, attribute| 39 | hash ^ send(attribute).hash 40 | end 41 | end 42 | 43 | def to_h 44 | { 45 | path: path, 46 | line: line, 47 | level: level, 48 | msg: msg, 49 | commit_sha: commit_sha, 50 | runner: @runner && @runner.title 51 | } 52 | end 53 | 54 | private 55 | 56 | def comparison_attributes 57 | line ? %i[path msg level line] : %i[path msg level commit_sha] 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/pronto/plugin.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Plugin 3 | module ClassMethods 4 | def repository 5 | @repository ||= [] 6 | end 7 | 8 | def inherited(klass) 9 | repository << klass 10 | end 11 | end 12 | 13 | def self.included(klass) 14 | klass.extend ClassMethods 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/pronto/rake_task/travis_pull_request.rb: -------------------------------------------------------------------------------- 1 | require 'pronto' 2 | require 'rake' 3 | require 'rake/tasklib' 4 | 5 | module Pronto 6 | module RakeTask 7 | # Provides a custom rake task to use with Travis. 8 | # 9 | # require 'rubocop/rake/travis_pull_request' 10 | # Pronto::Rake::TravisPullRequest.new 11 | class TravisPullRequest < Rake::TaskLib 12 | attr_accessor :name 13 | attr_accessor :verbose 14 | 15 | def initialize(*args, &task_block) 16 | setup_ivars(args) 17 | 18 | unless ::Rake.application.last_comment 19 | desc 'Run Pronto on Travis Pull Request' 20 | end 21 | 22 | task(name, *args) do |_, task_args| 23 | RakeFileUtils.send(:verbose, verbose) do 24 | yield(*[self, task_args].slice(0, task_block.arity)) if task_block 25 | run_task 26 | end 27 | end 28 | end 29 | 30 | def run_task 31 | return if pull_id.nil? || pull_id == 'false' 32 | 33 | pull_request = client.pull_request(repo_slug, pull_id) 34 | formatter = ::Pronto::Formatter::GithubFormatter.new 35 | 36 | gem_names = ::Pronto::GemNames.new.to_a 37 | gem_names.each { |gem_name| require "pronto/#{gem_name}" } 38 | ::Pronto.run(pull_request.base.sha, '.', formatter) 39 | end 40 | 41 | private 42 | 43 | def client 44 | Octokit::Client.new 45 | end 46 | 47 | def pull_id 48 | ENV['TRAVIS_PULL_REQUEST'] 49 | end 50 | 51 | def repo_slug 52 | ENV['TRAVIS_REPO_SLUG'] 53 | end 54 | 55 | def setup_ivars(args) 56 | @name = args.shift || :pronto_travis_pull_request 57 | @verbose = true 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/pronto/runner.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Runner 3 | include Plugin 4 | 5 | def initialize(patches, commit = nil) 6 | @patches = patches 7 | @commit = commit 8 | @config = Config.new 9 | end 10 | 11 | def self.runners 12 | repository 13 | end 14 | 15 | def self.title 16 | @runner_name ||= begin 17 | source_path, _line = instance_method(:run).source_location 18 | file_name, _extension = File.basename(source_path).split('.') 19 | file_name 20 | end 21 | end 22 | 23 | def ruby_patches 24 | return [] unless @patches 25 | 26 | @ruby_patches ||= @patches.select { |patch| patch.additions > 0 } 27 | .select { |patch| ruby_file?(patch.new_file_full_path) } 28 | end 29 | 30 | def ruby_file?(path) 31 | rb_file?(path) || 32 | rake_file?(path) || 33 | gem_file?(path) || 34 | ruby_executable?(path) 35 | end 36 | 37 | def repo_path 38 | @patches.first.repo.path 39 | end 40 | 41 | private 42 | 43 | def rb_file?(path) 44 | File.extname(path) == '.rb' 45 | end 46 | 47 | def rake_file?(path) 48 | File.extname(path) == '.rake' 49 | end 50 | 51 | def gem_file?(path) 52 | File.basename(path) == 'Gemfile' || File.extname(path) == '.gemspec' 53 | end 54 | 55 | def ruby_executable?(path) 56 | return false if File.directory?(path) 57 | line = File.open(path, &:readline) 58 | line =~ /#!.*ruby/ 59 | rescue ArgumentError, EOFError 60 | false 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/pronto/runners.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | class Runners 3 | def initialize(runners = Runner.runners, config = Config.new) 4 | @runners = runners 5 | @config = config 6 | end 7 | 8 | def run(patches) 9 | patches = reject_excluded(config.excluded_files('all'), patches) 10 | return [] if patches.none? 11 | 12 | result = [] 13 | active_runners.each do |runner| 14 | next if exceeds_max?(result) 15 | config.logger.log("Running #{runner}") 16 | runner_patches = reject_excluded( 17 | config.excluded_files(runner.title), patches 18 | ) 19 | next if runner_patches.none? 20 | result += runner.new(runner_patches, patches.commit).run.flatten.compact 21 | end 22 | result = result.take(config.max_warnings) if config.max_warnings 23 | result 24 | end 25 | 26 | private 27 | 28 | attr_reader :config, :runners 29 | 30 | def active_runners 31 | runners.select { |runner| active_runner?(runner) } 32 | end 33 | 34 | def active_runner?(runner) 35 | return true if config.runners.empty? && config.skip_runners.empty? 36 | 37 | if config.runners.empty? 38 | !config.skip_runners.include?(runner.title) 39 | else 40 | active_runner_names = config.runners - config.skip_runners 41 | active_runner_names.include?(runner.title) 42 | end 43 | end 44 | 45 | def reject_excluded(excluded_files, patches) 46 | return patches unless excluded_files.any? 47 | 48 | patches.reject do |patch| 49 | excluded_files.include?(patch.new_file_full_path.to_s) 50 | end 51 | end 52 | 53 | def exceeds_max?(warnings) 54 | config.max_warnings && warnings.count >= config.max_warnings 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/pronto/status.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | Status = Struct.new(:sha, :state, :context, :description) do 3 | def ==(other) 4 | sha == other.sha && 5 | state == other.state && 6 | context == other.context && 7 | description == other.description 8 | end 9 | 10 | def to_s 11 | "[#{sha}] #{context} #{state} - #{description}" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/pronto/version.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Version 3 | STRING = '0.11.4'.freeze 4 | 5 | MSG = '%s (running on %s %s %s)'.freeze 6 | 7 | module_function 8 | 9 | def verbose 10 | format(MSG, STRING, RUBY_ENGINE, RUBY_VERSION, RUBY_PLATFORM) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /pronto.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 4 | require 'pronto/version' 5 | require 'English' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'pronto' 9 | s.version = Pronto::Version::STRING 10 | s.platform = Gem::Platform::RUBY 11 | s.author = 'Mindaugas Mozūras' 12 | s.email = 'mindaugas.mozuras@gmail.com' 13 | s.homepage = 'https://github.com/prontolabs/pronto' 14 | s.summary = 'Pronto runs analysis by checking only the introduced changes' 15 | s.description = <<-EOF 16 | Pronto runs analysis quickly by checking only the relevant changes. Created 17 | to be used on pull requests, but suited for other scenarios as well. Perfect 18 | if you want to find out quickly if branch introduces changes that conform to 19 | your styleguide, are DRY, don't introduce security holes and more. 20 | EOF 21 | 22 | s.licenses = ['MIT'] 23 | s.required_ruby_version = '>= 2.3.0' 24 | 25 | s.files = `git ls-files`.split($RS).reject do |file| 26 | file =~ %r{^(?: 27 | spec/.* 28 | |Gemfile 29 | |Rakefile 30 | |pronto.gif 31 | |\.rspec 32 | |\.gitignore 33 | |\.rubocop.yml 34 | |\.travis.yml 35 | )$}x 36 | end 37 | s.test_files = [] 38 | s.extra_rdoc_files = ['LICENSE', 'README.md'] 39 | s.require_paths = ['lib'] 40 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 41 | 42 | s.add_runtime_dependency('gitlab', '>= 4.4.0', '< 5.0') 43 | s.add_runtime_dependency('httparty', '>= 0.13.7', '< 1.0') 44 | s.add_runtime_dependency('octokit', '>= 4.7.0', '< 11.0') 45 | s.add_runtime_dependency('ostruct') 46 | s.add_runtime_dependency('rainbow', '>= 2.2', '< 4.0') 47 | s.add_runtime_dependency('rexml', '>= 3.2.5', '< 4.0') 48 | s.add_runtime_dependency('rugged', '>= 0.23.0', '< 2.0') 49 | s.add_runtime_dependency('thor', '>= 0.20.3', '< 2.0') 50 | s.add_development_dependency('base64', '~> 0.1.2') 51 | s.add_development_dependency('bundler', '>= 1.15') 52 | s.add_development_dependency('pronto-rubocop', '~> 0.10.0') 53 | s.add_development_dependency('rake', '~> 12.0') 54 | s.add_development_dependency('rspec', '~> 3.4') 55 | s.add_development_dependency('rspec-its', '~> 1.2') 56 | s.add_development_dependency('rspec-expectations', '~> 3.4') 57 | s.add_development_dependency('rubocop', '~> 0.58') 58 | s.add_development_dependency('simplecov', '~> 0.17', '!= 0.18.0', '!= 0.18.1', '!= 0.18.2', '!= 0.18.3', '!= 0.18.4', 59 | '!= 0.18.5', '!= 0.19.0', '!= 0.19.1') # see https://docs.codeclimate.com/docs/configuring-test-coverage 60 | end 61 | -------------------------------------------------------------------------------- /pronto.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/pronto.gif -------------------------------------------------------------------------------- /spec/fixtures/message_with_path.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/message_without_line.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/message_without_path.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /spec/fixtures/new_file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/new_file.txt -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/new_branch 2 | -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/index -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 81d7ad66f39011d0eedaa70e33bae27b67c3c519 Alessio Signorini 1546113502 +0000 commit (initial): first commit 2 | 81d7ad66f39011d0eedaa70e33bae27b67c3c519 81d7ad66f39011d0eedaa70e33bae27b67c3c519 Alessio Signorini 1546113610 +0000 checkout: moving from master to new_branch 3 | 81d7ad66f39011d0eedaa70e33bae27b67c3c519 3e2cbd49d2d9d33b6c9eea4371eb67c947920f71 Alessio Signorini 1546113694 +0000 commit: added second file with no errors 4 | 3e2cbd49d2d9d33b6c9eea4371eb67c947920f71 93657bccfddbce46b58c23960fea84103d90d37d Alessio Signorini 1546113701 +0000 commit: renamed original-file 5 | -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 81d7ad66f39011d0eedaa70e33bae27b67c3c519 Alessio Signorini 1546113502 +0000 commit (initial): first commit 2 | -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/logs/refs/heads/new_branch: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 81d7ad66f39011d0eedaa70e33bae27b67c3c519 Alessio Signorini 1546113610 +0000 branch: Created from HEAD 2 | 81d7ad66f39011d0eedaa70e33bae27b67c3c519 3e2cbd49d2d9d33b6c9eea4371eb67c947920f71 Alessio Signorini 1546113694 +0000 commit: added second file with no errors 3 | 3e2cbd49d2d9d33b6c9eea4371eb67c947920f71 93657bccfddbce46b58c23960fea84103d90d37d Alessio Signorini 1546113701 +0000 commit: renamed original-file 4 | -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/objects/20/6ab76efefefb5b457107f9e841ccf06c3db4eb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/objects/20/6ab76efefefb5b457107f9e841ccf06c3db4eb -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/objects/3a/57a3eff4ae575cedf9643a36e61422fe9be74b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/objects/3a/57a3eff4ae575cedf9643a36e61422fe9be74b -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/objects/3e/2cbd49d2d9d33b6c9eea4371eb67c947920f71: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/objects/3e/2cbd49d2d9d33b6c9eea4371eb67c947920f71 -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/objects/81/d7ad66f39011d0eedaa70e33bae27b67c3c519: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/objects/81/d7ad66f39011d0eedaa70e33bae27b67c3c519 -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/objects/93/657bccfddbce46b58c23960fea84103d90d37d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/objects/93/657bccfddbce46b58c23960fea84103d90d37d -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/objects/97/422ff3a9482718dae96ab9afc6b60c16450076: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/objects/97/422ff3a9482718dae96ab9afc6b60c16450076 -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/objects/a1/69cecd3261a284b1c3bfd84ad4eb2c7cdcd7e4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/objects/a1/69cecd3261a284b1c3bfd84ad4eb2c7cdcd7e4 -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/objects/fd/497adbc2fec28a3a13f49693b7a03dbfe8f542: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/renamed-file.git/objects/fd/497adbc2fec28a3a13f49693b7a03dbfe8f542 -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 81d7ad66f39011d0eedaa70e33bae27b67c3c519 2 | -------------------------------------------------------------------------------- /spec/fixtures/renamed-file.git/refs/heads/new_branch: -------------------------------------------------------------------------------- 1 | 93657bccfddbce46b58c23960fea84103d90d37d 2 | -------------------------------------------------------------------------------- /spec/fixtures/test.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /spec/fixtures/test.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /spec/fixtures/test.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/index -------------------------------------------------------------------------------- /spec/fixtures/test.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 3e0e3ab0a436fc2a9c05253a439dc6084699b7d5 Mindaugas Mozūras 1424635358 +0200 commit (initial): Initial commit 2 | 3e0e3ab0a436fc2a9c05253a439dc6084699b7d5 ac86326d7231ad77dab94e2c4f6f61245a2d9bec Mindaugas Mozūras 1424635387 +0200 commit: To be, or not to be, that is the question 3 | ac86326d7231ad77dab94e2c4f6f61245a2d9bec d6d56582ebfd0c6c3263ea4c4e2d727048370124 Mindaugas Mozūras 1424635411 +0200 commit: Whether 'tis Nobler in the mind to suffer 4 | d6d56582ebfd0c6c3263ea4c4e2d727048370124 ec05bab7d263d5e01be99f2c4e10a5974e24e6de Mindaugas Mozūras 1424635431 +0200 commit: The Slings and Arrows of outrageous Fortune, 5 | ec05bab7d263d5e01be99f2c4e10a5974e24e6de 577afa184c9bc82a66c40047d0809e5fcc43489f Mindaugas Mozūras 1424635464 +0200 commit: Or to take Arms against a Sea of troubles, 6 | 577afa184c9bc82a66c40047d0809e5fcc43489f 7b21c8f4dfb0b8aa39739fc16678c5934877a414 Mindaugas Mozūras 1424635476 +0200 commit: And by opposing, end them? To die, to sleep— 7 | 7b21c8f4dfb0b8aa39739fc16678c5934877a414 64dadfdb7c7437476782e8eb024085862e6287d6 Mindaugas Mozūras 1424635511 +0200 commit: No more; and by a sleep, to say we end 8 | -------------------------------------------------------------------------------- /spec/fixtures/test.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 3e0e3ab0a436fc2a9c05253a439dc6084699b7d5 Mindaugas Mozūras 1424635358 +0200 commit (initial): Initial commit 2 | 3e0e3ab0a436fc2a9c05253a439dc6084699b7d5 ac86326d7231ad77dab94e2c4f6f61245a2d9bec Mindaugas Mozūras 1424635387 +0200 commit: To be, or not to be, that is the question 3 | ac86326d7231ad77dab94e2c4f6f61245a2d9bec d6d56582ebfd0c6c3263ea4c4e2d727048370124 Mindaugas Mozūras 1424635411 +0200 commit: Whether 'tis Nobler in the mind to suffer 4 | d6d56582ebfd0c6c3263ea4c4e2d727048370124 ec05bab7d263d5e01be99f2c4e10a5974e24e6de Mindaugas Mozūras 1424635431 +0200 commit: The Slings and Arrows of outrageous Fortune, 5 | ec05bab7d263d5e01be99f2c4e10a5974e24e6de 577afa184c9bc82a66c40047d0809e5fcc43489f Mindaugas Mozūras 1424635464 +0200 commit: Or to take Arms against a Sea of troubles, 6 | 577afa184c9bc82a66c40047d0809e5fcc43489f 7b21c8f4dfb0b8aa39739fc16678c5934877a414 Mindaugas Mozūras 1424635476 +0200 commit: And by opposing, end them? To die, to sleep— 7 | 7b21c8f4dfb0b8aa39739fc16678c5934877a414 64dadfdb7c7437476782e8eb024085862e6287d6 Mindaugas Mozūras 1424635511 +0200 commit: No more; and by a sleep, to say we end 8 | -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/07/b7623cc56ccfc25ef45a65e4723b3d720504b6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/07/b7623cc56ccfc25ef45a65e4723b3d720504b6 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/14/cf9b86fa9661e967c8a526374384a4f10fb5ea: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/14/cf9b86fa9661e967c8a526374384a4f10fb5ea -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/22/51181228419719f7d926b5326d744ad871ad4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/22/51181228419719f7d926b5326d744ad871ad4a -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/3e/0e3ab0a436fc2a9c05253a439dc6084699b7d5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/3e/0e3ab0a436fc2a9c05253a439dc6084699b7d5 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/3f/c0911fde02260ba650ca3bd428f2bb5f88e93f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/3f/c0911fde02260ba650ca3bd428f2bb5f88e93f -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/56/0c7d57b1ce19e1852c5193e826b62c25f7db4b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/56/0c7d57b1ce19e1852c5193e826b62c25f7db4b -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/57/7afa184c9bc82a66c40047d0809e5fcc43489f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/57/7afa184c9bc82a66c40047d0809e5fcc43489f -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/5a/6e1531cd6d79810356e7ea7c77485514db28c8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/5a/6e1531cd6d79810356e7ea7c77485514db28c8 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/64/dadfdb7c7437476782e8eb024085862e6287d6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/64/dadfdb7c7437476782e8eb024085862e6287d6 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/7b/21c8f4dfb0b8aa39739fc16678c5934877a414: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/7b/21c8f4dfb0b8aa39739fc16678c5934877a414 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/8f/a4900507f731bb4c827c606facdf8c76aa29ab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/8f/a4900507f731bb4c827c606facdf8c76aa29ab -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/92/8676bf506a52bf693b82a29f45db812e4f18d6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/92/8676bf506a52bf693b82a29f45db812e4f18d6 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/97/1783b0dbb66e03b18379a8604d2e1bb64cbb01: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/97/1783b0dbb66e03b18379a8604d2e1bb64cbb01 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/97/8cbcccb92bdfa34587dd175f183a2678f0c9b3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/97/8cbcccb92bdfa34587dd175f183a2678f0c9b3 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/ac/86326d7231ad77dab94e2c4f6f61245a2d9bec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/ac/86326d7231ad77dab94e2c4f6f61245a2d9bec -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/af/eef802b84cab684cf1b99fdbd444fe57c41141: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/af/eef802b84cab684cf1b99fdbd444fe57c41141 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/d0/e5047bf0930d6cfa151e65bc0aaaa4a7e7de0d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/d0/e5047bf0930d6cfa151e65bc0aaaa4a7e7de0d -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/d6/d56582ebfd0c6c3263ea4c4e2d727048370124: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/d6/d56582ebfd0c6c3263ea4c4e2d727048370124 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/ec/05bab7d263d5e01be99f2c4e10a5974e24e6de: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/ec/05bab7d263d5e01be99f2c4e10a5974e24e6de -------------------------------------------------------------------------------- /spec/fixtures/test.git/objects/ef/4cc92872c2e34ea4b508e024df2b80a2268f1b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontolabs/pronto/6a9ece6c048b3ba421c3311cdfb1c237493d1bb6/spec/fixtures/test.git/objects/ef/4cc92872c2e34ea4b508e024df2b80a2268f1b -------------------------------------------------------------------------------- /spec/fixtures/test.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 64dadfdb7c7437476782e8eb024085862e6287d6 2 | -------------------------------------------------------------------------------- /spec/pronto/bitbucket_server_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe BitbucketServer do 3 | let(:bitbucket) { described_class.new(repo) } 4 | 5 | let(:repo) do 6 | double(remote_urls: ['git@bitbucket.org:prontolabs/pronto.git'], 7 | branch: nil) 8 | end 9 | let(:sha) { '61e4bef' } 10 | 11 | describe '#pull_comments' do 12 | subject { bitbucket.pull_comments(sha) } 13 | 14 | let(:response) do 15 | { 16 | comment: { 17 | text: 'text' 18 | }, 19 | commentAnchor: { 20 | path: '/path', 21 | line: '1' 22 | } 23 | } 24 | end 25 | 26 | context 'three requests for same comments' do 27 | specify do 28 | BitbucketServerClient.any_instance 29 | .should_receive(:pull_requests) 30 | .once 31 | .and_return([]) 32 | 33 | BitbucketServerClient.any_instance 34 | .should_receive(:pull_comments) 35 | .with('prontolabs/pronto', 10) 36 | .once 37 | .and_return([response]) 38 | 39 | ENV['PRONTO_PULL_REQUEST_ID'] = '10' 40 | 41 | subject 42 | subject 43 | subject 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/pronto/bitbucket_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Bitbucket do 3 | let(:bitbucket) { described_class.new(repo) } 4 | 5 | let(:repo) do 6 | double(remote_urls: ['git@bitbucket.org:prontolabs/pronto.git'], 7 | branch: nil) 8 | end 9 | let(:sha) { '61e4bef' } 10 | let(:comment) do 11 | double(content: 'note', filename: 'path', line_to: 1, position: 1) 12 | end 13 | 14 | describe '#slug' do 15 | let(:repo) { double(remote_urls: ['git@bitbucket.org:prontolabs/pronto']) } 16 | subject { bitbucket.commit_comments(sha) } 17 | 18 | context 'git remote without .git suffix' do 19 | specify do 20 | BitbucketClient.any_instance 21 | .should_receive(:commit_comments) 22 | .with('prontolabs/pronto', sha) 23 | .once 24 | .and_return([comment]) 25 | 26 | subject 27 | end 28 | end 29 | end 30 | 31 | describe '#commit_comments' do 32 | subject { bitbucket.commit_comments(sha) } 33 | 34 | context 'three requests for same comments' do 35 | specify do 36 | BitbucketClient.any_instance 37 | .should_receive(:commit_comments) 38 | .with('prontolabs/pronto', sha) 39 | .once 40 | .and_return([comment]) 41 | 42 | subject 43 | subject 44 | subject 45 | end 46 | end 47 | end 48 | 49 | describe '#pull_comments' do 50 | subject { bitbucket.pull_comments(sha) } 51 | 52 | context 'three requests for same comments' do 53 | specify do 54 | BitbucketClient.any_instance 55 | .should_receive(:pull_requests) 56 | .once 57 | .and_return([]) 58 | 59 | BitbucketClient.any_instance 60 | .should_receive(:pull_comments) 61 | .with('prontolabs/pronto', 10) 62 | .once 63 | .and_return([comment]) 64 | 65 | ENV['PRONTO_PULL_REQUEST_ID'] = '10' 66 | 67 | subject 68 | subject 69 | subject 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/pronto/clients/bitbucket_client_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe BitbucketClient do 3 | let(:client) { described_class.new('username', 'password') } 4 | 5 | describe '#create_commit_comment' do 6 | subject { client.create_commit_comment(slug, sha, body, path, position) } 7 | let(:slug) { 'prontolabs/pronto' } 8 | let(:sha) { '123' } 9 | let(:body) { 'comment' } 10 | let(:path) { 'path/to/file' } 11 | let(:position) { 1 } 12 | 13 | context 'success' do 14 | before { BitbucketClient.stub(:post).and_return(response) } 15 | let(:response) { double('Response', success?: true) } 16 | its(:success?) { should be_truthy } 17 | end 18 | end 19 | 20 | describe '#approve_pull_request' do 21 | subject { client.approve_pull_request(slug, pull_id) } 22 | let(:slug) { 'prontolabs/pronto' } 23 | let(:pull_id) { 1 } 24 | 25 | context 'success' do 26 | before { BitbucketClient.stub(:post).and_return(response) } 27 | let(:response) { double('Response', success?: true) } 28 | its(:success?) { should be_truthy } 29 | end 30 | end 31 | 32 | describe '#unapprove_pull_request' do 33 | subject { client.unapprove_pull_request(slug, pull_id) } 34 | let(:slug) { 'prontolabs/pronto' } 35 | let(:pull_id) { 1 } 36 | 37 | context 'success' do 38 | before { BitbucketClient.stub(:delete).and_return(response) } 39 | let(:response) { double('Response', success?: true) } 40 | its(:success?) { should be_truthy } 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/pronto/clients/bitbucket_server_client_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe BitbucketServerClient do 3 | let(:client) { described_class.new('username', 'password', 'endpoint') } 4 | 5 | describe '#create_pull_comment' do 6 | subject { client.create_pull_comment(slug, sha, body, path, position) } 7 | let(:slug) { 'prontolabs/pronto' } 8 | let(:sha) { '123' } 9 | let(:body) { 'comment' } 10 | let(:path) { 'path/to/file' } 11 | let(:position) { 1 } 12 | 13 | context 'success' do 14 | before { BitbucketServerClient.stub(:post).and_return(response) } 15 | let(:response) { double('Response', success?: true) } 16 | its(:success?) { should be_truthy } 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/pronto/comment_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Comment do 3 | let(:comment) { described_class.new(sha, body, path, position) } 4 | let(:sha) { '3e0e3ab' } 5 | let(:body) { 'body' } 6 | let(:path) { '/path/to/file' } 7 | let(:position) { 1 } 8 | 9 | describe '==' do 10 | context 'itself' do 11 | subject { comment == comment.dup } 12 | it { should be_truthy } 13 | end 14 | 15 | context 'other comment' do 16 | subject { comment == other } 17 | 18 | context 'different sha' do 19 | let(:other) { described_class.new('sha', body, path, position) } 20 | it { should be_truthy } 21 | end 22 | 23 | context 'different position' do 24 | let(:other) { described_class.new(sha, body, path, 2) } 25 | it { should be_falsy } 26 | end 27 | 28 | context 'different body' do 29 | let(:other) { described_class.new(sha, 'other', path, position) } 30 | it { should be_falsy } 31 | end 32 | 33 | context 'different path' do 34 | let(:other) { described_class.new(sha, body, '/home', position) } 35 | it { should be_falsy } 36 | end 37 | end 38 | end 39 | 40 | describe '#to_s' do 41 | subject { comment.to_s } 42 | it { should == '[3e0e3ab] /path/to/file:1 - body' } 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/pronto/config_file_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe ConfigFile do 3 | let(:config_file) { described_class.new } 4 | 5 | describe '#path' do 6 | subject { config_file.path } 7 | 8 | it { should eq '.pronto.yml' } 9 | 10 | context 'when specified by the environment variable' do 11 | let(:file_path) { '/etc/pronto-config.yml' } 12 | 13 | before do 14 | stub_const('ENV', 'PRONTO_CONFIG_FILE' => file_path) 15 | end 16 | 17 | it { should eq file_path } 18 | end 19 | end 20 | 21 | describe '#to_h' do 22 | subject { config_file.to_h } 23 | 24 | context 'not existing config file' do 25 | it { should include('all' => { 'exclude' => [], 'include' => [] }) } 26 | it do 27 | should include( 28 | 'github' => { 29 | 'slug' => nil, 30 | 'access_token' => nil, 31 | 'api_endpoint' => 'https://api.github.com/', 32 | 'web_endpoint' => 'https://github.com/', 33 | 'review_type' => 'request_changes' 34 | } 35 | ) 36 | end 37 | it do 38 | should include( 39 | 'gitlab' => { 40 | 'slug' => nil, 41 | 'api_private_token' => nil, 42 | 'api_endpoint' => 'https://gitlab.com/api/v4' 43 | } 44 | ) 45 | end 46 | it { should include('runners' => []) } 47 | it { should include('formatters' => []) } 48 | end 49 | 50 | context 'only global excludes in file' do 51 | before do 52 | File.should_receive(:exist?) 53 | .and_return(true) 54 | 55 | YAML.should_receive(:load_file) 56 | .and_return('all' => { 'exclude' => ['a/**/*.rb'] }) 57 | end 58 | 59 | it do 60 | should include( 61 | 'all' => { 62 | 'exclude' => ['a/**/*.rb'], 'include' => [] 63 | } 64 | ) 65 | end 66 | end 67 | 68 | context 'a value is set to false' do 69 | before do 70 | File.should_receive(:exist?) 71 | .and_return(true) 72 | 73 | YAML.should_receive(:load_file) 74 | .and_return('verbose' => false) 75 | end 76 | 77 | it { should include('verbose' => false) } 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/pronto/config_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Config do 3 | let(:config) { described_class.new(config_hash) } 4 | let(:config_hash) { {} } 5 | 6 | describe '#default_commit' do 7 | subject { config.default_commit } 8 | 9 | context 'from env variable' do 10 | before { stub_const('ENV', 'PRONTO_DEFAULT_COMMIT' => 'development') } 11 | it { should == 'development' } 12 | end 13 | 14 | context 'from config hash' do 15 | let(:config_hash) { { 'default_commit' => 'development' } } 16 | it { should == 'development' } 17 | end 18 | 19 | context 'from default value' do 20 | it { should == 'master' } 21 | end 22 | end 23 | 24 | describe '#github_slug' do 25 | subject { config.github_slug } 26 | 27 | context 'from env variable' do 28 | before { stub_const('ENV', 'PRONTO_GITHUB_SLUG' => 'prontolabs/pronto') } 29 | it { should == 'prontolabs/pronto' } 30 | end 31 | 32 | context 'from config hash' do 33 | let(:config_hash) { { 'github' => { 'slug' => 'rails/rails' } } } 34 | it { should == 'rails/rails' } 35 | end 36 | end 37 | 38 | describe '#github_web_endpoint' do 39 | subject { config.github_web_endpoint } 40 | 41 | context 'from env variable' do 42 | before { stub_const('ENV', 'PRONTO_GITHUB_WEB_ENDPOINT' => '4.2.2.2') } 43 | it { should == '4.2.2.2' } 44 | end 45 | 46 | context 'from config hash' do 47 | let(:config_hash) { { 'github' => { 'web_endpoint' => 'localhost' } } } 48 | it { should == 'localhost' } 49 | end 50 | 51 | context 'default' do 52 | let(:config_hash) { ConfigFile::EMPTY } 53 | it { should == 'https://github.com/' } 54 | end 55 | end 56 | 57 | describe '#github_hostname' do 58 | subject { config.github_hostname } 59 | let(:config_hash) { ConfigFile::EMPTY } 60 | it { should == 'github.com' } 61 | end 62 | 63 | describe '#github_review_type' do 64 | subject { config.github_review_type } 65 | 66 | context 'from env variable' do 67 | before { stub_const('ENV', 'PRONTO_GITHUB_REVIEW_TYPE' => 'request_changes') } 68 | it { should == 'REQUEST_CHANGES' } 69 | end 70 | 71 | context 'from config hash' do 72 | let(:config_hash) { { 'github' => { 'review_type' => 'something_else' } } } 73 | it { should == 'COMMENT' } 74 | end 75 | 76 | context 'default' do 77 | let(:config_hash) { ConfigFile::EMPTY } 78 | it { should == 'COMMENT' } 79 | end 80 | end 81 | 82 | describe '#gitlab_slug' do 83 | subject { config.gitlab_slug } 84 | 85 | context 'from env variable' do 86 | before { stub_const('ENV', 'PRONTO_GITLAB_SLUG' => 'rick/deckard') } 87 | it { should == 'rick/deckard' } 88 | end 89 | 90 | context 'from config hash' do 91 | let(:config_hash) { { 'gitlab' => { 'slug' => 'ruby/ruby' } } } 92 | it { should == 'ruby/ruby' } 93 | end 94 | end 95 | 96 | { 97 | max_warnings: { 98 | default_value: nil 99 | }, 100 | warnings_per_review: { 101 | default_value: ConfigFile::DEFAULT_WARNINGS_PER_REVIEW 102 | } 103 | }.each do |setting_name, specifics| 104 | describe "##{setting_name}" do 105 | subject { config.public_send(setting_name) } 106 | 107 | context 'from env variable' do 108 | context 'with a valid value' do 109 | before { stub_const('ENV', "PRONTO_#{setting_name.upcase}" => '20') } 110 | it { should == 20 } 111 | end 112 | 113 | context 'with an invalid value' do 114 | before { stub_const('ENV', "PRONTO_#{setting_name.upcase}" => 'twenty') } 115 | 116 | specify do 117 | -> { subject }.should raise_error(ArgumentError) 118 | end 119 | end 120 | end 121 | 122 | context 'from config hash' do 123 | let(:config_hash) { { setting_name.to_s => 40 } } 124 | it { should == 40 } 125 | end 126 | 127 | context 'default' do 128 | let(:config_hash) { ConfigFile::EMPTY } 129 | it { should == specifics[:default_value] } 130 | end 131 | end 132 | end 133 | 134 | describe '#message_format' do 135 | subject { config.message_format('whatever') } 136 | 137 | context 'when there is an entry in the config file' do 138 | let(:config_hash) { { 'whatever' => { 'format' => whatever_format } } } 139 | let(:whatever_format) { "that's just like your opinion man" } 140 | 141 | it { should == whatever_format } 142 | end 143 | 144 | context 'when there is no entry in the config file' do 145 | let(:config_hash) { ConfigFile::EMPTY } 146 | 147 | it { should == ConfigFile::DEFAULT_MESSAGE_FORMAT } 148 | end 149 | end 150 | 151 | describe '#skip_runners' do 152 | subject { config.skip_runners } 153 | 154 | let(:env_variables) { {} } 155 | 156 | before do 157 | stub_const('ENV', env_variables) 158 | end 159 | 160 | context 'when runners are not skipped' do 161 | it { should be_empty } 162 | end 163 | 164 | context 'when runners are skipped via ENV variable' do 165 | let(:env_variables) { { 'PRONTO_SKIP_RUNNERS' => 'Runner,OtherRunner' } } 166 | 167 | it { should == %w[Runner OtherRunner] } 168 | end 169 | 170 | context 'when runners are skipped via config file' do 171 | let(:config_hash) { { 'skip_runners' => ['Runner'] } } 172 | 173 | it { should == %w[Runner] } 174 | end 175 | 176 | context 'when runners are skipped via config file and ENV variable' do 177 | let(:env_variables) { { 'PRONTO_SKIP_RUNNERS' => 'EnvRunner' } } 178 | let(:config_hash) { { 'skip_runners' => %w[ConfigRunner] } } 179 | 180 | it { should == %w[EnvRunner] } 181 | end 182 | end 183 | 184 | describe '#runners' do 185 | subject { config.runners } 186 | 187 | context 'when there is an entry in the config file' do 188 | let(:config_hash) { { 'runners' => ['Runner'] } } 189 | 190 | it { should == %w[Runner] } 191 | end 192 | 193 | context 'when there is no entry in the config file' do 194 | it { should be_empty } 195 | end 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /spec/pronto/formatter/bitbucket_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe BitbucketFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | describe '#format' do 7 | subject { formatter.format(messages, repo, nil) } 8 | let(:messages) { [message, message] } 9 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 10 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 11 | let(:line) { double(new_lineno: 1, commit_sha: '123', position: nil) } 12 | before { line.stub(:commit_line).and_return(line) } 13 | 14 | specify do 15 | BitbucketClient.any_instance 16 | .should_receive(:commit_comments) 17 | .once 18 | .and_return([]) 19 | 20 | BitbucketClient.any_instance 21 | .should_receive(:create_commit_comment) 22 | .once 23 | 24 | subject 25 | end 26 | end 27 | 28 | describe '#format without duplicates' do 29 | subject { formatter.format(messages, repo, nil) } 30 | let(:messages) { [message1, message2] } 31 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 32 | let(:message1) { Message.new('path/to1', line1, :warning, 'crucial') } 33 | let(:message2) { Message.new('path/to2', line2, :warning, 'crucial') } 34 | let(:line1) { double(new_lineno: 1, commit_sha: '123', position: nil) } 35 | let(:line2) { double(new_lineno: 2, commit_sha: '123', position: nil) } 36 | before do 37 | line1.stub(:commit_line).and_return(line1) 38 | line2.stub(:commit_line).and_return(line2) 39 | end 40 | 41 | specify do 42 | BitbucketClient.any_instance 43 | .should_receive(:commit_comments) 44 | .once 45 | .and_return([]) 46 | 47 | BitbucketClient.any_instance 48 | .should_receive(:create_commit_comment) 49 | .twice 50 | 51 | subject 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/pronto/formatter/bitbucket_pull_request_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe BitbucketPullRequestFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 7 | 8 | describe '#format' do 9 | subject { formatter.format(messages, repo, patches) } 10 | let(:messages) { [message, message] } 11 | let(:message) { Message.new(patch.new_file_full_path, line, :info, '') } 12 | let(:patch) { repo.show_commit('64dadfd').first } 13 | let(:line) { patch.added_lines.first } 14 | let(:patches) { repo.diff('64dadfd^') } 15 | 16 | before do 17 | ENV['PRONTO_PULL_REQUEST_ID'] = '10' 18 | BitbucketClient.any_instance 19 | .should_receive(:pull_requests) 20 | .once 21 | .and_return([]) 22 | 23 | BitbucketClient.any_instance 24 | .should_receive(:pull_comments) 25 | .once 26 | .and_return([]) 27 | end 28 | 29 | specify do 30 | BitbucketClient.any_instance 31 | .should_receive(:create_pull_comment) 32 | .once 33 | 34 | subject 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/pronto/formatter/bitbucket_server_pull_request_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe BitbucketServerPullRequestFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 7 | 8 | describe '#format' do 9 | subject { formatter.format(messages, repo, patches) } 10 | let(:messages) { [message, message] } 11 | let(:message) { Message.new(patch.new_file_full_path, line, :info, '') } 12 | let(:patch) { repo.show_commit('64dadfd').first } 13 | let(:line) { patch.added_lines.first } 14 | let(:patches) { repo.diff('64dadfd^') } 15 | 16 | before do 17 | ENV['PRONTO_PULL_REQUEST_ID'] = '10' 18 | BitbucketServerClient.any_instance 19 | .should_receive(:pull_requests) 20 | .once 21 | .and_return([]) 22 | 23 | BitbucketServerClient.any_instance 24 | .should_receive(:pull_comments) 25 | .once 26 | .and_return([]) 27 | end 28 | 29 | specify do 30 | BitbucketServerClient.any_instance 31 | .should_receive(:create_pull_comment) 32 | .once 33 | 34 | subject 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/pronto/formatter/checkstyle_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe CheckstyleFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | describe '#format' do 7 | subject { formatter.format(messages, nil, nil) } 8 | let(:line) { double(new_lineno: 1, commit_sha: '123') } 9 | let(:error) { Message.new('path/to', line, :error, 'Line Error') } 10 | let(:warning) { Message.new('path/to', line, :warning, 'Line Warning') } 11 | let(:messages) { [error, warning] } 12 | 13 | it { should eq load_fixture('message_with_path.xml') } 14 | 15 | context 'message without path' do 16 | let(:error) { Message.new(nil, line, :error, 'Line Error') } 17 | 18 | it { should eq load_fixture('message_without_path.xml') } 19 | end 20 | 21 | context 'message without line' do 22 | let(:error) { Message.new('path/to', nil, :error, 'Line Error') } 23 | 24 | it { should eq load_fixture('message_without_line.xml') } 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/pronto/formatter/colorizable_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe Colorizable do 4 | let(:formatter_class) do 5 | klass = described_class 6 | 7 | Class.new(TextFormatter) do 8 | include klass 9 | end 10 | end 11 | 12 | let(:formatter) do 13 | formatter_class.new 14 | end 15 | 16 | describe '#colorize' do 17 | subject { formatter.colorize('Warning', :yellow) } 18 | 19 | context 'in TTY' do 20 | before { $stdout.stub(:tty?) { true } } 21 | 22 | it 'colorizes the passed string' do 23 | should eq("\e[33mWarning\e[0m") 24 | end 25 | end 26 | 27 | context 'not in TTY' do 28 | before { $stdout.stub(:tty?) { false } } 29 | 30 | it 'returns the passed string' do 31 | should eq('Warning') 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/pronto/formatter/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe '.register' do 4 | context 'format method not implementend' do 5 | subject { Formatter.register(formatter) } 6 | 7 | let(:formatter) do 8 | Class.new(Pronto::Formatter::Base) do 9 | def self.name 10 | 'custom_formatter' 11 | end 12 | end 13 | end 14 | 15 | specify do 16 | -> { subject }.should raise_error( 17 | NoMethodError, "format method is not declared in the #{formatter.name} class." 18 | ) 19 | end 20 | end 21 | 22 | context 'formatter class is not Formatter::Base' do 23 | subject { Formatter.register(formatter) } 24 | 25 | let(:formatter) do 26 | Class.new do 27 | def self.name 28 | 'custom_formatter' 29 | end 30 | 31 | def format(_messages, _repo, _patches); end 32 | end 33 | end 34 | 35 | specify do 36 | -> { subject }.should raise_error(RuntimeError, "#{formatter.name} is not a #{Pronto::Formatter::Base}") 37 | end 38 | end 39 | end 40 | 41 | describe '.get' do 42 | context 'single' do 43 | subject { Formatter.get(name).first } 44 | 45 | context 'github' do 46 | let(:name) { 'github' } 47 | it { should be_an_instance_of GithubFormatter } 48 | end 49 | 50 | context 'github_pr' do 51 | let(:name) { 'github_pr' } 52 | it { should be_an_instance_of GithubPullRequestFormatter } 53 | end 54 | 55 | context 'github_pr_review' do 56 | let(:name) { 'github_pr_review' } 57 | it { should be_an_instance_of GithubPullRequestReviewFormatter } 58 | end 59 | 60 | context 'bitbucket' do 61 | let(:name) { 'bitbucket' } 62 | it { should be_an_instance_of BitbucketFormatter } 63 | end 64 | 65 | context 'bitbucket_pr' do 66 | let(:name) { 'bitbucket_pr' } 67 | it { should be_an_instance_of BitbucketPullRequestFormatter } 68 | end 69 | 70 | context 'bitbucket_server_pr' do 71 | let(:name) { 'bitbucket_server_pr' } 72 | it { should be_an_instance_of BitbucketServerPullRequestFormatter } 73 | end 74 | 75 | context 'github_status' do 76 | let(:name) { 'github_status' } 77 | it { should be_an_instance_of GithubStatusFormatter } 78 | end 79 | 80 | context 'json' do 81 | let(:name) { 'json' } 82 | it { should be_an_instance_of JsonFormatter } 83 | end 84 | 85 | context 'text' do 86 | let(:name) { 'text' } 87 | it { should be_an_instance_of TextFormatter } 88 | end 89 | 90 | context 'checkstyle' do 91 | let(:name) { 'checkstyle' } 92 | it { should be_an_instance_of CheckstyleFormatter } 93 | end 94 | 95 | context 'null' do 96 | let(:name) { 'null' } 97 | it { should be_an_instance_of NullFormatter } 98 | end 99 | 100 | context 'empty' do 101 | let(:name) { '' } 102 | it { should be_an_instance_of TextFormatter } 103 | end 104 | 105 | context 'nil' do 106 | let(:name) { nil } 107 | it { should be_an_instance_of TextFormatter } 108 | end 109 | end 110 | 111 | context 'multiple' do 112 | subject { Formatter.get(names) } 113 | 114 | context 'github and text' do 115 | let(:names) { %w[github text] } 116 | 117 | its(:count) { should == 2 } 118 | its(:first) { should be_an_instance_of GithubFormatter } 119 | its(:last) { should be_an_instance_of TextFormatter } 120 | end 121 | 122 | context 'nil and empty' do 123 | let(:names) { [nil, ''] } 124 | 125 | its(:count) { should == 1 } 126 | its(:first) { should be_an_instance_of TextFormatter } 127 | end 128 | end 129 | end 130 | 131 | describe '.names' do 132 | subject { Formatter.names } 133 | it do 134 | should =~ %w[github github_pr github_pr_review github_status 135 | github_combined_status gitlab gitlab_mr bitbucket bitbucket_pr 136 | bitbucket_server_pr json checkstyle text null] 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /spec/pronto/formatter/github_combined_status_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | RSpec.describe GithubCombinedStatusFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | describe '#format' do 7 | subject { formatter.format(messages, repo, nil) } 8 | 9 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 10 | let(:runner_class) do 11 | Class.new do 12 | def self.title 13 | 'fake_runner' 14 | end 15 | end 16 | end 17 | let(:message) do 18 | Pronto::Message.new('app/path', nil, level, '', sha, runner_class) 19 | end 20 | let(:sha) { '64dadfdb7c7437476782e8eb024085862e6287d6' } 21 | let(:status) { Status.new(sha, state, context, description) } 22 | let(:context) { 'pronto' } 23 | 24 | let(:messages) { [message, message] } 25 | 26 | before do 27 | Pronto::Github.any_instance 28 | .should_receive(:create_commit_status) 29 | .with(status) 30 | .once 31 | .and_return(nil) 32 | end 33 | 34 | context 'when has no messages' do 35 | let(:messages) { [] } 36 | 37 | let(:state) { :success } 38 | let(:description) { 'Coast is clear!' } 39 | 40 | it 'has no issues' do 41 | subject 42 | end 43 | end 44 | 45 | context 'when has one message' do 46 | let(:messages) { [message, message] } 47 | 48 | context 'when severity level is info' do 49 | let(:level) { :info } 50 | 51 | let(:state) { :success } 52 | let(:description) { 'Found 1 info.' } 53 | 54 | it 'has issue' do 55 | subject 56 | end 57 | end 58 | 59 | context 'when severity level is warning' do 60 | let(:level) { :warning } 61 | 62 | let(:state) { :failure } 63 | let(:description) { 'Found 1 warning.' } 64 | 65 | it 'has warning' do 66 | subject 67 | end 68 | end 69 | end 70 | 71 | context 'when has multiple messages' do 72 | let(:level) { :warning } 73 | let(:level2) { :error } 74 | let(:message2) do 75 | Pronto::Message.new('app/path', nil, level2, '', sha, runner_class) 76 | end 77 | 78 | let(:state) { :failure } 79 | let(:description) { 'Found 1 warning and 1 error.' } 80 | 81 | context 'order of messages does not matter' do 82 | context 'ordered' do 83 | let(:messages) { [message, message2] } 84 | 85 | it 'has issues' do 86 | subject 87 | end 88 | end 89 | 90 | context 'reversed' do 91 | let(:messages) { [message2, message] } 92 | 93 | it 'has issues' do 94 | subject 95 | end 96 | end 97 | end 98 | end 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/pronto/formatter/github_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe GithubFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | describe '#format' do 7 | subject { formatter.format(messages, repo, nil) } 8 | 9 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 10 | let(:published_comments_msg) do 11 | "%i Pronto messages posted to #{formatter.pretty_name} (%i existing)" 12 | end 13 | 14 | context 'with duplicates in the new messages' do 15 | let(:messages) { [message, message] } 16 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 17 | let(:line) { double(new_lineno: 1, commit_sha: '123', position: nil) } 18 | before { line.stub(:commit_line).and_return(line) } 19 | 20 | specify do 21 | Octokit::Client.any_instance 22 | .should_receive(:commit_comments) 23 | .once 24 | .and_return([]) 25 | 26 | Octokit::Client.any_instance 27 | .should_receive(:create_commit_comment) 28 | .once 29 | 30 | subject.should eq format(published_comments_msg, count: 1, existing_count: 0) 31 | end 32 | end 33 | 34 | context 'with duplicates in the existed messages' do 35 | let(:messages) { [message] } 36 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 37 | let(:line) { double(new_lineno: 1, commit_sha: '123', position: nil) } 38 | 39 | before { line.stub(:commit_line).and_return(line) } 40 | 41 | specify do 42 | Octokit::Client.any_instance 43 | .should_receive(:commit_comments) 44 | .once 45 | .and_return([double(body: 'crucial', path: 'path/to', line: nil)]) 46 | 47 | Octokit::Client.any_instance.should_not_receive(:create_commit_comment) 48 | 49 | subject.should eq format(published_comments_msg, count: 0, existing_count: 1) 50 | end 51 | end 52 | 53 | context 'with one duplicate and one non duplicated messages' do 54 | let(:messages) { [message, existed_message] } 55 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 56 | let(:existed_message) { Message.new('path/to', line, :warning, 'existed') } 57 | let(:line) { double(new_lineno: 1, commit_sha: '123', position: nil) } 58 | 59 | before { line.stub(:commit_line).and_return(line) } 60 | 61 | specify do 62 | Octokit::Client.any_instance 63 | .should_receive(:commit_comments) 64 | .once 65 | .and_return([double(body: 'existed', path: 'path/to', line: nil)]) 66 | 67 | Octokit::Client.any_instance.should_receive(:create_commit_comment).once 68 | 69 | subject.should eq format(published_comments_msg, count: 1, existing_count: 1) 70 | end 71 | end 72 | 73 | context 'without duplicates' do 74 | let(:messages) { [message1, message2] } 75 | let(:message1) { Message.new('path/to1', line1, :warning, 'crucial') } 76 | let(:message2) { Message.new('path/to2', line2, :warning, 'crucial') } 77 | let(:line1) { double(new_lineno: 1, commit_sha: '123', position: nil) } 78 | let(:line2) { double(new_lineno: 2, commit_sha: '123', position: nil) } 79 | 80 | before do 81 | line1.stub(:commit_line).and_return(line1) 82 | line2.stub(:commit_line).and_return(line2) 83 | end 84 | 85 | specify do 86 | Octokit::Client.any_instance 87 | .should_receive(:commit_comments) 88 | .once 89 | .and_return([]) 90 | 91 | Octokit::Client.any_instance 92 | .should_receive(:create_commit_comment) 93 | .twice 94 | 95 | subject.should eq format(published_comments_msg, count: 2, existing_count: 0) 96 | end 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/pronto/formatter/github_pull_request_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe GithubPullRequestFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 7 | 8 | describe '#format' do 9 | subject { formatter.format(messages, repo, patches) } 10 | let(:messages) { [message, message] } 11 | let(:message) { Message.new(patch.new_file_full_path, line, :info, '') } 12 | let(:patch) { repo.show_commit('64dadfd').first } 13 | let(:line) { patch.added_lines.first } 14 | let(:patches) { repo.diff('64dadfd^') } 15 | let(:octokit_client) { instance_double(Octokit::Client) } 16 | 17 | before do 18 | ENV['PRONTO_PULL_REQUEST_ID'] = '10' 19 | Octokit::Client.stub(:new).and_return(octokit_client) 20 | octokit_client 21 | .stub(:pull_requests) 22 | .once 23 | .and_return([{ number: 10, head: { sha: 'foo' } }]) 24 | 25 | octokit_client 26 | .stub(:pull_comments) 27 | .once 28 | .and_return([double(body: 'a comment', path: 'a/path', line: 5)]) 29 | end 30 | 31 | specify do 32 | octokit_client 33 | .should_receive(:create_pull_comment) 34 | .once 35 | 36 | subject 37 | end 38 | 39 | context 'with duplicate comment' do 40 | let(:messages) { [message] } 41 | let(:message) { Message.new('path/to', line, :warning, 'existed') } 42 | let(:line) { double(new_lineno: 3, commit_sha: '123', position: 3) } 43 | 44 | specify do 45 | octokit_client.should_receive(:pull_comments).and_return( 46 | [double(body: 'existed', path: 'path/to', line: line.new_lineno)] 47 | ) 48 | 49 | octokit_client.should_not_receive(:create_pull_comment) 50 | 51 | subject.should eq '0 Pronto messages posted to GitHub (1 existing)' 52 | end 53 | end 54 | 55 | context 'with one duplicate and one non duplicated comment' do 56 | let(:messages) { [message, existing_message] } 57 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 58 | let(:existing_message) { Message.new('path/to', line, :warning, 'existed') } 59 | 60 | specify do 61 | octokit_client.should_receive(:pull_comments).and_return( 62 | [double(body: 'existed', path: 'path/to', line: line.new_lineno)] 63 | ) 64 | 65 | octokit_client.should_receive(:create_pull_comment).once 66 | 67 | subject.should eq '1 Pronto messages posted to GitHub (1 existing)' 68 | end 69 | end 70 | 71 | context 'error handling' do 72 | let(:error_response) do 73 | { 74 | status: 422, 75 | body: { 76 | message: 'Validation Failed', 77 | errors: [ 78 | resource: 'Issue', 79 | field: 'title', 80 | code: 'missing_field' 81 | ] 82 | }.to_json, 83 | response_headers: { 84 | content_type: 'json' 85 | } 86 | } 87 | end 88 | 89 | it 'handles and prints details' do 90 | error = Octokit::UnprocessableEntity.from_response(error_response) 91 | octokit_client 92 | .should_receive(:create_pull_comment) 93 | .and_raise(error) 94 | 95 | $stderr.should_receive(:puts) do |line| 96 | line.should =~ /Failed to post/ 97 | line.should =~ /Validation Failed/ 98 | line.should =~ /missing_field/ 99 | line.should =~ /Issue/ 100 | end 101 | subject 102 | end 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/pronto/formatter/github_pull_request_review_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe GithubPullRequestReviewFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 7 | 8 | describe '#format' do 9 | subject { formatter.format(messages, repo, patches) } 10 | let(:messages) { [message, message] } 11 | let(:message) { Message.new(patch.new_file_full_path, line, :info, '') } 12 | let(:patch) { repo.show_commit('64dadfd').first } 13 | let(:line) { patch.added_lines.first } 14 | let(:patches) { repo.diff('64dadfd^') } 15 | let(:octokit_client) { instance_double(Octokit::Client) } 16 | 17 | before do 18 | ENV['PRONTO_PULL_REQUEST_ID'] = '10' 19 | Octokit::Client.stub(:new).and_return(octokit_client) 20 | end 21 | 22 | specify do 23 | octokit_client.should_receive(:pull_comments).and_return([]) 24 | octokit_client.should_receive(:create_pull_request_review).once 25 | 26 | subject 27 | end 28 | 29 | context 'with duplicate comment' do 30 | let(:messages) { [message] } 31 | let(:message) { Message.new('path/to', line, :warning, 'existed') } 32 | let(:line) { double(new_lineno: 3, commit_sha: '123', position: 3) } 33 | 34 | specify do 35 | octokit_client.should_receive(:pull_comments).and_return( 36 | [double(body: 'existed', path: 'path/to', line: line.new_lineno)] 37 | ) 38 | 39 | octokit_client.should_not_receive(:create_pull_request_review) 40 | 41 | subject.should eq '0 Pronto messages posted to GitHub (1 existing)' 42 | end 43 | end 44 | 45 | context 'with one duplicate and one non duplicated comment' do 46 | let(:messages) { [message, existing_message] } 47 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 48 | let(:existing_message) { Message.new('path/to', line, :warning, 'existed') } 49 | 50 | specify do 51 | octokit_client.should_receive(:pull_comments).and_return( 52 | [double(body: 'existed', path: 'path/to', line: line.new_lineno)] 53 | ) 54 | 55 | octokit_client.should_receive(:create_pull_request_review).once 56 | 57 | subject.should eq '1 Pronto messages posted to GitHub (1 existing)' 58 | end 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/pronto/formatter/github_status_formatter/sentence_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | RSpec.describe GithubStatusFormatter::Sentence do 4 | let(:sentence) { described_class.new(words) } 5 | 6 | describe '#to_s' do 7 | subject { sentence.to_s } 8 | 9 | context 'when no words' do 10 | let(:words) { [] } 11 | 12 | it 'returns empty string' do 13 | subject.should == '' 14 | end 15 | end 16 | 17 | context 'when 1 word' do 18 | let(:words) { %w[eeny] } 19 | 20 | it 'returns the word' do 21 | subject.should == 'eeny' 22 | end 23 | end 24 | 25 | context 'when 2 words' do 26 | let(:words) { %w[eeny meeny] } 27 | 28 | it 'uses and to join words' do 29 | subject.should == 'eeny and meeny' 30 | end 31 | end 32 | 33 | context 'when 3 words' do 34 | let(:words) { %w[eeny meeny miny moe] } 35 | 36 | it 'enumerates words using oxford comma' do 37 | subject.should == 'eeny, meeny, miny, and moe' 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/pronto/formatter/github_status_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | RSpec.describe GithubStatusFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | describe '#format' do 7 | subject { formatter.format(messages, repo, nil) } 8 | 9 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 10 | let(:runner_class) do 11 | Class.new do 12 | def self.title 13 | 'fake_runner' 14 | end 15 | end 16 | end 17 | let(:message) do 18 | Pronto::Message.new('app/path', nil, level, '', sha, runner_class) 19 | end 20 | let(:sha) { '64dadfdb7c7437476782e8eb024085862e6287d6' } 21 | let(:status) { Status.new(sha, state, context, description) } 22 | let(:context) { 'pronto/fake_runner' } 23 | 24 | let(:messages) { [message, message] } 25 | 26 | before do 27 | Pronto::Github.any_instance 28 | .should_receive(:create_commit_status) 29 | .with(status) 30 | .once 31 | .and_return(nil) 32 | 33 | Runner 34 | .should_receive(:runners) 35 | .and_return([runner_class]) 36 | end 37 | 38 | context 'when has no messages' do 39 | let(:messages) { [] } 40 | 41 | let(:state) { :success } 42 | let(:description) { 'Coast is clear!' } 43 | 44 | it 'has no issues' do 45 | subject 46 | end 47 | end 48 | 49 | context 'when has one message' do 50 | let(:messages) { [message, message] } 51 | 52 | context 'when severity level is info' do 53 | let(:level) { :info } 54 | 55 | let(:state) { :success } 56 | let(:description) { 'Found 1 info.' } 57 | 58 | it 'has issue' do 59 | subject 60 | end 61 | end 62 | 63 | context 'when severity level is warning' do 64 | let(:level) { :warning } 65 | 66 | let(:state) { :failure } 67 | let(:description) { 'Found 1 warning.' } 68 | 69 | it 'has warning' do 70 | subject 71 | end 72 | end 73 | end 74 | 75 | context 'when has multiple messages' do 76 | let(:level) { :warning } 77 | let(:level2) { :error } 78 | let(:message2) do 79 | Pronto::Message.new('app/path', nil, level2, '', sha, runner_class) 80 | end 81 | 82 | let(:state) { :failure } 83 | let(:description) { 'Found 1 warning and 1 error.' } 84 | 85 | context 'order of messages does not matter' do 86 | context 'ordered' do 87 | let(:messages) { [message, message2] } 88 | 89 | it 'has issues' do 90 | subject 91 | end 92 | end 93 | 94 | context 'reversed' do 95 | let(:messages) { [message2, message] } 96 | 97 | it 'has issues' do 98 | subject 99 | end 100 | end 101 | end 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/pronto/formatter/gitlab_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe GitlabFormatter do 4 | ENV['PRONTO_GITLAB_API_ENDPOINT'] = 'http://example.com/api/v4' 5 | ENV['PRONTO_GITLAB_API_PRIVATE_TOKEN'] = 'token' 6 | 7 | let(:formatter) { described_class.new } 8 | 9 | describe '#format' do 10 | subject { formatter.format(messages, repo, nil) } 11 | let(:messages) { [message, message] } 12 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 13 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 14 | let(:line) { double(new_lineno: 1, commit_sha: '123', position: nil) } 15 | let(:paginated_response) { double(auto_paginate: []) } 16 | before { line.stub(:commit_line).and_return(line) } 17 | 18 | specify do 19 | ::Gitlab::Client.any_instance 20 | .should_receive(:commit_comments) 21 | .once 22 | .and_return(paginated_response) 23 | 24 | ::Gitlab::Client.any_instance 25 | .should_receive(:create_commit_comment) 26 | .once 27 | 28 | subject 29 | end 30 | end 31 | 32 | describe '#format without duplicates' do 33 | subject { formatter.format(messages, repo, nil) } 34 | let(:messages) { [message1, message2] } 35 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 36 | let(:message1) { Message.new('path/to1', line1, :warning, 'crucial') } 37 | let(:message2) { Message.new('path/to2', line2, :warning, 'crucial') } 38 | let(:line1) { double(new_lineno: 1, commit_sha: '123', position: nil) } 39 | let(:line2) { double(new_lineno: 2, commit_sha: '123', position: nil) } 40 | let(:paginated_response) { double(auto_paginate: []) } 41 | before do 42 | line1.stub(:commit_line).and_return(line1) 43 | line2.stub(:commit_line).and_return(line2) 44 | end 45 | 46 | specify do 47 | ::Gitlab::Client.any_instance 48 | .should_receive(:commit_comments) 49 | .once 50 | .and_return(paginated_response) 51 | 52 | ::Gitlab::Client.any_instance 53 | .should_receive(:create_commit_comment) 54 | .twice 55 | 56 | subject 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/pronto/formatter/gitlab_merge_request_review_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe GitlabMergeRequestReviewFormatter do 4 | ENV['PRONTO_GITLAB_API_ENDPOINT'] = 'http://example.com/api/v4' 5 | ENV['PRONTO_GITLAB_API_PRIVATE_TOKEN'] = 'token' 6 | ENV['CI_MERGE_REQUEST_IID'] = '1' 7 | 8 | let(:formatter) { described_class.new } 9 | let(:diff_refs) { 10 | double(diff_refs: {"base_sha"=>"8f38ee927a5ea1e7fcf91e2603f9a09a2f6ad8a7", 11 | "head_sha"=>"4075991c8c9170e614f754aed3a52b25f4a586b4", 12 | "start_sha"=>"8f38ee927a5ea1e7fcf91e2603f9a09a2f6ad8a7"}) 13 | } 14 | 15 | describe '#format' do 16 | subject { formatter.format(messages, repo, nil) } 17 | let(:messages) { [message, message] } 18 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 19 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 20 | let(:line) { double(line: double(new_lineno: 1), commit_sha: '123', position: nil) } 21 | let(:paginated_response) { double(auto_paginate: []) } 22 | before { line.stub(:commit_line).and_return(line) } 23 | 24 | specify do 25 | ::Gitlab::Client.any_instance 26 | .should_receive(:merge_request_discussions) 27 | .once 28 | .and_return(paginated_response) 29 | 30 | ::Gitlab::Client.any_instance 31 | .should_receive(:merge_request) 32 | .once 33 | .and_return(diff_refs) 34 | 35 | ::Gitlab::Client.any_instance 36 | .should_receive(:create_merge_request_discussion) 37 | .once 38 | 39 | subject 40 | end 41 | end 42 | 43 | describe '#format without duplicates' do 44 | subject { formatter.format(messages, repo, nil) } 45 | let(:messages) { [message1, message2] } 46 | let(:repo) { Git::Repository.new('spec/fixtures/test.git') } 47 | let(:message1) { Message.new('path/to1', line1, :warning, 'crucial') } 48 | let(:message2) { Message.new('path/to2', line2, :warning, 'crucial') } 49 | let(:line1) { double(line: double(new_lineno: 1), commit_sha: '123', position: nil) } 50 | let(:line2) { double(line: double(new_lineno: 2), commit_sha: '123', position: nil) } 51 | let(:paginated_response) { double(auto_paginate: []) } 52 | before do 53 | line1.stub(:commit_line).and_return(line1) 54 | line2.stub(:commit_line).and_return(line2) 55 | end 56 | 57 | specify do 58 | ::Gitlab::Client.any_instance 59 | .should_receive(:merge_request_discussions) 60 | .once 61 | .and_return(paginated_response) 62 | 63 | ::Gitlab::Client.any_instance 64 | .should_receive(:merge_request) 65 | .once 66 | .and_return(diff_refs) 67 | 68 | ::Gitlab::Client.any_instance 69 | .should_receive(:create_merge_request_discussion) 70 | .twice 71 | 72 | subject 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/pronto/formatter/json_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe JsonFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | describe '#format' do 7 | subject { formatter.format(messages, nil, nil) } 8 | let(:messages) { [message, message] } 9 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 10 | let(:line) { double(new_lineno: 1, commit_sha: nil) } 11 | let(:runner) { Class } 12 | 13 | it do 14 | should == 15 | '[{"level":"W","message":"crucial","path":"path/to","line":1},'\ 16 | '{"level":"W","message":"crucial","path":"path/to","line":1}]' 17 | end 18 | 19 | context 'message without path' do 20 | let(:message) { Message.new(nil, line, :warning, 'careful') } 21 | 22 | it do 23 | should == '[{"level":"W","message":"careful","line":1},'\ 24 | '{"level":"W","message":"careful","line":1}]' 25 | end 26 | end 27 | 28 | context 'message without line' do 29 | let(:message) { Message.new('path/to', nil, :warning, 'careful') } 30 | 31 | it do 32 | should == '[{"level":"W","message":"careful","path":"path/to"},'\ 33 | '{"level":"W","message":"careful","path":"path/to"}]' 34 | end 35 | end 36 | 37 | context 'message with a runner' do 38 | let(:message) do 39 | Message.new(nil, line, :warning, 'careful', nil, runner) 40 | end 41 | 42 | it do 43 | should == 44 | '[{"level":"W","message":"careful","line":1,"runner":"Class"},'\ 45 | '{"level":"W","message":"careful","line":1,"runner":"Class"}]' 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/pronto/formatter/null_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe NullFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | describe '#format' do 7 | subject { formatter.format(messages, nil, nil) } 8 | let(:messages) { [message, message] } 9 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 10 | let(:line) { double(new_lineno: 1, commit_sha: '123') } 11 | 12 | it { should be_nil } 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/pronto/formatter/text_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Formatter 3 | describe TextFormatter do 4 | let(:formatter) { described_class.new } 5 | 6 | before { $stdout.stub(:tty?) { false } } 7 | 8 | describe '#format' do 9 | subject { formatter.format(messages, nil, nil) } 10 | let(:messages) { [message, message] } 11 | let(:message) { Message.new('path/to', line, :warning, 'crucial') } 12 | let(:line) { double(new_lineno: 1, commit_sha: '123') } 13 | 14 | its(:count) { should == 2 } 15 | its(:first) { should == 'path/to:1 W: crucial' } 16 | 17 | context 'message with commit SHA' do 18 | let(:message) { Message.new(nil, nil, :warning, 'careful', '8d79b5') } 19 | 20 | its(:count) { should == 2 } 21 | its(:first) { should == '8d79b5 W: careful' } 22 | end 23 | 24 | context 'message without path' do 25 | let(:message) { Message.new(nil, line, :warning, 'careful') } 26 | 27 | its(:count) { should == 2 } 28 | its(:first) { should == ':1 W: careful' } 29 | end 30 | 31 | context 'message without line' do 32 | let(:message) { Message.new('path/to', nil, :warning, 'careful') } 33 | 34 | its(:count) { should == 2 } 35 | its(:first) { should == 'path/to: W: careful' } 36 | end 37 | 38 | context 'message without line, path and commit SHA' do 39 | let(:message) { Message.new(nil, nil, :warning, 'careful', nil) } 40 | 41 | its(:count) { should == 2 } 42 | its(:first) { should == 'W: careful' } 43 | end 44 | 45 | context 'with custom config' do 46 | before do 47 | formatter.stub(:config) do 48 | Config.new('text' => { 'format' => '%{line} %{path}' }) 49 | end 50 | end 51 | its(:count) { should == 2 } 52 | its(:first) { should == '1 path/to' } 53 | end 54 | 55 | context 'in TTY' do 56 | before { $stdout.stub(:tty?) { true } } 57 | 58 | context 'message with commit SHA' do 59 | let(:message) { Message.new(nil, nil, :warning, 'msg', '8d79b5') } 60 | 61 | its(:first) { should == "\e[36m8d79b5\e[0m \e[35mW\e[0m: msg" } 62 | end 63 | 64 | context 'message without path' do 65 | let(:message) { Message.new(nil, line, :warning, 'msg') } 66 | 67 | its(:first) { should == ":1 \e[35mW\e[0m: msg" } 68 | end 69 | 70 | context 'message without line' do 71 | let(:message) { Message.new('path/to', nil, :warning, 'msg') } 72 | 73 | its(:first) { should == "\e[36mpath/to\e[0m: \e[35mW\e[0m: msg" } 74 | end 75 | 76 | context 'message without line, path and commit SHA' do 77 | let(:message) { Message.new(nil, nil, :warning, 'careful', nil) } 78 | 79 | its(:count) { should == 2 } 80 | its(:first) { should == "\e[35mW\e[0m: careful" } 81 | end 82 | 83 | context 'info message' do 84 | let(:message) { Message.new('path/to', line, :info, 'msg') } 85 | 86 | its(:first) { should == "\e[36mpath/to\e[0m:1 \e[33mI\e[0m: msg" } 87 | end 88 | 89 | context 'warning message' do 90 | let(:message) { Message.new('path/to', line, :warning, 'msg') } 91 | 92 | its(:first) { should == "\e[36mpath/to\e[0m:1 \e[35mW\e[0m: msg" } 93 | end 94 | 95 | context 'error message' do 96 | let(:message) { Message.new('path/to', line, :error, 'msg') } 97 | 98 | its(:first) { should == "\e[36mpath/to\e[0m:1 \e[31mE\e[0m: msg" } 99 | end 100 | 101 | context 'fatal message' do 102 | let(:message) { Message.new('path/to', line, :fatal, 'msg') } 103 | 104 | its(:first) { should == "\e[36mpath/to\e[0m:1 \e[31mF\e[0m: msg" } 105 | end 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/pronto/gem_names_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe GemNames do 3 | let(:gem_names) { described_class.new } 4 | 5 | describe '.to_a' do 6 | subject { gem_names.to_a } 7 | before { Gem::Specification.should_receive(:find_all) { gems } } 8 | 9 | context 'properly named gem' do 10 | let(:gems) { [double(name: 'pronto-rubocop')] } 11 | it { should include('rubocop') } 12 | end 13 | 14 | context 'duplicate names' do 15 | let(:gem) { double(name: 'pronto-rubocop') } 16 | let(:gems) { [gem, gem] } 17 | it { should include('rubocop') } 18 | its(:count) { should == 1 } 19 | end 20 | 21 | context 'improperly named gem' do 22 | context 'with good path' do 23 | let(:gems) { [double(name: 'good', full_gem_path: '/good')] } 24 | before do 25 | File.stub(:exist?).with('/good/lib/pronto/good.rb').and_return(true) 26 | end 27 | it { should include('good') } 28 | end 29 | 30 | context 'with bad path' do 31 | let(:gems) { [double(name: 'bad', full_gem_path: '/bad')] } 32 | before do 33 | File.stub(:exist?).with('/bad/lib/pronto/bad.rb').and_return(false) 34 | end 35 | it { should_not include('bad') } 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/pronto/git/line_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Git 3 | describe Line do 4 | let(:line) { described_class.new(rugged_line, patch, hunk) } 5 | 6 | let(:patch) { nil } 7 | let(:hunk) { nil } 8 | 9 | describe '#addition?' do 10 | subject { line.addition? } 11 | 12 | let(:rugged_line) { double(addition?: true) } 13 | it { should == true } 14 | end 15 | 16 | describe '#deletion?' do 17 | subject { line.deletion? } 18 | 19 | let(:rugged_line) { double(deletion?: false) } 20 | it { should == false } 21 | end 22 | 23 | describe '#content' do 24 | subject { line.content } 25 | 26 | let(:rugged_line) { double(content: 'hello') } 27 | it { should == 'hello' } 28 | end 29 | 30 | describe '#new_lineno' do 31 | subject { line.new_lineno } 32 | 33 | let(:rugged_line) { double(new_lineno: 1) } 34 | it { should == 1 } 35 | end 36 | 37 | describe '#old_lineno' do 38 | subject { line.old_lineno } 39 | 40 | let(:rugged_line) { double(old_lineno: 42) } 41 | it { should == 42 } 42 | end 43 | 44 | describe '#line_origin' do 45 | subject { line.line_origin } 46 | 47 | let(:rugged_line) { double(line_origin: 15) } 48 | it { should == 15 } 49 | end 50 | 51 | describe '#==' do 52 | subject { line == other } 53 | 54 | let(:rugged_line) do 55 | double(content: 'hello', 56 | line_origin: 2, 57 | new_lineno: 10, 58 | old_lineno: 11) 59 | end 60 | 61 | context 'equal' do 62 | let(:other) { rugged_line } 63 | it { should == true } 64 | end 65 | 66 | context 'different' do 67 | let(:other) do 68 | double(content: 'Bob', 69 | line_origin: 7, 70 | new_lineno: 17, 71 | old_lineno: 17) 72 | end 73 | it { should == false } 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/pronto/git/patch_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Git 3 | describe Patch do 4 | let(:patch) { described_class.new(rugged_patch, repo) } 5 | 6 | let(:repo) { nil } 7 | 8 | describe '#additions' do 9 | subject { patch.additions } 10 | 11 | let(:rugged_patch) { double(stat: [15, 13]) } 12 | it { should == 15 } 13 | end 14 | 15 | describe '#deletions' do 16 | subject { patch.deletions } 17 | 18 | let(:rugged_patch) { double(stat: [5, 17]) } 19 | it { should == 17 } 20 | end 21 | 22 | describe '#lines' do 23 | subject { patch.lines } 24 | 25 | let(:hunks) { [double(lines: [1, 2]), double(lines: [3])] } 26 | let(:rugged_patch) { double(hunks: hunks) } 27 | its(:count) { should == 3 } 28 | end 29 | 30 | describe '#new_file_full_path' do 31 | subject { patch.new_file_full_path } 32 | 33 | let(:rugged_patch) do 34 | double(delta: double(new_file: { path: 'test.md' })) 35 | end 36 | let(:repo) { double(path: Pathname.new('/house/of/cards')) } 37 | its(:to_s) { should == '/house/of/cards/test.md' } 38 | end 39 | 40 | describe '#new_file_path' do 41 | subject { patch.new_file_path } 42 | 43 | let(:rugged_patch) do 44 | double(delta: double(new_file: { path: 'test.md' })) 45 | end 46 | let(:repo) { double(path: Pathname.new('/house/of/cards')) } 47 | its(:to_s) { should == 'test.md' } 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/pronto/git/patches_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Git 3 | describe Patches do 4 | let(:patches) { described_class.new(repo, commit, patch_array) } 5 | let(:repo) { nil } 6 | let(:commit) { nil } 7 | let(:patch_array) { [] } 8 | 9 | describe '#repo' do 10 | subject { patches.repo } 11 | 12 | context 'non-nil repo' do 13 | let(:repo) { double } 14 | it { should_not be_nil } 15 | end 16 | end 17 | 18 | describe '#reject' do 19 | subject { patches.reject { |_| true } } 20 | let(:patch_array) { [double(new_file_full_path: 1)] } 21 | 22 | it 'does not modify original' do 23 | subject.to_a.should be_empty 24 | patches.to_a.should_not be_empty 25 | end 26 | end 27 | 28 | describe '#find_line' do 29 | subject { patches.find_line(path, line) } 30 | 31 | let(:path) { '/test.rb' } 32 | let(:line) { 1 } 33 | 34 | it { should be_nil } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/pronto/git/repository_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | module Git 3 | describe Repository do 4 | let(:repo) { described_class.new('spec/fixtures/test.git') } 5 | 6 | describe '#path' do 7 | subject { repo.path } 8 | its(:to_s) { should end_with 'pronto/spec/fixtures' } 9 | end 10 | 11 | describe '#branch' do 12 | subject { repo.branch } 13 | it { should == 'master' } 14 | end 15 | 16 | describe '#remote_urls' do 17 | subject { repo.remote_urls } 18 | it { should be_empty } 19 | end 20 | 21 | describe '#commits_until' do 22 | subject { repo.commits_until(sha) } 23 | 24 | context 'initial' do 25 | let(:sha) { '3e0e3ab' } 26 | it 'should list all the commits' do 27 | should == %w[64dadfdb7c7437476782e8eb024085862e6287d6 28 | 7b21c8f4dfb0b8aa39739fc16678c5934877a414 29 | 577afa184c9bc82a66c40047d0809e5fcc43489f 30 | ec05bab7d263d5e01be99f2c4e10a5974e24e6de 31 | d6d56582ebfd0c6c3263ea4c4e2d727048370124 32 | ac86326d7231ad77dab94e2c4f6f61245a2d9bec 33 | 3e0e3ab0a436fc2a9c05253a439dc6084699b7d5] 34 | end 35 | end 36 | 37 | context 'last' do 38 | let(:sha) { '64dadfd' } 39 | it do 40 | should contain_exactly('64dadfdb7c7437476782e8eb024085862e6287d6') 41 | end 42 | end 43 | end 44 | 45 | describe '#show_commit' do 46 | subject { repo.show_commit(sha) } 47 | 48 | context 'initial' do 49 | let(:sha) { '3e0e3ab' } 50 | it { should be_none } 51 | end 52 | 53 | context 'last' do 54 | let(:sha) { '64dadfd' } 55 | it { should be_one } 56 | end 57 | end 58 | 59 | describe '#diff' do 60 | subject { repo.diff(sha, options) } 61 | let(:options) { nil } 62 | 63 | context 'initial' do 64 | let(:sha) { '3e0e3ab' } 65 | it { should be_one } 66 | end 67 | 68 | context 'last' do 69 | let(:sha) { '64dadfd' } 70 | it { should be_none } 71 | end 72 | 73 | context 'unstaged' do 74 | let(:sha) { :unstaged } 75 | 76 | context 'all files' do 77 | it { should be_one } 78 | end 79 | 80 | context 'hamlet.txt' do 81 | let(:options) { { paths: ['hamlet.txt'] } } 82 | it { should be_one } 83 | end 84 | 85 | context 'hamlet.txt' do 86 | let(:options) { { paths: ['ophelia.txt'] } } 87 | it { should be_none } 88 | end 89 | end 90 | 91 | context 'staged' do 92 | let(:sha) { :staged } 93 | it { should be_one } 94 | end 95 | 96 | context 'workdir' do 97 | let(:sha) { :workdir } 98 | 99 | it do 100 | # this count includes all the files from the repositories (*.git) 101 | subject.count.should eq 48 102 | end 103 | end 104 | end 105 | 106 | describe '#blame' do 107 | context 'given an existent file' do 108 | subject { repo.blame('hamlet.txt', 1) } 109 | 110 | it do 111 | should match a_hash_including( 112 | orig_start_line_number: 1, 113 | orig_commit_id: 'ac86326d7231ad77dab94e2c4f6f61245a2d9bec', 114 | final_start_line_number: 1, 115 | final_commit_id: 'ac86326d7231ad77dab94e2c4f6f61245a2d9bec' 116 | ) 117 | end 118 | end 119 | 120 | context 'given a new file' do 121 | subject { repo.blame('new_file.txt', 1) } 122 | 123 | it do 124 | should be nil 125 | end 126 | end 127 | end 128 | end 129 | 130 | describe Repository do 131 | let(:repo) { described_class.new('spec/fixtures/renamed-file.git') } 132 | 133 | describe '#diff' do 134 | subject { repo.diff(sha, options) } 135 | let(:options) { nil } 136 | 137 | context 'initial' do 138 | let(:sha) { '3e2cbd4' } 139 | it { should be_one } 140 | end 141 | end 142 | end 143 | 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/pronto/github_pull_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe GithubPull do 3 | let(:github_pull) { described_class.new(octokit_client, slug) } 4 | let(:slug) { 'prontolabs/pronto' } 5 | let(:octokit_client) { double(Octokit::Client) } 6 | 7 | before do 8 | github_pull.instance_variable_set(:@client, octokit_client) 9 | end 10 | 11 | describe '#pull_by_id' do 12 | subject { github_pull.pull_by_id(10) } 13 | 14 | context 'pull request exists' do 15 | let(:pull) { { number: 10, head: { sha: 123_456 } } } 16 | specify do 17 | octokit_client 18 | .should_receive(:pull_requests) 19 | .once 20 | .and_return([pull]) 21 | 22 | subject.should eq(pull) 23 | end 24 | end 25 | 26 | context 'pull request does not exist' do 27 | specify do 28 | octokit_client 29 | .should_receive(:pull_requests) 30 | .once 31 | .and_return([]) 32 | 33 | -> { subject }.should raise_error(Pronto::Error, /Pull request ##{10}/) 34 | end 35 | end 36 | end 37 | 38 | describe '#pull_by_branch' do 39 | let(:branch) { 'awesome_branch' } 40 | 41 | context 'branch exists' do 42 | subject { github_pull.pull_by_branch(branch) } 43 | let(:pull) { { head: { ref: branch } } } 44 | specify do 45 | octokit_client 46 | .should_receive(:pull_requests) 47 | .once 48 | .and_return([pull]) 49 | 50 | subject.should eq(pull) 51 | end 52 | end 53 | 54 | context 'branch does not exist' do 55 | subject { github_pull.pull_by_branch(branch) } 56 | specify do 57 | octokit_client 58 | .should_receive(:pull_requests) 59 | .once 60 | .and_return([]) 61 | 62 | -> { subject }.should raise_error(Pronto::Error, /#{branch}/) 63 | end 64 | end 65 | end 66 | 67 | describe '#pull_by_commit' do 68 | let(:sha) { 'a6fmk32' } 69 | 70 | context 'commit exists' do 71 | subject { github_pull.pull_by_commit(sha) } 72 | let(:pull) { { head: { sha: sha } } } 73 | specify do 74 | octokit_client 75 | .should_receive(:pull_requests) 76 | .once 77 | .and_return([pull]) 78 | 79 | subject.should eq(pull) 80 | end 81 | end 82 | 83 | context 'commit does not exist' do 84 | subject { github_pull.pull_by_commit(sha) } 85 | specify do 86 | octokit_client 87 | .should_receive(:pull_requests) 88 | .once 89 | .and_return([]) 90 | 91 | -> { subject }.should raise_error(Pronto::Error, /#{sha}/) 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/pronto/github_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Github do 3 | let(:github) { described_class.new(repo) } 4 | let(:repo) { double(remote_urls: ssh_remote_urls, branch: nil, head_detached?: false) } 5 | let(:ssh_remote_urls) { ["git@github.com:#{github_slug}.git"] } 6 | let(:github_slug) { 'prontolabs/pronto' } 7 | let(:sha) { '61e4bef' } 8 | let(:comment) { double(body: 'note', path: 'path', line: 1, position: 1) } 9 | let(:empty_client_options) do 10 | { 11 | event: 'COMMENT' 12 | } 13 | end 14 | 15 | before { ENV.stub(:[]) } 16 | 17 | describe '#commit_comments' do 18 | subject { github.commit_comments(sha) } 19 | 20 | context 'three requests for same comments' do 21 | specify do 22 | Octokit::Client.any_instance 23 | .should_receive(:commit_comments) 24 | .with(github_slug, sha) 25 | .once 26 | .and_return([comment]) 27 | 28 | subject 29 | subject 30 | subject 31 | end 32 | end 33 | 34 | context 'git remote without .git suffix' do 35 | let(:repo) { double(remote_urls: ssh_remote_urls) } 36 | 37 | specify do 38 | Octokit::Client.any_instance 39 | .should_receive(:commit_comments) 40 | .with(github_slug, sha) 41 | .once 42 | .and_return([comment]) 43 | 44 | subject 45 | end 46 | end 47 | end 48 | 49 | describe '#pull_comments' do 50 | subject { github.pull_comments(sha) } 51 | 52 | before { ENV.stub(:[]).with('PRONTO_PULL_REQUEST_ID').and_return(10) } 53 | 54 | context 'three requests for same comments' do 55 | specify do 56 | Octokit::Client.any_instance 57 | .should_receive(:pull_comments) 58 | .with(github_slug, 10) 59 | .once 60 | .and_return([comment]) 61 | 62 | 3.times { subject } 63 | end 64 | end 65 | 66 | context 'pull request does not exist' do 67 | specify do 68 | Octokit::Client.any_instance 69 | .should_receive(:pull_comments) 70 | .and_raise(Octokit::NotFound) 71 | 72 | -> { subject }.should raise_error(Pronto::Error) 73 | end 74 | end 75 | end 76 | 77 | describe '#create_commit_status' do 78 | subject { github.create_commit_status(status) } 79 | 80 | let(:octokit_client) { double(Octokit::Client) } 81 | let(:github_pull) { double(GithubPull) } 82 | let(:status) { Status.new(sha, state, context, desc) } 83 | let(:state) { :success } 84 | let(:context) { :pronto } 85 | let(:desc) { 'No issues found!' } 86 | 87 | before do 88 | github.instance_variable_set(:@client, octokit_client) 89 | github.instance_variable_set(:@github_pull, github_pull) 90 | 91 | octokit_client 92 | .should_receive(:create_status) 93 | .with(github_slug, expected_sha, state, context: context, 94 | description: desc) 95 | .once 96 | end 97 | 98 | context 'uses PRONTO_PULL_REQUEST_ID to create commit status' do 99 | let(:pull_request_sha) { '123456' } 100 | let(:expected_sha) { pull_request_sha } 101 | 102 | specify do 103 | ENV.stub(:[]).with('PRONTO_PULL_REQUEST_ID').and_return(10) 104 | github_pull 105 | .should_receive(:pull_by_id) 106 | .with(10) 107 | .once 108 | .and_return(head: { sha: pull_request_sha }) 109 | 110 | subject 111 | end 112 | end 113 | 114 | context 'adds status to commit with sha' do 115 | let(:expected_sha) { sha } 116 | 117 | specify do 118 | octokit_client.should_not_receive(:pull_requests) 119 | 120 | subject 121 | end 122 | end 123 | end 124 | 125 | describe '#publish_pull_request_comments' do 126 | subject { github.publish_pull_request_comments(comments) } 127 | 128 | let(:octokit_client) { double(Octokit::Client) } 129 | 130 | before do 131 | github.instance_variable_set(:@client, octokit_client) 132 | end 133 | 134 | context 'with no comments' do 135 | let(:comments) { [] } 136 | 137 | specify do 138 | octokit_client.should_not_receive(:create_pull_request_review) 139 | subject 140 | end 141 | end 142 | 143 | context 'with comments and' do 144 | before do 145 | github.stub(:pull_id).and_return(pull_id) 146 | config.stub(:warnings_per_review).and_return(warnings_per_review) 147 | end 148 | 149 | let(:pull_id) { 10 } 150 | let(:config) { github.instance_variable_get(:@config) } 151 | let(:comments) do 152 | [ 153 | double(path: 'bad_file.rb', position: 10, body: 'Offense #1'), 154 | double(path: 'bad_file.rb', position: 20, body: 'Offense #2') 155 | ] 156 | end 157 | let(:options) do 158 | empty_client_options 159 | .merge(comments: [{ path: 'bad_file.rb', line: 10, body: 'Offense #1' }, 160 | { path: 'bad_file.rb', line: 20, body: 'Offense #2' }]) 161 | end 162 | 163 | { 164 | equal: 2, 165 | more_than: 5 166 | }.each do |condition, warnings_per_review| 167 | context "when warnings per review #{condition} total comments" do 168 | let(:warnings_per_review) { warnings_per_review } 169 | 170 | specify do 171 | octokit_client 172 | .should_receive(:create_pull_request_review) 173 | .with(github_slug, pull_id, options) 174 | .once 175 | 176 | subject 177 | end 178 | end 179 | end 180 | 181 | context 'when warnings per review are lower than comments' do 182 | 183 | let(:warnings_per_review) { 1 } 184 | let(:first_options) do 185 | empty_client_options 186 | .merge(comments: [{ path: 'bad_file.rb', line: 10, body: 'Offense #1' }]) 187 | end 188 | let(:second_options) do 189 | empty_client_options 190 | .merge(comments: [{ path: 'bad_file.rb', line: 20, body: 'Offense #2' }]) 191 | end 192 | 193 | specify do 194 | octokit_client 195 | .should_receive(:create_pull_request_review) 196 | .with(github_slug, pull_id, first_options) 197 | .once 198 | octokit_client 199 | .should_receive(:create_pull_request_review) 200 | .with(github_slug, pull_id, second_options) 201 | .once 202 | 203 | subject 204 | end 205 | end 206 | end 207 | end 208 | end 209 | end 210 | -------------------------------------------------------------------------------- /spec/pronto/gitlab_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Gitlab do 3 | let(:gitlab) { described_class.new(repo) } 4 | 5 | describe '#slug' do 6 | subject { gitlab.send(:slug) } 7 | before(:each) do 8 | ENV['PRONTO_GITLAB_API_ENDPOINT'] = 'http://gitlab.example.com/api/v4' 9 | ENV['PRONTO_GITLAB_API_PRIVATE_TOKEN'] = 'token' 10 | end 11 | 12 | context 'ssh with port remote url' do 13 | let(:repo) do 14 | remote_url = 'ssh://git@gitlab.example.com:111/prontolabs/pronto.git' 15 | double(remote_urls: [remote_url]) 16 | end 17 | 18 | it 'returns correct slug' do 19 | subject.should eql('prontolabs/pronto') 20 | end 21 | end 22 | 23 | context 'git remote url' do 24 | let(:repo) do 25 | double(remote_urls: ['git@gitlab.example.com:prontolabs/pronto.git']) 26 | end 27 | 28 | it 'returns correct slug' do 29 | subject.should eql('prontolabs/pronto') 30 | end 31 | end 32 | 33 | context 'http remote url' do 34 | let(:repo) do 35 | double(remote_urls: ['https://gitlab.example.com/prontolabs/pronto.git']) 36 | end 37 | 38 | it 'returns correct slug' do 39 | subject.should eql('prontolabs/pronto') 40 | end 41 | end 42 | 43 | context 'http remote url with different host' do 44 | let(:repo) do 45 | double(remote_urls: ['https://gitlab.example.net/prontolabs/pronto.git']) 46 | end 47 | 48 | it 'returns correct slug' do 49 | subject.should eql('prontolabs/pronto') 50 | end 51 | end 52 | end 53 | 54 | describe '#commit_comments' do 55 | subject { gitlab.commit_comments(sha) } 56 | 57 | context 'three requests for same comments' do 58 | let(:repo) do 59 | double(remote_urls: ['git@gitlab.example.com:prontolabs/pronto.git']) 60 | end 61 | let(:sha) { 'foobar' } 62 | let(:comment) { double(note: 'body', path: 'path', line: 1) } 63 | let(:paginated_response) { double(auto_paginate: [ comment ]) } 64 | 65 | specify do 66 | ENV['PRONTO_GITLAB_API_ENDPOINT'] = 'http://gitlab.example.com/api/v4' 67 | ENV['PRONTO_GITLAB_API_PRIVATE_TOKEN'] = 'token' 68 | 69 | ::Gitlab::Client.any_instance 70 | .should_receive(:commit_comments) 71 | .with('prontolabs/pronto', sha) 72 | .once 73 | .and_return(paginated_response) 74 | 75 | subject 76 | subject 77 | subject 78 | end 79 | end 80 | end 81 | 82 | describe '#pull_comments' do 83 | subject { gitlab.pull_comments(sha) } 84 | 85 | context 'three requests for same comments' do 86 | let(:repo) do 87 | double(remote_urls: ['git@gitlab.example.com:prontolabs/pronto.git']) 88 | end 89 | let(:sha) { 'foobar' } 90 | let(:comment) { double(notes: [{'body' => 'body', 'position' => {'new_path' => 'test', 'old_path' => nil}}]) } 91 | let(:paginated_response) { double(auto_paginate: [ comment ]) } 92 | 93 | specify do 94 | ENV['PRONTO_GITLAB_API_ENDPOINT'] = 'http://gitlab.example.com/api/v4' 95 | ENV['PRONTO_GITLAB_API_PRIVATE_TOKEN'] = 'token' 96 | ENV['CI_MERGE_REQUEST_IID'] = '10' 97 | 98 | ::Gitlab::Client.any_instance 99 | .should_receive(:merge_request_discussions) 100 | .with('prontolabs/pronto', 10) 101 | .once 102 | .and_return(paginated_response) 103 | 104 | subject 105 | subject 106 | subject 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/pronto/logger_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Logger do 3 | let(:logger) { described_class.new(io) } 4 | let(:io) { StringIO.new } 5 | let(:output) { io.string } 6 | 7 | describe '#log' do 8 | before { logger.log('hello world') } 9 | 10 | subject { output } 11 | 12 | it { should_not be_empty } 13 | its([-1, 1]) { should == "\n" } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/pronto/message_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Message do 3 | let(:message) { described_class.new(path, line, level, msg, '8cda581') } 4 | 5 | let(:path) { 'README.md' } 6 | let(:line) { Git::Line.new } 7 | let(:msg) { 'message' } 8 | let(:level) { :warning } 9 | 10 | describe '.new' do 11 | subject { message } 12 | 13 | Message::LEVELS.each do |message_level| 14 | context "set log level to #{message_level}" do 15 | let(:level) { message_level } 16 | its(:level) { should == message_level } 17 | end 18 | end 19 | 20 | context 'bad level' do 21 | let(:level) { :random } 22 | specify do 23 | -> { subject }.should raise_error(::ArgumentError) 24 | end 25 | end 26 | end 27 | 28 | describe '#full_path' do 29 | subject { message.full_path } 30 | 31 | context 'line is nil' do 32 | let(:line) { nil } 33 | it { should be_nil } 34 | end 35 | end 36 | 37 | describe '#repo' do 38 | subject { message.repo } 39 | 40 | context 'line is nil' do 41 | let(:line) { nil } 42 | it { should be_nil } 43 | end 44 | end 45 | 46 | describe '#==' do 47 | subject { message == other } 48 | 49 | context 'path, msg and line match' do 50 | let(:other) { described_class.new(path, line, level, msg, '1111') } 51 | it { should be_truthy } 52 | end 53 | 54 | context 'without lines, path, msg and sha match' do 55 | let(:line) { nil } 56 | let(:other) { described_class.new(path, nil, level, msg, '8cda581') } 57 | it { should be_truthy } 58 | end 59 | 60 | context 'only path and msg match' do 61 | let(:other) { described_class.new(path, nil, level, msg, '1111') } 62 | it { should be_falsy } 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/pronto/runner_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Runner do 3 | let(:runner) { Runner.new([]) } 4 | 5 | describe '.title' do 6 | before { Runner.any_instance.stub(:run) } 7 | subject { Runner.title } 8 | it { should == 'recorder' } 9 | end 10 | 11 | describe '#ruby_file?' do 12 | subject { runner.ruby_file?(path) } 13 | 14 | context 'ending with .rb' do 15 | let(:path) { 'test.rb' } 16 | it { should be_truthy } 17 | end 18 | 19 | context 'ending with .rb in directory' do 20 | let(:path) { 'amazing/test.rb' } 21 | it { should be_truthy } 22 | end 23 | 24 | context 'ending with .rake' do 25 | let(:path) { 'test.rake' } 26 | it { should be_truthy } 27 | end 28 | 29 | context 'ending with .gemspec' do 30 | let(:path) { 'test.gemspec' } 31 | it { should be_truthy } 32 | end 33 | 34 | context 'named Gemfile' do 35 | let(:path) { 'Gemfile' } 36 | it { should be_truthy } 37 | end 38 | 39 | context 'named Gemfile in directory' do 40 | let(:path) { 'test/Gemfile' } 41 | it { should be_truthy } 42 | end 43 | 44 | context 'executable' do 45 | let(:path) { 'test' } 46 | before { File.stub(:open).with(path).and_return(shebang) } 47 | 48 | context 'ruby' do 49 | let(:shebang) { '#!ruby' } 50 | it { should be_truthy } 51 | end 52 | 53 | context 'bash' do 54 | let(:shebang) { '#! bash' } 55 | it { should be_falsy } 56 | end 57 | end 58 | 59 | context 'directory' do 60 | let(:path) { 'directory' } 61 | before { File.stub(:directory?).with(path).and_return(true) } 62 | it { should be_falsy } 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/pronto/runners_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Runners do 3 | describe '#run' do 4 | subject { described_class.new(runners, config).run(patches) } 5 | let(:patches) { double(commit: nil, none?: false) } 6 | let(:config) { Config.new } 7 | 8 | let(:fake_runner) do 9 | Class.new do 10 | def self.title 11 | 'fake_runner' 12 | end 13 | 14 | def initialize(_, _); end 15 | 16 | def run 17 | [1, nil, [3]] 18 | end 19 | end 20 | end 21 | 22 | context 'no runners' do 23 | let(:runners) { [] } 24 | it { should be_empty } 25 | end 26 | 27 | context 'fake runner' do 28 | let(:runners) { [fake_runner] } 29 | it { should == [1, 3] } 30 | 31 | context 'rejects excluded files' do 32 | before { config.stub(:excluded_files) { ['1'] } } 33 | let(:patches) { [double(new_file_full_path: 1)] } 34 | it { should be_empty } 35 | end 36 | 37 | context 'max warnings' do 38 | before { config.stub(:excluded_files) { [] } } 39 | before { config.stub(:max_warnings) { 1 } } 40 | it { should == [1] } 41 | end 42 | end 43 | 44 | context 'when multiple runners exist' do # rubocop:disable Metrics/BlockLength 45 | let(:fake_runner_2) do 46 | Class.new(fake_runner) do 47 | def self.title 48 | 'fake_runner_2' 49 | end 50 | 51 | def run 52 | [2, nil, [6]] 53 | end 54 | end 55 | end 56 | 57 | let(:fake_runner_3) do 58 | Class.new(fake_runner) do 59 | def self.title 60 | 'fake_runner_2' 61 | end 62 | 63 | def run 64 | [5, nil, [10]] 65 | end 66 | end 67 | end 68 | 69 | let(:runners) { [fake_runner, fake_runner_2, fake_runner_3] } 70 | 71 | context 'when runners are not filtered' do 72 | it { should == [1, 3, 2, 6, 5, 10] } 73 | end 74 | 75 | context 'when runners are listed in config' do 76 | before { config.stub(:runners) { ['fake_runner'] } } 77 | 78 | it { should == [1, 3] } 79 | end 80 | 81 | context 'when some runners are skipped via config' do 82 | before { config.stub(:skip_runners) { ['fake_runner'] } } 83 | 84 | it { should == [2, 6, 5, 10] } 85 | end 86 | 87 | context 'when same runners are skipped and some are listed' do 88 | before do 89 | config.stub(:runners) { %w[fake_runner fake_runner_3] } 90 | config.stub(:skip_runners) { %w[fake_runner_3] } 91 | end 92 | 93 | it { should == [1, 3] } 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/pronto/status_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Status do 3 | let(:status) { described_class.new(sha, state, context, description) } 4 | let(:sha) { '3e0e3ab' } 5 | let(:state) { 'state' } 6 | let(:context) { 'context' } 7 | let(:description) { 'desc' } 8 | 9 | describe '==' do 10 | context 'itself' do 11 | subject { status == status.dup } 12 | it { should be_truthy } 13 | end 14 | end 15 | 16 | describe '#to_s' do 17 | subject { status.to_s } 18 | it { should == '[3e0e3ab] context state - desc' } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/pronto_spec.rb: -------------------------------------------------------------------------------- 1 | module Pronto 2 | describe Pronto do 3 | describe '.run' do 4 | subject { Pronto.run(commit, repo, Formatter::NullFormatter.new, file) } 5 | let(:commit) { '3e0e3ab' } 6 | let(:repo) { 'spec/fixtures/test.git' } 7 | let(:file) { nil } 8 | 9 | context 'no runners' do 10 | before { Runner.runners.clear } 11 | it { should be_empty } 12 | end 13 | 14 | context 'master' do 15 | let(:commit) { 'master' } 16 | it { should be_empty } 17 | end 18 | 19 | context 'a single runner' do 20 | class Shakespeare 21 | def self.title 22 | 'shakespeare' 23 | end 24 | 25 | def initialize(patches, _) 26 | @patches = patches 27 | end 28 | 29 | def run 30 | @patches.flat_map(&:added_lines).flat_map do |line| 31 | [new_message(line)] if no_more?(line) 32 | end 33 | end 34 | 35 | def new_message(line) 36 | Message.new('hamlet.txt', line, :error, 'No more') 37 | end 38 | 39 | def no_more?(line) 40 | line.content.include?('No more') 41 | end 42 | end 43 | 44 | before { Runner.runners << Shakespeare if Runner.runners.empty? } 45 | 46 | context 'all files' do 47 | its(:count) { should == 1 } 48 | end 49 | 50 | context 'specific file' do 51 | let(:file) { 'hamlet.txt' } 52 | its(:count) { should == 1 } 53 | end 54 | 55 | context 'non-existant file' do 56 | let(:file) { 'lear.txt' } 57 | it { should be_empty } 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'support/coverage' 2 | 3 | require 'rspec' 4 | require 'rspec/its' 5 | 6 | require 'pronto' 7 | 8 | RSpec.configure do |config| 9 | config.order = :random 10 | Kernel.srand config.seed 11 | 12 | config.expect_with(:rspec) { |c| c.syntax = :should } 13 | config.mock_with(:rspec) { |c| c.syntax = :should } 14 | end 15 | 16 | def load_fixture(fixture_name) 17 | path = File.join(%w[spec fixtures], fixture_name) 18 | File.read(path).strip 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/coverage.rb: -------------------------------------------------------------------------------- 1 | if ENV['COVERAGE'] 2 | require 'simplecov' 3 | 4 | SimpleCov.command_name "rspec_#{Process.pid}" 5 | SimpleCov.start do 6 | add_filter '/spec/' 7 | end 8 | end 9 | --------------------------------------------------------------------------------