├── .coveralls.yml ├── .rspec ├── lib ├── gistory │ ├── version.rb │ ├── errors.rb │ ├── commit.rb │ ├── cli │ │ ├── io.rb │ │ ├── main.rb │ │ └── arg_parser.rb │ ├── version_change.rb │ ├── configuration.rb │ ├── lockfile_parser.rb │ ├── git_repo.rb │ └── change_log.rb └── gistory.rb ├── bin ├── setup └── console ├── Rakefile ├── .travis.yml ├── .gitignore ├── exe └── gistory ├── .codeclimate.yml ├── Gemfile ├── .rubocop.yml ├── spec ├── gistory │ ├── lockfile_parser_spec.rb │ ├── git_repo_spec.rb │ ├── cli │ │ └── arg_parser_spec.rb │ └── change_log_spec.rb ├── spec_helper.rb └── test_files │ ├── Gemfile_with_merge_conflicts.lock │ └── Gemfile.lock ├── gistory.gemspec ├── LICENSE.txt └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/gistory/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Gistory 4 | VERSION = "0.4.0" 5 | end 6 | -------------------------------------------------------------------------------- /lib/gistory/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Gistory 4 | class Error < StandardError; end 5 | 6 | class ParserError < Error; end 7 | end 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | script: 4 | - bundle exec rspec 5 | rvm: 6 | - 2.6.6 7 | - 2.5.8 8 | - 2.4.10 9 | before_install: gem install bundler -v 1.17.3 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | 14 | NOTES.txt 15 | -------------------------------------------------------------------------------- /exe/gistory: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path("../lib", __dir__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | 7 | require "gistory" 8 | 9 | Gistory::Cli::Main.new(repo_path: Dir.getwd, args: ARGV).run 10 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | fixme: 9 | enabled: true 10 | rubocop: 11 | enabled: true 12 | ratings: 13 | paths: 14 | - "**.rb" 15 | exclude_paths: 16 | - spec/ 17 | -------------------------------------------------------------------------------- /lib/gistory/commit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | 5 | module Gistory 6 | class Commit 7 | attr_reader :short_hash, :date 8 | 9 | def initialize(short_hash:, date:) 10 | @short_hash = short_hash 11 | @date = DateTime.parse(date.to_s) 12 | freeze 13 | end 14 | 15 | def to_s 16 | "Commit #{short_hash} on #{date}" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/gistory/cli/io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "colorize" 4 | 5 | module Gistory 6 | module Cli 7 | class Io 8 | def initialize(out: $stdout, err: $stderr) 9 | @out = out 10 | @err = err 11 | end 12 | 13 | def puts(msg) 14 | @out.puts(msg) 15 | end 16 | 17 | def error(msg) 18 | @err.puts(msg.red) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/gistory/version_change.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Gistory 6 | class VersionChange 7 | extend Forwardable 8 | def_delegators :@commit, :short_hash, :date 9 | 10 | attr_reader :commit, :version 11 | 12 | def initialize(commit:, version:) 13 | @commit = commit 14 | @version = version 15 | freeze 16 | end 17 | 18 | def to_s 19 | "Version #{version} (on #{date} by #{short_hash})" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | gem "pry", "~> 0.13.1" 8 | gem "pry-byebug", "~> 3.9.0" 9 | gem "rake", "~> 13.0" 10 | gem "rspec", "~> 3.9" 11 | gem "rubocop" 12 | gem "rubocop-performance" 13 | gem "rubocop-rspec" 14 | 15 | group :test do 16 | gem "coveralls", require: false 17 | gem "simplecov", require: false # code coverage 18 | end 19 | 20 | local_gemfile = "Gemfile.local" 21 | eval_gemfile(local_gemfile) if File.exist?(local_gemfile) 22 | -------------------------------------------------------------------------------- /lib/gistory/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Gistory 4 | class Configuration 5 | attr_accessor :gem_name, :max_fetched_commits 6 | attr_writer :all_branches, :output_commit_hashes_only 7 | 8 | def initialize 9 | @max_fetched_commits = 100 10 | @all_branches = false 11 | @output_commit_hashes_only = false 12 | end 13 | 14 | def all_branches? 15 | @all_branches 16 | end 17 | 18 | def output_commit_hashes_only? 19 | @output_commit_hashes_only 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/gistory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "gistory/version" 4 | 5 | require "gistory/cli/main" 6 | require "gistory/cli/arg_parser" 7 | require "gistory/cli/io" 8 | require "gistory/configuration" 9 | 10 | require "gistory/errors" 11 | require "gistory/commit" 12 | require "gistory/version_change" 13 | require "gistory/git_repo" 14 | require "gistory/lockfile_parser" 15 | require "gistory/change_log" 16 | 17 | module Gistory 18 | class << self 19 | def config 20 | @config ||= Gistory::Configuration.new 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "gistory" 6 | require "pry" 7 | 8 | root_path = "#{File.dirname(__FILE__)}/.." 9 | 10 | # require all files in lib for quick access to them in the console 11 | Dir["#{root_path}/lib/**/*.rb"].sort.each { |file| require file } 12 | 13 | def reload! 14 | verbosity = $VERBOSE 15 | begin 16 | $VERBOSE = nil 17 | files = $LOADED_FEATURES.select { |feat| feat.include?("gistory") } 18 | files.each { |file| load(file) } 19 | ensure 20 | $VERBOSE = verbosity 21 | end 22 | true 23 | end 24 | 25 | Pry.start 26 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rspec 3 | - rubocop-performance 4 | 5 | AllCops: 6 | TargetRubyVersion: 2.4 7 | NewCops: enable 8 | 9 | Metrics/AbcSize: 10 | Max: 25 11 | Exclude: 12 | - 'spec/**/*_spec.rb' 13 | 14 | Metrics/BlockLength: 15 | Exclude: 16 | - 'spec/**/*_spec.rb' 17 | 18 | Metrics/MethodLength: 19 | Max: 20 20 | 21 | Layout/LineLength: 22 | Max: 120 23 | 24 | Style/BlockDelimiters: 25 | Exclude: 26 | - 'spec/**/*_spec.rb' 27 | 28 | Style/Documentation: 29 | Enabled: false 30 | 31 | Style/IfUnlessModifier: 32 | Enabled: false 33 | 34 | Style/FrozenStringLiteralComment: 35 | EnforcedStyle: always 36 | 37 | Style/StringLiterals: 38 | EnforcedStyle: double_quotes 39 | 40 | # rubocop-rspec 41 | 42 | RSpec/MultipleExpectations: 43 | Max: 15 44 | 45 | RSpec/ExampleLength: 46 | Max: 15 47 | -------------------------------------------------------------------------------- /spec/gistory/lockfile_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Gistory::LockfileParser do 4 | subject(:parser) { described_class.new(lockfile_content: lockfile_content) } 5 | 6 | describe "#gem_version" do 7 | let(:lockfile_path) { File.join(File.dirname(__FILE__), "../test_files/#{lockfile}") } 8 | let(:lockfile_content) { File.read(lockfile_path) } 9 | let(:lockfile) { "Gemfile.lock" } 10 | 11 | it "parses the gem's version" do 12 | expect(parser.gem_version("actionmailer")).to eq("5.2.2") 13 | end 14 | 15 | context "when the Gemfile has merge conflicts" do 16 | let(:lockfile) { "Gemfile_with_merge_conflicts.lock" } 17 | 18 | it "parses the gem's version" do 19 | expect(parser.gem_version("twitter")).to eq("5.11.0") 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "simplecov" 5 | require "coveralls" 6 | 7 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( 8 | [ 9 | SimpleCov::Formatter::HTMLFormatter, 10 | Coveralls::SimpleCov::Formatter 11 | ] 12 | ) 13 | SimpleCov.start 14 | 15 | $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) 16 | require "gistory" 17 | 18 | RSpec.configure do |config| 19 | # Enable flags like --only-failures and --next-failure 20 | config.example_status_persistence_file_path = ".rspec_status" 21 | 22 | # Disable RSpec exposing methods globally on `Module` and `main` 23 | config.disable_monkey_patching! 24 | 25 | config.expect_with :rspec do |c| 26 | c.syntax = :expect 27 | end 28 | end 29 | 30 | unless ENV["TRAVIS"] 31 | require "pry" 32 | end 33 | -------------------------------------------------------------------------------- /lib/gistory/lockfile_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler" 4 | 5 | module Gistory 6 | class LockfileParser 7 | def initialize(lockfile_content:) 8 | @lockfile_content = lockfile_content 9 | end 10 | 11 | def gem_version(gem_name) 12 | lockfile = Bundler::LockfileParser.new(@lockfile_content) 13 | gem_spec = lockfile.specs.find { |spec| spec.name == gem_name } 14 | gem_spec ? gem_spec.version.to_s : nil 15 | rescue Bundler::LockfileError => _e 16 | # bundler could not parse the lockfile 17 | # f.i. it could have been committed with merge conflicts 18 | # try to parse it with a regex 19 | # gem version looks like " byebug (9.0.6)" 20 | # TODO: what if the gem was in the merge conflict? 21 | regexp = /\n\s{4}#{gem_name} \((?.+)\)\n/ 22 | matches = @lockfile_content.match(regexp) 23 | matches ? matches[:version] : nil 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/gistory/git_repo_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Gistory::GitRepo do 4 | describe ".new" do 5 | it "raise if the provided path is not a git repository" do 6 | expect { described_class.new(path: Dir.tmpdir) }.to raise_error(Gistory::Error) 7 | end 8 | end 9 | 10 | describe "#changes_to_file" do 11 | it "converts the git output to commit objects" do 12 | allow(Dir).to receive(:exist?).and_return(true) 13 | repo = described_class.new(path: ".") 14 | 15 | allow(repo).to receive(:git).and_return( 16 | "1234567|Thu, 9 Feb 2017 18:38:33 +0100\n7654321|Wed, 8 Feb 2017 18:38:33 +0100" 17 | ) 18 | commits = repo.changes_to_file("file") 19 | 20 | expect(commits[0].short_hash).to eq("1234567") 21 | expect(commits[0].date.rfc2822).to eq("Thu, 9 Feb 2017 18:38:33 +0100") 22 | 23 | expect(commits[1].short_hash).to eq("7654321") 24 | expect(commits[1].date.rfc2822).to eq("Wed, 8 Feb 2017 18:38:33 +0100") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /gistory.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "gistory/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "gistory" 9 | spec.version = Gistory::VERSION 10 | spec.platform = Gem::Platform::RUBY 11 | spec.required_ruby_version = ">= 2.4" 12 | spec.authors = ["Sergio Medina"] 13 | spec.email = ["medinasergio@gmail.com"] 14 | 15 | spec.summary = "Gistory: Know exactly when a gem was updated in your Gemfile.lock" 16 | spec.description = "Gistory: Know exactly when a gem was updated in your Gemfile.lock" 17 | spec.homepage = "https://www.github.com/serch/gistory" 18 | spec.licenses = ["MIT"] 19 | 20 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 21 | f.match(%r{^(test|spec|features)/}) 22 | end 23 | spec.bindir = "exe" 24 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 25 | spec.require_paths = ["lib"] 26 | 27 | spec.add_dependency "bundler", "~> 2.0" 28 | spec.add_dependency "colorize" 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Sergio Medina 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 | -------------------------------------------------------------------------------- /spec/gistory/cli/arg_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Gistory::Cli::ArgParser do 4 | describe "#parse" do 5 | it "parses the gem name" do 6 | parser = described_class.new(args: ["mygem"]) 7 | config = parser.parse 8 | expect(config.gem_name).to eq("mygem") 9 | end 10 | 11 | it "parses max_fetched_commits" do 12 | parser = described_class.new(args: ["mygem", "-m10"]) 13 | config = parser.parse 14 | expect(config.gem_name).to eq("mygem") 15 | expect(config.max_fetched_commits).to eq(10) 16 | end 17 | 18 | it "raises when no arguments are passed" do 19 | parser = described_class.new(args: []) 20 | expect { parser.parse }.to raise_error(Gistory::ParserError) 21 | end 22 | 23 | it "raises when no gem name is passed" do 24 | parser = described_class.new(args: ["--max-fetched-commits", "10"]) 25 | expect { parser.parse }.to raise_error(Gistory::ParserError) 26 | end 27 | 28 | it "raises when invalid arguments are passed" do 29 | parser = described_class.new(args: ["mygem", "-m10", "-z1"]) 30 | expect { parser.parse }.to raise_error(Gistory::ParserError) 31 | end 32 | 33 | it "raises when max_fetched_commits is not an integer" do 34 | parser = described_class.new(args: ["mygem", "-mA"]) 35 | expect { parser.parse }.to raise_error(Gistory::ParserError) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/gistory/git_repo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "English" 4 | 5 | module Gistory 6 | class GitRepo 7 | def initialize(path:) 8 | raise(Gistory::Error, "This is not a valid git repository") unless Dir.exist?(File.join(path, ".git")) 9 | raise(Gistory::Error, "git is not available, please install it") unless git_cli_available? 10 | end 11 | 12 | def changes_to_file(filename) 13 | max_count = Gistory.config.max_fetched_commits 14 | strategy = git_log_strategy(filename) 15 | hashes_and_dates = git("log --pretty=format:'%h|%cD' --max-count=#{max_count} #{strategy}") 16 | to_commits(hashes_and_dates.split("\n")) 17 | end 18 | 19 | def file_content_at_commit(commit_hash, filename) 20 | git("show #{commit_hash}:#{filename}") 21 | end 22 | 23 | private 24 | 25 | def git_log_strategy(filename) 26 | if Gistory.config.all_branches? 27 | "--follow #{filename}" 28 | else 29 | # TODO: filter out commits that did not introduce changes to the lock file 30 | "--first-parent" 31 | end 32 | end 33 | 34 | def git_cli_available? 35 | system("which git > /dev/null 2>&1") 36 | end 37 | 38 | def to_commits(hashes_and_dates) 39 | hashes_and_dates.map do |hash_and_date| 40 | commit_hash, date = hash_and_date.split("|") 41 | Commit.new(short_hash: commit_hash, date: date) 42 | end 43 | end 44 | 45 | def git(command) 46 | out = `git #{command}` 47 | raise "Git CLI command failed" unless $CHILD_STATUS.success? 48 | 49 | out 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/gistory/change_log.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Gistory 4 | class ChangeLog 5 | LOCKFILE = "Gemfile.lock" 6 | 7 | def initialize(repo:) 8 | @repo = repo 9 | end 10 | 11 | def changelog_for_gem(gem_name) 12 | version_changes = [] 13 | commits_with_changes = @repo.changes_to_file(LOCKFILE) 14 | 15 | # no lockfile found or no changes to the lockfile found 16 | return [] if commits_with_changes.empty? 17 | 18 | previous_commit = commits_with_changes.shift 19 | previous_gem_spec = gem_version_at_commit_hash(previous_commit.short_hash, gem_name) 20 | # only one change to the lockfile was found and the gem was not there 21 | return [] if previous_gem_spec.nil? 22 | 23 | commits_with_changes.each do |current_commit| 24 | current_gem_spec = gem_version_at_commit_hash(current_commit.short_hash, gem_name) 25 | 26 | # we reached the end, the gem didn't exist back then 27 | # TODO: what if it was added then removed and then added again? 28 | break if current_gem_spec.nil? 29 | 30 | if current_gem_spec != previous_gem_spec 31 | version_changes << VersionChange.new(commit: previous_commit, version: previous_gem_spec) 32 | end 33 | 34 | previous_gem_spec = current_gem_spec 35 | previous_commit = current_commit 36 | end 37 | 38 | version_changes << VersionChange.new(commit: previous_commit, version: previous_gem_spec) 39 | 40 | version_changes 41 | end 42 | 43 | private 44 | 45 | def gem_version_at_commit_hash(commit_hash, gem_name) 46 | lockfile_content = @repo.file_content_at_commit(commit_hash, LOCKFILE) 47 | LockfileParser.new(lockfile_content: lockfile_content).gem_version(gem_name) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gistory 2 | 3 | [![Gem Version](https://badge.fury.io/rb/gistory.svg)](https://rubygems.org/gems/gistory) 4 | [![Code Climate](https://codeclimate.com/github/serch/gistory/badges/gpa.svg)](https://codeclimate.com/github/serch/gistory) 5 | [![Build Status](https://travis-ci.org/serch/gistory.svg?branch=master)](https://travis-ci.org/serch/gistory) 6 | [![Coverage Status](https://coveralls.io/repos/github/serch/gistory/badge.svg?branch=master)](https://coveralls.io/github/serch/gistory?branch=master) 7 | 8 | If you use bundler and git and you want to know when a gem was updated, `gistory` comes to the rescue, simply: 9 | 10 | ```shell 11 | gem install gistory 12 | cd /path/to/repo 13 | gistory sidekiq 14 | ``` 15 | 16 | and you'll see something like: 17 | 18 | ``` 19 | Gem: sidekiq 20 | Current version: 4.2.7 21 | 22 | Change history: 23 | 4.2.7 on Tue, 7 Feb 2017 16:05 +01:00 (commit c6edf321) 24 | 4.2.6 on Wed, 30 Nov 2016 13:47 +01:00 (commit bf6a0d17) 25 | 4.2.5 on Tue, 22 Nov 2016 14:48 -05:00 (commit 20ff5148) 26 | 4.1.4 on Wed, 9 Nov 2016 14:31 +01:00 (commit 05a3c549) 27 | ``` 28 | 29 | By default `gistory` only looks at the last 100 commits made to the current branch. 30 | If you want to see farther back in the past run: 31 | 32 | ```shell 33 | gistory sidekiq -m1000 34 | ``` 35 | 36 | If you want to look at all changes to Gemfile.lock in all branches, use the `-a` switch: 37 | 38 | ```shell 39 | gistory sidekiq -a 40 | ``` 41 | 42 | Note that if the gem was added, then removed, and then added again, `gistory` will 43 | only show the latest version changes up until it was removed. 44 | 45 | ## Roadmap 46 | 47 | - use red for changes in the major version, blue for changes in the minor version 48 | - support other VCSs like subversion, mercurial, etc. 49 | - detect if the gem was added, then removed and then added again 50 | - use a libgit2 binding instead of the git cli, how much faster it is? 51 | - remove bundler dep 52 | - add yard doc 53 | - do not print the warning text if there were no more changes in the lock file 54 | - link the commit SHA to the repo's hosting service, if any 55 | -------------------------------------------------------------------------------- /lib/gistory/cli/main.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Gistory 4 | module Cli 5 | class Main 6 | def initialize(repo_path:, args:, io: Gistory::Cli::Io.new) 7 | @repo_path = repo_path 8 | @args = args 9 | @io = io 10 | end 11 | 12 | def run 13 | repo = GitRepo.new(path: @repo_path) 14 | parser = Cli::ArgParser.new(args: @args, io: @io) 15 | config = parser.parse 16 | history(repo, config.gem_name) 17 | rescue Gistory::ParserError => e 18 | @io.error e.message 19 | @io.puts parser 20 | rescue Gistory::Error => e 21 | @io.error e.message 22 | end 23 | 24 | private 25 | 26 | def history(repo, gem_name) 27 | changes = ChangeLog.new(repo: repo).changelog_for_gem(gem_name) 28 | 29 | if changes.empty? 30 | raise(Gistory::Error, "Gem '#{gem_name}' not found in lock file, maybe a typo?") 31 | end 32 | 33 | if Gistory.config.output_commit_hashes_only? 34 | print_commit_hashes_only(changes) 35 | else 36 | print_full_output(gem_name, changes) 37 | end 38 | end 39 | 40 | def print_commit_hashes_only(changes) 41 | changes.each { |change| @io.puts change.short_hash } 42 | end 43 | 44 | def print_full_output(gem_name, changes) 45 | @io.puts "Gem: #{gem_name}" 46 | @io.puts "Current version: #{changes.first.version}" 47 | @io.puts "" 48 | 49 | print_change_history(changes) 50 | 51 | @io.puts "" 52 | 53 | print_configuration_info 54 | end 55 | 56 | def print_change_history(changes) 57 | @io.puts "Change history:" 58 | max_length = changes.map { |c| c.version.length }.max 59 | 60 | changes.each do |change| 61 | @io.puts "#{change.version.ljust(max_length)} on #{change.date.strftime('%a, %e %b %Y %H:%M %Z')} " \ 62 | "(commit #{change.short_hash})" 63 | end 64 | end 65 | 66 | def print_configuration_info 67 | max = Gistory.config.max_fetched_commits 68 | if Gistory.config.all_branches? 69 | @io.puts "The last #{max} changes to the lock file were fetched." 70 | else 71 | @io.puts "The last #{max} commits made to the current branch were fetched." 72 | end 73 | 74 | @io.puts "To see farther in the past use the -m switch" 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/gistory/cli/arg_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "optparse" 4 | 5 | module Gistory 6 | module Cli 7 | class ArgParser 8 | def initialize(args:, io: Gistory::Cli::Io.new) 9 | @args = args 10 | @config = Gistory.config 11 | @parser = create_parser(@config) 12 | @io = io 13 | end 14 | 15 | def parse 16 | @parser.parse!(@args) 17 | 18 | parse_gem_name 19 | @io.error("extra parameters ignored: #{@args}") unless @args.count.zero? 20 | @config 21 | rescue OptionParser::InvalidOption => e 22 | raise(Gistory::ParserError, e.message) 23 | end 24 | 25 | def to_s 26 | @parser.to_s 27 | end 28 | 29 | private 30 | 31 | def parse_gem_name 32 | gem_name = @args.shift 33 | raise(Gistory::ParserError, "No gem specified") unless gem_name 34 | 35 | @config.gem_name = gem_name 36 | end 37 | 38 | def create_parser(config) 39 | parser = OptionParser.new 40 | parser.banner = "Usage: gistory [options]" 41 | 42 | add_options(parser, config) 43 | 44 | parser 45 | end 46 | 47 | def add_options(parser, config) 48 | parser.separator "" 49 | parser.separator "Options:" 50 | 51 | add_max_fetched_commits(parser, config) 52 | add_use_commits_from_all_branches(parser, config) 53 | add_output_commit_hashes_only(parser, config) 54 | add_help(parser) 55 | add_version(parser) 56 | end 57 | 58 | def add_max_fetched_commits(parser, config) 59 | default = config.max_fetched_commits 60 | description = "max number of commits to be fetched (default #{default})" 61 | parser.on("-m", "--max-fetched-commits [Integer]", Integer, description) do |m| 62 | raise(Gistory::ParserError, "argument --max-fetched-commits must be an integer") if m.nil? 63 | 64 | config.max_fetched_commits = m 65 | end 66 | end 67 | 68 | def add_use_commits_from_all_branches(parser, config) 69 | description = "use commits from all branches " \ 70 | "(by default it uses only commits made to the current branch)" 71 | parser.on("-a", "--all-branches", description) do |a| 72 | config.all_branches = a 73 | end 74 | end 75 | 76 | def add_output_commit_hashes_only(parser, config) 77 | option_switch = "--hashes-only" 78 | parser.on(option_switch, 79 | "output commit hashes only so they can be piped", 80 | "for example: gistory #{option_switch} sidekiq | xargs git show") do |ho| 81 | config.output_commit_hashes_only = ho 82 | end 83 | end 84 | 85 | def add_help(parser) 86 | parser.on_tail("-h", "--help", "Show this message") do 87 | @io.puts parser 88 | exit 89 | end 90 | end 91 | 92 | def add_version(parser) 93 | parser.on_tail("--version", "Show version") do 94 | @io.puts "gistory version #{Gistory::VERSION}" 95 | exit 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/gistory/change_log_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Gistory::ChangeLog do 4 | describe "#changelog_for_gem" do 5 | subject(:change_log) { described_class.new(repo: repo) } 6 | 7 | let(:repo) { instance_double("GitRepo", changes_to_file: commits) } 8 | let(:gem_name) { "mygem" } 9 | 10 | before do 11 | allow(change_log).to receive(:gem_version_at_commit_hash).and_return(*versions) 12 | end 13 | 14 | context "when no changes to the lockfile are found" do 15 | let(:commits) { [] } 16 | let(:versions) { ["dummy"] } # dummy otherwise and_return(*[]) fails 17 | 18 | it "returns no version changes" do 19 | changes = change_log.changelog_for_gem(gem_name) 20 | expect(changes).to be_empty 21 | end 22 | end 23 | 24 | context "when the gem is not found in the lockfile" do 25 | let(:commits) { [Gistory::Commit.new(short_hash: "1234567", date: "Wed, 8 Feb 2017 18:38:33 +0100")] } 26 | let(:versions) { [nil] } 27 | 28 | it "returns no version changes" do 29 | changes = change_log.changelog_for_gem(gem_name) 30 | expect(changes).to be_empty 31 | end 32 | end 33 | 34 | context "when there is only one commit that added the gem to an already existing lockfile" do 35 | let(:commits) { [Gistory::Commit.new(short_hash: "1234567", date: "Wed, 8 Feb 2017 18:38:33 +0100")] } 36 | let(:versions) { ["1.2.3", nil] } 37 | 38 | it "returns one version change" do 39 | changes = change_log.changelog_for_gem(gem_name) 40 | expect(changes.count).to eq(1) 41 | 42 | version_change = changes.first 43 | 44 | expect(version_change.short_hash).to eq("1234567") 45 | expect(version_change.date).to eq(DateTime.parse("Wed, 8 Feb 2017 18:38:33 +0100")) 46 | expect(version_change.version).to eq("1.2.3") 47 | end 48 | end 49 | 50 | context "when there is only one commit that added the gem and the lockfile at the same time" do 51 | let(:commits) { [Gistory::Commit.new(short_hash: "1234567", date: "Wed, 8 Feb 2017 18:38:33 +0100")] } 52 | let(:versions) { ["1.2.3"] } 53 | 54 | it "returns one version change" do 55 | changes = change_log.changelog_for_gem(gem_name) 56 | expect(changes.count).to eq(1) 57 | 58 | version_change = changes.first 59 | 60 | expect(version_change.short_hash).to eq("1234567") 61 | expect(version_change.date).to eq(DateTime.parse("Wed, 8 Feb 2017 18:38:33 +0100")) 62 | expect(version_change.version).to eq("1.2.3") 63 | end 64 | end 65 | 66 | context "when there are multiple commits and the first one introduces the gem to the lockfile" do 67 | let(:commit_hashes_and_gem_versions) { 68 | { 69 | "c9d4a19" => "5.0.5", 70 | "8198719" => "5.0.2", 71 | "0b03443" => "5.0.2", 72 | "453f316" => "5.0.1", 73 | "db1ecad" => "5.0.1", 74 | "7fa4a05" => "5.0.0", 75 | "29b6e67" => nil 76 | } 77 | } 78 | let(:commits) { 79 | commit_hashes_and_gem_versions.keys.each_with_index.map do |hash, index| 80 | Gistory::Commit.new(short_hash: hash, date: Time.now - (index * 60)) 81 | end 82 | } 83 | let(:versions) { commit_hashes_and_gem_versions.values } 84 | 85 | it "correctly returns all version changes" do 86 | changes = change_log.changelog_for_gem(gem_name) 87 | 88 | expect(changes.count).to eq(4) 89 | 90 | expect(changes[0].short_hash).to eq("c9d4a19") 91 | expect(changes[0].version).to eq("5.0.5") 92 | 93 | expect(changes[1].short_hash).to eq("0b03443") 94 | expect(changes[1].version).to eq("5.0.2") 95 | 96 | expect(changes[2].short_hash).to eq("db1ecad") 97 | expect(changes[2].version).to eq("5.0.1") 98 | 99 | expect(changes[3].short_hash).to eq("7fa4a05") 100 | expect(changes[3].version).to eq("5.0.0") 101 | end 102 | end 103 | 104 | context "when there are multiple commits and the first one introduces both the gem and the lockfile" do 105 | let(:commit_hashes_and_gem_versions) { 106 | { 107 | "c9d4a19" => "5.0.5", 108 | "8198719" => "5.0.2", 109 | "0b03443" => "5.0.2", 110 | "453f316" => "5.0.1", 111 | "db1ecad" => "5.0.1", 112 | "7fa4a05" => "5.0.0", 113 | "29b6e67" => "4.9.9" 114 | } 115 | } 116 | let(:commits) { 117 | commit_hashes_and_gem_versions.keys.each_with_index.map do |hash, index| 118 | Gistory::Commit.new(short_hash: hash, date: Time.now - (index * 60)) 119 | end 120 | } 121 | let(:versions) { commit_hashes_and_gem_versions.values } 122 | 123 | it "correctly returns all version changes" do 124 | changes = change_log.changelog_for_gem(gem_name) 125 | expect(changes.count).to eq(5) 126 | 127 | expect(changes[0].short_hash).to eq("c9d4a19") 128 | expect(changes[0].version).to eq("5.0.5") 129 | 130 | expect(changes[1].short_hash).to eq("0b03443") 131 | expect(changes[1].version).to eq("5.0.2") 132 | 133 | expect(changes[2].short_hash).to eq("db1ecad") 134 | expect(changes[2].version).to eq("5.0.1") 135 | 136 | expect(changes[3].short_hash).to eq("7fa4a05") 137 | expect(changes[3].version).to eq("5.0.0") 138 | 139 | expect(changes[4].short_hash).to eq("29b6e67") 140 | expect(changes[4].version).to eq("4.9.9") 141 | end 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/test_files/Gemfile_with_merge_conflicts.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.0.beta2) 5 | actionpack (= 4.2.0.beta2) 6 | actionview (= 4.2.0.beta2) 7 | activejob (= 4.2.0.beta2) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.3) 10 | actionpack (4.2.0.beta2) 11 | actionview (= 4.2.0.beta2) 12 | activesupport (= 4.2.0.beta2) 13 | rack (~> 1.6.0.beta) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.3) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 17 | actionview (4.2.0.beta2) 18 | activesupport (= 4.2.0.beta2) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.3) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | activejob (4.2.0.beta2) 24 | activesupport (= 4.2.0.beta2) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.0.beta2) 27 | activesupport (= 4.2.0.beta2) 28 | builder (~> 3.1) 29 | activerecord (4.2.0.beta2) 30 | activemodel (= 4.2.0.beta2) 31 | activesupport (= 4.2.0.beta2) 32 | arel (>= 6.0.0.beta1, < 6.1) 33 | activesupport (4.2.0.beta2) 34 | i18n (>= 0.7.0.beta1, < 0.8) 35 | json (~> 1.7, >= 1.7.7) 36 | minitest (~> 5.1) 37 | thread_safe (~> 0.1) 38 | tzinfo (~> 1.1) 39 | addressable (2.3.6) 40 | arel (6.0.0.beta1) 41 | binding_of_caller (0.7.3.pre1) 42 | debug_inspector (>= 0.0.1) 43 | buftok (0.2.0) 44 | builder (3.2.2) 45 | byebug (3.5.1) 46 | columnize (~> 0.8) 47 | debugger-linecache (~> 1.2) 48 | slop (~> 3.6) 49 | coderay (1.1.0) 50 | coffee-rails (4.0.1) 51 | coffee-script (>= 2.2.0) 52 | railties (>= 4.0.0, < 5.0) 53 | coffee-script (2.3.0) 54 | coffee-script-source 55 | execjs 56 | coffee-script-source (1.8.0) 57 | columnize (0.8.9) 58 | debug_inspector (0.0.2) 59 | debugger-linecache (1.2.0) 60 | equalizer (0.0.9) 61 | erubis (2.7.0) 62 | execjs (2.2.2) 63 | faraday (0.9.0) 64 | multipart-post (>= 1.2, < 3) 65 | globalid (0.3.0) 66 | activesupport (>= 4.1.0) 67 | hike (1.2.3) 68 | http (0.6.2) 69 | http_parser.rb (~> 0.6.0) 70 | http_parser.rb (0.6.0) 71 | i18n (0.7.0.beta1) 72 | jbuilder (2.2.2) 73 | activesupport (>= 3.0.0, < 5) 74 | multi_json (~> 1.2) 75 | jquery-rails (4.0.0.beta2) 76 | rails-dom-testing (~> 1.0) 77 | railties (>= 4.2.0.beta, < 5.0) 78 | thor (>= 0.14, < 2.0) 79 | json (1.8.1) 80 | jwt (1.0.0) 81 | loofah (2.0.1) 82 | nokogiri (>= 1.5.9) 83 | mail (2.6.1) 84 | mime-types (>= 1.16, < 3) 85 | memoizable (0.4.2) 86 | thread_safe (~> 0.3, >= 0.3.1) 87 | method_source (0.8.2) 88 | mime-types (2.4.2) 89 | mini_portile (0.6.0) 90 | minitest (5.4.2) 91 | multi_json (1.10.1) 92 | multipart-post (2.0.0) 93 | naught (1.0.0) 94 | nokogiri (1.6.3.1) 95 | mini_portile (= 0.6.0) 96 | pg (0.17.1) 97 | pry (0.10.1) 98 | coderay (~> 1.1.0) 99 | method_source (~> 0.8.1) 100 | slop (~> 3.4) 101 | pry-byebug (2.0.0) 102 | byebug (~> 3.4) 103 | pry (~> 0.10) 104 | rack (1.6.0.beta) 105 | rack-test (0.6.2) 106 | rack (>= 1.0) 107 | rails (4.2.0.beta2) 108 | actionmailer (= 4.2.0.beta2) 109 | actionpack (= 4.2.0.beta2) 110 | actionview (= 4.2.0.beta2) 111 | activejob (= 4.2.0.beta2) 112 | activemodel (= 4.2.0.beta2) 113 | activerecord (= 4.2.0.beta2) 114 | activesupport (= 4.2.0.beta2) 115 | bundler (>= 1.3.0, < 2.0) 116 | railties (= 4.2.0.beta2) 117 | sprockets-rails (~> 3.0.0.beta1) 118 | rails-deprecated_sanitizer (1.0.3) 119 | activesupport (>= 4.2.0.alpha) 120 | rails-dom-testing (1.0.4) 121 | activesupport (>= 4.2.0.beta, < 5.0) 122 | nokogiri (~> 1.6.0) 123 | rails-deprecated_sanitizer (>= 1.0.1) 124 | rails-html-sanitizer (1.0.1) 125 | loofah (~> 2.0) 126 | railties (4.2.0.beta2) 127 | actionpack (= 4.2.0.beta2) 128 | activesupport (= 4.2.0.beta2) 129 | rake (>= 0.8.7) 130 | thor (>= 0.18.1, < 2.0) 131 | rake (10.3.2) 132 | rdoc (4.1.2) 133 | json (~> 1.4) 134 | sass (3.4.6) 135 | sass-rails (5.0.0.beta1) 136 | railties (>= 4.0.0, < 5.0) 137 | sass (~> 3.2) 138 | sprockets (~> 2.12) 139 | sprockets-rails (>= 2.0, < 4.0) 140 | sdoc (0.4.1) 141 | json (~> 1.7, >= 1.7.7) 142 | rdoc (~> 4.0) 143 | simple_oauth (0.2.0) 144 | slop (3.6.0) 145 | spring (1.1.3) 146 | sprockets (2.12.2) 147 | hike (~> 1.2) 148 | multi_json (~> 1.0) 149 | rack (~> 1.0) 150 | tilt (~> 1.1, != 1.3.0) 151 | sprockets-rails (3.0.0.beta1) 152 | actionpack (>= 4.0) 153 | activesupport (>= 4.0) 154 | sprockets (~> 2.8) 155 | thor (0.19.1) 156 | thread_safe (0.3.4) 157 | tilt (1.4.1) 158 | turbolinks (2.4.0) 159 | coffee-rails 160 | <<<<<<< HEAD 161 | twilio-ruby (3.13.1) 162 | builder (>= 2.1.2) 163 | jwt (~> 1.0.0) 164 | multi_json (>= 1.3.0) 165 | ======= 166 | twitter (5.11.0) 167 | addressable (~> 2.3) 168 | buftok (~> 0.2.0) 169 | equalizer (~> 0.0.9) 170 | faraday (~> 0.9.0) 171 | http (~> 0.6.0) 172 | http_parser.rb (~> 0.6.0) 173 | json (~> 1.8) 174 | memoizable (~> 0.4.0) 175 | naught (~> 1.0) 176 | simple_oauth (~> 0.2.0) 177 | >>>>>>> 97cefa491ac4217bdee8557c84f3657355f9b049 178 | tzinfo (1.2.2) 179 | thread_safe (~> 0.1) 180 | uglifier (2.5.3) 181 | execjs (>= 0.3.0) 182 | json (>= 1.8.0) 183 | web-console (2.0.0.beta4) 184 | activemodel (~> 4.0) 185 | binding_of_caller (= 0.7.3.pre1) 186 | railties (~> 4.0) 187 | sprockets-rails (>= 2.0, < 4.0) 188 | 189 | PLATFORMS 190 | ruby 191 | 192 | DEPENDENCIES 193 | coffee-rails (~> 4.0.0) 194 | jbuilder (~> 2.0) 195 | jquery-rails (~> 4.0.0.beta2) 196 | pg 197 | pry-byebug 198 | rails (= 4.2.0.beta2) 199 | sass-rails (~> 5.0.0.beta1) 200 | sdoc (~> 0.4.0) 201 | spring 202 | turbolinks 203 | <<<<<<< HEAD 204 | twilio-ruby (~> 3.12) 205 | ======= 206 | twitter 207 | >>>>>>> 97cefa491ac4217bdee8557c84f3657355f9b049 208 | uglifier (>= 1.3.0) 209 | web-console (~> 2.0.0.beta4) 210 | -------------------------------------------------------------------------------- /spec/test_files/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.2.2) 5 | actionpack (= 5.2.2) 6 | nio4r (~> 2.0) 7 | websocket-driver (>= 0.6.1) 8 | actionmailer (5.2.2) 9 | actionpack (= 5.2.2) 10 | actionview (= 5.2.2) 11 | activejob (= 5.2.2) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.2.2) 15 | actionview (= 5.2.2) 16 | activesupport (= 5.2.2) 17 | rack (~> 2.0) 18 | rack-test (>= 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.2.2) 22 | activesupport (= 5.2.2) 23 | builder (~> 3.1) 24 | erubi (~> 1.4) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.2.2) 28 | activesupport (= 5.2.2) 29 | globalid (>= 0.3.6) 30 | activemodel (5.2.2) 31 | activesupport (= 5.2.2) 32 | activerecord (5.2.2) 33 | activemodel (= 5.2.2) 34 | activesupport (= 5.2.2) 35 | arel (>= 9.0) 36 | activestorage (5.2.2) 37 | actionpack (= 5.2.2) 38 | activerecord (= 5.2.2) 39 | marcel (~> 0.3.1) 40 | activesupport (5.2.2) 41 | concurrent-ruby (~> 1.0, >= 1.0.2) 42 | i18n (>= 0.7, < 2) 43 | minitest (~> 5.1) 44 | tzinfo (~> 1.1) 45 | addressable (2.6.0) 46 | public_suffix (>= 2.0.2, < 4.0) 47 | archive-zip (0.11.0) 48 | io-like (~> 0.3.0) 49 | arel (9.0.0) 50 | bindex (0.5.0) 51 | bootsnap (1.3.2) 52 | msgpack (~> 1.0) 53 | builder (3.2.3) 54 | byebug (10.0.2) 55 | capybara (3.13.2) 56 | addressable 57 | mini_mime (>= 0.1.3) 58 | nokogiri (~> 1.8) 59 | rack (>= 1.6.0) 60 | rack-test (>= 0.6.3) 61 | regexp_parser (~> 1.2) 62 | xpath (~> 3.2) 63 | childprocess (0.9.0) 64 | ffi (~> 1.0, >= 1.0.11) 65 | chromedriver-helper (2.1.0) 66 | archive-zip (~> 0.10) 67 | nokogiri (~> 1.8) 68 | coffee-rails (4.2.2) 69 | coffee-script (>= 2.2.0) 70 | railties (>= 4.0.0) 71 | coffee-script (2.4.1) 72 | coffee-script-source 73 | execjs 74 | coffee-script-source (1.12.2) 75 | concurrent-ruby (1.1.4) 76 | connection_pool (2.2.2) 77 | crass (1.0.4) 78 | erubi (1.8.0) 79 | execjs (2.7.0) 80 | ffi (1.10.0) 81 | globalid (0.4.2) 82 | activesupport (>= 4.2.0) 83 | i18n (1.5.3) 84 | concurrent-ruby (~> 1.0) 85 | io-like (0.3.0) 86 | jbuilder (2.8.0) 87 | activesupport (>= 4.2.0) 88 | multi_json (>= 1.2) 89 | listen (3.1.5) 90 | rb-fsevent (~> 0.9, >= 0.9.4) 91 | rb-inotify (~> 0.9, >= 0.9.7) 92 | ruby_dep (~> 1.2) 93 | loofah (2.2.3) 94 | crass (~> 1.0.2) 95 | nokogiri (>= 1.5.9) 96 | mail (2.7.1) 97 | mini_mime (>= 0.1.1) 98 | marcel (0.3.3) 99 | mimemagic (~> 0.3.2) 100 | method_source (0.9.2) 101 | mimemagic (0.3.3) 102 | mini_mime (1.0.1) 103 | mini_portile2 (2.4.0) 104 | minitest (5.11.3) 105 | msgpack (1.2.6) 106 | multi_json (1.13.1) 107 | nio4r (2.3.1) 108 | nokogiri (1.10.1) 109 | mini_portile2 (~> 2.4.0) 110 | public_suffix (3.0.3) 111 | puma (3.12.0) 112 | rack (2.0.6) 113 | rack-protection (2.0.5) 114 | rack 115 | rack-test (1.1.0) 116 | rack (>= 1.0, < 3) 117 | rails (5.2.2) 118 | actioncable (= 5.2.2) 119 | actionmailer (= 5.2.2) 120 | actionpack (= 5.2.2) 121 | actionview (= 5.2.2) 122 | activejob (= 5.2.2) 123 | activemodel (= 5.2.2) 124 | activerecord (= 5.2.2) 125 | activestorage (= 5.2.2) 126 | activesupport (= 5.2.2) 127 | bundler (>= 1.3.0) 128 | railties (= 5.2.2) 129 | sprockets-rails (>= 2.0.0) 130 | rails-dom-testing (2.0.3) 131 | activesupport (>= 4.2.0) 132 | nokogiri (>= 1.6) 133 | rails-html-sanitizer (1.0.4) 134 | loofah (~> 2.2, >= 2.2.2) 135 | railties (5.2.2) 136 | actionpack (= 5.2.2) 137 | activesupport (= 5.2.2) 138 | method_source 139 | rake (>= 0.8.7) 140 | thor (>= 0.19.0, < 2.0) 141 | rake (12.3.2) 142 | rb-fsevent (0.10.3) 143 | rb-inotify (0.10.0) 144 | ffi (~> 1.0) 145 | redis (4.1.0) 146 | regexp_parser (1.3.0) 147 | ruby_dep (1.5.0) 148 | rubyzip (1.2.2) 149 | sass (3.7.3) 150 | sass-listen (~> 4.0.0) 151 | sass-listen (4.0.0) 152 | rb-fsevent (~> 0.9, >= 0.9.4) 153 | rb-inotify (~> 0.9, >= 0.9.7) 154 | sass-rails (5.0.7) 155 | railties (>= 4.0.0, < 6) 156 | sass (~> 3.1) 157 | sprockets (>= 2.8, < 4.0) 158 | sprockets-rails (>= 2.0, < 4.0) 159 | tilt (>= 1.1, < 3) 160 | selenium-webdriver (3.141.0) 161 | childprocess (~> 0.5) 162 | rubyzip (~> 1.2, >= 1.2.2) 163 | sidekiq (5.0.5) 164 | concurrent-ruby (~> 1.0) 165 | connection_pool (~> 2.2, >= 2.2.0) 166 | rack-protection (>= 1.5.0) 167 | redis (>= 3.3.4, < 5) 168 | spring (2.0.2) 169 | activesupport (>= 4.2) 170 | spring-watcher-listen (2.0.1) 171 | listen (>= 2.7, < 4.0) 172 | spring (>= 1.2, < 3.0) 173 | sprockets (3.7.2) 174 | concurrent-ruby (~> 1.0) 175 | rack (> 1, < 3) 176 | sprockets-rails (3.2.1) 177 | actionpack (>= 4.0) 178 | activesupport (>= 4.0) 179 | sprockets (>= 3.0.0) 180 | sqlite3 (1.3.13) 181 | thor (0.20.3) 182 | thread_safe (0.3.6) 183 | tilt (2.0.9) 184 | turbolinks (5.2.0) 185 | turbolinks-source (~> 5.2) 186 | turbolinks-source (5.2.0) 187 | tzinfo (1.2.5) 188 | thread_safe (~> 0.1) 189 | uglifier (4.1.20) 190 | execjs (>= 0.3.0, < 3) 191 | web-console (3.7.0) 192 | actionview (>= 5.0) 193 | activemodel (>= 5.0) 194 | bindex (>= 0.4.0) 195 | railties (>= 5.0) 196 | websocket-driver (0.7.0) 197 | websocket-extensions (>= 0.1.0) 198 | websocket-extensions (0.1.3) 199 | xpath (3.2.0) 200 | nokogiri (~> 1.8) 201 | 202 | PLATFORMS 203 | ruby 204 | 205 | DEPENDENCIES 206 | bootsnap (>= 1.1.0) 207 | byebug 208 | capybara (>= 2.15) 209 | chromedriver-helper 210 | coffee-rails (~> 4.2) 211 | jbuilder (~> 2.5) 212 | listen (>= 3.0.5, < 3.2) 213 | puma (~> 3.11) 214 | rails (~> 5.2.2) 215 | sass-rails (~> 5.0) 216 | selenium-webdriver 217 | sidekiq (= 5.0.5) 218 | spring 219 | spring-watcher-listen (~> 2.0.0) 220 | sqlite3 221 | turbolinks (~> 5) 222 | tzinfo-data 223 | uglifier (>= 1.3.0) 224 | web-console (>= 3.3.0) 225 | 226 | RUBY VERSION 227 | ruby 2.4.4p296 228 | 229 | BUNDLED WITH 230 | 1.17.3 231 | --------------------------------------------------------------------------------