derailed_benchmarks 1.4.3 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check_changelog.yml +11 -8
  3. data/.travis.yml +9 -7
  4. data/Appraisals +16 -16
  5. data/CHANGELOG.md +29 -1
  6. data/README.md +14 -4
  7. data/derailed_benchmarks.gemspec +4 -3
  8. data/gemfiles/rails_5_1.gemfile +3 -1
  9. data/gemfiles/rails_5_2.gemfile +3 -3
  10. data/lib/derailed_benchmarks.rb +2 -1
  11. data/lib/derailed_benchmarks/core_ext/kernel_require.rb +29 -24
  12. data/lib/derailed_benchmarks/git/commit.rb +36 -0
  13. data/lib/derailed_benchmarks/git/in_path.rb +59 -0
  14. data/lib/derailed_benchmarks/git/switch_project.rb +128 -0
  15. data/lib/derailed_benchmarks/git_switch_project.rb +1 -0
  16. data/lib/derailed_benchmarks/load_tasks.rb +11 -4
  17. data/lib/derailed_benchmarks/require_tree.rb +11 -1
  18. data/lib/derailed_benchmarks/{stats_in_file.rb → stats_for_file.rb} +8 -2
  19. data/lib/derailed_benchmarks/stats_from_dir.rb +68 -13
  20. data/lib/derailed_benchmarks/tasks.rb +34 -63
  21. data/lib/derailed_benchmarks/version.rb +1 -1
  22. data/test/derailed_benchmarks/core_ext/kernel_require_test.rb +70 -11
  23. data/test/derailed_benchmarks/git_switch_project_test.rb +83 -0
  24. data/test/derailed_benchmarks/require_tree_test.rb +1 -1
  25. data/test/derailed_benchmarks/stats_from_dir_test.rb +57 -9
  26. data/test/fixtures/require/autoload_child.rb +5 -0
  27. data/test/fixtures/require/autoload_parent.rb +8 -0
  28. data/test/fixtures/require/child_one.rb +1 -1
  29. data/test/fixtures/require/child_two.rb +1 -1
  30. data/test/fixtures/require/load_child.rb +3 -0
  31. data/test/fixtures/require/load_parent.rb +5 -0
  32. data/test/fixtures/require/parent_one.rb +1 -1
  33. data/test/integration/tasks_test.rb +43 -5
  34. data/test/rails_app/config/application.rb +2 -0
  35. data/test/test_helper.rb +6 -1
  36. metadata +46 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 348a836be042f2ebf7785075bc3f552c60ef9cefeb979d5dded1fcac5101111a
4
- data.tar.gz: 6c25b2275a5c57ae9abfc6b6c55021869aa8cea9cfcfdda7422df876caf5b10d
3
+ metadata.gz: c0f29e3b52bfe2c3c549a9908cf835ec63430afc8d8c733f8ccbf73646b0aa07
4
+ data.tar.gz: c43b9cc6602884bc33c3e6479b75250564ef4b54f895b87a6d0b5f79f4469704
5
5
  SHA512:
6
- metadata.gz: eb65bb19c23d4c112ae31289c3b610aca98f1541e7a4ee1a6fd1f1b98d1ace4849f061ebb323fc38b2f01ceae64907edd247e36becd849c52fa6998370faef5b
7
- data.tar.gz: 0273f740b7458e1feaa59f05681e815c41ad45830fb8b5590b6860769e4f4e96ee556b5249bb90598b96e987a68bf7f3939fba9837a8573b16f0f4ce7dbc182a
6
+ metadata.gz: 23583aa213f4f2ddb8ead5c9325972bd8fc696289303e16faf34023e5f732288d51cd9d5fe9edb0f37a44c0a1f3ebbe32d74c6355ba85b4f9dd1793e8e3359d5
7
+ data.tar.gz: f4074b2f83fd12c4b81e4f9714bae62a0ed15e012d2a4a821d2c038fc537b9b511475482e4928d689421ba473cfb91543431866d1cd424cf6db101b792bcfdb8
@@ -1,10 +1,13 @@
1
1
  name: Check Changelog
2
- on: [pull_request]
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, reopened, edited, synchronize]
3
6
  jobs:
4
- build:
5
- runs-on: ubuntu-latest
6
- steps:
7
- - uses: actions/checkout@v1
8
- - name: Check that CHANGELOG is touched
9
- run: |
10
- cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v1
11
+ - name: Check that CHANGELOG is touched
12
+ run: |
13
+ cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
@@ -1,18 +1,20 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.5.5
4
- - 2.6.3
5
- - ruby-head
3
+ - 2.2.10
4
+ - 2.5.8
5
+ - 2.7.1
6
6
 
