├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── ci.yml │ └── push.yml ├── .gitignore ├── .kodiak.toml ├── .overcommit.yml ├── .prettierignore ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── bundleup.gemspec ├── demo.gif ├── exe └── bundleup ├── lib ├── bundleup.rb └── bundleup │ ├── backup.rb │ ├── cli.rb │ ├── colors.rb │ ├── commands.rb │ ├── gemfile.rb │ ├── logger.rb │ ├── pin_report.rb │ ├── report.rb │ ├── shell.rb │ ├── update_report.rb │ ├── version.rb │ └── version_spec.rb └── test ├── bundleup ├── backup_test.rb ├── cli_test.rb ├── commands_test.rb ├── gemfile_test.rb ├── logger_test.rb ├── pin_report_test.rb ├── update_report_test.rb └── version_spec_test.rb ├── bundleup_test.rb ├── fixtures ├── Gemfile.sample ├── list.out ├── outdated-2.1.out ├── outdated-2.2.out └── project │ ├── Gemfile │ └── Gemfile.lock ├── support ├── mocha.rb ├── output_helpers.rb └── rg.rb └── test_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "16:00" 8 | timezone: America/Los_Angeles 9 | open-pull-requests-limit: 10 10 | labels: 11 | - "🏠 Housekeeping" 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: monthly 16 | time: "16:00" 17 | timezone: America/Los_Angeles 18 | open-pull-requests-limit: 10 19 | labels: 20 | - "🏠 Housekeeping" 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | categories: 4 | - title: "⚠️ Breaking Changes" 5 | label: "⚠️ Breaking" 6 | - title: "✨ New Features" 7 | label: "✨ Feature" 8 | - title: "🐛 Bug Fixes" 9 | label: "🐛 Bug Fix" 10 | - title: "📚 Documentation" 11 | label: "📚 Docs" 12 | - title: "🏠 Housekeeping" 13 | label: "🏠 Housekeeping" 14 | version-resolver: 15 | minor: 16 | labels: 17 | - "⚠️ Breaking" 18 | - "✨ Feature" 19 | default: patch 20 | change-template: "- $TITLE (#$NUMBER) @$AUTHOR" 21 | no-changes-template: "- No changes" 22 | template: | 23 | $CHANGES 24 | 25 | **Full Changelog:** https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | rubocop: 9 | name: "Rubocop" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: "ruby" 16 | bundler-cache: true 17 | - run: bundle exec rubocop 18 | test: 19 | name: "Test / Ruby ${{ matrix.ruby }}" 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | ruby: ["3.1", "3.2", "3.3", "3.4", "head"] 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | bundler-cache: true 30 | - run: bundle exec rake test 31 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v6 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /.kodiak.toml: -------------------------------------------------------------------------------- 1 | # .kodiak.toml 2 | # Minimal config. version is the only required field. 3 | version = 1 4 | 5 | [merge.automerge_dependencies] 6 | # auto merge all PRs opened by "dependabot" that are "minor" or "patch" version upgrades. "major" version upgrades will be ignored. 7 | versions = ["minor", "patch"] 8 | usernames = ["dependabot"] 9 | 10 | # if using `update.always`, add dependabot to `update.ignore_usernames` to allow 11 | # dependabot to update and close stale dependency upgrades. 12 | [update] 13 | ignored_usernames = ["dependabot"] 14 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Overcommit hooks run automatically on certain git operations, like "git commit". 2 | # For a complete list of options that you can use to customize hooks, see: 3 | # https://github.com/sds/overcommit 4 | 5 | gemfile: false 6 | verify_signatures: false 7 | 8 | PreCommit: 9 | BundleCheck: 10 | enabled: true 11 | 12 | FixMe: 13 | enabled: true 14 | keywords: ["FIXME"] 15 | exclude: 16 | - .overcommit.yml 17 | 18 | LocalPathsInGemfile: 19 | enabled: true 20 | 21 | RuboCop: 22 | enabled: true 23 | required_executable: bundle 24 | command: ["bundle", "exec", "rubocop"] 25 | on_warn: fail 26 | 27 | YamlSyntax: 28 | enabled: true 29 | 30 | PostCheckout: 31 | ALL: 32 | quiet: true 33 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /CODE_OF_CONDUCT.md 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-minitest 3 | - rubocop-packaging 4 | - rubocop-performance 5 | - rubocop-rake 6 | 7 | AllCops: 8 | NewCops: enable 9 | TargetRubyVersion: 3.1 10 | Exclude: 11 | - "vendor/**/*" 12 | 13 | Layout/FirstArrayElementIndentation: 14 | EnforcedStyle: consistent 15 | 16 | Layout/FirstArrayElementLineBreak: 17 | Enabled: true 18 | 19 | Layout/FirstHashElementLineBreak: 20 | Enabled: true 21 | 22 | Layout/FirstMethodArgumentLineBreak: 23 | Enabled: true 24 | 25 | Layout/HashAlignment: 26 | EnforcedColonStyle: 27 | - table 28 | - key 29 | EnforcedHashRocketStyle: 30 | - table 31 | - key 32 | 33 | Layout/MultilineArrayLineBreaks: 34 | Enabled: true 35 | 36 | Layout/MultilineHashKeyLineBreaks: 37 | Enabled: true 38 | 39 | Layout/MultilineMethodArgumentLineBreaks: 40 | Enabled: true 41 | 42 | Layout/MultilineMethodCallIndentation: 43 | EnforcedStyle: indented 44 | 45 | Layout/SpaceAroundEqualsInParameterDefault: 46 | EnforcedStyle: no_space 47 | 48 | Metrics/AbcSize: 49 | Exclude: 50 | - "test/**/*" 51 | 52 | Metrics/BlockLength: 53 | CountAsOne: ["heredoc"] 54 | 55 | Metrics/MethodLength: 56 | Exclude: 57 | - "test/**/*" 58 | 59 | Metrics/ClassLength: 60 | Exclude: 61 | - "test/**/*" 62 | 63 | Minitest/MultipleAssertions: 64 | Max: 6 65 | 66 | Minitest/EmptyLineBeforeAssertionMethods: 67 | Enabled: false 68 | 69 | Naming/MemoizedInstanceVariableName: 70 | Enabled: false 71 | 72 | Naming/VariableNumber: 73 | Enabled: false 74 | 75 | Rake/Desc: 76 | Enabled: false 77 | 78 | Style/BarePercentLiterals: 79 | EnforcedStyle: percent_q 80 | 81 | Style/ClassAndModuleChildren: 82 | Enabled: false 83 | 84 | Style/Documentation: 85 | Enabled: false 86 | 87 | Style/DoubleNegation: 88 | Enabled: false 89 | 90 | Style/FetchEnvVar: 91 | Enabled: false 92 | 93 | Style/FrozenStringLiteralComment: 94 | Enabled: false 95 | 96 | Style/StringLiterals: 97 | EnforcedStyle: double_quotes 98 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Release notes for this project are kept here: https://github.com/mattbrictson/bundleup/releases 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | - Trolling, insulting or derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | - Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at bundleup@mattbrictson.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | Thank you for your input and support! Here are some guidelines to follow when contributing. 4 | 5 | ## 🐛 Bug reports 6 | 7 | - Explain the troubleshooting steps you've already tried 8 | - Use GitHub-flavored Markdown, especially code fences ``` to format logs 9 | - Include reproduction steps or code for a failing test case if you can 10 | 11 | ## ✨ Feature requests 12 | 13 | Ideas for new bundleup features are appreciated! 14 | 15 | - Show examples of how the feature would work 16 | - Explain your motivation for requesting the feature 17 | - Would it be useful for the majority of bundleup users? 18 | - Is it a breaking change? 19 | 20 | ## ⤴️ Pull requests 21 | 22 | > Protip: If you have a big change in mind, it is a good idea to open an issue first to propose the idea and get some initial feedback. 23 | 24 | ### Working on code 25 | 26 | - Run `bin/setup` to install dependencies 27 | - `bin/console` opens an irb console if you need a REPL to try things out 28 | - `bundle exec bundleup` will run your working copy of bundleup 29 | - `rake install` will install your working copy of bundleup globally (so you can test it in other projects) 30 | - Make sure to run `rake` to run all tests and RuboCop checks prior to opening a PR 31 | 32 | ### PR guidelines 33 | 34 | - Give the PR a concise and descriptive title that completes this sentence: _If this PR is merged, it will [TITLE]_ 35 | - If the PR fixes an open issue, link to the issue in the description 36 | - Provide a description that ideally answers these questions: 37 | - Why is this change needed? What problem(s) does it solve? 38 | - Were there alternative solutions that you considered? 39 | - How has it been tested? 40 | - Is it a breaking change? 41 | - Does the documentation need to be updated? 42 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | gem "minitest", "~> 5.0" 5 | gem "minitest-rg", "~> 5.3" 6 | gem "mocha", "~> 2.0" 7 | gem "rake", "~> 13.0" 8 | gem "rubocop", "1.75.8" 9 | gem "rubocop-minitest", "0.38.1" 10 | gem "rubocop-packaging", "0.6.0" 11 | gem "rubocop-performance", "1.25.0" 12 | gem "rubocop-rake", "0.7.1" 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Matt Brictson 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 | 📣 **I am no longer actively developing this project.** I am focusing my attention on building [mattbrictson/bundle_update_interactive](https://github.com/mattbrictson/bundle_update_interactive) instead. It has many of the same features as `bundleup`, so please check it out! In the meantime, `bundleup` will continue to receive occasional maintenance, but likely no new capabilities. 2 | 3 | --- 4 | 5 | # bundleup 6 | 7 | [![Gem Version](https://img.shields.io/gem/v/bundleup)](https://rubygems.org/gems/bundleup) 8 | [![Gem Downloads](https://img.shields.io/gem/dt/bundleup)](https://www.ruby-toolbox.com/projects/bundleup) 9 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/mattbrictson/bundleup/ci.yml)](https://github.com/mattbrictson/bundleup/actions/workflows/ci.yml) 10 | 11 | **Run `bundleup` on a Ruby project containing a Gemfile to see what gem dependencies need updating.** It is a friendlier command-line interface to [Bundler’s][bundler] `bundle update` and `bundle outdated`. 12 | 13 | You might like bundleup because it: 14 | 15 | - shows you exactly what gems will be updated lets you decide whether to proceed 16 | - uses color to call your attention to important gem updates (based on [Semver][]) 17 | - lets you know when a version "pin" in your Gemfile is preventing an update 18 | - relies on standard Bundler output and does not patch code or use Bundler internals 19 | 20 | Here it is in action: 21 | 22 | Sample output 23 | 24 | ## Requirements 25 | 26 | - Bundler 1.16 or later 27 | - Ruby 3.1 or later 28 | 29 | ## Usage 30 | 31 | Assuming you have a Ruby environment, all you need to do is install the bundleup gem: 32 | 33 | ``` 34 | gem install bundleup 35 | ``` 36 | 37 | Now, within a Ruby project you can run the bundleup command (the project needs to have a Gemfile and Gemfile.lock): 38 | 39 | ``` 40 | bundleup 41 | ``` 42 | 43 | That’s it! 44 | 45 | Protip: Any extra command-line arguments will be passed along to `bundle update`. For example: 46 | 47 | ``` 48 | # Only upgrade development gems 49 | bundleup --group=development 50 | ``` 51 | 52 | ### Experimental: `--update-gemfile` 53 | 54 | > 💡 This is an experimental feature that may be removed or changed in future versions. 55 | 56 | Normally bundleup only makes changes to your Gemfile.lock. It honors the version restrictions ("pins") in your Gemfile and will not update your Gemfile.lock to have versions that are not allowed. However with the `--update-gemfile` flag, bundleup can update the version pins in your Gemfile as well. Consider the following Gemfile: 57 | 58 | ```ruby 59 | gem 'sidekiq', '~> 5.2' 60 | gem 'rubocop', '0.89.0' 61 | ``` 62 | 63 | Normally running `bundleup` will report that these gems are pinned and therefore cannot be updated to the latest versions. However, if you pass the `--update-gemfile` option like this: 64 | 65 | ``` 66 | $ bundleup --update-gemfile 67 | ``` 68 | 69 | Now bundleup will automatically edit your Gemfile pins as needed to bring those gems up to date. For example, bundleup would change the Gemfile to look like this: 70 | 71 | ```ruby 72 | gem 'sidekiq', '~> 6.1' 73 | gem 'rubocop', '0.90.0' 74 | ``` 75 | 76 | Note that `--update-gemfile` will _not_ modify Gemfile entries that contain a comment, like this: 77 | 78 | ```ruby 79 | gem 'sidekiq', '~> 5.2' # our monkey patch doesn't work on 6.0+ 80 | ``` 81 | 82 | ### Integrate bundlup in a script 83 | 84 | `Bundleup::CLI` gives you two methods to track updated and pinned gems: 85 | 86 | ```ruby 87 | cli = Bundleup::CLI.new([]) 88 | cli.run 89 | cli.updated_gems 90 | # > ["rubocop"] 91 | 92 | cli.pinned_gems 93 | # > ["rake"] 94 | ``` 95 | 96 | You can then easily create scripts to perform any actions such as running tests, running rubocop, or commit changes. 97 | 98 | ```ruby 99 | cli = Bundleup::CLI.new([]) 100 | cli.run 101 | 102 | if cli.updated_gems.any? 103 | system "bundle exec rspec" 104 | elsif cli.updated_gems.include?("rubocop") 105 | system "bundle exec rubocop" 106 | end 107 | 108 | if cli.updated_gems.any? 109 | system "git commit -m \"Update gems dependencies\" -- Gemfile.lock" 110 | end 111 | ``` 112 | 113 | ## How bundleup works 114 | 115 | bundleup starts by making a backup copy of your Gemfile.lock. Next it runs `bundle check` (and `bundle install` if any gems are missing in your local environment), `bundle list`, then `bundle update` and `bundle list` again to find what gems versions are being used before and after Bundler does its updating magic. (Since gems are actually being installed into your Ruby environment during these steps, the process may take a few moments to complete, especially if gems with native extensions need to be compiled.) 116 | 117 | Finally, bundleup runs `bundle outdated` to see the gems that were _not_ updated due to Gemfile restrictions. 118 | 119 | After displaying its findings, bundleup gives you the option of keeping the changes. If you answer "no", bundleup will restore your original Gemfile.lock from its backup, leaving your project untouched. 120 | 121 | ## Roadmap 122 | 123 | bundleup is in maintenance mode; no new features are planned. 124 | 125 | ## Contributing 126 | 127 | Code contributions are welcome! Read [CONTRIBUTING.md](CONTRIBUTING.md) to get started. 128 | 129 | [bundler]: http://bundler.io 130 | [semver]: http://semver.org 131 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | require "rubocop/rake_task" 4 | 5 | Rake::TestTask.new(:test) do |t| 6 | t.libs << "test" 7 | t.libs << "lib" 8 | t.test_files = FileList["test/**/*_test.rb"] 9 | end 10 | 11 | RuboCop::RakeTask.new 12 | 13 | task default: %i[test rubocop] 14 | 15 | # == "rake release" enhancements ============================================== 16 | 17 | Rake::Task["release"].enhance do 18 | puts "Don't forget to publish the release on GitHub!" 19 | system "open https://github.com/mattbrictson/bundleup/releases" 20 | end 21 | 22 | task :disable_overcommit do 23 | ENV["OVERCOMMIT_DISABLE"] = "1" 24 | end 25 | 26 | Rake::Task[:build].enhance [:disable_overcommit] 27 | 28 | task :verify_gemspec_files do 29 | git_files = `git ls-files -z`.split("\x0") 30 | gemspec_files = Gem::Specification.load("bundleup.gemspec").files.sort 31 | ignored_by_git = gemspec_files - git_files 32 | next if ignored_by_git.empty? 33 | 34 | raise <<~ERROR 35 | 36 | The `spec.files` specified in bundleup.gemspec include the following files 37 | that are being ignored by git. Did you forget to add them to the repo? If 38 | not, you may need to delete these files or modify the gemspec to ensure 39 | that they are not included in the gem by mistake: 40 | 41 | #{ignored_by_git.join("\n").gsub(/^/, ' ')} 42 | 43 | ERROR 44 | end 45 | 46 | Rake::Task[:build].enhance [:verify_gemspec_files] 47 | 48 | # == "rake bump" tasks ======================================================== 49 | 50 | task bump: %w[bump:bundler bump:ruby bump:year] 51 | 52 | namespace :bump do 53 | task :bundler do 54 | sh "bundle update --bundler" 55 | end 56 | 57 | task :ruby do 58 | replace_in_file "bundleup.gemspec", /ruby_version = .*">= (.*)"/ => RubyVersions.lowest 59 | replace_in_file ".rubocop.yml", /TargetRubyVersion: (.*)/ => RubyVersions.lowest 60 | replace_in_file ".github/workflows/ci.yml", /ruby: (\[.+\])/ => RubyVersions.all.inspect 61 | replace_in_file "README.md", /Ruby (\d\.\d) or later/i => RubyVersions.lowest 62 | end 63 | 64 | task :year do 65 | replace_in_file "LICENSE.txt", /\(c\) (\d+)/ => Date.today.year.to_s 66 | end 67 | end 68 | 69 | require "date" 70 | require "open-uri" 71 | require "yaml" 72 | 73 | def replace_in_file(path, replacements) 74 | contents = File.read(path) 75 | orig_contents = contents.dup 76 | replacements.each do |regexp, text| 77 | raise "Can't find #{regexp} in #{path}" unless regexp.match?(contents) 78 | 79 | contents.gsub!(regexp) do |match| 80 | match[regexp, 1] = text 81 | match 82 | end 83 | end 84 | File.write(path, contents) if contents != orig_contents 85 | end 86 | 87 | module RubyVersions 88 | class << self 89 | def lowest 90 | all.first 91 | end 92 | 93 | def all 94 | patches = versions.values_at(:stable, :security_maintenance).compact.flatten 95 | sorted_minor_versions = patches.map { |p| p[/\d+\.\d+/] }.sort_by(&:to_f) 96 | [*sorted_minor_versions, "head"] 97 | end 98 | 99 | private 100 | 101 | def versions 102 | @_versions ||= begin 103 | yaml = URI.open("https://raw.githubusercontent.com/ruby/www.ruby-lang.org/HEAD/_data/downloads.yml") 104 | YAML.safe_load(yaml, symbolize_names: true) 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "bundleup" 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/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | which overcommit > /dev/null 2>&1 && overcommit --install 7 | bundle install 8 | -------------------------------------------------------------------------------- /bundleup.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/bundleup/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "bundleup" 5 | spec.version = Bundleup::VERSION 6 | spec.authors = ["Matt Brictson"] 7 | spec.email = ["bundleup@mattbrictson.com"] 8 | 9 | spec.summary = "A friendlier command-line interface for Bundler’s `update` and `outdated` commands." 10 | spec.description = 11 | "Use `bundleup` whenever you want to update the locked Gemfile dependencies of a Ruby project. It shows exactly " \ 12 | "what gems will be updated with color output that calls attention to significant semver changes. Bundleup will " \ 13 | 'also let you know when a version "pin" in your Gemfile is preventing an update. Bundleup is a standalone tool ' \ 14 | "that leverages standard Bundler output and does not patch code or use Bundler internals." 15 | 16 | spec.homepage = "https://github.com/mattbrictson/bundleup" 17 | spec.license = "MIT" 18 | spec.required_ruby_version = ">= 3.1" 19 | 20 | spec.metadata = { 21 | "bug_tracker_uri" => "https://github.com/mattbrictson/bundleup/issues", 22 | "changelog_uri" => "https://github.com/mattbrictson/bundleup/releases", 23 | "source_code_uri" => "https://github.com/mattbrictson/bundleup", 24 | "homepage_uri" => spec.homepage, 25 | "rubygems_mfa_required" => "true" 26 | } 27 | 28 | # Specify which files should be added to the gem when it is released. 29 | spec.files = Dir.glob(%w[LICENSE.txt README.md {exe,lib}/**/*]).reject { |f| File.directory?(f) } 30 | spec.bindir = "exe" 31 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 32 | spec.require_paths = ["lib"] 33 | end 34 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/bundleup/f2458fba06a4110121ac56a0ae4bae44e3682a56/demo.gif -------------------------------------------------------------------------------- /exe/bundleup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundleup" 4 | 5 | begin 6 | Bundleup::CLI.new(ARGV).run 7 | rescue Bundleup::CLI::Error => e 8 | Bundleup.logger.error(e.message) 9 | exit(false) 10 | end 11 | -------------------------------------------------------------------------------- /lib/bundleup.rb: -------------------------------------------------------------------------------- 1 | require "bundleup/version" 2 | require "bundleup/backup" 3 | require "bundleup/colors" 4 | require "bundleup/cli" 5 | require "bundleup/commands" 6 | require "bundleup/gemfile" 7 | require "bundleup/logger" 8 | require "bundleup/report" 9 | require "bundleup/shell" 10 | require "bundleup/pin_report" 11 | require "bundleup/update_report" 12 | require "bundleup/version_spec" 13 | 14 | module Bundleup 15 | class << self 16 | attr_accessor :commands, :logger, :shell 17 | end 18 | end 19 | 20 | Bundleup.commands = Bundleup::Commands.new 21 | Bundleup.logger = Bundleup::Logger.new 22 | Bundleup.shell = Bundleup::Shell.new 23 | -------------------------------------------------------------------------------- /lib/bundleup/backup.rb: -------------------------------------------------------------------------------- 1 | module Bundleup 2 | class Backup 3 | def self.restore_on_error(*paths) 4 | backup = new(*paths) 5 | begin 6 | yield(backup) 7 | rescue StandardError, Interrupt 8 | backup.restore 9 | raise 10 | end 11 | end 12 | 13 | def initialize(*paths) 14 | @original_contents = paths.each_with_object({}) do |path, hash| 15 | hash[path] = File.read(path) 16 | end 17 | end 18 | 19 | def restore 20 | original_contents.each do |path, contents| 21 | File.write(path, contents) 22 | end 23 | end 24 | 25 | private 26 | 27 | attr_reader :original_contents 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/bundleup/cli.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | module Bundleup 4 | class CLI 5 | Error = Class.new(StandardError) 6 | 7 | include Colors 8 | extend Forwardable 9 | def_delegators :Bundleup, :commands, :logger 10 | 11 | attr_reader :updated_gems, :pinned_gems 12 | 13 | def initialize(args) 14 | @args = args.dup 15 | @update_gemfile = @args.delete("--update-gemfile") 16 | @updated_gems = [] 17 | @pinned_gems = [] 18 | end 19 | 20 | def run # rubocop:disable Metrics/AbcSize, Metrics/MethodLength 21 | print_usage && return if args.intersect?(%w[-h --help]) 22 | 23 | @updated_gems = [] 24 | @pinned_gems = [] 25 | 26 | assert_gemfile_and_lock_exist! 27 | 28 | logger.puts "Please wait a moment while I upgrade your Gemfile.lock..." 29 | Backup.restore_on_error("Gemfile", "Gemfile.lock") do |backup| 30 | perform_analysis_and_optionally_bump_gemfile_versions do |update_report, pin_report| 31 | if update_report.empty? 32 | logger.ok "Nothing to update." 33 | logger.puts "\n#{pin_report}" unless pin_report.empty? 34 | break 35 | end 36 | 37 | logger.puts 38 | logger.puts update_report 39 | logger.puts pin_report unless pin_report.empty? 40 | 41 | if logger.confirm?("Do you want to apply these changes?") 42 | @updated_gems = update_report.updated_gems 43 | @pinned_gems = pin_report.pinned_gems 44 | 45 | logger.ok "Done!" 46 | else 47 | backup.restore 48 | logger.puts "Your original Gemfile.lock has been restored." 49 | end 50 | end 51 | end 52 | end 53 | 54 | private 55 | 56 | attr_reader :args 57 | 58 | def print_usage # rubocop:disable Metrics/AbcSize, Metrics/MethodLength 59 | logger.puts(<<~USAGE.gsub(/^/, " ")) 60 | 61 | Usage: #{green('bundleup')} #{yellow('[GEMS...] [OPTIONS]')} 62 | 63 | Use #{blue('bundleup')} in place of #{blue('bundle update')} to interactively update your project 64 | Gemfile.lock to the latest gem versions. Bundleup will show what gems will 65 | be updated, color-code them based on semver, and ask you to confirm the 66 | updates before finalizing them. For example: 67 | 68 | The following gems will be updated: 69 | 70 | #{yellow('bundler-audit 0.6.1 → 0.7.0.1')} 71 | i18n 1.8.2 → 1.8.5 72 | #{red('json 2.2.0 → (removed)')} 73 | parser 2.7.1.1 → 2.7.1.4 74 | #{red('rails e063bef → 57a4ead')} 75 | #{blue('rubocop-ast (new) → 0.3.0')} 76 | #{red('thor 0.20.3 → 1.0.1')} 77 | #{yellow('zeitwerk 2.3.0 → 2.4.0')} 78 | 79 | #{yellow('Do you want to apply these changes [Yn]?')} 80 | 81 | Bundleup will also let you know if there are gems that can't be updated 82 | because they are pinned in the Gemfile. Any relevant comments from the 83 | Gemfile will also be included, explaining the pins: 84 | 85 | Note that the following gems are being held back: 86 | 87 | rake 12.3.3 → 13.0.1 : pinned at ~> 12.0 #{gray('# Not ready for 13 yet')} 88 | rubocop 0.89.0 → 0.89.1 : pinned at = 0.89.0 89 | 90 | You may optionally specify one or more #{yellow('GEMS')} or pass #{yellow('OPTIONS')} to bundleup; 91 | these will be passed through to bundler. See #{blue('bundle update --help')} for the 92 | full list of the options that bundler supports. 93 | 94 | Finally, bundleup also supports an experimental #{yellow('--update-gemfile')} option. 95 | If specified, bundleup with modify the version restrictions specified in 96 | your Gemfile so that it can install the latest version of each gem. For 97 | instance, if your Gemfile specifies #{yellow('gem "sidekiq", "~> 5.2"')} but an update 98 | to version 6.1.2 is available, bundleup will modify the Gemfile entry to 99 | be #{yellow('gem "sidekiq", "~> 6.1"')} in order to permit the update. 100 | 101 | Examples: 102 | 103 | #{gray('# Update all gems')} 104 | #{blue('$ bundleup')} 105 | 106 | #{gray('# Only update gems in the development group')} 107 | #{blue('$ bundleup --group=development')} 108 | 109 | #{gray('# Only update the rake gem')} 110 | #{blue('$ bundleup rake')} 111 | 112 | #{gray('# Experimental: modify Gemfile to allow the latest gem versions')} 113 | #{blue('$ bundleup --update-gemfile')} 114 | 115 | USAGE 116 | true 117 | end 118 | 119 | def assert_gemfile_and_lock_exist! 120 | return if File.exist?("Gemfile") && File.exist?("Gemfile.lock") 121 | 122 | raise Error, "Gemfile and Gemfile.lock must both be present." 123 | end 124 | 125 | def perform_analysis_and_optionally_bump_gemfile_versions # rubocop:disable Metrics/AbcSize, Metrics/MethodLength 126 | gemfile = Gemfile.new 127 | lockfile_backup = Backup.new("Gemfile.lock") 128 | update_report, pin_report, _, outdated_gems = perform_analysis 129 | updatable_gems = gemfile.gem_pins_without_comments.slice(*outdated_gems.keys) 130 | 131 | if updatable_gems.any? && @update_gemfile 132 | lockfile_backup.restore 133 | orig_gemfile = Gemfile.new 134 | gemfile.relax_gem_pins!(updatable_gems.keys) 135 | update_report, pin_report, new_versions, = perform_analysis 136 | orig_gemfile.shift_gem_pins!(new_versions.slice(*updatable_gems.keys)) 137 | commands.install 138 | end 139 | 140 | logger.clear_line 141 | yield(update_report, pin_report) 142 | end 143 | 144 | def perform_analysis # rubocop:disable Metrics/AbcSize 145 | gem_comments = Gemfile.new.gem_comments 146 | commands.check? || commands.install 147 | old_versions = commands.list 148 | commands.update(args) 149 | new_versions = commands.list 150 | outdated_gems = commands.outdated 151 | 152 | update_report = UpdateReport.new(old_versions:, new_versions:) 153 | pin_report = PinReport.new(gem_versions: new_versions, outdated_gems:, gem_comments:) 154 | 155 | [update_report, pin_report, new_versions, outdated_gems] 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/bundleup/colors.rb: -------------------------------------------------------------------------------- 1 | module Bundleup 2 | module Colors 3 | ANSI_CODES = { 4 | red: 31, 5 | green: 32, 6 | yellow: 33, 7 | blue: 34, 8 | gray: 90 9 | }.freeze 10 | private_constant :ANSI_CODES 11 | 12 | class << self 13 | attr_writer :enabled 14 | 15 | def enabled? 16 | return @enabled if defined?(@enabled) 17 | 18 | @enabled = determine_color_support 19 | end 20 | 21 | private 22 | 23 | def determine_color_support 24 | if ENV["CLICOLOR_FORCE"] == "1" 25 | true 26 | elsif ENV["TERM"] == "dumb" 27 | false 28 | else 29 | tty?($stdout) && tty?($stderr) 30 | end 31 | end 32 | 33 | def tty?(io) 34 | io.respond_to?(:tty?) && io.tty? 35 | end 36 | end 37 | 38 | module_function 39 | 40 | def plain(str) 41 | str 42 | end 43 | 44 | def strip(str) 45 | str.gsub(/\033\[[0-9;]*m/, "") 46 | end 47 | 48 | ANSI_CODES.each do |name, code| 49 | define_method(name) do |str| 50 | return str if str.to_s.empty? 51 | 52 | Colors.enabled? ? "\e[0;#{code};49m#{str}\e[0m" : str 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/bundleup/commands.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | module Bundleup 4 | class Commands 5 | GEMFILE_ENTRY_REGEXP = /\* (\S+) \((\S+)(?: (\S+))?\)/ 6 | OUTDATED_2_1_REGEXP = /\* (\S+) \(newest (\S+),.* requested (.*)\)/ 7 | OUTDATED_2_2_REGEXP = /^(\S+)\s\s+\S+\s\s+(\d\S+)\s\s+(\S.*?)(?:$|\s\s)/ 8 | 9 | extend Forwardable 10 | def_delegators :Bundleup, :shell 11 | 12 | def check? 13 | shell.run?(%w[bundle check]) 14 | end 15 | 16 | def install 17 | shell.run(%w[bundle install]) 18 | end 19 | 20 | def list 21 | output = shell.capture(%w[bundle list]) 22 | output.scan(GEMFILE_ENTRY_REGEXP).each_with_object({}) do |(name, ver, sha), gems| 23 | gems[name] = sha || ver 24 | end 25 | end 26 | 27 | def outdated 28 | output = shell.capture(%w[bundle outdated], raise_on_error: false) 29 | expr = output.match?(/^Gem\s+Current\s+Latest/) ? OUTDATED_2_2_REGEXP : OUTDATED_2_1_REGEXP 30 | 31 | output.scan(expr).each_with_object({}) do |(name, newest, pin), gems| 32 | gems[name] = { newest:, pin: } 33 | end 34 | end 35 | 36 | def update(args=[]) 37 | shell.run(%w[bundle update] + args) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/bundleup/gemfile.rb: -------------------------------------------------------------------------------- 1 | module Bundleup 2 | class Gemfile 3 | attr_reader :path 4 | 5 | def initialize(path="Gemfile") 6 | @path = path 7 | @contents = File.read(path) 8 | end 9 | 10 | def gem_comments 11 | gem_names.each_with_object({}) do |gem_name, hash| 12 | comment = inline_comment(gem_name) || prefix_comment(gem_name) 13 | hash[gem_name] = comment unless comment.nil? 14 | end 15 | end 16 | 17 | def gem_pins_without_comments 18 | (gem_names - gem_comments.keys).each_with_object({}) do |gem_name, hash| 19 | next unless (match = gem_declaration_with_pinned_version_re(gem_name).match(contents)) 20 | 21 | version = match[1] 22 | hash[gem_name] = VersionSpec.parse(version) 23 | end 24 | end 25 | 26 | def relax_gem_pins!(gem_names) 27 | gem_names.each do |gem_name| 28 | rewrite_gem_version!(gem_name, &:relax) 29 | end 30 | end 31 | 32 | def shift_gem_pins!(new_gem_versions) 33 | new_gem_versions.each do |gem_name, new_version| 34 | rewrite_gem_version!(gem_name) { |version_spec| version_spec.shift(new_version) } 35 | end 36 | end 37 | 38 | private 39 | 40 | def rewrite_gem_version!(gem_name) 41 | found = contents.sub!(gem_declaration_with_pinned_version_re(gem_name)) do |match| 42 | version = Regexp.last_match[1] 43 | match[Regexp.last_match.regexp, 1] = yield(VersionSpec.parse(version)).to_s 44 | match 45 | end 46 | raise "Can't rewrite version for #{gem_name}; it does not have a pin" unless found 47 | 48 | File.write(path, contents) 49 | end 50 | 51 | attr_reader :contents 52 | 53 | def gem_names 54 | contents.scan(/^\s*gem\s+["'](.+?)["']/).flatten.uniq 55 | end 56 | 57 | def inline_comment(gem_name) 58 | contents[/#{gem_declaration_re(gem_name)}.*(#\s*\S+.*)/, 1] 59 | end 60 | 61 | def prefix_comment(gem_name) 62 | contents[/^\s*(#\s*\S+.*)\n#{gem_declaration_re(gem_name)}/, 1] 63 | end 64 | 65 | def gem_declaration_re(gem_name) 66 | /^\s*gem\s+["']#{Regexp.escape(gem_name)}["']/ 67 | end 68 | 69 | def gem_declaration_with_pinned_version_re(gem_name) 70 | /#{gem_declaration_re(gem_name)},\s*["']([^'"]+)["']\s*$/ 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/bundleup/logger.rb: -------------------------------------------------------------------------------- 1 | require "io/console" 2 | 3 | module Bundleup 4 | class Logger 5 | extend Forwardable 6 | def_delegators :@stdout, :print, :puts, :tty? 7 | def_delegators :@stdin, :gets 8 | 9 | def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) 10 | @stdin = stdin 11 | @stdout = stdout 12 | @stderr = stderr 13 | @spinner = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].cycle 14 | end 15 | 16 | def ok(message) 17 | puts Colors.green("✔ #{message}") 18 | end 19 | 20 | def error(message) 21 | stderr.puts Colors.red("ERROR: #{message}") 22 | end 23 | 24 | def attention(message) 25 | puts Colors.yellow(message) 26 | end 27 | 28 | def confirm?(question) 29 | print Colors.yellow(question.sub(/\??\z/, " [Yn]? ")) 30 | gets =~ /^($|y)/i 31 | end 32 | 33 | def clear_line 34 | print "\r".ljust(console_width - 1) 35 | print "\r" 36 | end 37 | 38 | def while_spinning(message, &) 39 | thread = Thread.new(&) 40 | thread.report_on_exception = false 41 | message = message.ljust(console_width - 2) 42 | print "\r#{Colors.blue([spinner.next, message].join(' '))}" until wait_for_exit(thread, 0.1) 43 | thread.value 44 | end 45 | 46 | private 47 | 48 | attr_reader :spinner, :stderr 49 | 50 | def console_width 51 | width = IO.console.winsize.last if tty? 52 | width.to_i.positive? ? width : 80 53 | end 54 | 55 | def wait_for_exit(thread, seconds) 56 | thread.join(seconds) 57 | rescue StandardError 58 | # Sanity check. If we get an exception, the thread should be dead. 59 | raise if thread.alive? 60 | 61 | thread 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/bundleup/pin_report.rb: -------------------------------------------------------------------------------- 1 | module Bundleup 2 | class PinReport < Report 3 | def initialize(gem_versions:, outdated_gems:, gem_comments:) 4 | super() 5 | @gem_versions = gem_versions 6 | @outdated_gems = outdated_gems 7 | @gem_comments = gem_comments 8 | end 9 | 10 | def title 11 | return "Note that this gem is being held back:" if rows.count == 1 12 | 13 | "Note that the following gems are being held back:" 14 | end 15 | 16 | def rows 17 | outdated_gems.keys.sort.map do |gem| 18 | meta = outdated_gems[gem] 19 | current_version = gem_versions[gem] 20 | newest_version = meta[:newest] 21 | pin = meta[:pin] 22 | 23 | [gem, current_version, "→", newest_version, *pin_reason(gem, pin)] 24 | end 25 | end 26 | 27 | def pinned_gems 28 | outdated_gems.keys.sort 29 | end 30 | 31 | private 32 | 33 | attr_reader :gem_versions, :outdated_gems, :gem_comments 34 | 35 | def pin_reason(gem, pin) 36 | notes = Colors.gray(gem_comments[gem].to_s) 37 | pin_operator, pin_version = pin.split(" ", 2) 38 | [":", "pinned at", pin_operator.rjust(2), pin_version, notes] 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/bundleup/report.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | module Bundleup 4 | class Report 5 | extend Forwardable 6 | def_delegators :rows, :empty?, :one? 7 | 8 | def many? 9 | rows.length > 1 10 | end 11 | 12 | def to_s 13 | [ 14 | title, 15 | tableize(rows).map { |row| row.join(" ").rstrip }.join("\n"), 16 | "" 17 | ].join("\n\n") 18 | end 19 | 20 | private 21 | 22 | def tableize(rows) 23 | widths = max_length_of_each_column(rows) 24 | rows.map do |row| 25 | row.zip(widths).map do |value, width| 26 | padding = " " * (width - Colors.strip(value).length) 27 | "#{value}#{padding}" 28 | end 29 | end 30 | end 31 | 32 | def max_length_of_each_column(rows) 33 | Array.new(rows.first.count) do |i| 34 | rows.map { |values| Colors.strip(values[i]).length }.max 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/bundleup/shell.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | require "open3" 3 | 4 | module Bundleup 5 | class Shell 6 | extend Forwardable 7 | def_delegators :Bundleup, :logger 8 | 9 | def capture(command, raise_on_error: true) 10 | stdout, stderr, status = capture3(command) 11 | raise ["Failed to execute: #{command}", stdout, stderr].compact.join("\n") if raise_on_error && !status.success? 12 | 13 | stdout 14 | end 15 | 16 | def run(command) 17 | capture(command) 18 | true 19 | end 20 | 21 | def run?(command) 22 | _, _, status = capture3(command) 23 | status.success? 24 | end 25 | 26 | private 27 | 28 | def capture3(command) 29 | command = Array(command) 30 | logger.while_spinning("running: #{command.join(' ')}") do 31 | Open3.capture3(*command) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/bundleup/update_report.rb: -------------------------------------------------------------------------------- 1 | module Bundleup 2 | class UpdateReport < Report 3 | def initialize(old_versions:, new_versions:) 4 | super() 5 | @old_versions = old_versions 6 | @new_versions = new_versions 7 | end 8 | 9 | def title 10 | return "This gem will be updated:" if rows.count == 1 11 | 12 | "The following gems will be updated:" 13 | end 14 | 15 | def rows 16 | gem_names.each_with_object([]) do |gem, rows| 17 | old = old_versions[gem] 18 | new = new_versions[gem] 19 | next if old == new 20 | 21 | row = [gem, old || "(new)", "→", new || "(removed)"] 22 | 23 | color = color_for_gem(gem) 24 | rows << row.map { |col| Colors.public_send(color, col) } 25 | end 26 | end 27 | 28 | def updated_gems 29 | gem_names.reject do |gem| 30 | old_versions[gem] == new_versions[gem] 31 | end 32 | end 33 | 34 | private 35 | 36 | attr_reader :old_versions, :new_versions 37 | 38 | def gem_names 39 | (old_versions.keys | new_versions.keys).sort 40 | end 41 | 42 | def color_for_gem(gem) 43 | old_version = old_versions[gem] 44 | new_version = new_versions[gem] 45 | 46 | return :blue if old_version.nil? 47 | return :red if new_version.nil? || major_upgrade?(old_version, new_version) 48 | return :yellow if minor_upgrade?(old_version, new_version) 49 | 50 | :plain 51 | end 52 | 53 | def major_upgrade?(old_version, new_version) 54 | major(new_version) != major(old_version) 55 | end 56 | 57 | def minor_upgrade?(old_version, new_version) 58 | minor(new_version) != minor(old_version) 59 | end 60 | 61 | def major(version) 62 | version.split(".", 2)[0] 63 | end 64 | 65 | def minor(version) 66 | version.split(".", 3)[1] 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/bundleup/version.rb: -------------------------------------------------------------------------------- 1 | module Bundleup 2 | VERSION = "2.5.3".freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/bundleup/version_spec.rb: -------------------------------------------------------------------------------- 1 | module Bundleup 2 | class VersionSpec 3 | def self.parse(version) 4 | return version if version.is_a?(VersionSpec) 5 | 6 | version = version.strip 7 | _, operator, number = version.match(/^([^\d\s]*)\s*(.+)/).to_a 8 | operator = nil if operator.empty? 9 | 10 | new(parts: number.split("."), operator:) 11 | end 12 | 13 | attr_reader :parts, :operator 14 | 15 | def initialize(parts:, operator: nil) 16 | @parts = parts 17 | @operator = operator 18 | end 19 | 20 | def exact? 21 | operator.nil? 22 | end 23 | 24 | def relax 25 | return self if %w[!= > >=].include?(operator) 26 | return self.class.parse(">= 0") if %w[< <=].include?(operator) 27 | 28 | self.class.new(parts:, operator: ">=") 29 | end 30 | 31 | def shift(new_version) # rubocop:disable Metrics/AbcSize 32 | return self.class.parse(new_version) if exact? 33 | return self if Gem::Requirement.new(to_s).satisfied_by?(Gem::Version.new(new_version)) 34 | return self.class.new(parts: self.class.parse(new_version).parts, operator: "<=") if %w[< <=].include?(operator) 35 | 36 | new_slice = self.class.parse(new_version).slice(parts.length) 37 | self.class.new(parts: new_slice.parts, operator: "~>") 38 | end 39 | 40 | def slice(amount) 41 | self.class.new(parts: parts[0, amount], operator:) 42 | end 43 | 44 | def to_s 45 | [operator, parts.join(".")].compact.join(" ") 46 | end 47 | 48 | def ==(other) 49 | return false unless other.is_a?(VersionSpec) 50 | 51 | to_s == other.to_s 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/bundleup/backup_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "tempfile" 3 | 4 | class Bundleup::BackupTest < Minitest::Test 5 | def test_restore_on_error 6 | original_contents = ["Hello, world!\n", "Another file\n"] 7 | files = original_contents.map do |content| 8 | file = Tempfile.new 9 | file << content 10 | file.close 11 | file 12 | end 13 | 14 | assert_raises(StandardError, "oh no!") do 15 | Bundleup::Backup.restore_on_error(*files.map(&:path)) do 16 | files.each { |file| File.write(file.path, "Modified!\n") } 17 | raise "oh no!" 18 | end 19 | end 20 | 21 | original_contents.zip(files).each do |content, file| 22 | assert_equal(content, File.read(file.path)) 23 | end 24 | end 25 | 26 | def test_restore 27 | original_contents = "Hello, world!\n" 28 | file = Tempfile.new 29 | file << original_contents 30 | file.close 31 | 32 | backup = Bundleup::Backup.new(file.path) 33 | 34 | assert_equal(original_contents, File.read(file.path)) 35 | 36 | File.write(file.path, "Modified!\n") 37 | assert_equal("Modified!\n", File.read(file.path)) 38 | 39 | backup.restore 40 | assert_equal(original_contents, File.read(file.path)) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/bundleup/cli_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "fileutils" 3 | require "tempfile" 4 | 5 | class Bundleup::CLITest < Minitest::Test 6 | include OutputHelpers 7 | 8 | def test_it_works_with_a_sample_project # rubocop:disable Minitest/MultipleAssertions 9 | stdout = within_copy_of_sample_project do 10 | capturing_plain_output(stdin: "n\n") do 11 | with_clean_bundler_env do 12 | Bundleup::CLI.new([]).run 13 | end 14 | end 15 | end 16 | assert_includes(stdout, "Please wait a moment while I upgrade your Gemfile.lock...") 17 | assert_includes(stdout, "The following gems will be updated:") 18 | assert_match(/^mail\s+2\.7\.0\s+→ [\d.]+\s*$/, stdout) 19 | assert_match(/^mocha\s+1\.11\.1\s+→ [\d.]+\s*$/, stdout) 20 | assert_includes(stdout, "Note that the following gems are being held back:") 21 | assert_match(/^rake\s+12\.3\.3\s+→ [\d.]+\s+:\s+pinned at ~> 12\.0\s+# Not ready for 13 yet\s*$/, stdout) 22 | assert_match(/^rubocop\s+0\.89\.0\s+→ [\d.]+\s+:\s+pinned at = 0\.89\.0\s*$/, stdout) 23 | assert_includes(stdout, "Do you want to apply these changes [Yn]?") 24 | assert_includes(stdout, "Your original Gemfile.lock has been restored.") 25 | end 26 | 27 | def test_it_passes_args_to_bundle_update 28 | stdout = capturing_plain_output(stdin: "n\n") do 29 | Dir.chdir(File.expand_path("../fixtures/project", __dir__)) do 30 | with_clean_bundler_env do 31 | Bundleup::CLI.new(["--group=development"]).run 32 | end 33 | end 34 | end 35 | assert_includes(stdout, "running: bundle update --group=development") 36 | end 37 | 38 | def test_it_displays_usage 39 | stdout = capturing_plain_output do 40 | Bundleup::CLI.new(["-h"]).run 41 | end 42 | 43 | assert_includes(stdout, "Usage: bundleup [GEMS...] [OPTIONS]") 44 | end 45 | 46 | def test_it_raises_if_gemfile_not_present_in_working_dir 47 | Dir.chdir(__dir__) do 48 | error = assert_raises(Bundleup::CLI::Error) { Bundleup::CLI.new([]).run } 49 | assert_equal("Gemfile and Gemfile.lock must both be present.", error.message) 50 | end 51 | end 52 | 53 | def test_update_gemfile_flag # rubocop:disable Minitest/MultipleAssertions 54 | stdout, updated_gemfile = within_copy_of_sample_project do 55 | out = capturing_plain_output(stdin: "y\n") do 56 | with_clean_bundler_env do 57 | Bundleup::CLI.new(["--update-gemfile"]).run 58 | end 59 | end 60 | [out, File.read("Gemfile")] 61 | end 62 | 63 | assert_match(/^mail\s+2\.7\.0\s+→ [\d.]+\s*$/, stdout) 64 | assert_match(/^mocha\s+1\.11\.1\s+→ [\d.]+\s*$/, stdout) 65 | assert_match(/^rubocop\s+0\.89\.0\s+→ [\d.]+\s*$/, stdout) 66 | assert_match(/^rake\s+12\.3\.3\s+→ [\d.]+\s+:\s+pinned at ~> 12\.0\s+# Not ready for 13 yet\s*$/, stdout) 67 | assert_includes(stdout, "Do you want to apply these changes [Yn]?") 68 | assert_includes(stdout, "✔ Done!") 69 | 70 | assert_includes(updated_gemfile, <<~GEMFILE) 71 | gem "mail" 72 | gem "mocha" 73 | gem "rake", "~> 12.0" # Not ready for 13 yet 74 | GEMFILE 75 | assert_match(/^gem "rubocop", "[.\d]+"$/, updated_gemfile) 76 | refute_match(/^gem "rubocop", "0.89.0"$/, updated_gemfile) 77 | end 78 | 79 | def test_returned_updated_and_pinned_gems_after_run 80 | cli = Bundleup::CLI.new([]) 81 | 82 | within_copy_of_sample_project do 83 | capturing_plain_output(stdin: "y\n") do 84 | with_clean_bundler_env do 85 | cli.run 86 | end 87 | end 88 | end 89 | 90 | assert_kind_of(Array, cli.updated_gems) 91 | assert_kind_of(Array, cli.pinned_gems) 92 | assert_includes(cli.updated_gems, "mail") 93 | assert_includes(cli.updated_gems, "mocha") 94 | assert_includes(cli.pinned_gems, "rake") 95 | assert_includes(cli.pinned_gems, "rubocop") 96 | end 97 | 98 | def test_returned_no_chenged_gems_after_rejected_changes 99 | cli = Bundleup::CLI.new([]) 100 | 101 | within_copy_of_sample_project do 102 | capturing_plain_output(stdin: "n\n") do 103 | with_clean_bundler_env do 104 | cli.run 105 | end 106 | end 107 | end 108 | 109 | assert_kind_of(Array, cli.updated_gems) 110 | assert_kind_of(Array, cli.pinned_gems) 111 | assert_empty(cli.updated_gems) 112 | assert_empty(cli.pinned_gems) 113 | end 114 | 115 | private 116 | 117 | def with_clean_bundler_env(&) 118 | if defined?(Bundler) 119 | if Bundler.respond_to?(:with_unbundled_env) 120 | Bundler.with_unbundled_env(&) 121 | else 122 | Bundler.with_clean_env(&) 123 | end 124 | else 125 | yield 126 | end 127 | end 128 | 129 | def within_copy_of_sample_project(&block) 130 | sample_dir = File.expand_path("../fixtures/project", __dir__) 131 | sample_files = %w[Gemfile Gemfile.lock].map { |file| File.join(sample_dir, file) } 132 | 133 | Dir.mktmpdir do |path| 134 | FileUtils.cp(sample_files, path) 135 | Dir.chdir(path, &block) 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/bundleup/commands_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Bundleup::CommandsTest < Minitest::Test 4 | def test_check? 5 | Bundleup.shell.expects(:run?).with(%w[bundle check]).returns(true) 6 | assert_predicate(Bundleup::Commands.new, :check?) 7 | end 8 | 9 | def test_install 10 | Bundleup.shell.expects(:run).with(%w[bundle install]).returns(true) 11 | assert(Bundleup::Commands.new.install) 12 | end 13 | 14 | def test_list 15 | Bundleup.shell.expects(:capture).with(%w[bundle list]).returns(read_fixture("list.out")) 16 | gems = Bundleup::Commands.new.list 17 | assert_equal(118, gems.count) 18 | assert_equal("f5733d0", gems["friendly_id"]) 19 | assert_equal("2.7.1", gems["rubocop-rails"]) 20 | assert_equal("0.3.6", gems["thread_safe"]) 21 | end 22 | 23 | def test_outdated_2_1 24 | Bundleup.shell 25 | .stubs(:capture) 26 | .with(%w[bundle outdated], raise_on_error: false) 27 | .returns(read_fixture("outdated-2.1.out")) 28 | 29 | assert_equal( 30 | { 31 | "redis" => { newest: "4.2.1", pin: "~> 4.1.4" }, 32 | "sidekiq" => { newest: "6.1.0", pin: "~> 6.0" }, 33 | "sprockets" => { newest: "4.0.2", pin: "~> 3.5" } 34 | }, 35 | Bundleup::Commands.new.outdated 36 | ) 37 | end 38 | 39 | def test_outdated_2_2 40 | Bundleup.shell 41 | .stubs(:capture) 42 | .with(%w[bundle outdated], raise_on_error: false) 43 | .returns(read_fixture("outdated-2.2.out")) 44 | 45 | assert_equal( 46 | { 47 | "redis" => { newest: "4.2.1", pin: "~> 4.1.4" }, 48 | "sidekiq" => { newest: "6.1.0", pin: "~> 6.0" }, 49 | "sprockets" => { newest: "4.0.2", pin: "~> 3.5" } 50 | }, 51 | Bundleup::Commands.new.outdated 52 | ) 53 | end 54 | 55 | def test_update 56 | Bundleup.shell.expects(:run).with(%w[bundle update]).returns(true) 57 | assert(Bundleup::Commands.new.update) 58 | end 59 | 60 | def test_update_with_args 61 | Bundleup.shell.expects(:run).with(%w[bundle update --group=development --strict]).returns(true) 62 | assert(Bundleup::Commands.new.update(%w[--group=development --strict])) 63 | end 64 | 65 | private 66 | 67 | def read_fixture(fixture) 68 | fixture_path = File.expand_path("../fixtures/#{fixture}", __dir__) 69 | File.read(fixture_path) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/bundleup/gemfile_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "tempfile" 3 | 4 | class Bundleup::GemfileTest < Minitest::Test 5 | def test_gem_comments 6 | gemfile = with_copy_of_sample_gemfile { |path| Bundleup::Gemfile.new(path) } 7 | 8 | assert_equal( 9 | { 10 | "rails" => "# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'", 11 | "pg" => "# Use postgresql as the database for Active Record", 12 | "puma" => "# Use Puma as the app server", 13 | "sass-rails" => "# Use SCSS for stylesheets", 14 | "uglifier" => "# Use Uglifier as compressor for JavaScript assets", 15 | "coffee-rails" => "# Use CoffeeScript for .coffee assets and views", 16 | "turbolinks" => "# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks", 17 | "jbuilder" => "# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder", 18 | "byebug" => "# Call 'byebug' anywhere in the code to stop execution and get a debugger console", 19 | "capybara" => "# Adds support for Capybara system testing and selenium driver", 20 | "selenium-webdriver" => "# This is needed for driven_by :chrome", 21 | "web-console" => "# Access an IRB console on exception pages or by using <%= console %> anywhere in the code.", 22 | "spring" => "# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring", 23 | "tzinfo-data" => "# Windows does not include zoneinfo files, so bundle the tzinfo-data gem" 24 | }, 25 | gemfile.gem_comments 26 | ) 27 | end 28 | 29 | def test_gem_pins_without_comments 30 | gemfile = with_copy_of_sample_gemfile { |path| Bundleup::Gemfile.new(path) } 31 | assert_equal( 32 | { 33 | "spring-watcher-listen" => Bundleup::VersionSpec.parse("~> 2.0.0") 34 | }, 35 | gemfile.gem_pins_without_comments 36 | ) 37 | end 38 | 39 | def test_relax_gem_pins! 40 | with_copy_of_sample_gemfile do |path| 41 | Bundleup::Gemfile.new(path).relax_gem_pins!(%w[rails uglifier capybara]) 42 | 43 | assert_equal(<<~'GEMFILE', File.read(path)) 44 | source 'https://rubygems.org' 45 | 46 | git_source(:github) do |repo_name| 47 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 48 | "https://github.com/#{repo_name}.git" 49 | end 50 | 51 | 52 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 53 | gem 'rails', '>= 5.1.4' 54 | # Use postgresql as the database for Active Record 55 | gem 'pg', '~> 0.18' 56 | # Use Puma as the app server 57 | gem 'puma', '~> 3.7' 58 | # Use SCSS for stylesheets 59 | gem 'sass-rails', '~> 5.0' 60 | # Use Uglifier as compressor for JavaScript assets 61 | gem 'uglifier', '>= 1.3.0' 62 | # See https://github.com/rails/execjs#readme for more supported runtimes 63 | # gem 'therubyracer', platforms: :ruby 64 | 65 | # Use CoffeeScript for .coffee assets and views 66 | gem 'coffee-rails', '~> 4.2' 67 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks 68 | gem 'turbolinks', '~> 5' 69 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 70 | gem 'jbuilder', '~> 2.5' 71 | # Use Redis adapter to run Action Cable in production 72 | # gem 'redis', '~> 3.0' 73 | # Use ActiveModel has_secure_password 74 | # gem 'bcrypt', '~> 3.1.7' 75 | 76 | # Use Capistrano for deployment 77 | # gem 'capistrano-rails', group: :development 78 | 79 | group :development, :test do 80 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 81 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 82 | # Adds support for Capybara system testing and selenium driver 83 | gem 'capybara', '>= 2.13' 84 | gem 'selenium-webdriver' # This is needed for driven_by :chrome 85 | end 86 | 87 | group :development do 88 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 89 | gem 'web-console', '>= 3.3.0' 90 | gem 'listen', '>= 3.0.5', '< 3.2' 91 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 92 | gem 'spring' 93 | gem 'spring-watcher-listen', '~> 2.0.0' 94 | end 95 | 96 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 97 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 98 | GEMFILE 99 | end 100 | end 101 | 102 | def test_shift_gem_pins! 103 | with_copy_of_sample_gemfile do |path| 104 | Bundleup::Gemfile.new(path).shift_gem_pins!( 105 | "rails" => "6.0.1.3", 106 | "uglifier" => "1.4.5", 107 | "capybara" => "3.9.1" 108 | ) 109 | 110 | assert_equal(<<~'GEMFILE', File.read(path)) 111 | source 'https://rubygems.org' 112 | 113 | git_source(:github) do |repo_name| 114 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 115 | "https://github.com/#{repo_name}.git" 116 | end 117 | 118 | 119 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 120 | gem 'rails', '~> 6.0.1' 121 | # Use postgresql as the database for Active Record 122 | gem 'pg', '~> 0.18' 123 | # Use Puma as the app server 124 | gem 'puma', '~> 3.7' 125 | # Use SCSS for stylesheets 126 | gem 'sass-rails', '~> 5.0' 127 | # Use Uglifier as compressor for JavaScript assets 128 | gem 'uglifier', '>= 1.3.0' 129 | # See https://github.com/rails/execjs#readme for more supported runtimes 130 | # gem 'therubyracer', platforms: :ruby 131 | 132 | # Use CoffeeScript for .coffee assets and views 133 | gem 'coffee-rails', '~> 4.2' 134 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks 135 | gem 'turbolinks', '~> 5' 136 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 137 | gem 'jbuilder', '~> 2.5' 138 | # Use Redis adapter to run Action Cable in production 139 | # gem 'redis', '~> 3.0' 140 | # Use ActiveModel has_secure_password 141 | # gem 'bcrypt', '~> 3.1.7' 142 | 143 | # Use Capistrano for deployment 144 | # gem 'capistrano-rails', group: :development 145 | 146 | group :development, :test do 147 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 148 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 149 | # Adds support for Capybara system testing and selenium driver 150 | gem 'capybara', '~> 3.9' 151 | gem 'selenium-webdriver' # This is needed for driven_by :chrome 152 | end 153 | 154 | group :development do 155 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 156 | gem 'web-console', '>= 3.3.0' 157 | gem 'listen', '>= 3.0.5', '< 3.2' 158 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 159 | gem 'spring' 160 | gem 'spring-watcher-listen', '~> 2.0.0' 161 | end 162 | 163 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 164 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 165 | GEMFILE 166 | end 167 | end 168 | 169 | private 170 | 171 | def with_copy_of_sample_gemfile 172 | file = Tempfile.new 173 | file << File.read(File.expand_path("../fixtures/Gemfile.sample", __dir__)) 174 | file.close 175 | yield file.path 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /test/bundleup/logger_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Bundleup::LoggerTest < Minitest::Test 4 | include OutputHelpers 5 | 6 | def test_confirm_with_default_response 7 | stdout = capturing_plain_output(stdin: "\n") do 8 | confirmed = Bundleup.logger.confirm?("Are you sure?") 9 | assert(confirmed) 10 | end 11 | 12 | assert_equal("Are you sure [Yn]? ", stdout) 13 | end 14 | 15 | def test_confirm_with_y_response 16 | capturing_plain_output(stdin: "y\n") do 17 | confirmed = Bundleup.logger.confirm?("Are you sure?") 18 | assert(confirmed) 19 | end 20 | end 21 | 22 | def test_confirm_with_n_response 23 | capturing_plain_output(stdin: "n\n") do 24 | confirmed = Bundleup.logger.confirm?("Are you sure?") 25 | refute(confirmed) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/bundleup/pin_report_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Bundleup::PinReportTest < Minitest::Test 4 | include OutputHelpers 5 | 6 | def test_singular_title_when_one_gem 7 | report = Bundleup::PinReport.new( 8 | gem_versions: { "rubocop" => "0.89.0" }, 9 | outdated_gems: { "rubocop" => { newest: "0.89.1", pin: "= 0.89.0" } }, 10 | gem_comments: {} 11 | ) 12 | assert_equal("Note that this gem is being held back:", report.title) 13 | end 14 | 15 | def test_plural_title_when_two_gems 16 | report = Bundleup::PinReport.new( 17 | gem_versions: { "rake" => "12.3.3", "rubocop" => "0.89.0" }, 18 | outdated_gems: { 19 | "rake" => { newest: "13.0.1", pin: "~> 12.0" }, 20 | "rubocop" => { newest: "0.89.1", pin: "= 0.89.0" } 21 | }, 22 | gem_comments: {} 23 | ) 24 | assert_equal("Note that the following gems are being held back:", report.title) 25 | end 26 | 27 | def test_generates_sorted_table_with_pins_and_gray_color_comments 28 | with_color do 29 | report = Bundleup::PinReport.new( 30 | gem_versions: { "rake" => "12.3.3", "rubocop" => "0.89.0" }, 31 | outdated_gems: { 32 | "rubocop" => { newest: "0.89.1", pin: "= 0.89.0" }, 33 | "rake" => { newest: "13.0.1", pin: "~> 12.0" } 34 | }, 35 | gem_comments: { "rake" => "# Not ready for 13 yet" } 36 | ) 37 | assert_equal(<<~REPORT, report.to_s) 38 | Note that the following gems are being held back: 39 | 40 | rake 12.3.3 → 13.0.1 : pinned at ~> 12.0 \e[0;90;49m# Not ready for 13 yet\e[0m 41 | rubocop 0.89.0 → 0.89.1 : pinned at = 0.89.0 42 | 43 | REPORT 44 | end 45 | end 46 | 47 | def test_list_pinned_gems 48 | report = Bundleup::PinReport.new( 49 | gem_versions: { "rake" => "12.3.3", "rubocop" => "0.89.0" }, 50 | outdated_gems: { 51 | "rake" => { newest: "13.0.1", pin: "~> 12.0" }, 52 | "rubocop" => { newest: "0.89.1", pin: "= 0.89.0" } 53 | }, 54 | gem_comments: {} 55 | ) 56 | 57 | assert_equal(%w[rake rubocop], report.pinned_gems) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/bundleup/update_report_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Bundleup::UpdateReportTest < Minitest::Test 4 | include OutputHelpers 5 | 6 | def test_singular_title_when_one_gem 7 | report = Bundleup::UpdateReport.new( 8 | old_versions: { "rubocop" => "0.89.0" }, 9 | new_versions: { "rubocop" => "0.89.1" } 10 | ) 11 | assert_equal("This gem will be updated:", report.title) 12 | end 13 | 14 | def test_plural_title_when_two_gems 15 | report = Bundleup::UpdateReport.new( 16 | old_versions: { "rake" => "12.3.3", "rubocop" => "0.89.0" }, 17 | new_versions: { "rake" => "13.0.1", "rubocop" => "0.89.1" } 18 | ) 19 | assert_equal("The following gems will be updated:", report.title) 20 | end 21 | 22 | def test_generates_sorted_table 23 | without_color do 24 | report = Bundleup::UpdateReport.new( 25 | old_versions: { 26 | "i18n" => "1.8.2", 27 | "zeitwerk" => "2.3.0", 28 | "json" => "2.2.0", 29 | "parser" => "2.7.1.1", 30 | "bundler-audit" => "0.6.1", 31 | "rails" => "e063bef", 32 | "thor" => "0.20.3" 33 | }, 34 | new_versions: { 35 | "i18n" => "1.8.5", 36 | "zeitwerk" => "2.4.0", 37 | "parser" => "2.7.1.4", 38 | "bundler-audit" => "0.7.0.1", 39 | "rails" => "57a4ead", 40 | "rubocop-ast" => "0.3.0", 41 | "thor" => "1.0.1" 42 | } 43 | ) 44 | assert_equal(<<~REPORT, report.to_s) 45 | The following gems will be updated: 46 | 47 | bundler-audit 0.6.1 → 0.7.0.1 48 | i18n 1.8.2 → 1.8.5 49 | json 2.2.0 → (removed) 50 | parser 2.7.1.1 → 2.7.1.4 51 | rails e063bef → 57a4ead 52 | rubocop-ast (new) → 0.3.0 53 | thor 0.20.3 → 1.0.1 54 | zeitwerk 2.3.0 → 2.4.0 55 | 56 | REPORT 57 | end 58 | end 59 | 60 | def test_colorizes_rows_according_to_semver 61 | with_color do 62 | report = Bundleup::UpdateReport.new( 63 | old_versions: { 64 | "i18n" => "1.8.2", 65 | "zeitwerk" => "2.3.0", 66 | "json" => "2.2.0", 67 | "parser" => "2.7.1.1", 68 | "bundler-audit" => "0.6.1", 69 | "rails" => "e063bef", 70 | "thor" => "0.20.3" 71 | }, 72 | new_versions: { 73 | "i18n" => "1.8.5", 74 | "zeitwerk" => "2.4.0", 75 | "parser" => "2.7.1.4", 76 | "bundler-audit" => "0.7.0.1", 77 | "rails" => "57a4ead", 78 | "rubocop-ast" => "0.3.0", 79 | "thor" => "1.0.1" 80 | } 81 | ) 82 | assert_equal(<<~REPORT, report.to_s) 83 | The following gems will be updated: 84 | 85 | \e[0;33;49mbundler-audit\e[0m \e[0;33;49m0.6.1\e[0m \e[0;33;49m→\e[0m \e[0;33;49m0.7.0.1\e[0m 86 | i18n 1.8.2 → 1.8.5 87 | \e[0;31;49mjson\e[0m \e[0;31;49m2.2.0\e[0m \e[0;31;49m→\e[0m \e[0;31;49m(removed)\e[0m 88 | parser 2.7.1.1 → 2.7.1.4 89 | \e[0;31;49mrails\e[0m \e[0;31;49me063bef\e[0m \e[0;31;49m→\e[0m \e[0;31;49m57a4ead\e[0m 90 | \e[0;34;49mrubocop-ast\e[0m \e[0;34;49m(new)\e[0m \e[0;34;49m→\e[0m \e[0;34;49m0.3.0\e[0m 91 | \e[0;31;49mthor\e[0m \e[0;31;49m0.20.3\e[0m \e[0;31;49m→\e[0m \e[0;31;49m1.0.1\e[0m 92 | \e[0;33;49mzeitwerk\e[0m \e[0;33;49m2.3.0\e[0m \e[0;33;49m→\e[0m \e[0;33;49m2.4.0\e[0m 93 | 94 | REPORT 95 | end 96 | end 97 | 98 | def test_list_updated_gems 99 | report = Bundleup::UpdateReport.new( 100 | old_versions: { 101 | "i18n" => "1.8.2", 102 | "zeitwerk" => "2.4.0", 103 | "json" => "2.2.0" 104 | }, 105 | new_versions: { 106 | "i18n" => "1.8.5", 107 | "zeitwerk" => "2.4.0", 108 | "parser" => "2.7.1.4" 109 | } 110 | ) 111 | 112 | assert_equal(%w[i18n json parser], report.updated_gems) 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /test/bundleup/version_spec_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Bundleup::VersionSpecTest < Minitest::Test 4 | def test_relax_doesnt_affect_greater_than_equal_specs 5 | assert_equal(">= 1.0.1", parse(">= 1.0.1").relax.to_s) 6 | end 7 | 8 | def test_relax_doesnt_affect_not_equal_specs 9 | assert_equal("!= 5.2.0", parse("!= 5.2.0").relax.to_s) 10 | end 11 | 12 | def test_relax_doesnt_affect_greater_than_specs 13 | assert_equal("> 1.0.1", parse("> 1.0.1").relax.to_s) 14 | end 15 | 16 | def test_relax_changes_approximate_specs_to_greater_than_equal 17 | assert_equal(">= 5", parse("~> 5").relax.to_s) 18 | assert_equal(">= 5.2", parse("~> 5.2").relax.to_s) 19 | assert_equal(">= 5.2.0", parse("~> 5.2.0").relax.to_s) 20 | end 21 | 22 | def test_relax_changes_exact_specs_to_greater_than_equal 23 | assert_equal(">= 0.89.0", parse("0.89.0").relax.to_s) 24 | end 25 | 26 | def test_relax_changes_less_than_specs_to_greater_than_equal_zero 27 | assert_equal(">= 0", parse("< 1.9.5").relax.to_s) 28 | end 29 | 30 | def test_relax_changes_less_than_equal_specs_to_greater_than_equal_zero 31 | assert_equal(">= 0", parse("<= 1.9.5").relax.to_s) 32 | end 33 | 34 | def test_shift_doesnt_affect_greater_than_equal_specs 35 | assert_equal(">= 1.0.1", parse(">= 1.0.1").shift("2.3.5").to_s) 36 | end 37 | 38 | def test_shift_doesnt_affect_not_equal_specs 39 | assert_equal("!= 5.2.0", parse("!= 5.2.0").shift("6.1.2").to_s) 40 | end 41 | 42 | def test_shift_doesnt_affect_greater_than_specs 43 | assert_equal("> 1.0.1", parse("> 1.0.1").shift("2.3.5").to_s) 44 | end 45 | 46 | def test_shift_doesnt_affect_approximate_specs_if_new_version_is_compatible 47 | assert_equal("~> 5.2.0", parse("~> 5.2.0").shift("5.2.6").to_s) 48 | assert_equal("~> 5.2", parse("~> 5.2").shift("5.4.9").to_s) 49 | end 50 | 51 | def test_shift_changes_exact_spec_to_new_version 52 | assert_equal("0.90.0", parse("0.89.0").shift("0.90.0").to_s) 53 | end 54 | 55 | def test_shift_changes_approximate_specs_to_accomodate_new_version 56 | assert_equal("~> 6", parse("~> 5").shift("6.1.2").to_s) 57 | assert_equal("~> 6.1", parse("~> 5.2").shift("6.1.2").to_s) 58 | assert_equal("~> 6.1.2", parse("~> 5.2.0").shift("6.1.2").to_s) 59 | end 60 | 61 | def test_shift_changes_less_than_specs_to_less_than_equal 62 | assert_equal("<= 2.1.4", parse("< 1.9.5").shift("2.1.4").to_s) 63 | end 64 | 65 | def test_shift_changes_less_than_equal_specs_to_accomodate_new_veresion 66 | assert_equal("<= 2.1.4", parse("<= 1.9.5").shift("2.1.4").to_s) 67 | end 68 | 69 | private 70 | 71 | def parse(spec) 72 | Bundleup::VersionSpec.parse(spec) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/bundleup_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class BundleupTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::Bundleup::VERSION 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/Gemfile.sample: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) do |repo_name| 4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | 8 | 9 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 10 | gem 'rails', '~> 5.1.4' 11 | # Use postgresql as the database for Active Record 12 | gem 'pg', '~> 0.18' 13 | # Use Puma as the app server 14 | gem 'puma', '~> 3.7' 15 | # Use SCSS for stylesheets 16 | gem 'sass-rails', '~> 5.0' 17 | # Use Uglifier as compressor for JavaScript assets 18 | gem 'uglifier', '>= 1.3.0' 19 | # See https://github.com/rails/execjs#readme for more supported runtimes 20 | # gem 'therubyracer', platforms: :ruby 21 | 22 | # Use CoffeeScript for .coffee assets and views 23 | gem 'coffee-rails', '~> 4.2' 24 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks 25 | gem 'turbolinks', '~> 5' 26 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 27 | gem 'jbuilder', '~> 2.5' 28 | # Use Redis adapter to run Action Cable in production 29 | # gem 'redis', '~> 3.0' 30 | # Use ActiveModel has_secure_password 31 | # gem 'bcrypt', '~> 3.1.7' 32 | 33 | # Use Capistrano for deployment 34 | # gem 'capistrano-rails', group: :development 35 | 36 | group :development, :test do 37 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 38 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 39 | # Adds support for Capybara system testing and selenium driver 40 | gem 'capybara', '~> 2.13' 41 | gem 'selenium-webdriver' # This is needed for driven_by :chrome 42 | end 43 | 44 | group :development do 45 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 46 | gem 'web-console', '>= 3.3.0' 47 | gem 'listen', '>= 3.0.5', '< 3.2' 48 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 49 | gem 'spring' 50 | gem 'spring-watcher-listen', '~> 2.0.0' 51 | end 52 | 53 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 54 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 55 | -------------------------------------------------------------------------------- /test/fixtures/list.out: -------------------------------------------------------------------------------- 1 | Gems included by the bundle: 2 | * actioncable (6.0.3.2 79af891) 3 | * actionmailbox (6.0.3.2 79af891) 4 | * actionmailer (6.0.3.2 79af891) 5 | * actionpack (6.0.3.2 79af891) 6 | * actiontext (6.0.3.2 79af891) 7 | * actionview (6.0.3.2 79af891) 8 | * activejob (6.0.3.2 79af891) 9 | * activemodel (6.0.3.2 79af891) 10 | * activerecord (6.0.3.2 79af891) 11 | * activestorage (6.0.3.2 79af891) 12 | * activesupport (6.0.3.2 79af891) 13 | * addressable (2.7.0) 14 | * amazing_print (1.2.1) 15 | * annotate (3.1.1) 16 | * ast (2.4.1) 17 | * better_errors (2.7.1) 18 | * binding_of_caller (0.8.0) 19 | * brakeman (4.9.0) 20 | * builder (3.2.4) 21 | * bundler-audit (0.7.0.1) 22 | * capybara (3.33.0) 23 | * childprocess (3.0.0) 24 | * coderay (1.1.3) 25 | * concurrent-ruby (1.1.7) 26 | * crass (1.0.6) 27 | * debug_inspector (0.0.3) 28 | * dotenv (2.7.6) 29 | * dotenv-rails (2.7.6) 30 | * em-websocket (0.5.1) 31 | * erubi (1.9.0) 32 | * eventmachine (1.2.7) 33 | * ffi (1.13.1) 34 | * formatador (0.2.5) 35 | * friendly_id (5.3.0 f5733d0) 36 | * globalid (0.4.2) 37 | * guard (2.16.2) 38 | * guard-compat (1.2.1) 39 | * guard-livereload (2.5.2) 40 | * guard-minitest (2.4.6) 41 | * guard-shell (0.7.1) 42 | * http_parser.rb (0.6.0) 43 | * i18n (1.8.5) 44 | * launchy (2.5.0) 45 | * listen (3.2.1) 46 | * loofah (2.6.0) 47 | * lumberjack (1.2.7) 48 | * mail (2.7.1) 49 | * marcel (0.3.3) 50 | * method_source (1.0.0) 51 | * mimemagic (0.3.5) 52 | * mini_mime (1.0.2) 53 | * mini_portile2 (2.4.0) 54 | * minitest (5.14.1) 55 | * minitest-ci (3.4.0) 56 | * multi_json (1.15.0) 57 | * nenv (0.3.0) 58 | * nio4r (2.5.2) 59 | * nokogiri (1.10.10) 60 | * notiffany (0.1.3) 61 | * parallel (1.19.2) 62 | * parser (2.7.1.4) 63 | * pg (1.2.3) 64 | * pgcli-rails (0.5.0) 65 | * pry (0.13.1) 66 | * public_suffix (4.0.5) 67 | * puma (4.3.5) 68 | * pygments.rb (1.2.1) 69 | * rack (2.2.3) 70 | * rack-canonical-host (1.0.0) 71 | * rack-livereload (0.3.17) 72 | * rack-proxy (0.6.5) 73 | * rack-test (1.1.0) 74 | * rails (6.0.3.2 79af891) 75 | * rails-dom-testing (2.0.3) 76 | * rails-html-sanitizer (1.3.0) 77 | * railties (6.0.3.2 79af891) 78 | * rainbow (3.0.0) 79 | * rake (13.0.1) 80 | * rb-fsevent (0.10.4) 81 | * rb-inotify (0.10.1) 82 | * redcarpet (3.5.0) 83 | * regexp_parser (1.7.1) 84 | * rexml (3.2.4) 85 | * rollbar (2.27.0) 86 | * rubocop (0.88.0) 87 | * rubocop-ast (0.2.0) 88 | * rubocop-minitest (0.10.1) 89 | * rubocop-performance (1.7.1) 90 | * rubocop-rails (2.7.1) 91 | * ruby-progressbar (1.10.1) 92 | * rubypants (0.7.1) 93 | * rubyzip (2.3.0) 94 | * secure_headers (6.3.1) 95 | * selenium-webdriver (3.142.7) 96 | * semantic_range (2.3.0) 97 | * shellany (0.0.1) 98 | * sitemap_generator (6.1.2) 99 | * spring (2.1.0) 100 | * spring-watcher-listen (2.0.1) 101 | * sprockets (4.0.2) 102 | * sprockets-rails (3.2.1) 103 | * terminal-notifier (2.0.0) 104 | * terminal-notifier-guard (1.7.0) 105 | * thor (1.0.1) 106 | * thread_safe (0.3.6) 107 | * tomo (1.3.0 0d3334c) 108 | * tomo-plugin-rollbar (1.0.1 abbbcb2) 109 | * turbolinks (5.2.1) 110 | * turbolinks-source (5.2.0) 111 | * typogruby (1.0.18) 112 | * tzinfo (1.2.7) 113 | * unicode-display_width (1.7.0) 114 | * webdrivers (4.4.1) 115 | * webpacker (5.1.1) 116 | * websocket-driver (0.7.3) 117 | * websocket-extensions (0.1.5) 118 | * xpath (3.2.0) 119 | * zeitwerk (2.4.0) 120 | Use `bundle info` to print more detailed information about a gem 121 | -------------------------------------------------------------------------------- /test/fixtures/outdated-2.1.out: -------------------------------------------------------------------------------- 1 | Outdated gems included in the bundle: 2 | * childprocess (newest 4.0.0, installed 3.0.0) 3 | * diff-lcs (newest 1.4.4, installed 1.4.2) 4 | * json (newest 2.3.1, installed 2.3.0) 5 | * mini_portile2 (newest 2.5.0, installed 2.4.0) 6 | * redis (newest 4.2.1, installed 4.1.4, requested ~> 4.1.4) in group "default" 7 | * sidekiq (newest 6.1.0, installed 6.0.7, requested ~> 6.0) in group "default" 8 | * sprockets (newest 4.0.2, installed 3.7.2, requested ~> 3.5) in group "default" 9 | * tzinfo (newest 2.0.2, installed 1.2.7) 10 | -------------------------------------------------------------------------------- /test/fixtures/outdated-2.2.out: -------------------------------------------------------------------------------- 1 | Gem Current Latest Requested Groups 2 | childprocess 3.0.0 4.0.0 3 | diff-lcs 1.4.2 1.4.4 4 | json 2.3.0 2.3.1 5 | mini_portile2 2.4.0 2.5.0 6 | redis 4.1.4 4.2.1 ~> 4.1.4 default 7 | sidekiq 6.0.7 6.1.0 ~> 6.0 default 8 | sprockets 3.7.2 4.0.2 ~> 3.5 default 9 | tzinfo 1.2.7 2.0.2 10 | -------------------------------------------------------------------------------- /test/fixtures/project/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "mail" 4 | gem "mocha" 5 | gem "rake", "~> 12.0" # Not ready for 13 yet 6 | gem "rubocop", "0.89.0" 7 | -------------------------------------------------------------------------------- /test/fixtures/project/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.1) 5 | mail (2.7.0) 6 | mini_mime (>= 0.1.1) 7 | mini_mime (1.0.2) 8 | mocha (1.11.1) 9 | parallel (1.19.2) 10 | parser (2.7.1.4) 11 | ast (~> 2.4.1) 12 | rainbow (3.0.0) 13 | rake (12.3.3) 14 | regexp_parser (1.7.1) 15 | rexml (3.2.4) 16 | rubocop (0.89.0) 17 | parallel (~> 1.10) 18 | parser (>= 2.7.1.1) 19 | rainbow (>= 2.2.2, < 4.0) 20 | regexp_parser (>= 1.7) 21 | rexml 22 | rubocop-ast (>= 0.1.0, < 1.0) 23 | ruby-progressbar (~> 1.7) 24 | unicode-display_width (>= 1.4.0, < 2.0) 25 | rubocop-ast (0.3.0) 26 | parser (>= 2.7.1.4) 27 | ruby-progressbar (1.10.1) 28 | unicode-display_width (1.7.0) 29 | 30 | PLATFORMS 31 | ruby 32 | 33 | DEPENDENCIES 34 | mail 35 | mocha 36 | rake (~> 12.0) 37 | rubocop (= 0.89.0) 38 | 39 | BUNDLED WITH 40 | 2.6.2 41 | -------------------------------------------------------------------------------- /test/support/mocha.rb: -------------------------------------------------------------------------------- 1 | require "mocha/minitest" 2 | -------------------------------------------------------------------------------- /test/support/output_helpers.rb: -------------------------------------------------------------------------------- 1 | module OutputHelpers 2 | private 3 | 4 | def capturing_plain_output(stdin: nil) 5 | without_color do 6 | original_logger = Bundleup.logger 7 | stdout = StringIO.new 8 | stdin = stdin.nil? ? $stdin : StringIO.new(stdin) 9 | Bundleup.logger = Bundleup::Logger.new(stdout:, stdin:) 10 | yield 11 | stdout.string 12 | ensure 13 | Bundleup.logger = original_logger 14 | end 15 | end 16 | 17 | def with_color 18 | original_color = Bundleup::Colors.enabled? 19 | Bundleup::Colors.enabled = true 20 | yield 21 | ensure 22 | Bundleup::Colors.enabled = original_color 23 | end 24 | 25 | def without_color 26 | original_color = Bundleup::Colors.enabled? 27 | Bundleup::Colors.enabled = false 28 | yield 29 | ensure 30 | Bundleup::Colors.enabled = original_color 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/support/rg.rb: -------------------------------------------------------------------------------- 1 | # Enable color test output 2 | require "minitest/rg" 3 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 2 | require "bundleup" 3 | 4 | require "minitest/autorun" 5 | Dir[File.expand_path("support/**/*.rb", __dir__)].each { |rb| require(rb) } 6 | --------------------------------------------------------------------------------