├── .cirrus.yml ├── .github_changelog_generator ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── ISSUES.md ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── prerelease-generate-changelog └── setup ├── bors.toml ├── dashboard-mockups └── mockup.html ├── exe └── inq ├── fixtures └── vcr_cassettes │ ├── how-is-example-empty-repository.yml │ ├── how-is-example-repository-with-date-interval.yml │ ├── how-is-example-repository.yml │ ├── how-is-from-config-frontmatter.yml │ ├── how-is-how-is-travis-api-repos-builds.yml │ ├── how-is-with-config-file.yml │ ├── how_is_contributions_additions_count.yml │ ├── how_is_contributions_all_contributors.yml │ ├── how_is_contributions_changed_files.yml │ ├── how_is_contributions_changes.yml │ ├── how_is_contributions_commits.yml │ ├── how_is_contributions_compare_url.yml │ ├── how_is_contributions_default_branch.yml │ ├── how_is_contributions_deletions_count.yml │ ├── how_is_contributions_new_contributors.yml │ ├── how_is_contributions_summary.yml │ └── how_is_contributions_summary_2.yml ├── inq.gemspec ├── lib ├── inq.rb └── inq │ ├── cacheable.rb │ ├── cli.rb │ ├── config.rb │ ├── constants.rb │ ├── date_time_helpers.rb │ ├── exe.rb │ ├── frontmatter.rb │ ├── report.rb │ ├── report_collection.rb │ ├── sources.rb │ ├── sources │ ├── ci │ │ ├── appveyor.rb │ │ └── travis.rb │ ├── github.rb │ ├── github │ │ ├── contributions.rb │ │ ├── issue_fetcher.rb │ │ ├── issues.rb │ │ └── pulls.rb │ └── github_helpers.rb │ ├── template.rb │ ├── templates │ ├── contributions_partial.html │ ├── issues_or_pulls_partial.html │ ├── new_contributors_partial.html │ ├── report.html │ └── report_partial.html │ ├── text.rb │ └── version.rb └── spec ├── capture_warnings.rb ├── data ├── fake │ └── issues.json ├── how-is-date-interval-example-repository-report.html ├── how-is-date-interval-example-repository-report.json ├── how-is-example-empty-repository-report.html ├── how-is-example-repository-report.html ├── how-is-example-repository-report.json ├── how_is.yml ├── how_is │ ├── cli_spec │ │ ├── example_report.json │ │ ├── how_is.yml │ │ └── output │ │ │ └── .gitkeep │ └── fetcher_spec │ │ └── fetcher_call.json ├── how_is_spec │ └── generate_report--generates-a-correct-JSON-report.json ├── issues.json └── pulls.json ├── inq ├── builds_spec.rb ├── cacheable_spec.rb ├── cli_spec.rb ├── config_spec.rb ├── contributions_spec.rb ├── github_helpers_spec.rb └── integration.rb ├── inq_spec.rb ├── rspec_helper.rb ├── spec_helper.rb └── vcr_helper.rb /.cirrus.yml: -------------------------------------------------------------------------------- 1 | # Use an expired token that was used when fixtures were generated. 2 | env: 3 | INQ_GITHUB_USERNAME: "duckinator" 4 | INQ_GITHUB_TOKEN: "9182777ff3c006795193a570cdac326b64459dc9" 5 | INQ_USE_ENV: "true" 6 | 7 | task: 8 | name: "CI Success" 9 | container: {image: "busybox"} 10 | depends_on: 11 | - Lint 12 | - Linux 13 | - macOS 14 | - FreeBSD 15 | #- Windows 16 | 17 | Lint_task: 18 | container: 19 | image: ruby:2.7-alpine 20 | install_script: 21 | - apk add --no-cache git build-base 22 | - gem install bundler 23 | - bundle install 24 | script: 25 | - ruby --version 26 | - bundle exec rubocop 27 | 28 | Linux_task: 29 | container: 30 | matrix: 31 | - image: ruby:2.7-alpine 32 | install_script: 33 | - apk add --no-cache git build-base 34 | - gem install bundler 35 | - bundle install 36 | script: 37 | - ruby --version 38 | - bundle exec rake spec 39 | 40 | macOS_task: 41 | osx_instance: 42 | image: mojave-base 43 | env: 44 | matrix: 45 | - RUBY_VERSION: 2.7 46 | PATH: "/usr/local/opt/ruby@${RUBY_VERSION}/bin:$HOME/.gem/ruby/${RUBY_VERSION}.0/bin:$PATH" 47 | install_script: 48 | - "brew install ruby@${RUBY_VERSION}" 49 | - gem install bundler --user-install 50 | - bundle install 51 | script: 52 | - ruby --version 53 | - bundle exec rake spec 54 | 55 | # NOTE: The `ruby` package on FreeBSD 12.1 is currently Ruby 2.6. 56 | FreeBSD_task: 57 | freebsd_instance: 58 | image_family: freebsd-13-0 59 | env: 60 | RUBY_VERSION: 2.7 61 | install_script: 62 | - RB=`echo $RUBY_VERSION | tr -d '.'` 63 | - pkg install -y ruby ruby${RB}-gems rubygem-rake git 64 | - gem install bundler 65 | - bundle install 66 | script: 67 | - ruby --version 68 | - bundle exec rake spec 69 | 70 | #Windows_task: 71 | # env: 72 | # matrix: 73 | # - RUBY_VERSION: 2.5.3.101 74 | # - RUBY_VERSION: 2.6.3.1 75 | # - RUBY_VERSION: 2.7.0.1 76 | # windows_container: 77 | # os_version: 2019 78 | # image: cirrusci/windowsservercore:2019 79 | # install_script: 80 | # - choco install -y ruby --version %RUBY_VERSION% 81 | # - refreshenv 82 | # - gem update --system 83 | # - gem install bundler 84 | # - bundle install 85 | # script: 86 | # - refreshenv 87 | # - bundle exec rake spec 88 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | user=duckinator 2 | project=inq 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /Gemfile.lock 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | report.json 11 | report.html 12 | !lib/inq/templates/report.html 13 | *.dat 14 | *.png 15 | Thumbs.db 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisplayCopNames: true 3 | DisplayStyleGuide: true 4 | TargetRubyVersion: 2.4 5 | Exclude: 6 | - 'inq.gemspec' 7 | - 'bin/*' 8 | - '**/*~' 9 | - 'spec/capture_warnings.rb' 10 | - 'spec/**/*.rb' # FIXME: Don't exclude this! 11 | - 'lib/inq/report_collection.rb' # FIXME: Don't exclude this! 12 | - 'lib/inq/text.rb' # FIXME: Don't exclude this! 13 | 14 | 15 | # ===== BEGIN TEMPORARY DISABLED THINGS === 16 | # TODO: Go through these in detail and enable/configure any we want. 17 | 18 | Style/RescueStandardError: # FIXME: Don't disable this. 19 | Enabled: false 20 | 21 | Style/SafeNavigation: 22 | Enabled: false 23 | 24 | Naming/MemoizedInstanceVariableName: # FIXME: Don't disable this. 25 | Enabled: false 26 | 27 | Style/AccessModifierDeclarations: # ??? wtf? 28 | Enabled: false 29 | 30 | Layout/AlignArguments: # I Really thought this was already enabled. 31 | Enabled: false 32 | 33 | Style/MutableConstant: # FIXME: Don't disable this. 34 | Enabled: false 35 | 36 | Naming/HeredocDelimiterNaming: # FIXME: Don't disable this. 37 | Enabled: false 38 | 39 | Style/EmptyLambdaParameter: # FIXME: Don't disable this. 40 | Enabled: false 41 | 42 | Naming/UncommunicativeMethodParamName: # FIXME: Probably don't disable this. 43 | Enabled: false 44 | 45 | # ===== END TEMPORARY DISABLED THINGS === 46 | 47 | 48 | 49 | # Exceptions should inherit from StandardError. 50 | # (RuboCop default is to inherit from RuntimeError.) 51 | Lint/InheritException: 52 | EnforcedStyle: standard_error 53 | 54 | Metrics/AbcSize: 55 | Max: 17 56 | 57 | Metrics/BlockLength: 58 | Exclude: 59 | - 'spec/**/*_spec.rb' 60 | 61 | 62 | # Getting this back to the default of 100 would be nice, 63 | # but the few cases that exceed it don't seem overly concerning. 64 | Metrics/ClassLength: 65 | Max: 130 66 | 67 | # Still try for 80, but we'll allow 110 because there's a not-insignificant 68 | # number of cases where we have long lines. 69 | # 70 | # It may be worth revisiting this in the future and refactoring those lines. 71 | Metrics/LineLength: 72 | Max: 120 73 | AllowHeredoc: true 74 | 75 | # Too short methods lead to extraction of single-use methods, which can make 76 | # the code easier to read (by naming things), but can also clutter the class 77 | Metrics/MethodLength: 78 | Max: 25 79 | 80 | Style/Alias: 81 | EnforcedStyle: prefer_alias_method 82 | 83 | # Most readable form. 84 | Layout/AlignHash: 85 | EnforcedHashRocketStyle: table 86 | EnforcedColonStyle: table 87 | # Disable because it wound up conflicting with a lot of things like: 88 | # foo('bar', 89 | # baz: 'asdf', 90 | # beep: 'boop') 91 | # 92 | # I suspect these'll disappear when overarching architectural issues are 93 | # addressed. 94 | Enabled: false 95 | 96 | Layout/AlignParameters: 97 | # See Style/AlignedHash. 98 | Enabled: false 99 | 100 | # This codebase may be English, but some English words contain diacritics. 101 | Style/AsciiComments: 102 | Enabled: false 103 | 104 | # Despite the fact that some English words contain diacritics, we want all 105 | # method names to be writable by people who don't have an easy way to type 106 | # words with diacritics. 107 | Naming/AsciiIdentifiers: 108 | Enabled: true 109 | 110 | # { ... } for multi-line blocks is okay, follow Weirichs rule instead: 111 | # https://web.archive.org/web/20140221124509/http://onestepback.org/index.cgi/Tech/Ruby/BraceVsDoEnd.rdoc 112 | Style/BlockDelimiters: 113 | Enabled: false 114 | 115 | # There's more nuance around this than RuboCop seems capable of. 116 | Style/BracesAroundHashParameters: 117 | Enabled: false 118 | 119 | Style/ConditionalAssignment: 120 | Enabled: false 121 | 122 | # Don't force use of Time or Date; DateTime is okay. 123 | Style/DateTime: 124 | Enabled: false 125 | 126 | # Unicode is good, mkay? 127 | Style/Encoding: 128 | Enabled: true 129 | 130 | # Force Unix line endings. 131 | Layout/EndOfLine: 132 | Enabled: true 133 | EnforcedStyle: lf 134 | 135 | Layout/EmptyLineAfterGuardClause: 136 | Enabled: false 137 | 138 | # A lot of the approaches I use for making things readable makes this angry. 139 | # E.g., formatting multiple consecutive assignments so that the equal signs 140 | # and values line up. 141 | # 142 | # foobar = 'blah' 143 | # baz = 'asdf' 144 | # beep = 'boop' 145 | Layout/ExtraSpacing: 146 | Enabled: false 147 | 148 | # # bad 149 | # 150 | # format('%s', greeting: 'Hello') 151 | # format('%s', 'Hello') 152 | # 153 | # # good 154 | # 155 | # format('%{greeting}', greeting: 'Hello') 156 | Style/FormatStringToken: 157 | EnforcedStyle: template 158 | 159 | # Freeze string literals to future-proof the code. 160 | Style/FrozenStringLiteralComment: 161 | Enabled: true 162 | EnforcedStyle: always 163 | 164 | # Mixing hash styles just looks silly. 165 | # http://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Style/HashSyntax 166 | Style/HashSyntax: 167 | EnforcedStyle: no_mixed_keys 168 | 169 | Layout/IndentFirstHashElement: 170 | Enabled: true 171 | EnforcedStyle: consistent 172 | 173 | Layout/IndentFirstArrayElement: 174 | Enabled: true 175 | EnforcedStyle: consistent 176 | 177 | # I deplore assignments in conditions and never want them in any codebase 178 | # I have direct control over. 179 | Style/ParenthesesAroundCondition: 180 | AllowSafeAssignment: false 181 | Lint/AssignmentInCondition: 182 | AllowSafeAssignment: false 183 | 184 | # Use [] for `%`-literal delimiters (e.g. for %q[]) that RuboCop doesn't define 185 | # anything for. (E.g., %q[].) 186 | # 187 | # The reason I prefer [] instead of () is that most of the time I use 188 | # `%`-literals is inside of function calls, and using () makes it blend in too 189 | # much. 190 | Style/PercentLiteralDelimiters: 191 | Enabled: true 192 | PreferredDelimiters: 193 | default: "[]" 194 | '%w': '[]' 195 | '%W': '[]' 196 | 197 | # `has_key?` and `has_value?` are clearer than `key?` and `value?`. 198 | Style/PreferredHashMethods: 199 | Enabled: true 200 | EnforcedStyle: verbose 201 | 202 | # do / end blocks should be used for side effects, 203 | # methods that run a block for side effects and have 204 | # a useful return value are rare, assign the return 205 | # value to a local variable for those cases. 206 | Style/MethodCalledOnDoEndBlock: 207 | Enabled: true 208 | 209 | # Indent method calls relative to the receiver, e.g.: 210 | # foo \ 211 | # .bar \ 212 | # .baz \ 213 | # .asdf 214 | Layout/MultilineMethodCallIndentation: 215 | EnforcedStyle: indented_relative_to_receiver 216 | 217 | # Indenting the chained dots beneath each other is not supported by this cop, 218 | # see https://github.com/bbatsov/rubocop/issues/1633 219 | Layout/MultilineOperationIndentation: 220 | Enabled: false 221 | 222 | # {'foo' => 'bar'} not { 'foo' => 'bar' } 223 | Layout/SpaceInsideHashLiteralBraces: 224 | Enabled: true 225 | EnforcedStyle: no_space 226 | 227 | # I find "foo > 0" more readable than "foo.positive?" personally. 228 | Style/NumericPredicate: 229 | Enabled: false 230 | 231 | # https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Style/RegexpLiteral 232 | Style/RegexpLiteral: 233 | Enabled: false 234 | 235 | # Use double quotes everywhere by default. 236 | Style/StringLiterals: 237 | EnforcedStyle: double_quotes 238 | 239 | # Prefer [:foo, :bar] over %i[foo bar]. 240 | Style/SymbolArray: 241 | Enabled: true 242 | EnforcedStyle: brackets 243 | 244 | # Prefer ["foo", "bar"] over %w[foo bar]. 245 | Style/WordArray: 246 | Enabled: true 247 | EnforcedStyle: brackets 248 | 249 | # Require parentheses around complex ternary conditions. 250 | Style/TernaryParentheses: 251 | Enabled: true 252 | EnforcedStyle: require_parentheses_when_complex 253 | 254 | # Require a comma after the last item in an array or hash if each item is on 255 | # its own line. 256 | Style/TrailingCommaInArrayLiteral: 257 | EnforcedStyleForMultiline: comma 258 | 259 | Style/TrailingCommaInHashLiteral: 260 | EnforcedStyleForMultiline: comma 261 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at me@duckie.co. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Want to contribute to Inq? This document has the information you need 4 | to make sure the process goes smoothly. 5 | 6 | By contributing to this project, you agree to follow our 7 | [Code of Conduct](https://github.com/duckinator/inq/blob/master/CODE_OF_CONDUCT.md). 8 | 9 | ## Guidelines 10 | 11 | 2. New features should be paired with tests. 12 | 3. Ensure your code follows our general code style. 13 | * Run `bundle exec rubocop` to check for any violations. 14 | 4. Don't modify the CHANGELOG or version number. 15 | 5. If you need feedback or assistance, feel free to join us on Slack 16 | (http://slack.bundler.io/) or open an issue on GitHub 17 | (https://github.com/duckinator/inq). 18 | 19 | ## Getting started 20 | 21 | ``` 22 | $ bundle install 23 | $ bundle exec rake test 24 | ``` 25 | 26 | Then, to run inq commands, simply run `bundle exec inq `. 27 | 28 | ## Issues 29 | 30 | Inq uses the GitHub issue tracker for tracking bugs and feature 31 | requests. 32 | 33 | For more details, including information on the various labels we use, see 34 | [ISSUES.md](https://github.com/duckinator/inq/blob/master/ISSUES.md). 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Inq only supports Ruby versions under "normal maintenance". 6 | # This number should be updated when a Ruby version goes into security 7 | # maintenance. 8 | # 9 | # Ruby maintenance info: https://www.ruby-lang.org/en/downloads/branches/ 10 | # 11 | # NOTE: Update inq.gemspec when this is updated! 12 | ruby "~> 3.3" 13 | 14 | # Specify your gem's dependencies in inq.gemspec 15 | gemspec 16 | -------------------------------------------------------------------------------- /ISSUES.md: -------------------------------------------------------------------------------- 1 | # Issue Management 2 | 3 | This document explains what labels on a given issue mean, or to help you 4 | choose labels for new/updated issues. 5 | 6 | We use an issue tracker to organize and prioritize work by allowing users 7 | to report bugs, request features or documentation, and start discussions. 8 | 9 | --- 10 | 11 | ## high priority 12 | 13 | An issue that should be resolved as soon as possible. 14 | 15 | ## in progress 16 | 17 | An issue which is currently being worked on. This should be added when 18 | someone begins work on an issue. 19 | 20 | ## ready 21 | 22 | An issue which is ready to be worked on. This should be removed when 23 | someone begins work on an issue. 24 | 25 | ## size: large 26 | 27 | An issue which is expected to take a large amount of effort to resolve. 28 | 29 | ## size: small 30 | 31 | An issue which is expected to be relatively simple to resolve. 32 | 33 | ## type: bug report 34 | 35 | A bug report. 36 | 37 | ## type: documentation 38 | 39 | A request for additional or updated documentation. 40 | 41 | ## type: feature request 42 | 43 | A request for a new feature to be added. 44 | 45 | ## type: question/discussion 46 | 47 | A question or discussion. Issues with this label are likely to not have 48 | any immediately-actionable tasks. 49 | 50 | When any tasks do arise from an issue with this label, more specific 51 | issues will likely be opened to replace it. 52 | 53 | ## type: refactoring 54 | 55 | A request for a particular section of code to be refactored. 56 | 57 | Including concrete suggestions for smaller areas will likely get these 58 | issues resolved more quickly. 59 | 60 | ## type: tests 61 | 62 | A request for new or improved tests addressing specific situations. 63 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2019 Ellen Marie Dash and Inq contributors 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inq is on an indefinite development hiatus 2 | 3 | See [issue #292](https://github.com/duckinator/inq/issues/292) for details. 4 | 5 | --- 6 | 7 | [![Travis](https://img.shields.io/travis/duckinator/inq.svg)](https://travis-ci.org/duckinator/inq) 8 | [![Code Climate](https://img.shields.io/codeclimate/github/duckinator/inq.svg)](https://codeclimate.com/github/duckinator/inq) 9 | [![Gem](https://img.shields.io/gem/v/inq.svg)](https://rubygems.org/gems/inq) 10 | [Documentation](https://how-is.github.io) 11 | 12 | # Inq 13 | 14 | Inq is tool for generating summaries of the health of a codebase hosted on GitHub. It uses information available from issues and pull requests to provide an overview of a repository and highlight problem areas of the codebase. 15 | 16 | Reports can be generated retroactively. 17 | 18 | If you want to contribute or discuss inq, you can [join Bundler's slack](http://slack.bundler.io/) and join the #inq channel. 19 | 20 | ## Installation 21 | 22 | $ gem install inq 23 | 24 | ## Configuration 25 | 26 | To avoid errors due to hitting rate limits, Inq requires a Personal 27 | Access Token for GitHub. 28 | 29 | ### Acquiring A Personal Access Token 30 | 31 | To acquire a personal access token: 32 | 33 | 1. Go to: https://github.com/settings/tokens/new 34 | 2. For `Token description`, enter `inq CLI client`. 35 | 3. Scroll to the bottom of the page. 36 | 4. Click `Generate token`. This will take you to a new page. 37 | 5. Save the token somewhere. **You can't access it again.** 38 | 39 | **NOTE:** Inq _only_ needs read access. 40 | 41 | #### Using The Token 42 | 43 | Create a file in `~/.config/inq/config.yml`: 44 | 45 | ``` 46 | sources/github: 47 | username: 48 | token: 49 | ``` 50 | 51 | Make sure to replace `` with the actual token, and `` 52 | with your GitHub username. 53 | 54 | ## Usage 55 | 56 | ### Command Line 57 | 58 | $ inq --repository REPOSITORY --date DATE [--output OUTPUT_FILENAME] 59 | # OUTPUT_FILENAME defaults to ./report.html. 60 | 61 | or 62 | 63 | $ inq --date DATE --config CONFIG_FILENAME 64 | 65 | #### Example \#1 66 | 67 | $ inq --repository rubygems/rubygems --date 2016-12-01 --output report-2016-12-01.html 68 | 69 | The above command creates a HTML file containing the report for the state of 70 | the rubygems/rubygems repository, for November 01 2016 to 71 | December 01 2016, and saves it as `./report-2016-12-01.html`. 72 | 73 | #### Example \#2 74 | 75 | $ inq --date 2016-12-01 --config some-config.yml 76 | 77 | This generates the report(s) specified in the config file, for the period 78 | from November 01 2016 to December 01 2016, and saves them in the 79 | locations specified in the config file. 80 | 81 | #### Screenshot 82 | 83 | ![image](https://user-images.githubusercontent.com/211/55504154-89284180-5650-11e9-9a13-e03e9b83c749.png) 84 | 85 | ## Development 86 | 87 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Run `bundle exec inq` to use the gem in this directory, ignoring other installed copies of this gem. 88 | 89 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 90 | 91 | ## Contributing 92 | 93 | Bug reports and pull requests are welcome on GitHub at https://github.com/duckinator/inq. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 94 | 95 | ## License 96 | 97 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 98 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | require "timecop" 6 | require "inq" 7 | 8 | RSpec::Core::RakeTask.new(:spec) do |t| 9 | t.ruby_opts = "-w -r./spec/capture_warnings.rb" 10 | end 11 | 12 | task :default => :spec 13 | 14 | task :generate_changelog do 15 | sh "github_changelog_generator" 16 | end 17 | 18 | task :future_changelog do 19 | sh "github_changelog_generator --future-release v#{Inq::VERSION}" 20 | end 21 | 22 | # Helper functions used later in the Rakefile. 23 | class HelperFunctions 24 | def self.freeze_time(&_block) 25 | date = DateTime.parse("2016-11-01").new_offset(0) 26 | Timecop.freeze(date) do 27 | yield 28 | end 29 | end 30 | 31 | def self.generate_report(repository, format) 32 | require "./spec/vcr_helper.rb" 33 | 34 | freeze_time do 35 | report = nil 36 | 37 | options = { 38 | repository: repository, 39 | format: format, 40 | } 41 | 42 | cassette = repository.tr("/", "-") 43 | VCR.use_cassette(cassette) do 44 | report = Inq.generate_report(**options) 45 | end 46 | 47 | filename = "#{cassette}-report.#{format}" 48 | path = File.expand_path("spec/data/#{filename}", __dir__) 49 | File.open(path, "w") do |f| 50 | f.puts report 51 | # HACK: Trailing newline is missing, otherwise. 52 | f.puts if format == "html" 53 | end 54 | end 55 | end 56 | end 57 | 58 | namespace :generate_reports do 59 | desc "Generate example HTML reports." 60 | task :html do 61 | [ 62 | "how-is/example-repository", 63 | "how-is/example-empty-repository", 64 | ].each do |repo| 65 | HelperFunctions.generate_report(repo, "html") 66 | end 67 | end 68 | 69 | desc "Generate example JSON reports." 70 | task :json do 71 | HelperFunctions.generate_report("how-is/example-repository", "json") 72 | end 73 | 74 | task :all => [:html, :json] 75 | end 76 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "inq" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/prerelease-generate-changelog: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is a fucking tragedy. 4 | 5 | PROJECT="inq" 6 | 7 | SCRIPT_DIR="$(dirname $(readlink -f $0))" # Directory this script is in. 8 | cd "$SCRIPT_DIR/.." 9 | 10 | LAST_RELEASE="$(git tag --list | cut -d 'v' -f 2 | sort -n | tail -n 1)" 11 | NEXT_RELEASE=$(cat lib/$PROJECT/version.rb | grep '^\s*VERSION\s*=\s*' | cut -d '"' -f 2) 12 | 13 | if [ -z "$LAST_RELEASE" ]; then 14 | echo "error: Could not find latest release using \`git tag --list\`" 15 | exit 1 16 | fi 17 | 18 | if [ -z "$NEXT_RELEASE" ]; then 19 | echo "error: No version number found in $(pwd)lib/$PROEJCT/version.rb." 20 | exit 1 21 | fi 22 | 23 | if [ "$LAST_RELEASE" == "$NEXT_RELEASE" ]; then 24 | echo "error: last release (\"$LAST_RELEASE\") and next release (\"$NEXT_RELEASE\") are equivalent." 25 | exit 1 26 | fi 27 | 28 | bundle exec github_changelog_generator --future-release "v$NEXT_RELEASE" 29 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = ["CI Success"] 2 | 3 | delete_merged_branches = true 4 | -------------------------------------------------------------------------------- /dashboard-mockups/mockup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | How is 5 | 46 | 47 | 48 |
49 |
50 |

