herokugarden 0.4.2

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.
@@ -0,0 +1,101 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all specs"
5
+ Spec::Rake::SpecTask.new('spec') do |t|
6
+ t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
7
+ t.spec_files = FileList['spec/*_spec.rb']
8
+ end
9
+
10
+ desc "Print specdocs"
11
+ Spec::Rake::SpecTask.new(:doc) do |t|
12
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
13
+ t.spec_files = FileList['spec/*_spec.rb']
14
+ end
15
+
16
+ desc "Generate RCov code coverage report"
17
+ Spec::Rake::SpecTask.new('rcov') do |t|
18
+ t.spec_files = FileList['spec/*_spec.rb']
19
+ t.rcov = true
20
+ t.rcov_opts = ['--exclude', 'examples']
21
+ end
22
+
23
+ task :default => :spec
24
+
25
+ ######################################################
26
+
27
+ require 'rake'
28
+ require 'rake/testtask'
29
+ require 'rake/clean'
30
+ require 'rake/gempackagetask'
31
+ require 'rake/rdoctask'
32
+ require 'fileutils'
33
+ include FileUtils
34
+ require 'lib/herokugarden'
35
+
36
+ version = HerokuGarden::Client.version
37
+ name = "herokugarden"
38
+
39
+ spec = Gem::Specification.new do |s|
40
+ s.name = name
41
+ s.version = version
42
+ s.summary = "Client library and CLI to deploy Rails apps on Heroku Garden."
43
+ s.description = "Client library and command-line tool to manage and deploy Rails apps on Heroku Garden."
44
+ s.author = "Adam Wiggins"
45
+ s.email = "feedback@heroku.com"
46
+ s.homepage = "http://herokugarden.com/"
47
+ s.executables = [ "herokugarden" ]
48
+ s.default_executable = "herokugarden"
49
+ s.rubyforge_project = "herokugarden"
50
+ s.post_install_message = <<EOF
51
+
52
+ ---> Installing herokugarden v#{version}
53
+ ---> Migrate local checkouts using the git:transition command:
54
+
55
+ cd myapp/
56
+ herokugarden git:transition
57
+
58
+ EOF
59
+
60
+ s.platform = Gem::Platform::RUBY
61
+ s.has_rdoc = true
62
+
63
+ s.files = %w(Rakefile) +
64
+ Dir.glob("{bin,lib,spec}/**/*")
65
+
66
+ s.require_path = "lib"
67
+ s.bindir = "bin"
68
+
69
+ s.add_dependency('rest-client', '>=0.5')
70
+ end
71
+
72
+ Rake::GemPackageTask.new(spec) do |p|
73
+ p.need_tar = true if RUBY_PLATFORM !~ /mswin/
74
+ end
75
+
76
+ task :install => [ :test, :package ] do
77
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
78
+ end
79
+
80
+ task :uninstall => [ :clean ] do
81
+ sh %{sudo gem uninstall #{name}}
82
+ end
83
+
84
+ Rake::TestTask.new do |t|
85
+ t.libs << "spec"
86
+ t.test_files = FileList['spec/*_spec.rb']
87
+ t.verbose = true
88
+ end
89
+
90
+ Rake::RDocTask.new do |t|
91
+ t.rdoc_dir = 'rdoc'
92
+ t.title = "Heroku API"
93
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
94
+ t.options << '--charset' << 'utf-8'
95
+ t.rdoc_files.include('README')
96
+ t.rdoc_files.include('REST')
97
+ t.rdoc_files.include('lib/heroku/*.rb')
98
+ end
99
+
100
+ CLEAN.include [ 'build/*', '**/*.o', '**/*.so', '**/*.a', 'lib/*-*', '**/*.log', 'pkg', 'lib/*.bundle', '*.gem', '.config' ]
101
+
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'herokugarden'
6
+ require 'herokugarden/command_line'
7
+
8
+ usage = <<EOTXT
9
+ === Heroku Garden Commands
10
+ list - list your apps
11
+ create [<name>] - create a new app
12
+ info <app> - show app info, like web url and git repo
13
+ update <app> - update the app
14
+ --name <newname>
15
+ --public (true|false)
16
+ --mode (production|development)
17
+ sharing <app> - manage collaborators
18
+ --add <email>
19
+ --remove <email>
20
+ --access (edit|view)
21
+ rake <app> <command> - remotely execute a rake command
22
+ destroy <app> - destroy the app permanently
23
+ keys - manage your user's ssh public keys for git access
24
+ --add [<path to keyfile>]
25
+ --remove <keyname or all>
26
+
27
+ Example story:
28
+ herokugarden create myapp
29
+ git clone git@herokugarden.com:myapp.git
30
+ cd myapp
31
+ (...make edits...)
32
+ git add .
33
+ git commit -m "changes"
34
+ git push
35
+ herokugarden update myapp --public true --mode production
36
+ EOTXT
37
+
38
+ command = ARGV.shift.strip.gsub(/:/, '_') rescue ""
39
+ if command.length == 0
40
+ puts usage
41
+ exit 1
42
+ end
43
+
44
+ cli = HerokuGarden::CommandLine.new
45
+ unless cli.methods.include? command
46
+ puts "no such method as #{command}, run without arguments for usage"
47
+ exit 2
48
+ end
49
+
50
+ cli.execute(command, ARGV)
51
+
@@ -0,0 +1,5 @@
1
+ module HerokuGarden; end
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/herokugarden')
4
+
5
+ require 'client'
@@ -0,0 +1,153 @@
1
+ require 'rubygems'
2
+ require 'rexml/document'
3
+ require 'rest_client'
4
+ require 'uri'
5
+
6
+ # A Ruby class to call the Heroku REST API. You might use this if you want to
7
+ # manage your Heroku apps from within a Ruby program, such as Capistrano.
8
+ #
9
+ # Example:
10
+ #
11
+ # require 'heroku'
12
+ # heroku = HerokuGarden::Client.new('me@example.com', 'mypass')
13
+ # heroku.create('myapp')
14
+ #
15
+ class HerokuGarden::Client
16
+ def self.version
17
+ '0.4.2'
18
+ end
19
+
20
+ attr_accessor :host, :user, :password
21
+
22
+ def initialize(user, password, host='herokugarden.com')
23
+ @user = user
24
+ @password = password
25
+ @host = host
26
+ end
27
+
28
+ # Show a list of apps which you are a collaborator on.
29
+ def list
30
+ doc = xml(get('/apps'))
31
+ doc.elements.to_a("//apps/app/name").map { |a| a.text }
32
+ end
33
+
34
+ # Show info such as mode, custom domain, and collaborators on an app.
35
+ def info(name, saferoute=false)
36
+ doc = xml(get("/#{'safe' if saferoute}apps/#{name}"))
37
+ attrs = { :collaborators => list_collaborators(name) }
38
+ doc.elements.to_a('//app/*').inject(attrs) do |hash, element|
39
+ hash[element.name.to_sym] = element.text; hash
40
+ end
41
+ end
42
+
43
+ # Create a new app, with an optional name.
44
+ def create(name=nil, options={})
45
+ params = {}
46
+ params['app[name]'] = name if name
47
+ xml(post('/apps', params)).elements["//app/name"].text
48
+ end
49
+
50
+ # Update an app. Available attributes:
51
+ # :name => rename the app (changes http and git urls)
52
+ # :public => true | false
53
+ # :mode => production | development
54
+ def update(name, attributes)
55
+ put("/apps/#{name}", :app => attributes)
56
+ end
57
+
58
+ # Destroy the app permanently.
59
+ def destroy(name)
60
+ delete("/apps/#{name}")
61
+ end
62
+
63
+ # Get a list of collaborators on the app, returns an array of hashes each of
64
+ # which contain :email and :access (=> edit | view) elements.
65
+ def list_collaborators(app_name)
66
+ doc = xml(get("/apps/#{app_name}/collaborators"))
67
+ doc.elements.to_a("//collaborators/collaborator").map do |a|
68
+ { :email => a.elements['email'].text, :access => a.elements['access'].text }
69
+ end
70
+ end
71
+
72
+ # Invite a person by email address to collaborate on the app. Optional
73
+ # third parameter can be edit or view.
74
+ def add_collaborator(app_name, email, access='view')
75
+ xml(post("/apps/#{app_name}/collaborators", { 'collaborator[email]' => email, 'collaborator[access]' => access }))
76
+ end
77
+
78
+ # Change an existing collaborator.
79
+ def update_collaborator(app_name, email, access)
80
+ put("/apps/#{app_name}/collaborators/#{escape(email)}", { 'collaborator[access]' => access })
81
+ end
82
+
83
+ # Remove a collaborator.
84
+ def remove_collaborator(app_name, email)
85
+ delete("/apps/#{app_name}/collaborators/#{escape(email)}")
86
+ end
87
+
88
+ # Get the list of ssh public keys for the current user.
89
+ def keys
90
+ doc = xml get('/user/keys')
91
+ doc.elements.to_a('//keys/key').map do |key|
92
+ key.elements['contents'].text
93
+ end
94
+ end
95
+
96
+ # Add an ssh public key to the current user.
97
+ def add_key(key)
98
+ post("/user/keys", key, { 'Content-Type' => 'text/ssh-authkey' })
99
+ end
100
+
101
+ # Remove an existing ssh public key from the current user.
102
+ def remove_key(key)
103
+ delete("/user/keys/#{escape(key)}")
104
+ end
105
+
106
+ # Clear all keys on the current user.
107
+ def remove_all_keys
108
+ delete("/user/keys")
109
+ end
110
+
111
+ # Run a rake command on the Heroku app.
112
+ def rake(app_name, cmd)
113
+ post("/apps/#{app_name}/rake", cmd)
114
+ end
115
+
116
+ ##################
117
+
118
+ def resource(uri)
119
+ RestClient::Resource.new("http://#{host}", user, password)[uri]
120
+ end
121
+
122
+ def get(uri, extra_headers={}) # :nodoc:
123
+ resource(uri).get(heroku_headers.merge(extra_headers))
124
+ end
125
+
126
+ def post(uri, payload="", extra_headers={}) # :nodoc:
127
+ resource(uri).post(payload, heroku_headers.merge(extra_headers))
128
+ end
129
+
130
+ def put(uri, payload, extra_headers={}) # :nodoc:
131
+ resource(uri).put(payload, heroku_headers.merge(extra_headers))
132
+ end
133
+
134
+ def delete(uri) # :nodoc:
135
+ resource(uri).delete
136
+ end
137
+
138
+ def heroku_headers # :nodoc:
139
+ {
140
+ 'X-Heroku-API-Version' => '2',
141
+ 'User-Agent' => "herokugarden-gem/#{self.class.version}",
142
+ }
143
+ end
144
+
145
+ def xml(raw) # :nodoc:
146
+ REXML::Document.new(raw)
147
+ end
148
+
149
+ def escape(value) # :nodoc:
150
+ escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
151
+ escaped.gsub('.', '%2E') # not covered by the previous URI.escape
152
+ end
153
+ end
@@ -0,0 +1,450 @@
1
+ require 'fileutils'
2
+
3
+ # This wraps the Heroku::Client class with higher-level actions suitable for
4
+ # use from the command line.
5
+ class HerokuGarden::CommandLine
6
+ class CommandFailed < RuntimeError; end
7
+
8
+ def execute(command, args)
9
+ send(command, args)
10
+ rescue RestClient::Unauthorized
11
+ display "Authentication failure"
12
+ rescue RestClient::ResourceNotFound
13
+ display "Resource not found. (Did you mistype the app name?)"
14
+ rescue RestClient::RequestFailed => e
15
+ display extract_error(e.response)
16
+ rescue HerokuGarden::CommandLine::CommandFailed => e
17
+ display e.message
18
+ end
19
+
20
+ def list(args)
21
+ list = heroku.list
22
+ if list.size > 0
23
+ display list.join("\n")
24
+ else
25
+ display "You have no apps."
26
+ end
27
+ end
28
+
29
+ def info(args)
30
+ name = args.shift.downcase.strip rescue ""
31
+ if name.length == 0 or name.slice(0, 1) == '-'
32
+ display "Usage: herokugarden info <app>"
33
+ else
34
+ attrs = heroku.info(name)
35
+ display "=== #{attrs[:name]}"
36
+ display "Web URL: http://#{attrs[:name]}.#{heroku.host}/"
37
+ display "Domain name: http://#{attrs[:domain_name]}/" if attrs[:domain_name]
38
+ display "Git Repo: git@#{heroku.host}:#{attrs[:name]}.git"
39
+ display "Mode: #{ attrs[:production] == 'true' ? 'production' : 'development' }"
40
+ display "Public: #{ attrs[:'share-public'] == 'true' ? 'true' : 'false' }"
41
+
42
+ first = true
43
+ lead = "Collaborators:"
44
+ attrs[:collaborators].each do |collaborator|
45
+ display "#{first ? lead : ' ' * lead.length} #{collaborator[:email]} (#{collaborator[:access]})"
46
+ first = false
47
+ end
48
+ end
49
+ end
50
+
51
+ def create(args)
52
+ name = args.shift.downcase.strip rescue nil
53
+ name = heroku.create(name, {})
54
+ display "Created http://#{name}.#{heroku.host}/ | git@#{heroku.host}:#{name}.git"
55
+ end
56
+
57
+ def update(args)
58
+ name = args.shift.downcase.strip rescue ""
59
+ raise CommandFailed, "Invalid app name" if name.length == 0 or name.slice(0, 1) == '-'
60
+
61
+ attributes = {}
62
+ extract_option(args, '--name') do |new_name|
63
+ attributes[:name] = new_name
64
+ end
65
+ extract_option(args, '--public', %w( true false )) do |public|
66
+ attributes[:share_public] = (public == 'true')
67
+ end
68
+ extract_option(args, '--mode', %w( production development )) do |mode|
69
+ attributes[:production] = (mode == 'production')
70
+ end
71
+ raise CommandFailed, "Nothing to update" if attributes.empty?
72
+ heroku.update(name, attributes)
73
+
74
+ app_name = attributes[:name] || name
75
+ display "http://#{app_name}.#{heroku.host}/ updated"
76
+ end
77
+
78
+ def clone(args)
79
+ name = args.shift.downcase.strip rescue ""
80
+ if name.length == 0 or name.slice(0, 1) == '-'
81
+ display "Usage: herokugarden clone <app>"
82
+ display "(this command is deprecated in favor of using the git repo url directly)"
83
+ else
84
+ cmd = "git clone #{git_repo_for(name)}"
85
+ display cmd
86
+ system cmd
87
+ end
88
+ end
89
+
90
+ def destroy(args)
91
+ name = args.shift.strip.downcase rescue ""
92
+ if name.length == 0 or name.slice(0, 1) == '-'
93
+ display "Usage: herokugarden destroy <app>"
94
+ else
95
+ heroku.destroy(name)
96
+ display "Destroyed #{name}"
97
+ end
98
+ end
99
+
100
+ def sharing(args)
101
+ name = args.shift.strip.downcase rescue ""
102
+ if name.length == 0 or name.slice(0, 1) == '-'
103
+ display "Usage: herokugarden sharing <app>"
104
+ else
105
+ access = extract_option(args, '--access', %w( edit view )) || 'view'
106
+ extract_option(args, '--add') do |email|
107
+ return add_collaborator(name, email, access)
108
+ end
109
+ extract_option(args, '--update') do |email|
110
+ return update_collaborator(name, email, access)
111
+ end
112
+ extract_option(args, '--remove') do |email|
113
+ return remove_collaborator(name, email)
114
+ end
115
+ return list_collaborators(name)
116
+ end
117
+ end
118
+
119
+ def collaborators(args)
120
+ sharing(args)
121
+ end
122
+
123
+ def add_collaborator(name, email, access)
124
+ display heroku.add_collaborator(name, email, access)
125
+ end
126
+
127
+ def update_collaborator(name, email, access)
128
+ heroku.update_collaborator(name, email, access)
129
+ display "Collaborator updated"
130
+ end
131
+
132
+ def remove_collaborator(name, email)
133
+ heroku.remove_collaborator(name, email)
134
+ display "Collaborator removed"
135
+ end
136
+
137
+ def list_collaborators(name)
138
+ list = heroku.list_collaborators(name)
139
+ display list.map { |c| "#{c[:email]} (#{c[:access]})" }.join("\n")
140
+ end
141
+
142
+ def git_repo_for(name)
143
+ "git@#{heroku.host}:#{name}.git"
144
+ end
145
+
146
+ def rake(args)
147
+ app_name = args.shift.strip.downcase rescue ""
148
+ cmd = args.join(' ')
149
+ if app_name.length == 0 or cmd.length == 0
150
+ display "Usage: herokugarden rake <app> <command>"
151
+ else
152
+ puts heroku.rake(app_name, cmd)
153
+ end
154
+ end
155
+
156
+ def git_transition(args)
157
+ unless File.exists?(Dir.pwd + '/.git')
158
+ raise HerokuGarden::CommandLine::CommandFailed, "The current directory is not a git repository.\nBe sure to cd into the app's directory and try again."
159
+ end
160
+
161
+ gitconfig = File.read(Dir.pwd + '/.git/config')
162
+ if gitconfig.match(/url = git@herokugarden\.com:(.+)\.git/)
163
+ raise HerokuGarden::CommandLine::CommandFailed, "This git repo for #{app_name} is already up to date and pointing at herokugarden.com."
164
+ end
165
+ if app_name_match = gitconfig.match(/url = git@heroku\.com:(.+)\.git/)
166
+ app_name = app_name_match[1]
167
+ begin
168
+ attempt_git_transition(gitconfig, app_name, 'heroku.com')
169
+ rescue RestClient::ResourceNotFound
170
+ begin
171
+ attempt_git_transition(gitconfig, app_name, 'herokugarden.com')
172
+ rescue RestClient::ResourceNotFound
173
+ raise HerokuGarden::CommandLine::CommandFailed, "The current directory does not contain a known Heroku app."
174
+ end
175
+ end
176
+ else
177
+ raise HerokuGarden::CommandLine::CommandFailed, "The current directory does not contain a known Heroku app."
178
+ end
179
+ end
180
+
181
+ def attempt_git_transition(gitconfig, app_name, domain)
182
+ heroku.host = domain
183
+ if [nil, 'true'].include?(heroku.info(app_name, true)[:stack2])
184
+ raise HerokuGarden::CommandLine::CommandFailed, "#{app_name} is running on the Heroku production platform.\nThis git repo URL is valid and does not need to be changed."
185
+ else
186
+ File.open(Dir.pwd + '/.git/config', 'w') do |f|
187
+ f.write gitconfig.gsub("url = git@heroku.com:#{app_name}.git", "url = git@herokugarden.com:#{app_name}.git")
188
+ end
189
+ display "This git repo for #{app_name} has been updated to the new herokugarden.com URL."
190
+ end
191
+ end
192
+
193
+ ############
194
+ attr_accessor :credentials
195
+
196
+ def heroku # :nodoc:
197
+ @heroku ||= init_heroku
198
+ end
199
+
200
+ def init_heroku # :nodoc:
201
+ HerokuGarden::Client.new(user, password, ENV['HEROKU_HOST'] || 'herokugarden.com')
202
+ end
203
+
204
+ def user # :nodoc:
205
+ get_credentials
206
+ @credentials[0]
207
+ end
208
+
209
+ def password # :nodoc:
210
+ get_credentials
211
+ @credentials[1]
212
+ end
213
+
214
+ def credentials_file
215
+ "#{home_directory}/.heroku/herokugarden_credentials"
216
+ end
217
+
218
+ def get_credentials # :nodoc:
219
+ return if @credentials
220
+ unless @credentials = read_credentials
221
+ @credentials = ask_for_credentials
222
+ save_credentials
223
+ end
224
+ @credentials
225
+ end
226
+
227
+ def read_credentials
228
+ if File.exists? credentials_file
229
+ return File.read(credentials_file).split("\n")
230
+ end
231
+ end
232
+
233
+ def echo_off
234
+ system "stty -echo"
235
+ end
236
+
237
+ def echo_on
238
+ system "stty echo"
239
+ end
240
+
241
+ def ask_for_credentials
242
+ puts "Enter your Heroku credentials."
243
+
244
+ print "Email: "
245
+ user = gets.strip
246
+
247
+ print "Password: "
248
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
249
+
250
+ [ user, password ]
251
+ end
252
+
253
+ def ask_for_password_on_windows
254
+ require "Win32API"
255
+ char = nil
256
+ password = ''
257
+
258
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
259
+ break if char == 10 || char == 13 # received carriage return or newline
260
+ if char == 127 || char == 8 # backspace and delete
261
+ password.slice!(-1, 1)
262
+ else
263
+ password << char.chr
264
+ end
265
+ end
266
+ puts
267
+ return password
268
+ end
269
+
270
+ def ask_for_password
271
+ echo_off
272
+ password = gets.strip
273
+ puts
274
+ echo_on
275
+ return password
276
+ end
277
+
278
+ def save_credentials
279
+ begin
280
+ write_credentials
281
+ add_key
282
+ rescue RestClient::Unauthorized => e
283
+ delete_credentials
284
+ raise e unless retry_login?
285
+
286
+ display "\nAuthentication failed"
287
+ @credentials = ask_for_credentials
288
+ @heroku = init_heroku
289
+ retry
290
+ rescue Exception => e
291
+ delete_credentials
292
+ raise e
293
+ end
294
+ end
295
+
296
+ def retry_login?
297
+ @login_attempts ||= 0
298
+ @login_attempts += 1
299
+ @login_attempts < 3
300
+ end
301
+
302
+ def write_credentials
303
+ FileUtils.mkdir_p(File.dirname(credentials_file))
304
+ File.open(credentials_file, 'w') do |f|
305
+ f.puts self.credentials
306
+ end
307
+ set_credentials_permissions
308
+ end
309
+
310
+ def set_credentials_permissions
311
+ FileUtils.chmod 0700, File.dirname(credentials_file)
312
+ FileUtils.chmod 0600, credentials_file
313
+ end
314
+
315
+ def delete_credentials
316
+ FileUtils.rm_f(credentials_file)
317
+ end
318
+
319
+ def extract_option(args, options, valid_values=nil)
320
+ values = options.is_a?(Array) ? options : [options]
321
+ return unless opt_index = args.select { |a| values.include? a }.first
322
+ opt_value = args[args.index(opt_index) + 1] rescue nil
323
+
324
+ # remove option from args
325
+ args.delete(opt_index)
326
+ args.delete(opt_value)
327
+
328
+ if valid_values
329
+ opt_value = opt_value.downcase if opt_value
330
+ raise CommandFailed, "Invalid value '#{opt_value}' for option #{values.last}" unless valid_values.include?(opt_value)
331
+ end
332
+
333
+ block_given? ? yield(opt_value) : opt_value
334
+ end
335
+
336
+ def keys(*args)
337
+ args = args.first # something weird with ruby argument passing
338
+
339
+ if args.empty?
340
+ list_keys
341
+ return
342
+ end
343
+
344
+ extract_option(args, '--add') do |keyfile|
345
+ add_key(keyfile)
346
+ return
347
+ end
348
+ extract_option(args, '--remove') do |arg|
349
+ remove_key(arg)
350
+ return
351
+ end
352
+
353
+ display "Usage: heroku keys [--add or --remove]"
354
+ end
355
+
356
+ def list_keys
357
+ keys = heroku.keys
358
+ if keys.empty?
359
+ display "No keys for #{user}"
360
+ else
361
+ display "=== #{keys.size} key#{keys.size > 1 ? 's' : ''} for #{user}"
362
+ keys.each do |key|
363
+ display key
364
+ end
365
+ end
366
+ end
367
+
368
+ def add_key(keyfile=nil)
369
+ keyfile ||= find_key
370
+ key = File.read(keyfile)
371
+
372
+ display "Uploading ssh public key #{keyfile}"
373
+ heroku.add_key(key)
374
+ end
375
+
376
+ def remove_key(arg)
377
+ if arg == 'all'
378
+ heroku.remove_all_keys
379
+ display "All keys removed."
380
+ else
381
+ heroku.remove_key(arg)
382
+ display "Key #{arg} removed."
383
+ end
384
+ end
385
+
386
+ def find_key
387
+ %w(rsa dsa).each do |key_type|
388
+ keyfile = "#{home_directory}/.ssh/id_#{key_type}.pub"
389
+ return keyfile if File.exists? keyfile
390
+ end
391
+ raise CommandFailed, "No ssh public key found in #{home_directory}/.ssh/id_[rd]sa.pub. You may want to specify the full path to the keyfile."
392
+ end
393
+
394
+ # vvv Deprecated
395
+ def upload_authkey(*args)
396
+ extract_key!
397
+ display "Uploading ssh public key"
398
+ display "(upload_authkey is deprecated, please use \"heroku keys --add\" instead)"
399
+ heroku.add_key(authkey)
400
+ end
401
+
402
+ def extract_key!
403
+ return unless key_path = extract_option(ARGV, ['-k', '--key'])
404
+ raise "Please inform the full path for your ssh public key" if File.directory?(key_path)
405
+ raise "Could not read ssh public key in #{key_path}" unless @ssh_key = authkey_read(key_path)
406
+ end
407
+
408
+ def authkey_type(key_type)
409
+ authkey_read("#{home_directory}/.ssh/id_#{key_type}.pub")
410
+ end
411
+
412
+ def authkey_read(filename)
413
+ File.read(filename) if File.exists?(filename)
414
+ end
415
+
416
+ def authkey
417
+ return @ssh_key if @ssh_key
418
+ %w( rsa dsa ).each do |key_type|
419
+ key = authkey_type(key_type)
420
+ return key if key
421
+ end
422
+ raise "Your ssh public key was not found. Make sure you have a rsa or dsa key in #{home_directory}/.ssh, or specify the full path to the keyfile."
423
+ end
424
+ # ^^^ Deprecated
425
+
426
+ def display(msg)
427
+ puts msg
428
+ end
429
+
430
+ def home_directory
431
+ running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
432
+ end
433
+
434
+ def extract_error(response)
435
+ return "Not found" if response.code.to_i == 404
436
+
437
+ msg = parse_error_xml(response.body) rescue ''
438
+ msg = 'Internal server error' if msg.empty?
439
+ msg
440
+ end
441
+
442
+ def parse_error_xml(body)
443
+ xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
444
+ xml_errors.map { |a| a.text }.join(" / ")
445
+ end
446
+
447
+ def running_on_windows?
448
+ RUBY_PLATFORM =~ /mswin32/
449
+ end
450
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'fileutils'
4
+
5
+ require File.dirname(__FILE__) + '/../lib/herokugarden'
6
+ require 'command_line'
7
+
8
+ class Module
9
+ def redefine_const(name, value)
10
+ __send__(:remove_const, name) if const_defined?(name)
11
+ const_set(name, value)
12
+ end
13
+ end
@@ -0,0 +1,157 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe HerokuGarden::Client do
4
+ before do
5
+ @client = HerokuGarden::Client.new(nil, nil)
6
+ @resource = mock('heroku rest resource')
7
+ end
8
+
9
+ it "list -> get a list of this user's apps" do
10
+ @client.should_receive(:resource).with('/apps').and_return(@resource)
11
+ @resource.should_receive(:get).and_return <<EOXML
12
+ <?xml version="1.0" encoding="UTF-8"?>
13
+ <apps type="array">
14
+ <app><name>myapp1</name></app>
15
+ <app><name>myapp2</name></app>
16
+ </apps>
17
+ EOXML
18
+ @client.list.should == %w(myapp1 myapp2)
19
+ end
20
+
21
+ it "info -> get app attributes" do
22
+ @client.should_receive(:resource).with('/apps/myapp').and_return(@resource)
23
+ @resource.should_receive(:get).and_return <<EOXML
24
+ <?xml version='1.0' encoding='UTF-8'?>
25
+ <app>
26
+ <blessed type='boolean'>true</blessed>
27
+ <created-at type='datetime'>2008-07-08T17:21:50-07:00</created-at>
28
+ <id type='integer'>49134</id>
29
+ <name>testgems</name>
30
+ <production type='boolean'>true</production>
31
+ <share-public type='boolean'>true</share-public>
32
+ <domain_name/>
33
+ </app>
34
+ EOXML
35
+ @client.stub!(:list_collaborators).and_return([:jon, :mike])
36
+ @client.info('myapp').should == { :blessed => 'true', :'created-at' => '2008-07-08T17:21:50-07:00', :id => '49134', :name => 'testgems', :production => 'true', :'share-public' => 'true', :domain_name => nil, :collaborators => [:jon, :mike] }
37
+ end
38
+
39
+ it "create -> create a new blank app" do
40
+ @client.should_receive(:resource).with('/apps').and_return(@resource)
41
+ @resource.should_receive(:post).and_return <<EOXML
42
+ <?xml version="1.0" encoding="UTF-8"?>
43
+ <app><name>untitled-123</name></app>
44
+ EOXML
45
+ @client.create.should == "untitled-123"
46
+ end
47
+
48
+ it "create(name) -> create a new blank app with a specified name" do
49
+ @client.should_receive(:resource).with('/apps').and_return(@resource)
50
+ @resource.should_receive(:post).with({ 'app[name]' => 'newapp' }, @client.heroku_headers).and_return <<EOXML
51
+ <?xml version="1.0" encoding="UTF-8"?>
52
+ <app><name>newapp</name></app>
53
+ EOXML
54
+ @client.create("newapp").should == "newapp"
55
+ end
56
+
57
+ it "update(name, attributes) -> updates existing apps" do
58
+ @client.should_receive(:resource).with('/apps/myapp').and_return(@resource)
59
+ @resource.should_receive(:put).with({ :app => { :mode => 'production', :public => true } }, anything)
60
+ @client.update("myapp", :mode => 'production', :public => true)
61
+ end
62
+
63
+ it "destroy(name) -> destroy the named app" do
64
+ @client.should_receive(:resource).with('/apps/destroyme').and_return(@resource)
65
+ @resource.should_receive(:delete)
66
+ @client.destroy("destroyme")
67
+ end
68
+
69
+ it "rake(app_name, cmd) -> run a rake command on the app" do
70
+ @client.should_receive(:resource).with('/apps/myapp/rake').and_return(@resource)
71
+ @resource.should_receive(:post).with('db:migrate', @client.heroku_headers)
72
+ @client.rake('myapp', 'db:migrate')
73
+ end
74
+
75
+ describe "collaborators" do
76
+ it "list(app_name) -> list app collaborators" do
77
+ @client.should_receive(:resource).with('/apps/myapp/collaborators').and_return(@resource)
78
+ @resource.should_receive(:get).and_return <<EOXML
79
+ <?xml version="1.0" encoding="UTF-8"?>
80
+ <collaborators type="array">
81
+ <collaborator><email>joe@example.com</email><access>edit</access></collaborator>
82
+ <collaborator><email>jon@example.com</email><access>view</access></collaborator>
83
+ </collaborators>
84
+ EOXML
85
+ @client.list_collaborators('myapp').should == [
86
+ { :email => 'joe@example.com', :access => 'edit' },
87
+ { :email => 'jon@example.com', :access => 'view' }
88
+ ]
89
+ end
90
+
91
+ it "add_collaborator(app_name, email, access) -> adds collaborator to app" do
92
+ @client.should_receive(:resource).with('/apps/myapp/collaborators').and_return(@resource)
93
+ @resource.should_receive(:post).with({ 'collaborator[email]' => 'joe@example.com', 'collaborator[access]' => 'edit'}, anything)
94
+ @client.add_collaborator('myapp', 'joe@example.com', 'edit')
95
+ end
96
+
97
+ it "update_collaborator(app_name, email, access) -> updates existing collaborator record" do
98
+ @client.should_receive(:resource).with('/apps/myapp/collaborators/joe%40example%2Ecom').and_return(@resource)
99
+ @resource.should_receive(:put).with({ 'collaborator[access]' => 'view'}, anything)
100
+ @client.update_collaborator('myapp', 'joe@example.com', 'view')
101
+ end
102
+
103
+ it "remove_collaborator(app_name, email, access) -> removes collaborator from app" do
104
+ @client.should_receive(:resource).with('/apps/myapp/collaborators/joe%40example%2Ecom').and_return(@resource)
105
+ @resource.should_receive(:delete)
106
+ @client.remove_collaborator('myapp', 'joe@example.com')
107
+ end
108
+ end
109
+
110
+ describe "ssh keys" do
111
+ it "fetches a list of the user's current keys" do
112
+ @client.should_receive(:resource).with('/user/keys').and_return(@resource)
113
+ @resource.should_receive(:get).and_return <<EOXML
114
+ <?xml version="1.0" encoding="UTF-8"?>
115
+ <keys type="array">
116
+ <key>
117
+ <contents>ssh-dss thekey== joe@workstation</contents>
118
+ </key>
119
+ </keys>
120
+ EOXML
121
+ @client.keys.should == [ "ssh-dss thekey== joe@workstation" ]
122
+ end
123
+
124
+ it "add_key(key) -> add an ssh key (e.g., the contents of id_rsa.pub) to the user" do
125
+ @client.should_receive(:resource).with('/user/keys').and_return(@resource)
126
+ @client.stub!(:heroku_headers).and_return({})
127
+ @resource.should_receive(:post).with('a key', 'Content-Type' => 'text/ssh-authkey')
128
+ @client.add_key('a key')
129
+ end
130
+
131
+ it "remove_key(key) -> remove an ssh key by name (user@box)" do
132
+ @client.should_receive(:resource).with('/user/keys/joe%40workstation').and_return(@resource)
133
+ @resource.should_receive(:delete)
134
+ @client.remove_key('joe@workstation')
135
+ end
136
+
137
+ it "remove_all_keys -> removes all ssh keys for the user" do
138
+ @client.should_receive(:resource).with('/user/keys').and_return(@resource)
139
+ @resource.should_receive(:delete)
140
+ @client.remove_all_keys
141
+ end
142
+ end
143
+
144
+ describe "internal" do
145
+ it "creates a RestClient resource for making calls" do
146
+ @client.stub!(:host).and_return('heroku.com')
147
+ @client.stub!(:user).and_return('joe@example.com')
148
+ @client.stub!(:password).and_return('secret')
149
+
150
+ res = @client.resource('/xyz')
151
+
152
+ res.url.should == 'http://heroku.com/xyz'
153
+ res.user.should == 'joe@example.com'
154
+ res.password.should == 'secret'
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,334 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe HerokuGarden::CommandLine do
4
+ before do
5
+ @cli = HerokuGarden::CommandLine.new
6
+ @cli.stub!(:display)
7
+ @cli.stub!(:print)
8
+ @cli.stub!(:ask_for_credentials).and_raise("ask_for_credentials should not be called by specs")
9
+ end
10
+
11
+ describe "credentials" do
12
+ it "reads credentials from the credentials file" do
13
+ sandbox = "/tmp/cli_spec_#{Process.pid}"
14
+ File.open(sandbox, "w") { |f| f.write "user\npass\n" }
15
+ @cli.stub!(:credentials_file).and_return(sandbox)
16
+ @cli.read_credentials.should == %w(user pass)
17
+ end
18
+
19
+ it "takes the user from the first line and the password from the second line" do
20
+ @cli.stub!(:read_credentials).and_return(%w(user pass))
21
+ @cli.user.should == 'user'
22
+ @cli.password.should == 'pass'
23
+ end
24
+
25
+ it "asks for credentials when the file doesn't exist" do
26
+ sandbox = "/tmp/cli_spec_#{Process.pid}"
27
+ FileUtils.rm_rf(sandbox)
28
+ @cli.stub!(:credentials_file).and_return(sandbox)
29
+ @cli.should_receive(:ask_for_credentials).and_return([ 'u', 'p'])
30
+ @cli.should_receive(:save_credentials)
31
+ @cli.get_credentials.should == [ 'u', 'p' ]
32
+ end
33
+
34
+ it "writes the credentials to a file" do
35
+ sandbox = "/tmp/cli_spec_#{Process.pid}"
36
+ FileUtils.rm_rf(sandbox)
37
+ @cli.stub!(:credentials_file).and_return(sandbox)
38
+ @cli.stub!(:credentials).and_return(['one', 'two'])
39
+ @cli.should_receive(:set_credentials_permissions)
40
+ @cli.write_credentials
41
+ File.read(sandbox).should == "one\ntwo\n"
42
+ end
43
+
44
+ it "sets ~/.heroku/credentials to be readable only by the user" do
45
+ sandbox = "/tmp/cli_spec_#{Process.pid}"
46
+ FileUtils.rm_rf(sandbox)
47
+ FileUtils.mkdir_p(sandbox)
48
+ fname = "#{sandbox}/file"
49
+ system "touch #{fname}"
50
+ @cli.stub!(:credentials_file).and_return(fname)
51
+ @cli.set_credentials_permissions
52
+ File.stat(sandbox).mode.should == 040700
53
+ File.stat(fname).mode.should == 0100600
54
+ end
55
+
56
+ it "writes credentials and uploads authkey when credentials are saved" do
57
+ @cli.stub!(:credentials)
58
+ @cli.should_receive(:write_credentials)
59
+ @cli.should_receive(:add_key)
60
+ @cli.save_credentials
61
+ end
62
+
63
+ it "save_credentials deletes the credentials when the upload authkey is unauthorized" do
64
+ @cli.stub!(:write_credentials)
65
+ @cli.stub!(:retry_login?).and_return(false)
66
+ @cli.should_receive(:add_key).and_raise(RestClient::Unauthorized)
67
+ @cli.should_receive(:delete_credentials)
68
+ lambda { @cli.save_credentials }.should raise_error(RestClient::Unauthorized)
69
+ end
70
+
71
+ it "save_credentials deletes the credentials when there's no authkey" do
72
+ @cli.stub!(:write_credentials)
73
+ @cli.should_receive(:add_key).and_raise(RuntimeError)
74
+ @cli.should_receive(:delete_credentials)
75
+ lambda { @cli.save_credentials }.should raise_error
76
+ end
77
+
78
+ it "save_credentials deletes the credentials when the authkey is weak" do
79
+ @cli.stub!(:write_credentials)
80
+ @cli.should_receive(:add_key).and_raise(RestClient::RequestFailed)
81
+ @cli.should_receive(:delete_credentials)
82
+ lambda { @cli.save_credentials }.should raise_error
83
+ end
84
+
85
+ it "asks for login again when not authorized, for three times" do
86
+ @cli.stub!(:write_credentials)
87
+ @cli.stub!(:delete_credentials)
88
+ @cli.stub!(:add_key).and_raise(RestClient::Unauthorized)
89
+ @cli.should_receive(:ask_for_credentials).exactly(4).times
90
+ lambda { @cli.save_credentials }.should raise_error(RestClient::Unauthorized)
91
+ end
92
+
93
+ it "deletes the credentials file" do
94
+ FileUtils.should_receive(:rm_f).with(@cli.credentials_file)
95
+ @cli.delete_credentials
96
+ end
97
+ end
98
+
99
+ describe "key management" do
100
+ before do
101
+ @cli.instance_variable_set('@credentials', %w(user pass))
102
+ end
103
+
104
+ it "finds the user's ssh key in ~/ssh/id_rsa.pub" do
105
+ @cli.stub!(:home_directory).and_return('/home/joe')
106
+ File.should_receive(:exists?).with('/home/joe/.ssh/id_rsa.pub').and_return(true)
107
+ @cli.find_key.should == '/home/joe/.ssh/id_rsa.pub'
108
+ end
109
+
110
+ it "finds the user's ssh key in ~/ssh/id_dsa.pub" do
111
+ @cli.stub!(:home_directory).and_return('/home/joe')
112
+ File.should_receive(:exists?).with('/home/joe/.ssh/id_rsa.pub').and_return(false)
113
+ File.should_receive(:exists?).with('/home/joe/.ssh/id_dsa.pub').and_return(true)
114
+ @cli.find_key.should == '/home/joe/.ssh/id_dsa.pub'
115
+ end
116
+
117
+ it "raises an exception if neither id_rsa or id_dsa were found" do
118
+ @cli.stub!(:home_directory).and_return('/home/joe')
119
+ File.stub!(:exists?).and_return(false)
120
+ lambda { @cli.find_key }.should raise_error(HerokuGarden::CommandLine::CommandFailed)
121
+ end
122
+
123
+ it "adds a key from the default locations if no key filename is supplied" do
124
+ @cli.should_receive(:find_key).and_return('/home/joe/.ssh/id_rsa.pub')
125
+ File.should_receive(:read).with('/home/joe/.ssh/id_rsa.pub').and_return('ssh-rsa xyz')
126
+ @cli.heroku.should_receive(:add_key).with('ssh-rsa xyz')
127
+ @cli.add_key
128
+ end
129
+
130
+ it "adds a key from a specified keyfile path" do
131
+ @cli.should_not_receive(:find_key)
132
+ File.should_receive(:read).with('/my/key.pub').and_return('ssh-rsa xyz')
133
+ @cli.heroku.should_receive(:add_key).with('ssh-rsa xyz')
134
+ @cli.add_key('/my/key.pub')
135
+ end
136
+ end
137
+
138
+ describe "deprecated key management" do
139
+ it "gets pub keys from the user's home directory" do
140
+ @cli.should_receive(:home_directory).and_return('/Users/joe')
141
+ File.should_receive(:exists?).with('/Users/joe/.ssh/id_xyz.pub').and_return(true)
142
+ File.should_receive(:read).with('/Users/joe/.ssh/id_xyz.pub').and_return('ssh-xyz somehexkey')
143
+ @cli.authkey_type('xyz').should == 'ssh-xyz somehexkey'
144
+ end
145
+
146
+ it "gets the rsa key" do
147
+ @cli.stub!(:authkey_type).with('rsa').and_return('ssh-rsa somehexkey')
148
+ @cli.authkey.should == 'ssh-rsa somehexkey'
149
+ end
150
+
151
+ it "gets the dsa key when there's no rsa" do
152
+ @cli.stub!(:authkey_type).with('rsa').and_return(nil)
153
+ @cli.stub!(:authkey_type).with('dsa').and_return('ssh-dsa somehexkey')
154
+ @cli.authkey.should == 'ssh-dsa somehexkey'
155
+ end
156
+
157
+ it "raises a friendly error message when no key is found" do
158
+ @cli.stub!(:authkey_type).with('rsa').and_return(nil)
159
+ @cli.stub!(:authkey_type).with('dsa').and_return(nil)
160
+ lambda { @cli.authkey }.should raise_error
161
+ end
162
+
163
+ it "accepts a custom key via the -k parameter" do
164
+ Object.redefine_const(:ARGV, ['-k', '/Users/joe/sshkeys/mykey.pub'])
165
+ @cli.should_receive(:authkey_read).with('/Users/joe/sshkeys/mykey.pub').and_return('ssh-rsa somehexkey')
166
+ @cli.extract_key!
167
+ @cli.authkey.should == 'ssh-rsa somehexkey'
168
+ end
169
+
170
+ it "extracts options from ARGV" do
171
+ Object.redefine_const(:ARGV, %w( a b --something value c d ))
172
+ @cli.extract_option(ARGV, '--something').should == 'value'
173
+ ARGV.should == %w( a b c d )
174
+ end
175
+
176
+ it "rejects options outside valid values for ARGV" do
177
+ Object.redefine_const(:ARGV, %w( -boolean_option t ))
178
+ lambda { @cli.extract_argv_option('-boolean_option', %w( true false )) }.should raise_error
179
+ end
180
+
181
+ it "uploads the ssh authkey (deprecated in favor of add_key)" do
182
+ @cli.should_receive(:extract_key!)
183
+ @cli.should_receive(:authkey).and_return('my key')
184
+ heroku = mock("heroku client")
185
+ @cli.should_receive(:init_heroku).and_return(heroku)
186
+ heroku.should_receive(:add_key).with('my key')
187
+ @cli.upload_authkey
188
+ end
189
+
190
+ it "gets the home directory from HOME when running on *nix" do
191
+ ENV.should_receive(:[]).with('HOME').and_return(@home)
192
+ @cli.stub!(:running_on_windows?).and_return(false)
193
+ @cli.home_directory.should == @home
194
+ end
195
+
196
+ it "gets the home directory from USERPROFILE when running on windows" do
197
+ ENV.should_receive(:[]).with('USERPROFILE').and_return(@home)
198
+ @cli.stub!(:running_on_windows?).and_return(true)
199
+ @cli.home_directory.should == @home
200
+ end
201
+
202
+ it "detects it's running on windows" do
203
+ Object.redefine_const(:RUBY_PLATFORM, 'i386-mswin32')
204
+ @cli.should be_running_on_windows
205
+ end
206
+
207
+ it "doesn't consider cygwin as windows" do
208
+ Object.redefine_const(:RUBY_PLATFORM, 'i386-cygwin')
209
+ @cli.should_not be_running_on_windows
210
+ end
211
+ end
212
+
213
+ describe "execute" do
214
+ it "executes an action" do
215
+ @cli.should_receive(:my_action).with(%w(arg1 arg2))
216
+ @cli.execute('my_action', %w(arg1 arg2))
217
+ end
218
+
219
+ it "catches unauthorized errors" do
220
+ @cli.should_receive(:my_action).and_raise(RestClient::Unauthorized)
221
+ @cli.should_receive(:display).with('Authentication failure')
222
+ @cli.execute('my_action', 'args')
223
+ end
224
+
225
+ it "parses rails-format error xml" do
226
+ @cli.parse_error_xml('<errors><error>Error 1</error><error>Error 2</error></errors>').should == 'Error 1 / Error 2'
227
+ end
228
+
229
+ it "does not catch general exceptions, those are shown to the user as normal" do
230
+ @cli.should_receive(:my_action).and_raise(RuntimeError)
231
+ lambda { @cli.execute('my_action', 'args') }.should raise_error(RuntimeError)
232
+ end
233
+ end
234
+
235
+ describe "app actions" do
236
+ before do
237
+ @cli.instance_variable_set('@credentials', %w(user pass))
238
+ end
239
+
240
+ it "shows app info" do
241
+ @cli.heroku.should_receive(:info).with('myapp').and_return({ :name => 'myapp', :collaborators => [] })
242
+ @cli.heroku.stub!(:domain).and_return('heroku.com')
243
+ @cli.should_receive(:display).with('=== myapp')
244
+ @cli.should_receive(:display).with('Web URL: http://myapp.herokugarden.com/')
245
+ @cli.info([ 'myapp' ])
246
+ end
247
+
248
+ it "creates without a name" do
249
+ @cli.heroku.should_receive(:create).with(nil, {}).and_return("untitled-123")
250
+ @cli.create([])
251
+ end
252
+
253
+ it "creates with a name" do
254
+ @cli.heroku.should_receive(:create).with('myapp', {}).and_return("myapp")
255
+ @cli.create([ 'myapp' ])
256
+ end
257
+
258
+ it "updates app" do
259
+ @cli.heroku.should_receive(:update).with('myapp', { :name => 'myapp2', :share_public => true, :production => true })
260
+ @cli.update([ 'myapp', '--name', 'myapp2', '--public', 'true', '--mode', 'production' ])
261
+ end
262
+
263
+ it "clones the app (deprecated in favor of straight git clone)" do
264
+ @cli.should_receive(:system).with('git clone git@herokugarden.com:myapp.git')
265
+ @cli.clone([ 'myapp' ])
266
+ end
267
+ end
268
+
269
+ describe "collaborators" do
270
+ before do
271
+ @cli.instance_variable_set('@credentials', %w(user pass))
272
+ end
273
+
274
+ it "list collaborators when there's just the app name" do
275
+ @cli.heroku.should_receive(:list_collaborators).and_return([])
276
+ @cli.collaborators(['myapp'])
277
+ end
278
+
279
+ it "add collaborators with default access to view only" do
280
+ @cli.heroku.should_receive(:add_collaborator).with('myapp', 'joe@example.com', 'view')
281
+ @cli.collaborators(['myapp', '--add', 'joe@example.com'])
282
+ end
283
+
284
+ it "add collaborators with edit access" do
285
+ @cli.heroku.should_receive(:add_collaborator).with('myapp', 'joe@example.com', 'edit')
286
+ @cli.collaborators(['myapp', '--add', 'joe@example.com', '--access', 'edit'])
287
+ end
288
+
289
+ it "updates collaborators" do
290
+ @cli.heroku.should_receive(:update_collaborator).with('myapp', 'joe@example.com', 'view')
291
+ @cli.collaborators(['myapp', '--update', 'joe@example.com', '--access', 'view'])
292
+ end
293
+
294
+ it "removes collaborators" do
295
+ @cli.heroku.should_receive(:remove_collaborator).with('myapp', 'joe@example.com')
296
+ @cli.collaborators(['myapp', '--remove', 'joe@example.com'])
297
+ end
298
+ end
299
+
300
+ describe "git transition" do
301
+ before do
302
+ @cli.instance_variable_set('@credentials', %w(user pass))
303
+ Dir.stub!(:pwd).and_return('/apps/myapp')
304
+ end
305
+
306
+ it "requires a git repo" do
307
+ File.stub!(:exists?).with('/apps/myapp/.git').and_return(false)
308
+ lambda { @cli.git_transition([]) }.should raise_error(HerokuGarden::CommandLine::CommandFailed)
309
+ end
310
+
311
+ it "requires a remote entry with git@heroku.com:appname.git" do
312
+ File.stub!(:exists?).with('/apps/myapp/.git').and_return(true)
313
+ File.stub!(:read).with('/apps/myapp/.git/config').and_return('config without app')
314
+ lambda { @cli.git_transition([]) }.should raise_error(HerokuGarden::CommandLine::CommandFailed)
315
+ end
316
+
317
+ it "doesn't update stack2 apps" do
318
+ File.stub!(:exists?).with('/apps/myapp/.git').and_return(true)
319
+ File.stub!(:read).with('/apps/myapp/.git/config').and_return("[remote \"heroku\"]\n\turl = git@heroku.com:myapp.git")
320
+ @cli.heroku.should_receive(:info).with('myapp', true).and_return({ :stack2 => 'true' })
321
+ lambda { @cli.git_transition([]) }.should raise_error(HerokuGarden::CommandLine::CommandFailed)
322
+ end
323
+
324
+ it "updates the gitconfig" do
325
+ File.stub!(:exists?).with('/apps/myapp/.git').and_return(true)
326
+ File.stub!(:read).with('/apps/myapp/.git/config').and_return("[remote \"heroku\"]\n\turl = git@heroku.com:myapp.git")
327
+ @cli.heroku.should_receive(:info).with('myapp', true).and_return({ :stack2 => 'false' })
328
+ gitconfig = mock('git config')
329
+ File.should_receive(:open).with('/apps/myapp/.git/config', 'w').and_yield(gitconfig)
330
+ gitconfig.should_receive(:write).with("[remote \"heroku\"]\n\turl = git@herokugarden.com:myapp.git")
331
+ @cli.git_transition([])
332
+ end
333
+ end
334
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: herokugarden
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.2
5
+ platform: ruby
6
+ authors:
7
+ - Adam Wiggins
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-14 00:00:00 -08:00
13
+ default_executable: herokugarden
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rest-client
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.5"
24
+ version:
25
+ description: Client library and command-line tool to manage and deploy Rails apps on Heroku Garden.
26
+ email: feedback@heroku.com
27
+ executables:
28
+ - herokugarden
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - Rakefile
35
+ - bin/herokugarden
36
+ - lib/herokugarden
37
+ - lib/herokugarden/client.rb
38
+ - lib/herokugarden/command_line.rb
39
+ - lib/herokugarden.rb
40
+ - spec/base.rb
41
+ - spec/client_spec.rb
42
+ - spec/command_line_spec.rb
43
+ has_rdoc: true
44
+ homepage: http://herokugarden.com/
45
+ post_install_message: |+
46
+
47
+ ---> Installing herokugarden v0.4.2
48
+ ---> Migrate local checkouts using the git:transition command:
49
+
50
+ cd myapp/
51
+ herokugarden git:transition
52
+
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project: herokugarden
72
+ rubygems_version: 1.2.0
73
+ signing_key:
74
+ specification_version: 2
75
+ summary: Client library and CLI to deploy Rails apps on Heroku Garden.
76
+ test_files: []
77
+