methadone 1.0.0.rc5 → 1.0.0.rc6
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/methadone/cli.rb +2 -0
- data/lib/methadone/cli_logging.rb +3 -0
- data/lib/methadone/cucumber.rb +1 -1
- data/lib/methadone/error.rb +8 -1
- data/lib/methadone/execution_strategy/jvm.rb +2 -0
- data/lib/methadone/execution_strategy/mri.rb +2 -0
- data/lib/methadone/execution_strategy/open_3.rb +2 -0
- data/lib/methadone/execution_strategy/open_4.rb +2 -0
- data/lib/methadone/execution_strategy/rbx_open_4.rb +2 -0
- data/lib/methadone/exit_now.rb +18 -3
- data/lib/methadone/main.rb +34 -6
- data/lib/methadone/process_status.rb +45 -0
- data/lib/methadone/sh.rb +52 -29
- data/lib/methadone/version.rb +1 -1
- data/methadone.gemspec +10 -0
- data/test/test_main.rb +20 -0
- data/test/test_sh.rb +104 -3
- metadata +23 -47
- data/tutorial/.vimrc +0 -6
- data/tutorial/1_intro.md +0 -52
- data/tutorial/2_bootstrap.md +0 -174
- data/tutorial/3_ui.md +0 -336
- data/tutorial/4_happy_path.md +0 -405
- data/tutorial/5_more_features.md +0 -693
- data/tutorial/6_refactor.md +0 -220
- data/tutorial/7_logging_debugging.md +0 -274
- data/tutorial/8_conclusion.md +0 -11
- data/tutorial/code/.rvmrc +0 -1
- data/tutorial/code/fullstop/.gitignore +0 -5
- data/tutorial/code/fullstop/Gemfile +0 -4
- data/tutorial/code/fullstop/LICENSE.txt +0 -202
- data/tutorial/code/fullstop/README.rdoc +0 -23
- data/tutorial/code/fullstop/Rakefile +0 -31
- data/tutorial/code/fullstop/bin/fullstop +0 -43
- data/tutorial/code/fullstop/features/fullstop.feature +0 -40
- data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +0 -64
- data/tutorial/code/fullstop/features/support/env.rb +0 -22
- data/tutorial/code/fullstop/fullstop.gemspec +0 -28
- data/tutorial/code/fullstop/lib/fullstop.rb +0 -2
- data/tutorial/code/fullstop/lib/fullstop/repo.rb +0 -38
- data/tutorial/code/fullstop/lib/fullstop/version.rb +0 -3
- data/tutorial/code/fullstop/test/tc_something.rb +0 -7
- data/tutorial/en.utf-8.add +0 -18
- data/tutorial/toc.md +0 -27
data/tutorial/5_more_features.md
DELETED
@@ -1,693 +0,0 @@
|
|
1
|
-
# Adding Features
|
2
|
-
|
3
|
-
Our command-line app isn't very interesting at this point; it's more of a glorified shell script. Where Ruby and Methadone
|
4
|
-
really shine is when things start getting complex.
|
5
|
-
|
6
|
-
There's a lot of features we can add and error cases we can handle, for example:
|
7
|
-
|
8
|
-
* The app will blow up if the git repo is already cloned
|
9
|
-
* The app might blow up if the files are already symlinked
|
10
|
-
* The app won't symlink new files
|
11
|
-
* The app checks out your dotfiles repo in your home directory
|
12
|
-
* The app uses symlinks, but we might want copies instead
|
13
|
-
|
14
|
-
You can probably think of even more features. To demonstrate how Methadone works, we're going to add these features:
|
15
|
-
|
16
|
-
* a "force" switch that will blow away the git repo and re-clone it, called `--force`
|
17
|
-
* a "location" flag that will allow us to control where the repo gets cloned, called `--checkout-dir` (which we'll also make available via `-d` as a mnemonic for "directory". See [my book][clibook] for an in-depth discussion on why you should provide long-form options along with short-form options)
|
18
|
-
|
19
|
-
Since we're working outside-in, we'll first create the user interface by modifying our UI scenario:
|
20
|
-
|
21
|
-
```cucumber
|
22
|
-
Feature: Checkout dotfiles
|
23
|
-
In order to get my dotfiles onto a new computer
|
24
|
-
I want a one-command way to keep them up to date
|
25
|
-
So I don't have to do it myself
|
26
|
-
|
27
|
-
Scenario: Basic UI
|
28
|
-
When I get help for "fullstop"
|
29
|
-
Then the exit status should be 0
|
30
|
-
And the banner should be present
|
31
|
-
And there should be a one line summary of what the app does
|
32
|
-
And the banner should include the version
|
33
|
-
And the banner should document that this app takes options
|
34
|
-
And the banner should document that this app's arguments are:
|
35
|
-
|repo_url|which is required|
|
36
|
-
# vvv
|
37
|
-
And the following options should be documented:
|
38
|
-
| --force |
|
39
|
-
| --checkout-dir |
|
40
|
-
| -d |
|
41
|
-
# ^^^
|
42
|
-
|
43
|
-
Scenario: Happy Path
|
44
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
45
|
-
When I successfully run `fullstop file:///tmp/dotfiles.git`
|
46
|
-
Then the dotfiles should be checked out in the directory "~/dotfiles"
|
47
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
48
|
-
```
|
49
|
-
|
50
|
-
We've added the step "And the following options should be documented". This will allow us to write the code necessary to handle
|
51
|
-
these options. First, we'll run our tests to see that we have a failing scenario:
|
52
|
-
|
53
|
-
```sh
|
54
|
-
$ rake features
|
55
|
-
Feature: Checkout dotfiles
|
56
|
-
In order to get my dotfiles onto a new computer
|
57
|
-
I want a one-command way to keep them up to date
|
58
|
-
So I don't have to do it myself
|
59
|
-
|
60
|
-
Scenario: Basic UI
|
61
|
-
When I get help for "fullstop"
|
62
|
-
Then the exit status should be 0
|
63
|
-
And the banner should be present
|
64
|
-
And there should be a one line summary of what the app does
|
65
|
-
And the banner should include the version
|
66
|
-
And the banner should document that this app takes options
|
67
|
-
And the banner should document that this app's arguments are:
|
68
|
-
| repo_url | which is required |
|
69
|
-
And the following options should be documented:
|
70
|
-
| --force |
|
71
|
-
| --checkout-dir |
|
72
|
-
| -d |
|
73
|
-
expected: /^\s*--force\s+\w\w\w+/m
|
74
|
-
got: "Usage: fullstop [options] repo_url\n\nManages dotfiles from a git repo\n\nv0.0.1\n\nOptions:\n --version Show help/version info\n --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)\n (Default: info)\n" (using =~)
|
75
|
-
Diff:
|
76
|
-
@@ -1,2 +1,11 @@
|
77
|
-
-/^\s*--force\s+\w\w\w+/m
|
78
|
-
+Usage: fullstop [options] repo_url
|
79
|
-
+
|
80
|
-
+Manages dotfiles from a git repo
|
81
|
-
+
|
82
|
-
+v0.0.1
|
83
|
-
+
|
84
|
-
+Options:
|
85
|
-
+ --version Show help/version info
|
86
|
-
+ --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
|
87
|
-
+ (Default: info)
|
88
|
-
(RSpec::Expectations::ExpectationNotMetError)
|
89
|
-
features/fullstop.feature:15:in `And the following options should be documented:'
|
90
|
-
|
91
|
-
Scenario: Happy Path
|
92
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
93
|
-
When I successfully run `fullstop file:///tmp/dotfiles.git`
|
94
|
-
Then the dotfiles should be checked out in the directory "~/dotfiles"
|
95
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
96
|
-
|
97
|
-
Failing Scenarios:
|
98
|
-
cucumber features/fullstop.feature:6
|
99
|
-
|
100
|
-
2 scenarios (1 failed, 1 passed)
|
101
|
-
12 steps (1 failed, 11 passed)
|
102
|
-
0m0.574s
|
103
|
-
rake aborted!
|
104
|
-
Cucumber failed
|
105
|
-
|
106
|
-
Tasks: TOP => features
|
107
|
-
(See full trace by running task with --trace)
|
108
|
-
```
|
109
|
-
|
110
|
-
As you can see, our tests are failing, because the documentation of the existence of our command-line option couldn't be found in
|
111
|
-
the help output.
|
112
|
-
|
113
|
-
To make this test pass in an `OptionParser`-driven application, you might write something like this:
|
114
|
-
|
115
|
-
```ruby
|
116
|
-
options = {
|
117
|
-
'checkout-dir' => ENV['HOME'],
|
118
|
-
}
|
119
|
-
parser = OptionParser.new do |opts|
|
120
|
-
opts.on("--force","Force overwriting existing files") do
|
121
|
-
options[:force] = true
|
122
|
-
end
|
123
|
-
opts.on("-d DIR","--checkout-dir",
|
124
|
-
"Set the location of the checkout dir",
|
125
|
-
"(default: #{options['checkout-dir']})") do |dir|
|
126
|
-
options['checkout-dir'] = dir
|
127
|
-
end
|
128
|
-
end
|
129
|
-
parser.parse!
|
130
|
-
```
|
131
|
-
|
132
|
-
As we learned earlier, Methadone manages an instance of `OptionParser` for us (and calls `parse!` in its `go!` method), so we could reduce this code to:
|
133
|
-
|
134
|
-
```ruby
|
135
|
-
options = {
|
136
|
-
'checkout-dir' => ENV['HOME'],
|
137
|
-
}
|
138
|
-
opts.on("--force","Force overwriting existing files") do
|
139
|
-
options[:force] = true
|
140
|
-
end
|
141
|
-
opts.on("-d DIR","--checkout-dir",
|
142
|
-
"Set the location of the checkout dir",
|
143
|
-
"(default: #{options['checkout-dir']})") do |dir|
|
144
|
-
options['checkout-dir'] = dir
|
145
|
-
end
|
146
|
-
```
|
147
|
-
|
148
|
-
Methadone *also* manages an options hash for us, so that it can be made available to the `main` block. It's available via the
|
149
|
-
method `options`. Methadone also provides a method `on` that delegates all of its arguments to the underlying `OptionParser`'s
|
150
|
-
`on` method. With both of these in mind, we could further reduce the code to:
|
151
|
-
|
152
|
-
```ruby
|
153
|
-
options['checkout-dir'] = ENV['HOME']
|
154
|
-
on("--force","Force overwriting existing files") do
|
155
|
-
options[:force] = true
|
156
|
-
end
|
157
|
-
on("-d DIR","--checkout-dir",
|
158
|
-
"Set the location of the checkout dir",
|
159
|
-
"(default: #{options['checkout-dir']})") do |dir|
|
160
|
-
options['checkout-dir'] = dir
|
161
|
-
end
|
162
|
-
```
|
163
|
-
|
164
|
-
This is still pretty tedious:
|
165
|
-
|
166
|
-
* Both blocks to `on` just set the value in the `options` hash.
|
167
|
-
* We have to include the default value of `checkout-dir` in the documentation string.
|
168
|
-
|
169
|
-
For apps with a lot of options, this can be a real pain to maintain. Methadone has us covered. The `on` method has more smarts
|
170
|
-
than just delegating to `OptionParser`. Specifically:
|
171
|
-
|
172
|
-
* If you do *not* pass a block to `on`, it will provide `OptionParser` a block that sets the value of the command-line option inside the `options` hash.
|
173
|
-
* If there is a default value for your flag (option that takes an argument), it will be included in the help string automatically.
|
174
|
-
|
175
|
-
In other words, we can reduce our option parsing code to these three lines:
|
176
|
-
|
177
|
-
```ruby
|
178
|
-
options['checkout-dir'] = ENV['HOME']
|
179
|
-
|
180
|
-
on("--force","Force overwriting of existing files")
|
181
|
-
on("-d DIR","--checkout-dir","Set the location of the checkout dir")
|
182
|
-
```
|
183
|
-
|
184
|
-
That's it! 14 lines become 3.
|
185
|
-
When `main` executes, the following keys in `options` will be available:
|
186
|
-
|
187
|
-
* `"force"` - true if the user specified `--force`
|
188
|
-
* `:force` - the same
|
189
|
-
* `"d"` - the value of the checkout dir (as given to `-d` or `--checkout-dir`), or the default, i.e. never `nil`
|
190
|
-
* `:d` - the same
|
191
|
-
* `"checkout-dir"` - the same
|
192
|
-
* `:'checkout-dir'` - the same
|
193
|
-
|
194
|
-
Notice that each flag is available as a `String` or `Symbol` and that all forms of each option are present in the hash, meaning
|
195
|
-
you can refer to the options in whichever way makes the code most readable.
|
196
|
-
|
197
|
-
Coming back to our app, let's add this code and see if our test passes. Here's what `bin/fullstop` looks like now:
|
198
|
-
|
199
|
-
```ruby
|
200
|
-
#!/usr/bin/env ruby
|
201
|
-
|
202
|
-
require 'optparse'
|
203
|
-
require 'methadone'
|
204
|
-
require 'fullstop'
|
205
|
-
require 'fileutils'
|
206
|
-
|
207
|
-
class App
|
208
|
-
include Methadone::Main
|
209
|
-
include Methadone::CLILogging
|
210
|
-
include Methadone::SH
|
211
|
-
|
212
|
-
main do |repo_url|
|
213
|
-
|
214
|
-
# vvv
|
215
|
-
Dir.chdir options['checkout-dir'] do
|
216
|
-
# ^^^
|
217
|
-
sh "git clone #{repo_url}"
|
218
|
-
basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
219
|
-
Dir.entries(basedir).each do |file|
|
220
|
-
next if file == '.' || file == '..' || file == '.git'
|
221
|
-
FileUtils.ln_s file,'.'
|
222
|
-
end
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
|
-
version Fullstop::VERSION
|
227
|
-
|
228
|
-
description 'Manages dotfiles from a git repo'
|
229
|
-
|
230
|
-
# vvv
|
231
|
-
options['checkout-dir'] = ENV['HOME']
|
232
|
-
on("--force","Overwrite files if they exist")
|
233
|
-
on("-d DIR","--checkout-dir","Where to clone the repo")
|
234
|
-
# ^^^
|
235
|
-
|
236
|
-
arg :repo_url, "URL to the git repository containing your dotfiles"
|
237
|
-
|
238
|
-
use_log_level_option
|
239
|
-
|
240
|
-
go!
|
241
|
-
end
|
242
|
-
```
|
243
|
-
|
244
|
-
We've highlighted the lines we changed. Note that, because we added the option to change the checkout dir, we started using it
|
245
|
-
inside `main`. If we wanted to be very strict in our TDD, we would've written a test for that, but that's a bit outside the
|
246
|
-
scope of Methadone (feel free to do this on your own, however!).
|
247
|
-
|
248
|
-
Now, we can see that our test passes, *and* our other scenario doesn't fail, meaning we didn't introduce any bugs.
|
249
|
-
|
250
|
-
```sh
|
251
|
-
$ rake features
|
252
|
-
Feature: Checkout dotfiles
|
253
|
-
In order to get my dotfiles onto a new computer
|
254
|
-
I want a one-command way to keep them up to date
|
255
|
-
So I don't have to do it myself
|
256
|
-
|
257
|
-
Scenario: Basic UI
|
258
|
-
When I get help for "fullstop"
|
259
|
-
Then the exit status should be 0
|
260
|
-
And the banner should be present
|
261
|
-
And there should be a one line summary of what the app does
|
262
|
-
And the banner should include the version
|
263
|
-
And the banner should document that this app takes options
|
264
|
-
And the banner should document that this app's arguments are:
|
265
|
-
| repo_url | which is required |
|
266
|
-
And the following options should be documented:
|
267
|
-
| --force |
|
268
|
-
| --checkout-dir |
|
269
|
-
| -d |
|
270
|
-
|
271
|
-
Scenario: Happy Path
|
272
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
273
|
-
When I successfully run `fullstop file:///tmp/dotfiles.git`
|
274
|
-
Then the dotfiles should be checked out in the directory "~/dotfiles"
|
275
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
276
|
-
|
277
|
-
2 scenarios (2 passed)
|
278
|
-
12 steps (12 passed)
|
279
|
-
0m0.398s
|
280
|
-
```
|
281
|
-
|
282
|
-
We can also see the UI that's generated by this small amount of code:
|
283
|
-
|
284
|
-
```sh
|
285
|
-
$ bundle exec bin/fullstop --help
|
286
|
-
Usage: fullstop [options] repo_url
|
287
|
-
|
288
|
-
Manages dotfiles from a git repo
|
289
|
-
|
290
|
-
v0.0.1
|
291
|
-
|
292
|
-
Options:
|
293
|
-
--version Show help/version info
|
294
|
-
--force Overwrite files if they exist
|
295
|
-
-d, --checkout-dir DIR Where to clone the repo
|
296
|
-
(default: /Users/davec)
|
297
|
-
--log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
|
298
|
-
(Default: info)
|
299
|
-
```
|
300
|
-
|
301
|
-
Take a moment to reflect on everything you're getting. We specify only the names of options and their description, and Methadone
|
302
|
-
handles the rest. *And*, you can avoid all of this magic entirely, if you really need to, since you have access to the
|
303
|
-
`OptionParser` instance via the `opts` method. You get all of the power of `OptionParser`, but without any framework lock-in.
|
304
|
-
|
305
|
-
For completeness, let's go ahead an implement the two features now that we have the UI in place. To do this, we'll create two
|
306
|
-
new scenarios.
|
307
|
-
|
308
|
-
First, let's implement the `--force` flag with the following scneario:
|
309
|
-
|
310
|
-
```cucumber
|
311
|
-
Scenario: Force overwrite
|
312
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
313
|
-
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
314
|
-
And there's a new file in the git repo
|
315
|
-
When I run `fullstop --force file:///tmp/dotfiles.git`
|
316
|
-
Then the dotfiles in "~/dotfiles" should be re-cloned
|
317
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
318
|
-
```
|
319
|
-
|
320
|
-
Several of these steps aren't there, so we'll have to implement them ourselves. Rather than get wrapped up into
|
321
|
-
that, let's focus on getting our app to pass this scenario. Supposing these steps are implemented, we now have a failing
|
322
|
-
scenario:
|
323
|
-
|
324
|
-
```sh
|
325
|
-
$ rake features
|
326
|
-
Feature: Checkout dotfiles
|
327
|
-
In order to get my dotfiles onto a new computer
|
328
|
-
I want a one-command way to keep them up to date
|
329
|
-
So I don't have to do it myself
|
330
|
-
|
331
|
-
Scenario: Basic UI
|
332
|
-
When I get help for "fullstop"
|
333
|
-
Then the exit status should be 0
|
334
|
-
And the banner should be present
|
335
|
-
And there should be a one line summary of what the app does
|
336
|
-
And the banner should include the version
|
337
|
-
And the banner should document that this app takes options
|
338
|
-
And the banner should document that this app's arguments are:
|
339
|
-
| repo_url | which is required |
|
340
|
-
And the following options should be documented:
|
341
|
-
| --force |
|
342
|
-
| --checkout-dir |
|
343
|
-
| -d |
|
344
|
-
|
345
|
-
Scenario: Happy Path
|
346
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
347
|
-
When I successfully run `fullstop file:///tmp/dotfiles.git`
|
348
|
-
Then the dotfiles should be checked out in the directory "~/dotfiles"
|
349
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
350
|
-
|
351
|
-
Scenario: Force overwrite
|
352
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
353
|
-
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
354
|
-
And there's a new file in the git repo
|
355
|
-
When I run `fullstop --force file:///tmp/dotfiles.git`
|
356
|
-
Then the dotfiles in "~/dotfiles" should be re-cloned
|
357
|
-
expected: true
|
358
|
-
got: false (using ==) (RSpec::Expectations::ExpectationNotMetError)
|
359
|
-
./features/step_definitions/fullstop_steps.rb:35:in `block (3 levels) in <top (required)>'
|
360
|
-
./features/step_definitions/fullstop_steps.rb:33:in `each'
|
361
|
-
./features/step_definitions/fullstop_steps.rb:33:in `block (2 levels) in <top (required)>'
|
362
|
-
./features/step_definitions/fullstop_steps.rb:32:in `chdir'
|
363
|
-
./features/step_definitions/fullstop_steps.rb:32:in `/^the dotfiles should be checked out in the directory "([^"]*)"$/'
|
364
|
-
features/fullstop.feature:31:in `Then the dotfiles in "~/dotfiles" should be re-cloned'
|
365
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
366
|
-
|
367
|
-
Failing Scenarios:
|
368
|
-
cucumber features/fullstop.feature:26
|
369
|
-
|
370
|
-
3 scenarios (1 failed, 2 passed)
|
371
|
-
18 steps (1 failed, 1 skipped, 16 passed)
|
372
|
-
0m0.703s
|
373
|
-
rake aborted!
|
374
|
-
Cucumber failed
|
375
|
-
|
376
|
-
Tasks: TOP => features
|
377
|
-
(See full trace by running task with --trace)
|
378
|
-
```
|
379
|
-
|
380
|
-
We're failing because the new file we added to our repo after the initial clone can't be found. It's likely that our second clone failed, but we didn't notice, because we aren't checking. If we run our app manually, we can see that errors are flying, but we're ignoring them:
|
381
|
-
|
382
|
-
```sh
|
383
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
|
384
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
|
385
|
-
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
386
|
-
Output of 'git clone file:///tmp/dotfiles.git':
|
387
|
-
Error running 'git clone file:///tmp/dotfiles.git'
|
388
|
-
File exists - (.bashrc, ./.bashrc)
|
389
|
-
$ echo $?
|
390
|
-
70
|
391
|
-
```
|
392
|
-
|
393
|
-
We can see that error output is being produced from `git`, but we're ignoring it. `fullstop` fails later in the process when we ask it to symlink files that already exist. This is actually a bug, so let's take a short detour and fix this problem. When doing TDD, it's important to know how your app is failing, so you can be confident that the code you are about to write fixes the correct failing in the existing app.
|
394
|
-
|
395
|
-
We'll write a scenario to reveal the bug:
|
396
|
-
|
397
|
-
```cucumber
|
398
|
-
Scenario: Fail if directory is cloned
|
399
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
400
|
-
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
401
|
-
And there's a new file in the git repo
|
402
|
-
When I run `fullstop file:///tmp/dotfiles.git`
|
403
|
-
Then the exit status should not be 0
|
404
|
-
And the stderr should contain "checkout dir already exists, use --force to overwrite"
|
405
|
-
```
|
406
|
-
|
407
|
-
Now, we can see that it doesn't pass:
|
408
|
-
```sh
|
409
|
-
$ rake features
|
410
|
-
Feature: Checkout dotfiles
|
411
|
-
In order to get my dotfiles onto a new computer
|
412
|
-
I want a one-command way to keep them up to date
|
413
|
-
So I don't have to do it myself
|
414
|
-
|
415
|
-
Scenario: Basic UI
|
416
|
-
When I get help for "fullstop"
|
417
|
-
Then the exit status should be 0
|
418
|
-
And the banner should be present
|
419
|
-
And there should be a one line summary of what the app does
|
420
|
-
And the banner should include the version
|
421
|
-
And the banner should document that this app takes options
|
422
|
-
And the banner should document that this app's arguments are:
|
423
|
-
| repo_url | which is required |
|
424
|
-
And the following options should be documented:
|
425
|
-
| --force |
|
426
|
-
| --checkout-dir |
|
427
|
-
| -d |
|
428
|
-
|
429
|
-
Scenario: Happy Path
|
430
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
431
|
-
When I successfully run `fullstop file:///tmp/dotfiles.git`
|
432
|
-
Then the dotfiles should be checked out in the directory "~/dotfiles"
|
433
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
434
|
-
|
435
|
-
Scenario: Fail if directory is cloned
|
436
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
437
|
-
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
438
|
-
And there's a new file in the git repo
|
439
|
-
When I run `fullstop file:///tmp/dotfiles.git`
|
440
|
-
Then the exit status should not be 0
|
441
|
-
And the stderr should contain "checkout dir already exists, uses --force to overwrite"
|
442
|
-
expected "W, [2012-02-12T15:12:35.720851 #43530] WARN -- : Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.\nW, [2012-02-12T15:12:35.720964 #43530] WARN -- : Error running 'git clone file:///tmp/dotfiles.git'\nE, [2012-02-12T15:12:35.721503 #43530] ERROR -- : File exists - (.bashrc, ./.bashrc)\n" to include "checkout dir already exists, uses --force to overwrite"
|
443
|
-
Diff:
|
444
|
-
@@ -1,2 +1,4 @@
|
445
|
-
-["checkout dir already exists, uses --force to overwrite"]
|
446
|
-
+W, [2012-02-12T15:12:35.720851 #43530] WARN -- : Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
447
|
-
+W, [2012-02-12T15:12:35.720964 #43530] WARN -- : Error running 'git clone file:///tmp/dotfiles.git'
|
448
|
-
+E, [2012-02-12T15:12:35.721503 #43530] ERROR -- : File exists - (.bashrc, ./.bashrc)
|
449
|
-
(RSpec::Expectations::ExpectationNotMetError)
|
450
|
-
features/fullstop.feature:32:in `And the stderr should contain "checkout dir already exists, uses --force to overwrite"'
|
451
|
-
|
452
|
-
Failing Scenarios:
|
453
|
-
cucumber features/fullstop.feature:26
|
454
|
-
|
455
|
-
3 scenarios (1 failed, 2 passed)
|
456
|
-
18 steps (1 failed, 17 passed)
|
457
|
-
0m0.694s
|
458
|
-
rake aborted!
|
459
|
-
Cucumber failed
|
460
|
-
|
461
|
-
Tasks: TOP => features
|
462
|
-
(See full trace by running task with --trace)
|
463
|
-
```
|
464
|
-
|
465
|
-
It looks like `fullstop` is writing log messages. It is, and we'll talk about that more later, but right now, we need to focus on the fact that we aren't producing the error message we expect. Let's modify `bin/fullstop` to check that the call to `git` succeeded. `sh` returns the exit status of the command it calls, so we can use that to fix things.
|
466
|
-
|
467
|
-
Here's the changes we'll make to `bin/fullstop` to check for this:
|
468
|
-
|
469
|
-
```ruby
|
470
|
-
#!/usr/bin/env ruby
|
471
|
-
|
472
|
-
require 'optparse'
|
473
|
-
require 'methadone'
|
474
|
-
require 'fullstop'
|
475
|
-
require 'fileutils'
|
476
|
-
|
477
|
-
class App
|
478
|
-
include Methadone::Main
|
479
|
-
include Methadone::CLILogging
|
480
|
-
include Methadone::SH
|
481
|
-
|
482
|
-
main do |repo_url|
|
483
|
-
|
484
|
-
Dir.chdir options['checkout-dir'] do
|
485
|
-
# vvv
|
486
|
-
if sh("git clone #{repo_url}") == 0
|
487
|
-
# ^^^
|
488
|
-
basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
489
|
-
Dir.entries(basedir).each do |file|
|
490
|
-
next if file == '.' || file == '..' || file == '.git'
|
491
|
-
FileUtils.ln_s file,'.'
|
492
|
-
end
|
493
|
-
else
|
494
|
-
# vvv
|
495
|
-
exit_now!("checkout dir already exists, use --force to overwrite")
|
496
|
-
# ^^^
|
497
|
-
end
|
498
|
-
end
|
499
|
-
end
|
500
|
-
|
501
|
-
version Fullstop::VERSION
|
502
|
-
|
503
|
-
description 'Manages dotfiles from a git repo'
|
504
|
-
|
505
|
-
options['checkout-dir'] = ENV['HOME']
|
506
|
-
on("--force","Overwrite files if they exist")
|
507
|
-
on("-d DIR","--checkout-dir","Where to clone the repo")
|
508
|
-
|
509
|
-
arg :repo_url, "URL to the git repository containing your dotfiles"
|
510
|
-
|
511
|
-
use_log_level_option
|
512
|
-
|
513
|
-
go!
|
514
|
-
end
|
515
|
-
```
|
516
|
-
|
517
|
-
Since an exit of zero is considered success, we branch on that value, and call the method `exit_now!`, provided by Methadone, to
|
518
|
-
stop the app with an error. The argument is an error message to
|
519
|
-
print to the standard error to let the user know why the app stopped abnormally. The app will then stop and exit nonzero. If
|
520
|
-
you want to customize the exit code, you can provide it as the first argument, with the message being the second argument.
|
521
|
-
|
522
|
-
As you can see, our test now passes:
|
523
|
-
|
524
|
-
```sh
|
525
|
-
$ rake features
|
526
|
-
Feature: Checkout dotfiles
|
527
|
-
In order to get my dotfiles onto a new computer
|
528
|
-
I want a one-command way to keep them up to date
|
529
|
-
So I don't have to do it myself
|
530
|
-
|
531
|
-
Scenario: Basic UI
|
532
|
-
When I get help for "fullstop"
|
533
|
-
Then the exit status should be 0
|
534
|
-
And the banner should be present
|
535
|
-
And there should be a one line summary of what the app does
|
536
|
-
And the banner should include the version
|
537
|
-
And the banner should document that this app takes options
|
538
|
-
And the banner should document that this app's arguments are:
|
539
|
-
| repo_url | which is required |
|
540
|
-
And the following options should be documented:
|
541
|
-
| --force |
|
542
|
-
| --checkout-dir |
|
543
|
-
| -d |
|
544
|
-
|
545
|
-
Scenario: Happy Path
|
546
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
547
|
-
When I successfully run `fullstop file:///tmp/dotfiles.git`
|
548
|
-
Then the dotfiles should be checked out in the directory "~/dotfiles"
|
549
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
550
|
-
|
551
|
-
Scenario: Fail if directory is cloned
|
552
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
553
|
-
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
554
|
-
And there's a new file in the git repo
|
555
|
-
When I run `fullstop file:///tmp/dotfiles.git`
|
556
|
-
Then the exit status should not be 0
|
557
|
-
And the stderr should contain "checkout dir already exists, use --force to overwrite"
|
558
|
-
|
559
|
-
3 scenarios (3 passed)
|
560
|
-
18 steps (18 passed)
|
561
|
-
0m0.789s
|
562
|
-
```
|
563
|
-
|
564
|
-
Note that there is a companion method to `sh`, called `sh!` that will throw an exception if the underlying command it calls fails. In a Methadone app, any unhandled exception will trigger a nonzero exit from the app, and show the user the message of the exception that caused the exit. We can customize the message of the exception thrown from `sh!`, and thus our change to our app could also be implemented like so:
|
565
|
-
|
566
|
-
```ruby
|
567
|
-
main do |repo_url|
|
568
|
-
|
569
|
-
Dir.chdir options['checkout-dir'] do
|
570
|
-
# vvv
|
571
|
-
sh! git clone #{repo_url}", :on_fail => "checkout dir already exists, use --force to overwrite"
|
572
|
-
# ^^^
|
573
|
-
basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
574
|
-
Dir.entries(basedir).each do |file|
|
575
|
-
next if file == '.' || file == '..' || file == '.git'
|
576
|
-
FileUtils.ln_s file,'.'
|
577
|
-
end
|
578
|
-
end
|
579
|
-
end
|
580
|
-
```
|
581
|
-
|
582
|
-
Which method to use is purely stylistic and up to you.
|
583
|
-
|
584
|
-
NOW, we can get back to the `--force` flag. We're going to change our scenario a bit, as well. Instead of using "When I run `fullstop --force file:///tmp/dotfiles.git`" we'll use "When I successfully run `fullstop --force file:///tmp/dotfiles.git`", which will fail if the app exits nonzero. This will cause our scenario to fail earlier.
|
585
|
-
|
586
|
-
To fix this, we'll change the code in `bin/fullstop` so that if the user specified `--force`, we'll delete the directory before we clone. We'll also need to delete the files that were symlinked in the home directory as well.
|
587
|
-
|
588
|
-
```ruby
|
589
|
-
#!/usr/bin/env ruby
|
590
|
-
|
591
|
-
require 'optparse'
|
592
|
-
require 'methadone'
|
593
|
-
require 'fullstop'
|
594
|
-
require 'fileutils'
|
595
|
-
|
596
|
-
class App
|
597
|
-
include Methadone::Main
|
598
|
-
include Methadone::CLILogging
|
599
|
-
include Methadone::SH
|
600
|
-
|
601
|
-
main do |repo_url|
|
602
|
-
|
603
|
-
Dir.chdir options['checkout-dir'] do
|
604
|
-
basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
605
|
-
# vvv
|
606
|
-
if options[:force] && Dir.exists?(basedir)
|
607
|
-
warn "deleting #{basedir} before cloning"
|
608
|
-
FileUtils.rm_rf basedir
|
609
|
-
end
|
610
|
-
# ^^^
|
611
|
-
if sh("git clone #{repo_url}") == 0
|
612
|
-
Dir.entries(basedir).each do |file|
|
613
|
-
next if file == '.' || file == '..' || file == '.git'
|
614
|
-
source_file = File.join(basedir,file)
|
615
|
-
# vvv
|
616
|
-
FileUtils.rm(file) if File.exists?(file) && options[:force]
|
617
|
-
# ^^^
|
618
|
-
FileUtils.ln_s source_file,'.'
|
619
|
-
end
|
620
|
-
else
|
621
|
-
exit_now!("checkout dir already exists, use --force to overwrite")
|
622
|
-
end
|
623
|
-
end
|
624
|
-
end
|
625
|
-
|
626
|
-
version Fullstop::VERSION
|
627
|
-
|
628
|
-
description 'Manages dotfiles from a git repo'
|
629
|
-
|
630
|
-
options['checkout-dir'] = ENV['HOME']
|
631
|
-
on("--force","Overwrite files if they exist")
|
632
|
-
on("-d DIR","--checkout-dir","Where to clone the repo")
|
633
|
-
|
634
|
-
arg :repo_url, "URL to the git repository containing your dotfiles"
|
635
|
-
|
636
|
-
use_log_level_option
|
637
|
-
|
638
|
-
go!
|
639
|
-
end
|
640
|
-
```
|
641
|
-
|
642
|
-
Now, we can see that our scenario passes:
|
643
|
-
|
644
|
-
```sh
|
645
|
-
$ rake features
|
646
|
-
Feature: Checkout dotfiles
|
647
|
-
In order to get my dotfiles onto a new computer
|
648
|
-
I want a one-command way to keep them up to date
|
649
|
-
So I don't have to do it myself
|
650
|
-
|
651
|
-
Scenario: Basic UI
|
652
|
-
When I get help for "fullstop"
|
653
|
-
Then the exit status should be 0
|
654
|
-
And the banner should be present
|
655
|
-
And there should be a one line summary of what the app does
|
656
|
-
And the banner should include the version
|
657
|
-
And the banner should document that this app takes options
|
658
|
-
And the banner should document that this app's arguments are:
|
659
|
-
| repo_url | which is required |
|
660
|
-
And the following options should be documented:
|
661
|
-
| --force |
|
662
|
-
| --checkout-dir |
|
663
|
-
| -d |
|
664
|
-
|
665
|
-
Scenario: Happy Path
|
666
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
667
|
-
When I successfully run `fullstop file:///tmp/dotfiles.git`
|
668
|
-
Then the dotfiles should be checked out in the directory "~/dotfiles"
|
669
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
670
|
-
|
671
|
-
Scenario: Fail if directory is cloned
|
672
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
673
|
-
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
674
|
-
And there's a new file in the git repo
|
675
|
-
When I run `fullstop file:///tmp/dotfiles.git`
|
676
|
-
Then the exit status should not be 0
|
677
|
-
And the stderr should contain "checkout dir already exists, use --force to overwrite"
|
678
|
-
|
679
|
-
Scenario: Force overwrite
|
680
|
-
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
681
|
-
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
682
|
-
And there's a new file in the git repo
|
683
|
-
When I successfully run `fullstop --force file:///tmp/dotfiles.git`
|
684
|
-
Then the dotfiles in "~/dotfiles" should be re-cloned
|
685
|
-
And the files in "~/dotfiles" should be symlinked in my home directory
|
686
|
-
|
687
|
-
4 scenarios (4 passed)
|
688
|
-
24 steps (24 passed)
|
689
|
-
0m1.171s
|
690
|
-
```
|
691
|
-
|
692
|
-
We're starting to see a few features of Methadone that need some explanation, such as what those log messages are, and how the
|
693
|
-
output of our commands is getting there. But first, our `main` method is also becoming pretty messy. Since Methadone allows and encourages you to write your app cleanly, we can refactor that code into classes that live inside `lib`. We'll see how to do that in the next section.
|