geordi 0.18.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +13 -5
  2. data/.ruby-version +1 -0
  3. data/README.md +199 -110
  4. data/Rakefile +40 -0
  5. data/bin/apache-site +1 -19
  6. data/bin/cap-all +1 -27
  7. data/bin/cleanup-directory +1 -11
  8. data/bin/console-for +1 -12
  9. data/bin/cuc +1 -2
  10. data/bin/cuc-show +1 -2
  11. data/bin/cuc-vnc-setup +1 -114
  12. data/bin/deploy-to-production +1 -91
  13. data/bin/dump-for +4 -32
  14. data/bin/dumple +2 -4
  15. data/bin/geordi +10 -0
  16. data/bin/gitpt +1 -2
  17. data/bin/launchy_browser +9 -3
  18. data/bin/load-dump +1 -4
  19. data/bin/migrate-all +1 -13
  20. data/bin/optimize-png +1 -118
  21. data/bin/power-rake +1 -7
  22. data/bin/remove-executable-flags +1 -6
  23. data/bin/rs +1 -26
  24. data/bin/run_tests +1 -2
  25. data/bin/setup-firefox-for-selenium +1 -3
  26. data/bin/shell-for +1 -8
  27. data/bin/tests +1 -5
  28. data/geordi.gemspec +19 -0
  29. data/lib/geordi/COMMAND_TEMPLATE +29 -0
  30. data/lib/geordi/capistrano.rb +9 -7
  31. data/lib/geordi/capistrano_config.rb +66 -0
  32. data/lib/geordi/cli.rb +28 -0
  33. data/lib/geordi/commands/all_targets.rb +26 -0
  34. data/lib/geordi/commands/apache_site.rb +22 -0
  35. data/lib/geordi/commands/bundle_install.rb +7 -0
  36. data/lib/geordi/commands/cleanup_directory.rb +16 -0
  37. data/lib/geordi/commands/commit.rb +171 -0
  38. data/lib/geordi/commands/console.rb +24 -0
  39. data/lib/geordi/commands/create_database_yml.rb +18 -0
  40. data/lib/geordi/commands/create_databases.rb +16 -0
  41. data/lib/geordi/commands/cucumber.rb +20 -0
  42. data/lib/geordi/commands/deploy.rb +65 -0
  43. data/lib/geordi/commands/devserver.rb +14 -0
  44. data/lib/geordi/commands/dump.rb +63 -0
  45. data/lib/geordi/commands/migrate.rb +26 -0
  46. data/lib/geordi/commands/png_optimize.rb +92 -0
  47. data/lib/geordi/commands/rake.rb +21 -0
  48. data/lib/geordi/commands/remove_executable_flags.rb +14 -0
  49. data/lib/geordi/commands/rspec.rb +40 -0
  50. data/lib/geordi/commands/security_update.rb +56 -0
  51. data/lib/geordi/commands/setup.rb +33 -0
  52. data/lib/geordi/commands/setup_firefox_for_selenium.rb +6 -0
  53. data/lib/geordi/commands/setup_vnc.rb +84 -0
  54. data/lib/geordi/commands/shell.rb +20 -0
  55. data/lib/geordi/commands/test.rb +9 -0
  56. data/lib/geordi/commands/unit.rb +11 -0
  57. data/lib/geordi/commands/update.rb +33 -0
  58. data/lib/geordi/commands/version.rb +7 -0
  59. data/lib/geordi/commands/vnc_show.rb +6 -0
  60. data/lib/geordi/commands/with_firefox_for_selenium.rb +18 -0
  61. data/lib/geordi/commands/with_rake.rb +11 -0
  62. data/lib/geordi/{cuc.rb → cucumber.rb} +28 -39
  63. data/lib/geordi/dump_loader.rb +44 -70
  64. data/lib/geordi/firefox_for_selenium.rb +93 -74
  65. data/lib/geordi/interaction.rb +47 -0
  66. data/lib/geordi/remote.rb +66 -0
  67. data/lib/geordi/util.rb +60 -0
  68. data/lib/geordi/version.rb +1 -1
  69. data/lib/geordi.rb +1 -0
  70. metadata +50 -16
  71. data/bin/install-gems-remotely +0 -18
  72. data/bin/install-gems-remotely.sh +0 -16
  73. data/bin/power-deploy +0 -18
  74. data/bin/remotify-local-branch +0 -12
  75. data/lib/geordi/gitpt.rb +0 -175