How is bundler/bundler?

51 |

< Repository List

52 |
53 |
54 |

How is rubygems/rubygems?

55 |

< Repository List

56 |
57 |
58 |

How is

59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
RepositoryTotal IssuesTotal PRsAverage Issue AgeAverage PR AgeTest Coverage
bundler/bundler 70 | 178 30 4 months and 2 weeks2 months and 2 weeks75%
rubygems/rubygems132 36 1 year and 4 months11 months and 1 weekunknown
84 |
85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /exe/inq: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | require "inq/exe" 6 | 7 | Inq::Exe.run(ARGV) 8 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/how_is_contributions_changed_files.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.github.com/repos/how-is/example-repository/commits?since=2017-08-01&until=2017-09-01 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept: 11 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 12 | Accept-Charset: 13 | - utf-8 14 | User-Agent: 15 | - Github API Ruby Gem 0.18.2 16 | Authorization: 17 | - "" 18 | Accept-Encoding: 19 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - GitHub.com 27 | Date: 28 | - Mon, 06 Apr 2020 14:08:52 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Status: 34 | - 200 OK 35 | X-Ratelimit-Limit: 36 | - '5000' 37 | X-Ratelimit-Remaining: 38 | - '4929' 39 | X-Ratelimit-Reset: 40 | - '1586182135' 41 | Cache-Control: 42 | - private, max-age=60, s-maxage=60 43 | Vary: 44 | - Accept, Authorization, Cookie, X-GitHub-OTP 45 | - Accept-Encoding, Accept, X-Requested-With 46 | Etag: 47 | - W/"93a0b2c0149212449cde92bd25d2f1d3" 48 | Last-Modified: 49 | - Sat, 05 Aug 2017 20:39:01 GMT 50 | X-Oauth-Scopes: 51 | - '' 52 | X-Accepted-Oauth-Scopes: 53 | - '' 54 | X-Github-Media-Type: 55 | - github.v3; format=json 56 | Access-Control-Expose-Headers: 57 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 58 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 59 | X-GitHub-Media-Type, Deprecation, Sunset 60 | Access-Control-Allow-Origin: 61 | - "*" 62 | Strict-Transport-Security: 63 | - max-age=31536000; includeSubdomains; preload 64 | X-Frame-Options: 65 | - deny 66 | X-Content-Type-Options: 67 | - nosniff 68 | X-Xss-Protection: 69 | - 1; mode=block 70 | Referrer-Policy: 71 | - origin-when-cross-origin, strict-origin-when-cross-origin 72 | Content-Security-Policy: 73 | - default-src 'none' 74 | X-Github-Request-Id: 75 | - F7A8:4FFF:034E:06FB:5E8B37F4 76 | body: 77 | encoding: ASCII-8BIT 78 | string: '[{"sha":"40c01ab6ebec6cbd8ad9e521a732f941c169e557","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6NDBjMDFhYjZlYmVjNmNiZDhhZDllNTIxYTczMmY5NDFjMTY5ZTU1Nw==","commit":{"author":{"name":"Ellen 79 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"committer":{"name":"Ellen 80 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"message":"meep","tree":{"sha":"6911e0637822f44b83f04f47821adab56fdbc0b9","url":"https://api.github.com/repos/how-is/example-repository/git/trees/6911e0637822f44b83f04f47821adab56fdbc0b9"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","html_url":"https://github.com/how-is/example-repository/commit/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557/comments","author":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[{"sha":"3794aa1c4b76623748faf280abe5760b76823162","url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162"}]},{"sha":"3794aa1c4b76623748faf280abe5760b76823162","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6Mzc5NGFhMWM0Yjc2NjIzNzQ4ZmFmMjgwYWJlNTc2MGI3NjgyMzE2Mg==","commit":{"author":{"name":"fake 81 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"committer":{"name":"fake 82 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"message":"test 83 | commit","tree":{"sha":"8286e548e330cfe01efcf7189f4df1fa53e777a7","url":"https://api.github.com/repos/how-is/example-repository/git/trees/8286e548e330cfe01efcf7189f4df1fa53e777a7"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/3794aa1c4b76623748faf280abe5760b76823162","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162/comments","author":null,"committer":null,"parents":[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd"}]}]' 84 | http_version: 85 | recorded_at: Mon, 06 Apr 2020 14:08:52 GMT 86 | - request: 87 | method: get 88 | uri: https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557 89 | body: 90 | encoding: US-ASCII 91 | string: '' 92 | headers: 93 | Accept: 94 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 95 | Accept-Charset: 96 | - utf-8 97 | User-Agent: 98 | - Github API Ruby Gem 0.18.2 99 | Authorization: 100 | - "" 101 | Accept-Encoding: 102 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 103 | response: 104 | status: 105 | code: 200 106 | message: OK 107 | headers: 108 | Server: 109 | - GitHub.com 110 | Date: 111 | - Mon, 06 Apr 2020 14:08:52 GMT 112 | Content-Type: 113 | - application/json; charset=utf-8 114 | Transfer-Encoding: 115 | - chunked 116 | Status: 117 | - 200 OK 118 | X-Ratelimit-Limit: 119 | - '5000' 120 | X-Ratelimit-Remaining: 121 | - '4928' 122 | X-Ratelimit-Reset: 123 | - '1586182135' 124 | Cache-Control: 125 | - private, max-age=60, s-maxage=60 126 | Vary: 127 | - Accept, Authorization, Cookie, X-GitHub-OTP 128 | - Accept-Encoding, Accept, X-Requested-With 129 | Etag: 130 | - W/"9f3e6f86f734d64306d0207b1e0ac1d1" 131 | Last-Modified: 132 | - Sat, 05 Aug 2017 20:39:01 GMT 133 | X-Oauth-Scopes: 134 | - '' 135 | X-Accepted-Oauth-Scopes: 136 | - '' 137 | X-Github-Media-Type: 138 | - github.v3; format=json 139 | Access-Control-Expose-Headers: 140 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 141 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 142 | X-GitHub-Media-Type, Deprecation, Sunset 143 | Access-Control-Allow-Origin: 144 | - "*" 145 | Strict-Transport-Security: 146 | - max-age=31536000; includeSubdomains; preload 147 | X-Frame-Options: 148 | - deny 149 | X-Content-Type-Options: 150 | - nosniff 151 | X-Xss-Protection: 152 | - 1; mode=block 153 | Referrer-Policy: 154 | - origin-when-cross-origin, strict-origin-when-cross-origin 155 | Content-Security-Policy: 156 | - default-src 'none' 157 | X-Github-Request-Id: 158 | - 63DE:621E:044D:0838:5E8B37F4 159 | body: 160 | encoding: ASCII-8BIT 161 | string: '{"sha":"40c01ab6ebec6cbd8ad9e521a732f941c169e557","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6NDBjMDFhYjZlYmVjNmNiZDhhZDllNTIxYTczMmY5NDFjMTY5ZTU1Nw==","commit":{"author":{"name":"Ellen 162 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"committer":{"name":"Ellen 163 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"message":"meep","tree":{"sha":"6911e0637822f44b83f04f47821adab56fdbc0b9","url":"https://api.github.com/repos/how-is/example-repository/git/trees/6911e0637822f44b83f04f47821adab56fdbc0b9"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","html_url":"https://github.com/how-is/example-repository/commit/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557/comments","author":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[{"sha":"3794aa1c4b76623748faf280abe5760b76823162","url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162"}],"stats":{"total":2,"additions":1,"deletions":1},"files":[{"sha":"1573b3cd673938ed5f9ee8c41952c77b456ffab9","filename":"README.md","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/how-is/example-repository/blob/40c01ab6ebec6cbd8ad9e521a732f941c169e557/README.md","raw_url":"https://github.com/how-is/example-repository/raw/40c01ab6ebec6cbd8ad9e521a732f941c169e557/README.md","contents_url":"https://api.github.com/repos/how-is/example-repository/contents/README.md?ref=40c01ab6ebec6cbd8ad9e521a732f941c169e557","patch":"@@ 164 | -1,3 +1,3 @@\n # example-repository\n \n-Example repository for testing how_is.\n+An 165 | example repository for testing how_is."}]}' 166 | http_version: 167 | recorded_at: Mon, 06 Apr 2020 14:08:52 GMT 168 | - request: 169 | method: get 170 | uri: https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162 171 | body: 172 | encoding: US-ASCII 173 | string: '' 174 | headers: 175 | Accept: 176 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 177 | Accept-Charset: 178 | - utf-8 179 | User-Agent: 180 | - Github API Ruby Gem 0.18.2 181 | Authorization: 182 | - "" 183 | Accept-Encoding: 184 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 185 | response: 186 | status: 187 | code: 200 188 | message: OK 189 | headers: 190 | Server: 191 | - GitHub.com 192 | Date: 193 | - Mon, 06 Apr 2020 14:08:53 GMT 194 | Content-Type: 195 | - application/json; charset=utf-8 196 | Transfer-Encoding: 197 | - chunked 198 | Status: 199 | - 200 OK 200 | X-Ratelimit-Limit: 201 | - '5000' 202 | X-Ratelimit-Remaining: 203 | - '4927' 204 | X-Ratelimit-Reset: 205 | - '1586182136' 206 | Cache-Control: 207 | - private, max-age=60, s-maxage=60 208 | Vary: 209 | - Accept, Authorization, Cookie, X-GitHub-OTP 210 | - Accept-Encoding, Accept, X-Requested-With 211 | Etag: 212 | - W/"b18989d3979bf43901d0512b448820f2" 213 | Last-Modified: 214 | - Sat, 05 Aug 2017 20:23:10 GMT 215 | X-Oauth-Scopes: 216 | - '' 217 | X-Accepted-Oauth-Scopes: 218 | - '' 219 | X-Github-Media-Type: 220 | - github.v3; format=json 221 | Access-Control-Expose-Headers: 222 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 223 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 224 | X-GitHub-Media-Type, Deprecation, Sunset 225 | Access-Control-Allow-Origin: 226 | - "*" 227 | Strict-Transport-Security: 228 | - max-age=31536000; includeSubdomains; preload 229 | X-Frame-Options: 230 | - deny 231 | X-Content-Type-Options: 232 | - nosniff 233 | X-Xss-Protection: 234 | - 1; mode=block 235 | Referrer-Policy: 236 | - origin-when-cross-origin, strict-origin-when-cross-origin 237 | Content-Security-Policy: 238 | - default-src 'none' 239 | X-Github-Request-Id: 240 | - E6B0:4E10:03DE:080B:5E8B37F5 241 | body: 242 | encoding: ASCII-8BIT 243 | string: '{"sha":"3794aa1c4b76623748faf280abe5760b76823162","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6Mzc5NGFhMWM0Yjc2NjIzNzQ4ZmFmMjgwYWJlNTc2MGI3NjgyMzE2Mg==","commit":{"author":{"name":"fake 244 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"committer":{"name":"fake 245 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"message":"test 246 | commit","tree":{"sha":"8286e548e330cfe01efcf7189f4df1fa53e777a7","url":"https://api.github.com/repos/how-is/example-repository/git/trees/8286e548e330cfe01efcf7189f4df1fa53e777a7"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/3794aa1c4b76623748faf280abe5760b76823162","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162/comments","author":null,"committer":null,"parents":[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd"}],"stats":{"total":1,"additions":1,"deletions":0},"files":[{"sha":"a69ca6962d2a86d7c5a041338938e0b4fe6bf515","filename":"README.md","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/how-is/example-repository/blob/3794aa1c4b76623748faf280abe5760b76823162/README.md","raw_url":"https://github.com/how-is/example-repository/raw/3794aa1c4b76623748faf280abe5760b76823162/README.md","contents_url":"https://api.github.com/repos/how-is/example-repository/contents/README.md?ref=3794aa1c4b76623748faf280abe5760b76823162","patch":"@@ 247 | -1,2 +1,3 @@\n # example-repository\n+\n Example repository for testing how_is."}]}' 248 | http_version: 249 | recorded_at: Mon, 06 Apr 2020 14:08:53 GMT 250 | recorded_with: VCR 4.0.0 251 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/how_is_contributions_changes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.github.com/repos/how-is/example-repository/commits?since=2017-08-01&until=2017-09-01 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept: 11 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 12 | Accept-Charset: 13 | - utf-8 14 | User-Agent: 15 | - Github API Ruby Gem 0.18.2 16 | Authorization: 17 | - "" 18 | Accept-Encoding: 19 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - GitHub.com 27 | Date: 28 | - Mon, 06 Apr 2020 14:08:53 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Status: 34 | - 200 OK 35 | X-Ratelimit-Limit: 36 | - '5000' 37 | X-Ratelimit-Remaining: 38 | - '4926' 39 | X-Ratelimit-Reset: 40 | - '1586182135' 41 | Cache-Control: 42 | - private, max-age=60, s-maxage=60 43 | Vary: 44 | - Accept, Authorization, Cookie, X-GitHub-OTP 45 | - Accept-Encoding, Accept, X-Requested-With 46 | Etag: 47 | - W/"93a0b2c0149212449cde92bd25d2f1d3" 48 | Last-Modified: 49 | - Sat, 05 Aug 2017 20:39:01 GMT 50 | X-Oauth-Scopes: 51 | - '' 52 | X-Accepted-Oauth-Scopes: 53 | - '' 54 | X-Github-Media-Type: 55 | - github.v3; format=json 56 | Access-Control-Expose-Headers: 57 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 58 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 59 | X-GitHub-Media-Type, Deprecation, Sunset 60 | Access-Control-Allow-Origin: 61 | - "*" 62 | Strict-Transport-Security: 63 | - max-age=31536000; includeSubdomains; preload 64 | X-Frame-Options: 65 | - deny 66 | X-Content-Type-Options: 67 | - nosniff 68 | X-Xss-Protection: 69 | - 1; mode=block 70 | Referrer-Policy: 71 | - origin-when-cross-origin, strict-origin-when-cross-origin 72 | Content-Security-Policy: 73 | - default-src 'none' 74 | X-Github-Request-Id: 75 | - 5F4C:535C:06A8:0BB7:5E8B37F5 76 | body: 77 | encoding: ASCII-8BIT 78 | string: '[{"sha":"40c01ab6ebec6cbd8ad9e521a732f941c169e557","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6NDBjMDFhYjZlYmVjNmNiZDhhZDllNTIxYTczMmY5NDFjMTY5ZTU1Nw==","commit":{"author":{"name":"Ellen 79 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"committer":{"name":"Ellen 80 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"message":"meep","tree":{"sha":"6911e0637822f44b83f04f47821adab56fdbc0b9","url":"https://api.github.com/repos/how-is/example-repository/git/trees/6911e0637822f44b83f04f47821adab56fdbc0b9"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","html_url":"https://github.com/how-is/example-repository/commit/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557/comments","author":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[{"sha":"3794aa1c4b76623748faf280abe5760b76823162","url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162"}]},{"sha":"3794aa1c4b76623748faf280abe5760b76823162","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6Mzc5NGFhMWM0Yjc2NjIzNzQ4ZmFmMjgwYWJlNTc2MGI3NjgyMzE2Mg==","commit":{"author":{"name":"fake 81 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"committer":{"name":"fake 82 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"message":"test 83 | commit","tree":{"sha":"8286e548e330cfe01efcf7189f4df1fa53e777a7","url":"https://api.github.com/repos/how-is/example-repository/git/trees/8286e548e330cfe01efcf7189f4df1fa53e777a7"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/3794aa1c4b76623748faf280abe5760b76823162","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162/comments","author":null,"committer":null,"parents":[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd"}]}]' 84 | http_version: 85 | recorded_at: Mon, 06 Apr 2020 14:08:53 GMT 86 | - request: 87 | method: get 88 | uri: https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557 89 | body: 90 | encoding: US-ASCII 91 | string: '' 92 | headers: 93 | Accept: 94 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 95 | Accept-Charset: 96 | - utf-8 97 | User-Agent: 98 | - Github API Ruby Gem 0.18.2 99 | Authorization: 100 | - "" 101 | Accept-Encoding: 102 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 103 | response: 104 | status: 105 | code: 200 106 | message: OK 107 | headers: 108 | Server: 109 | - GitHub.com 110 | Date: 111 | - Mon, 06 Apr 2020 14:08:53 GMT 112 | Content-Type: 113 | - application/json; charset=utf-8 114 | Transfer-Encoding: 115 | - chunked 116 | Status: 117 | - 200 OK 118 | X-Ratelimit-Limit: 119 | - '5000' 120 | X-Ratelimit-Remaining: 121 | - '4925' 122 | X-Ratelimit-Reset: 123 | - '1586182135' 124 | Cache-Control: 125 | - private, max-age=60, s-maxage=60 126 | Vary: 127 | - Accept, Authorization, Cookie, X-GitHub-OTP 128 | - Accept-Encoding, Accept, X-Requested-With 129 | Etag: 130 | - W/"9f3e6f86f734d64306d0207b1e0ac1d1" 131 | Last-Modified: 132 | - Sat, 05 Aug 2017 20:39:01 GMT 133 | X-Oauth-Scopes: 134 | - '' 135 | X-Accepted-Oauth-Scopes: 136 | - '' 137 | X-Github-Media-Type: 138 | - github.v3; format=json 139 | Access-Control-Expose-Headers: 140 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 141 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 142 | X-GitHub-Media-Type, Deprecation, Sunset 143 | Access-Control-Allow-Origin: 144 | - "*" 145 | Strict-Transport-Security: 146 | - max-age=31536000; includeSubdomains; preload 147 | X-Frame-Options: 148 | - deny 149 | X-Content-Type-Options: 150 | - nosniff 151 | X-Xss-Protection: 152 | - 1; mode=block 153 | Referrer-Policy: 154 | - origin-when-cross-origin, strict-origin-when-cross-origin 155 | Content-Security-Policy: 156 | - default-src 'none' 157 | X-Github-Request-Id: 158 | - DDB6:46FC:033B:07AC:5E8B37F5 159 | body: 160 | encoding: ASCII-8BIT 161 | string: '{"sha":"40c01ab6ebec6cbd8ad9e521a732f941c169e557","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6NDBjMDFhYjZlYmVjNmNiZDhhZDllNTIxYTczMmY5NDFjMTY5ZTU1Nw==","commit":{"author":{"name":"Ellen 162 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"committer":{"name":"Ellen 163 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"message":"meep","tree":{"sha":"6911e0637822f44b83f04f47821adab56fdbc0b9","url":"https://api.github.com/repos/how-is/example-repository/git/trees/6911e0637822f44b83f04f47821adab56fdbc0b9"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","html_url":"https://github.com/how-is/example-repository/commit/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557/comments","author":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[{"sha":"3794aa1c4b76623748faf280abe5760b76823162","url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162"}],"stats":{"total":2,"additions":1,"deletions":1},"files":[{"sha":"1573b3cd673938ed5f9ee8c41952c77b456ffab9","filename":"README.md","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/how-is/example-repository/blob/40c01ab6ebec6cbd8ad9e521a732f941c169e557/README.md","raw_url":"https://github.com/how-is/example-repository/raw/40c01ab6ebec6cbd8ad9e521a732f941c169e557/README.md","contents_url":"https://api.github.com/repos/how-is/example-repository/contents/README.md?ref=40c01ab6ebec6cbd8ad9e521a732f941c169e557","patch":"@@ 164 | -1,3 +1,3 @@\n # example-repository\n \n-Example repository for testing how_is.\n+An 165 | example repository for testing how_is."}]}' 166 | http_version: 167 | recorded_at: Mon, 06 Apr 2020 14:08:53 GMT 168 | - request: 169 | method: get 170 | uri: https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162 171 | body: 172 | encoding: US-ASCII 173 | string: '' 174 | headers: 175 | Accept: 176 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 177 | Accept-Charset: 178 | - utf-8 179 | User-Agent: 180 | - Github API Ruby Gem 0.18.2 181 | Authorization: 182 | - "" 183 | Accept-Encoding: 184 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 185 | response: 186 | status: 187 | code: 200 188 | message: OK 189 | headers: 190 | Server: 191 | - GitHub.com 192 | Date: 193 | - Mon, 06 Apr 2020 14:08:53 GMT 194 | Content-Type: 195 | - application/json; charset=utf-8 196 | Transfer-Encoding: 197 | - chunked 198 | Status: 199 | - 200 OK 200 | X-Ratelimit-Limit: 201 | - '5000' 202 | X-Ratelimit-Remaining: 203 | - '4924' 204 | X-Ratelimit-Reset: 205 | - '1586182135' 206 | Cache-Control: 207 | - private, max-age=60, s-maxage=60 208 | Vary: 209 | - Accept, Authorization, Cookie, X-GitHub-OTP 210 | - Accept-Encoding, Accept, X-Requested-With 211 | Etag: 212 | - W/"b18989d3979bf43901d0512b448820f2" 213 | Last-Modified: 214 | - Sat, 05 Aug 2017 20:23:10 GMT 215 | X-Oauth-Scopes: 216 | - '' 217 | X-Accepted-Oauth-Scopes: 218 | - '' 219 | X-Github-Media-Type: 220 | - github.v3; format=json 221 | Access-Control-Expose-Headers: 222 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 223 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 224 | X-GitHub-Media-Type, Deprecation, Sunset 225 | Access-Control-Allow-Origin: 226 | - "*" 227 | Strict-Transport-Security: 228 | - max-age=31536000; includeSubdomains; preload 229 | X-Frame-Options: 230 | - deny 231 | X-Content-Type-Options: 232 | - nosniff 233 | X-Xss-Protection: 234 | - 1; mode=block 235 | Referrer-Policy: 236 | - origin-when-cross-origin, strict-origin-when-cross-origin 237 | Content-Security-Policy: 238 | - default-src 'none' 239 | X-Github-Request-Id: 240 | - D5A2:4E10:0423:0892:5E8B37F5 241 | body: 242 | encoding: ASCII-8BIT 243 | string: '{"sha":"3794aa1c4b76623748faf280abe5760b76823162","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6Mzc5NGFhMWM0Yjc2NjIzNzQ4ZmFmMjgwYWJlNTc2MGI3NjgyMzE2Mg==","commit":{"author":{"name":"fake 244 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"committer":{"name":"fake 245 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"message":"test 246 | commit","tree":{"sha":"8286e548e330cfe01efcf7189f4df1fa53e777a7","url":"https://api.github.com/repos/how-is/example-repository/git/trees/8286e548e330cfe01efcf7189f4df1fa53e777a7"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/3794aa1c4b76623748faf280abe5760b76823162","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162/comments","author":null,"committer":null,"parents":[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd"}],"stats":{"total":1,"additions":1,"deletions":0},"files":[{"sha":"a69ca6962d2a86d7c5a041338938e0b4fe6bf515","filename":"README.md","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/how-is/example-repository/blob/3794aa1c4b76623748faf280abe5760b76823162/README.md","raw_url":"https://github.com/how-is/example-repository/raw/3794aa1c4b76623748faf280abe5760b76823162/README.md","contents_url":"https://api.github.com/repos/how-is/example-repository/contents/README.md?ref=3794aa1c4b76623748faf280abe5760b76823162","patch":"@@ 247 | -1,2 +1,3 @@\n # example-repository\n+\n Example repository for testing how_is."}]}' 248 | http_version: 249 | recorded_at: Mon, 06 Apr 2020 14:08:53 GMT 250 | recorded_with: VCR 4.0.0 251 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/how_is_contributions_commits.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.github.com/repos/how-is/example-repository/commits?since=2017-08-01&until=2017-09-01 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept: 11 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 12 | Accept-Charset: 13 | - utf-8 14 | User-Agent: 15 | - Github API Ruby Gem 0.18.2 16 | Authorization: 17 | - "" 18 | Accept-Encoding: 19 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - GitHub.com 27 | Date: 28 | - Mon, 06 Apr 2020 14:08:50 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Status: 34 | - 200 OK 35 | X-Ratelimit-Limit: 36 | - '5000' 37 | X-Ratelimit-Remaining: 38 | - '4944' 39 | X-Ratelimit-Reset: 40 | - '1586182136' 41 | Cache-Control: 42 | - private, max-age=60, s-maxage=60 43 | Vary: 44 | - Accept, Authorization, Cookie, X-GitHub-OTP 45 | - Accept-Encoding, Accept, X-Requested-With 46 | Etag: 47 | - W/"93a0b2c0149212449cde92bd25d2f1d3" 48 | Last-Modified: 49 | - Sat, 05 Aug 2017 20:39:01 GMT 50 | X-Oauth-Scopes: 51 | - '' 52 | X-Accepted-Oauth-Scopes: 53 | - '' 54 | X-Github-Media-Type: 55 | - github.v3; format=json 56 | Access-Control-Expose-Headers: 57 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 58 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 59 | X-GitHub-Media-Type, Deprecation, Sunset 60 | Access-Control-Allow-Origin: 61 | - "*" 62 | Strict-Transport-Security: 63 | - max-age=31536000; includeSubdomains; preload 64 | X-Frame-Options: 65 | - deny 66 | X-Content-Type-Options: 67 | - nosniff 68 | X-Xss-Protection: 69 | - 1; mode=block 70 | Referrer-Policy: 71 | - origin-when-cross-origin, strict-origin-when-cross-origin 72 | Content-Security-Policy: 73 | - default-src 'none' 74 | X-Github-Request-Id: 75 | - 25A0:0B0D:035E:06E8:5E8B37F2 76 | body: 77 | encoding: ASCII-8BIT 78 | string: '[{"sha":"40c01ab6ebec6cbd8ad9e521a732f941c169e557","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6NDBjMDFhYjZlYmVjNmNiZDhhZDllNTIxYTczMmY5NDFjMTY5ZTU1Nw==","commit":{"author":{"name":"Ellen 79 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"committer":{"name":"Ellen 80 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"message":"meep","tree":{"sha":"6911e0637822f44b83f04f47821adab56fdbc0b9","url":"https://api.github.com/repos/how-is/example-repository/git/trees/6911e0637822f44b83f04f47821adab56fdbc0b9"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","html_url":"https://github.com/how-is/example-repository/commit/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557/comments","author":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[{"sha":"3794aa1c4b76623748faf280abe5760b76823162","url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162"}]},{"sha":"3794aa1c4b76623748faf280abe5760b76823162","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6Mzc5NGFhMWM0Yjc2NjIzNzQ4ZmFmMjgwYWJlNTc2MGI3NjgyMzE2Mg==","commit":{"author":{"name":"fake 81 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"committer":{"name":"fake 82 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"message":"test 83 | commit","tree":{"sha":"8286e548e330cfe01efcf7189f4df1fa53e777a7","url":"https://api.github.com/repos/how-is/example-repository/git/trees/8286e548e330cfe01efcf7189f4df1fa53e777a7"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/3794aa1c4b76623748faf280abe5760b76823162","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162/comments","author":null,"committer":null,"parents":[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd"}]}]' 84 | http_version: 85 | recorded_at: Mon, 06 Apr 2020 14:08:50 GMT 86 | - request: 87 | method: get 88 | uri: https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557 89 | body: 90 | encoding: US-ASCII 91 | string: '' 92 | headers: 93 | Accept: 94 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 95 | Accept-Charset: 96 | - utf-8 97 | User-Agent: 98 | - Github API Ruby Gem 0.18.2 99 | Authorization: 100 | - "" 101 | Accept-Encoding: 102 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 103 | response: 104 | status: 105 | code: 200 106 | message: OK 107 | headers: 108 | Server: 109 | - GitHub.com 110 | Date: 111 | - Mon, 06 Apr 2020 14:08:50 GMT 112 | Content-Type: 113 | - application/json; charset=utf-8 114 | Transfer-Encoding: 115 | - chunked 116 | Status: 117 | - 200 OK 118 | X-Ratelimit-Limit: 119 | - '5000' 120 | X-Ratelimit-Remaining: 121 | - '4943' 122 | X-Ratelimit-Reset: 123 | - '1586182136' 124 | Cache-Control: 125 | - private, max-age=60, s-maxage=60 126 | Vary: 127 | - Accept, Authorization, Cookie, X-GitHub-OTP 128 | - Accept-Encoding, Accept, X-Requested-With 129 | Etag: 130 | - W/"9f3e6f86f734d64306d0207b1e0ac1d1" 131 | Last-Modified: 132 | - Sat, 05 Aug 2017 20:39:01 GMT 133 | X-Oauth-Scopes: 134 | - '' 135 | X-Accepted-Oauth-Scopes: 136 | - '' 137 | X-Github-Media-Type: 138 | - github.v3; format=json 139 | Access-Control-Expose-Headers: 140 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 141 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 142 | X-GitHub-Media-Type, Deprecation, Sunset 143 | Access-Control-Allow-Origin: 144 | - "*" 145 | Strict-Transport-Security: 146 | - max-age=31536000; includeSubdomains; preload 147 | X-Frame-Options: 148 | - deny 149 | X-Content-Type-Options: 150 | - nosniff 151 | X-Xss-Protection: 152 | - 1; mode=block 153 | Referrer-Policy: 154 | - origin-when-cross-origin, strict-origin-when-cross-origin 155 | Content-Security-Policy: 156 | - default-src 'none' 157 | X-Github-Request-Id: 158 | - AB9A:4D57:008D:01D4:5E8B37F2 159 | body: 160 | encoding: ASCII-8BIT 161 | string: '{"sha":"40c01ab6ebec6cbd8ad9e521a732f941c169e557","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6NDBjMDFhYjZlYmVjNmNiZDhhZDllNTIxYTczMmY5NDFjMTY5ZTU1Nw==","commit":{"author":{"name":"Ellen 162 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"committer":{"name":"Ellen 163 | Marie Dash","email":"me@duckie.co","date":"2017-08-05T20:39:01Z"},"message":"meep","tree":{"sha":"6911e0637822f44b83f04f47821adab56fdbc0b9","url":"https://api.github.com/repos/how-is/example-repository/git/trees/6911e0637822f44b83f04f47821adab56fdbc0b9"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557","html_url":"https://github.com/how-is/example-repository/commit/40c01ab6ebec6cbd8ad9e521a732f941c169e557","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/40c01ab6ebec6cbd8ad9e521a732f941c169e557/comments","author":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"committer":{"login":"duckinator","id":39698,"node_id":"MDQ6VXNlcjM5Njk4","avatar_url":"https://avatars3.githubusercontent.com/u/39698?v=4","gravatar_id":"","url":"https://api.github.com/users/duckinator","html_url":"https://github.com/duckinator","followers_url":"https://api.github.com/users/duckinator/followers","following_url":"https://api.github.com/users/duckinator/following{/other_user}","gists_url":"https://api.github.com/users/duckinator/gists{/gist_id}","starred_url":"https://api.github.com/users/duckinator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/duckinator/subscriptions","organizations_url":"https://api.github.com/users/duckinator/orgs","repos_url":"https://api.github.com/users/duckinator/repos","events_url":"https://api.github.com/users/duckinator/events{/privacy}","received_events_url":"https://api.github.com/users/duckinator/received_events","type":"User","site_admin":false},"parents":[{"sha":"3794aa1c4b76623748faf280abe5760b76823162","url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162"}],"stats":{"total":2,"additions":1,"deletions":1},"files":[{"sha":"1573b3cd673938ed5f9ee8c41952c77b456ffab9","filename":"README.md","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/how-is/example-repository/blob/40c01ab6ebec6cbd8ad9e521a732f941c169e557/README.md","raw_url":"https://github.com/how-is/example-repository/raw/40c01ab6ebec6cbd8ad9e521a732f941c169e557/README.md","contents_url":"https://api.github.com/repos/how-is/example-repository/contents/README.md?ref=40c01ab6ebec6cbd8ad9e521a732f941c169e557","patch":"@@ 164 | -1,3 +1,3 @@\n # example-repository\n \n-Example repository for testing how_is.\n+An 165 | example repository for testing how_is."}]}' 166 | http_version: 167 | recorded_at: Mon, 06 Apr 2020 14:08:50 GMT 168 | - request: 169 | method: get 170 | uri: https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162 171 | body: 172 | encoding: US-ASCII 173 | string: '' 174 | headers: 175 | Accept: 176 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 177 | Accept-Charset: 178 | - utf-8 179 | User-Agent: 180 | - Github API Ruby Gem 0.18.2 181 | Authorization: 182 | - "" 183 | Accept-Encoding: 184 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 185 | response: 186 | status: 187 | code: 200 188 | message: OK 189 | headers: 190 | Server: 191 | - GitHub.com 192 | Date: 193 | - Mon, 06 Apr 2020 14:08:50 GMT 194 | Content-Type: 195 | - application/json; charset=utf-8 196 | Transfer-Encoding: 197 | - chunked 198 | Status: 199 | - 200 OK 200 | X-Ratelimit-Limit: 201 | - '5000' 202 | X-Ratelimit-Remaining: 203 | - '4942' 204 | X-Ratelimit-Reset: 205 | - '1586182135' 206 | Cache-Control: 207 | - private, max-age=60, s-maxage=60 208 | Vary: 209 | - Accept, Authorization, Cookie, X-GitHub-OTP 210 | - Accept-Encoding, Accept, X-Requested-With 211 | Etag: 212 | - W/"b18989d3979bf43901d0512b448820f2" 213 | Last-Modified: 214 | - Sat, 05 Aug 2017 20:23:10 GMT 215 | X-Oauth-Scopes: 216 | - '' 217 | X-Accepted-Oauth-Scopes: 218 | - '' 219 | X-Github-Media-Type: 220 | - github.v3; format=json 221 | Access-Control-Expose-Headers: 222 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 223 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 224 | X-GitHub-Media-Type, Deprecation, Sunset 225 | Access-Control-Allow-Origin: 226 | - "*" 227 | Strict-Transport-Security: 228 | - max-age=31536000; includeSubdomains; preload 229 | X-Frame-Options: 230 | - deny 231 | X-Content-Type-Options: 232 | - nosniff 233 | X-Xss-Protection: 234 | - 1; mode=block 235 | Referrer-Policy: 236 | - origin-when-cross-origin, strict-origin-when-cross-origin 237 | Content-Security-Policy: 238 | - default-src 'none' 239 | X-Github-Request-Id: 240 | - 41B8:4613:021E:05A5:5E8B37F2 241 | body: 242 | encoding: ASCII-8BIT 243 | string: '{"sha":"3794aa1c4b76623748faf280abe5760b76823162","node_id":"MDY6Q29tbWl0NjUxMTQ0NDk6Mzc5NGFhMWM0Yjc2NjIzNzQ4ZmFmMjgwYWJlNTc2MGI3NjgyMzE2Mg==","commit":{"author":{"name":"fake 244 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"committer":{"name":"fake 245 | author","email":"fake@duckinator.net","date":"2017-08-05T20:23:10Z"},"message":"test 246 | commit","tree":{"sha":"8286e548e330cfe01efcf7189f4df1fa53e777a7","url":"https://api.github.com/repos/how-is/example-repository/git/trees/8286e548e330cfe01efcf7189f4df1fa53e777a7"},"url":"https://api.github.com/repos/how-is/example-repository/git/commits/3794aa1c4b76623748faf280abe5760b76823162","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162","html_url":"https://github.com/how-is/example-repository/commit/3794aa1c4b76623748faf280abe5760b76823162","comments_url":"https://api.github.com/repos/how-is/example-repository/commits/3794aa1c4b76623748faf280abe5760b76823162/comments","author":null,"committer":null,"parents":[{"sha":"9e29405efa433529b86722542b8fb4b34dfd9edd","url":"https://api.github.com/repos/how-is/example-repository/commits/9e29405efa433529b86722542b8fb4b34dfd9edd","html_url":"https://github.com/how-is/example-repository/commit/9e29405efa433529b86722542b8fb4b34dfd9edd"}],"stats":{"total":1,"additions":1,"deletions":0},"files":[{"sha":"a69ca6962d2a86d7c5a041338938e0b4fe6bf515","filename":"README.md","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/how-is/example-repository/blob/3794aa1c4b76623748faf280abe5760b76823162/README.md","raw_url":"https://github.com/how-is/example-repository/raw/3794aa1c4b76623748faf280abe5760b76823162/README.md","contents_url":"https://api.github.com/repos/how-is/example-repository/contents/README.md?ref=3794aa1c4b76623748faf280abe5760b76823162","patch":"@@ 247 | -1,2 +1,3 @@\n # example-repository\n+\n Example repository for testing how_is."}]}' 248 | http_version: 249 | recorded_at: Mon, 06 Apr 2020 14:08:50 GMT 250 | recorded_with: VCR 4.0.0 251 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/how_is_contributions_compare_url.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.github.com/repos/how-is/example-repository 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept: 11 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 12 | Accept-Charset: 13 | - utf-8 14 | User-Agent: 15 | - Github API Ruby Gem 0.18.2 16 | Authorization: 17 | - "" 18 | Accept-Encoding: 19 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - GitHub.com 27 | Date: 28 | - Mon, 06 Apr 2020 14:08:50 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Status: 34 | - 200 OK 35 | X-Ratelimit-Limit: 36 | - '5000' 37 | X-Ratelimit-Remaining: 38 | - '4945' 39 | X-Ratelimit-Reset: 40 | - '1586182135' 41 | Cache-Control: 42 | - private, max-age=60, s-maxage=60 43 | Vary: 44 | - Accept, Authorization, Cookie, X-GitHub-OTP 45 | - Accept-Encoding, Accept, X-Requested-With 46 | Etag: 47 | - W/"618fb9796f8af8fe6e1607021658b758" 48 | Last-Modified: 49 | - Sun, 07 Aug 2016 03:52:53 GMT 50 | X-Oauth-Scopes: 51 | - '' 52 | X-Accepted-Oauth-Scopes: 53 | - repo 54 | X-Github-Media-Type: 55 | - github.v3; format=json 56 | Access-Control-Expose-Headers: 57 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 58 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 59 | X-GitHub-Media-Type, Deprecation, Sunset 60 | Access-Control-Allow-Origin: 61 | - "*" 62 | Strict-Transport-Security: 63 | - max-age=31536000; includeSubdomains; preload 64 | X-Frame-Options: 65 | - deny 66 | X-Content-Type-Options: 67 | - nosniff 68 | X-Xss-Protection: 69 | - 1; mode=block 70 | Referrer-Policy: 71 | - origin-when-cross-origin, strict-origin-when-cross-origin 72 | Content-Security-Policy: 73 | - default-src 'none' 74 | X-Github-Request-Id: 75 | - 2FBA:0B24:0108:0274:5E8B37F1 76 | body: 77 | encoding: ASCII-8BIT 78 | string: '{"id":65114449,"node_id":"MDEwOlJlcG9zaXRvcnk2NTExNDQ0OQ==","name":"example-repository","full_name":"how-is/example-repository","private":false,"owner":{"login":"how-is","id":20577802,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIwNTc3ODAy","avatar_url":"https://avatars0.githubusercontent.com/u/20577802?v=4","gravatar_id":"","url":"https://api.github.com/users/how-is","html_url":"https://github.com/how-is","followers_url":"https://api.github.com/users/how-is/followers","following_url":"https://api.github.com/users/how-is/following{/other_user}","gists_url":"https://api.github.com/users/how-is/gists{/gist_id}","starred_url":"https://api.github.com/users/how-is/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/how-is/subscriptions","organizations_url":"https://api.github.com/users/how-is/orgs","repos_url":"https://api.github.com/users/how-is/repos","events_url":"https://api.github.com/users/how-is/events{/privacy}","received_events_url":"https://api.github.com/users/how-is/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/how-is/example-repository","description":"Example 79 | repository for testing how_is.","fork":false,"url":"https://api.github.com/repos/how-is/example-repository","forks_url":"https://api.github.com/repos/how-is/example-repository/forks","keys_url":"https://api.github.com/repos/how-is/example-repository/keys{/key_id}","collaborators_url":"https://api.github.com/repos/how-is/example-repository/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/how-is/example-repository/teams","hooks_url":"https://api.github.com/repos/how-is/example-repository/hooks","issue_events_url":"https://api.github.com/repos/how-is/example-repository/issues/events{/number}","events_url":"https://api.github.com/repos/how-is/example-repository/events","assignees_url":"https://api.github.com/repos/how-is/example-repository/assignees{/user}","branches_url":"https://api.github.com/repos/how-is/example-repository/branches{/branch}","tags_url":"https://api.github.com/repos/how-is/example-repository/tags","blobs_url":"https://api.github.com/repos/how-is/example-repository/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/how-is/example-repository/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/how-is/example-repository/git/refs{/sha}","trees_url":"https://api.github.com/repos/how-is/example-repository/git/trees{/sha}","statuses_url":"https://api.github.com/repos/how-is/example-repository/statuses/{sha}","languages_url":"https://api.github.com/repos/how-is/example-repository/languages","stargazers_url":"https://api.github.com/repos/how-is/example-repository/stargazers","contributors_url":"https://api.github.com/repos/how-is/example-repository/contributors","subscribers_url":"https://api.github.com/repos/how-is/example-repository/subscribers","subscription_url":"https://api.github.com/repos/how-is/example-repository/subscription","commits_url":"https://api.github.com/repos/how-is/example-repository/commits{/sha}","git_commits_url":"https://api.github.com/repos/how-is/example-repository/git/commits{/sha}","comments_url":"https://api.github.com/repos/how-is/example-repository/comments{/number}","issue_comment_url":"https://api.github.com/repos/how-is/example-repository/issues/comments{/number}","contents_url":"https://api.github.com/repos/how-is/example-repository/contents/{+path}","compare_url":"https://api.github.com/repos/how-is/example-repository/compare/{base}...{head}","merges_url":"https://api.github.com/repos/how-is/example-repository/merges","archive_url":"https://api.github.com/repos/how-is/example-repository/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/how-is/example-repository/downloads","issues_url":"https://api.github.com/repos/how-is/example-repository/issues{/number}","pulls_url":"https://api.github.com/repos/how-is/example-repository/pulls{/number}","milestones_url":"https://api.github.com/repos/how-is/example-repository/milestones{/number}","notifications_url":"https://api.github.com/repos/how-is/example-repository/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/how-is/example-repository/labels{/name}","releases_url":"https://api.github.com/repos/how-is/example-repository/releases{/id}","deployments_url":"https://api.github.com/repos/how-is/example-repository/deployments","created_at":"2016-08-07T03:52:53Z","updated_at":"2016-08-07T03:52:53Z","pushed_at":"2017-08-05T20:39:30Z","git_url":"git://github.com/how-is/example-repository.git","ssh_url":"git@github.com:how-is/example-repository.git","clone_url":"https://github.com/how-is/example-repository.git","svn_url":"https://github.com/how-is/example-repository","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","organization":{"login":"how-is","id":20577802,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIwNTc3ODAy","avatar_url":"https://avatars0.githubusercontent.com/u/20577802?v=4","gravatar_id":"","url":"https://api.github.com/users/how-is","html_url":"https://github.com/how-is","followers_url":"https://api.github.com/users/how-is/followers","following_url":"https://api.github.com/users/how-is/following{/other_user}","gists_url":"https://api.github.com/users/how-is/gists{/gist_id}","starred_url":"https://api.github.com/users/how-is/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/how-is/subscriptions","organizations_url":"https://api.github.com/users/how-is/orgs","repos_url":"https://api.github.com/users/how-is/repos","events_url":"https://api.github.com/users/how-is/events{/privacy}","received_events_url":"https://api.github.com/users/how-is/received_events","type":"Organization","site_admin":false},"network_count":0,"subscribers_count":1}' 80 | http_version: 81 | recorded_at: Mon, 06 Apr 2020 14:08:50 GMT 82 | recorded_with: VCR 4.0.0 83 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/how_is_contributions_default_branch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.github.com/repos/how-is/example-repository 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept: 11 | - application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1 12 | Accept-Charset: 13 | - utf-8 14 | User-Agent: 15 | - Github API Ruby Gem 0.18.2 16 | Authorization: 17 | - "" 18 | Accept-Encoding: 19 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Server: 26 | - GitHub.com 27 | Date: 28 | - Mon, 06 Apr 2020 14:08:52 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Transfer-Encoding: 32 | - chunked 33 | Status: 34 | - 200 OK 35 | X-Ratelimit-Limit: 36 | - '5000' 37 | X-Ratelimit-Remaining: 38 | - '4930' 39 | X-Ratelimit-Reset: 40 | - '1586182135' 41 | Cache-Control: 42 | - private, max-age=60, s-maxage=60 43 | Vary: 44 | - Accept, Authorization, Cookie, X-GitHub-OTP 45 | - Accept-Encoding, Accept, X-Requested-With 46 | Etag: 47 | - W/"618fb9796f8af8fe6e1607021658b758" 48 | Last-Modified: 49 | - Sun, 07 Aug 2016 03:52:53 GMT 50 | X-Oauth-Scopes: 51 | - '' 52 | X-Accepted-Oauth-Scopes: 53 | - repo 54 | X-Github-Media-Type: 55 | - github.v3; format=json 56 | Access-Control-Expose-Headers: 57 | - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, 58 | X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, 59 | X-GitHub-Media-Type, Deprecation, Sunset 60 | Access-Control-Allow-Origin: 61 | - "*" 62 | Strict-Transport-Security: 63 | - max-age=31536000; includeSubdomains; preload 64 | X-Frame-Options: 65 | - deny 66 | X-Content-Type-Options: 67 | - nosniff 68 | X-Xss-Protection: 69 | - 1; mode=block 70 | Referrer-Policy: 71 | - origin-when-cross-origin, strict-origin-when-cross-origin 72 | Content-Security-Policy: 73 | - default-src 'none' 74 | X-Github-Request-Id: 75 | - 1979:46FC:02DC:06EF:5E8B37F4 76 | body: 77 | encoding: ASCII-8BIT 78 | string: '{"id":65114449,"node_id":"MDEwOlJlcG9zaXRvcnk2NTExNDQ0OQ==","name":"example-repository","full_name":"how-is/example-repository","private":false,"owner":{"login":"how-is","id":20577802,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIwNTc3ODAy","avatar_url":"https://avatars0.githubusercontent.com/u/20577802?v=4","gravatar_id":"","url":"https://api.github.com/users/how-is","html_url":"https://github.com/how-is","followers_url":"https://api.github.com/users/how-is/followers","following_url":"https://api.github.com/users/how-is/following{/other_user}","gists_url":"https://api.github.com/users/how-is/gists{/gist_id}","starred_url":"https://api.github.com/users/how-is/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/how-is/subscriptions","organizations_url":"https://api.github.com/users/how-is/orgs","repos_url":"https://api.github.com/users/how-is/repos","events_url":"https://api.github.com/users/how-is/events{/privacy}","received_events_url":"https://api.github.com/users/how-is/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/how-is/example-repository","description":"Example 79 | repository for testing how_is.","fork":false,"url":"https://api.github.com/repos/how-is/example-repository","forks_url":"https://api.github.com/repos/how-is/example-repository/forks","keys_url":"https://api.github.com/repos/how-is/example-repository/keys{/key_id}","collaborators_url":"https://api.github.com/repos/how-is/example-repository/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/how-is/example-repository/teams","hooks_url":"https://api.github.com/repos/how-is/example-repository/hooks","issue_events_url":"https://api.github.com/repos/how-is/example-repository/issues/events{/number}","events_url":"https://api.github.com/repos/how-is/example-repository/events","assignees_url":"https://api.github.com/repos/how-is/example-repository/assignees{/user}","branches_url":"https://api.github.com/repos/how-is/example-repository/branches{/branch}","tags_url":"https://api.github.com/repos/how-is/example-repository/tags","blobs_url":"https://api.github.com/repos/how-is/example-repository/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/how-is/example-repository/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/how-is/example-repository/git/refs{/sha}","trees_url":"https://api.github.com/repos/how-is/example-repository/git/trees{/sha}","statuses_url":"https://api.github.com/repos/how-is/example-repository/statuses/{sha}","languages_url":"https://api.github.com/repos/how-is/example-repository/languages","stargazers_url":"https://api.github.com/repos/how-is/example-repository/stargazers","contributors_url":"https://api.github.com/repos/how-is/example-repository/contributors","subscribers_url":"https://api.github.com/repos/how-is/example-repository/subscribers","subscription_url":"https://api.github.com/repos/how-is/example-repository/subscription","commits_url":"https://api.github.com/repos/how-is/example-repository/commits{/sha}","git_commits_url":"https://api.github.com/repos/how-is/example-repository/git/commits{/sha}","comments_url":"https://api.github.com/repos/how-is/example-repository/comments{/number}","issue_comment_url":"https://api.github.com/repos/how-is/example-repository/issues/comments{/number}","contents_url":"https://api.github.com/repos/how-is/example-repository/contents/{+path}","compare_url":"https://api.github.com/repos/how-is/example-repository/compare/{base}...{head}","merges_url":"https://api.github.com/repos/how-is/example-repository/merges","archive_url":"https://api.github.com/repos/how-is/example-repository/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/how-is/example-repository/downloads","issues_url":"https://api.github.com/repos/how-is/example-repository/issues{/number}","pulls_url":"https://api.github.com/repos/how-is/example-repository/pulls{/number}","milestones_url":"https://api.github.com/repos/how-is/example-repository/milestones{/number}","notifications_url":"https://api.github.com/repos/how-is/example-repository/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/how-is/example-repository/labels{/name}","releases_url":"https://api.github.com/repos/how-is/example-repository/releases{/id}","deployments_url":"https://api.github.com/repos/how-is/example-repository/deployments","created_at":"2016-08-07T03:52:53Z","updated_at":"2016-08-07T03:52:53Z","pushed_at":"2017-08-05T20:39:30Z","git_url":"git://github.com/how-is/example-repository.git","ssh_url":"git@github.com:how-is/example-repository.git","clone_url":"https://github.com/how-is/example-repository.git","svn_url":"https://github.com/how-is/example-repository","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","organization":{"login":"how-is","id":20577802,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIwNTc3ODAy","avatar_url":"https://avatars0.githubusercontent.com/u/20577802?v=4","gravatar_id":"","url":"https://api.github.com/users/how-is","html_url":"https://github.com/how-is","followers_url":"https://api.github.com/users/how-is/followers","following_url":"https://api.github.com/users/how-is/following{/other_user}","gists_url":"https://api.github.com/users/how-is/gists{/gist_id}","starred_url":"https://api.github.com/users/how-is/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/how-is/subscriptions","organizations_url":"https://api.github.com/users/how-is/orgs","repos_url":"https://api.github.com/users/how-is/repos","events_url":"https://api.github.com/users/how-is/events{/privacy}","received_events_url":"https://api.github.com/users/how-is/received_events","type":"Organization","site_admin":false},"network_count":0,"subscribers_count":1}' 80 | http_version: 81 | recorded_at: Mon, 06 Apr 2020 14:08:52 GMT 82 | recorded_with: VCR 4.0.0 83 | -------------------------------------------------------------------------------- /inq.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'inq/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "inq" 9 | spec.version = Inq::VERSION 10 | spec.authors = ["Ellen Marie Dash"] 11 | spec.email = ["me@duckie.co"] 12 | 13 | spec.summary = %q{Quantify the health of a GitHub repository.} 14 | spec.homepage = "https://github.com/duckinator/inq" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|bin|fixtures)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | # Inq only supports Ruby versions under "normal maintenance". 23 | # This number should be updated when a Ruby version goes into security 24 | # maintenance. 25 | # 26 | # Ruby maintenance info: https://www.ruby-lang.org/en/downloads/branches/ 27 | # 28 | # NOTE: Update Gemfile when this is updated! 29 | spec.required_ruby_version = "~> 3.3" 30 | 31 | spec.add_runtime_dependency "github_api", "= 0.18.2" 32 | spec.add_runtime_dependency "okay", "~> 12.0" 33 | 34 | spec.add_runtime_dependency "json_pure" 35 | 36 | spec.add_development_dependency "bundler", "~> 2.0" 37 | spec.add_development_dependency "rake", "~> 13.0" 38 | spec.add_development_dependency "rspec", "~> 3.9" 39 | spec.add_development_dependency "timecop", "= 0.9.1" 40 | spec.add_development_dependency "vcr", "~> 4.0" 41 | spec.add_development_dependency "webmock" 42 | # Rubocop pulls in C extensions, which we want to avoid in Windows CI. 43 | spec.add_development_dependency "rubocop", "= 0.68.1" unless Gem.win_platform? && ENV["CI"] 44 | spec.add_development_dependency "github_changelog_generator" 45 | end 46 | -------------------------------------------------------------------------------- /lib/inq.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/version" 4 | require "inq/config" 5 | require "inq/report" 6 | require "inq/report_collection" 7 | 8 | ## 9 | # Top-level module for creating a report. 10 | module Inq 11 | def self.default_config(repository) 12 | { 13 | "repository" => repository, 14 | "reports" => { 15 | "html" => { 16 | "directory" => ".", 17 | "frontmatter" => {}, 18 | "filename" => "report.html", 19 | }, 20 | }, 21 | } 22 | end 23 | 24 | def self.new(repository, start_date, end_date = nil, cache_mechanism = nil) 25 | config = 26 | Config.new 27 | .load_defaults 28 | .load(default_config(repository)) 29 | config["cache"] = {"type" => "self", "cache_mechanism" => cache_mechanism} if cache_mechanism 30 | Report.new(config, start_date, end_date) 31 | end 32 | 33 | ## 34 | # Generates a series of report files based on a config Hash. 35 | # 36 | # @param config [ReportCollection] All the information needed to generate 37 | # the reports. 38 | # @param date [String] A string containing the date (YYYY-MM-DD) that the 39 | # report ends on. E.g., for Jan 1-Feb 1 2017, you'd pass 2017-02-01. 40 | def self.from_config(config, start_date, end_date = nil) 41 | raise "Expected config to be Hash, got #{config.class}" unless \ 42 | config.is_a?(Hash) 43 | 44 | ReportCollection.new(config, start_date, end_date) 45 | end 46 | 47 | ## 48 | # Returns a list of possible export formats. 49 | # 50 | # @return [Array] An array of the types of reports you can generate. 51 | def self.supported_formats 52 | ["html", "json"] 53 | end 54 | 55 | ## 56 | # Returns whether or not the specified +format+ is supported. 57 | # 58 | # @param format_name [String] The format in question. 59 | # @return [Boolean] +true+ if HowIs supports the format, +false+ otherwise. 60 | def self.supported_format?(format_name) 61 | supported_formats.include?(format_name) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/inq/cacheable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest" 4 | require "tmpdir" 5 | 6 | module Inq 7 | # Class for use in caching expensive operations 8 | class Cacheable 9 | def initialize(config, start_date, end_date, tmpdir = Dir.mktmpdir) 10 | @config = config 11 | @start_date = start_date 12 | @end_date = end_date 13 | @tmpdir = tmpdir 14 | end 15 | 16 | def cached(key, extra_digest = nil) 17 | cache = @config["cache"] 18 | return yield if cache.nil? 19 | 20 | hash_key = [] 21 | hash_key << Digest::SHA1.hexdigest(extra_digest) if extra_digest 22 | hash_key << Digest::SHA1.hexdigest(@config.to_json) 23 | cache_key = File.join(@start_date, @end_date, key, hash_key.join("-")) 24 | 25 | case cache["type"] 26 | when "marshal" 27 | MarshalCache.cached(cache_key, @tmpdir) { yield } 28 | when "self" 29 | # Can provide your own cache in Inq.new 30 | # e.g. 31 | # cache_mechanism = ->(cache_key, config, block) do 32 | # if cached? 33 | # cached_value 34 | # else 35 | # block.call 36 | # end 37 | # end 38 | # Inq.new("owner/repo", date, cache_mechanism) 39 | cache["cache_mechanism"].call(cache_key, @config, ->() { yield }) 40 | end 41 | end 42 | 43 | # This is only okay on a local system 44 | module MarshalCache 45 | class << self 46 | # rubocop:disable Security/MarshalLoad 47 | def cached(key, tmpdir) 48 | require "fileutils" 49 | 50 | path = File.join(tmpdir, "inq", key) 51 | FileUtils.mkdir_p(File.dirname(path)) 52 | 53 | ret = nil 54 | if File.exist?(path) 55 | File.open(path, "rb") do |f| 56 | ret = Marshal.load(f) 57 | end 58 | ret 59 | else 60 | ret = yield 61 | File.open(path, "wb") do |file| 62 | Marshal.dump(ret, file) 63 | end 64 | end 65 | ret 66 | end 67 | # rubocop:enable Security/MarshalLoad 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/inq/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq" 4 | require "inq/constants" 5 | require "okay/simple_opts" 6 | 7 | module Inq 8 | ## 9 | # Parses command-line arguments for inq. 10 | class CLI 11 | MissingArgument = Class.new(OptionParser::MissingArgument) 12 | AmbiguousArgument = Class.new(OptionParser::AmbiguousArgument) 13 | 14 | REPO_REGEXP = /.+\/.+/ 15 | DATE_REGEXP = /\d\d\d\d-\d\d-\d\d/ 16 | 17 | attr_reader :options, :help_text 18 | 19 | def self.parse(*args) 20 | new.parse(*args) 21 | end 22 | 23 | def initialize 24 | @options = nil 25 | @help_text = nil 26 | end 27 | 28 | # Parses an Array of command-line arguments into an equivalent Hash. 29 | # 30 | # The results of this can be used to control the behavior of the rest 31 | # of the library. 32 | # 33 | # @params argv [Array] An array of command-line arguments, e.g. +ARGV+. 34 | # @return [Hash] A Hash containing data used to control Inq's behavior. 35 | def parse(argv) 36 | parser, options = parse_main(argv) 37 | 38 | # Options that are mutually-exclusive with everything else. 39 | options = {:help => true} if options[:help] 40 | options = {:version => true} if options[:version] 41 | 42 | validate_options!(options) 43 | 44 | @options = options 45 | @help_text = parser.to_s 46 | 47 | self 48 | end 49 | 50 | private 51 | 52 | # parse_main() is as short as can be managed. It's fine as-is. 53 | # rubocop:disable Metrics/MethodLength 54 | 55 | # This does a significant chunk of the work for parse(). 56 | # 57 | # @return [Array] An array containing the +OptionParser+ and the result 58 | # of running it. 59 | def parse_main(argv) 60 | defaults = { 61 | report: Inq::DEFAULT_REPORT_FILE, 62 | } 63 | 64 | opts = Okay::SimpleOpts.new(defaults: defaults) 65 | 66 | opts.banner = <<~EOF 67 | Usage: inq --repository REPOSITORY --date REPORT_DATE [--output REPORT_FILE] 68 | inq --config CONFIG_FILE --date REPORT_DATE 69 | EOF 70 | 71 | opts.separator "\nOptions:" 72 | 73 | opts.simple("--config CONFIG_FILE", 74 | "YAML config file for automated reports.", 75 | :config) 76 | 77 | opts.simple("--no-user-config", 78 | "Don't load user configuration file.", 79 | :no_user_config) 80 | 81 | opts.simple("--env-config", 82 | "Use environment variables for configuration.", 83 | "Read this first: https://inqrb.com/config", 84 | :env_login) 85 | 86 | opts.simple("--repository USER/REPO", REPO_REGEXP, 87 | "Repository to generate a report for.", 88 | :repository) 89 | 90 | opts.simple("--date YYYY-MM-DD", DATE_REGEXP, "Last date of the report.", 91 | :date) 92 | 93 | opts.simple("--start-date YYYY-MM-DD", DATE_REGEXP, "Start date of the report.", 94 | :start_date) 95 | 96 | opts.simple("--end-date YYYY-MM-DD", DATE_REGEXP, "Last date of the report.", 97 | :end_date) 98 | 99 | opts.simple("--output REPORT_FILE", format_regexp, 100 | "Output file for the report.", 101 | "Supported file formats: #{formats}.", 102 | :report) 103 | 104 | opts.simple("--verbose", "Print debug information.", :verbose) 105 | opts.simple("-v", "--version", "Prints version information.", :version) 106 | opts.simple("-h", "--help", "Print help text.", :help) 107 | 108 | [opts, opts.parse(argv)] 109 | end 110 | 111 | # rubocop:enable Metrics/MethodLength 112 | 113 | # Given an +options+ Hash, determine if we got a valid combination of 114 | # options. 115 | # 116 | # 1. Anything with `--help` and `--version` is always valid. 117 | # 2. Otherwise, `--repository` or `--config` is required. 118 | # 3. If `--repository` or `--config` is required, so is `--date`. 119 | # 120 | # @param options [Hash] The result of CLI#parse(). 121 | # @raise [MissingArgument] if we did not get a valid options Hash. 122 | # @raise [AmbiguousArgument] if we give an ambiguous options Hash. 123 | def validate_options!(options) 124 | return if options[:help] || options[:version] 125 | validate_date(options) 126 | raise MissingArgument, "--repository or --config" unless 127 | options[:repository] || options[:config] 128 | end 129 | 130 | # @return [String] A comma-separated list of supported formats. 131 | def formats 132 | Inq.supported_formats.join(", ") 133 | end 134 | 135 | # @return [Regexp] a +Regexp+ object which matches any path ending 136 | # with an extension corresponding to a supported format. 137 | def format_regexp 138 | regexp_parts = Inq.supported_formats.map { |x| Regexp.escape(x) } 139 | 140 | /.+\.(#{regexp_parts.join("|")})/ 141 | end 142 | 143 | # @param options [Hash] The result of CLI#parse(). 144 | # @raise [AmbiguousArgument] if we did not get a valid options Hash. 145 | def validate_date(options) 146 | if options[:date] 147 | raise AmbiguousArgument, "--date, --start-date, --end-date" if options[:start_date] || options[:end_date] 148 | else 149 | raise MissingArgument, "--date, --start-date, --end-date" unless options[:start_date] || options[:end_date] 150 | end 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/inq/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "yaml" 4 | require "inq/text" 5 | 6 | module Inq 7 | HOME_CONFIG = File.join(Dir.home, ".config", "inq", "config.yml") 8 | 9 | # Usage: 10 | # Inq::Config 11 | # .load_site_configs("/path/to/config1.yml", "/path/to/config2.yml") 12 | # .load_file("./repo-config.yml") 13 | # Or: 14 | # Inq::Config 15 | # .load_defaults 16 | # .load_file("./repo-config.yml") 17 | # Or: 18 | # Inq::Config 19 | # .load_defaults 20 | # .load({"repository" => "how-is/example-repository"}) 21 | class Config < Hash 22 | attr_reader :site_configs 23 | 24 | # If the INQ_USE_ENV+ environment variable is set, load config from 25 | # the environment. 26 | # 27 | # Otherwise, load the the default config file. 28 | # 29 | # @return [Hash] A Hash representation of the config. 30 | def load_defaults 31 | if ENV["INQ_USE_ENV"] == "true" 32 | load_env 33 | else 34 | load_site_configs(HOME_CONFIG) 35 | end 36 | end 37 | 38 | def initialize 39 | super() 40 | @site_configs = [] 41 | end 42 | 43 | # Load the config files as specified via +files+. 44 | # 45 | # @param files [Array] The path(s) for config files. 46 | # @return [Config] The config hash. (+self+) 47 | def load_site_configs(*files) 48 | # Allows both: 49 | # load_site_configs('foo', 'bar') 50 | # load_site_configs(['foo', bar']) 51 | # but not: 52 | # load_site_configs(['foo'], 'bar') 53 | files = files[0] if files.length == 1 && files[0].is_a?(Array) 54 | 55 | load_files(*files) 56 | end 57 | 58 | # TODO: See if this can be consolidated with load_site_configs. 59 | def load_files(*file_paths) 60 | files = (site_configs + file_paths).map { |f| Pathname.new(f) } 61 | 62 | # Keep only files that exist. 63 | files.select!(&:file?) 64 | 65 | # Load the YAML files into Hashes. 66 | configs = files.map { |file| YAML.safe_load(file.read) } 67 | 68 | # Apply configs. 69 | load(*configs) 70 | end 71 | 72 | # Take a collection of config hashes and cascade them, meaning values 73 | # in later ones override values in earlier ones. 74 | # 75 | # E.g., this results in +{'a'=>'x', 'c'=>'d'}+: 76 | # load({'a'=>'b'}, {'c'=>'d'}, {'a'=>'x'}) 77 | # 78 | # And this results in +{'a'=>['b', 'c']}+: 79 | # load({'a'=>['b']}, {'a'=>['c']}) 80 | # 81 | # @param [Array] The configuration hashes. 82 | # @return [Config] The final configuration value. 83 | def load(*configs) 84 | configs.each do |config| 85 | config.each do |k, v| 86 | if self[k] && self[k].is_a?(Array) 87 | self[k] += v 88 | else 89 | self[k] = v 90 | end 91 | end 92 | end 93 | 94 | self 95 | end 96 | 97 | # Load config info from environment variables. 98 | # 99 | # Supported environment variables: 100 | # - INQ_GITHUB_TOKEN: a GitHub authentication token. 101 | # - INQ_GITHUB_USERNAME: the GitHub username corresponding to the token. 102 | # 103 | # @return [Config] The resulting configuration. 104 | def load_env 105 | Inq::Text.puts "Using configuration from environment variables." 106 | 107 | gh_token = ENV["INQ_GITHUB_TOKEN"] || ENV["HOWIS_GITHUB_TOKEN"] 108 | gh_username = ENV["INQ_GITHUB_USERNAME"] || ENV["HOWIS_GITHUB_USERNAME"] 109 | 110 | raise "INQ_GITHUB_TOKEN environment variable is not set" \ 111 | unless gh_token 112 | raise "INQ_GITHUB_USERNAME environment variable is not set" \ 113 | unless gh_username 114 | 115 | load({ 116 | "sources/github" => { 117 | "username" => gh_username, 118 | "token" => gh_token, 119 | }, 120 | }) 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/inq/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Inq 4 | # The file name used for a report if one isn't specified. 5 | DEFAULT_REPORT_FILE = "report.html" 6 | 7 | # Used by things making HTTP requests. 8 | USER_AGENT = "inq/#{Inq::VERSION} (https://github.com/duckinator/inq)" 9 | end 10 | -------------------------------------------------------------------------------- /lib/inq/date_time_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/version" 4 | require "date" 5 | 6 | module Inq 7 | ## 8 | # Various helper functions for working with DateTime objects. 9 | module DateTimeHelpers 10 | # Check if +left+ is less than or equal to +right+, where both are string 11 | # representations of a date. 12 | # 13 | # @param left [String] A string representation of a date. 14 | # @param right [String] A string representation of a date. 15 | # @return [Boolean] True if +left+ is less-than-or-equal to +right+, 16 | # otherwise false. 17 | def date_le(left, right) 18 | left = str_to_dt(left) 19 | right = str_to_dt(right) 20 | 21 | left <= right 22 | end 23 | 24 | # Check if +left+ is greater than or equal to +right+, where both are string 25 | # representations of a date. 26 | # 27 | # @param left [String] A string representation of a date. 28 | # @param right [String] A string representation of a date. 29 | # @return [Boolean] True if +left+ is greater-than-or-equal to +right+, 30 | # otherwise false. 31 | def date_ge(left, right) 32 | left = str_to_dt(left) 33 | right = str_to_dt(right) 34 | 35 | left >= right 36 | end 37 | 38 | private 39 | 40 | # Converts a +String+ representation of a date to a +DateTime+. 41 | # 42 | # @param str [String] A date. 43 | # @return [DateTime] A DateTime representation of +str+. 44 | def str_to_dt(str) 45 | DateTime.parse(str) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/inq/exe.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq" 4 | require "inq/cli" 5 | require "inq/config" 6 | require "inq/text" 7 | 8 | module Inq 9 | ## 10 | # A module which implements the entire command-line interface for Inq. 11 | module Exe 12 | def self.run(argv) 13 | cli = parse_args(argv) 14 | options = cli.options 15 | 16 | abort cli.help_text if options[:help] 17 | abort Inq::VERSION_STRING if options[:version] 18 | 19 | execute(options) 20 | end 21 | 22 | def self.parse_args(argv) 23 | Inq::CLI.parse(argv) 24 | rescue OptionParser::ParseError => e 25 | abort "inq: error: #{e.message}" 26 | end 27 | private_class_method :parse_args 28 | 29 | def self.load_config(options) 30 | config = Inq::Config.new 31 | 32 | config.load_defaults unless options[:no_user_config] 33 | config.load_env if options[:env_config] 34 | 35 | if options[:config] 36 | config.load_files(options[:config]) 37 | else 38 | config.load(Inq.default_config(options[:repository])) 39 | end 40 | 41 | config 42 | end 43 | private_class_method :load_config 44 | 45 | def self.save_reports(reports) 46 | files = reports.save_all 47 | Inq::Text.puts "Saved reports to:" 48 | files.each { |file| Inq::Text.puts "- #{file}" } 49 | end 50 | private_class_method :save_reports 51 | 52 | def self.execute(options) 53 | start_date = options[:date] || options[:start_date] 54 | end_date = options[:end_date] 55 | 56 | config = load_config(options) 57 | reports = Inq.from_config(config, start_date, end_date) 58 | save_reports(reports) 59 | rescue => e 60 | raise if options[:verbose] 61 | 62 | warn "inq: error: #{e.message} (Pass --verbose for more details.)" 63 | warn " at: #{e.backtrace_locations.first}" 64 | exit 1 65 | end 66 | private_class_method :execute 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/inq/frontmatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/version" 4 | require "okay/warning_helpers" 5 | 6 | module Inq 7 | ## 8 | # Module for generating YAML frontmatter, as used by Jekyll and other 9 | # blog engines. 10 | module Frontmatter 11 | extend Okay::WarningHelpers 12 | 13 | # Generates YAML frontmatter, as is used in Jekyll and other blog engines. 14 | # 15 | # E.g., 16 | # generate_frontmatter({'foo' => "bar %{baz}"}, {'baz' => "asdf"}) 17 | # => "---\nfoo: bar asdf\n" 18 | # 19 | # @param frontmatter [Hash] Frontmatter for the report. 20 | # @param report_data [Hash] The report data itself. 21 | # @return [String] A YAML dump of the generated frontmatter. 22 | def self.generate(frontmatter, report_data) 23 | return "" if frontmatter.nil? 24 | 25 | frontmatter = convert_keys(frontmatter, :to_s) 26 | report_data = convert_keys(report_data, :to_sym) 27 | 28 | frontmatter = frontmatter.map { |k, v| 29 | # Sometimes report_data has unused keys, which generates a warning, but 30 | # we're okay with it. 31 | v = silence_warnings { v % report_data } 32 | 33 | [k, v] 34 | }.to_h 35 | 36 | YAML.dump(frontmatter) + "---\n\n" 37 | end 38 | 39 | # @example 40 | # convert_keys({'foo' => 'bar'}, :to_sym) 41 | # # => {:foo => 'bar'} 42 | # @param data [Hash] The input hash. 43 | # @param method_name [Symbol] The method name used to convert keys. 44 | # (E.g. :to_s, :to_sym, etc.) 45 | # @return [Hash] The converted result. 46 | def self.convert_keys(data, method_name) 47 | data.map { |k, v| [k.send(method_name), v] }.to_h 48 | end 49 | private_class_method :convert_keys 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/inq/report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/frontmatter" 4 | require "inq/cacheable" 5 | require "inq/sources/github/contributions" 6 | require "inq/sources/github/issues" 7 | require "inq/sources/github/pulls" 8 | require "inq/sources/ci/travis" 9 | require "inq/sources/ci/appveyor" 10 | require "inq/template" 11 | require "json" 12 | 13 | module Inq 14 | ## 15 | # Class for generating a report. 16 | class Report 17 | def initialize(config, start_date, end_date = nil) 18 | @config = config 19 | @repository = config["repository"] 20 | 21 | # NOTE: Use DateTime because it defaults to UTC and that's less gross 22 | # than trying to get Date to use UTC. 23 | # 24 | # Not using UTC for this results in #compare_url giving different 25 | # results for different time zones, which makes it harder to test. 26 | # 27 | # (I'm also guessing/hoping that GitHub's URLs use UTC.) 28 | end_dt = DateTime.strptime(end_date || start_date, "%Y-%m-%d") 29 | start_dt = 30 | if end_date 31 | DateTime.strptime(start_date, "%Y-%m-%d") 32 | else 33 | start_dt_from_end_dt(end_dt) 34 | end 35 | 36 | @end_date = end_dt.strftime("%Y-%m-%d") 37 | @start_date = start_dt.strftime("%Y-%m-%d") 38 | end 39 | 40 | def cache 41 | @cache ||= Cacheable.new(@config, @start_date, @end_date) 42 | end 43 | 44 | def contributions 45 | @gh_contributions ||= Sources::Github::Contributions.new(@config, @start_date, @end_date, cache) 46 | end 47 | 48 | def issues 49 | @gh_issues ||= Sources::Github::Issues.new(@config, @start_date, @end_date, cache) 50 | end 51 | 52 | def pulls 53 | @gh_pulls ||= Sources::Github::Pulls.new(@config, @start_date, @end_date, cache) 54 | end 55 | 56 | def travis 57 | @travis ||= Sources::CI::Travis.new(@config, @start_date, @end_date, cache) 58 | end 59 | 60 | def appveyor 61 | @appveyor ||= Sources::CI::Appveyor.new(@config, @start_date, @end_date, cache) 62 | end 63 | 64 | def to_h(frontmatter_data = nil) 65 | @report_hash ||= report_hash 66 | frontmatter = Frontmatter.generate(frontmatter_data, @report_hash) 67 | 68 | @report_hash.merge(frontmatter: frontmatter) 69 | end 70 | 71 | def to_html_partial(frontmatter = nil) 72 | Inq::Template.apply("report_partial.html", to_h(frontmatter)) 73 | end 74 | 75 | def to_html(frontmatter = nil) 76 | template_data = to_h(frontmatter).merge({report: to_html_partial}) 77 | Inq::Template.apply("report.html", template_data) 78 | end 79 | 80 | def to_json(frontmatter = nil) 81 | frontmatter.to_s + JSON.pretty_generate(to_h) 82 | end 83 | 84 | def save_as(filename) 85 | File.write(filename, to_format_for(filename)) 86 | end 87 | 88 | def to_format_for(filename) 89 | format = File.extname(filename)[1..-1] 90 | send("to_#{format}") 91 | end 92 | private :to_format_for 93 | 94 | def start_dt_from_end_dt(end_dt) 95 | d = end_dt.day 96 | m = end_dt.month 97 | y = end_dt.year 98 | start_year = y 99 | start_month = m - 1 100 | if start_month <= 0 101 | start_month = 12 - start_month 102 | start_year -= 1 103 | end 104 | 105 | DateTime.new(start_year, start_month, d) 106 | end 107 | 108 | # rubocop:disable Metrics/AbcSize 109 | def report_hash 110 | { 111 | title: "How is #{@repository}?", 112 | repository: @repository, 113 | 114 | contributions_summary: contributions.to_html, 115 | new_contributors: contributions.new_contributors_html, 116 | issues_summary: issues.to_html, 117 | pulls_summary: pulls.to_html, 118 | 119 | issues: issues.to_a, 120 | pulls: issues.to_a, 121 | 122 | average_issue_age: issues.average_age, 123 | average_pull_age: pulls.average_age, 124 | 125 | oldest_issue_link: issues.oldest["url"], 126 | oldest_issue_date: issues.oldest["createdAt"], 127 | 128 | newest_issue_link: issues.newest["url"], 129 | newest_issue_date: issues.newest["createdAt"], 130 | 131 | newest_pull_link: pulls.newest["url"], 132 | newest_pull_date: pulls.newest["createdAt"], 133 | 134 | oldest_pull_link: pulls.oldest["url"], 135 | oldest_pull_date: pulls.oldest["createdAt"], 136 | 137 | travis_builds: travis.builds, 138 | appveyor_builds: appveyor.builds, 139 | 140 | date: @end_date, 141 | } 142 | end 143 | # rubocop:enable Metrics/AbcSize 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/inq/report_collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/report" 4 | require "okay/warning_helpers" 5 | 6 | module Inq 7 | ## 8 | # A class representing a collection of Reports. 9 | class ReportCollection 10 | include Okay::WarningHelpers 11 | 12 | def initialize(config, start_date, end_date=nil) 13 | @config = config 14 | 15 | # If the config is in the old format, convert it to the new one. 16 | unless @config["repositories"] 17 | @config["repositories"] = [{ 18 | "repository" => @config.delete("repository"), 19 | "reports" => @config.delete("reports"), 20 | }] 21 | end 22 | 23 | @start_date = start_date 24 | @end_date = end_date 25 | @reports = config["repositories"].map(&method(:fetch_report)).to_h 26 | end 27 | 28 | # Generates the metadata for the collection of Reports. 29 | def metadata(repository) 30 | end_date = DateTime.strptime(@end_date || @start_date, "%Y-%m-%d") 31 | friendly_end_date = end_date.strftime("%B %d, %y") 32 | 33 | { 34 | sanitized_repository: repository.tr("/", "-"), 35 | repository: repository, 36 | date: end_date, 37 | friendly_date: friendly_end_date, 38 | } 39 | end 40 | private :metadata 41 | 42 | def config_for(repo) 43 | defaults = @config.fetch("default_reports", {}) 44 | config = @config.dup 45 | repos = config.delete("repositories") 46 | 47 | # Find the _last_ one that matches, to allow overriding. 48 | repo_config = repos.reverse.find { |conf| conf["repository"] == repo } 49 | 50 | # Use values from default_reports, unless overridden. 51 | config["repository"] = repo 52 | config["reports"] = defaults.merge(repo_config.fetch("reports", {})) 53 | config 54 | end 55 | private :config_for 56 | 57 | def fetch_report(repo_config) 58 | repo = repo_config["repository"] 59 | report = Report.new(config_for(repo), @start_date, @end_date) 60 | [repo, report] 61 | end 62 | private :fetch_report 63 | 64 | # Converts a ReportCollection to a Hash. 65 | # 66 | # Also good for giving programmers nightmares, I suspect. 67 | def to_h 68 | results = {} 69 | defaults = @config["default_reports"] || {} 70 | 71 | @config["repositories"].map { |repo_config| 72 | repo = repo_config["repository"] 73 | config = config_for(repo) 74 | 75 | config["reports"].map { |format, report_config| 76 | # Sometimes report_data has unused keys, which generates a warning, but 77 | # we're okay with it, so we wrap it with silence_warnings {}. 78 | filename = silence_warnings { 79 | tmp_filename = report_config["filename"] || defaults[format]["filename"] 80 | tmp_filename % metadata(repo) 81 | } 82 | 83 | directory = report_config["directory"] || defaults[format]["directory"] 84 | file = File.join(directory, filename) 85 | 86 | # Export +report+ to the specified +format+ with the specified 87 | # +frontmatter+. 88 | frontmatter = report_config["frontmatter"] || {} 89 | if defaults.has_key?(format) && defaults[format].has_key?("frontmatter") 90 | frontmatter = defaults[format]["frontmatter"].merge(frontmatter) 91 | end 92 | frontmatter = nil if frontmatter == {} 93 | 94 | export = @reports[repo].send("to_#{format}", frontmatter) 95 | 96 | results[file] = export 97 | } 98 | } 99 | results 100 | end 101 | 102 | # Save all of the reports to the corresponding files. 103 | # 104 | # @return [Array] An array of file paths. 105 | def save_all 106 | reports = to_h 107 | reports.each do |file, report| 108 | File.write(file, report) 109 | end 110 | 111 | reports.keys 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/inq/sources.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/version" 4 | 5 | module Inq 6 | ## 7 | # Various information sources used by Inq. 8 | module Sources 9 | # Simply for creating a namespace. 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inq/sources/ci/appveyor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "okay/default" 4 | require "okay/http" 5 | require "inq/constants" 6 | require "inq/sources" 7 | require "inq/sources/github/contributions" 8 | require "inq/text" 9 | 10 | module Inq 11 | module Sources 12 | module CI 13 | # Fetches metadata about CI builds from appveyor.com. 14 | class Appveyor 15 | # @param repository [String] GitHub repository name, of the format user/repo. 16 | # @param start_date [String] Start date for the report being generated. 17 | # @param end_date [String] End date for the report being generated. 18 | # @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls 19 | def initialize(config, start_date, end_date, cache) 20 | @config = config 21 | @cache = cache 22 | @repository = config["repository"] 23 | @start_date = DateTime.parse(start_date) 24 | @end_date = DateTime.parse(end_date) 25 | @default_branch = Okay.default 26 | end 27 | 28 | # @return [String] The default branch name. 29 | def default_branch 30 | return @default_branch unless @default_branch.nil? 31 | 32 | contributions = Sources::GitHub::Contributions.new(@config, nil, nil) 33 | 34 | @default_branch = contributions.default_branch 35 | end 36 | 37 | # Fetches builds for the default branch. 38 | # 39 | # @return [Hash] Builds for the default branch. 40 | def builds 41 | @builds ||= 42 | fetch_builds["builds"] \ 43 | .map(&method(:normalize_build)) \ 44 | .select(&method(:in_date_range?)) 45 | rescue Net::HTTPServerException 46 | # It's not elegant, but it works™. 47 | [] 48 | end 49 | 50 | private 51 | 52 | def in_date_range?(build) 53 | build["started_at"] >= @start_date && 54 | build["started_at"] <= @end_date 55 | end 56 | 57 | def normalize_build(build) 58 | build["started_at"] = DateTime.parse(build["created"]) 59 | build["html_url"] = "https://ci.appveyor.com/project/#{@repository}/build/#{build['buildNumber']}" 60 | build 61 | end 62 | 63 | # Returns API result of /api/projects/:repository. 64 | # FIXME: This doesn't limit results based on the date range. 65 | # 66 | # @return [Hash] API results. 67 | def fetch_builds 68 | @cache.cached("appveyor_builds") do 69 | Inq::Text.print "Fetching Appveyor build data." 70 | 71 | ret = Okay::HTTP.get( 72 | "https://ci.appveyor.com/api/projects/#{@repository}/history", 73 | parameters: {"recordsNumber" => "100"}, 74 | headers: { 75 | "Accept" => "application/json", 76 | "User-Agent" => Inq::USER_AGENT, 77 | } 78 | ).or_raise!.from_json 79 | 80 | Inq::Text.puts 81 | ret 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/inq/sources/ci/travis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | require "okay/default" 5 | require "okay/http" 6 | require "inq/constants" 7 | require "inq/sources/github" 8 | require "inq/text" 9 | 10 | module Inq 11 | module Sources 12 | module CI 13 | # Fetches metadata about CI builds from travis-ci.org. 14 | class Travis 15 | BadResponseError = Class.new(StandardError) 16 | 17 | # @param repository [String] GitHub repository name, of the format user/repo. 18 | # @param start_date [String] Start date for the report being generated. 19 | # @param end_date [String] End date for the report being generated. 20 | # @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls 21 | def initialize(config, start_date, end_date, cache) 22 | @config = config 23 | @cache = cache 24 | @repository = config["repository"] 25 | raise "Travis.new() got nil repository." if @repository.nil? 26 | @start_date = DateTime.parse(start_date) 27 | @end_date = DateTime.parse(end_date) 28 | @default_branch = Okay.default 29 | end 30 | 31 | # @return [String] The default branch name. 32 | def default_branch 33 | return @default_branch unless @default_branch == Okay.default 34 | 35 | response = fetch("branches", {"sort_by" => "default_branch"}) 36 | validate_response!(response) 37 | 38 | branches = response["branches"] 39 | validate_branches!(branches) 40 | 41 | branch = branches.find { |b| b["default_branch"] == true } 42 | @default_branch = branch&.fetch("name", nil) 43 | end 44 | 45 | # Returns the builds for the default branch. 46 | # 47 | # @return [Hash] Hash containing the builds for the default branch. 48 | def builds 49 | @cache.cached("travis_builds") do 50 | raw_builds \ 51 | .map(&method(:normalize_build)) \ 52 | .select(&method(:in_date_range?)) \ 53 | .map(&method(:add_build_urls)) 54 | end 55 | end 56 | 57 | private 58 | 59 | def add_build_urls(build) 60 | build["html_url"] = "https://travis-ci.org/#{build['repository']}#{build['@href']}" 61 | build 62 | end 63 | 64 | def validate_response!(response) 65 | return true if hash_with_key?(response, "branches") 66 | 67 | raise BadResponseError, 68 | "expected `response' (#{response.class}) to be a Hash with key `\"branches\"'." 69 | end 70 | 71 | def validate_branches!(branches) 72 | return true if array_of_hashes?(branches) 73 | 74 | raise BadResponseError, "expected `branches' to be Array of Hashes." 75 | end 76 | 77 | def array_of_hashes?(ary) 78 | ary.is_a?(Array) && ary.all? { |obj| obj.is_a?(Hash) } 79 | end 80 | 81 | def hash_with_key?(hsh, key) 82 | hsh.is_a?(Hash) && hsh.has_key?(key) 83 | end 84 | 85 | def in_date_range?(build, start_date = @start_date, end_date = @end_date) 86 | return unless build["started_at"] && build["finished_at"] 87 | (build["started_at"] >= start_date) \ 88 | && (build["finished_at"] <= end_date) 89 | end 90 | 91 | def raw_builds 92 | results = fetch("builds", { 93 | "event_type" => "push", 94 | "branch.name" => default_branch, 95 | }) 96 | 97 | results["builds"] || {} 98 | rescue Net::HTTPServerException 99 | # It's not elegant, but it works™. 100 | {} 101 | end 102 | 103 | def normalize_build(build) 104 | build_keys = ["@href", "pull_request_title", "pull_request_number", 105 | "started_at", "finished_at", "repository", "commit", 106 | "jobs"] 107 | result = pluck_keys(build, build_keys) 108 | 109 | commit_keys = ["sha", "ref", "message", "compare_url", 110 | "committed_at", "jobs"] 111 | result["commit"] = pluck_keys(result["commit"], commit_keys) 112 | 113 | job_keys = ["href", "id"] 114 | result["jobs"] = result["jobs"].map { |j| pluck_keys(j, job_keys) } 115 | 116 | result["repository"] = result["repository"]["slug"] 117 | 118 | ["started_at", "finished_at"].each do |k| 119 | next if result[k].nil? 120 | result[k] = DateTime.parse(result[k]) 121 | end 122 | 123 | result 124 | end 125 | 126 | def pluck_keys(hsh, keys) 127 | keys.map { |k| [k, hsh[k]] }.to_h 128 | end 129 | 130 | # Returns API results for /repos/:user/:repo/. 131 | # 132 | # @param path [String] Path suffix (appended to /repo//). 133 | # @param parameters [Hash] Parameters. 134 | # @return [String] JSON result. 135 | def fetch(path, parameters = {}) 136 | @cache.cached("travis_path") do 137 | Inq::Text.print "Fetching Travis CI #{path.sub(/e?s$/, '')} data." 138 | 139 | # Apparently this is required for the Travis CI API to work. 140 | repo = @repository.sub("/", "%2F") 141 | 142 | ret = Okay::HTTP.get( 143 | "https://api.travis-ci.org/repo/#{repo}/#{path}", 144 | parameters: parameters, 145 | headers: { 146 | "Travis-Api-Version" => "3", 147 | "Accept" => "application/json", 148 | "User-Agent" => Inq::USER_AGENT, 149 | } 150 | ).or_raise!.from_json 151 | 152 | Inq::Text.puts 153 | 154 | ret 155 | end 156 | end 157 | end 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /lib/inq/sources/github.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/version" 4 | require "inq/sources" 5 | require "okay/graphql" 6 | 7 | module Inq 8 | module Sources 9 | # Contains configuration information for GitHub-based sources. 10 | class Github 11 | class ConfigError < StandardError 12 | end 13 | 14 | def initialize(config) 15 | must_have_key!(config, "sources/github") 16 | 17 | @config = config["sources/github"] 18 | 19 | must_have_key!(@config, "username") 20 | must_have_key!(@config, "token") 21 | end 22 | 23 | # Verify that +hash+ has a particular +key+. 24 | # 25 | # @raise [ConfigError] If +hash+ does not have the required +key+. 26 | def must_have_key!(hash, key) 27 | raise ConfigError, "Expected Hash, got #{hash.class}" unless hash.is_a?(Hash) 28 | raise ConfigError, "Expected key `#{key}'" unless hash.has_key?(key) 29 | end 30 | private :must_have_key! 31 | 32 | # The GitHub username used for authenticating with GitHub. 33 | def username 34 | @config["username"] 35 | end 36 | 37 | # A GitHub Personal Access Token which goes with +username+. 38 | def access_token 39 | @config["token"] 40 | end 41 | 42 | # A string containing both the GitHub username and access token, 43 | # used in instances where we use Basic Auth. 44 | def basic_auth 45 | "#{username}:#{access_token}" 46 | end 47 | 48 | # Submit a GraphQL query, and convert it from JSON to a Ruby object. 49 | def graphql(query_string) 50 | Okay::GraphQL.query(query_string) 51 | .submit!(:github, {bearer_token: access_token}) 52 | .or_raise! 53 | .from_json 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/inq/sources/github/contributions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "github_api" 4 | require "inq/cacheable" 5 | require "inq/sources/github" 6 | require "inq/sources/github_helpers" 7 | require "inq/template" 8 | require "inq/text" 9 | require "date" 10 | 11 | module Inq 12 | module Sources 13 | class Github 14 | # Fetch information about who has contributed to a repository during 15 | # a given period. 16 | # 17 | # Usage: 18 | # 19 | # c = Inq::Contributions.new(start_date: '2017-07-01', user: 'duckinator', repo: 'inq') 20 | # c.commits #=> All commits during July 2017. 21 | # c.contributors #=> All contributors during July 2017. 22 | # c.new_contributors #=> New contributors during July 2017. 23 | class Contributions 24 | include Inq::Sources::GithubHelpers 25 | 26 | # Returns an object that fetches contributor information about a 27 | # particular repository for a month-long period starting on +start_date+. 28 | # 29 | # @param config [Hash] A config object. 30 | # @param start_date [String] Date in the format YYYY-MM-DD. 31 | # The first date to include commits from. 32 | # @param end_date [String] Date in the format YYYY-MM-DD. 33 | # The last date to include commits from. 34 | # @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls 35 | def initialize(config, start_date, end_date, cache) 36 | raise "Got String, need Hash. The Github::Contributions API changed." if config.is_a?(String) 37 | 38 | @config = config 39 | @cache = cache 40 | @github = Sources::Github.new(config) 41 | @repository = config["repository"] 42 | 43 | @user, @repo = @repository.split("/") 44 | @github = ::Github.new(auto_pagination: true) { |conf| 45 | conf.basic_auth = @github.basic_auth 46 | } 47 | 48 | # IMPL. DETAIL: The external API uses "end_date" so it's clearer, 49 | # but internally we use "until_date" to match GitHub's API. 50 | @since_date = start_date 51 | @until_date = end_date 52 | 53 | @commit = {} 54 | @stats = nil 55 | @changed_files = nil 56 | end 57 | 58 | # Returns a list of contributors that have zero commits before the @since_date. 59 | # 60 | # @return [Hash{String => Hash}] Contributors keyed by email 61 | def new_contributors 62 | @new_contributors ||= contributors.select { |email, _committer| 63 | args = { 64 | user: @user, 65 | repo: @repo, 66 | until: @since_date, 67 | author: email, 68 | } 69 | # True if +email+ never wrote a commit for +@repo+ before +@since_date+, false otherwise. 70 | commits = @cache.cached("repos_commits", args.to_json) do 71 | @github.repos.commits.list(**args) 72 | end 73 | commits.count.zero? 74 | } 75 | end 76 | 77 | def new_contributors_html 78 | names = new_contributors.values.map { |c| c["name"] } 79 | list_items = names.map { |n| "
  • #{n}
  • " }.join("\n") 80 | 81 | if names.length.zero? 82 | num_new_contributors = "no" 83 | else 84 | num_new_contributors = names.length 85 | end 86 | 87 | if names.length == 1 88 | was_were = "was" 89 | contributor_s = "" 90 | else 91 | was_were = "were" 92 | contributor_s = "s" 93 | end 94 | 95 | Template.apply("new_contributors_partial.html", { 96 | was_were: was_were, 97 | contributor_s: contributor_s, 98 | number_of_new_contributors: num_new_contributors, 99 | list_items: list_items, 100 | }).strip 101 | end 102 | 103 | # @return [Hash{String => Hash}] Author information keyed by author's email. 104 | def contributors 105 | commits.map { |api_response| 106 | [api_response.commit.author.email, api_response.commit.author.to_h] 107 | }.to_h 108 | end 109 | 110 | def commits 111 | return @commits if instance_variable_defined?(:@commits) 112 | 113 | args = { 114 | user: @user, 115 | repo: @repo, 116 | since: @since_date, 117 | until: @until_date, 118 | } 119 | 120 | Inq::Text.print "Fetching #{@repository} commit data." 121 | 122 | # The commits list endpoint doesn't include all stats. 123 | # So, to compensate, we make N requests here, where N is number 124 | # of commits returned, and then we die a bit inside. 125 | @commits = @cache.cached("repos_commits", args.to_json) do 126 | @github.repos.commits.list(**args).map { |c| 127 | Inq::Text.print "." 128 | commit(c.sha) 129 | } 130 | end 131 | Inq::Text.puts 132 | @commits 133 | end 134 | 135 | def commit(sha) 136 | @commit[sha] ||= @cache.cached("repos_commit_#{sha}") do 137 | @github.repos.commits.get(user: @user, repo: @repo, sha: sha) 138 | end 139 | end 140 | 141 | def stats 142 | return @stats if @stats 143 | 144 | stats = {"total" => 0, "additions" => 0, "deletions" => 0} 145 | commits.map do |commit| 146 | stats.keys.each do |key| 147 | stats[key] += commit.stats[key] 148 | end 149 | end 150 | @stats = stats 151 | end 152 | 153 | def changed_files 154 | return @changed_files if @changed_files 155 | files = commits.flat_map do |commit| 156 | commit.files.map { |file| file["filename"] } 157 | end 158 | @changed_files = files.sort.uniq 159 | end 160 | 161 | def changes 162 | {"stats" => stats, "files" => changed_files} 163 | end 164 | 165 | def additions_count 166 | changes["stats"]["additions"] 167 | end 168 | 169 | def deletions_count 170 | changes["stats"]["deletions"] 171 | end 172 | 173 | def compare_url 174 | "https://github.com/#{@user}/#{@repo}/compare/#{default_branch}@%7B#{@since_date}%7D...#{default_branch}@%7B#{@until_date}%7D" # rubocop:disable Metrics/LineLength 175 | end 176 | 177 | def default_branch 178 | @default_branch ||= @cache.cached("repos_default_branch") do 179 | @github.repos.get(user: @user, repo: @repo).default_branch 180 | end 181 | end 182 | 183 | # rubocop:disable Metrics/AbcSize 184 | def to_html(start_text: nil) 185 | start_text ||= "From #{pretty_date(@since_date)} through #{pretty_date(@until_date)}" 186 | 187 | Inq::Template.apply("contributions_partial.html", { 188 | start_text: start_text, 189 | user: @user, 190 | repo: @repo, 191 | compare_url: compare_url, 192 | additions_count_str: (additions_count == 1) ? "was" : "were", 193 | authors: pluralize("author", contributors.length), 194 | new_commits: pluralize("new commit", commits.length), 195 | additions: pluralize("addition", additions_count), 196 | deletions: pluralize("deletion", deletions_count), 197 | changed_files: pluralize("file", changed_files.length), 198 | }).strip 199 | end 200 | # rubocop:enable Metrics/AbcSize 201 | end 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/inq/sources/github/issue_fetcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/version" 4 | require "inq/date_time_helpers" 5 | require "inq/sources/github" 6 | require "inq/text" 7 | 8 | module Inq 9 | module Sources 10 | class Github 11 | ## 12 | # Fetches raw data for GitHub issues. 13 | class IssueFetcher 14 | include Inq::DateTimeHelpers 15 | 16 | END_LOOP = :terminate_graphql_loop 17 | 18 | GRAPHQL_QUERY = <<~QUERY 19 | repository(owner: %{user}, name: %{repo}) { 20 | %{type}(first: %{chunk_size}%{after_str}, orderBy:{field: CREATED_AT, direction: ASC}) { 21 | edges { 22 | cursor 23 | node { 24 | number 25 | createdAt 26 | closedAt 27 | updatedAt 28 | state 29 | title 30 | url 31 | labels(first: 100) { 32 | nodes { 33 | name 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | QUERY 41 | 42 | CHUNK_SIZE = 100 43 | 44 | attr_reader :type 45 | 46 | # @param issues_source [Issues] Inq::Issues or Inq::Pulls instance for which to fetch issues 47 | def initialize(issues_source) 48 | @issues_source = issues_source 49 | @cache = issues_source.cache 50 | @github = Sources::Github.new(issues_source.config) 51 | @repository = issues_source.config["repository"] 52 | @user, @repo = @repository.split("/", 2) 53 | @start_date = issues_source.start_date 54 | @end_date = issues_source.end_date 55 | @type = issues_source.type 56 | end 57 | 58 | def data 59 | return @data if instance_variable_defined?(:@data) 60 | 61 | @data = [] 62 | return @data if last_cursor.nil? 63 | 64 | Inq::Text.print "Fetching #{@repository} #{@issues_source.pretty_type} data." 65 | 66 | @data = @cache.cached("fetch-#{type}") do 67 | data = [] 68 | after, data = fetch_issues(after, data) until after == END_LOOP 69 | data.select(&method(:issue_is_relevant?)) 70 | end 71 | 72 | Inq::Text.puts 73 | 74 | @data 75 | end 76 | 77 | def issue_is_relevant?(issue) 78 | if !issue["closedAt"].nil? && date_le(issue["closedAt"], @start_date) 79 | false 80 | else 81 | date_ge(issue["createdAt"], @start_date) && date_le(issue["createdAt"], @end_date) 82 | end 83 | end 84 | 85 | def last_cursor 86 | return @last_cursor if instance_variable_defined?(:@last_cursor) 87 | 88 | raw_data = @github.graphql <<~QUERY 89 | repository(owner: #{@user.inspect}, name: #{@repo.inspect}) { 90 | #{type}(last: 1, orderBy:{field: CREATED_AT, direction: ASC}) { 91 | edges { 92 | cursor 93 | } 94 | } 95 | } 96 | QUERY 97 | 98 | edges = raw_data.dig("data", "repository", type, "edges") 99 | @last_cursor = 100 | if edges.nil? || edges.empty? 101 | nil 102 | else 103 | edges.last["cursor"] 104 | end 105 | end 106 | 107 | def fetch_issues(after, data) 108 | Inq::Text.print "." 109 | 110 | after_str = ", after: #{after.inspect}" unless after.nil? 111 | 112 | query = build_query(@user, @repo, type, after_str) 113 | raw_data = @github.graphql(query) 114 | edges = raw_data.dig("data", "repository", type, "edges") 115 | 116 | data += edge_nodes(edges) 117 | 118 | next_cursor = edges.last["cursor"] 119 | next_cursor = END_LOOP if next_cursor == last_cursor 120 | 121 | [next_cursor, data] 122 | end 123 | 124 | def build_query(user, repo, type, after_str) 125 | format(GRAPHQL_QUERY, { 126 | user: user.inspect, 127 | repo: repo.inspect, 128 | type: type, 129 | chunk_size: CHUNK_SIZE, 130 | after_str: after_str, 131 | }) 132 | end 133 | 134 | def edge_nodes(edges) 135 | return [] if edges.nil? 136 | new_data = edges.map { |issue| 137 | node = issue["node"] 138 | node["labels"] = node["labels"]["nodes"] 139 | 140 | node 141 | } 142 | 143 | new_data 144 | end 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/inq/sources/github/issues.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/date_time_helpers" 4 | require "inq/sources/github" 5 | require "inq/sources/github_helpers" 6 | require "inq/sources/github/issue_fetcher" 7 | require "inq/template" 8 | require "date" 9 | 10 | module Inq 11 | module Sources 12 | class Github 13 | ## 14 | # Fetches various information about GitHub Issues. 15 | class Issues 16 | include Inq::DateTimeHelpers 17 | include Inq::Sources::GithubHelpers 18 | 19 | attr_reader :config, :start_date, :end_date, :cache 20 | 21 | # @param repository [String] GitHub repository name, of the format user/repo. 22 | # @param start_date [String] Start date for the report being generated. 23 | # @param end_date [String] End date for the report being generated. 24 | # @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls 25 | def initialize(config, start_date, end_date, cache) 26 | @config = config 27 | @cache = cache 28 | @repository = config["repository"] 29 | raise "#{self.class}.new() got nil repository." if @repository.nil? 30 | @start_date = start_date 31 | @end_date = end_date 32 | end 33 | 34 | def url(values = {}) 35 | defaults = { 36 | "is" => singular_type, 37 | "created" => "#{@start_date}..#{@end_date}", 38 | } 39 | values = defaults.merge(values) 40 | raw_query = values.map { |k, v| 41 | [k, v].join(":") 42 | }.join(" ") 43 | 44 | query = CGI.escape(raw_query) 45 | 46 | "https://github.com/#{@repository}/#{url_suffix}?q=#{query}" 47 | end 48 | 49 | def average_age 50 | average_age_for(data) 51 | end 52 | 53 | def oldest 54 | result = oldest_for(data) 55 | return {} if result.nil? 56 | 57 | result["date"] = pretty_date(result["createdAt"]) 58 | 59 | result 60 | end 61 | 62 | def newest 63 | result = newest_for(data) 64 | return {} if result.nil? 65 | 66 | result["date"] = pretty_date(result["createdAt"]) 67 | 68 | result 69 | end 70 | 71 | def summary 72 | number_open = to_a.length 73 | pretty_number = pluralize(pretty_type, number_open, zero_is_no: false) 74 | was_were = (number_open == 1) ? "was" : "were" 75 | 76 | "

    A total of #{pretty_number} #{was_were} opened during this period.

    " 77 | end 78 | 79 | def to_html 80 | return summary if to_a.empty? 81 | 82 | Inq::Template.apply("issues_or_pulls_partial.html", { 83 | summary: summary, 84 | average_age: average_age, 85 | pretty_type: pretty_type, 86 | 87 | oldest_link: oldest["url"], 88 | oldest_date: oldest["date"], 89 | 90 | newest_link: newest["url"], 91 | newest_date: newest["date"], 92 | }) 93 | end 94 | 95 | def to_a 96 | obj_to_array_of_hashes(data) 97 | end 98 | 99 | def type 100 | singular_type + "s" 101 | end 102 | 103 | def pretty_type 104 | "issue" 105 | end 106 | 107 | private 108 | 109 | def url_suffix 110 | "issues" 111 | end 112 | 113 | def singular_type 114 | "issue" 115 | end 116 | 117 | def data 118 | return @data if instance_variable_defined?(:@data) 119 | 120 | fetcher = IssueFetcher.new(self) 121 | @data = fetcher.data 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/inq/sources/github/pulls.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/sources/github/issues" 4 | 5 | module Inq 6 | module Sources 7 | class Github 8 | ## 9 | # Fetches various information about GitHub Pull Requests 10 | class Pulls < Issues 11 | def url_suffix 12 | "pulls" 13 | end 14 | 15 | def singular_type 16 | "pull" 17 | end 18 | 19 | def type 20 | "pullRequests" 21 | end 22 | 23 | def pretty_type 24 | "pull request" 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/inq/sources/github_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/sources/github" 4 | require "date" 5 | 6 | module Inq 7 | module Sources 8 | ## 9 | # Helper functions used by GitHub-related sources. 10 | module GithubHelpers 11 | def obj_to_array_of_hashes(object) 12 | object.to_a.map(&:to_h) 13 | end 14 | 15 | # Given an Array of issues or pulls, return the average age of them. 16 | # Returns nil if no issues or pulls are provided. 17 | def average_age_for(issues_or_pulls) 18 | return nil if issues_or_pulls.empty? 19 | 20 | ages = issues_or_pulls.map { |iop| time_ago_in_seconds(iop["createdAt"]) } 21 | average_age_in_seconds = ages.reduce(:+) / ages.length 22 | 23 | values = 24 | period_pairs_for(average_age_in_seconds) \ 25 | .reject { |(v, _)| v.zero? } \ 26 | .map { |(v, k)| pluralize(k, v) } 27 | 28 | value = values[0, 2].join(" and ") 29 | 30 | "approximately #{value}" 31 | end 32 | 33 | def sort_iops_by_created_at(issues_or_pulls) 34 | issues_or_pulls.sort_by { |x| DateTime.parse(x["createdAt"]) } 35 | end 36 | 37 | # Given an Array of issues or pulls, return the oldest. 38 | # Returns nil if no issues or pulls are provided. 39 | def oldest_for(issues_or_pulls) 40 | return nil if issues_or_pulls.empty? 41 | 42 | sort_iops_by_created_at(issues_or_pulls).first 43 | end 44 | 45 | # Given an Array of issues or pulls, return the newest. 46 | # Returns nil if no issues or pulls are provided. 47 | def newest_for(issues_or_pulls) 48 | return nil if issues_or_pulls.empty? 49 | 50 | sort_iops_by_created_at(issues_or_pulls).last 51 | end 52 | 53 | private 54 | 55 | # Returns how many seconds ago a date (as a String) was. 56 | def time_ago_in_seconds(x) 57 | DateTime.now.strftime("%s").to_i - DateTime.parse(x).strftime("%s").to_i 58 | end 59 | 60 | SECONDS_IN_A_YEAR = 31_556_926 61 | SECONDS_IN_A_MONTH = 2_629_743 62 | SECONDS_IN_A_WEEK = 604_800 63 | SECONDS_IN_A_DAY = 86_400 64 | 65 | # Calculates a list of pairs of value and period label. 66 | # 67 | # @param age_in_seconds [Float] 68 | # 69 | # @return [Array] The input age_in_seconds expressed as different 70 | # units, as pairs of value and unit name. 71 | def period_pairs_for(age_in_seconds) 72 | years_remainder = age_in_seconds % SECONDS_IN_A_YEAR 73 | 74 | months_remainder = years_remainder % SECONDS_IN_A_MONTH 75 | 76 | weeks_remainder = months_remainder % SECONDS_IN_A_WEEK 77 | 78 | [ 79 | [age_in_seconds / SECONDS_IN_A_YEAR, "year"], 80 | [years_remainder / SECONDS_IN_A_MONTH, "month"], 81 | [months_remainder / SECONDS_IN_A_WEEK, "week"], 82 | [weeks_remainder / SECONDS_IN_A_DAY, "day"], 83 | ] 84 | end 85 | 86 | def pluralize(string, number, zero_is_no: false) 87 | number_str = number 88 | number_str = "no" if number.zero? && zero_is_no 89 | 90 | "#{number_str} #{string}#{(number == 1) ? '' : 's'}" 91 | end 92 | 93 | def pretty_date(date_or_str) 94 | if date_or_str.is_a?(DateTime) 95 | date = datetime_or_str 96 | elsif date_or_str.is_a?(String) 97 | date = DateTime.parse(date_or_str) 98 | else 99 | raise ArgumentError, "expected DateTime or String, got #{date_or_str.class}" 100 | end 101 | 102 | date.strftime("%b %d, %Y") 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/inq/template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/version" 4 | require "okay/template" 5 | 6 | module Inq 7 | # Provides basic templating functionality. 8 | Template = Okay::Template.new(File.expand_path("./templates/", __dir__)) 9 | end 10 | -------------------------------------------------------------------------------- /lib/inq/templates/contributions_partial.html: -------------------------------------------------------------------------------- 1 | %{start_text}, %{user}/%{repo} gained %{new_commits}, contributed by %{authors}. There %{additions_count_str} %{additions} and %{deletions} across %{changed_files}. 2 | -------------------------------------------------------------------------------- /lib/inq/templates/issues_or_pulls_partial.html: -------------------------------------------------------------------------------- 1 | %{summary} 2 | 3 | 8 | -------------------------------------------------------------------------------- /lib/inq/templates/new_contributors_partial.html: -------------------------------------------------------------------------------- 1 |

    There %{was_were} %{number_of_new_contributors} new contributor%{contributor_s} during this report period.

    2 | 3 |
      4 | %{list_items} 5 |
    6 | -------------------------------------------------------------------------------- /lib/inq/templates/report.html: -------------------------------------------------------------------------------- 1 | %{frontmatter} 2 | 3 | 4 | %{title} 5 | 13 | 14 | 15 |
    16 | %{report} 17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/inq/templates/report_partial.html: -------------------------------------------------------------------------------- 1 | %{frontmatter} 2 |

    %{title}

    3 |

    %{contributions_summary}

    4 | 5 |

    New Contributors

    6 | %{new_contributors} 7 | 8 |

    Pull Requests

    9 | %{pulls_summary} 10 | 11 |

    Issues

    12 | %{issues_summary} 13 | -------------------------------------------------------------------------------- /lib/inq/text.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Inq 4 | ## 5 | # Helper class for printing text, but hiding it when e.g. running in CI. 6 | class Text 7 | def self.show_default_output 8 | @show_default_output = true unless 9 | instance_variable_defined?(:"@show_default_output") 10 | 11 | @show_default_output 12 | end 13 | 14 | def self.show_default_output=(val) 15 | @show_default_output = val 16 | end 17 | 18 | def self.print(*args) 19 | Kernel.print(*args) if Inq::Text.show_default_output 20 | end 21 | 22 | def self.puts(*args) 23 | Kernel.puts(*args) if Inq::Text.show_default_output 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/inq/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Inq 4 | VERSION = "26.0.0" 5 | VERSION_STRING = "inq #{VERSION}" 6 | end 7 | -------------------------------------------------------------------------------- /spec/capture_warnings.rb: -------------------------------------------------------------------------------- 1 | module Warning 2 | # Where the codebase we're checking is located. 3 | CODEBASE_LOCATION = File.expand_path('../', __dir__) 4 | 5 | # .bundler/ is where Bundler dependencies are on most systems. 6 | BUNDLER_DIR_LOCATION = File.expand_path('.bundle', CODEBASE_LOCATION) 7 | 8 | # vendor/ is where Bundler dependencies are located on Travis CI. 9 | VENDOR_DIR_LOCATION = File.expand_path('vendor', CODEBASE_LOCATION) 10 | 11 | # Directories we want to ignore because they aren't part of the codebase 12 | # we're checking. 13 | IGNORED_DIRS = [ 14 | BUNDLER_DIR_LOCATION, 15 | VENDOR_DIR_LOCATION, 16 | ] 17 | 18 | @@dependency_warnings = [] 19 | @@codebase_warnings = [] 20 | 21 | # Override Warning.warn(), so warnings from -w and -W are only printed 22 | # for things in how_is' codebase. 23 | def self.warn(msg) 24 | path = File.realpath(caller_locations.first.path) 25 | 26 | # Only print warnings for files in how_is' codebase. 27 | if path.start_with?(CODEBASE_LOCATION) && IGNORED_DIRS.none? { |dir| path.start_with?(dir) } 28 | @@codebase_warnings << msg 29 | super(msg) 30 | else 31 | @@dependency_warnings << msg 32 | end 33 | end 34 | 35 | def codebase_warnings 36 | @@codebase_warnings 37 | end 38 | 39 | def dependency_warnings 40 | @@dependency_warnings 41 | end 42 | 43 | def has_warnings? 44 | @@codebase_warnings.length > 0 || @@dependency_warnings.length > 0 45 | end 46 | end 47 | 48 | at_exit { 49 | if Warning.has_warnings? 50 | puts "=== Warnings ===" 51 | puts "#{Warning.codebase_warnings.length} warnings in inq." 52 | puts "#{Warning.dependency_warnings.length} warnings in inq's dependencies." 53 | puts "================" 54 | else 55 | puts "No warnings found in the codebase." 56 | end 57 | } 58 | -------------------------------------------------------------------------------- /spec/data/fake/issues.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://github.com/rubygems/rubygems/issues/9001", 4 | "id": 150951839, 5 | "number": 9001, 6 | "title": "Fake Oldest Issue", 7 | "user": { 8 | "login": "example", 9 | "id": 57936, 10 | "avatar_url": "https://avatars.githubusercontent.com/u/57936?v=3", 11 | "url": "https://github.com/example" 12 | }, 13 | "labels": [ 14 | { 15 | "name": "triage", 16 | "color": "ffffff" 17 | } 18 | ], 19 | "state": "open", 20 | "locked": false, 21 | "assignee": null, 22 | "milestone": null, 23 | "comments": 2, 24 | "createdAt": "1999-01-01T00:00:00Z", 25 | "updatedAt": "1999-01-01T20:30:40Z", 26 | "closedAt": null, 27 | "body": "Argle bargle example issue #9001.\r\nMade artificially old." 28 | }, 29 | 30 | { 31 | "url": "https://github.com/rubygems/rubygems/issues/9002", 32 | "id": 150951839, 33 | "number": 9001, 34 | "title": "Fake Second-Oldest Issue", 35 | "user": { 36 | "login": "example", 37 | "id": 57936, 38 | "avatar_url": "https://avatars.githubusercontent.com/u/57936?v=3", 39 | "url": "https://github.com/example" 40 | }, 41 | "labels": [ 42 | { 43 | "name": "administrative", 44 | "color": "C0C0C0" 45 | }, 46 | { 47 | "name": "triage", 48 | "color": "ffffff" 49 | } 50 | ], 51 | "state": "open", 52 | "locked": false, 53 | "assignee": null, 54 | "milestone": null, 55 | "comments": 2, 56 | "createdAt": "1999-01-02T00:00:00Z", 57 | "updatedAt": "1999-01-02T20:30:40Z", 58 | "closedAt": null, 59 | "body": "Argle bargle example issue #9002.\r\nWith extra old-ness, but less-so than issue #9001." 60 | }, 61 | 62 | { 63 | "url": "https://github.com/rubygems/rubygems/issues/9003", 64 | "id": 150951839, 65 | "number": 9001, 66 | "title": "Fake Third-Oldest Issue", 67 | "user": { 68 | "login": "example", 69 | "id": 57936, 70 | "avatar_url": "https://avatars.githubusercontent.com/u/57936?v=3", 71 | "url": "https://api.github.com/users/example", 72 | "url": "https://github.com/example" 73 | }, 74 | "labels": [ 75 | { 76 | "name": "triage", 77 | "color": "ffffff" 78 | } 79 | ], 80 | "state": "open", 81 | "locked": false, 82 | "assignee": null, 83 | "milestone": null, 84 | "comments": 2, 85 | "createdAt": "2010-01-01T00:00:00Z", 86 | "updatedAt": "2010-01-01T20:30:40Z", 87 | "closedAt": null, 88 | "body": "Argle bargle example issue #9003." 89 | }, 90 | 91 | { 92 | "url": "https://github.com/rubygems/rubygems/issues/9004", 93 | "id": 150951839, 94 | "number": 9001, 95 | "title": "Fake Fourth-Oldest Issue", 96 | "user": { 97 | "login": "example", 98 | "id": 57936, 99 | "avatar_url": "https://avatars.githubusercontent.com/u/57936?v=3", 100 | "url": "https://github.com/example" 101 | }, 102 | "labels": [ 103 | { 104 | "name": "administrative", 105 | "color": "C0C0C0" 106 | } 107 | ], 108 | "state": "open", 109 | "locked": false, 110 | "assignee": null, 111 | "milestone": null, 112 | "comments": 2, 113 | "createdAt": "2016-01-01T00:00:00Z", 114 | "updatedAt": "2016-01-01T20:30:40Z", 115 | "closedAt": null, 116 | "body": "Argle bargle example issue #9004.\r\nIt's magic." 117 | } 118 | ] 119 | -------------------------------------------------------------------------------- /spec/data/how-is-date-interval-example-repository-report.html: -------------------------------------------------------------------------------- 1 | 2 |

    How is how-is/example-repository?

    3 |

    From Aug 01, 2016 through Dec 01, 2016, how-is/example-repository gained 1 new commit, contributed by 1 author. There were 2 additions and 0 deletions across 1 file.

    4 | 5 |

    New Contributors

    6 |

    There was 1 new contributor during this report period.

    7 | 8 |
      9 |
    • Ellen Marie Dash
    • 10 |
    11 | 12 |

    Pull Requests

    13 |

    A total of 1 pull request was opened during this period.

    14 | 15 | 20 | 21 | 22 |

    Issues

    23 |

    A total of 3 issues were opened during this period.

    24 | 25 |
      26 |
    • Average age: approximately 2 months and 3 weeks.
    • 27 |
    • Oldest issue was opened on Aug 07, 2016.
    • 28 |
    • Newest issue was opened on Aug 07, 2016.
    • 29 |
    30 | 31 | 32 | -------------------------------------------------------------------------------- /spec/data/how-is-date-interval-example-repository-report.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "How is how-is/example-repository?", 3 | "repository": "how-is/example-repository", 4 | "contributions_summary": "From Aug 01, 2016 through Dec 01, 2016, how-is/example-repository gained 1 new commit, contributed by 1 author. There were 2 additions and 0 deletions across 1 file.", 5 | "new_contributors": "

    There was 1 new contributor during this report period.

    \n\n
      \n
    • Ellen Marie Dash
    • \n
    ", 6 | "issues_summary": "

    A total of 3 issues were opened during this period.

    \n\n
      \n
    • Average age: approximately 2 months and 3 weeks.
    • \n
    • Oldest issue was opened on Aug 07, 2016.
    • \n
    • Newest issue was opened on Aug 07, 2016.
    • \n
    \n", 7 | "pulls_summary": "

    A total of 1 pull request was opened during this period.

    \n\n\n", 8 | "issues": [ 9 | { 10 | "number": 1, 11 | "createdAt": "2016-08-07T03:53:26Z", 12 | "closedAt": null, 13 | "updatedAt": "2016-08-07T03:53:49Z", 14 | "state": "OPEN", 15 | "title": "An issue", 16 | "url": "https://github.com/how-is/example-repository/issues/1", 17 | "labels": [ 18 | { 19 | "name": "bug" 20 | } 21 | ], 22 | "date": "Aug 07, 2016" 23 | }, 24 | { 25 | "number": 2, 26 | "createdAt": "2016-08-07T03:54:13Z", 27 | "closedAt": null, 28 | "updatedAt": "2016-08-07T03:54:13Z", 29 | "state": "OPEN", 30 | "title": "Another issue", 31 | "url": "https://github.com/how-is/example-repository/issues/2", 32 | "labels": [ 33 | { 34 | "name": "bug" 35 | }, 36 | { 37 | "name": "question" 38 | } 39 | ] 40 | }, 41 | { 42 | "number": 3, 43 | "createdAt": "2016-08-07T03:54:33Z", 44 | "closedAt": null, 45 | "updatedAt": "2016-08-07T03:54:33Z", 46 | "state": "OPEN", 47 | "title": "Yet another issue!", 48 | "url": "https://github.com/how-is/example-repository/issues/3", 49 | "labels": [ 50 | 51 | ], 52 | "date": "Aug 07, 2016" 53 | } 54 | ], 55 | "pulls": [ 56 | { 57 | "number": 1, 58 | "createdAt": "2016-08-07T03:53:26Z", 59 | "closedAt": null, 60 | "updatedAt": "2016-08-07T03:53:49Z", 61 | "state": "OPEN", 62 | "title": "An issue", 63 | "url": "https://github.com/how-is/example-repository/issues/1", 64 | "labels": [ 65 | { 66 | "name": "bug" 67 | } 68 | ], 69 | "date": "Aug 07, 2016" 70 | }, 71 | { 72 | "number": 2, 73 | "createdAt": "2016-08-07T03:54:13Z", 74 | "closedAt": null, 75 | "updatedAt": "2016-08-07T03:54:13Z", 76 | "state": "OPEN", 77 | "title": "Another issue", 78 | "url": "https://github.com/how-is/example-repository/issues/2", 79 | "labels": [ 80 | { 81 | "name": "bug" 82 | }, 83 | { 84 | "name": "question" 85 | } 86 | ] 87 | }, 88 | { 89 | "number": 3, 90 | "createdAt": "2016-08-07T03:54:33Z", 91 | "closedAt": null, 92 | "updatedAt": "2016-08-07T03:54:33Z", 93 | "state": "OPEN", 94 | "title": "Yet another issue!", 95 | "url": "https://github.com/how-is/example-repository/issues/3", 96 | "labels": [ 97 | 98 | ], 99 | "date": "Aug 07, 2016" 100 | } 101 | ], 102 | "average_issue_age": "approximately 2 months and 3 weeks", 103 | "average_pull_age": "approximately 2 months and 3 weeks", 104 | "oldest_issue_link": "https://github.com/how-is/example-repository/issues/1", 105 | "oldest_issue_date": "2016-08-07T03:53:26Z", 106 | "newest_issue_link": "https://github.com/how-is/example-repository/issues/3", 107 | "newest_issue_date": "2016-08-07T03:54:33Z", 108 | "newest_pull_link": "https://github.com/how-is/example-repository/pull/4", 109 | "newest_pull_date": "2016-08-07T04:02:59Z", 110 | "oldest_pull_link": "https://github.com/how-is/example-repository/pull/4", 111 | "oldest_pull_date": "2016-08-07T04:02:59Z", 112 | "travis_builds": [ 113 | 114 | ], 115 | "appveyor_builds": [ 116 | 117 | ], 118 | "date": "2016-12-01", 119 | "frontmatter": "" 120 | } -------------------------------------------------------------------------------- /spec/data/how-is-example-empty-repository-report.html: -------------------------------------------------------------------------------- 1 | 2 |

    How is how-is/example-empty-repository?

    3 |

    From Dec 01, 2016 through Jan 01, 2017, how-is/example-empty-repository gained 1 new commit, contributed by 1 author. There were 2 additions and 0 deletions across 1 file.

    4 | 5 |

    New Contributors

    6 |

    There was 1 new contributor during this report period.

    7 | 8 |
      9 |
    • Ellen Marie Dash
    • 10 |
    11 | 12 |

    Pull Requests

    13 |

    A total of 0 pull requests were opened during this period.

    14 | 15 |

    Issues

    16 |

    A total of 0 issues were opened during this period.

    17 | 18 | -------------------------------------------------------------------------------- /spec/data/how-is-example-repository-report.html: -------------------------------------------------------------------------------- 1 | 2 |

    How is how-is/example-repository?

    3 |

    From Aug 01, 2016 through Sep 01, 2016, how-is/example-repository gained 1 new commit, contributed by 1 author. There were 2 additions and 0 deletions across 1 file.

    4 | 5 |

    New Contributors

    6 |

    There was 1 new contributor during this report period.

    7 | 8 |
      9 |
    • Ellen Marie Dash
    • 10 |
    11 | 12 |

    Pull Requests

    13 |

    A total of 1 pull request was opened during this period.

    14 | 15 | 20 | 21 | 22 |

    Issues

    23 |

    A total of 3 issues were opened during this period.

    24 | 25 |
      26 |
    • Average age: approximately 2 months and 3 weeks.
    • 27 |
    • Oldest issue was opened on Aug 07, 2016.
    • 28 |
    • Newest issue was opened on Aug 07, 2016.
    • 29 |
    30 | 31 | 32 | -------------------------------------------------------------------------------- /spec/data/how-is-example-repository-report.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "How is how-is/example-repository?", 3 | "repository": "how-is/example-repository", 4 | "contributions_summary": "From Aug 01, 2016 through Sep 01, 2016, how-is/example-repository gained 1 new commit, contributed by 1 author. There were 2 additions and 0 deletions across 1 file.", 5 | "new_contributors": "

    There was 1 new contributor during this report period.

    \n\n
      \n
    • Ellen Marie Dash
    • \n
    ", 6 | "issues_summary": "

    A total of 3 issues were opened during this period.

    \n\n
      \n
    • Average age: approximately 2 months and 3 weeks.
    • \n
    • Oldest issue was opened on Aug 07, 2016.
    • \n
    • Newest issue was opened on Aug 07, 2016.
    • \n
    \n", 7 | "pulls_summary": "

    A total of 1 pull request was opened during this period.

    \n\n\n", 8 | "issues": [ 9 | { 10 | "number": 1, 11 | "createdAt": "2016-08-07T03:53:26Z", 12 | "closedAt": null, 13 | "updatedAt": "2016-08-07T03:53:49Z", 14 | "state": "OPEN", 15 | "title": "An issue", 16 | "url": "https://github.com/how-is/example-repository/issues/1", 17 | "labels": [ 18 | { 19 | "name": "bug" 20 | } 21 | ], 22 | "date": "Aug 07, 2016" 23 | }, 24 | { 25 | "number": 2, 26 | "createdAt": "2016-08-07T03:54:13Z", 27 | "closedAt": null, 28 | "updatedAt": "2016-08-07T03:54:13Z", 29 | "state": "OPEN", 30 | "title": "Another issue", 31 | "url": "https://github.com/how-is/example-repository/issues/2", 32 | "labels": [ 33 | { 34 | "name": "bug" 35 | }, 36 | { 37 | "name": "question" 38 | } 39 | ] 40 | }, 41 | { 42 | "number": 3, 43 | "createdAt": "2016-08-07T03:54:33Z", 44 | "closedAt": null, 45 | "updatedAt": "2016-08-07T03:54:33Z", 46 | "state": "OPEN", 47 | "title": "Yet another issue!", 48 | "url": "https://github.com/how-is/example-repository/issues/3", 49 | "labels": [ 50 | 51 | ], 52 | "date": "Aug 07, 2016" 53 | } 54 | ], 55 | "pulls": [ 56 | { 57 | "number": 1, 58 | "createdAt": "2016-08-07T03:53:26Z", 59 | "closedAt": null, 60 | "updatedAt": "2016-08-07T03:53:49Z", 61 | "state": "OPEN", 62 | "title": "An issue", 63 | "url": "https://github.com/how-is/example-repository/issues/1", 64 | "labels": [ 65 | { 66 | "name": "bug" 67 | } 68 | ], 69 | "date": "Aug 07, 2016" 70 | }, 71 | { 72 | "number": 2, 73 | "createdAt": "2016-08-07T03:54:13Z", 74 | "closedAt": null, 75 | "updatedAt": "2016-08-07T03:54:13Z", 76 | "state": "OPEN", 77 | "title": "Another issue", 78 | "url": "https://github.com/how-is/example-repository/issues/2", 79 | "labels": [ 80 | { 81 | "name": "bug" 82 | }, 83 | { 84 | "name": "question" 85 | } 86 | ] 87 | }, 88 | { 89 | "number": 3, 90 | "createdAt": "2016-08-07T03:54:33Z", 91 | "closedAt": null, 92 | "updatedAt": "2016-08-07T03:54:33Z", 93 | "state": "OPEN", 94 | "title": "Yet another issue!", 95 | "url": "https://github.com/how-is/example-repository/issues/3", 96 | "labels": [ 97 | 98 | ], 99 | "date": "Aug 07, 2016" 100 | } 101 | ], 102 | "average_issue_age": "approximately 2 months and 3 weeks", 103 | "average_pull_age": "approximately 2 months and 3 weeks", 104 | "oldest_issue_link": "https://github.com/how-is/example-repository/issues/1", 105 | "oldest_issue_date": "2016-08-07T03:53:26Z", 106 | "newest_issue_link": "https://github.com/how-is/example-repository/issues/3", 107 | "newest_issue_date": "2016-08-07T03:54:33Z", 108 | "newest_pull_link": "https://github.com/how-is/example-repository/pull/4", 109 | "newest_pull_date": "2016-08-07T04:02:59Z", 110 | "oldest_pull_link": "https://github.com/how-is/example-repository/pull/4", 111 | "oldest_pull_date": "2016-08-07T04:02:59Z", 112 | "travis_builds": [ 113 | 114 | ], 115 | "appveyor_builds": [ 116 | 117 | ], 118 | "date": "2016-09-01", 119 | "frontmatter": "" 120 | } 121 | -------------------------------------------------------------------------------- /spec/data/how_is.yml: -------------------------------------------------------------------------------- 1 | repository: rubygems/rubygems 2 | reports: 3 | html: 4 | directory: . 5 | frontmatter: 6 | title: "%{repository} report" 7 | layout: default 8 | filename: "report.html" 9 | json: 10 | directory: . 11 | filename: "report.json" 12 | -------------------------------------------------------------------------------- /spec/data/how_is/cli_spec/example_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "issues_url": "https://github.com/rubygems/rubygems/issues", 3 | "pulls_url": "https://github.com/rubygems/rubygems/pulls", 4 | "repository": "rubygems/rubygems", 5 | "number_of_issues": 140, 6 | "number_of_pulls": 33, 7 | "issues_with_label": { 8 | "status: triage": { 9 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+triage%22", 10 | "total": 35 11 | }, 12 | "type: feature request": { 13 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+feature+request%22", 14 | "total": 36 15 | }, 16 | "contribution: unclaimed": { 17 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22contribution%3A+unclaimed%22", 18 | "total": 3 19 | }, 20 | "status: ready": { 21 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+ready%22", 22 | "total": 30 23 | }, 24 | "platform: windows": { 25 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22platform%3A+windows%22", 26 | "total": 10 27 | }, 28 | "type: bug report": { 29 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug+report%22", 30 | "total": 25 31 | }, 32 | "contribution: small": { 33 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22contribution%3A+small%22", 34 | "total": 1 35 | }, 36 | "type: cleanup": { 37 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+cleanup%22", 38 | "total": 1 39 | }, 40 | "status: user feedback required": { 41 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+user+feedback+required%22", 42 | "total": 1 43 | }, 44 | "type: major bump": { 45 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+major+bump%22", 46 | "total": 7 47 | }, 48 | "category: install": { 49 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22category%3A+install%22", 50 | "total": 22 51 | }, 52 | "category: command": { 53 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22category%3A+command%22", 54 | "total": 3 55 | }, 56 | "platform: java": { 57 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22platform%3A+java%22", 58 | "total": 1 59 | }, 60 | "category: #gem or #require": { 61 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22category%3A+%23gem+or+%23require%22", 62 | "total": 3 63 | }, 64 | "platform: osx": { 65 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22platform%3A+osx%22", 66 | "total": 1 67 | }, 68 | "type: documentation": { 69 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+documentation%22", 70 | "total": 2 71 | }, 72 | "category: gem spec": { 73 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22category%3A+gem+spec%22", 74 | "total": 7 75 | }, 76 | "category: other": { 77 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22category%3A+other%22", 78 | "total": 9 79 | }, 80 | "performance": { 81 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22performance%22", 82 | "total": 4 83 | }, 84 | "category: API": { 85 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22category%3A+API%22", 86 | "total": 2 87 | }, 88 | "platform: linux": { 89 | "link": "https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22platform%3A+linux%22", 90 | "total": 1 91 | } 92 | }, 93 | "issues_with_no_label": { 94 | "link": null, 95 | "total": 16 96 | }, 97 | "average_issue_age": "approximately 1 year and 6 months", 98 | "average_pull_age": "approximately 1 year and 3 months", 99 | "oldest_issue": { 100 | "html_url": "https://github.com/rubygems/rubygems/issues/108", 101 | "number": 108, 102 | "date": "2011-06-14T23:08:09+00:00" 103 | }, 104 | "oldest_pull": { 105 | "html_url": "https://github.com/rubygems/rubygems/pull/649", 106 | "number": 649, 107 | "date": "2013-09-16T15:04:07+00:00" 108 | }, 109 | "newest_issue": { 110 | "html_url": "https://github.com/rubygems/rubygems/issues/1838", 111 | "number": 1838, 112 | "date": "2017-02-10T13:19:49+00:00" 113 | }, 114 | "newest_pull": { 115 | "html_url": "https://github.com/rubygems/rubygems/pull/1834", 116 | "number": 1834, 117 | "date": "2017-02-05T08:38:44+00:00" 118 | }, 119 | "pulse": "Excluding merges, 6 authors\n have pushed\n 39 commits to master and\n 42 commits\n to all branches.\n On master, 36 files\n have changed and there have been\n \n 486 additions and\n 117 deletions." 120 | } -------------------------------------------------------------------------------- /spec/data/how_is/cli_spec/how_is.yml: -------------------------------------------------------------------------------- 1 | default_reports: 2 | html: 3 | directory: output 4 | frontmatter: 5 | title: "%{repository} report" 6 | layout: default 7 | filename: "report.html" 8 | json: 9 | directory: output 10 | filename: "report.json" 11 | 12 | repositories: 13 | - repository: rubygems/rubygems 14 | - repository: how-is/how_is 15 | reports: 16 | html: 17 | filename: "%{sanitized_repository}-report.html" 18 | frontmatter: 19 | layout: alt_layout 20 | json: 21 | filename: "%{sanitized_repository}-report.json" 22 | -------------------------------------------------------------------------------- /spec/data/how_is/cli_spec/output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duckinator/inq/28885b9ddcea683e13c7deaf4f6ce0b91041195d/spec/data/how_is/cli_spec/output/.gitkeep -------------------------------------------------------------------------------- /spec/data/how_is_spec/generate_report--generates-a-correct-JSON-report.json: -------------------------------------------------------------------------------- 1 | {"issues_url":"https://github.com/rubygems/rubygems/issues","pulls_url":"https://github.com/rubygems/rubygems/pulls","repository":"rubygems/rubygems","number_of_issues":30,"number_of_pulls":30,"issues_with_label":{"triage":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22triage%22","total":7},"bug report":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22bug+report%22","total":9},"feedback":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22feedback%22","total":4},"osx":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22osx%22","total":1},"bug fix":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22bug+fix%22","total":4},"category - install":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22category+-+install%22","total":5},"feature implementation":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+implementation%22","total":2},"major bump":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22major+bump%22","total":2},"windows":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22windows%22","total":1},"feature request":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22","total":1},"cleanup":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22cleanup%22","total":1},"accepted":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22accepted%22","total":2},"category - #gem or #require":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22category+-+%23gem+or+%23require%22","total":1},"ready for work":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22ready+for+work%22","total":1},"question":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22question%22","total":3},"administrative":{"link":"https://github.com/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3A%22administrative%22","total":1}},"issues_with_no_label":{"link":null,"total":0},"average_issue_age":"approximately 3 months and 3 weeks","average_pull_age":"approximately 11 months and 2 weeks","oldest_issue":{"html_url":"https://github.com/rubygems/rubygems/pull/1468","number":1468,"date":"2016-01-31T21:28:02+00:00"},"oldest_pull":{"html_url":"https://github.com/rubygems/rubygems/pull/649","number":649,"date":"2013-09-16T15:04:07+00:00"}} 2 | -------------------------------------------------------------------------------- /spec/inq/builds_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/sources/ci/travis" 4 | require "inq/sources/ci/appveyor" 5 | 6 | describe Inq::Sources::CI::Travis do 7 | subject do 8 | cache = cache("2018-03-01", "2017-04-15") 9 | described_class.new(config("duckinator/inq"), "2018-03-01", "2017-04-15", cache) 10 | end 11 | 12 | describe "#builds" do 13 | around(:example) do |example| 14 | load_test_env { example.run } 15 | 16 | # This will fail without VCR if the cache isn't working 17 | expect(subject.builds).to be_a(Array) 18 | end 19 | 20 | it "returns an Array" do 21 | VCR.use_cassette("how-is-how-is-travis-api-repos-builds") do 22 | expect(subject.builds).to be_a(Array) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/inq/cacheable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/cacheable" 4 | 5 | describe Inq::Cacheable do 6 | around(:example) do |example| 7 | load_test_env do 8 | example.run 9 | end 10 | end 11 | 12 | let(:marshal_cache_config) do 13 | config = 14 | Inq::Config.new 15 | .load_defaults 16 | .load({"repository" => "duckinator/inq", "cache" => {"type" => "marshal"}}) 17 | end 18 | 19 | CACHE_HASH = {} 20 | let(:self_cache_config) do 21 | config = 22 | Inq::Config.new 23 | .load_defaults 24 | .load({ 25 | "repository" => "duckinator/inq", 26 | "cache" => { 27 | "type" => "self", 28 | "cache_mechanism" => ->(cache_key, _, block) { 29 | CACHE_HASH[cache_key] ||= block.call 30 | } 31 | } 32 | }) 33 | end 34 | 35 | let(:no_cache_config) do 36 | config = 37 | Inq::Config.new 38 | .load_defaults 39 | .load({"repository" => "duckinator/inq"}) 40 | end 41 | 42 | describe "#cached" do 43 | it "caches with Marshal" do 44 | marshal_cache = described_class.new(marshal_cache_config, "2018-03-01", "2017-04-15") 45 | x = 0 46 | 2.times do 47 | marshal_cache.cached("marshal_cache") do 48 | x += 1 49 | end 50 | end 51 | expect(x).to eq(1) 52 | end 53 | 54 | it "caches with self" do 55 | self_cache = described_class.new(self_cache_config, "2018-03-01", "2017-04-15") 56 | 57 | x = 0 58 | 2.times do 59 | self_cache.cached("self_cache") do 60 | x += 1 61 | end 62 | end 63 | expect(x).to eq(1) 64 | 65 | digest = Digest::SHA1.hexdigest(self_cache_config.to_json) 66 | key = "2018-03-01/2017-04-15/self_cache/#{digest}" 67 | expect(CACHE_HASH[key]).to eq(1) 68 | end 69 | 70 | it "caches only when opted in" do 71 | no_cache = described_class.new(no_cache_config, "2018-03-01", "2017-04-15") 72 | x = 0 73 | 2.times do 74 | no_cache.cached("no_cache") do 75 | x += 1 # Will be run twice 76 | end 77 | end 78 | expect(x).to eq(2) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/inq/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "inq/cli" 5 | 6 | CLI_EXAMPLE_REPORT_FILE = File.expand_path("../data/how_is/cli_spec/example_report.json", __dir__) 7 | 8 | describe Inq::CLI do 9 | subject { Inq::CLI } 10 | 11 | context "#parse" do 12 | it "takes an Array of args and returns a Inq::CLI object" do 13 | cli = subject.parse(["--version"]) 14 | 15 | expect(cli.help_text).to be_a(String) 16 | expect(cli.options).to be_a(Hash) 17 | expect(cli.options[:version]).to eq(true) 18 | end 19 | 20 | it "raises an OptionParser::MissingArgument if no date is specified" do 21 | expect { 22 | subject.parse([]) 23 | }.to raise_error(OptionParser::MissingArgument, /--date/) 24 | end 25 | 26 | 27 | it "raises an OptionParser::MissingArgument if start date is required but not specified" do 28 | expect { 29 | subject.parse(["--start-date"]) 30 | }.to raise_error(OptionParser::MissingArgument, /--start-date/) 31 | end 32 | 33 | it "raises an OptionParser::MissingArgument if end date is required but not specified" do 34 | expect { 35 | subject.parse(["--end-date"]) 36 | }.to raise_error(OptionParser::MissingArgument, /--end-date/) 37 | end 38 | 39 | it "raises an OptionParser::MissingArgument if a repository is required but not specified" do 40 | expect { 41 | subject.parse(["--date", "2018-01-01"]) 42 | }.to raise_error(OptionParser::MissingArgument, /--repository/) 43 | end 44 | 45 | it "returns an error if you specify an invalid format" do 46 | expect { 47 | subject.parse([ 48 | "--output", "invalid.format", 49 | "--date", "2018-01-01", 50 | "--repossitory", "how-is/example-repository" 51 | ]) 52 | }.to raise_error(OptionParser::InvalidArgument, /--output/) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/inq/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "inq" 5 | require "inq/config" 6 | require "json" 7 | 8 | describe Inq::Config do 9 | context "#load" do 10 | it "normalizes configs correctly" do 11 | hash1 = { 12 | 'a' => '1', 13 | 'b' => ['b1'], 14 | } 15 | hash2 = { 16 | 'c' => '2', 17 | 'b' => ['b2'], 18 | } 19 | hash3 = { 20 | 'a' => '-1', 21 | 'b' => ['b3'], 22 | } 23 | 24 | config = subject.load(hash1, hash2, hash3) 25 | expect(config).to eq({ 26 | 'a' => '-1', 27 | 'b' => ['b1', 'b2', 'b3'], 28 | 'c' => '2', 29 | }) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/inq/contributions_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "inq/config" 4 | require "inq/sources/github/contributions" 5 | 6 | describe Inq::Sources::Github::Contributions, skip: env_vars_hidden? do 7 | let(:contributions) { 8 | cache = cache("2017-08-01", "2017-09-01") 9 | described_class.new(config("how-is/example-repository"), "2017-08-01", "2017-09-01", cache) 10 | } 11 | 12 | context "#contributors" do 13 | it "lists the contributors hash keyed by email" do 14 | VCR.use_cassette("how_is_contributions_all_contributors") do 15 | expect(contributions.contributors.keys).to( 16 | match_array(["me@duckie.co", "fake@duckinator.net"]) 17 | ) 18 | end 19 | 20 | # This will fail without VCR if the cache isn't working 21 | expect(contributions.contributors.keys).to( 22 | match_array(["me@duckie.co", "fake@duckinator.net"]) 23 | ) 24 | end 25 | end 26 | 27 | # NOTE: This implicitly tests #contributors. (Because it doesn't work right 28 | # if #contributors doesn't.) 29 | context "#new_contributors" do 30 | it "lists only the new contributors during the month starting with the specified date" do 31 | VCR.use_cassette("how_is_contributions_new_contributors") do 32 | expect(contributions.new_contributors.keys).to( 33 | match_array(["fake@duckinator.net"]) 34 | ) 35 | end 36 | 37 | # This will fail without VCR if the cache isn't working 38 | expect(contributions.new_contributors.keys).to( 39 | match_array(["fake@duckinator.net"]) 40 | ) 41 | end 42 | end 43 | 44 | # NOTE: This implicitly tests #commit, since it includes #commit's output. 45 | context "#commits" do 46 | it "lists all commits during the month starting with the specified date" do 47 | VCR.use_cassette("how_is_contributions_commits") do 48 | commit_shas = contributions.commits.map(&:commit).map { |commit| 49 | commit["tree"]["sha"] 50 | } 51 | expect(commit_shas).to eq([ 52 | "6911e0637822f44b83f04f47821adab56fdbc0b9", 53 | "8286e548e330cfe01efcf7189f4df1fa53e777a7", 54 | ]) 55 | end 56 | 57 | # This will fail without VCR if the cache isn't working 58 | contributions.commits 59 | end 60 | end 61 | 62 | context "#changes" do 63 | it "returns a hash containing all of the changed stats and files" do 64 | VCR.use_cassette("how_is_contributions_changes") do 65 | results_hash = contributions.changes 66 | stats = results_hash["stats"] 67 | files = results_hash["files"] 68 | 69 | expect(stats).to eq({ 70 | "total" => 3, 71 | "additions" => 2, 72 | "deletions" => 1, 73 | }) 74 | expect(files).to eq(["README.md"]) 75 | end 76 | 77 | # This will fail without VCR if the cache isn't working 78 | contributions.changes 79 | end 80 | end 81 | 82 | context "#changed_files" do 83 | it "returns a hash containing all of the changed files" do 84 | VCR.use_cassette("how_is_contributions_changed_files") do 85 | expect(contributions.changed_files).to eq(["README.md"]) 86 | end 87 | end 88 | end 89 | 90 | context "#additions_count" do 91 | it "returns the number of additions during the specified period" do 92 | VCR.use_cassette("how_is_contributions_additions_count") do 93 | expect(contributions.additions_count).to eq(2) 94 | end 95 | end 96 | end 97 | 98 | context "#deletions_count" do 99 | it "returns the number of deletions during the specified period" do 100 | VCR.use_cassette("how_is_contributions_deletions_count") do 101 | expect(contributions.deletions_count).to eq(1) 102 | end 103 | end 104 | end 105 | 106 | context "#compare_url" do 107 | it "returns the GitHub URL that shows information about the specified period" do 108 | VCR.use_cassette("how_is_contributions_compare_url") do 109 | # rubocop:disable Metrics/LineLength 110 | expect(contributions.compare_url).to eq("https://github.com/how-is/example-repository/compare/master@%7B2017-08-01%7D...master@%7B2017-09-01%7D") 111 | # rubocop:enable Metrics/LineLength 112 | end 113 | end 114 | end 115 | 116 | context "#default_branch" do 117 | it "fetches default branch" do 118 | VCR.use_cassette("how_is_contributions_default_branch") do 119 | expect(contributions.default_branch).to eq "master" 120 | end 121 | end 122 | end 123 | 124 | context "#to_html" do 125 | it "generate an HTML summary of the changes" do 126 | VCR.use_cassette("how_is_contributions_summary") do 127 | # rubocop:disable Metrics/LineLength 128 | summary = 'From Aug 01, 2017 through Sep 01, 2017, how-is/example-repository gained 2 new commits, contributed by 2 authors. There were 2 additions and 1 deletion across 1 file.' 129 | # rubocop:enable Metrics/LineLength 130 | expect(contributions.to_html).to eq summary 131 | end 132 | end 133 | 134 | it "lets you change the beginning text" do 135 | VCR.use_cassette("how_is_contributions_summary_2") do 136 | expect(contributions.to_html(start_text: "woof")).to start_with( 137 | "woof, how-is/example-repository" 138 | ) 139 | end 140 | end 141 | end 142 | 143 | context "#pretty_date" do 144 | it "formats the date correctly" do 145 | date = "2017-01-02" 146 | expect(contributions.send(:pretty_date, date)).to eq("Jan 02, 2017") 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /spec/inq/github_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "inq/sources/github_helpers" 5 | require "json" 6 | 7 | describe Inq::Sources::GithubHelpers do 8 | let(:issues) { JSON.parse(open(File.expand_path("../data/issues.json", __dir__)).read) } 9 | let(:pulls) { JSON.parse(open(File.expand_path("../data/pulls.json", __dir__)).read) } 10 | 11 | let(:fake_issues) { JSON.parse(open(File.expand_path("../data/fake/issues.json", __dir__)).read) } 12 | # let(:fake_pulls) { JSON.parse(open(File.expand_path('../data/pulls.json', __dir__)).read) } 13 | 14 | subject { Class.new { extend Inq::Sources::GithubHelpers } } 15 | 16 | context "#average_age_for" do 17 | it "returns the average age for the provided issues or pulls" do 18 | actual = nil 19 | 20 | date = DateTime.parse("2016-07-07") 21 | Timecop.freeze(date) do 22 | actual = subject.average_age_for(fake_issues) 23 | end 24 | 25 | expected = "approximately 10 years and 6 months" 26 | 27 | expect(actual).to eq(expected) 28 | end 29 | end 30 | 31 | context "#oldest_for" do 32 | it "returns the oldest item for the provided issues or pulls" do 33 | actual = subject.oldest_for(fake_issues) 34 | 35 | expect(actual["createdAt"]).to eq("1999-01-01T00:00:00Z") 36 | expect(actual["url"]).to eq("https://github.com/rubygems/rubygems/issues/9001") 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/inq/integration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | INQ_EXE = File.expand_path("../../exe/inq", __dir__) 6 | 7 | describe "Integration Tests" do 8 | context "--help and -h flags" do 9 | it "outputs usage information" do 10 | ["--help", "-h"].each do |flag| 11 | stub_const("ARGV", [flag]) 12 | 13 | expect { 14 | load INQ_EXE 15 | }.to output(/Usage: inq /).to_stdout 16 | end 17 | end 18 | end 19 | 20 | context "--version and -v flags" do 21 | it "outputs the version number" do 22 | ["--version", "-v"].each do |flag| 23 | stub_const("ARGV", [flag]) 24 | 25 | expect { 26 | load INQ_EXE 27 | }.to output("#{Inq::VERSION}\n").to_stdout 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/inq_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "inq" 5 | require "inq/config" 6 | require "inq/frontmatter" 7 | require "json" 8 | require "open3" 9 | require "timecop" 10 | require "tmpdir" 11 | require "yaml" 12 | 13 | HOW_IS_CONFIG_FILE = File.expand_path("./data/how_is/cli_spec/how_is.yml", __dir__) 14 | 15 | HOW_IS_EXAMPLE_REPOSITORY_JSON_REPORT = File.expand_path("./data/how-is-example-repository-report.json", __dir__) 16 | HOW_IS_EXAMPLE_REPOSITORY_HTML_REPORT = File.expand_path("./data/how-is-example-repository-report.html", __dir__) 17 | 18 | INQ_DATE_INTERVAL_EXAMPLE_REPOSITORY_HTML_REPORT = File.expand_path("./data/how-is-date-interval-example-repository-report.html", __dir__) 19 | INQ_DATE_INTERVAL_EXAMPLE_REPOSITORY_JSON_REPORT = File.expand_path("./data/how-is-date-interval-example-repository-report.json", __dir__) 20 | 21 | 22 | HOW_IS_EXAMPLE_EMPTY_REPOSITORY_HTML_REPORT = 23 | File.expand_path("./data/how-is-example-empty-repository-report.html", __dir__) 24 | 25 | JEKYLL_HEADER = 26 | <<~HEADER 27 | --- 28 | title: rubygems/rubygems report 29 | layout: default 30 | --- 31 | HEADER 32 | 33 | describe Inq do 34 | context "#from_config" do 35 | let(:config) { 36 | Inq::Config.new 37 | .load_defaults 38 | .load_files(HOW_IS_CONFIG_FILE) 39 | } 40 | 41 | it "generates valid report files", skip: env_vars_hidden? do 42 | Dir.mktmpdir { |dir| 43 | Dir.chdir(dir) { 44 | reports = nil 45 | 46 | VCR.use_cassette("how-is-with-config-file") do 47 | expect { 48 | reports = Inq.from_config(config, "2017-08-01").to_h 49 | }.to_not raise_error 50 | # This instance, and all other instances in this file, of 51 | # ".to_not output.to_stderr" are replaced with 52 | # ".to_not raise_error" due to a deprecation warning. 53 | #}.to_not output.to_stderr 54 | end 55 | 56 | html_report = reports["output/report.html"] 57 | json_report = reports["output/report.json"] 58 | 59 | expect(html_report).to include(JEKYLL_HEADER) 60 | expect { 61 | JSON.parse(json_report) 62 | }.to_not raise_error 63 | } 64 | } 65 | end 66 | 67 | it "adds correct frontmatter", skip: env_vars_hidden? do 68 | reports = nil 69 | 70 | VCR.use_cassette("how-is-from-config-frontmatter") do 71 | reports = Inq.from_config(config, "2017-08-01").to_h 72 | end 73 | 74 | actual_html = reports["output/report.html"] 75 | # NOTE: If JSON reports get frontmatter applied in the future, 76 | # uncomment the following line (+ the one at the end of 77 | # this block) to test it. 78 | # actual_json = reports["output/report.json"] 79 | 80 | expected_frontmatter = <<~FRONTMATTER 81 | --- 82 | title: rubygems/rubygems report 83 | layout: default 84 | --- 85 | 86 | FRONTMATTER 87 | 88 | expect(actual_html).to start_with(expected_frontmatter) 89 | # NOTE: If JSON reports get frontmatter applied in the future, 90 | # uncomment the following line (+ the one earlier in 91 | # this block) to test it. 92 | # expect(actual_json).to start_with(expected_frontmatter) 93 | end 94 | end 95 | 96 | context "HTML report for how-is/example-repository" do 97 | # TODO: Stop using Timecop once reports are no longer time-dependent. 98 | 99 | before do 100 | # 2016-11-01 00:00:00 UTC. 101 | # TODO: Stop pretending to always be in UTC. 102 | date = DateTime.parse("2016-11-01").new_offset(0) 103 | Timecop.freeze(date) 104 | end 105 | 106 | after do 107 | Timecop.return 108 | end 109 | 110 | it "generates a valid report", skip: env_vars_hidden? do 111 | expected_html = File.open(HOW_IS_EXAMPLE_REPOSITORY_HTML_REPORT).read.chomp 112 | expected_json = File.open(HOW_IS_EXAMPLE_REPOSITORY_JSON_REPORT).read.chomp 113 | actual_report = nil 114 | 115 | VCR.use_cassette("how-is-example-repository") do 116 | expect { 117 | actual_report = Inq.new("how-is/example-repository", "2016-09-01") 118 | }.to_not raise_error 119 | #}.to_not output.to_stderr 120 | 121 | expect(actual_report.to_html_partial).to eq(expected_html) 122 | expect(actual_report.to_json).to eq(expected_json) 123 | end 124 | end 125 | 126 | context "when a date interval is passed" do 127 | it "generates a valid report", skip: env_vars_hidden? do 128 | expected_html = File.open(INQ_DATE_INTERVAL_EXAMPLE_REPOSITORY_HTML_REPORT).read.chomp 129 | expected_json = File.open(INQ_DATE_INTERVAL_EXAMPLE_REPOSITORY_JSON_REPORT).read.chomp 130 | actual_report = nil 131 | 132 | VCR.use_cassette("how-is-example-repository-with-date-interval") do 133 | expect { 134 | actual_report = Inq.new("how-is/example-repository", "2016-08-01", "2016-12-01") 135 | }.to_not raise_error 136 | 137 | expect(actual_report.to_html_partial).to eq(expected_html) 138 | expect(actual_report.to_json).to eq(expected_json) 139 | end 140 | end 141 | end 142 | end 143 | 144 | context "HTML report for repository with no PRs or issues" do 145 | it "generates a valid report file", skip: env_vars_hidden? do 146 | expected = File.open(HOW_IS_EXAMPLE_EMPTY_REPOSITORY_HTML_REPORT).read.chomp 147 | actual = nil 148 | 149 | VCR.use_cassette("how-is-example-empty-repository") do 150 | expect { 151 | actual = Inq.new("how-is/example-empty-repository", "2017-01-01").to_html_partial 152 | }.to_not raise_error 153 | #}.to_not output.to_stderr 154 | end 155 | 156 | expect(actual).to eq(expected) 157 | end 158 | end 159 | 160 | context "#generate_frontmatter" do 161 | it "works with frontmatter parameter using String keys, report_data using String keys" do 162 | actual = nil 163 | expected = nil 164 | 165 | VCR.use_cassette("how-is-example-repository") do 166 | actual = Inq::Frontmatter.generate({"foo" => "bar %{baz}"}, {"baz" => "asdf"}) 167 | expected = "---\nfoo: bar asdf\n---\n\n" 168 | end 169 | 170 | expect(actual).to eq(expected) 171 | end 172 | 173 | it "works with frontmatter parameter using Symbol keys, report_data using Symbol keys" do 174 | actual = nil 175 | expected = nil 176 | 177 | VCR.use_cassette("how-is-example-repository") do 178 | actual = Inq::Frontmatter.generate({:foo => "bar %{baz}"}, {:baz => "asdf"}) 179 | expected = "---\nfoo: bar asdf\n---\n\n" 180 | end 181 | 182 | expect(actual).to eq(expected) 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /spec/rspec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.order = "random" 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 4 | require "inq" 5 | require "inq/text" 6 | require "timecop" 7 | require File.expand_path("./vcr_helper.rb", __dir__) 8 | require File.expand_path("./rspec_helper.rb", __dir__) 9 | 10 | Inq::Text.show_default_output = false 11 | 12 | def env_vars_hidden? 13 | travis_pr = ENV["TRAVIS_PULL_REQUEST"] 14 | !travis_pr.nil? && (travis_pr != "false") 15 | end 16 | 17 | def config(repo) 18 | Inq::Config.new 19 | .load_defaults 20 | .load({ 21 | "repository" => repo, 22 | "cache" => { "type" => "marshal" } 23 | }) 24 | end 25 | 26 | def cache(start_date, end_date) 27 | Inq::Cacheable.new(config("duckinator/inq"), start_date, end_date) 28 | end 29 | 30 | def load_test_env 31 | token = ENV["HOWIS_GITHUB_TOKEN"] 32 | username = ENV["HOWIS_GITHUB_USERNAME"] 33 | begin 34 | ENV["HOWIS_GITHUB_TOKEN"] = "blah" 35 | ENV["HOWIS_GITHUB_USERNAME"] = "who" 36 | yield 37 | ensure 38 | ENV["HOWIS_GITHUB_TOKEN"] = token 39 | ENV["HOWIS_GITHUB_USERNAME"] = username 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/vcr_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "vcr" 4 | 5 | VCR.configure do |config| 6 | # To have VCR re-record all casettes, do `VCR_MODE=rec bundle exec rake test` 7 | vcr_mode = ENV["VCR_MODE"] =~ /rec/i ? :all : :once 8 | 9 | config.configure_rspec_metadata! 10 | 11 | config.default_cassette_options = { 12 | record: vcr_mode, 13 | match_requests_on: [:method, :uri, :body] 14 | } 15 | 16 | # Filter out the values of 'Authorization:' headers. 17 | config.filter_sensitive_data("") { |interaction| 18 | auth_headers = interaction.request.headers["Authorization"] 19 | if auth_headers.is_a?(Array) && auth_headers.length > 0 20 | auth_headers.first 21 | else 22 | "" # idk if you can just return nil here without weirdness? 23 | end 24 | } 25 | 26 | config.filter_sensitive_data("") { |interaction| 27 | auth_headers = interaction.request.headers["Bearer-Token"] 28 | if auth_headers.is_a?(Array) && auth_headers.length > 0 29 | auth_headers.first 30 | else 31 | "" # idk if you can just return nil here without weirdness? 32 | end 33 | } 34 | 35 | 36 | config.allow_http_connections_when_no_cassette = false 37 | config.cassette_library_dir = "fixtures/vcr_cassettes" 38 | config.hook_into :webmock 39 | end 40 | --------------------------------------------------------------------------------