cuki 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -47,15 +47,20 @@ You can also pull a single feature:
47
47
 
48
48
  == Options
49
49
 
50
- - SKIP_AUTOFORMAT to avoid reformatting features (runs over the whole features directory)
51
- - PEM=/path/to/something.pem.cer (for client certificates)
52
- - CER=/path/to/something.pem (for client certificates)
50
+ - --skip--autoformat to avoid reformatting features (runs over the whole features directory)
51
+
52
+ == Configuration
53
+
54
+ If your Confluence installation requires a client certificate, you can supply the paths for these:
55
+
56
+ - CER=/path/to/ca.pem.cer
57
+ - PEM=/path/to/something.pem
53
58
 
54
59
  == Known Issues and Limitations
55
60
 
56
- - Will only work with Confluence setups which have no password, or use client certificates for authentcation
61
+ - Will only work with Confluence setups which have no password, or use client certificates for authentication
57
62
  - Only provides one-way sync, i.e. you can't edit a file locally and push it to Confluence
58
- - Fails if the AC block is the last H1 section of the page
63
+ - Fails if the AC block is the last h1. section of the page
59
64
  - Should fail if no features found in container block
60
65
 
61
66
  == TODO
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.9
1
+ 0.0.10
data/cucumber.yml CHANGED
@@ -1,2 +1,2 @@
1
1
  ---
2
- default: '--require features/'
2
+ default: '--require features/ -t ~@pending'
data/cuki.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "cuki"
8
- s.version = "0.0.9"
8
+ s.version = "0.0.10"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Andy Waite"]
12
- s.date = "2011-10-18"
12
+ s.date = "2011-10-22"
13
13
  s.description = ""
14
14
  s.email = "andy@andywaite.com"
15
15
  s.executables = ["cuki"]
@@ -31,6 +31,7 @@ Gem::Specification.new do |s|
31
31
  "cucumber.yml",
32
32
  "cuki.gemspec",
33
33
  "cuki.yaml.sample",
34
+ "features/pull/error_handling.feature",
34
35
  "features/pull/pull.feature",
35
36
  "features/pull/pull_single.feature",
36
37
  "features/pull/splitting.feature",
@@ -43,9 +44,14 @@ Gem::Specification.new do |s|
43
44
  "features/step_defs/pull_steps.rb",
44
45
  "features/step_defs/push_steps.rb",
45
46
  "features/support/env.rb",
47
+ "lib/cleaner.rb",
48
+ "lib/confluence_page.rb",
46
49
  "lib/cuki.rb",
50
+ "lib/feature_file.rb",
51
+ "lib/link_builder.rb",
47
52
  "lib/pusher.rb",
48
53
  "lib/string_utils.rb",
54
+ "lib/test_bits.rb",
49
55
  "spec/cuki_spec.rb",
50
56
  "spec/spec_helper.rb"
51
57
  ]
@@ -0,0 +1,16 @@
1
+ Feature: Error Handling
2
+
3
+ Scenario: Missing action
4
+ When I run `cuki`
5
+ Then it should fail with:
6
+ """
7
+ No action given
8
+ """
9
+
10
+ Scenario: Missing config
11
+ Given no file named "config/cuki.yaml" exists
12
+ When I run `cuki pull`
13
+ Then it should fail with:
14
+ """
15
+ No config file found at config/cuki.yaml
16
+ """
@@ -1,20 +1,5 @@
1
1
  Feature: Pull
2
2
 
3
- Scenario: Missing action
4
- When I run `cuki`
5
- Then it should fail with:
6
- """
7
- No action given
8
- """
9
-
10
- Scenario: Missing config
11
- Given no file named "config/cuki.yaml" exists
12
- When I run `cuki pull`
13
- Then it should fail with:
14
- """
15
- No config file found at config/cuki.yaml
16
- """
17
-
18
3
  Scenario: Pull all features
19
4
  Given a file named "config/cuki.yaml" with:
20
5
  """
@@ -16,7 +16,7 @@ Feature: Splitting
16
16
  <input id="content-title" value="Product Management">
17
17
  <div id="markupTextarea">
18
18
  Pretext
19
-
19
+
20
20
  h1. Acceptance Criteria
21
21
 
22
22
  Something
@@ -41,8 +41,6 @@ Feature: Splitting
41
41
  """