@@ -0,0 +1,66 @@
1
+ require 'capistrano'
2
+
3
+ module Geordi
4
+ class CapistranoConfig
5
+
6
+ attr_accessor :root
7
+
8
+ def initialize(stage)
9
+ ENV['BUNDLE_BIN_PATH'] = 'Trick capistrano safeguard in deploy.rb into believing bundler is present by setting this variable.'
10
+
11
+ @stage = stage
12
+ @root = find_project_root!
13
+ load_capistrano_config
14
+ end
15
+
16
+ def user
17
+ @capistrano_config.fetch(:user)
18
+ end
19
+
20
+ def servers
21
+ @capistrano_config.find_servers(:roles => [:app])
22
+ end
23
+
24
+ def primary_server
25
+ @capistrano_config.find_servers(:roles => [:app], :only => { :primary => true }).first
26
+ end
27
+
28
+ def path
29
+ @capistrano_config.fetch(:deploy_to) + '/current'
30
+ end
31
+
32
+ def env
33
+ @capistrano_config.fetch(:rails_env, 'production')
34
+ end
35
+
36
+ def shell
37
+ @capistrano_config.fetch(:default_shell, 'bash --login')
38
+ end
39
+
40
+
41
+ private
42
+
43
+ def load_capistrano_config
44
+ config = ::Capistrano::Configuration.new
45
+ config.load('deploy')
46
+ config.load('config/deploy')
47
+ if @stage and @stage != ''
48
+ config[:stage] = @stage
49
+ config.find_and_execute_task(@stage)
50
+ end
51
+
52
+ @capistrano_config = config
53
+ end
54
+
55
+ def find_project_root!
56
+ current = Dir.pwd
57
+ until (File.exists? 'Capfile')
58
+ Dir.chdir '..'
59
+ raise 'Call me from inside a Rails project!' if current == Dir.pwd
60
+ current = Dir.pwd
61
+ end
62
+ current
63
+ end
64
+
65
+ end
66
+ end
data/lib/geordi/cli.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'thor'
2
+ require 'bundler'
3
+ require 'geordi/interaction'
4
+ require 'geordi/util'
5
+
6
+ module Geordi
7
+ class CLI < Thor
8
+ include Geordi::Interaction
9
+
10
+ # load all tasks defined in lib/geordi/commands
11
+ Dir[File.expand_path '../commands/*.rb', __FILE__].each do |file|
12
+ class_eval File.read(file), file
13
+ end
14
+
15
+ private
16
+
17
+ def file_containing?(file, regex)
18
+ File.exists?(file) and File.read(file).scan(regex).any?
19
+ end
20
+
21
+ # fix weird implementation of #invoke
22
+ def invoke_cmd(name, *args)
23
+ options = args.last.is_a?(Hash) ? args.pop : {}
24
+ invoke(name, args, options)
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ desc 'all-targets COMMAND', 'Run a capistrano command on all deploy targets'
2
+ long_desc <<-LONGDESC
3
+ Example: `geordi all-targets deploy`
4
+ LONGDESC
5
+
6
+ def all_targets(*args)
7
+ targets = Dir['config/deploy/*.rb'].map { |file| File.basename(file, '.rb') }.sort
8
+
9
+ note 'Found the following deploy targets:'
10
+ puts targets
11
+ puts
12
+
13
+ print 'Continue? [yN] '
14
+ exit unless $stdin.gets =~ /[yes]+/
15
+
16
+ targets << nil if targets.empty? # default target
17
+ targets.each do |stage|
18
+ announce 'Target: ' + (stage || '(default)')
19
+
20
+ command = "bundle exec cap #{stage} " + args.join(' ')
21
+ note_cmd command
22
+
23
+ Util.system!(command, :fail_message => 'Capistrano failed. Have a look!')
24
+ end
25
+
26
+ end
@@ -0,0 +1,22 @@
1
+ desc 'apache-site VIRTUAL_HOST', 'Enable the given virtual host, disabling all others'
2
+ def apache_site(*args)
3
+ site = args.shift
4
+ old_cwd = Dir.pwd
5
+ Dir.chdir '/etc/apache2/sites-available/'
6
+
7
+ # show available sites if no site was passed as argument
8
+ unless site
9
+ puts 'ERROR: Argument site is missing.'
10
+ puts 'Please call: apache-site my-site'
11
+ puts
12
+ puts 'Available sites:'
13
+ Dir.new(".").each do |file|
14
+ puts "- #{file}" if file != '.' && file != '..'
15
+ end
16
+ exit
17
+ end
18
+
19
+ has_default = File.exists?('default')
20
+ exec "sudo a2dissite \*; sudo a2ensite #{"default " if has_default}#{site} && sudo apache2ctl restart"
21
+ Dir.chdir old_cwd
22
+ end
@@ -0,0 +1,7 @@
1
+ desc 'bundle-install', 'Run bundle install if required', :hide => true
2
+ def bundle_install
3
+ if File.exists?('Gemfile') and !system('bundle check > /dev/null 2>&1')
4
+ announce 'Bundling'
5
+ Util.system! 'bundle install'
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ desc 'cleanup-directory', 'Remove unneeded files'
2
+ def cleanup_directory
3
+
4
+ announce 'Removing tempfiles'
5
+ for pattern in %w[ webrat-* capybara-* tmp/webrat-* tmp/capybara-* tmp/rtex/* log/*.log ]
6
+ note pattern
7
+ puts `rm -vfR #{pattern}`
8
+ end
9
+
10
+ announce 'Finding recursively and removing backup files'
11
+ for pattern in %w[ *~ ]
12
+ note pattern
13
+ `find . -name #{pattern} -exec rm {} ';'`
14
+ end
15
+
16
+ end
@@ -0,0 +1,171 @@
1
+ desc 'commit', 'Commit using a story title from Pivotal Tracker'
2
+ def commit
3
+ Gitpt.new.run
4
+ end
5
+
6
+ class Gitpt
7
+ include Geordi::Interaction
8
+ require 'yaml'
9
+ require 'highline'
10
+ require 'pivotal-tracker'
11
+
12
+ attr_reader :token, :initials, :settings_file, :deprecated_token_file,
13
+ :highline, :applicable_stories, :memberships
14
+
15
+ def initialize
16
+ @highline = HighLine.new
17
+ @settings_file = File.join(ENV['HOME'], '.gitpt')
18
+ @deprecated_token_file = File.join(ENV['HOME'], '.pt_token')
19
+ load_settings
20
+ settings_were_invalid = (not settings_valid?)
21
+
22
+ hello unless settings_valid?
23
+ request_settings while not settings_valid?
24
+ stored if settings_were_invalid
25
+
26
+ PivotalTracker::Client.use_ssl = true
27
+ PivotalTracker::Client.token = token
28
+ end
29
+
30
+ def settings_valid?
31
+ token and token.size > 10
32
+ end
33
+
34
+ def bold(string)
35
+ HighLine::BOLD + string + HighLine::RESET
36
+ end
37
+
38
+ def highlight(string)
39
+ bold HighLine::BLUE + string
40
+ end
41
+
42
+ def hello
43
+ highline.say HighLine::RESET
44
+ highline.say "Welcome to #{bold 'gitpt'}.\n\n"
45
+ end
46
+
47
+ def loading(message, &block)
48
+ print message
49
+ STDOUT.flush
50
+ yield
51
+ print "\r" + ' ' * message.size + "\r" # Remove loading message
52
+ STDOUT.flush
53
+ end
54
+
55
+ def stored
56
+ highline.say left(<<-MESSAGE)
57
+ Thank you. Your settings have been stored at #{highlight @settings_file}
58
+ You may remove that file for the wizard to reappear.
59
+
60
+ ----------------------------------------------------
61
+
62
+ MESSAGE
63
+ end
64
+
65
+ def request_settings
66
+ highline.say highlight('Your settings are missing or invalid.')
67
+ highline.say "Please configure your Pivotal Tracker access.\n\n"
68
+ token = highline.ask bold("Your API key:") + " "
69
+ initials = highline.ask bold("Your PT initials") + " (optional, used for highlighting your stories): "
70
+ highline.say "\n"
71
+
72
+ settings = { :token => token, :initials => initials }
73
+ File.open settings_file, 'w' do |file|
74
+ file.write settings.to_yaml
75
+ end
76
+ load_settings
77
+ end
78
+
79
+ def load_settings
80
+ if File.exists? settings_file
81
+ settings = YAML.load(File.read settings_file)
82
+ @initials = settings[:initials]
83
+ @token = settings[:token]
84
+ else
85
+ if File.exists?(deprecated_token_file)
86
+ highline.say left(<<-MESSAGE)
87
+ #{HighLine::YELLOW}You are still using #{highlight(deprecated_token_file) + HighLine::YELLOW} which will be deprecated in a future version.
88
+ Please migrate your settings to ~/.gitpt or remove #{deprecated_token_file} for the wizard to cast magic.
89
+ MESSAGE
90
+ @token = File.read(deprecated_token_file)
91
+ end
92
+ end
93
+ end
94
+
95
+ def load_projects
96
+ project_id_filename = '.pt_project_id'
97
+ if File.exists?(project_id_filename)
98
+ project_ids = File.read('.pt_project_id').split(/[\s]+/).map(&:to_i)
99
+ end
100
+
101
+ unless project_ids and project_ids.size > 0
102
+ highline.say left(<<-MESSAGE)
103
+ Sorry, I could not find a project ID in #{highlight project_id_filename} :(
104
+
105
+ Please put at least one Pivotal Tracker project id into #{project_id_filename} in this directory.
106
+ You may add multiple IDs, separated using white space.
107
+ MESSAGE
108
+ exit 1
109
+ end
110
+
111
+ loading 'Connecting to Pivotal Tracker...' do
112
+ projects = project_ids.collect do |project_id|
113
+ PivotalTracker::Project.find(project_id)
114
+ end
115
+
116
+ @memberships = projects.collect(&:memberships).map(&:all).flatten
117
+
118
+ @applicable_stories = projects.collect do |project|
119
+ project.stories.all(:state => 'started,finished,rejected')
120
+ end.flatten
121
+ end
122
+ end
123
+
124
+ def choose_story
125
+ selected_story = nil
126
+
127
+ highline.choose do |menu|
128
+ menu.header = "Choose a story"
129
+ applicable_stories.each do |story|
130
+ owner_name = story.owned_by
131
+ owner = if owner_name
132
+ owners = memberships.select{|member| member.name == owner_name}
133
+ owners.first ? owners.first.initials : '?'
134
+ else
135
+ '?'
136
+ end
137
+
138
+ state = story.current_state
139
+ if state == 'started'
140
+ state = HighLine::GREEN + state + HighLine::RESET
141
+ elsif state != 'finished'
142
+ state = HighLine::RED + state + HighLine::RESET
143
+ end
144
+ state += HighLine::BOLD if owner == initials
145
+
146
+ label = "(#{owner}, #{state}) #{story.name}"
147
+ label = bold(label) if owner == initials
148
+ menu.choice(label) { selected_story = story }
149
+ end
150
+ menu.hidden ''
151
+ end
152
+
153
+ if selected_story
154
+ message = highline.ask("\nAdd an optional message")
155
+ highline.say message
156
+
157
+ commit_message = "[##{selected_story.id}] #{selected_story.name}"
158
+ if message.strip != ''
159
+ commit_message << ' - '<< message.strip
160
+ end
161
+
162
+ exec('git', 'commit', '-m', commit_message)
163
+ end
164
+ end
165
+
166
+ def run
167
+ load_projects
168
+ choose_story
169
+ end
170
+
171
+ end
@@ -0,0 +1,24 @@
1
+ desc 'console [TARGET]', 'Open a Rails console locally or on a Capistrano deploy target'
2
+ long_desc <<-LONGDESC
3
+ Open a local Rails console: `geordi console`
4
+
5
+ Open a Rails console on `staging`: `geordi console staging`
6
+ LONGDESC
7
+
8
+
9
+ option :select_server, :default => false, :type => :boolean, :aliases => '-s'
10
+
11
+ def console(target = 'development', *args)
12
+ require 'geordi/remote'
13
+
14
+ if target == 'development'
15
+ announce 'Opening a local Rails console'
16
+
17
+ Util.system! Util.console_command(target)
18
+
19
+ else
20
+ announce 'Opening a Rails console on ' + target
21
+
22
+ Geordi::Remote.new(target).console(options)
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ desc 'create-database-yml', '[sic]', :hide => true
2
+ def create_database_yml
3
+ real_yml = 'config/database.yml'
4
+ sample_yml = 'config/database.sample.yml'
5
+
6
+ if File.exists?(sample_yml) and not File.exists?(real_yml)
7
+ announce 'Creating ' + real_yml
8
+
9
+ print 'Please enter your DB password: '
10
+ db_password = STDIN.gets.strip
11
+
12
+ sample = File.read(sample_yml)
13
+ real = sample.gsub(/password:.*$/, "password: #{db_password}")
14
+ File.open(real_yml, 'w') { |f| f.write(real) }
15
+
16
+ note "Created #{real_yml}."
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ desc 'create-databases', 'Create all databases', :hide => true
2
+ def create_databases
3
+ invoke_cmd 'create_database_yml'
4
+ invoke_cmd 'bundle_install'
5
+
6
+ announce 'Creating databases'
7
+
8
+ if File.exists?('config/database.yml')
9
+ command = 'bundle exec rake db:create:all'
10
+ command << ' parallel:create' if file_containing?('Gemfile', /parallel_tests/)
11
+
12
+ Util.system! command
13
+ else
14
+ puts 'config/database.yml does not exist. Nothing to do.'
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ desc 'cucumber [FILES]', 'Run Cucumber features'
2
+ long_desc <<-LONGDESC
3
+ Example: `geordi cucumber features/authentication_feature:3`
4
+
5
+ Runs Cucumber as you want: with `bundle exec`, `cucumber_spinner` detection,
6
+ separate Firefox for Selenium, etc.
7
+ LONGDESC
8
+
9
+ def cucumber(*files)
10
+ require 'geordi/cucumber'
11
+
12
+ invoke_cmd 'bundle_install'
13
+
14
+ if File.directory?('features')
15
+ announce 'Running features'
16
+ Geordi::Cucumber.new.run(files) or fail 'Features failed.'
17
+ else
18
+ note 'Cucumber not employed.'
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ desc 'deploy', 'Guided deployment'
2
+ def deploy
3
+ ENV['PAGER'] = 'cat'
4
+
5
+ master_branch = prompt('master branch', 'master')
6
+ production_branch = prompt('production branch', 'production')
7
+ production_stage = prompt('production capistrano stage', 'production')
8
+
9
+ announce "Checking if your #{master_branch} is up to date"
10
+
11
+ diff_size = call_or_fail("git fetch && git diff #{master_branch} origin/#{master_branch} | wc -l", true)
12
+ changes_size = call_or_fail('git status -s | wc -l', true)
13
+
14
+ if diff_size == '0' and changes_size == '0'
15
+ note 'All good.'
16
+ else
17
+ fail "Your #{master_branch} is not the same as on origin or holds uncommitted changes. Fix that first."
18
+ end
19
+
20
+ announce "Checking what's on #{production_stage} right now..."
21
+
22
+ call_or_fail "git checkout #{production_branch} && git pull"
23
+
24
+ announce "You are about to deploy the following commits from #{master_branch} to #{production_branch}:"
25
+
26
+ call_or_fail "git log #{production_branch}..#{master_branch} --oneline"
27
+
28
+ if prompt('Go ahead with the deployment?', 'n').downcase == 'y'
29
+ puts
30
+ capistrano_call = "cap #{production_stage} deploy:migrations"
31
+ if file_containing?('Gemfile', /capistrano/)
32
+ capistrano_call = "bundle exec #{capistrano_call}"
33
+ end
34
+ call_or_fail("git merge #{master_branch} && git push && #{capistrano_call}")
35
+ success 'Deployment complete.'
36
+ else
37
+ fail 'Deployment cancelled.'
38
+ end
39
+
40
+ end
41
+
42
+ private
43
+
44
+ def call_or_fail(command, return_output = false)
45
+ note_cmd command
46
+ if return_output
47
+ result = `#{command}`.to_s.strip
48
+ $?.success? or fail "Error while calling #{command}: #{$?}"
49
+ else
50
+ result = system(command) or fail "Error while calling #{command}: #{$?}"
51
+ puts
52
+ end
53
+ result
54
+ end
55
+
56
+ def prompt(message, default)
57
+ print "#{message}"
58
+ print " [#{default}]" if default
59
+ print ": "
60
+ input = $stdin.gets.strip
61
+ if input.empty? && default
62
+ input = default
63
+ end
64
+ input
65
+ end
@@ -0,0 +1,14 @@
1
+ desc 'devserver', 'Start a development server'
2
+
3
+ option :port, :aliases => '-p', :default => '3000'
4
+
5
+ def devserver
6
+ invoke_cmd 'bundle_install'
7
+ require 'geordi/util'
8
+
9
+ announce 'Booting a development server'
10
+ note "URL: http://#{File.basename(Dir.pwd)}.vcap.me:#{options.port}"
11
+ puts
12
+
13
+ Util.system! Util.server_command + " -p #{options.port}"
14
+ end
@@ -0,0 +1,63 @@
1
+ desc 'dump [TARGET]', 'Handle dumps'
2
+ long_desc <<-DESC
3
+ When called without arguments, dumps the development database with `dumple`.
4
+
5
+ geordi dump
6
+
7
+ When called with the `--load` option, sources the specified dump into the
8
+ development database.
9
+
10
+ geordi dump -l tmp/staging.dump
11
+
12
+ When called with a capistrano deploy target (e.g. `staging`), remotely dumps
13
+ the specified target's database and downloads it to `tmp/`.
14
+
15
+ geordi dump staging
16
+
17
+ When called with a capistrano deploy target and the `--load` option, sources the
18
+ dump into the development database after downloading it.
19
+
20
+ geordi dump staging -l
21
+ DESC
22
+
23
+ option :load, :aliases => ['-l'], :type => :string, :desc => 'Load a dump'
24
+ option :select_server, :default => false, :type => :boolean, :aliases => '-s'
25
+
26
+ def dump(target = nil, *args)
27
+ require 'geordi/dump_loader'
28
+ require 'geordi/remote'
29
+
30
+ if target.nil?
31
+ if options.load
32
+ # validate load option
33
+ fail 'Missing a dump file.' if options.load == 'load'
34
+ File.exists?(options.load) or fail 'Could not find the given dump file: ' + options.load
35
+
36
+ loader = DumpLoader.new(options.load)
37
+
38
+ announce "Sourcing dump into the #{loader.config['database']} db"
39
+ loader.load
40
+
41
+ success "Your #{loader.config['database']} database has now the data of #{options.load}."
42
+
43
+ else
44
+ announce 'Dumping the development database'
45
+ Util.system! 'dumple development'
46
+ success 'Successfully dumped the development database.'
47
+ end
48
+
49
+ else
50
+ announce 'Dumping the database of ' + target
51
+ dump_path = Geordi::Remote.new(target).dump(options)
52
+
53
+ if options.load
54
+ loader = DumpLoader.new(dump_path)
55
+
56
+ announce "Sourcing dump into the #{loader.config['database']} db"
57
+ loader.load
58
+
59
+ success "Your #{loader.config['database']} database has now the data of #{target}."
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,26 @@
1
+ desc 'migrate', 'Migrate all databases'
2
+ long_desc <<-LONGDESC
3
+ Example: `geordi migrate`
4
+
5
+ If you are using `parallel_tests`, this runs migrations in your development
6
+ environment and `rake parallel:prepare` afterwards. Otherwise, invokes `geordi rake`
7
+ with `db:migrate`.
8
+ LONGDESC
9
+
10
+ def migrate
11
+ invoke_cmd 'bundle_install'
12
+ announce 'Migrating'
13
+
14
+ if File.directory?('db/migrate')
15
+ if file_containing?('Gemfile', /parallel_tests/)
16
+ note 'Development and parallel test databases'
17
+ puts
18
+
19
+ Util.system! 'bundle exec rake db:migrate parallel:prepare'
20
+ else
21
+ invoke_cmd 'rake', 'db:migrate'
22
+ end
23
+ else
24
+ note 'No migrations directory found.'
25
+ end
26
+ end
@@ -0,0 +1,92 @@
1
+ desc 'png-optimize', 'Optimize .png files'
2
+ long_desc <<-LONGDESC
3
+ - Removes color profiles: cHRM, sRGB, gAMA, ICC, etc.
4
+ - Eliminates unused colors and reduces bit-depth (if possible)
5
+ - May reduce PNG file size lossless
6
+
7
+ Batch-optimize all `*.png` files in a directory:
8
+
9
+ geordi png-optimize directory
10
+
11
+ Batch-optimize the current directory:
12
+
13
+ geordi png-optimize .
14
+
15
+ Optimize a single file:
16
+
17
+ geordi png-optimize input.png
18
+ LONGDESC
19
+
20
+ def png_optimize(*args)
21
+ require 'fileutils'
22
+
23
+ announce 'Optimizing .png files'
24
+
25
+ if `which pngcrush`.strip.empty?
26
+ fail 'You have to install pngcrush first (sudo apt-get install pngcrush)'
27
+ end
28
+
29
+ po = PngOptimizer.new
30
+ path = args[0]
31
+ if File.directory?(path)
32
+ po.batch_optimize_inplace(path)
33
+ elsif File.file?(path)
34
+ po.optimize_inplace(path)
35
+ else
36
+ fail 'Neither directory nor file: ' + path
37
+ end
38
+
39
+ success 'PNG optimization completed.'
40
+ end
41
+
42
+ class PngOptimizer
43
+
44
+ def ends_with?(string, suffix)
45
+ string[-suffix.length, suffix.length] == suffix
46
+ end
47
+
48
+ def optimization_default_args
49
+ args = ""
50
+ args << "-rem alla " # remove everything except transparency
51
+ args << "-rem text " # remove text chunks
52
+ args << "-reduce " # eliminate unused colors and reduce bit-depth (if possible)
53
+ args
54
+ end
55
+
56
+ def optimize_file(input_file, output_file)
57
+ system "pngcrush #{optimization_default_args} '#{input_file}' '#{output_file}'"
58
+ end
59
+
60
+ def unused_tempfile_path(original)
61
+ dirname = File.dirname(original)
62
+ basename = File.basename(original)
63
+ count = 0
64
+ begin
65
+ tmp_name = "#{dirname}/#{basename}_temp_#{count += 1}.png"
66
+ end while File.exists?(tmp_name)
67
+ tmp_name
68
+ end
69
+
70
+ def optimize_inplace(input_file)
71
+ temp_file = unused_tempfile_path(input_file)
72
+ result = optimize_file(input_file, temp_file)
73
+ if result
74
+ FileUtils.rm(input_file)
75
+ FileUtils.mv("#{temp_file}", "#{input_file}")
76
+ else
77
+ fail 'Error:' + $?
78
+ end
79
+ end
80
+
81
+ def batch_optimize_inplace(path)
82
+ # Dir[".png"] works case sensitive, so to catch all funky .png extensions we have to go the following way:
83
+ png_relative_paths = []
84
+ Dir["#{path}/*.*"].each do |file_name|
85
+ png_relative_paths << file_name if ends_with?(File.basename(file_name.downcase), ".png")
86
+ end
87
+ png_relative_paths.each do |png_relative_path|
88
+ optimize_inplace(png_relative_path)
89
+ end
90
+ end
91
+
92
+ end