slugforge 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +316 -0
  3. data/bin/slugforge +9 -0
  4. data/lib/slugforge.rb +19 -0
  5. data/lib/slugforge/build.rb +4 -0
  6. data/lib/slugforge/build/build_project.rb +31 -0
  7. data/lib/slugforge/build/export_upstart.rb +85 -0
  8. data/lib/slugforge/build/package.rb +63 -0
  9. data/lib/slugforge/cli.rb +125 -0
  10. data/lib/slugforge/commands.rb +130 -0
  11. data/lib/slugforge/commands/build.rb +20 -0
  12. data/lib/slugforge/commands/config.rb +24 -0
  13. data/lib/slugforge/commands/deploy.rb +383 -0
  14. data/lib/slugforge/commands/project.rb +21 -0
  15. data/lib/slugforge/commands/tag.rb +148 -0
  16. data/lib/slugforge/commands/wrangler.rb +142 -0
  17. data/lib/slugforge/configuration.rb +125 -0
  18. data/lib/slugforge/helper.rb +186 -0
  19. data/lib/slugforge/helper/build.rb +46 -0
  20. data/lib/slugforge/helper/config.rb +37 -0
  21. data/lib/slugforge/helper/enumerable.rb +46 -0
  22. data/lib/slugforge/helper/fog.rb +90 -0
  23. data/lib/slugforge/helper/git.rb +89 -0
  24. data/lib/slugforge/helper/path.rb +76 -0
  25. data/lib/slugforge/helper/project.rb +86 -0
  26. data/lib/slugforge/models/host.rb +233 -0
  27. data/lib/slugforge/models/host/fog_host.rb +33 -0
  28. data/lib/slugforge/models/host/hostname_host.rb +9 -0
  29. data/lib/slugforge/models/host/ip_address_host.rb +9 -0
  30. data/lib/slugforge/models/host_group.rb +65 -0
  31. data/lib/slugforge/models/host_group/aws_tag_group.rb +22 -0
  32. data/lib/slugforge/models/host_group/ec2_instance_group.rb +21 -0
  33. data/lib/slugforge/models/host_group/hostname_group.rb +16 -0
  34. data/lib/slugforge/models/host_group/ip_address_group.rb +16 -0
  35. data/lib/slugforge/models/host_group/security_group_group.rb +20 -0
  36. data/lib/slugforge/models/logger.rb +36 -0
  37. data/lib/slugforge/models/tag_manager.rb +125 -0
  38. data/lib/slugforge/slugins.rb +125 -0
  39. data/lib/slugforge/version.rb +9 -0
  40. data/scripts/post-install.sh +143 -0
  41. data/scripts/unicorn-shepherd.sh +305 -0
  42. data/spec/fixtures/array.yaml +3 -0
  43. data/spec/fixtures/fog_credentials.yaml +4 -0
  44. data/spec/fixtures/invalid_syntax.yaml +1 -0
  45. data/spec/fixtures/one.yaml +3 -0
  46. data/spec/fixtures/two.yaml +3 -0
  47. data/spec/fixtures/valid.yaml +4 -0
  48. data/spec/slugforge/commands/deploy_spec.rb +72 -0
  49. data/spec/slugforge/commands_spec.rb +33 -0
  50. data/spec/slugforge/configuration_spec.rb +200 -0
  51. data/spec/slugforge/helper/fog_spec.rb +81 -0
  52. data/spec/slugforge/helper/git_spec.rb +152 -0
  53. data/spec/slugforge/models/host_group/aws_tag_group_spec.rb +54 -0
  54. data/spec/slugforge/models/host_group/ec2_instance_group_spec.rb +51 -0
  55. data/spec/slugforge/models/host_group/hostname_group_spec.rb +20 -0
  56. data/spec/slugforge/models/host_group/ip_address_group_spec.rb +54 -0
  57. data/spec/slugforge/models/host_group/security_group_group_spec.rb +52 -0
  58. data/spec/slugforge/models/tag_manager_spec.rb +75 -0
  59. data/spec/spec_helper.rb +37 -0
  60. data/spec/support/env.rb +3 -0
  61. data/spec/support/example_groups/configuration_writer.rb +24 -0
  62. data/spec/support/example_groups/helper_provider.rb +10 -0
  63. data/spec/support/factories.rb +18 -0
  64. data/spec/support/fog.rb +15 -0
  65. data/spec/support/helpers.rb +18 -0
  66. data/spec/support/mock_logger.rb +6 -0
  67. data/spec/support/ssh.rb +8 -0
  68. data/spec/support/streams.rb +13 -0
  69. data/templates/foreman/master.conf.erb +21 -0
  70. data/templates/foreman/process-master.conf.erb +2 -0
  71. data/templates/foreman/process.conf.erb +19 -0
  72. metadata +344 -0
