pessimizer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ before_install: gem install bundler
3
+ rvm:
4
+ - 2.2.3
5
+ - 2.2.0
6
+ - 2.1.5
7
+ - 2.0.0
8
+ - 1.9.3
9
+ - 1.9.2
10
+ - jruby-1.7.10
11
+ - jruby-1.7.19
data/CHANGELOG ADDED
@@ -0,0 +1,43 @@
1
+ === Pessimize 0.4.0
2
+ * Specify version of codeclimate-test-reporter to avert a breaking change (introduced in 1.0.0)
3
+ * Fix documentation link for pessimistic operator (issue #19)
4
+ * Support comments in the Gemfile tokenization (issue #15)
5
+
6
+ === Pessimize 0.3.0
7
+
8
+ Features:
9
+ * Increased compatibility with Gemfile syntax, which should support
10
+ practically all Gemfiles.
11
+ * Some minor refactoring for clarity (ongoing, as always)
12
+
13
+ Bug fixes:
14
+ * Fix issue with certain valid ruby expressions not being parsed correctly,
15
+ e.g. new lines in "gem" definitions
16
+
17
+ === Pessimize 0.2.0
18
+
19
+ Features:
20
+ * Use token parsing instead of interpreting the Gemfile, which allows the Gemfile formatting to be retained (issue #5)
21
+
22
+ === Pessimize 0.1.0
23
+
24
+ Features:
25
+ * Introduce proper command line interface, with help and various options
26
+ * Change default behaviour to constrain versions on the minor number, instead of patch (issue #8)
27
+ * Add command line option to change version level constraint (issue #8)
28
+ * Use Bundler's Gemfile.lock parser, instead of a custom parser (issue #7)
29
+ * Add license to gemspec (issue #6)
30
+
31
+ === Pessimize 0.0.3
32
+
33
+ Bug fixes:
34
+ * Fix bug with multiple arguments to `group` not being recognised (issue #1)
35
+ * Fix missing newlines between statements in Gemfile (issue #2)
36
+
37
+ Features:
38
+ * Better error messages when evaluating a Gemfile with errors (thanks @deepak, #3)
39
+
40
+ === Pessimize 0.0.2
41
+
42
+ Features:
43
+ * Was born (we don't talk about v0.0.1)
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jon Cairns
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Pessimize
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/pessimize.png)](http://badge.fury.io/rb/pessimize)
4
+ [![Build Status](https://travis-ci.org/joonty/pessimize.png?branch=master)](https://travis-ci.org/joonty/pessimize)
5
+ [![Code Climate](https://codeclimate.com/github/joonty/pessimize/badges/gpa.svg)](https://codeclimate.com/github/joonty/pessimize)
6
+ [![Test Coverage](https://codeclimate.com/github/joonty/pessimize/badges/coverage.svg)](https://codeclimate.com/github/joonty/pessimize/coverage)
7
+
8
+ ### Who is this for?
9
+ Anyone who works with a Gemfile, i.e. a project that uses [bundler][1].
10
+
11
+ ### What does it do?
12
+ Pessimize adds version numbers with the pessimistic constraint operator (`~>`, a.k.a. "spermy" operator) to all gems in your `Gemfile`.
13
+
14
+ ### Why?
15
+ You should be using `~> x.x` to limit the version numbers of your gems, otherwise `bundle update` could potentially break your application. Read the section on "why bundle update can be dangerous" for a more detailed description, or take a look at the [rubygems explanation][2].
16
+
17
+ ### But why a gem?
18
+
19
+ *I.e. why not just do it by hand?*
20
+
21
+ When you start building an application that uses bundler, you aren't yet sure which versions of gems will work together, so you probably won't specify the precise versions. You would normally only do this when the application and it's dependencies are in a fairly stable state. However, if you have more than a few gems, it's very tedious to collect the version numbers for each gem and add the pessimistic constraint operator to each line in the `Gemfile`.
22
+
23
+ Pessimize works out the version of each gem from `Gemfile.lock` then generates a new `Gemfile` with the added version constraints. But don't worry: it backs up the existing Gemfile before running, so you can get the original back if it goes wrong. (If it does go wrong, please submit an issue.)
24
+
25
+ ## Installation
26
+
27
+ You don't need to add it to your Gemfile - it's best kept as a system-wide gem. All you need to do is install it from the command line:
28
+
29
+ $ gem install pessimize
30
+
31
+ This installs the command line tool `pessimize`.
32
+
33
+ ## Usage
34
+
35
+ Change to a directory that contains a `Gemfile` and execute:
36
+
37
+ $ pessimize
38
+
39
+ This backs up the existing `Gemfile` and creates a new one with everything neatly organised and versioned.
40
+
41
+ And that's it!
42
+
43
+ ### Options
44
+
45
+ The executable has various options, which you can read with the `--help` argument:
46
+
47
+ ```bash
48
+ Usage: pessimize [options]
49
+
50
+ Add the pessimistic constraint operator to all gems in your Gemfile, restricting the maximum update
51
+ version.
52
+
53
+ Run this in a directory containing a Gemfile to apply the version constraint operator to all gems, at
54
+ their current version. By default, it will restrict updates to the minor version number, but this can be
55
+ changed to patch level updates.
56
+
57
+ Options:
58
+ --version-constraint, -c <s>: Version constraint ('minor' or 'patch') (default: minor)
59
+ --backup, --no-backup, -b: Backup existing Gemfile and Gemfile.lock (default: true)
60
+ --version, -v: Print version and exit
61
+ --help, -h: Show this message
62
+ ```
63
+
64
+ By default, the versions will be constrained to the minor version number (e.g. "~> 3.2"). If you supply the option `--version-constraint patch` then it will only allow updates on the patch version number (e.g. "~> 3.2.5").
65
+
66
+ Also, by default, the Gemfile and Gemfile.lock are copied as a form of backup. To skip this backup (for instance, if you're confident that your Gemfile is committed into version control) then add the `--no-backup` option.
67
+
68
+ ## Known issues
69
+
70
+ Recent issues with certain Gemfile syntax edge cases have been fixed, so there are currently no known issues. If you do find pessimize doing something strange, or not correctly picking up information in your Gemfile, it would be greatly appreciated if you submit an issue.
71
+
72
+ ## Why `bundle update` can be dangerous
73
+
74
+ If you add gems to your Gemfile without specifying a version, bundler will attempt to get the latest stable version for that gem. When you first run `bundle install`, bundler will get and install the latest versions, then create a `Gemfile.lock` which specifies the versions used.
75
+
76
+ This is fine until someone runs `bundle update`. In this case, bundler will try to update each gem to the maximum possible version. If no constraints have been applied, that means that **major** versions can potentially be incremented. Gems have interdependencies with other gems, and if those gems haven't specified the version constraints then breakages could occur.
77
+
78
+ The pessimistic constraint operator will only allow the final number of the version string to increase. You can use this to only allow patch or minor level upgrades. This means that when you run `bundle update`, there's a limit on how far gems will update.
79
+
80
+ ## Contributing
81
+
82
+ 1. Fork it
83
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
84
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
85
+ 4. Push to the branch (`git push origin my-new-feature`)
86
+ 5. Create new Pull Request
87
+
88
+ [1]: http://gembundler.com
89
+ [2]: http://guides.rubygems.org/patterns/#declaring-dependencies
90
+ [3]: https://github.com/joonty/pessimize/issues/5
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => :spec
data/bin/pessimize ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pessimize'
3
+ require 'pessimize/shell'
4
+
5
+ Pessimize::Shell.new.run
@@ -0,0 +1,39 @@
1
+ module Pessimize
2
+ class FileManager
3
+ def gemfile
4
+ 'Gemfile'
5
+ end
6
+
7
+ def gemfile_lock
8
+ 'Gemfile.lock'
9
+ end
10
+
11
+ def gemfile?
12
+ File.exist? gemfile
13
+ end
14
+
15
+ def gemfile_contents
16
+ File.read gemfile
17
+ end
18
+
19
+ def gemfile_lock?
20
+ File.exist? gemfile_lock
21
+ end
22
+
23
+ def backup_gemfile!
24
+ backup_file! gemfile
25
+ end
26
+
27
+ def backup_gemfile_lock!
28
+ backup_file! gemfile_lock
29
+ end
30
+
31
+ private
32
+ def backup_file!(file)
33
+ cmd = "cp #{file} #{file}.backup"
34
+ puts " + #{cmd}"
35
+ `#{cmd}`
36
+ $?.exitstatus == 0
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,63 @@
1
+ module Pessimize
2
+ class Gem
3
+ attr_reader :name, :original_tokens
4
+ attr_accessor :version
5
+
6
+ def initialize(gem_tokens)
7
+ self.original_tokens = gem_tokens
8
+ parse_name
9
+ parse_version
10
+ end
11
+
12
+ def tokens
13
+ compiled_tokens = original_tokens.dup
14
+ if version_index
15
+ compiled_tokens[version_index][2] = version
16
+ elsif version
17
+ compiled_tokens[after_name_index..0] = version_tokens
18
+ end
19
+ compiled_tokens
20
+ end
21
+
22
+ def to_s
23
+ tokens.inject("") { |a, t| a << t[2] }
24
+ end
25
+
26
+ protected
27
+ attr_writer :name, :original_tokens
28
+ attr_accessor :version_index,
29
+ :token_before_version,
30
+ :after_name_index
31
+
32
+ def version_tokens
33
+ [
34
+ [[], :on_comma, ","],
35
+ [[], :on_sp, " "],
36
+ [[], :on_tstring_beg, "\""],
37
+ [[], :on_tstring_content, version],
38
+ [[], :on_tstring_end, "\""]
39
+ ]
40
+ end
41
+
42
+ def parse_name
43
+ name_index = original_tokens.index { |t| t[1] == :on_tstring_content }
44
+ self.name = original_tokens[name_index][2]
45
+ self.after_name_index = name_index + 2
46
+ end
47
+
48
+ def parse_version
49
+ token_before_version = original_tokens.index { |t| t[1] == :on_comma }
50
+ if token_before_version
51
+ remaining_tokens = original_tokens[(token_before_version + 1)..-1]
52
+ while [:on_sp, :on_tstring_beg, :on_ignored_nl].include? remaining_tokens.first[1]
53
+ remaining_tokens.shift
54
+ token_before_version += 1
55
+ end
56
+ if remaining_tokens.first[1] == :on_tstring_content
57
+ self.version_index = token_before_version + 1
58
+ self.version = remaining_tokens.first[2]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,73 @@
1
+ require 'ripper'
2
+ require 'pessimize/gem'
3
+
4
+ module Pessimize
5
+ class Gemfile
6
+ attr_reader :gems
7
+
8
+ def initialize(contents)
9
+ self.tokens = Ripper.lex(contents)
10
+ self.gems = []
11
+ self.gem_token_map = []
12
+
13
+ parse_gems_from_tokens!
14
+ end
15
+
16
+ def to_s
17
+ TokenCompiler.new(tokens).compile_to_string(gems, gem_token_map)
18
+ end
19
+
20
+ protected
21
+ attr_writer :gems
22
+ attr_accessor :gem_token_map, :tokens
23
+
24
+ def parse_gems_from_tokens!
25
+ enum = tokens.each_with_index
26
+
27
+ loop do
28
+ (tok, i) = enum.next
29
+
30
+ if tok[1] == :on_ident && tok[2] == "gem"
31
+ gem_toks = []
32
+ until all_gem_tokens_collected?(tok[0], enum.peek[0])
33
+ (tok, j) = enum.next
34
+ gem_toks << tok
35
+ end
36
+
37
+ self.gems << Pessimize::Gem.new(gem_toks)
38
+ self.gem_token_map << [i + 1, j]
39
+ end
40
+ end
41
+ rescue StopIteration
42
+ end
43
+
44
+ def all_gem_tokens_collected?(current_token, next_token)
45
+ [:on_nl, :on_comment].include?(next_token[1])
46
+ end
47
+
48
+ class TokenCompiler
49
+ def initialize(tokens)
50
+ self.tokens = tokens.dup
51
+ self.offset = 0
52
+ end
53
+
54
+ def compile_to_string(gems, gem_token_map)
55
+ gem_token_map.zip(gems).each do |(token_start, token_end), gem|
56
+ insert_gem_into_tokens! gem, token_start, token_end
57
+ end
58
+
59
+ tokens.reduce("") { |a, e|
60
+ a + e[2]
61
+ }
62
+ end
63
+
64
+ protected
65
+ attr_accessor :tokens, :offset
66
+
67
+ def insert_gem_into_tokens!(gem, token_start, token_end)
68
+ self.tokens[(offset + token_start)..(offset + token_end)] = gem.tokens
69
+ self.offset += gem.tokens.length - (token_end - token_start + 1)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,26 @@
1
+ require 'bundler'
2
+
3
+ module Pessimize
4
+ class GemfileLockVersionParser
5
+ attr_reader :versions
6
+
7
+ def initialize
8
+ self.versions = {}
9
+ end
10
+
11
+ def call(gemfile_lock_file)
12
+ parser = Bundler::LockfileParser.new(gemfile_lock_file.read)
13
+ self.versions = collect_names_and_versions(parser.specs)
14
+ self
15
+ end
16
+
17
+ protected
18
+ attr_writer :versions
19
+
20
+ def collect_names_and_versions(specs)
21
+ Hash[specs.
22
+ reject { |s| s.source.is_a?(Bundler::Source::Git) }.
23
+ collect { |s| [s.name, s.version.to_s] }]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ require 'pessimize/gemfile'
2
+ require 'pessimize/gemfile_lock_version_parser'
3
+ require 'pessimize/version_mapper'
4
+
5
+ module Pessimize
6
+ class Pessimizer
7
+ def initialize(file_manager, options)
8
+ self.file_manager = file_manager
9
+ self.options = options
10
+ self.lock_parser = GemfileLockVersionParser.new
11
+ end
12
+
13
+ def run
14
+ collect_gems_and_versions
15
+ update_gem_versions
16
+ write_new_gemfile
17
+ puts "~> written #{gemfile.gems.length} gems to Gemfile, constrained to #{options[:version_constraint]} version updates\n\n"
18
+ end
19
+
20
+ protected
21
+ attr_accessor :gemfile, :lock_parser, :file_manager, :options
22
+
23
+ def sep(num = 1)
24
+ "\n" * num
25
+ end
26
+
27
+ def collect_gems_and_versions
28
+ self.gemfile = Gemfile.new(file_manager.gemfile_contents)
29
+ lock_parser.call File.open(file_manager.gemfile_lock)
30
+ end
31
+
32
+ def update_gem_versions
33
+ VersionMapper.new.call(gemfile.gems, lock_parser.versions, options[:version_constraint])
34
+ end
35
+
36
+ def write_new_gemfile
37
+ File.delete(file_manager.gemfile)
38
+ File.open(file_manager.gemfile, 'w') do |f|
39
+ f.write gemfile.to_s
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,79 @@
1
+ require 'pessimize/file_manager'
2
+ require 'pessimize/pessimizer'
3
+ require 'optimist'
4
+
5
+ module Pessimize
6
+ class Shell
7
+ def initialize
8
+ self.file_manager = FileManager.new
9
+ end
10
+
11
+ def run
12
+ options = cli_options
13
+ check_options! options
14
+ verify_files!(options[:backup])
15
+ Pessimizer.new(file_manager, options).run
16
+ end
17
+
18
+ protected
19
+ attr_accessor :file_manager
20
+
21
+ def sep(num = 1)
22
+ "\n" * num
23
+ end
24
+
25
+ def cli_options
26
+ Optimist::options do
27
+ version "pessimize #{VERSION} (c) #{Time.now.year} Jon Cairns"
28
+ banner <<-EOS
29
+ Usage: pessimize [options]
30
+
31
+ Add the pessimistic constraint operator to all gems in your Gemfile, restricting the maximum update version.
32
+
33
+ Run this in a directory containing a Gemfile to apply the version constraint operator to all gems, at their current version. By default, it will restrict updates to the minor version number, but this can be changed to patch level updates.
34
+
35
+ Options:
36
+ EOS
37
+
38
+ opt :version_constraint, "Version constraint ('minor' or 'patch')", default: 'minor', type: :string, short: 'c'
39
+ opt :backup, "Backup existing Gemfile and Gemfile.lock", default: true, type: :boolean, short: 'b'
40
+ end
41
+ end
42
+
43
+ def check_options!(options)
44
+ constraints = %w(minor patch)
45
+ unless constraints.include? options[:version_constraint]
46
+ Optimist::die :version_constraint, "must be one of #{constraints.join("|")}"
47
+ end
48
+ end
49
+
50
+ def verify_files!(backup)
51
+ file_manager.gemfile? or exit_with 1, <<-ERR.strip
52
+ error: no Gemfile exists in the current directory, exiting
53
+ ERR
54
+
55
+ file_manager.gemfile_lock? or exit_with 2, <<-ERR.strip
56
+ error: no Gemfile.lock exists in the current directory, exiting
57
+ Please run `bundle install` before running pessimize
58
+ ERR
59
+
60
+ if backup
61
+ puts "Backing up Gemfile and Gemfile.lock"
62
+
63
+ file_manager.backup_gemfile! or exit_with 3, <<-ERR.strip
64
+ error: failed to backup existing Gemfile, exiting
65
+ ERR
66
+
67
+ file_manager.backup_gemfile_lock! or exit_with 4, <<-ERR.strip
68
+ error: failed to backup existing Gemfile.lock, exiting
69
+ ERR
70
+ puts ""
71
+ end
72
+ end
73
+
74
+ def exit_with(status, message)
75
+ $stderr.write message
76
+ exit status
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,3 @@
1
+ module Pessimize
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,13 @@
1
+ module Pessimize
2
+ class VersionMapper
3
+ def call(gems, versions, version_constraint)
4
+ gems.each do |gem|
5
+ if versions.has_key? gem.name
6
+ version_parts = versions[gem.name].split('.')
7
+ version = version_constraint == 'minor' ? version_parts.first(2).join('.') : version_parts.join('.')
8
+ gem.version = "~> #{version}"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/pessimize.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "pessimize/version"
2
+
3
+ module Pessimize; end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pessimize/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "pessimizer"
8
+ gem.version = Pessimize::VERSION
9
+ gem.authors = ["Jon Cairns", "Federico Iachetti"]
10
+ gem.email = ["jon@joncairns.com", "iachetti.federico@gmail.com"]
11
+ gem.description = %q{Add the pessimistic constraint operator to all gems in your Gemfile, restricting the maximum update version.
12
+
13
+ This is for people who work with projects that use bundler, such as rails projects. The pessimistic constraint operator (~>) allows you to specify the maximum version that a gem can be updated, and reduces potential breakages when running `bundle update`. Pessimize automatically retrieves the current versions of your gems, then adds them to your Gemfile (so you don't have to do it by hand).}
14
+ gem.summary = %q{Add the pessimistic constraint operator to all gems in your Gemfile, restricting the maximum update version.}
15
+ gem.homepage = "https://github.com/iachettifederico/pessimizer"
16
+
17
+ gem.add_dependency 'bundler'
18
+ gem.add_dependency 'optimist'
19
+ gem.add_development_dependency 'rspec', '~> 3.12.0'
20
+ gem.add_development_dependency 'rspec-its', '~> 1.3.0'
21
+ gem.add_development_dependency 'rake', '~> 10.0.3'
22
+ gem.add_development_dependency 'codeclimate-test-reporter', '~> 0.6.0'
23
+
24
+ gem.files = `git ls-files`.split($/)
25
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
26
+ gem.test_files = gem.files.grep(%r{^spec/})
27
+ gem.require_paths = ["lib"]
28
+ gem.license = 'MIT'
29
+ end
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pessimize (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.4)
10
+ rake (10.0.4)
11
+ rspec (2.13.0)
12
+ rspec-core (~> 2.13.0)
13
+ rspec-expectations (~> 2.13.0)
14
+ rspec-mocks (~> 2.13.0)
15
+ rspec-core (2.13.1)
16
+ rspec-expectations (2.13.0)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.13.1)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ pessimize!
25
+ rake (~> 10.0.3)
26
+ rspec (~> 2.13.0)