42
42
  Feature: Add Product
43
43
 
44
-
45
-
46
44
  http://example.com/pages/viewpage.action?pageId=123#ProductManagement-AddProduct
47
45
 
48
46
  This feature describes adding a product
@@ -55,8 +53,6 @@ Feature: Splitting
55
53
  """
56
54
  Feature: Remove Product
57
55
 
58
-
59
-
60
56
  http://example.com/pages/viewpage.action?pageId=123#ProductManagement-RemoveProduct
61
57
 
62
58
  This feature describes removing a product
@@ -66,45 +62,83 @@ Feature: Splitting
66
62
 
67
63
  """
68
64
 
69
- @announce
70
- Scenario: Pull all features (specified container)
71
- Given a file named "config/cuki.yaml" with:
72
- """
73
- ---
74
- host: http://example.com
75
- container: !ruby/regexp '/h1\. \*Acceptance Criteria\*(.*)h1\./m'
76
- mappings:
77
- 123: features/products
78
- """
79
- And a Confluence page on "example.com" with id 123:
80
- """
81
- <input id="content-title" value="Product Management">
82
- <div id="markupTextarea">
83
- h1. *Acceptance Criteria*
65
+ @announce @focus
66
+ Scenario: Pull all features (specified container)
67
+ Given a file named "config/cuki.yaml" with:
68
+ """
69
+ ---
70
+ host: http://example.com
71
+ container: !ruby/regexp '/h1\. \*Acceptance Criteria\*/'
72
+ mappings:
73
+ 123: features/products
74
+ """
75
+ And a Confluence page on "example.com" with id 123:
76
+ """
77
+ <input id="content-title" value="Product Management">
78
+ <div id="markupTextarea">
79
+ h1. *Acceptance Criteria*
80
+
81
+ Something
84
82
 
85
- Something
83
+ h2. Add Product
86
84
 
87
- h2. Add Product
85
+ This feature describes adding a product
88
86
 
89
- This feature describes adding a product
87
+ h6. Scenario: Scenario A
90
88
 
91
- h6. Scenario: Scenario A
89
+ h1. Next Section
90
+ </div>
91
+ """
92
+ When I run `cuki pull --skip-autoformat --skip-header`
93
+ Then the file "features/products/add_product.feature" should contain exactly:
94
+ """
95
+ Feature: Add Product
92
96
 
93
- h1. Next Section
94
- </div>
95
- """
96
- When I run `cuki pull --skip-autoformat --skip-header`
97
- Then the file "features/products/add_product.feature" should contain exactly:
98
- """
99
- Feature: Add Product
97
+ http://example.com/pages/viewpage.action?pageId=123#ProductManagement-AddProduct
100
98
 
99
+ This feature describes adding a product
101
100
 
101
+ Scenario: Scenario A
102
102
 
103
- http://example.com/pages/viewpage.action?pageId=123#ProductManagement-AddProduct
104
103
 
105
- This feature describes adding a product
104
+ """
106
105
 
107
- Scenario: Scenario A
106
+ @announce
107
+ Scenario:Special Chars
108
+ Given a file named "config/cuki.yaml" with:
109
+ """
110
+ ---
111
+ host: http://example.com
112
+ mappings:
113
+ 123: features/products
114
+ """
115
+ And a Confluence page on "example.com" with id 123:
116
+ """
117
+ <input id="content-title" value="Product Management">
118
+ <div id="markupTextarea">
119
+ h1. Acceptance Criteria
120
+
121
+ Something
122
+
123
+ h2. Add/Remove Product
124
+
125
+ This feature describes adding a product
126
+
127
+ h6. Scenario: Scenario A
128
+
129
+ h1. Next Section
130
+ </div>
131
+ """
132
+ When I run `cuki pull --skip-autoformat --skip-header`
133
+ Then the file "features/products/add_remove_product.feature" should contain exactly:
134
+ """
135
+ Feature: Add/Remove Product
136
+
137
+ http://example.com/pages/viewpage.action?pageId=123#ProductManagement-Add%2FRemoveProduct
138
+
139
+ This feature describes adding a product
140
+
141
+ Scenario: Scenario A
108
142
 
