slugforge 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+