├── .ruby-version ├── lib ├── bummr │ ├── version.rb │ ├── scm.rb │ ├── log.rb │ ├── prompt.rb │ ├── git.rb │ ├── bisecter.rb │ ├── remover.rb │ ├── outdated.rb │ ├── updater.rb │ ├── check.rb │ └── cli.rb └── bummr.rb ├── bin └── bummr ├── Gemfile ├── spec ├── spec_helper.rb ├── lib │ ├── log_spec.rb │ ├── remover_spec.rb │ ├── bisecter_spec.rb │ ├── prompt_spec.rb │ ├── git_spec.rb │ ├── updater_spec.rb │ ├── cli_spec.rb │ └── outdated_spec.rb ├── black_box │ └── bummr_update_spec.rb └── check_spec.rb ├── .gitignore ├── Rakefile ├── .circleci └── config.yml ├── LICENSE ├── bummr.gemspec ├── Gemfile.lock └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /lib/bummr/version.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | VERSION = "1.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /bin/bummr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../lib/bummr' 4 | Bummr::CLI.start(ARGV) 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in bummr.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/bummr/scm.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | module Scm 3 | private 4 | 5 | def git 6 | Bummr::Git.instance 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start 3 | 4 | require 'pry' 5 | require 'bummr' 6 | require 'rainbow/ext/string' 7 | require 'jet_black/rspec' 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.bundle 10 | *.so 11 | *.o 12 | *.a 13 | mkmf.log 14 | -------------------------------------------------------------------------------- /lib/bummr/log.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | module Log 3 | def log(message) 4 | puts message 5 | system("touch log/bummr.log && echo '#{message}' >> log/bummr.log") 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/bummr/prompt.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | module Prompt 3 | def yes?(*args) 4 | headless? || super 5 | end 6 | 7 | private 8 | 9 | def headless? 10 | HEADLESS == true || 11 | HEADLESS == "true" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require 'bundler/gem_tasks' 3 | 4 | # Default directory to look in is `/specs` 5 | # Run with `rake spec` 6 | RSpec::Core::RakeTask.new(:spec) do |task| 7 | task.rspec_opts = ['--color', '--format', 'progress'] 8 | end 9 | 10 | task :default => :spec 11 | -------------------------------------------------------------------------------- /lib/bummr.rb: -------------------------------------------------------------------------------- 1 | # grouped by dependency order than alpha 2 | require 'bummr/log' 3 | require 'bummr/prompt' 4 | require "bummr/scm" 5 | require 'rainbow/ext/string' 6 | require 'open3' 7 | require 'singleton' 8 | require 'thor' 9 | 10 | require "bummr/bisecter" 11 | require "bummr/check" 12 | require "bummr/git" 13 | require "bummr/outdated" 14 | require "bummr/remover" 15 | require "bummr/updater" 16 | 17 | require "bummr/cli" 18 | require "bummr/version" 19 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1, 2 | orbs: 3 | ruby: circleci/ruby@1.1.2 4 | jobs: 5 | test: 6 | docker: 7 | - image: cimg/ruby:2.7-node 8 | steps: 9 | - checkout 10 | - run: 11 | name: Install dependencies 12 | command: bundle install 13 | - run: 14 | name: Test 15 | command: bundle exec rake 16 | - run: 17 | name: 18 | Report coverage to codeclimate 19 | command: bundle exec codeclimate-test-reporter 20 | workflows: 21 | version: 2 22 | test: 23 | jobs: 24 | - test 25 | -------------------------------------------------------------------------------- /lib/bummr/git.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | class Git 3 | include Singleton 4 | include Log 5 | 6 | def initialize 7 | @git_commit = ENV.fetch("BUMMR_GIT_COMMIT") { "git commit" } 8 | end 9 | 10 | def add(files) 11 | system("git add #{files}") 12 | end 13 | 14 | def commit(message) 15 | log "Commit: #{message}".color(:green) 16 | system("#{git_commit} -m '#{message}'") 17 | end 18 | 19 | def rebase_interactive(sha) 20 | system("git rebase -i #{BASE_BRANCH}") unless HEADLESS 21 | end 22 | 23 | def message(sha) 24 | `git log --pretty=format:'%s' -n 1 #{sha}` 25 | end 26 | 27 | private 28 | 29 | attr_reader :git_commit 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/log_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bummr::Log do 4 | let(:object) { Object.new } 5 | let(:message) { "test message" } 6 | 7 | before do 8 | `mkdir -p log` 9 | object.extend(Bummr::Log) 10 | end 11 | 12 | after do 13 | `rm log/bummr.log` 14 | end 15 | 16 | describe "#log" do 17 | it "puts the message" do 18 | allow(STDOUT).to receive(:puts) 19 | 20 | object.log message 21 | 22 | expect(STDOUT).to have_received(:puts).with(message) 23 | end 24 | 25 | it "outputs the message to log/bummr.log" do 26 | object.log message 27 | 28 | result = `cat log/bummr.log` 29 | 30 | expect(result).to eq message + "\n" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/bummr/bisecter.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | class Bisecter 3 | include Singleton 4 | 5 | def bisect 6 | puts "Bad commits found! Bisecting...".color(:red) 7 | 8 | system("bundle") 9 | system("git bisect start") 10 | system("git bisect bad") 11 | system("git bisect good #{BASE_BRANCH}") 12 | 13 | Open3.popen2e("git bisect run #{TEST_COMMAND}") do |_std_in, std_out_err| 14 | while line = std_out_err.gets 15 | puts line 16 | 17 | sha_regex = Regexp::new("(.*) is the first bad commit\n").match(line) 18 | unless sha_regex.nil? 19 | sha = sha_regex[1] 20 | end 21 | 22 | if line == "bisect run success\n" 23 | Bummr::Remover.instance.remove_commit(sha) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/lib/remover_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bummr::Remover do 4 | let(:remover) { Bummr::Remover.instance } 5 | let(:git) { Bummr::Git.instance } 6 | let(:sha) { "testsha" } 7 | 8 | before do 9 | allow(remover).to receive(:log) 10 | allow(remover).to receive(:system) 11 | allow(remover).to receive(:yes?).and_return(true) 12 | end 13 | 14 | describe "#remove_commit" do 15 | it "logs the bad commit" do 16 | allow(git).to receive(:message).and_return("commit message") 17 | 18 | remover.remove_commit(sha) 19 | 20 | expect(remover).to have_received(:log).with( 21 | "Bad commit: commit message, #{sha}".color(:red) 22 | ) 23 | end 24 | 25 | it "resets the bisection" do 26 | remover.remove_commit(sha) 27 | 28 | expect(remover).to have_received(:system).with("git bisect reset") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/bummr/remover.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | class Remover < Thor 3 | include Singleton 4 | include Log 5 | include Scm 6 | 7 | desc "remove_commit", "Remove a commit from the history" 8 | def remove_commit(sha) 9 | log "Bad commit: #{git.message(sha)}, #{sha}".color(:red) 10 | log "Resetting..." 11 | system("git bisect reset") 12 | 13 | message = "\nThe commit:\n\n `#{sha} #{git.message(sha)}`\n\n" + 14 | "Is breaking the build.\n\n" + 15 | "Please do one of the following: \n\n" + 16 | " 1. Update your code to work with the latest version of this gem.\n\n" + 17 | " 2. Perform the following steps to lock the gem version:\n\n" + 18 | " - `git reset --hard main`\n" + 19 | " - Lock the version of this Gem in your Gemfile.\n" + 20 | " - Commit the changes.\n" + 21 | " - Run `bummr update` again.\n\n" 22 | 23 | puts message.color(:yellow) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/lib/bisecter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Bummr::Bisecter do 4 | let(:std_out_err_bad_commit) { 5 | output = String.new 6 | output += "mybadcommit is the first bad commit\n" 7 | output += "bisect run success\n" 8 | StringIO.new(output) 9 | } 10 | let(:bisecter) { described_class.instance } 11 | let(:remover) { Bummr::Remover.instance } 12 | 13 | before do 14 | allow(STDOUT).to receive(:puts) 15 | allow(bisecter).to receive(:system).with("bundle") 16 | allow(bisecter).to receive(:system) 17 | end 18 | 19 | describe "#bisect" do 20 | context "bad commit" do 21 | it "rebases it out" do 22 | allow(Open3).to receive(:popen2e).and_yield(nil, std_out_err_bad_commit) 23 | allow(remover).to receive(:remove_commit) 24 | .with("mybadcommit") 25 | 26 | bisecter.bisect 27 | 28 | expect(remover).to have_received(:remove_commit).with("mybadcommit") 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /spec/lib/prompt_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bummr::Prompt do 4 | let(:parent_class) do 5 | Class.new do 6 | def yes?(message) 7 | "called parent with #{message}" 8 | end 9 | end 10 | end 11 | let(:object_class) { Class.new(parent_class) } 12 | let(:object) { object_class.new } 13 | 14 | before do 15 | object.extend(Bummr::Prompt) 16 | end 17 | 18 | describe "#yes?" do 19 | context "when HEADLESS is false" do 20 | it "calls super" do 21 | stub_const("HEADLESS", false) 22 | 23 | expect( 24 | object.yes?("foo") 25 | ).to eq "called parent with foo" 26 | end 27 | end 28 | 29 | context "when HEADLESS is nil" do 30 | it "calls super" do 31 | stub_const("HEADLESS", nil) 32 | 33 | expect( 34 | object.yes?("foo") 35 | ).to eq "called parent with foo" 36 | end 37 | end 38 | 39 | context "when HEADLESS is true" do 40 | it "returns true and skips asking for user input" do 41 | stub_const("HEADLESS", true) 42 | 43 | expect( 44 | object.yes?("foo") 45 | ).to eq true 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/bummr/outdated.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'singleton' 3 | require 'bundler' 4 | 5 | module Bummr 6 | class Outdated 7 | include Singleton 8 | 9 | def outdated_gems(options = {}) 10 | results = [] 11 | 12 | bundle_options = "" 13 | bundle_options << " --parseable" if Gem::Version.new(Bundler::VERSION) >= Gem::Version.new("2") 14 | bundle_options << " --strict" unless options[:all_gems] 15 | bundle_options << " --group #{options[:group]}" if options[:group] 16 | bundle_options << " #{options[:gem]}" if options[:gem] 17 | 18 | Open3.popen2("bundle outdated" + bundle_options) do |_std_in, std_out| 19 | while line = std_out.gets 20 | puts line 21 | gem = parse_gem_from(line) 22 | 23 | if gem && (options[:all_gems] || gemfile_contains(gem[:name])) 24 | results.push gem 25 | end 26 | end 27 | end 28 | 29 | results 30 | end 31 | 32 | def parse_gem_from(line) 33 | regex = /(?:\s+\* )?(.*) \(newest (\d[\d\.]*\d)[,\s] installed (\d[\d\.]*\d)[\),\s]/.match line 34 | 35 | unless regex.nil? 36 | { name: regex[1], newest: regex[2], installed: regex[3] } 37 | end 38 | end 39 | 40 | private 41 | 42 | def gemfile_contains(gem_name) 43 | /gem ['"]#{gem_name}['"]/.match gemfile 44 | end 45 | 46 | def gemfile 47 | @gemfile ||= `cat Gemfile` 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/bummr/updater.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | class Updater 3 | include Log 4 | include Scm 5 | 6 | def initialize(outdated_gems) 7 | @outdated_gems = outdated_gems 8 | end 9 | 10 | def update_gems 11 | puts "Updating outdated gems".color(:green) 12 | 13 | @outdated_gems.each_with_index do |gem, index| 14 | update_gem(gem, index) 15 | end 16 | end 17 | 18 | def update_gem(gem, index) 19 | puts "Updating #{gem[:name]}: #{index + 1} of #{@outdated_gems.count}" 20 | system("bundle update #{gem[:name]}") 21 | 22 | updated_version = updated_version_for(gem) 23 | 24 | message = if updated_version 25 | "Update #{gem[:name]} from #{gem[:installed]} to #{updated_version}" 26 | else 27 | "Update dependencies for #{gem[:name]}" 28 | end 29 | 30 | if gem[:installed] == updated_version 31 | log("#{gem[:name]} not updated") 32 | return 33 | end 34 | 35 | if gem[:newest] != updated_version 36 | log("#{gem[:name]} not updated from #{gem[:installed]} to latest: #{gem[:newest]}") 37 | end 38 | 39 | git.add("Gemfile") 40 | git.add("Gemfile.lock") 41 | git.add("vendor/cache") 42 | git.commit(message) 43 | end 44 | 45 | def updated_version_for(gem) 46 | string = `bundle list --paths | grep "#{gem[:name]}"` 47 | string.match(/#{File::SEPARATOR}#{gem[:name]}-(.*)$/)[1] 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /bummr.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'bummr/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "bummr" 8 | spec.version = Bummr::VERSION 9 | spec.authors = ["Lee Pender"] 10 | spec.email = ["lpender@gmail.com"] 11 | spec.summary = %q{Helper script to intelligently update your Gemfile} 12 | spec.description = %q{See Readme} 13 | spec.homepage = "https://github.com/lpender/bummr" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "thor" 22 | spec.add_dependency "rainbow" 23 | 24 | spec.add_development_dependency "rspec" 25 | spec.add_development_dependency "rspec-nc" 26 | spec.add_development_dependency "simplecov" 27 | spec.add_development_dependency "spring" 28 | spec.add_development_dependency "bundler" 29 | spec.add_development_dependency "rake" 30 | spec.add_development_dependency "jet_black", "~> 0.3" 31 | spec.add_development_dependency "pry" 32 | spec.add_development_dependency "pry-remote" 33 | spec.add_development_dependency "pry-nav" 34 | spec.add_development_dependency "codeclimate-test-reporter", "~> 1.0.0" 35 | end 36 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | bummr (1.1.0) 5 | rainbow 6 | thor 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | codeclimate-test-reporter (1.0.9) 12 | simplecov (<= 0.13) 13 | coderay (1.1.3) 14 | diff-lcs (1.5.1) 15 | docile (1.1.5) 16 | jet_black (0.7.1) 17 | json (2.7.2) 18 | method_source (1.1.0) 19 | pry (0.14.2) 20 | coderay (~> 1.1) 21 | method_source (~> 1.0) 22 | pry-nav (1.0.0) 23 | pry (>= 0.9.10, < 0.15) 24 | pry-remote (0.1.8) 25 | pry (~> 0.9) 26 | slop (~> 3.0) 27 | rainbow (3.1.1) 28 | rake (13.2.1) 29 | rspec (3.13.0) 30 | rspec-core (~> 3.13.0) 31 | rspec-expectations (~> 3.13.0) 32 | rspec-mocks (~> 3.13.0) 33 | rspec-core (3.13.0) 34 | rspec-support (~> 3.13.0) 35 | rspec-expectations (3.13.0) 36 | diff-lcs (>= 1.2.0, < 2.0) 37 | rspec-support (~> 3.13.0) 38 | rspec-mocks (3.13.1) 39 | diff-lcs (>= 1.2.0, < 2.0) 40 | rspec-support (~> 3.13.0) 41 | rspec-nc (0.3.0) 42 | rspec (>= 3) 43 | terminal-notifier (>= 1.4) 44 | rspec-support (3.13.1) 45 | simplecov (0.13.0) 46 | docile (~> 1.1.0) 47 | json (>= 1.8, < 3) 48 | simplecov-html (~> 0.10.0) 49 | simplecov-html (0.10.2) 50 | slop (3.6.0) 51 | spring (4.2.1) 52 | terminal-notifier (2.0.0) 53 | thor (1.3.1) 54 | 55 | PLATFORMS 56 | ruby 57 | 58 | DEPENDENCIES 59 | bummr! 60 | bundler 61 | codeclimate-test-reporter (~> 1.0.0) 62 | jet_black (~> 0.3) 63 | pry 64 | pry-nav 65 | pry-remote 66 | rake 67 | rspec 68 | rspec-nc 69 | simplecov 70 | spring 71 | 72 | BUNDLED WITH 73 | 2.4.22 74 | -------------------------------------------------------------------------------- /spec/black_box/bummr_update_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "jet_black" 3 | 4 | describe "bummr update command" do 5 | let(:session) { JetBlack::Session.new(options: { clean_bundler_env: true }) } 6 | let(:bummr_gem_path) { File.expand_path("../../", __dir__) } 7 | 8 | it "updates outdated gems" do 9 | session.create_file "Gemfile", <<~RUBY 10 | source "https://rubygems.org" 11 | gem "rake", "~> 10.0" 12 | gem "bummr", path: "#{bummr_gem_path}" 13 | RUBY 14 | 15 | session.create_file "Rakefile", <<~RUBY 16 | task :default do 17 | puts "Hello from the Rakefile" 18 | end 19 | RUBY 20 | 21 | expect(session.run("bundle install --retry 3")). 22 | to be_a_success 23 | 24 | # Now allow newer versions of Rake to be installed 25 | session.run("sed -i.bak 's/, \"~> 10.0\"//' Gemfile") 26 | 27 | session.run("mkdir -p log") 28 | 29 | expect(session.run("git init .")). 30 | to be_a_success.and have_stdout("Initialized empty Git repository") 31 | 32 | session.run("git config user.name 'Bummr Test'") 33 | session.run("git config user.email 'test@example.com'") 34 | 35 | expect(session.run("git add . && git commit -m 'Initial commit'")). 36 | to be_a_success.and have_stdout("Initial commit") 37 | 38 | session.run("git checkout -b bummr-branch") 39 | 40 | update_result = session.run( 41 | "bundle exec bummr update", 42 | stdin: "y\ny\ny\n", 43 | env: { EDITOR: nil, BUMMR_HEADLESS: "true" }, 44 | ) 45 | 46 | rake_gem_updated = /Update rake from 10\.\d\.\d to 1[1-9]\.\d\.\d/ 47 | 48 | expect(update_result). 49 | to be_a_success.and have_stdout(rake_gem_updated) 50 | 51 | expect(update_result).to have_stdout("Passed the build!") 52 | 53 | expect(session.run("git log")). 54 | to be_a_success.and have_stdout(rake_gem_updated) 55 | 56 | expect(session.run("bundle show")). 57 | to be_a_success.and have_stdout(/rake\s\(1[1-9]/) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/bummr/check.rb: -------------------------------------------------------------------------------- 1 | module Bummr 2 | class Check < Thor 3 | include Singleton 4 | include Bummr::Prompt 5 | 6 | desc "check", "Run automated checks to see if bummr can be run" 7 | def check(fullcheck=true) 8 | @errors = [] 9 | 10 | check_base_branch 11 | check_log 12 | check_status 13 | 14 | if fullcheck == true 15 | check_diff 16 | end 17 | 18 | if @errors.any? 19 | unless yes? "Bummr found errors! Do you want to continue anyway?".color(:red) 20 | exit 0 21 | end 22 | else 23 | puts "Ready to run bummr.".color(:green) 24 | end 25 | end 26 | 27 | private 28 | 29 | def check_base_branch 30 | if `git rev-parse --abbrev-ref HEAD` == "#{BASE_BRANCH}\n" 31 | message = "Bummr is not meant to be run on your base branch" 32 | puts message.color(:red) 33 | puts "Please checkout a branch with 'git checkout -b update-gems'" 34 | @errors.push message 35 | end 36 | end 37 | 38 | def check_log 39 | unless File.directory? "log" 40 | message = "There is no log directory or you are not in the root" 41 | puts message.color(:red) 42 | @errors.push message 43 | end 44 | end 45 | 46 | def check_status 47 | status = `git status` 48 | 49 | if status.index 'are currently' 50 | message = "" 51 | 52 | if status.index 'rebasing' 53 | message += "You are already rebasing. " 54 | elsif status.index 'bisecting' 55 | message += "You are already bisecting. " 56 | end 57 | 58 | message += "Make sure `git status` is clean" 59 | puts message.color(:red) 60 | @errors.push message 61 | end 62 | end 63 | 64 | def check_diff 65 | unless `git diff #{BASE_BRANCH}`.empty? 66 | message = "Please make sure that `git diff #{BASE_BRANCH}` returns empty" 67 | puts message.color(:red) 68 | @errors.push message 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/lib/git_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bummr::Git do 4 | describe "#add" do 5 | it "stages specified files with git" do 6 | git = stub_git 7 | files = "Gemfile Gemfile.lock" 8 | 9 | git.add(files) 10 | 11 | expect(git).to have_received(:system).with( 12 | "git add #{files}" 13 | ) 14 | end 15 | end 16 | 17 | describe "#commit" do 18 | it "logs the commit" do 19 | git = stub_git 20 | commit_message = "Update Foo from 0.0.1 to 0.0.2" 21 | 22 | git.commit(commit_message) 23 | 24 | expect(git).to have_received(:log).with( 25 | /Commit: #{commit_message}/ 26 | ) 27 | end 28 | 29 | it "commits with a message" do 30 | git = stub_git 31 | commit_message = "Update Foo from 0.0.1 to 0.0.2" 32 | 33 | git.commit(commit_message) 34 | 35 | expect(git).to have_received(:system).with( 36 | "git commit -m '#{commit_message}'" 37 | ) 38 | end 39 | 40 | describe "when BUMMR_GIT_COMMIT is defined" do 41 | it "commits using defined value" do 42 | allow(ENV).to receive(:fetch).with("BUMMR_GIT_COMMIT").and_return("git commit --no-verify") 43 | git = stub_git 44 | commit_message = "Update Foo from 0.0.1 to 0.0.2" 45 | 46 | git.commit(commit_message) 47 | 48 | expect(git).to have_received(:system).with( 49 | "git commit --no-verify -m '#{commit_message}'" 50 | ) 51 | end 52 | end 53 | end 54 | 55 | describe "#rebase_interactive" do 56 | it "runs git interactive rebase to the given sha" do 57 | git = stub_git 58 | sha = "b39dcd8" 59 | 60 | git.rebase_interactive(sha) 61 | 62 | expect(git).to have_received(:system).with( 63 | "git rebase -i #{BASE_BRANCH}" 64 | ) 65 | end 66 | end 67 | 68 | describe "#message" do 69 | it "displays the commit message for a given sha" do 70 | git = stub_git 71 | sha = "b39dcd8" 72 | 73 | git.message(sha) 74 | 75 | expect(git).to have_received(:`).with( 76 | "git log --pretty=format:'%s' -n 1 #{sha}" 77 | ) 78 | end 79 | end 80 | 81 | def stub_git 82 | git = Bummr::Git.clone.instance 83 | allow(git).to receive(:log) 84 | allow(git).to receive(:system) 85 | allow(git).to receive(:`) 86 | git 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/bummr/cli.rb: -------------------------------------------------------------------------------- 1 | TEST_COMMAND = ENV["BUMMR_TEST"] || "bundle exec rake" 2 | BASE_BRANCH = ENV["BASE_BRANCH"] || "main" 3 | HEADLESS = ENV["BUMMR_HEADLESS"] || false 4 | 5 | module Bummr 6 | class CLI < Thor 7 | include Bummr::Log 8 | include Bummr::Prompt 9 | include Bummr::Scm 10 | 11 | desc "check", "Run automated checks to see if bummr can be run" 12 | def check(fullcheck=true) 13 | Bummr::Check.instance.check(fullcheck) 14 | end 15 | 16 | desc "update", 17 | "Update outdated gems, run tests, bisect if tests fail\n\n" + 18 | "--all: Update indirect dependencies\n" + 19 | "--group: Specify a group from the Gemfile to update\n" + 20 | "--gem: Specify a specific gem to update\n" + 21 | "\n" 22 | 23 | 24 | method_option :all, type: :boolean, default: false 25 | method_option :group, type: :string 26 | method_option :gem, type: :string 27 | 28 | def update 29 | system("bundle install") 30 | display_info 31 | 32 | if yes? "Are you ready to use Bummr? (y/n)" 33 | check 34 | log("Bummr update initiated #{Time.now}") 35 | 36 | outdated_gems = Bummr::Outdated.instance.outdated_gems( 37 | all_gems: options[:all], group: options[:group], gem: options[:gem] 38 | ) 39 | 40 | if outdated_gems.empty? 41 | puts "No outdated gems to update".color(:green) 42 | else 43 | Bummr::Updater.new(outdated_gems).update_gems 44 | 45 | git.rebase_interactive(BASE_BRANCH) 46 | test 47 | end 48 | else 49 | puts "Thank you!".color(:green) 50 | end 51 | end 52 | 53 | desc "test", "Test for a successful build and bisect if necesssary" 54 | def test 55 | check(false) 56 | 57 | if yes? "Do you want to test the build now? (y/n)" 58 | system "bundle install" 59 | puts "Testing the build!".color(:green) 60 | 61 | if system(TEST_COMMAND) == false 62 | bisect 63 | else 64 | puts "Passed the build!".color(:green) 65 | puts "See log/bummr.log for details".color(:yellow) 66 | end 67 | end 68 | end 69 | 70 | desc "bisect", "Find the bad commit, remove it, test again" 71 | def bisect 72 | check(false) 73 | 74 | if yes? "Would you like to bisect in order to find which gem is causing " + 75 | "your build to break? (y/n)" 76 | Bummr::Bisecter.instance.bisect 77 | end 78 | end 79 | 80 | desc "remove_commit", "Remove a commit from the history" 81 | def remove_commit(sha) 82 | Bummr::Remover.instance.remove_commit(sha) 83 | end 84 | 85 | private 86 | 87 | def display_info 88 | puts "Bummr #{VERSION}" 89 | puts "To run Bummr, you must:" 90 | puts "- Be in the root path of a clean git branch off of " + "#{BASE_BRANCH}".color(:yellow) 91 | puts "- Have no commits or local changes" 92 | puts "- Have a 'log' directory, where we can place logs" 93 | puts "- Have your build configured to fail fast (recommended)" 94 | puts "- Have locked any Gem version that you don't wish to update in your Gemfile" 95 | puts "- It is recommended that you lock your versions of `ruby` and `rails` in your `Gemfile`" 96 | puts "\n" 97 | puts "Your test command is: " + "'#{TEST_COMMAND}'".color(:yellow) 98 | puts "\n" 99 | print_received_options 100 | end 101 | 102 | def print_received_options 103 | puts "Bummr will run with the following options:" 104 | 105 | options.each do |key, value| 106 | puts "--#{key.color(:yellow)}: #{value}" 107 | end 108 | 109 | puts "\nRun `#{"bummr help update".color(:yellow)}` for more information.\n\n" 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/check_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Bummr::Check do 4 | let(:check) { Bummr::Check.instance } 5 | 6 | before(:each) do 7 | allow(check) 8 | .to receive(:check_base_branch).and_return(nil) 9 | allow(check) 10 | .to receive(:check_log).and_return(nil) 11 | allow(check) 12 | .to receive(:check_status).and_return(nil) 13 | allow(check) 14 | .to receive(:check_diff).and_return(nil) 15 | allow(check).to receive(:puts) 16 | allow(check).to receive(:yes?).and_return(false) 17 | allow(check).to receive(:exit).and_return(false) 18 | end 19 | 20 | describe "#check" do 21 | context "all checks pass" do 22 | context "not full check" do 23 | it "returns 'Ready to run bummr.' and proceeds" do 24 | check.check 25 | 26 | expect(check).to have_received(:puts).with("Ready to run bummr.".color(:green)) 27 | end 28 | end 29 | 30 | context "full check" do 31 | it "returns 'Ready to run bummr.' and proceeds" do 32 | check.check(true) 33 | 34 | expect(check).to have_received(:puts).with("Ready to run bummr.".color(:green)) 35 | end 36 | end 37 | end 38 | 39 | context "check_base_branch fails" do 40 | it "reports the error and exits after confirm" do 41 | allow(check) 42 | .to receive(:check_base_branch).and_call_original 43 | allow(check).to receive(:`).with('git rev-parse --abbrev-ref HEAD') 44 | .and_return "main\n" 45 | 46 | check.check 47 | 48 | expect(check).to have_received(:puts) 49 | .with("Bummr is not meant to be run on your base branch".color(:red)) 50 | expect(check).to have_received(:yes?) 51 | expect(check).to have_received(:exit).with(0) 52 | end 53 | end 54 | 55 | context "check_log fails" do 56 | it "reports the error and exits after confirm" do 57 | allow(check) 58 | .to receive(:check_log).and_call_original 59 | allow(File).to receive(:directory?).with('log') 60 | .and_return false 61 | 62 | check.check 63 | 64 | expect(check).to have_received(:puts) 65 | .with("There is no log directory or you are not in the root".color(:red)) 66 | expect(check).to have_received(:yes?) 67 | expect(check).to have_received(:exit).with(0) 68 | end 69 | end 70 | 71 | context "check_status fails" do 72 | context "due to bisecting" do 73 | before do 74 | allow(check) 75 | .to receive(:check_status).and_call_original 76 | allow(check).to receive(:`).with('git status') 77 | .and_return "are currently bisecting" 78 | end 79 | 80 | it "reports the error and exits after confirm" do 81 | check.check 82 | 83 | expect(check).to have_received(:puts) 84 | .with("You are already bisecting. Make sure `git status` is clean".color(:red)) 85 | expect(check).to have_received(:yes?) 86 | expect(check).to have_received(:exit).with(0) 87 | end 88 | end 89 | 90 | context "due to rebasing" do 91 | before do 92 | allow(check).to receive(:check_status).and_call_original 93 | allow(check).to receive(:`).with('git status') 94 | .and_return "are currently rebasing" 95 | end 96 | 97 | it "reports the error and exits after confirm" do 98 | check.check 99 | 100 | expect(check).to have_received(:puts) 101 | .with("You are already rebasing. Make sure `git status` is clean".color(:red)) 102 | expect(check).to have_received(:yes?) 103 | expect(check).to have_received(:exit).with(0) 104 | end 105 | end 106 | end 107 | 108 | context "check_diff fails" do 109 | before do 110 | allow(check).to receive(:check_diff).and_call_original 111 | allow(check).to receive(:`).with('git diff main') 112 | .and_return "+ file" 113 | end 114 | 115 | it "reports the error and exits after confirm" do 116 | check.check(true) 117 | 118 | expect(check).to have_received(:puts) 119 | .with("Please make sure that `git diff main` returns empty".color(:red)) 120 | expect(check).to have_received(:yes?) 121 | expect(check).to have_received(:exit).with(0) 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bummr 2 | 3 | Updating Gems one by one is a bumm(e)r: especially when one gem causes your build 4 | to fail. 5 | 6 | Gems should be updated in [separate commits](https://thoughtbot.com/blog/keep-your-gems-up-to-date). 7 | 8 | The bummr gem allows you to automatically update all gems which pass your 9 | build in separate commits, and logs the name and sha of any gem update that fails your build. 10 | 11 | Bummr assumes you have good test coverage and follow a [pull-request workflow]. 12 | 13 | By default, bummr will assume your base branch is named `main`. If you would 14 | like to designate a different base branch, you can set the `BASE_BRANCH` 15 | environment variable: `export BASE_BRANCH='main'` 16 | 17 | ## Compatibility 18 | 19 | | Bundler Version | Bummr Version | 20 | | --------------- | ------------- | 21 | | <= 2.1 | <= 0.6.0 | 22 | | >= 2.2 | >= 1.0.0 | 23 | 24 | ## Setup 25 | 26 | ```bash 27 | $ gem install bummr 28 | ``` 29 | 30 | To run headless (skip interactive rebasing/confirmation), use 31 | `BUMMR_HEADLESS=true bundle exec bummr update`. 32 | 33 | By default, bummr will use `bundle exec rake` to run your build. 34 | 35 | To customize your build command, `export BUMMR_TEST="./bummr-build.sh"` 36 | 37 | If you prefer, you can [run the build more than once], to protect against 38 | brittle tests and false positives. 39 | 40 | [run the build more than once]: https://gist.github.com/lpender/f6b55e7f3649db3b6df5 41 | 42 | ## Usage 43 | 44 | Using bummr can take anywhere from a few minutes to several hours, depending 45 | on the number of outdated gems you have and the number of tests in your test 46 | suite. 47 | 48 | For the purpose of these instructions, we are assuming that your base branch is 49 | `main`. If you would like to specify a different base branch, see the 50 | instructions in the Installation section of this README. 51 | 52 | - After installing, create a new, clean branch off of your main branch. 53 | - Run `bummr update`. This may take some time. 54 | - `Bummr` will give you the opportunity to interactively rebase your branch 55 | before running the tests. Careful. 56 | - At this point, you can leave `bummr` to work for some time. 57 | - If your build fails, `bummr` will notify you of failures, logging the failures to 58 | `log/bummr.log`. At this point it is recommended that you lock that gem version in 59 | your Gemfile and start the process over from the top. Alternatively, you may wish 60 | to implement code changes which fix the problem. 61 | - Once your build passes, open a pull-request and merge it to your main branch. 62 | 63 | ##### `bummr update` 64 | 65 | - Options: 66 | 67 | - `--all` to include indirect dependencies (`bummr` defaults to direct dependencies only) 68 | - `--group` to update only gems from a specific group (i.e. `test`, `development`) 69 | - `--gem` to update only a specific gem (i.e. `tzinfo`) 70 | 71 | - Finds all your outdated gems 72 | - Updates them each individually, using `bundle update --source #{gemname}`. To use a less 73 | conservative update strategy, start `bummr update` with the `--all` option. 74 | - Commits each gem update separately, with a commit message like: 75 | 76 | ``` 77 | Update gemname from 0.0.1 to 0.0.2 78 | ``` 79 | 80 | - Runs `git rebase -i main` to allow you the chance to review and make changes. 81 | - Runs `bummr test` 82 | 83 | ##### `bummr test` 84 | 85 | - Runs your build script (`.bummr-build.sh`). 86 | - If there is a failure, runs `bummr bisect`. 87 | 88 | ##### `bummr bisect` 89 | 90 | - `git bisect`s against main. 91 | - Upon finding the bad commit, runs `git bisect reset` and notifies the developer on 92 | how best to proceed. 93 | - Logs the bad commit in `log/bummr.log`. 94 | 95 | ## Notes 96 | 97 | - Bummr assumes you have good test coverage and follow a [pull-request workflow] 98 | with `main` as your default branch. 99 | - Once the build passes, you can push your branch and create a pull-request! 100 | - You may wish to `tail -f log/bummr.log` in a separate terminal window so you 101 | can see which commits are being removed. 102 | 103 | ## License 104 | 105 | See LICENSE 106 | 107 | ## Developing 108 | 109 | Set version in `lib/bummr/version.rb` 110 | 111 | `rake build` to build locally 112 | 113 | `gem install --local ./pkg/bummr-X.X.X.gem` with the ruby version of the app 114 | you'd like to use it with. 115 | 116 | `rake` will run the suite of unit tests. 117 | 118 | `gem push ./pkg/bummr-x.x.x.gem` to publish new versions. 119 | 120 | The suite relies on Oliver Peate's [jet 121 | black](https://github.com/odlp/jet_black) testing library for command line feature 122 | tests. 123 | 124 | ## Thank you! 125 | 126 | Thanks to Ryan Sonnek for the [Bundler 127 | Updater](https://github.com/wireframe/bundler-updater) gem. 128 | 129 | [pull-request workflow]: https://help.github.com/articles/using-pull-requests 130 | -------------------------------------------------------------------------------- /spec/lib/updater_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bummr::Updater do 4 | let(:outdated_gems) { 5 | [ 6 | { name: "myGem", installed: "0.3.2", newest: "0.3.5" }, 7 | { name: "otherGem", installed: "1.3.2.23", newest: "1.6.5" }, 8 | { name: "thirdGem", installed: "4.3.4", newest: "5.6.45" }, 9 | ] 10 | } 11 | let(:gem) { outdated_gems[0] } 12 | let(:updater) { described_class.new(outdated_gems) } 13 | let(:newest) { outdated_gems[0][:newest] } 14 | let(:installed) { outdated_gems[0][:installed] } 15 | let(:intermediate_version) { "0.3.4" } 16 | let(:update_cmd) { "bundle update #{gem[:name]}" } 17 | let(:git) { Bummr::Git.instance } 18 | 19 | describe "#update_gems" do 20 | it "calls update_gem on each gem" do 21 | allow(updater).to receive(:update_gem) 22 | 23 | updater.update_gems 24 | 25 | outdated_gems.each_with_index do |gem, index| 26 | expect(updater).to have_received(:update_gem).with(gem, index) 27 | end 28 | end 29 | end 30 | 31 | describe "#update_gem" do 32 | it "attempts to update the gem" do 33 | allow(updater).to receive(:system).with(update_cmd) 34 | allow(updater).to receive(:updated_version_for).with(gem).and_return installed 35 | allow(updater).to receive(:log) 36 | allow(git).to receive(:commit) 37 | 38 | updater.update_gem(gem, 0) 39 | end 40 | 41 | context "not updated at all" do 42 | it "logs that it's not updated to the latest" do 43 | allow(updater).to receive(:system).with(update_cmd) 44 | allow(updater).to receive(:updated_version_for).with(gem).and_return installed 45 | allow(updater).to receive(:log) 46 | allow(git).to receive(:commit) 47 | 48 | updater.update_gem(gem, 0) 49 | 50 | expect(updater).to have_received(:log).with("#{gem[:name]} not updated") 51 | end 52 | 53 | it "doesn't commit anything" do 54 | allow(updater).to receive(:system).with(update_cmd) 55 | allow(updater).to receive(:updated_version_for).with(gem).and_return installed 56 | allow(updater).to receive(:log) 57 | allow(git).to receive(:commit) 58 | 59 | updater.update_gem(gem, 0) 60 | 61 | expect(git).to_not have_received(:commit) 62 | end 63 | end 64 | 65 | context "not updated to the newest version" do 66 | before(:each) do 67 | allow(updater).to receive(:updated_version_for).with(gem).and_return( 68 | intermediate_version 69 | ) 70 | end 71 | 72 | it "logs that it's not updated to the latest" do 73 | not_latest_message = 74 | "#{gem[:name]} not updated from #{gem[:installed]} to latest: #{gem[:newest]}" 75 | allow(updater).to receive(:system) 76 | allow(updater).to receive(:log) 77 | allow(git).to receive(:commit) 78 | 79 | updater.update_gem(gem, 0) 80 | 81 | expect(updater).to have_received(:log).with not_latest_message 82 | end 83 | 84 | it "commits" do 85 | commit_message = 86 | "Update #{gem[:name]} from #{gem[:installed]} to #{intermediate_version}" 87 | allow(updater).to receive(:system) 88 | allow(updater).to receive(:log) 89 | allow(git).to receive(:add) 90 | allow(git).to receive(:commit) 91 | 92 | updater.update_gem(gem, 0) 93 | 94 | expect(git).to have_received(:add).with("Gemfile") 95 | expect(git).to have_received(:add).with("Gemfile.lock") 96 | expect(git).to have_received(:add).with("vendor/cache") 97 | expect(git).to have_received(:commit).with(commit_message) 98 | end 99 | end 100 | 101 | context "updated the gem to the latest" do 102 | before(:each) do 103 | allow(updater).to receive(:updated_version_for).and_return newest 104 | end 105 | 106 | it "commits" do 107 | commit_message = 108 | "Update #{gem[:name]} from #{gem[:installed]} to #{gem[:newest]}" 109 | allow(updater).to receive(:system) 110 | allow(updater).to receive(:log) 111 | allow(git).to receive(:add) 112 | allow(git).to receive(:commit) 113 | 114 | updater.update_gem(gem, 0) 115 | 116 | expect(git).to have_received(:add).with("Gemfile") 117 | expect(git).to have_received(:add).with("Gemfile.lock") 118 | expect(git).to have_received(:add).with("vendor/cache") 119 | expect(git).to have_received(:commit).with(commit_message) 120 | end 121 | end 122 | end 123 | 124 | describe "#updated_version_for" do 125 | it "returns the correct version from bundle list" do 126 | allow(updater).to receive(:`).with( 127 | "bundle list --paths | grep \"#{gem[:name]}\"" 128 | ).and_return("asdf/asdf/asdf/#{gem[:name]}-3.5.2") 129 | 130 | expect(updater.updated_version_for(gem)).to eq "3.5.2" 131 | end 132 | 133 | it "returns the correct version when there are similarly named gems" do 134 | allow(updater).to receive(:`).with( 135 | "bundle list --paths | grep \"#{gem[:name]}\"" 136 | ).and_return(<<~BUNDLE_LIST) 137 | asdf/asdf/asdf/foo-#{gem[:name]}-1.2.3 138 | asdf/asdf/asdf/#{gem[:name]}-3.5.2 139 | BUNDLE_LIST 140 | 141 | expect(updater.updated_version_for(gem)).to eq "3.5.2" 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/lib/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Bummr::CLI do 4 | # https://github.com/wireframe/gitx/blob/171da367072b0e82d5906d1e5b3f8ff38e5774e7/spec/thegarage/gitx/cli/release_command_spec.rb#L9 5 | let(:args) { [] } 6 | let(:options) { {} } 7 | let(:config) { { pretend: true } } 8 | let(:cli) { described_class.new(args, options, config) } 9 | let(:git) { Bummr::Git.instance } 10 | let(:outdated_gems) { 11 | [ 12 | { name: "myGem", installed: "0.3.2", newest: "0.3.5" }, 13 | { name: "otherGem", installed: "1.3.2.23", newest: "1.6.5" }, 14 | { name: "thirdGem", installed: "4.3.4", newest: "5.6.45" }, 15 | ] 16 | } 17 | 18 | describe "#update" do 19 | context "when user rejects moving forward" do 20 | it "does not attempt to move forward" do 21 | expect(cli).to receive(:yes?).and_return(false) 22 | expect(cli).not_to receive(:check) 23 | 24 | cli.update 25 | end 26 | end 27 | 28 | context "when user agrees to move forward" do 29 | def mock_bummr_standard_flow 30 | updater = double 31 | allow(updater).to receive(:update_gems) 32 | 33 | expect(cli).to receive(:display_info) 34 | expect(cli).to receive(:yes?).and_return(true) 35 | expect(cli).to receive(:check) 36 | expect(cli).to receive(:log) 37 | expect(cli).to receive(:system).with("bundle install") 38 | expect(Bummr::Updater).to receive(:new).with(outdated_gems).and_return updater 39 | expect(cli).to receive(:test) 40 | expect(git).to receive(:rebase_interactive).with(BASE_BRANCH) 41 | end 42 | 43 | context "and there are no outdated gems" do 44 | it "informs that there are no outdated gems" do 45 | allow_any_instance_of(Bummr::Outdated).to receive(:outdated_gems) 46 | .and_return [] 47 | 48 | expect(cli).to receive(:display_info) 49 | expect(cli).to receive(:yes?).and_return(true) 50 | expect(cli).to receive(:check) 51 | expect(cli).to receive(:log) 52 | expect(cli).to receive(:system).with("bundle install") 53 | expect(cli).to receive(:puts).with("No outdated gems to update".color(:green)) 54 | 55 | cli.update 56 | end 57 | end 58 | 59 | context "and there are outdated gems" do 60 | it "calls 'update' on the updater" do 61 | allow_any_instance_of(Bummr::Outdated).to receive(:outdated_gems) 62 | .and_return outdated_gems 63 | 64 | mock_bummr_standard_flow 65 | 66 | cli.update 67 | end 68 | end 69 | 70 | describe "all option" do 71 | it "requests all outdated gems be listed" do 72 | options[:all] = true 73 | 74 | expect_any_instance_of(Bummr::Outdated) 75 | .to receive(:outdated_gems).with(hash_including({ all_gems: true })) 76 | .and_return outdated_gems 77 | 78 | mock_bummr_standard_flow 79 | 80 | cli.update 81 | end 82 | end 83 | 84 | describe "group option" do 85 | it "requests only outdated gems from supplied be listed" do 86 | options[:group] = 'test' 87 | 88 | expect_any_instance_of(Bummr::Outdated) 89 | .to receive(:outdated_gems).with(hash_including({ group: 'test' })) 90 | .and_return outdated_gems 91 | 92 | mock_bummr_standard_flow 93 | 94 | cli.update 95 | end 96 | end 97 | 98 | describe "gem option" do 99 | it "requests only outdated specific gem from supplied be listed" do 100 | options[:gem] = 'tzdata' 101 | 102 | expect_any_instance_of(Bummr::Outdated) 103 | .to receive(:outdated_gems).with(hash_including({ gem: 'tzdata' })) 104 | .and_return outdated_gems 105 | 106 | mock_bummr_standard_flow 107 | 108 | cli.update 109 | end 110 | end 111 | end 112 | 113 | context "when in headless mode" do 114 | context "and there are no outdated gems" do 115 | it "informs that there are no outdated gems" do 116 | stub_const("HEADLESS", true) 117 | allow_any_instance_of(Bummr::Outdated).to receive(:outdated_gems) 118 | .and_return [] 119 | 120 | expect(cli).to receive(:display_info) 121 | expect(cli).to receive(:check) 122 | expect(cli).to receive(:log) 123 | expect(cli).to receive(:system).with("bundle install") 124 | expect(cli).to receive(:puts).with("No outdated gems to update".color(:green)) 125 | 126 | cli.update 127 | end 128 | end 129 | end 130 | end 131 | 132 | describe "#test" do 133 | before do 134 | allow(STDOUT).to receive(:puts) 135 | allow(cli).to receive(:check) 136 | allow(cli).to receive(:system) 137 | allow(cli).to receive(:bisect) 138 | allow(cli).to receive(:yes?).and_return true 139 | end 140 | 141 | context "build passes" do 142 | it "reports that it passed the build, does not bisect" do 143 | allow(cli).to receive(:system).with("bundle exec rake").and_return true 144 | 145 | cli.test 146 | 147 | expect(cli).to have_received(:check).with(false) 148 | expect(cli).to have_received(:system).with("bundle install") 149 | expect(cli).to have_received(:system).with("bundle exec rake") 150 | expect(cli).not_to have_received(:bisect) 151 | end 152 | end 153 | 154 | context "build fails" do 155 | it "bisects" do 156 | allow(cli).to receive(:system).with("bundle exec rake").and_return false 157 | 158 | cli.test 159 | 160 | expect(cli).to have_received(:check).with(false) 161 | expect(cli).to have_received(:system).with("bundle install") 162 | expect(cli).to have_received(:system).with("bundle exec rake") 163 | expect(cli).to have_received(:bisect) 164 | end 165 | end 166 | end 167 | 168 | describe "#bisect" do 169 | it "calls Bummr:Bisecter.instance.bisect" do 170 | allow(cli).to receive(:check) 171 | allow(cli).to receive(:yes?).and_return true 172 | allow_any_instance_of(Bummr::Bisecter).to receive(:bisect) 173 | bisecter = Bummr::Bisecter.instance 174 | 175 | cli.bisect 176 | 177 | expect(bisecter).to have_received(:bisect) 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/lib/outdated_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Bummr::Outdated do 4 | # https://github.com/wireframe/gitx/blob/8e3cdc8b5d0c2082ed3daaf2fc054654b2e7a6c8/spec/gitx/executor_spec.rb#L9 5 | let(:stdoutput_legacy) { 6 | output = String.new 7 | output += " * devise (newest 4.1.1, installed 3.5.2) in group \"default\"\n" 8 | output += " * rake (newest 11.1.2, installed 10.4.2)\n" 9 | output += " * rails (newest 4.2.6, installed 4.2.5.1, requested ~> 4.2.0) in group \"default\"\n" 10 | output += " * spring (newest 4.2.6, installed 4.2.5.1, requested ~> 4.2.0) in group \"development\"\n" 11 | output += " * indirect_dep (newest 1.0.0, installed 0.0.1)\n" 12 | StringIO.new(output) 13 | } 14 | 15 | let(:stdoutput) { 16 | output = stdoutput_legacy.string.gsub(/^([\s*]+)/, "") 17 | StringIO.new(output) 18 | } 19 | 20 | let(:gemfile) { 21 | gemfile = String.new 22 | gemfile += "gem 'devise'\n" 23 | gemfile += "gem 'rake'\n" 24 | gemfile += "gem 'rails'\n" 25 | gemfile += "gem 'spring', :group => :development\n" 26 | gemfile 27 | } 28 | 29 | describe "#outdated_gems" do 30 | { bundler2: :stdoutput, bundler1: :stdoutput_legacy }.each_pair do |version, output| 31 | it "Correctly identifies outdated gems with bundler #{version}" do 32 | allow(Open3).to receive(:popen2).and_yield(nil, public_send(output)) 33 | allow_any_instance_of(described_class).to receive(:gemfile).and_return gemfile 34 | 35 | instance = Bummr::Outdated.instance 36 | result = instance.outdated_gems 37 | 38 | expect(result[0][:name]).to eq('devise') 39 | expect(result[0][:newest]).to eq('4.1.1') 40 | expect(result[0][:installed]).to eq('3.5.2') 41 | 42 | expect(result[1][:name]).to eq('rake') 43 | expect(result[1][:newest]).to eq('11.1.2') 44 | expect(result[1][:installed]).to eq('10.4.2') 45 | 46 | expect(result[2][:name]).to eq('rails') 47 | expect(result[2][:newest]).to eq('4.2.6') 48 | expect(result[2][:installed]).to eq('4.2.5.1') 49 | 50 | expect(result[3][:name]).to eq('spring') 51 | expect(result[3][:newest]).to eq('4.2.6') 52 | expect(result[3][:installed]).to eq('4.2.5.1') 53 | end 54 | end 55 | 56 | describe "all gems option" do 57 | it "lists all outdated dependencies by omitting the strict option" do 58 | allow(Open3).to receive(:popen2).with("bundle outdated --parseable").and_yield(nil, stdoutput) 59 | 60 | allow(Bummr::Outdated.instance).to receive(:gemfile).and_return gemfile 61 | 62 | results = Bummr::Outdated.instance.outdated_gems(all_gems: true) 63 | gem_names = results.map { |result| result[:name] } 64 | 65 | expect(gem_names).to include "indirect_dep" 66 | end 67 | 68 | it "defaults to false" do 69 | expect(Open3).to receive(:popen2).with("bundle outdated --parseable --strict").and_yield(nil, stdoutput) 70 | 71 | allow(Bummr::Outdated.instance).to receive(:gemfile).and_return gemfile 72 | 73 | results = Bummr::Outdated.instance.outdated_gems 74 | gem_names = results.map { |result| result[:name] } 75 | 76 | expect(gem_names).to_not include "indirect_dep" 77 | end 78 | end 79 | 80 | describe "group option" do 81 | let(:stdoutput_from_development_group) { 82 | output = String.new 83 | output += "spring (newest 4.2.6, installed 4.2.5.1, requested ~> 4.2.0)" 84 | StringIO.new(output) 85 | } 86 | 87 | it "lists outdated gems only from supplied group" do 88 | allow(Open3).to receive(:popen2) 89 | .with("bundle outdated --parseable --strict --group development") 90 | .and_yield(nil, stdoutput_from_development_group) 91 | 92 | allow(Bummr::Outdated.instance).to receive(:gemfile).and_return gemfile 93 | 94 | results = Bummr::Outdated.instance.outdated_gems(group: :development) 95 | gem_names = results.map { |result| result[:name] } 96 | 97 | expect(gem_names).to match_array ['spring'] 98 | end 99 | 100 | it "defaults to all groups" do 101 | allow(Open3).to receive(:popen2) 102 | .with("bundle outdated --parseable --strict") 103 | .and_yield(nil, stdoutput) 104 | 105 | allow(Bummr::Outdated.instance).to receive(:gemfile).and_return gemfile 106 | 107 | results = Bummr::Outdated.instance.outdated_gems 108 | gem_names = results.map { |result| result[:name] } 109 | 110 | expect(gem_names).to include 'devise', 'rake', 'rails', 'spring' 111 | end 112 | end 113 | 114 | describe "gem option" do 115 | let(:stdoutput_from_spring_gem) { 116 | output = String.new 117 | output += "spring (newest 4.2.6, installed 4.2.5.1, requested ~> 4.2.0)" 118 | StringIO.new(output) 119 | } 120 | 121 | it "lists outdated gems only from supplied gem" do 122 | allow(Open3).to receive(:popen2) 123 | .with("bundle outdated --parseable --strict spring") 124 | .and_yield(nil, stdoutput_from_spring_gem) 125 | 126 | allow(Bummr::Outdated.instance).to receive(:gemfile).and_return gemfile 127 | 128 | results = Bummr::Outdated.instance.outdated_gems(gem: :spring) 129 | gem_names = results.map { |result| result[:name] } 130 | 131 | expect(gem_names).to match_array ['spring'] 132 | end 133 | end 134 | end 135 | 136 | describe "#parse_gem_from" do 137 | it 'line' do 138 | line = 'devise (newest 4.1.1, installed 3.5.2) in group "default"' 139 | 140 | gem = Bummr::Outdated.instance.parse_gem_from(line) 141 | 142 | expect(gem[:name]).to eq('devise') 143 | expect(gem[:newest]).to eq('4.1.1') 144 | expect(gem[:installed]).to eq('3.5.2') 145 | end 146 | 147 | it 'line in group' do 148 | line = 'rake (newest 11.1.2, installed 10.4.2)' 149 | 150 | gem = Bummr::Outdated.instance.parse_gem_from(line) 151 | 152 | expect(gem[:name]).to eq('rake') 153 | expect(gem[:newest]).to eq('11.1.2') 154 | expect(gem[:installed]).to eq('10.4.2') 155 | end 156 | 157 | it 'line with requested' do 158 | line = 'rails (newest 4.2.6, installed 4.2.5.1, requested ~> 4.2.0) in group "default"' 159 | 160 | gem = Bummr::Outdated.instance.parse_gem_from(line) 161 | 162 | expect(gem[:name]).to eq('rails') 163 | expect(gem[:newest]).to eq('4.2.6') 164 | expect(gem[:installed]).to eq('4.2.5.1') 165 | end 166 | end 167 | end 168 | --------------------------------------------------------------------------------