geordi 0.18.0 → 1.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 (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