├── .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 | [](https://rubygems.org/gems/bundleup)
8 | [](https://www.ruby-toolbox.com/projects/bundleup)
9 | [](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 |
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 |
--------------------------------------------------------------------------------