7
7
  gemfile:
8
8
  - gemfiles/rails_5_1.gemfile
9
- - gemfiles/rails_5_2.gemfile
10
9
  - gemfiles/rails_6_0.gemfile
11
10
  - gemfiles/rails_git.gemfile
12
11
 
13
12
  before_install:
14
- - gem install bundler
13
+ - gem install bundler -v 1.17.3
15
14
 
16
- matrix:
15
+ jobs:
17
16
  allow_failures:
18
- - rvm: ruby-head
17
+ - rvm: 2.2.10
18
+ gemfile: gemfiles/rails_6_0.gemfile
19
+ - rvm: 2.2.10
20
+ gemfile: gemfiles/rails_git.gemfile
data/Appraisals CHANGED
@@ -1,26 +1,26 @@
1
1
  # -*- mode: ruby -*-
2
2
  # vi: set ft=ruby :
3
3
 
4
- appraise "rails-3-2" do
5
- gem "rails", "~> 3.2.0"
6
- end
4
+ # appraise "rails-3-2" do
5
+ # gem "rails", "~> 3.2.0"
6
+ # end
7
7
 
8
- appraise "rails-4-0" do
9
- gem "rails", "~> 4.0.0"
10
- end
8
+ # appraise "rails-4-0" do
9
+ # gem "rails", "~> 4.0.0"
10
+ # end
11
11
 
12
- appraise "rails-4-1" do
13
- gem "rails", "~> 4.1.0"
14
- end
12
+ # appraise "rails-4-1" do
13
+ # gem "rails", "~> 4.1.0"
14
+ # end
15
15
 
16
- appraise "rails-4-2" do
17
- gem "rails", "~> 4.2.0"
18
- end
16
+ # appraise "rails-4-2" do
17
+ # gem "rails", "~> 4.2.0"
18
+ # end
19
19
 
20
- appraise "rails-5-0" do
21
- gem "rails", "~> 5.0.0"
22
- end
20
+ # appraise "rails-5-0" do
21
+ # gem "rails", "~> 5.0.0"
22
+ # end
23
23
 
24
24
  appraise "rails-5-1" do
25
- gem "rails", "~> 5.1.0"
25
+ gem "rails", "~> 5.1.7"
26
26
  end