@@ -0,0 +1,21 @@
1
+ require 'slugforge/models/tag_manager'
2
+
3
+ module Slugforge
4
+ module Commands
5
+ class Project < Slugforge::SubCommand
6
+ desc 'list', 'list available projects'
7
+ def list
8
+ logger.say "Available projects:"
9
+
10
+ tag_manager.projects.map do |project|
11
+ out = [set_color(project, :green)]
12
+ pc = tag_manager.slug_for_tag(project, 'production-current')
13
+ out << "(production-current: #{set_color(pc, :yellow)})" unless pc.nil?
14
+ out
15
+ end.sort.each do |o|
16
+ logger.say " #{o * ' '}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,148 @@
1
+ module Slugforge
2
+ module Commands
3
+ class Tag < Slugforge::SubCommand
4
+ desc 'clean [ARGS]', 'remove tags that point to missing slugs (excluding production-current)'
5
+ def clean
6
+ verify_project_name!
7
+
8
+ tags = tag_manager.tags(project_name)
9
+ tags.delete('production-current')
10
+
11
+ bucket # initialize value before using threads
12
+ logger.say_status :clean, "Reviewing #{tags.count} tags for #{project_name}", :cyan
13
+ results = tags.parallel_map do |tag|
14
+ begin
15
+ slug_file = tag_manager.slug_for_tag(project_name, tag)
16
+ if !slug_file.nil? && bucket.files.head(slug_file)
17
+ logger.say '.', :green, false
18
+ [tag, :valid]
19
+ else
20
+ if pretend?
21
+ [tag, :pretend]
22
+ else
23
+ tag_manager.delete_tag(project_name, tag)
24
+ logger.say '.', :red, false
25
+ [tag, :deleted]
26
+ end
27
+ end
28
+ rescue Excon::Errors::Forbidden
29
+ logger.say '.', :yellow, false
30
+ [tag, :forbidden]
31
+ end
32
+ end.sort {|a,b| b[1].to_s <=> a[1].to_s}
33
+
34
+ logger.say
35
+ results.each do |result|
36
+ tag, status = result
37
+ next if status == :valid
38
+ logger.say_status status, tag, (status == :deleted) ? :red : :yellow
39
+ end
40
+ end
41
+
42
+ desc 'clone <tag> <new_tag> [ARGS]', 'create a new tag with the same slug as an existing tag'
43
+ def clone(tag, new_tag)
44
+ verify_project_name!
45
+
46
+ slug_name = tag_manager.slug_for_tag(project_name, tag)
47
+ unless slug_name.nil?
48
+ tag_manager.clone_tag(project_name, tag, new_tag)
49
+ logger.say_json :project => project_name, :tag => new_tag, :slug => slug_name
50
+ logger.say_status :set, "#{project_name} #{new_tag} to Slug #{slug_name}"
51
+ true
52
+ else
53
+ logger.say_json :tag => tag, :exists => false
54
+ logger.say_status :clone, "could not find existing tag #{tag} for project '#{project_name}'", :red
55
+ false
56
+ end
57
+ end
58
+
59
+ desc 'history <tag> [ARGS]', 'show history of a project\'s tag'
60
+ def history(tag)
61
+ verify_project_name!
62
+
63
+ slug_names = tag_manager.slugs_for_tag(project_name, tag)
64
+ unless slug_names.empty?
65
+ logger.say_json :project => project_name, :tag => tag, :slug_names => slug_names, :exists => true
66
+ slug_names.each.with_index(0) do |slug_name, index|
67
+ logger.say_status (index == 0 ? 'current' : "-#{index}"), slug_name, :yellow
68
+ end
69
+ else
70
+ logger.say_json :tag => tag, :exists => false
71
+ end
72
+ end
73
+
74
+ desc 'list [ARGS]', 'list a project\'s tags'
75
+ def list
76
+ verify_project_name!
77
+
78
+ tags = tag_manager.tags(project_name)
79
+ pc = tags.delete('production-current')
80
+
81
+ if json?
82
+ logger.say_json tags
83
+ else
84
+ logger.say "Tags for #{project_name}"
85
+ logger.say_status :'production-current', tag_manager.slug_for_tag(project_name, 'production-current') unless pc.nil?
86
+
87
+ tag_list = tags.parallel_map do |tag|
88
+ [tag, tag_manager.slug_for_tag(project_name, tag)]
89
+ end
90
+ tag_list.sort {|a,b| b[1].to_s<=>a[1].to_s }.each do |tag, slug|
91
+ logger.say_status tag, slug, :yellow
92
+ end
93
+ end
94
+ end
95
+
96
+ desc 'migrate', 'migrate tags to new format', :hide => true
97
+ def migrate
98
+ metadata = JSON.parse(bucket.files.get('projects.json').body)
99
+ metadata.each do |project, data|
100
+ data['tags'].each do |tag, value|
101
+ puts "create_tag(#{project}, #{tag}, #{value['s3']})"
102
+ tag_manager.create_tag(project, tag, value['s3'])
103
+ end
104
+ end
105
+ end
106
+
107
+ desc 'show <tag> [ARGS]', 'show value of a project\'s tag'
108
+ def show(tag)
109
+ verify_project_name!
110
+
111
+ slug_name = tag_manager.slug_for_tag(project_name, tag)
112
+ unless slug_name.nil?
113
+ exists = !bucket.files.head(slug_name).nil?
114
+ logger.say_json :project => project_name, :tag => tag, :slug_name => slug_name, :exists => exists
115
+ logger.say_status tag, "#{slug_name} (#{exists ? "exists" : "missing"})", :yellow
116
+ else
117
+ logger.say_json :tag => tag, :exists => false
118
+ end
119
+ end
120
+
121
+ desc 'set <tag> <name_part>', 'update a tag to point to a slug in s3'
122
+ def set(tag, name_part)
123
+ verify_project_name!
124
+
125
+ slug = find_slug(name_part)
126
+
127
+ tag_manager.create_tag(project_name, tag, slug.key)
128
+ logger.say_json :project => project_name, :tag => tag, :slug => slug.key
129
+ logger.say_status :set, "#{project_name} #{tag} to Slug #{slug.key}"
130
+ end
131
+
132
+ desc 'delete <tag> [ARGS]', 'delete a tag'
133
+ option :yes, :type => :boolean, :aliases => '-y', :default => false,
134
+ :desc => 'answer "yes" to all questions'
135
+ def delete(tag)
136
+ verify_project_name!
137
+
138
+ if options[:yes] || (ask("Are you sure you wish to delete tag '#{tag}'? [Yn]").downcase != 'n')
139
+ tag_manager.delete_tag(project_name, tag)
140
+ logger.say_status :delete, "#{project_name} #{tag}"
141
+ else
142
+ logger.say_status :keep, "#{project_name} #{tag}"
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+
@@ -0,0 +1,142 @@
1
+ module Slugforge
2
+ module Commands
3
+ class Wrangler < Slugforge::SubCommand
4
+
5
+ desc 'push <file>', 'push a slug to S3'
6
+ option :tag, :type => :string, :aliases => '-t',
7
+ :desc => 'once pushed tag the slug with this tag.'
8
+ def push(file)
9
+ verify_project_name!
10
+ unless File.exist?(file)
11
+ raise error_class, "file does not exist"
12
+ end
13
+
14
+ dest = "#{project_name}/#{file}"
15
+ logger.say_status :upload, "slug #{file} to #{project_name}", :yellow
16
+ s3.put_object(aws_bucket, dest, File.read(file))
17
+ logger.say_status :uploaded, "slug saved"
18
+
19
+ if options[:tag]
20
+ logger.say_status :build, "applying tag '#{options[:tag]}' to your fancy new build", :green
21
+ invoke Slugforge::Commands::Tag, [:set, options[:tag], dest], []
22
+ end
23
+ end
24
+
25
+ desc 'pull [name_part]', 'pull a slug from S3 (most recent if no name part is specified)'
26
+ def pull(name_part)
27
+ verify_project_name!
28
+
29
+ slug = name_part ? find_slug(name_part) : find_latest_slug
30
+
31
+ logger.say_status :fetch, "#{slug.attributes[:name]} (#{slug.attributes[:pretty_length]})", :yellow
32
+ logger.say "Note: process will block until download completes."
33
+
34
+ # This should block until the body is downloaded.
35
+ # We open the file for writing afterwards to prevent creating empty files.
36
+ slug.body
37
+ File.open(slug.attributes[:name], 'w+') { |f| f.write(slug.body) }
38
+
39
+ logger.say_status :fetched, "pull complete, saved to #{slug.attributes[:name]}"
40
+ end
41
+
42
+ desc 'list [ARGS]', 'list published slugs for a project'
43
+ option :count, :type => :numeric, :aliases => '-c', :default => 10,
44
+ :desc => 'how many slugs to list'
45
+ option :sort, :type => :string, :aliases => '-s', :default => 'last_modified:desc',
46
+ :desc => 'change the sorting option (field:dir)'
47
+ option :all, :type => :boolean, :aliases => '-a', :default => false,
48
+ :desc => 'list all slugs'
49
+ def list
50
+ raise error_class, "count must be greater than 0" if !options[:all] && options[:count] <= 0
51
+
52
+ begin
53
+ project_name = self.project_name
54
+ rescue Thor::Error
55
+ # This is the only case where we don't care if project is not found.
56
+ end
57
+
58
+ slugs = (project_name.nil? ? self.files : self.slugs(project_name)).values
59
+ raise error_class, "No slugs found for #{project_name}" if slugs.first.nil?
60
+
61
+ total = slugs.length
62
+
63
+ sorting = options[:sort].split(':')
64
+ field = sorting.first.to_sym
65
+ direction = (sorting.last || 'desc')
66
+
67
+ unless slugs.first.class.attributes.include?(field)
68
+ raise error_class, "unknown attribute for sorting: #{field}. Available fields: #{slugs.first.class.attributes * ', '}"
69
+ end
70
+
71
+ slugs = slugs.sort_by { |f| f.attributes[field] }
72
+ slugs = slugs.reverse! if direction != 'asc'
73
+ slugs = slugs.slice(0...options[:count]) unless options[:all]
74
+
75
+ logger.say "Slugs for #{project_name} (#{slugs.size}", nil, false
76
+ logger.say " of #{total}", nil, false unless options[:all]
77
+ logger.say ")"
78
+
79
+ tag_manager.memoize_slugs_for_tags(project_name)
80
+ slugs.each do |slug|
81
+ tags = tag_manager.tags_for_slug(project_name, slug.key)
82
+ logger.say " #{tags.size > 0 ? ' (' + tags.join(', ') + ')' : ''} ", :yellow
83
+ logger.say "#{slug.attributes[:name]} ", :green
84
+ logger.say "- "
85
+ logger.say "#{slug.attributes[:pretty_age]} ago ", :cyan
86
+ logger.say "(#{set_color(slug.attributes[:pretty_length], :magenta)})"
87
+ end
88
+ end
89
+
90
+ desc 'delete <name_part>', 'delete a slug'
91
+ option :yes, :type => :boolean, :aliases => '-y', :default => false,
92
+ :desc => 'answer "yes" to all questions'
93
+ def delete(name_part)
94
+ slug = find_slug(name_part)
95
+ if options[:yes] || (ask("Are you sure you wish to delete '#{slug.attributes[:name]}'? [yN]").downcase == 'y')
96
+ slug.destroy
97
+ logger.say_status :destroy, slug.key, :red
98
+ else
99
+ logger.say_status :keep, slug.key, :green
100
+ end
101
+ end
102
+
103
+ desc 'purge', 'purge slugs for a project'
104
+ option :yes, :type => :boolean, :aliases => '-y', :default => false,
105
+ :desc => 'answer "yes" to all questions'
106
+ option :keep, :type => :numeric, :aliases => '-k', :default => 10,
107
+ :desc => 'number of slugs to keep'
108
+ option :all, :type => :boolean, :aliases => '-a', :default => false,
109
+ :desc => 'purge all slugs'
110
+ def purge
111
+ raise error_class, "keep must be greater than 0" if !options[:all] && options[:keep] <= 0
112
+
113
+ slugs = self.slugs(project_name).values
114
+
115
+ if options[:all]
116
+ if !options[:yes] && ask("Are you sure you wish to delete #{slugs.size} slugs for #{project_name}? [yN]").downcase == 'y'
117
+ options[:yes] = true
118
+ end
119
+
120
+ if options[:yes]
121
+ slugs.each do |slug|
122
+ slug.destroy
123
+ logger.say_status :destroy, slug.key, :red
124
+ end
125
+ end
126
+ elsif slugs.size > options[:keep]
127
+ slugs.slice(options[:keep]..-1).each do |slug|
128
+ if options[:yes] || (ask("Are you sure you wish to delete #{slug.attributes[:name]}? [Yn]").downcase != 'n')
129
+ slug.destroy
130
+ logger.say_status :destroy, slug.key, :red
131
+ else
132
+ logger.say_status :keep, slug.key, :green
133
+ end
134
+ end
135
+ else
136
+ logger.say "Aborting, only #{slugs.size} slugs for #{project_name} and we want to keep #{options[:keep]}"
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+
@@ -0,0 +1,125 @@
1
+ require 'singleton'
2
+ require 'forwardable'
3
+ require 'slugforge/slugins'
4
+
5
+ module Slugforge
6
+ # Handles loading configuration data from files and the environment. Order of precedence:
7
+ #
8
+ # 1) ENV
9
+ # 2) `pwd`/.slugforge
10
+ # 3) $HOME/.slugforge
11
+ # 4) /etc/slugforge
12
+ #
13
+ # We load in reverse order, allowing us to simply overwrite values whenever found.
14
+ class Configuration
15
+ extend Forwardable
16
+
17
+ class << self
18
+ attr_accessor :configuration_files
19
+ end
20
+ self.configuration_files = [ '/etc/slugforge', File.join(ENV['HOME'], '.slugforge'), File.join(Dir.pwd, '.slugforge') ]
21
+
22
+ class <<self
23
+ def options
24
+ @options ||= {}
25
+ end
26
+
27
+ def option(name, config)
28
+ raise "configuration option #{name} has already been defined" if options.key?(name)
29
+
30
+ options[name] = config
31
+ define_method(name) { values[name] }
32
+ end
33
+ end
34
+
35
+ option :aws_access_key, :key => 'aws.access_key', :option => :'aws-access-key-id', :env => 'AWS_ACCESS_KEY_ID'
36
+ option :aws_secret_key, :key => 'aws.secret_key', :option => :'aws-secret-key', :env => 'AWS_SECRET_ACCESS_KEY'
37
+ option :aws_region, :key => 'aws.region', :option => :'aws-region', :env => 'AWS_DEFAULT_REGION', :default => 'us-east-1'
38
+ option :slug_bucket, :key => 'aws.slug_bucket', :option => :'slug-bucket', :env => 'SLUG_BUCKET'
39
+ option :aws_session_token, :option => :'aws-session-token'
40
+
41
+ option :project, :key => 'slugforge.project', :option => :project, :env => 'SLUGFORGE_PROJECT'
42
+
43
+ option :ssh_username, :key => 'ssh.username', :option => :'ssh-username', :env => 'SSH_USERNAME'
44
+
45
+ option :disable_slugins, :key => 'disable_slugins', :option => :'disable-slugins', :env => 'DISABLE_SLUGINS'
46
+
47
+ attr_reader :values
48
+
49
+ def initialize(options = {})
50
+ @slugin_manager = SluginManager.new
51
+ self.load
52
+ update_from_options options
53
+ end
54
+
55
+ def_delegators :@slugin_manager, :load_slugins, :locate_slugins, :slugins
56
+
57
+ # Get a hash of all options with default values. The list of values is initialized with the result.
58
+ def defaults
59
+ @values = Hash[self.class.options.select { |_, c| c.key?(:default) }.map { |n,c| [n, c[:default]] }].merge(@values)
60
+ end
61
+
62
+ def activate_slugins
63
+ @slugin_manager.activate_slugins(self) unless disable_slugins
64
+ end
65
+
66
+ protected
67
+ def load
68
+ # Read configuration files to load list of slugins. Load the slugin classes so that
69
+ # their configuration options are added and reload the configs to populate the new
70
+ # options.
71
+ @values = {}
72
+ load_configuration_files
73
+ defaults
74
+
75
+ locate_slugins
76
+ #TODO: disable individual slugins via configuration
77
+ load_slugins unless disable_slugins
78
+
79
+ load_configuration_files
80
+ read_env
81
+ end
82
+
83
+ def load_configuration_files
84
+ self.class.configuration_files.each { |f| read_yaml f }
85
+ end
86
+
87
+ # Attempt to read option keys from a YAML file
88
+ def read_yaml(path)
89
+ return unless File.exist?(path)
90
+ source = YAML.load_file(path)
91
+ return unless source.is_a?(Hash)
92
+
93
+ update_with { |config| read_yaml_key(source, config[:key]) }
94
+ end
95
+
96
+ # Split a dot-separated key and locate the value from a hash loaded by YAML.
97
+ # eg. `aws.bucket` looks for `source['aws']['bucket']`.
98
+ def read_yaml_key(source, key)
99
+ return unless key.is_a?(String)
100
+ paths = key.split('.')
101
+ source = source[paths.shift] until paths.empty? || source.nil?
102
+ source
103
+ end
104
+
105
+ # Attempt to read option keys from the environment
106
+ def read_env
107
+ update_with { |config| config[:env] && ENV[config[:env]] }
108
+ end
109
+
110
+ # Update values with a hash of options.
111
+ def update_from_options(options={})
112
+ update_with { |config| config[:option] && options[config[:option]] }
113
+ end
114
+
115
+ # For every option we yield the configuration and expect a value back. If the block returns a value we set the
116
+ # option to it.
117
+ def update_with(&blk)
118
+ self.class.options.each do |name, config|
119
+ value = yield(config)
120
+ @values[name] = value unless value.nil?
121
+ end
122
+ end
123
+ end
124
+ end
125
+
@@ -0,0 +1,186 @@
1
+ require 'slugforge/helper/build'
2
+ require 'slugforge/helper/config'
3
+ require 'slugforge/helper/enumerable'
4
+ require 'slugforge/helper/fog'
5
+ require 'slugforge/helper/git'
6
+ require 'slugforge/helper/path'
7
+ require 'slugforge/helper/project'
8
+ require 'slugforge/models/logger'
9
+
10
+ module Slugforge
11
+ module Helper
12
+ def self.included(base)
13
+ base.send(:include, Slugforge::Helper::Build)
14
+ base.send(:include, Slugforge::Helper::Config)
15
+ base.send(:include, Slugforge::Helper::Fog)
16
+ base.send(:include, Slugforge::Helper::Git)
17
+ base.send(:include, Slugforge::Helper::Path)
18
+ base.send(:include, Slugforge::Helper::Project)
19
+ end
20
+
21
+ protected
22
+ def force?
23
+ options[:force] == true
24
+ end
25
+
26
+ def json?
27
+ options[:json] == true
28
+ end
29
+
30
+ def pretend?
31
+ test? || options[:pretend] == true
32
+ end
33
+
34
+ def test?
35
+ options[:test] == true
36
+ end
37
+
38
+ def notifications_enabled?
39
+ test? || !pretend?
40
+ end
41
+
42
+ def quiet?
43
+ options[:quiet] == true
44
+ end
45
+
46
+ def verbose?
47
+ options[:verbose] == true
48
+ end
49
+
50
+ def error_class
51
+ json? ? JsonError : Thor::Error
52
+ end
53
+
54
+ def elapsed_time
55
+ format_age @command_start_time
56
+ end
57
+
58
+ def logger
59
+ @logger ||= begin
60
+ log_level = if quiet?
61
+ :quiet
62
+ elsif verbose?
63
+ :verbose
64
+ elsif json?
65
+ :json
66
+ end
67
+ Slugforge::Logger.new(self.shell, log_level)
68
+ end
69
+ end
70
+
71
+ def execute(cmd)
72
+ unless pretend?
73
+ if ruby_version_specified?
74
+ cmd = "rvm #{options[:ruby]} do #{cmd}"
75
+ elsif has_ruby_version_file?
76
+ cmd = "rvm #{get_ruby_version_from_file} do #{cmd}"
77
+ end
78
+
79
+ # in thor, if capture is set, it uses backticks to run the command which returns a string.
80
+ # Otherwise they use `system` which returns true or nil if it worked. So check the return value
81
+ # and if it used backticks examine $? which keeps the result of the last command run to see
82
+ # if it worked.
83
+ returned = run(cmd, {:verbose => verbose?, :capture => verbose?})
84
+ if returned.is_a?(String)
85
+ process_status = $?
86
+ logger.say_status :run, "Command result #{process_status.to_s}. Command output: #{returned}", :green
87
+ return process_status.success?
88
+ end
89
+
90
+ return returned
91
+ end
92
+ true
93
+ end
94
+
95
+ def with_env(env={}, &blk)
96
+ original = ENV.to_hash
97
+ ENV.replace(original.merge(env))
98
+
99
+ # Ensure rbenv isn't locked into a version
100
+ if ENV['RBENV_VERSION']
101
+ ENV.delete('RBENV_VERSION')
102
+
103
+ # when you use a shim provided by rbenv the $PATH is modified to point to the proper ruby version so shims are
104
+ # bypassed. We need to remove those path entries to totally unset rbenv. We remove every .rbenv path _except_
105
+ # shims so it can still use the correct version defined by .ruby-version.
106
+ paths = ENV['PATH'].split(':').reject do |path|
107
+ path =~ /\.rbenv\/(\w+)/ && !%w(shims bin).include?($1)
108
+ end
109
+ ENV['PATH'] = paths * ':'
110
+ end
111
+
112
+ # Ensure RVM isn't locked into a version
113
+ ENV.delete('RUBY_VERSION')
114
+ yield
115
+ ensure
116
+ ENV.replace(original)
117
+ end
118
+
119
+ def with_gemfile(gemfile, &blk)
120
+ with_env('BUNDLE_GEMFILE' => gemfile) do
121
+ ENV.delete('GEM_HOME')
122
+ ENV.delete('GEM_PATH')
123
+ ENV.delete('RUBYOPT')
124
+ yield
125
+ end
126
+ end
127
+
128
+ def delete_option(options, option)
129
+ result = options.dup
130
+ index = result.index(option)
131
+ if index
132
+ result.delete_at(index)
133
+ result.delete_at(index)
134
+ end
135
+ result
136
+ end
137
+
138
+ def delete_switch(options, switch)
139
+ result = options.dup
140
+ index = result.index(option)
141
+ result.delete_at(index) if index
142
+ result
143
+ end
144
+
145
+ def format_size(size)
146
+ units = %w(B KB MB GB TB)
147
+ size, unit = units.reduce(size.to_f) do |(fsize, _), utype|
148
+ fsize > 512 ? [fsize / 1024, utype] : (break [fsize, utype])
149
+ end
150
+
151
+ "#{size > 9 || size.modulo(1) < 0.1 ? '%d' : '%.1f'} %s" % [size, unit]
152
+ end
153
+
154
+ SEGMENTS = {
155
+ :year => 60 * 60 * 24 * 365,
156
+ :month => 60 * 60 * 24 * 7 * 4,
157
+ :week => 60 * 60 * 24 * 7,
158
+ :day => 60 * 60 * 24,
159
+ :hour => 60 * 60
160
+ }
161
+
162
+ def format_age(age)
163
+ age = Time.now - age
164
+ segments = {}
165
+
166
+ SEGMENTS.each do |segment, length|
167
+ next unless age >= length
168
+
169
+ segments[segment] = (age / length).floor
170
+ age = age % length
171
+ end
172
+
173
+ # We only show Minutes or Seconds if there is no other scope
174
+ if segments.empty?
175
+ segments[:minute] = (age / 60).floor if age >= 60
176
+ segments[:second] = (age % 60).floor
177
+ end
178
+
179
+ segments.map do |seg, size|
180
+ plural = 's' if size != 1
181
+ "#{size} #{seg}#{plural}"
182
+ end.join(', ')
183
+ end
184
+ end
185
+ end
186
+