lapidarist 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 52a3f4f375bdf8923d629d74fbe00ebfb7e7dd84738ff6c8e3977d1be2dbcba0
4
+ data.tar.gz: f6b6ff542df51de84e710725b8a8d9bc53b9e39d529fb1c4e61cbf6eb4d1c40f
5
+ SHA512:
6
+ metadata.gz: 3ba56292233a696e78c23d274d8ab85d8ec04c5345d2991e2cbbae7f6b52b3dd08090c90af08eed8f63916676061f87415271dfeb6b2e6da020985cf07890f49
7
+ data.tar.gz: e09937e6620630a277868e27941fca69d893b3fb2513fa0ca24cbfdb06975c5ed238f33604297e6628d73edeaf3b185faed32ba83f55fef7043f0d255c2d25d3
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in lapidarist.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Mark Gangl
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.
@@ -0,0 +1,39 @@
1
+ # Lapidarist
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/lapidarist`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'lapidarist'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install lapidarist
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/attack/lapidarist.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "lapidarist"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "lapidarist"
4
+
5
+ cli = Lapidarist::CLI.new(ARGV)
6
+ exit cli.run
@@ -0,0 +1,26 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "lapidarist/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "lapidarist"
7
+ spec.version = Lapidarist::VERSION
8
+ spec.authors = ["Mark Gangl"]
9
+ spec.email = ["mark@attackcorp.com"]
10
+
11
+ spec.summary = %q{Automatically update ruby gem dependencies.}
12
+ spec.description = %q{Sit back, relax, and allow Lapidarist to do the heavy lifiting and update your ruby gem dependencies for you.}
13
+ spec.homepage = "https://github.com/attack/lapidarist"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(spec)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.16"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.0"
26
+ end
@@ -0,0 +1,29 @@
1
+ require 'lapidarist/version'
2
+ require 'open3'
3
+
4
+ require_relative 'lapidarist/configuration'
5
+ require_relative 'lapidarist/options'
6
+ require_relative 'lapidarist/level'
7
+ require_relative 'lapidarist/gem'
8
+ require_relative 'lapidarist/gems'
9
+
10
+ require_relative 'lapidarist/logger'
11
+ require_relative 'lapidarist/shell'
12
+ require_relative 'lapidarist/bundle_command'
13
+ require_relative 'lapidarist/git_command'
14
+ require_relative 'lapidarist/test_command'
15
+ require_relative 'lapidarist/update'
16
+ require_relative 'lapidarist/outdated'
17
+ require_relative 'lapidarist/sha'
18
+
19
+ require_relative 'lapidarist/summary'
20
+ require_relative 'lapidarist/status'
21
+ require_relative 'lapidarist/cli'
22
+
23
+ module Lapidarist
24
+ class << self
25
+ def config
26
+ @config ||= Lapidarist::Configuration.new
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ module Lapidarist
2
+ class BundleCommand
3
+ def initialize
4
+ @shell = Shell.new
5
+ @logger = Logger.new
6
+ end
7
+
8
+ def outdated
9
+ shell.run('cat Gemfile') if Lapidarist.config.debug
10
+
11
+ Enumerator.new do |y|
12
+ shell.run('bundle outdated --strict') do |std_out_err|
13
+ while line = std_out_err.gets
14
+ logger.std_out_err(line, 'bundle outdated')
15
+ gem = parse_gem_from(line)
16
+ y.yield(gem) if gem
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def update(gem, level: MAJOR)
23
+ shell.run("bundle update #{gem.name} --strict --#{level.to_s}")
24
+ end
25
+
26
+ def version(gem)
27
+ stdout = shell.run('bundle list', "grep \" #{gem.name} \"")
28
+ result = stdout.match(/\((?<version>[0-9\.]+)\)/)
29
+ result[:version] if result
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :shell, :logger
35
+
36
+ def parse_gem_from(line)
37
+ regex = / \* (.*) \(newest (\d[\d\.]*\d)[,\s] installed (\d[\d\.]*\d)[\),\s](.*groups \"(.*)\")?/.match line
38
+
39
+ unless regex.nil?
40
+ Gem.new(
41
+ name: regex[1],
42
+ newest_version: regex[2],
43
+ installed_version: regex[3],
44
+ groups: Array(regex[5]&.split(',')).map(&:strip)
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,94 @@
1
+ module Lapidarist
2
+ class CLI
3
+ def initialize(args)
4
+ @args = args
5
+ @git = GitCommand.new
6
+ @test = TestCommand.new
7
+ @outdated = Outdated.new
8
+ @update = Update.new
9
+ @sha = Sha.new
10
+
11
+ @logger = Logger.new
12
+ @logger.setup
13
+ end
14
+
15
+ def run
16
+ Options.new(args).parse
17
+ logger.header('Starting lapidarist')
18
+ logger.debug("directory: #{Lapidarist.config.directory}", :options)
19
+ logger.debug("test_script: #{Lapidarist.config.test_script}", :options)
20
+
21
+ unless git.clean?
22
+ logger.footer('stopping, there are uncommitted changes')
23
+ return 1
24
+ end
25
+
26
+ sha.record_good
27
+ gems = outdated.run
28
+
29
+ status = nil
30
+ 1.step do |attempt|
31
+ logger.header("Attempt ##{attempt}")
32
+
33
+ if gems.outdated.none?
34
+ logger.footer('stopping, there are no applicable outdated gems')
35
+ status = Status.new(gems, attempt)
36
+ break
37
+ end
38
+
39
+ updated_gems = update.run(gems, attempt)
40
+
41
+ if sha.new_commit_count.zero?
42
+ logger.footer('nothing updated, trying again')
43
+ gems = gems.merge(updated_gems)
44
+ next
45
+ end
46
+
47
+ logger.header("Testing gem updates")
48
+ if test.success?
49
+ logger.footer('test passed, nothing left to do')
50
+ gems = gems.merge(updated_gems)
51
+ status = Status.new(gems, attempt)
52
+ break
53
+ else
54
+ logger.footer('test failed, investigating failure')
55
+ end
56
+
57
+ failed_gem =
58
+ if updated_gems.one?
59
+ updated_but_failed_gem = updated_gems.first
60
+ git.reset_hard('HEAD^')
61
+
62
+ Gem.from(
63
+ updated_but_failed_gem,
64
+ attempt: attempt,
65
+ status: :failed,
66
+ updated_version: updated_but_failed_gem.latest_attempt[:version],
67
+ level: updated_but_failed_gem.latest_attempt[:level]
68
+ )
69
+ else
70
+ failed_gem_name = git.bisect(sha.last_good, test)
71
+ updated_but_failed_gem = updated_gems.detect { |g| g.name == failed_gem_name }
72
+ gems = gems.merge(updated_gems.take(sha.new_commit_count))
73
+ sha.record_good
74
+
75
+ Gem.from(
76
+ updated_but_failed_gem,
77
+ attempt: attempt,
78
+ status: :failed,
79
+ updated_version: updated_but_failed_gem.latest_attempt[:version],
80
+ level: updated_but_failed_gem.latest_attempt[:level]
81
+ )
82
+ end
83
+ gems = gems.merge(failed_gem)
84
+ end
85
+
86
+ Summary.new(gems, logger).display
87
+ return status.to_i
88
+ end
89
+
90
+ private
91
+
92
+ attr_reader :args, :git, :test, :outdated, :update, :sha, :logger
93
+ end
94
+ end
@@ -0,0 +1,22 @@
1
+ require 'ostruct'
2
+
3
+ module Lapidarist
4
+ class Configuration < OpenStruct
5
+ def initialize
6
+ super(
7
+ directory: Pathname.new('.'),
8
+ test_script: 'test.sh',
9
+ all: false,
10
+ quiet: false,
11
+ verbosity: 0,
12
+ commit_flags: '',
13
+ debug: false,
14
+ log_path: Pathname.new('./tmp/lapidarist.log'),
15
+ update_limit: nil,
16
+ groups: [],
17
+ version: MAJOR,
18
+ recursive: false
19
+ )
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,137 @@
1
+ module Lapidarist
2
+ class Gem
3
+ attr_reader :name, :newest_version, :installed_version, :attempts
4
+
5
+ def initialize(name:, newest_version:, installed_version:, groups: [], attempts: {})
6
+ @name = name
7
+ @newest_version = newest_version
8
+ @installed_version = installed_version
9
+ @groups = groups
10
+ @attempts = attempts
11
+ end
12
+
13
+ def self.from(gem, attempt: 0, status: nil, reason: nil, updated_version: nil, level: nil)
14
+ attempts = gem.attempts
15
+
16
+ if status
17
+ attempts = attempts.merge(
18
+ attempt => {
19
+ status: status,
20
+ reason: reason,
21
+ version: updated_version,
22
+ level: level
23
+ }
24
+ )
25
+ end
26
+
27
+ new(
28
+ name: gem.name,
29
+ newest_version: gem.newest_version,
30
+ installed_version: gem.installed_version,
31
+ groups: gem.groups,
32
+ attempts: attempts
33
+ )
34
+ end
35
+
36
+ def ==(other_gem)
37
+ current_status == other_gem.current_status &&
38
+ name == other_gem.name &&
39
+ installed_version == other_gem.installed_version &&
40
+ newest_version == other_gem.newest_version &&
41
+ groups == other_gem.groups
42
+ end
43
+
44
+ def groups
45
+ @groups.sort
46
+ end
47
+
48
+ def dependency?
49
+ groups.any?
50
+ end
51
+
52
+ def current_status
53
+ latest_attempt&.fetch(:status, nil)
54
+ end
55
+
56
+ def current_level
57
+ latest_attempt&.fetch(:level, nil)
58
+ end
59
+
60
+ def outdated?(recursive: false)
61
+ current_status.nil? || (recursive && available_update_levels?)
62
+ end
63
+
64
+ def failed?
65
+ current_status == :failed
66
+ end
67
+
68
+ def skip?
69
+ current_status == :skipped
70
+ end
71
+
72
+ def updated?
73
+ current_status == :updated
74
+ end
75
+
76
+ def updated_version
77
+ updated_attempt&.fetch(:version, nil)
78
+ end
79
+
80
+ def what_changed
81
+ if version_changed?
82
+ "#{name} from #{installed_version} to #{updated_version}"
83
+ else
84
+ "#{name} dependencies"
85
+ end
86
+ end
87
+
88
+ def available_update_levels?
89
+ failed? && current_level > PATCH
90
+ end
91
+
92
+ def next_semver_level
93
+ if current_level
94
+ LEVELS.detect { |level| level < current_level }
95
+ else
96
+ MAJOR
97
+ end
98
+ end
99
+
100
+ def log_s
101
+ [
102
+ "outdated gem: #{name}",
103
+ "installed: #{installed_version}",
104
+ "newest: #{newest_version}",
105
+ "updated: #{updated_version}"
106
+ ].join(', ')
107
+ end
108
+
109
+ def to_h
110
+ {
111
+ name: name,
112
+ newest_version: newest_version,
113
+ installed_version: installed_version,
114
+ groups: groups,
115
+ attempts: attempts.to_h
116
+ }
117
+ end
118
+
119
+ def latest_attempt_number
120
+ @latest_attempt_number ||= attempts.keys.last
121
+ end
122
+
123
+ def latest_attempt
124
+ @latest_attempt ||= attempts[latest_attempt_number] || {}
125
+ end
126
+
127
+ private
128
+
129
+ def version_changed?
130
+ updated_version && installed_version != updated_version
131
+ end
132
+
133
+ def updated_attempt
134
+ @updated_attempt ||= attempts.values.detect { |a| a[:status] == :updated }
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,47 @@
1
+ module Lapidarist
2
+ class Gems
3
+ include Enumerable
4
+
5
+ def initialize(gems)
6
+ @gems = gems
7
+ end
8
+
9
+ def each(&block)
10
+ gems.sort_by(&:name).each(&block)
11
+ end
12
+
13
+ def outdated
14
+ entries.select do |gem|
15
+ if (Lapidarist.config.all || gem.dependency?) && gem.outdated?(recursive: Lapidarist.config.recursive)
16
+ gem
17
+ end
18
+ end
19
+ end
20
+
21
+ def updated
22
+ entries.select(&:updated?)
23
+ end
24
+
25
+ def skipped
26
+ entries.select(&:skip?)
27
+ end
28
+
29
+ def failed
30
+ entries.select(&:failed?)
31
+ end
32
+
33
+ def merge(other_gems)
34
+ Gems.new(
35
+ Array(other_gems) + entries.select { |gem| !Array(other_gems).map(&:name).include?(gem.name) }
36
+ )
37
+ end
38
+
39
+ def attempts
40
+ entries.map(&:latest_attempt_number).compact.max
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :gems
46
+ end
47
+ end
@@ -0,0 +1,128 @@
1
+ module Lapidarist
2
+ class GitCommand
3
+ def initialize
4
+ @shell = Shell.new
5
+ @logger = Logger.new
6
+ end
7
+
8
+ def head
9
+ shell.run('git rev-parse HEAD')[0].strip
10
+ end
11
+
12
+ def add(*files)
13
+ shell.run("git add #{files.join(' ')}")
14
+ end
15
+
16
+ def commit(message)
17
+ shell.run("git commit -m '#{message}' #{Lapidarist.config.commit_flags}".strip, label: 'git commit')
18
+ end
19
+
20
+ def bisect(start_sha, test)
21
+ logger.header('Starting bisect')
22
+ bisect_start(start_sha)
23
+ bisect_run(start_sha, test)
24
+ end
25
+
26
+ def log(sha)
27
+ shell.run("git log HEAD...#{sha}^ --no-color --oneline", label: 'git log')
28
+ end
29
+
30
+ def reset_hard(ref)
31
+ shell.run("git reset --hard #{ref}")
32
+ end
33
+
34
+ def clean?
35
+ shell.run('[ -z "$(git status --porcelain)" ]')[1] == 0
36
+ end
37
+
38
+ def count_commits(start_sha, end_sha)
39
+ shell.run("git rev-list #{end_sha} ^#{start_sha} --count")[0].to_i
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :shell, :logger
45
+
46
+ def bisect_start(sha)
47
+ shell.run('git bisect start')
48
+ shell.run('git bisect bad')
49
+ shell.run("git bisect good #{sha}", label: 'git bisect good')
50
+ end
51
+
52
+ def bisect_run(start_sha, test)
53
+ failing_gem_name = nil
54
+
55
+ shell.run("git bisect run #{test}") do |std_out_err|
56
+ while line = std_out_err.gets
57
+ bisect_step = BisectStep.new(line, shell)
58
+
59
+ if bisect_step.failure?
60
+ failing_sha = bisect_step.failing_sha
61
+ failing_gem_name = bisect_step.failing_gem(failing_sha)
62
+ logger.info("... found failing gem update: #{failing_gem_name}")
63
+ end
64
+
65
+ if bisect_step.success?
66
+ bisect_reset
67
+ rewind_to_last_good_commit(failing_sha)
68
+ end
69
+ end
70
+
71
+ unless failing_gem_name
72
+ logger.info("... last commit was failing commit")
73
+ end
74
+
75
+ logger.footer("bisect done")
76
+ end
77
+
78
+ if failing_gem_name && Lapidarist.config.debug
79
+ log(start_sha)
80
+ end
81
+
82
+ failing_gem_name
83
+ end
84
+
85
+ def bisect_reset
86
+ shell.run('git bisect reset')
87
+ end
88
+
89
+ def rewind_to_last_good_commit(sha)
90
+ reset_hard("#{sha}^")
91
+ end
92
+ end
93
+
94
+ class BisectStep
95
+ def initialize(line, shell)
96
+ @line = line
97
+ @shell = shell
98
+ end
99
+
100
+ def success?
101
+ line == "bisect run success\n"
102
+ end
103
+
104
+ def failure?
105
+ !failing_sha.nil?
106
+ end
107
+
108
+ def failing_sha
109
+ sha_regex = Regexp::new("(.*) is the first bad commit\n").match(line)
110
+ unless sha_regex.nil?
111
+ sha_regex[1]
112
+ end
113
+ end
114
+
115
+ def failing_gem(sha)
116
+ commit_message = shell.run("git log --format=%s -n 1 #{sha}", label: 'git log')[0]
117
+
118
+ sha_regex = Regexp::new('Update (.*) from').match(commit_message)
119
+ unless sha_regex.nil?
120
+ sha_regex[1]
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ attr_reader :shell, :line
127
+ end
128
+ end
@@ -0,0 +1,32 @@
1
+ module Lapidarist
2
+ class Level
3
+ include Comparable
4
+
5
+ attr_reader :index
6
+
7
+ def initialize(name:, index:)
8
+ @name = name
9
+ @index = index
10
+ end
11
+
12
+ def to_s
13
+ name.to_s
14
+ end
15
+
16
+ def <=>(other)
17
+ return 1 if index < other.index
18
+ return 0 if index == other.index
19
+ return -1 if index > other.index
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :name
25
+ end
26
+
27
+ MAJOR = Level.new(name: :major, index: 1)
28
+ MINOR = Level.new(name: :minor, index: 2)
29
+ PATCH = Level.new(name: :patch, index: 3)
30
+
31
+ LEVELS = [MAJOR, MINOR, PATCH].freeze
32
+ end
@@ -0,0 +1,76 @@
1
+ module Lapidarist
2
+ class Logger
3
+ def header(message)
4
+ write '', 0
5
+ write "#{message} ...", 0
6
+ end
7
+
8
+ def smart_header(message)
9
+ if Lapidarist.config.verbosity > 0
10
+ header(message)
11
+ else
12
+ info(message, 0)
13
+ end
14
+ end
15
+
16
+ def footer(message)
17
+ write "... #{message}", 0
18
+ end
19
+
20
+ def info(message, level = 0)
21
+ write message, level
22
+ end
23
+
24
+ def debug(message, label = nil)
25
+ return unless Lapidarist.config.debug
26
+
27
+ if label
28
+ puts " DEBUG (#{label}) > #{message}"
29
+ else
30
+ puts " DEBUG > #{message}"
31
+ end
32
+ end
33
+
34
+ def std_out_err(message, command)
35
+ write "OUT_ERR (#{command}) > #{message.strip}", 2
36
+ end
37
+
38
+ def setup
39
+ if Lapidarist.config.log_path && !Lapidarist.config.log_path.empty?
40
+ Open3.capture2("mkdir -p #{Lapidarist.config.log_path.dirname}", chdir: Lapidarist.config.directory)
41
+ Open3.capture2("touch #{Lapidarist.config.log_path}", chdir: Lapidarist.config.directory)
42
+ clear_log_file
43
+ end
44
+ end
45
+
46
+ def summary(message)
47
+ write_std_out(message, 0)
48
+ write_log_file(message)
49
+ end
50
+
51
+ private
52
+
53
+ def write(message, level)
54
+ log_line = "#{' ' * level}#{message}"
55
+
56
+ write_std_out(log_line, level)
57
+ write_log_file(log_line)
58
+ end
59
+
60
+ def write_std_out(message, level)
61
+ if level <= Lapidarist.config.verbosity && !Lapidarist.config.quiet
62
+ puts message
63
+ end
64
+ end
65
+
66
+ def clear_log_file
67
+ Open3.capture2("cp /dev/null #{Lapidarist.config.log_path}", chdir: Lapidarist.config.directory)
68
+ end
69
+
70
+ def write_log_file(message)
71
+ if Lapidarist.config.log_path && !Lapidarist.config.log_path.empty?
72
+ Open3.capture2("echo \"#{message}\" >> #{Lapidarist.config.log_path}", chdir: Lapidarist.config.directory)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,79 @@
1
+ require 'optparse'
2
+
3
+ module Lapidarist
4
+ class Options
5
+ def initialize(args)
6
+ @args = args
7
+ end
8
+
9
+ def parse
10
+ opt_parser = OptionParser.new do |opts|
11
+ opts.on("-d", "--directory DIRECTORY", "Directory to run Lapidarist from.") do |d|
12
+ Lapidarist.config.directory = Pathname.new(d)
13
+ end
14
+
15
+ opts.on("-t", "--test TEST_SCRIPT", "Test script given to git bisect.") do |t|
16
+ Lapidarist.config.test_script = t
17
+ end
18
+
19
+ opts.on("-a", "--all", "Update gems that are sub-dependencies.") do |t|
20
+ Lapidarist.config.all = true
21
+ end
22
+
23
+ opts.on("-q", "--quiet", "Do not print anything to stdout.") do |t|
24
+ Lapidarist.config.quiet = true
25
+ end
26
+
27
+ opts.on("-v", "Increase verbosity, repeat for more verbosity.") do |t|
28
+ Lapidarist.config.verbosity += 1
29
+ end
30
+
31
+ opts.on("-f", "--commit-flags flags", "Append flags to the commit command.") do |t|
32
+ Lapidarist.config.commit_flags = t
33
+ end
34
+
35
+ opts.on("--debug", "Display debug output.") do |t|
36
+ Lapidarist.config.debug = true
37
+ end
38
+
39
+ opts.on("-l LOG_PATH", "Path to log file") do |t|
40
+ Lapidarist.config.log_path = t
41
+ end
42
+
43
+ opts.on("-n NUMBER_OF_GEMS", "Limit the number of gems to be updated.") do |t|
44
+ Lapidarist.config.update_limit = t.to_i
45
+ end
46
+
47
+ opts.on("--one", "Limit the number of gems to be updated to just one.") do |t|
48
+ Lapidarist.config.update_limit = 1
49
+ end
50
+
51
+ opts.on("-g GROUP_NAME", "--group GROUP_NAME", "Limit gems to be updated to a specified group(s).") do |g|
52
+ Lapidarist.config.groups << g
53
+ end
54
+
55
+ opts.on("--major", "Limit updates to major, minor and patch versions (essentially everything).") do |p|
56
+ Lapidarist.config.version = MAJOR
57
+ end
58
+
59
+ opts.on("--minor", "Limit updates to minor and patch versions.") do |p|
60
+ Lapidarist.config.version = MINOR
61
+ end
62
+
63
+ opts.on("--patch", "Limit updates to patch versions.") do |p|
64
+ Lapidarist.config.version = PATCH
65
+ end
66
+
67
+ opts.on("-r", "--recursive", "Try updating a the major version, minor version then patch version.") do |t|
68
+ Lapidarist.config.recursive = true
69
+ end
70
+ end
71
+
72
+ opt_parser.parse!(args)
73
+ end
74
+
75
+ private
76
+
77
+ attr_reader :args
78
+ end
79
+ end
@@ -0,0 +1,37 @@
1
+ module Lapidarist
2
+ class Outdated
3
+ def initialize
4
+ @bundle = BundleCommand.new
5
+ @logger = Logger.new
6
+ end
7
+
8
+ def run
9
+ logger.header('Detecting outdated gems')
10
+
11
+ gems = bundle.outdated.each_with_object([]) do |gem, results|
12
+ reason = reason_to_skip(gem)
13
+ if reason.nil?
14
+ logger.info(" + #{gem.log_s}")
15
+ results.push Gem.from(gem)
16
+ else
17
+ logger.info(" - (#{reason}) #{gem.log_s}")
18
+ results.push Gem.from(gem, status: :skipped, reason: reason)
19
+ end
20
+ end
21
+
22
+ Gems.new(gems)
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :bundle, :logger
28
+
29
+ def reason_to_skip(gem)
30
+ if !Lapidarist.config.all && !gem.dependency?
31
+ :sub_dependency
32
+ elsif Lapidarist.config.groups.any? && (Lapidarist.config.groups & gem.groups).none?
33
+ :unmatched_group
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ module Lapidarist
2
+ class Sha
3
+ def initialize
4
+ @good_shas = []
5
+ @git = GitCommand.new
6
+ @logger = Logger.new
7
+ end
8
+
9
+ def record_good
10
+ good_sha = git.head
11
+ logger.debug("good sha: #{good_sha}")
12
+ @good_shas << good_sha
13
+ end
14
+
15
+ def last_good
16
+ good_shas.last
17
+ end
18
+
19
+ def new_commit_count
20
+ git.count_commits(last_good, 'HEAD')
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :good_shas, :git, :logger
26
+ end
27
+ end
@@ -0,0 +1,57 @@
1
+ module Lapidarist
2
+ class Shell
3
+ def initialize
4
+ @logger = Logger.new
5
+ end
6
+
7
+ def run(*commands, label: nil, &block)
8
+ if commands.one?
9
+ run_single_command(commands.first, label, &block)
10
+ else
11
+ pipe_multiple_commands(*commands, &block)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :logger
18
+
19
+ def run_single_command(command, label)
20
+ logger.info "COMMAND > `#{command}`", 1
21
+
22
+ if block_given?
23
+ Open3.popen2e(command, chdir: Lapidarist.config.directory) do |_std_in, std_out_err|
24
+ yield(std_out_err)
25
+ end
26
+ else
27
+ out_err = []
28
+
29
+ status = Open3.popen2e(command, chdir: Lapidarist.config.directory) do |_std_in, std_out_err, wait_thr|
30
+ while line = std_out_err.gets
31
+ logger.std_out_err(line, label || command)
32
+ out_err << line
33
+ end
34
+ wait_thr.value
35
+ end
36
+
37
+ logger.info "STATUS > #{status}", 2
38
+
39
+ [out_err.join("\n"), status]
40
+ end
41
+ end
42
+
43
+ def pipe_multiple_commands(*commands)
44
+ if block_given?
45
+ Open3.pipeline_r(*commands, chdir: Lapidarist.config.directory) do |std_out, _ts|
46
+ yield(std_out)
47
+ end
48
+ else
49
+ output = ''
50
+ Open3.pipeline_r(*commands, chdir: Lapidarist.config.directory) do |std_out, _ts|
51
+ output = std_out.read
52
+ end
53
+ output
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ module Lapidarist
2
+ class Status
3
+ def initialize(gems, attempt)
4
+ @gems = gems
5
+ @attempt = attempt
6
+ end
7
+
8
+ def to_i
9
+ gems.updated.any? || attempt == 1
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :gems, :attempt
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ module Lapidarist
2
+ class Summary
3
+ def initialize(gems, logger)
4
+ @gems = gems
5
+ @logger = logger
6
+ end
7
+
8
+ def display
9
+ logger.summary ''
10
+ logger.summary 'Summary'
11
+ logger.summary '-'*50
12
+ logger.summary "#{object_count(gems.updated, 'gem', 'gems')} updated, #{object_count(gems.failed, 'gem', 'gems')} failed and #{object_count(gems.skipped, 'gem', 'gems')} skipped in #{object_count(gems.attempts, 'attempt', 'attempts')}"
13
+ gems.each do |gem|
14
+ gem.attempts.each do |i, data|
15
+ case data[:status]
16
+ when :updated
17
+ logger.summary " + updated #{gem.name} from #{gem.installed_version} to #{data[:version]}"
18
+ when :failed
19
+ logger.summary " x failed #{gem.name} from #{gem.installed_version} to #{data[:version]}"
20
+ when :skipped
21
+ logger.summary " - skipped #{gem.name} (#{data[:reason]})"
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :gems, :logger
30
+
31
+ def object_count(objects_or_length, singlular, plural)
32
+ length =
33
+ if objects_or_length.respond_to?(:length)
34
+ objects_or_length.length
35
+ else
36
+ objects_or_length
37
+ end
38
+
39
+ if length == 1
40
+ "1 #{singlular}"
41
+ else
42
+ "#{length} #{plural}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ module Lapidarist
2
+ class TestCommand
3
+ def initialize
4
+ @shell = Shell.new
5
+ end
6
+
7
+ def run
8
+ shell.run(to_s)[1]
9
+ end
10
+
11
+ def success?
12
+ run == 0
13
+ end
14
+
15
+ def to_s
16
+ Lapidarist.config.test_script
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :shell
22
+ end
23
+ end
@@ -0,0 +1,60 @@
1
+ module Lapidarist
2
+ class Update
3
+ def initialize
4
+ @bundle = BundleCommand.new
5
+ @git = GitCommand.new
6
+ @logger = Logger.new
7
+ end
8
+
9
+ def run(gems, attempt)
10
+ before_sha = git.head if Lapidarist.config.debug
11
+
12
+ logger.header('Updating outdated gems')
13
+
14
+ limit =
15
+ if Lapidarist.config.update_limit
16
+ [Lapidarist.config.update_limit - gems.updated.length, 0].max
17
+ else
18
+ gems.outdated.length
19
+ end
20
+
21
+ updated_gems = gems.outdated.take(limit).map do |outdated_gem|
22
+ update_gem(outdated_gem, attempt)
23
+ end
24
+
25
+ git.log(before_sha) if Lapidarist.config.debug
26
+
27
+ updated_gems
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :outdated_gems, :bundle, :git, :logger
33
+
34
+ def update_gem(outdated_gem, attempt)
35
+ logger.smart_header "Updating #{outdated_gem.name} from #{outdated_gem.installed_version}"
36
+
37
+ available_semver_levels = [Lapidarist.config.version]
38
+ available_semver_levels << outdated_gem.next_semver_level if Lapidarist.config.recursive
39
+ semver_level_restriction = available_semver_levels.compact.min
40
+
41
+ bundle.update(outdated_gem, level: semver_level_restriction)
42
+ updated_version = bundle.version(outdated_gem)
43
+
44
+ if git.clean?
45
+ skipped_gem = Gem.from(outdated_gem, attempt: attempt, status: :skipped, reason: :nothing_to_update)
46
+ logger.footer "nothing to update for #{skipped_gem.name}"
47
+
48
+ skipped_gem
49
+ else
50
+ updated_gem = Gem.from(outdated_gem, attempt: attempt, status: :updated, updated_version: updated_version, level: semver_level_restriction)
51
+ logger.footer "updated #{updated_gem.name} to #{updated_gem.updated_version}"
52
+
53
+ git.add('Gemfile', 'Gemfile.lock')
54
+ git.commit("Update #{updated_gem.what_changed}")
55
+
56
+ updated_gem
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module Lapidarist
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lapidarist
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Mark Gangl
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Sit back, relax, and allow Lapidarist to do the heavy lifiting and update
56
+ your ruby gem dependencies for you.
57
+ email:
58
+ - mark@attackcorp.com
59
+ executables:
60
+ - lapidarist
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - ".rspec"
66
+ - ".travis.yml"
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - bin/console
72
+ - bin/setup
73
+ - exe/lapidarist
74
+ - lapidarist.gemspec
75
+ - lib/lapidarist.rb
76
+ - lib/lapidarist/bundle_command.rb
77
+ - lib/lapidarist/cli.rb
78
+ - lib/lapidarist/configuration.rb
79
+ - lib/lapidarist/gem.rb
80
+ - lib/lapidarist/gems.rb
81
+ - lib/lapidarist/git_command.rb
82
+ - lib/lapidarist/level.rb
83
+ - lib/lapidarist/logger.rb
84
+ - lib/lapidarist/options.rb
85
+ - lib/lapidarist/outdated.rb
86
+ - lib/lapidarist/sha.rb
87
+ - lib/lapidarist/shell.rb
88
+ - lib/lapidarist/status.rb
89
+ - lib/lapidarist/summary.rb
90
+ - lib/lapidarist/test_command.rb
91
+ - lib/lapidarist/update.rb
92
+ - lib/lapidarist/version.rb
93
+ homepage: https://github.com/attack/lapidarist
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.7.6
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Automatically update ruby gem dependencies.
117
+ test_files: []