@@ -1,4 +1,32 @@
1
- ## master (unreleased)
1
+ ## HEAD
2
+
3
+ ## 1.8.1
4
+
5
+ - Derailed now tracks memory use from `load` in addition to `require` (https://github.com/schneems/derailed_benchmarks/pull/178)
6
+ - Correct logging of unsuccessful curl requests to file (https://github.com/schneems/derailed_benchmarks/pull/172)
7
+
8
+ ## 1.8.0
9
+
10
+ - Ruby 2.2 is now officialy supported and tested (https://github.com/schneems/derailed_benchmarks/pull/177)
11
+
12
+ ## 1.7.0
13
+
14
+ - Add histogram support to `perf:library` (https://github.com/schneems/derailed_benchmarks/pull/169)
15
+ - Fix bug with `Kernel#require` patch when Zeitwerk is enabled (https://github.com/schneems/derailed_benchmarks/pull/170)
16
+
17
+ ## 1.6.0
18
+
19
+ - Added the `perf:app` command to compare commits within the same application. (https://github.com/schneems/derailed_benchmarks/pull/157)
20
+ - Allow Rails < 7 and 1.0 <= Thor < 2 (https://github.com/schneems/derailed_benchmarks/pull/168)
21
+
22
+ ## 1.5.0
23
+
24
+ - Test `perf:library` results against 99% confidence interval in addition to 95% (https://github.com/schneems/derailed_benchmarks/pull/165)
25
+ - Change default, `perf:library` tests do not stop automatically any more (https://github.com/schneems/derailed_benchmarks/pull/164)
26
+
27
+ ## 1.4.4
28
+
29
+ - Fix alignment of deicmals in output (https://github.com/schneems/derailed_benchmarks/pull/161)
2
30
 
3
31
  ## 1.4.3
4
32
 
data/README.md CHANGED
@@ -10,7 +10,7 @@ A series of things you can use to benchmark a Rails or Ruby app.
10
10
  ## Compatibility/Requirements
11
11
 
12
12
  This gem has been tested and is known to work with Rails 3.2+ using Ruby
13
- 2.1+. Some commands __may__ work on older versions of Ruby, but not all commands are supported.
13
+ 2.2+. Some commands __may__ work on older versions of Ruby, but not all commands are supported.
14
14
 
15
15
  For some benchmarks, not all, you'll need to verify you have a working version of curl on your OS:
16
16
 
@@ -415,13 +415,23 @@ or point it at your local copy:
415
415
  gem 'rails', path: "<path/to/your/local/copy/rails>"
416
416
  ```
417
417
 
418
- To run your test:
418
+ To run your tests within the context of your current app/repo:
419
+
420
+ ```
421
+ $ bundle exec derailed exec perf:app
422
+ ```
423
+
424
+ This will automatically test the two latest commits of your library/current directory.
425
+
426
+ If you'd like to test the Rails library instead, make sure that `ENV[DERAILED_PATH_TO_LIBRARY]` is unset.
419
427
 
420
428
  ```
421
429
  $ bundle exec derailed exec perf:library
422
430
  ```
423
431
 
424
- This will automatically test the two latest commits of Rails (or the library you've specified). If you would like to compare against different SHAs you can manually specify them:
432
+ This will automatically test the two latest commits of Rails.
433
+
434
+ If you would also like to compare against different SHAs you can manually specify them:
425
435
 
426
436
  ```
427
437
  $ SHAS_TO_TEST="7b4d80cb373e,13d6aa3a7b70" bundle exec derailed exec perf:library
@@ -453,7 +463,7 @@ You can use this to test changes in other libraries that aren't rails, you just
453
463
 
454
464
  > To get the best results before running tests you should close all programs on your laptop, turn on a program to prevent your laptop from going to sleep (or increase your sleep timer). Make sure it's plugged into a power outlet and go grab a cup of coffee. If you do anything on your laptop while this test is running you risk the chance of skewing your results.
455
465
 
456
- By default derailed will stop once statistical signficance has been detected, you can tune this behavior by setting `DERAILED_STOP_VALID_COUNT` env var. Setting this to a positive number, will increase the number of iterations required that are detected to be statistically significant. For example setting it to 100 might result in 120 runs if it takes 20 runs to detect significance. Generally the more runs you have, the more accurate your averages will be. You can disable this all together by setting `DERAILED_STOP_VALID_COUNT=0` which will force derailed to run all iterations.
466
+ As the test is executing, intermediate results will be printed every 50 iterations.
457
467
 
458
468
  ## Environment Variables
459
469
 
@@ -20,7 +20,7 @@ Gem::Specification.new do |gem|
20
20
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
21
  gem.require_paths = ["lib"]
22
22
 
23
- gem.required_ruby_version = ">= 2.1.0"
23
+ gem.required_ruby_version = ">= 2.2.0"
24
24
 
25
25
  gem.add_dependency "heapy", "~> 0"
26
26
  gem.add_dependency "memory_profiler", "~> 0"
@@ -28,12 +28,13 @@ Gem::Specification.new do |gem|
28
28
  gem.add_dependency "benchmark-ips", "~> 2"
29
29
  gem.add_dependency "rack", ">= 1"
30
30
  gem.add_dependency "rake", "> 10", "< 14"
31
- gem.add_dependency "thor", "~> 0.19"
31
+ gem.add_dependency "thor", ">= 0.19", "< 2"
32
32
  gem.add_dependency "ruby-statistics", ">= 2.1"
33
+ gem.add_dependency "mini_histogram", ">= 0.2.1"
33
34
 
34
35
  gem.add_development_dependency "capybara", "~> 2"
35
36
  gem.add_development_dependency "m"
36
- gem.add_development_dependency "rails", "> 3", "<= 6"
37
+ gem.add_development_dependency "rails", "> 3", "<= 7"
37
38
  gem.add_development_dependency "devise", "> 3", "< 6"
38
39
  gem.add_development_dependency "appraisal", "2.2.0"
39
40
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # BUNDLE_GEMFILE="gemfiles/rails_5_1.gemfile" bundle exec m test/integration/tasks_test.rb:30
4
+ #
3
5
  # This file was generated by Appraisal
4
6
 
5
7
  source "https://rubygems.org"
6
8
 
7
- gem "rails", "~> 5.1.0"
9
+ gem "rails", "~> 5.1.7"
8
10
 
9
11
  group :development, :test do
10
12
  gem "sqlite3", platform: [:ruby, :mswin, :mingw]
@@ -1,10 +1,10 @@
1
- # frozen_string_literal: true
2
-
3
1
  # This file was generated by Appraisal
2
+ #
3
+ # BUNDLE_GEMFILE="gemfiles/rails_5_2.gemfile" bundle exec m test/integration/tasks_test.rb:30
4
4
 
5
5
  source "https://rubygems.org"
6
6
 
7
- gem "rails", "~> 5.2.0"
7
+ gem "rails", "~> 5.2.4.4"
8
8
 
9
9
  group :development, :test do
10
10
  gem "sqlite3", platform: [:ruby, :mswin, :mingw]
@@ -43,8 +43,9 @@ end
43
43
  require 'derailed_benchmarks/require_tree'
44
44
  require 'derailed_benchmarks/auth_helper'
45
45
 
46
- require 'derailed_benchmarks/stats_in_file'
46
+ require 'derailed_benchmarks/stats_for_file'
47
47
  require 'derailed_benchmarks/stats_from_dir'
48
+ require 'derailed_benchmarks/git/switch_project'
48
49
 
49
50
  if DerailedBenchmarks.gem_is_bundled?("devise")
50
51
  DerailedBenchmarks.auth = DerailedBenchmarks::AuthHelpers::Devise.new
@@ -11,14 +11,24 @@ ENV['CUT_OFF'] ||= "0.3"
11
11
  # Monkey patch kernel to ensure that all `require` calls call the same
12
12
  # method
13
13
  module Kernel
14
+ REQUIRE_STACK = []
14
15
 
15
- private
16
+ module_function
16
17
 
17
- alias :original_require :require
18
- REQUIRE_STACK = []
18
+ alias_method :original_require, :require
19
+ alias_method :original_require_relative, :require_relative
20
+ alias_method(:original_load, :load)
21
+
22
+ def load(file, wrap = false)
23
+ measure_memory_impact(file) do |file|
24
+ original_load(file)
25
+ end
26
+ end
19
27
 
20
28
  def require(file)
21
- Kernel.require(file)
29
+ measure_memory_impact(file) do |file|
30
+ original_require(file)
31
+ end
22
32
  end
23
33
 
24
34
  def require_relative(file)
@@ -29,10 +39,7 @@ module Kernel
29
39
  end
30
40
  end
31
41
 
32
- class << self
33
- alias :original_require :require
34
- alias :original_require_relative :require_relative
35
- end
42
+ private
36
43
 
37
44
  # The core extension we use to measure require time of all requires
38
45
  # When a file is required we create a tree node with its file name.
@@ -46,7 +53,7 @@ module Kernel
46
53
  # When a require returns we remove it from the require stack so we don't
47
54
  # accidentally push additional children nodes to it. We then store the
48
55
  # memory cost of the require in the tree node.
49
- def self.measure_memory_impact(file, &block)
56
+ def measure_memory_impact(file, &block)
50
57
  mem = GetProcessMem.new
51
58
  node = DerailedBenchmarks::RequireTree.new(file)
52
59
 
@@ -64,31 +71,29 @@ module Kernel
64
71
  end
65
72
  end
66
73
 
67
- # Top level node that will store all require information for the entire app
68
- TOP_REQUIRE = DerailedBenchmarks::RequireTree.new("TOP")
69
- REQUIRE_STACK.push(TOP_REQUIRE)
70
-
71
- Kernel.define_singleton_method(:require) do |file|
72
- measure_memory_impact(file) do |file|
73
- # "source_annotation_extractor" is deprecated in Rails 6
74
- # # if we don't skip the library it leads to a crash
75
- # next if file == "rails/source_annotation_extractor" && Rails.version >= '6.0'
76
- original_require(file)
77
- end
78
- end
79
74
 
75
+ # I honestly have no idea why this Object delegation is needed
76
+ # I keep staring at bootsnap and it doesn't have to do this
77
+ # is there a bug in their implementation they haven't caught or
78
+ # am I doing something different?
80
79
  class Object
81
80
  private
81
+ def load(path, wrap = false)
82
+ Kernel.load(path, wrap)
83
+ end
82
84
 
83
85
  def require(path)
84
86
  Kernel.require(path)
85
87
  end
86
88
  end
87
89
 
88
- # Don't forget to assign a cost to the top level
89
- cost_before_requiring_anything = GetProcessMem.new.mb
90
- TOP_REQUIRE.cost = cost_before_requiring_anything
90
+ # Top level node that will store all require information for the entire app
91
+ TOP_REQUIRE = DerailedBenchmarks::RequireTree.new("TOP")
92
+ REQUIRE_STACK.push(TOP_REQUIRE)
93
+ TOP_REQUIRE.cost = GetProcessMem.new.mb
94
+
91
95
  def TOP_REQUIRE.print_sorted_children(*args)
92
96
  self.cost = GetProcessMem.new.mb - self.cost
93
97
  super
94
98
  end
99
+
@@ -0,0 +1,36 @@
1
+ module DerailedBenchmarks
2
+ # Represents a specific commit in a git repo
3
+ #
4
+ # Can be used to get information from the commit or to check it out
5
+ #
6
+ # commit = GitCommit.new(path: "path/to/repo", ref: "6e642963acec0ff64af51bd6fba8db3c4176ed6e")
7
+ # commit.short_sha # => "6e64296"
8
+ # commit.checkout! # Will check out the current commit at the repo in the path
9
+ class Git::Commit
10
+ attr_reader :ref, :description, :time, :short_sha, :log
11
+
12
+ def initialize(path: , ref: , log_dir: Pathname.new("/dev/null"))
13
+ @in_git_path = Git::InPath.new(path)
14
+ @ref = ref
15
+ @log = log_dir.join("#{file_safe_ref}.bench.txt")
16
+
17
+ Dir.chdir(path) do
18
+ checkout!
19
+ @description = @in_git_path.description
20
+ @short_sha = @in_git_path.short_sha
21
+ @time = @in_git_path.time
22
+ end
23
+ end
24
+
25
+ alias :desc :description
26
+ alias :file :log
27
+
28
+ def checkout!
29
+ @in_git_path.checkout!(ref)
30
+ end
31
+
32
+ private def file_safe_ref
33
+ ref.gsub('/', ':')
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ module DerailedBenchmarks
2
+ # A class for running commands in a git directory
3
+ #
4
+ # It's faster to check if we're already in that directory instead
5
+ # of having to `cd` into each time. https://twitter.com/schneems/status/1305196730170961920
6
+ #
7
+ # Example:
8
+ #
9
+ # in_git_path = InGitPath.new(`bundle info heapy --path`.strip)
10
+ # in_git_path.checkout!("f0f92b06156f2274021aa42f15326da041ee9009")
11
+ # in_git_path.short_sha # => "f0f92b0"
12
+ class Git::InPath
13
+ attr_reader :path
14
+
15
+ def initialize(path)
16
+ @path = path
17
+ end
18
+
19
+ def description
20
+ run!("git log --oneline --format=%B -n 1 HEAD | head -n 1")
21
+ end
22
+
23
+ def short_sha
24
+ run!("git rev-parse --short HEAD")
25
+ end
26
+
27
+ def time_stamp_string
28
+ run!("git log -n 1 --pretty=format:%ci") # https://stackoverflow.com/a/25921837/147390
29
+ end
30
+
31
+ def branch
32
+ branch = run!("git rev-parse --abbrev-ref HEAD")
33
+ branch == "HEAD" ? nil : branch
34
+ end
35
+
36
+ def checkout!(ref)
37
+ run!("git checkout '#{ref}' 2>&1")
38
+ end
39
+
40
+ def time
41
+ DateTime.parse(time_stamp_string)
42
+ end
43
+
44
+ def run(cmd)
45
+ if Dir.pwd == path
46
+ out = `#{cmd}`.strip
47
+ else
48
+ out = `cd #{path} && #{cmd}`.strip
49
+ end
50
+ out
51
+ end
52
+
53
+ def run!(cmd)
54
+ out = run(cmd)
55
+ raise "Error while running #{cmd.inspect}: #{out}" unless $?.success?
56
+ out
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,128 @@
1
+ module DerailedBenchmarks
2
+ class Git
3
+ end
4
+ end
5
+ require_relative "in_path.rb"
6
+ require_relative "commit.rb"
7
+
8
+ module DerailedBenchmarks
9
+ # Wraps two or more git commits in a specific location
10
+ #
11
+ # Returns an array of GitCommit objects that can be used to manipulate
12
+ # and checkout the repo
13
+ #
14
+ # Example:
15
+ #
16
+ # `git clone https://sharpstone/default_ruby tmp/default_ruby`
17
+ #
18
+ # project = GitSwitchProject.new(path: "tmp/default_ruby")
19
+ #
20
+ # By default it will represent the last two commits:
21
+ #
22
+ # project.commits.length # => 2
23
+ #
24
+ # You can pass in explicit REFs in an array:
25
+ #
26
+ # ref_array = ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"]
27
+ # project = GitSwitchProject.new(path: "tmp/default_ruby", ref_array: ref_array)
28
+ #
29
+ # puts project.commits.map(&:ref) == ref_array # => true
30
+ #
31
+ #
32
+ # It knows the current branch or sha:
33
+ #
34
+ # `cd tmp/ruby && git checkout -b mybranch`
35
+ # project.current_branch_or_sha #=> "mybranch"
36
+ #
37
+ # It can be used for safely wrapping checkouts to ensure the project returns to it's original branch:
38
+ #
39
+ # project.restore_branch_on_return do
40
+ # project.commits.first.checkout!
41
+ # project.current_branch_or_sha # => "da748a593"
42
+ # end
43
+ #
44
+ # project.current_branch_or_sha # => "mybranch"
45
+ class Git::SwitchProject
46
+ attr_reader :commits
47
+
48
+ def initialize(path: , ref_array: [], io: STDOUT, log_dir: "/dev/null")
49
+ @path = Pathname.new(path)
50
+
51
+ @in_git_path = Git::InPath.new(@path.expand_path)
52
+
53
+ raise "Must be a path with a .git directory '#{@path}'" if !@path.join(".git").exist?
54
+ @io = io
55
+ @commits = []
56
+ log_dir = Pathname(log_dir)
57
+
58
+ expand_refs(ref_array).each do |ref|
59
+ restore_branch_on_return(quiet: true) do
60
+ @commits << Git::Commit.new(path: @path, ref: ref, log_dir: log_dir)
61
+ end
62
+ end
63
+
64
+ if (duplicate = @commits.group_by(&:short_sha).detect {|(k, v)| v.length > 1})
65
+ raise "Duplicate SHA resolved #{duplicate[0].inspect}: #{duplicate[1].map {|c| "'#{c.ref}' => '#{c.short_sha}'"}.join(", ") } at #{@path}"
66
+ end
67
+ end
68
+
69
+ def current_branch_or_sha
70
+ branch_or_sha = @in_git_path.branch
71
+ branch_or_sha ||= @in_git_path.short_sha
72
+ branch_or_sha
73
+ end
74
+
75
+ def dirty?
76
+ !clean?
77
+ end
78
+
79
+ # https://stackoverflow.com/a/3879077/147390
80
+ def clean?
81
+ @in_git_path.run("git diff-index --quiet HEAD --") && $?.success?
82
+ end
83
+
84
+ private def status(pattern: "*.gemspec")
85
+ @in_git_path.run("git status #{pattern}")
86
+ end
87
+
88
+ def restore_branch_on_return(quiet: false)
89
+ if dirty? && status.include?("gemspec")
90
+ dirty_gemspec = true
91
+ unless quiet
92
+ @io.puts "Working tree at #{@path} is dirty, stashing. This will be popped on return"
93
+ @io.puts "Bundler modifies gemspec files on git install, this is normal"
94
+ @io.puts "Original status:\n#{status}"
95
+ end
96
+ @in_git_path.run!("git stash")
97
+ end
98
+ branch_or_sha = self.current_branch_or_sha
99
+ yield
100
+ ensure
101
+ return unless branch_or_sha
102
+ @io.puts "Resetting git dir of '#{@path.to_s}' to #{branch_or_sha.inspect}" unless quiet
103
+
104
+ @in_git_path.checkout!(branch_or_sha)
105
+ if dirty_gemspec
106
+ out = @in_git_path.run!("git stash apply 2>&1")
107
+ @io.puts "Applying stash of '#{@path.to_s}':\n#{out}" unless quiet
108
+ end
109
+ end
110
+
111
+ # case ref_array.length
112
+ # when >= 2
113
+ # returns original array
114
+ # when 1
115
+ # returns the given ref plus the one before it
116
+ # when 0
117
+ # returns the most recent 2 refs
118
+ private def expand_refs(ref_array)
119
+ return ref_array if ref_array.length >= 2
120
+
121
+ @in_git_path.checkout!(ref_array.first) if ref_array.first
122
+
123
+ branches_string = @in_git_path.run!("git log --format='%H' -n 2")
124
+ ref_array = branches_string.split($/)
125
+ return ref_array
126
+ end
127
+ end
128
+ end