109
143
 
110
- """
144
+ """
@@ -1,6 +1,6 @@
1
1
  Feature: Push
2
2
 
3
- @focus @pending
3
+ @pending
4
4
  Scenario: Push a feature with no scenarios
5
5
  Given a file named "config/cuki.yaml" with:
6
6
  """
data/lib/cleaner.rb ADDED
@@ -0,0 +1,32 @@
1
+ class Cleaner
2
+
3
+ def self.clean(content)
4
+
5
+ content.gsub!('&nbsp;', '')
6
+
7
+ # remove the double pipes used for table headers in Confluence
8
+ content.gsub!('||', '|')
9
+
10
+ # remove other noise
11
+ content.gsub!("\r\n", "\n")
12
+ content.gsub!("\\\\\n", '')
13
+ content.gsub!('\\', '')
14
+
15
+ # remove any unwanted headers
16
+ content.gsub!(/h\d\. (Scenario: .*)/, '\1')
17
+ content.gsub!(/h\d\. (Scenario Outline: .*)/, '\1')
18
+ content.gsub!(/h\d\. (Background: .*)/, '\1')
19
+
20
+ #Remove fancy quotes
21
+ content.gsub!('’', "'")
22
+ content.gsub!('‘', "'")
23
+ content.gsub!('“', '"')
24
+ content.gsub!('”', '"')
25
+
26
+ content.gsub!(/^#(.*)/, '-' + '\1')
27
+
28
+ content
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,17 @@
1
+ require 'nokogiri'
2
+
3
+ class ConfluencePage
4
+
5
+ def initialize(response)
6
+ @doc = Nokogiri(response)
7
+ end
8
+
9
+ def title
10
+ @doc.at('#content-title')[:value]
11
+ end
12
+
13
+ def content
14
+ CGI.unescapeHTML @doc.css('#markupTextarea').text
15
+ end
16
+
17
+ end
data/lib/cuki.rb CHANGED
@@ -1,17 +1,15 @@
1
1
  require 'rubygems'
2
2
  require 'httpclient'
3
- require 'nokogiri'
4
3
  require 'yaml'
5
4
  require 'CGI'
6
5
  require 'json'
7
6
  require 'parallel'
8
- require File.dirname(__FILE__) + '/string_utils.rb'
9
-
10
- # terrible hack
11
- if File.exist?('stubs.json') || File.exist?('stubs.json')
12
- require 'webmock'
13
- include WebMock::API
14
- end
7
+ require File.dirname(__FILE__) + '/string_utils'
8
+ require File.dirname(__FILE__) + '/link_builder'
9
+ require File.dirname(__FILE__) + '/cleaner'
10
+ require File.dirname(__FILE__) + '/confluence_page'
11
+ require File.dirname(__FILE__) + '/feature_file'
12
+ require File.dirname(__FILE__) + '/test_bits'
15
13
 
16
14
  class Cuki
17
15
 
@@ -49,9 +47,6 @@ class Cuki
49
47
  end
50
48
  end
51
49
  autoformat
52
- elsif 'push' == action
53
- configure_push_stubs
54
- Pusher.push file, @config
55
50
  else
56
51
  puts "Unknown action '#{action}"
57
52
  exit(1)
@@ -72,8 +67,14 @@ class Cuki
72
67
  exit(1)
73
68
  end
74
69
  @config = YAML::load( File.open( CONFIG_PATH ) )
75
- raise "Host not found in #{CONFIG_PATH}" unless @config["host"]
76
- raise "Mappings not found in #{CONFIG_PATH}" unless @config["mappings"]
70
+ unless @config["host"]
71
+ puts "Host not found in #{CONFIG_PATH}"
72
+ exit(1)
73
+ end
74
+ unless @config["mappings"]
75
+ puts "Mappings not found in #{CONFIG_PATH}"
76
+ exit(1)
77
+ end
77
78
  end
78
79
 
79
80
  def configure_http_client
@@ -84,37 +85,42 @@ class Cuki
84
85
 
85
86
  def pull_feature(id, filepath)
86
87
  @content = ''
87
- wiki_edit_link = @config['host'] + '/pages/editpage.action?pageId=' + id.to_s
88
- wiki_view_link = @config['host'] + '/pages/viewpage.action?pageId=' + id.to_s
88
+
89
+ link_builder = LinkBuilder.new(@config['host'])
90
+
91
+ wiki_edit_link = link_builder.edit(id)
92
+ wiki_view_link = link_builder.view(id)
93
+
89
94
  puts "Downloading #{wiki_edit_link}"
90
95
  response = @client.get wiki_edit_link
91
- doc = Nokogiri(response.body)
96
+
97
+ confluence_page = ConfluencePage.new(response.body)
92
98
 
93
- unless doc.at('#content-title')
99
+ unless confluence_page.content
94
100
  puts "Not a valid confluence page:"
95
- puts doc.to_s
101
+ puts response.body
96
102
  exit(1)
97
103
  end
98
104
 
99
-
100
105
  unless filepath.include?('.feature')
101
106
 
102
- @config['container'] ||= /h1\. Acceptance Criteria(.*)h1\./m
107
+ @config['container'] ||= /h1\. Acceptance Criteria/
103
108
 
104
- handle_multi doc, id
109
+ handle_multi response.body, id
105
110
  else
106
111
 
107
- @content += "Feature: " + doc.at('#content-title')[:value] + "\n\n"
108
- @content += "#{wiki_view_link}\n\n"
109
- @content += get_markup(doc)
112
+ feature_file = FeatureFile.new
113
+ feature_file.title = confluence_page.title
114
+ feature_file.link = wiki_view_link
115
+ feature_file.content = confluence_page.content
110
116
 
111
- clean
117
+ content = Cleaner.clean(feature_file.to_s)
112
118
 
113
- process_tags
119
+ content = process_tags content
114
120
 
115
- @content = generated_by + @content unless @skip_header
121
+ content = generated_by + content unless @skip_header
116
122
 
117
- save_file filepath
123
+ save_file content, filepath
118
124
  end
119
125
  end
120
126
 
@@ -126,28 +132,57 @@ class Cuki
126
132
  `cucumber -a . --dry-run -P` unless @skip_autoformat
127
133
  end
128
134
 
129
- def handle_multi doc, id
130
- feature_title_compressed = doc.at('#content-title')[:value].anchorize
135
+ def handle_multi response_body, id
136
+ confluence_page = ConfluencePage.new(response_body)
137
+
138
+ feature_title_compressed = confluence_page.title.anchorize
131
139
 
132
- @content += get_markup(doc)
140
+ @content += confluence_page.content
133
141
 
134
- clean
142
+ @content = Cleaner.clean(@content)
135
143
 
136
- acceptance_criteria_matches = @content.match(@config['container'])
137
- unless acceptance_criteria_matches
144
+ unless @content.match(@config['container'])
145
+ puts "Could not find acceptance criteria container"
146
+ exit(1)
147
+ end
148
+ acceptance_criteria_block = @content.split(@config['container']).last
149
+ if acceptance_criteria_block.match(/h1\./)
150
+ acceptance_criteria_block = acceptance_criteria_block.split(/h1\./).first
151
+ end
152
+ unless acceptance_criteria_block
138
153
  puts "Could not match #{@config['container']} in #{id}"
139
154
  exit(1)
140
155
  end
141
- acceptance_criteria = acceptance_criteria_matches[1]
142
- scenario_titles = acceptance_criteria.scan(/h2. (.*)/).flatten
143
- scenario_blocks = acceptance_criteria.split(/h2. .*/)
156
+ acceptance_criteria = acceptance_criteria_block
157
+ scenario_titles = acceptance_criteria.scan(/h2\. (.*)/).flatten
158
+ scenario_blocks = acceptance_criteria.split(/h2\. .*/)
144
159
  scenario_blocks.shift
145
160
 
146
161
  combined = {}
162
+ found = 0
147
163
  scenario_titles.each_with_index do |title, index|
148
164
  combined[title] = scenario_blocks[index].gsub(/h6. (.*)/, '\1')
165
+ found += 1
166
+ end
167
+ if 0 == found
168
+ puts "No scenarios found in doc #{id}"
169
+ exit(1)
149
170
  end
150
171
  combined.each do |title, content|
172
+
173
+ tags = []
174
+ if @config['tags']
175
+ @config['tags'].each_pair do |tag, snippet|
176
+ tags << "@#{tag}" if @content.include?(snippet)
177
+ end
178
+ end
179
+ unless tags.empty?
180
+ content = tags.join(' ') + "\n" + content
181
+ # tags.each do |tag|
182
+ # content.gsub!(@config['tags'][tag.gsub('@', '')], '')
183
+ # end
184
+ end
185
+
151
186
  scenario_title_compressed = title.anchorize
152
187
  feature_filename = title.parameterize
153
188
 
@@ -155,94 +190,45 @@ class Cuki
155
190
 
156
191
  FileUtils.mkdir_p(dirpath)
157
192
 
158
- fname = "#{dirpath}/#{feature_filename.gsub("\r", '')}.feature"
193
+ fname = "#{dirpath}/#{feature_filename.gsub("\r", '').parameterize}.feature"
159
194
  File.open(fname, 'w') do |f|
160
195
  puts "Writing #{fname}"
161
196
  f.write generated_by unless @skip_header
162
197
  f.write "Feature: #{title}\n\n"
163
198
  link = @config['host'] + "/pages/viewpage.action?pageId=#{id}##{feature_title_compressed}-#{scenario_title_compressed}"
164
- f.write "\n\n" + link
199
+ f.write link
165
200
  f.write content
166
201
  end
167
202
  end
168
203
  end
169
204
 
170
- def clean
171
-
172
- @content.gsub!('&nbsp;', '')
173
-
174
- # remove the double pipes used for table headers in Confluence
175
- @content.gsub!('||', '|')
176
-
177
- # remove other noise
178
- @content.gsub!("\r\n", "\n")
179
- @content.gsub!("\\\\\n", '')
180
- @content.gsub!('\\', '')
181
-
182
- # remove any unwanted headers
183
- @content.gsub!(/h\d\. (Scenario: .*)/, '\1')
184
- @content.gsub!(/h\d\. (Scenario Outline: .*)/, '\1')
185
- @content.gsub!(/h\d\. (Background: .*)/, '\1')
186
-
187
- #Remove fancy quotes
188
- @content.gsub!('’', "'")
189
- @content.gsub!('‘', "'")
190
- @content.gsub!('“', '"')
191
- @content.gsub!('”', '"')
192
-
193
- @content.gsub!(/^#(.*)/, '-' + '\1')
194
-
195
- end
196
-
197
- def process_tags
205
+ def process_tags(content)
198
206
  tags = []
199
207
  if @config['tags']
200
208
  @config['tags'].each_pair do |tag, snippet|
201
- tags << "@#{tag}" if @content.include?(snippet)
209
+ tags << "@#{tag}" if content.include?(snippet)
202
210
  end
203
211
  end
204
212
  unless tags.empty?
205
- @content = tags.join(' ') + "\n" + @content
213
+ content = tags.join(' ') + "\n" + content
206
214
  tags.each do |tag|
207
- @content.gsub!(@config['tags'][tag.gsub('@', '')], '')
215
+ content.gsub!(@config['tags'][tag.gsub('@', '')], '')
208
216
  end
209
217
  end
218
+ content
210
219
  end
211
220
 
212
- def save_file(filepath)
221
+ def save_file(content, filepath)
213
222
  dir_path = File.dirname(filepath)
214
223
 
215
224
  FileUtils.mkdir_p(dir_path) unless File.exists?(dir_path)
216
225
 
217
226
  File.open(filepath, 'w') do |f|
218
227
  puts "Writing #{filepath}"
219
- f.puts @content
228
+ f.puts content
220
229
  end
221
230
  end
222
-
223
- def configure_pull_stubs
224
- if File.exist?('stubs.json')
225
- stubs = JSON.parse(File.open('stubs.json').read)
226
- stubs.each_pair do |url, body|
227
- stub_request(:get, url).to_return(:status => 200, :body => body, :headers => {})
228
- end
229
- FileUtils.rm 'stubs.json'
230
- end
231
- end
232
-
233
- def configure_push_stubs
234
- if File.exist?('push_stubs.json')
235
- stubs = JSON.parse(File.open('push_stubs.json').read)
236
- stubs.each do |a|
237
- stub_request(:post, "http://mywiki/").
238
- with(:body => {"title" => a['title'], "content"=> "\n\n" + a['content']},
239
- :headers => {'Content-Type'=>'application/x-www-form-urlencoded'}).
240
- to_return(:status => 200, :body => "", :headers => {})
241
- end
242
- FileUtils.rm 'push_stubs.json'
243
- end
244
- end
245
-
231
+
246
232
  def validate_args args
247
233
  if args.empty?
248
234
  puts "No action given"
@@ -250,8 +236,4 @@ class Cuki
250
236
  end
251
237
  end
252
238
 
253
- def get_markup(doc)
254
- CGI.unescapeHTML(doc.css('#markupTextarea').text)
255
- end
256
-
257
239
  end
@@ -0,0 +1,18 @@
1
+ class FeatureFile
2
+
3
+ attr_accessor :title, :link, :content
4
+
5
+ def initialize
6
+ @title = nil
7
+ @link = nil
8
+ @content = nil
9
+ end
10
+
11
+ def to_s
12
+ result = "Feature: " + title + "\n\n"
13
+ result += "#{link}\n\n"
14
+ result += content
15
+ result
16
+ end
17
+
18
+ end
@@ -0,0 +1,15 @@
1
+ class LinkBuilder
2
+
3
+ def initialize(host)
4
+ @host = host
5
+ end
6
+
7
+ def edit(id)
8
+ @host + '/pages/editpage.action?pageId=' + id.to_s
9
+ end
10
+
11
+ def view(id)
12
+ @host + '/pages/viewpage.action?pageId=' + id.to_s
13
+ end
14
+
15
+ end
data/lib/string_utils.rb CHANGED
@@ -4,6 +4,6 @@ class String
4
4
  end
5
5
 
6
6
  def parameterize
7
- self.downcase.gsub(' ', '_')
7
+ self.downcase.gsub(/[^a-z0-9\-]/, '_')
8
8
  end
9
9
  end
data/lib/test_bits.rb ADDED
@@ -0,0 +1,15 @@
1
+ # terrible hack
2
+ if File.exist?('stubs.json') || File.exist?('stubs.json')
3
+ require 'webmock'
4
+ include WebMock::API
5
+ end
6
+
7
+ def configure_pull_stubs
8
+ if File.exist?('stubs.json')
9
+ stubs = JSON.parse(File.open('stubs.json').read)
10
+ stubs.each_pair do |url, body|
11
+ stub_request(:get, url).to_return(:status => 200, :body => body, :headers => {})
12
+ end
13
+ FileUtils.rm 'stubs.json'
14
+ end
15
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cuki
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
4
+ hash: 11
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 9
10
- version: 0.0.9
9
+ - 10
10
+ version: 0.0.10
11
11
  platform: ruby
12
12
  authors:
13
13
  - Andy Waite
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-10-18 00:00:00 Z
18
+ date: 2011-10-22 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  requirement: &id001 !ruby/object:Gem::Requirement
@@ -156,6 +156,7 @@ files:
156
156
  - cucumber.yml
157
157
  - cuki.gemspec
158
158
  - cuki.yaml.sample
159
+ - features/pull/error_handling.feature
159
160
  - features/pull/pull.feature
160
161
  - features/pull/pull_single.feature
161
162
  - features/pull/splitting.feature
@@ -168,9 +169,14 @@ files:
168
169
  - features/step_defs/pull_steps.rb
169
170
  - features/step_defs/push_steps.rb
170
171
  - features/support/env.rb
172
+ - lib/cleaner.rb
173
+ - lib/confluence_page.rb
171
174
  - lib/cuki.rb
175
+ - lib/feature_file.rb
176
+ - lib/link_builder.rb
172
177
  - lib/pusher.rb
173
178
  - lib/string_utils.rb
179
+ - lib/test_bits.rb
174
180
  - spec/cuki_spec.rb
175
181
  - spec/spec_helper.rb
176
182
  homepage: http://github.com/andyw8/cuki