gdocs_features 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +0 -0
- data/Rakefile +35 -0
- data/VERSION +1 -0
- data/bin/remotefeatures +7 -0
- data/cucumber.yml +2 -0
- data/features/patch_local_files_from_gdocs.feature +33 -0
- data/features/step_definitions/diff_steps.rb +19 -0
- data/features/step_definitions/google_steps.rb +19 -0
- data/features/step_definitions/local_steps.rb +16 -0
- data/features/support/env.rb +5 -0
- data/gdocs_features.gemspec +97 -0
- data/lib/difference.rb +31 -0
- data/lib/feature.rb +26 -0
- data/lib/feature_diff.rb +46 -0
- data/lib/file_feature.rb +37 -0
- data/lib/file_feature_store.rb +17 -0
- data/lib/google_authorization.rb +18 -0
- data/lib/google_docs_client.rb +20 -0
- data/lib/google_feature.rb +51 -0
- data/lib/google_feature_store.rb +44 -0
- data/lib/google_resource.rb +11 -0
- data/lib/remote_features/cli/main.rb +30 -0
- data/lib/remote_features/dialogue.rb +25 -0
- data/pkg/gdocs_features-0.1.0.gem +0 -0
- data/spec/cli_spec.rb +69 -0
- data/spec/dialogue_spec.rb +69 -0
- data/spec/feature_diff_spec.rb +80 -0
- data/spec/feature_reformat_spec.rb +40 -0
- data/spec/file_feature_spec.rb +23 -0
- data/spec/file_feature_store_spec.rb +33 -0
- data/spec/google_authorization_spec.rb +26 -0
- data/spec/google_docs_client_spec.rb +32 -0
- data/spec/google_feature_spec.rb +46 -0
- data/spec/google_feature_store_spec.rb +64 -0
- data/spec/google_resource_spec.rb +25 -0
- data/spec/integration_spec.rb +42 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/stubs/documents.atom.xml +36 -0
- data/spec/stubs/example.feature +16 -0
- data/spec/stubs/folders.atom.xml +69 -0
- data/spec/temp_file_system.rb +19 -0
- data/spec/temp_file_system_spec.rb +28 -0
- metadata +117 -0
data/README
ADDED
File without changes
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
require 'cucumber/rake/task'
|
4
|
+
|
5
|
+
desc "Run all examples"
|
6
|
+
Spec::Rake::SpecTask.new('examples') do |t|
|
7
|
+
t.spec_files = FileList['spec/**/*.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Run all features"
|
11
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
12
|
+
t.cucumber_opts = "--format pretty"
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Run pending features"
|
16
|
+
Cucumber::Rake::Task.new(:pending) do |t|
|
17
|
+
t.cucumber_opts = "--format pretty -e features/** features-pending"
|
18
|
+
end
|
19
|
+
|
20
|
+
begin
|
21
|
+
require 'jeweler'
|
22
|
+
Jeweler::Tasks.new do |gemspec|
|
23
|
+
gemspec.name = "gdocs_features"
|
24
|
+
gemspec.summary = "Cucumber features in gdocs"
|
25
|
+
gemspec.description = "Cucumber features merged locally from Google Docs"
|
26
|
+
gemspec.email = "jody@alkema.ca"
|
27
|
+
gemspec.homepage = "http://github.com/alkema/gdocs-features"
|
28
|
+
gemspec.authors = ["Josh Chisholm"]
|
29
|
+
end
|
30
|
+
Jeweler::GemcutterTasks.new
|
31
|
+
rescue LoadError
|
32
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
33
|
+
end
|
34
|
+
|
35
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/remotefeatures
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Add '.rb' to work around a bug in IronRuby's File#dirname
|
3
|
+
$:.unshift(File.dirname(__FILE__ + '.rb') + '/../lib') unless $:.include?(File.dirname(__FILE__ + '.rb') + '/../lib')
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'remote_features/cli/main'
|
7
|
+
RemoteFeatures::Cli::Main.execute(ARGV.dup)
|
data/cucumber.yml
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
Feature: Patch local files from Google docs
|
2
|
+
|
3
|
+
In order for non-technical stakeholders to specify user stories
|
4
|
+
As a developer
|
5
|
+
I want to merge in features from Google docs
|
6
|
+
|
7
|
+
Scenario: Diff features
|
8
|
+
|
9
|
+
Given I have a local feature store
|
10
|
+
And a google docs feature store
|
11
|
+
And the local feature store contains:
|
12
|
+
| path | version |
|
13
|
+
| fruit/yellow/banana.feature | 2009-02-20T00:36:34.402Z |
|
14
|
+
| fruit/green/apple.feature | some-new-version |
|
15
|
+
| fruit/yellow/pineapple.feature | some-old-version |
|
16
|
+
And the google docs feature store contains:
|
17
|
+
| path | version |
|
18
|
+
| fruit/yellow/banana.feature | 2009-02-20T00:36:34.402Z |
|
19
|
+
| fruit/yellow/pineapple.feature | 2009-02-20T01:12:36.130Z |
|
20
|
+
| fruit/green/grape.feature | 2009-02-21T10:27:16.971Z |
|
21
|
+
When I run the program
|
22
|
+
Then I should see the following changes:
|
23
|
+
| path | change |
|
24
|
+
| fruit/green/grape.feature | created |
|
25
|
+
| fruit/green/apple.feature | deleted |
|
26
|
+
| fruit/yellow/pineapple.feature | updated |
|
27
|
+
When I apply change 1
|
28
|
+
And I run the program
|
29
|
+
Then I should see the following changes:
|
30
|
+
| path | change |
|
31
|
+
| fruit/green/apple.feature | deleted |
|
32
|
+
| fruit/yellow/pineapple.feature | updated |
|
33
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'feature_diff'
|
2
|
+
require 'remote_features/cli/main'
|
3
|
+
|
4
|
+
When /^I run the program$/ do
|
5
|
+
RemoteFeatures::Cli::Main.execute([@local_dir, "restapitest:testrestapi@docs.google.com"])
|
6
|
+
end
|
7
|
+
|
8
|
+
Then /^I should see the following changes:$/ do | table |
|
9
|
+
table.hashes.each do | row |
|
10
|
+
difference = @diff[row['path']]
|
11
|
+
raise "#{row['path']} not present -- #{@diff.inspect}" if difference.nil?
|
12
|
+
difference.change_description.should == row['change']
|
13
|
+
end
|
14
|
+
@diff.differences.each do | path, difference |
|
15
|
+
if table.hashes.find { |h| h['path'] == path }.nil?
|
16
|
+
raise "Unexpected change to #{path} (#{difference.change_description})"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'google_feature_store'
|
2
|
+
require 'google_docs_client'
|
3
|
+
|
4
|
+
Given /a google docs feature store$/ do
|
5
|
+
@client = GoogleDocsClient.new("restapitest@googlemail.com", "testrestapi")
|
6
|
+
@remote_store = GoogleFeatureStore.new(@client)
|
7
|
+
end
|
8
|
+
|
9
|
+
Given /^the google docs feature store contains:$/ do | table |
|
10
|
+
features = @remote_store.features
|
11
|
+
features.size.should == table.hashes.size
|
12
|
+
table.hashes.each do | row |
|
13
|
+
matches = features.find_all { |f| f.path == row['path'] }
|
14
|
+
matches.size.should == 1
|
15
|
+
if matches.first.version != row['version']
|
16
|
+
raise "expected '#{row['path']}' to have version '#{row['version']}', got '#{matches.first.version}'"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'file_feature_store'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
Given /a local feature store$/ do
|
6
|
+
@local_dir = File.join(Dir.tmpdir, 'feature_store')
|
7
|
+
FileUtils.rm_rf @local_dir
|
8
|
+
Dir.mkdir(@local_dir)
|
9
|
+
@local_store = FileFeatureStore.new(@local_dir)
|
10
|
+
end
|
11
|
+
|
12
|
+
Given /^the local feature store contains:$/ do |table|
|
13
|
+
table.hashes.each do | row |
|
14
|
+
@local_store.create_feature(row['path'], row['version'], 'any old thing')
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{gdocs_features}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Josh Chisholm"]
|
12
|
+
s.date = %q{2010-02-22}
|
13
|
+
s.default_executable = %q{remotefeatures}
|
14
|
+
s.description = %q{Cucumber features merged locally from Google Docs}
|
15
|
+
s.email = %q{jody@alkema.ca}
|
16
|
+
s.executables = ["remotefeatures"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"README"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
"README",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"bin/remotefeatures",
|
25
|
+
"cucumber.yml",
|
26
|
+
"features/patch_local_files_from_gdocs.feature",
|
27
|
+
"features/step_definitions/diff_steps.rb",
|
28
|
+
"features/step_definitions/google_steps.rb",
|
29
|
+
"features/step_definitions/local_steps.rb",
|
30
|
+
"features/support/env.rb",
|
31
|
+
"gdocs_features.gemspec",
|
32
|
+
"lib/difference.rb",
|
33
|
+
"lib/feature.rb",
|
34
|
+
"lib/feature_diff.rb",
|
35
|
+
"lib/file_feature.rb",
|
36
|
+
"lib/file_feature_store.rb",
|
37
|
+
"lib/google_authorization.rb",
|
38
|
+
"lib/google_docs_client.rb",
|
39
|
+
"lib/google_feature.rb",
|
40
|
+
"lib/google_feature_store.rb",
|
41
|
+
"lib/google_resource.rb",
|
42
|
+
"lib/remote_features/cli/main.rb",
|
43
|
+
"lib/remote_features/dialogue.rb",
|
44
|
+
"pkg/gdocs_features-0.1.0.gem",
|
45
|
+
"spec/cli_spec.rb",
|
46
|
+
"spec/dialogue_spec.rb",
|
47
|
+
"spec/feature_diff_spec.rb",
|
48
|
+
"spec/feature_reformat_spec.rb",
|
49
|
+
"spec/file_feature_spec.rb",
|
50
|
+
"spec/file_feature_store_spec.rb",
|
51
|
+
"spec/google_authorization_spec.rb",
|
52
|
+
"spec/google_docs_client_spec.rb",
|
53
|
+
"spec/google_feature_spec.rb",
|
54
|
+
"spec/google_feature_store_spec.rb",
|
55
|
+
"spec/google_resource_spec.rb",
|
56
|
+
"spec/integration_spec.rb",
|
57
|
+
"spec/spec_helper.rb",
|
58
|
+
"spec/stubs/documents.atom.xml",
|
59
|
+
"spec/stubs/example.feature",
|
60
|
+
"spec/stubs/folders.atom.xml",
|
61
|
+
"spec/temp_file_system.rb",
|
62
|
+
"spec/temp_file_system_spec.rb"
|
63
|
+
]
|
64
|
+
s.homepage = %q{http://github.com/alkema/gdocs-features}
|
65
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
66
|
+
s.require_paths = ["lib"]
|
67
|
+
s.rubygems_version = %q{1.3.6}
|
68
|
+
s.summary = %q{Cucumber features in gdocs}
|
69
|
+
s.test_files = [
|
70
|
+
"spec/cli_spec.rb",
|
71
|
+
"spec/dialogue_spec.rb",
|
72
|
+
"spec/feature_diff_spec.rb",
|
73
|
+
"spec/feature_reformat_spec.rb",
|
74
|
+
"spec/file_feature_spec.rb",
|
75
|
+
"spec/file_feature_store_spec.rb",
|
76
|
+
"spec/google_authorization_spec.rb",
|
77
|
+
"spec/google_docs_client_spec.rb",
|
78
|
+
"spec/google_feature_spec.rb",
|
79
|
+
"spec/google_feature_store_spec.rb",
|
80
|
+
"spec/google_resource_spec.rb",
|
81
|
+
"spec/integration_spec.rb",
|
82
|
+
"spec/spec_helper.rb",
|
83
|
+
"spec/temp_file_system.rb",
|
84
|
+
"spec/temp_file_system_spec.rb"
|
85
|
+
]
|
86
|
+
|
87
|
+
if s.respond_to? :specification_version then
|
88
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
89
|
+
s.specification_version = 3
|
90
|
+
|
91
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
92
|
+
else
|
93
|
+
end
|
94
|
+
else
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
data/lib/difference.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
class Difference
|
2
|
+
attr_reader :local, :remote
|
3
|
+
|
4
|
+
def initialize(local, remote, local_store)
|
5
|
+
@local, @remote, @local_store = local, remote, local_store
|
6
|
+
end
|
7
|
+
|
8
|
+
def local_only?
|
9
|
+
!@local.nil? && @remote.nil?
|
10
|
+
end
|
11
|
+
|
12
|
+
def remote_only?
|
13
|
+
@local.nil? && !@remote.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def change_description
|
17
|
+
local_only? ? 'deleted' : remote_only? ? 'created' : 'updated'
|
18
|
+
end
|
19
|
+
|
20
|
+
def patch
|
21
|
+
if remote_only?
|
22
|
+
@local_store.create_feature(@remote.path, @remote.version, @remote.reformat)
|
23
|
+
elsif !local_only?
|
24
|
+
@local.patch_from(@remote)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def path
|
29
|
+
(@local || @remote).path
|
30
|
+
end
|
31
|
+
end
|
data/lib/feature.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
class Feature
|
2
|
+
def reformat
|
3
|
+
indent(body).join("\n")
|
4
|
+
end
|
5
|
+
|
6
|
+
def indent(lines)
|
7
|
+
result = []
|
8
|
+
tabs = ""
|
9
|
+
lines.map do |line|
|
10
|
+
if line =~ /^feature\:/i
|
11
|
+
result << line + "\n"
|
12
|
+
tabs = " "
|
13
|
+
elsif line =~ /^scenario\:/i
|
14
|
+
result << "\n " + line + "\n"
|
15
|
+
tabs = " "
|
16
|
+
else
|
17
|
+
result << tabs + line
|
18
|
+
end
|
19
|
+
end
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def body
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
end
|
data/lib/feature_diff.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'difference'
|
2
|
+
|
3
|
+
class FeatureDiff
|
4
|
+
attr_reader :differences
|
5
|
+
|
6
|
+
def initialize(local_store, remote_store)
|
7
|
+
@local_store, @remote_store = local_store, remote_store
|
8
|
+
@differences = {}
|
9
|
+
remote_features = Hash[*remote_store.features.map { | f | [f.path, f] }.flatten]
|
10
|
+
local_features = Hash[*local_store.features.map { | f | [f.path, f] }.flatten]
|
11
|
+
add_differences(local_features, remote_features)
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](path)
|
15
|
+
@differences[path]
|
16
|
+
end
|
17
|
+
|
18
|
+
def patch(indices=nil)
|
19
|
+
@differences.values.each_with_index do | difference, index |
|
20
|
+
if indices.nil? || indices.include?(index + 1)
|
21
|
+
difference.patch
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def add(local, remote)
|
29
|
+
@differences[(local || remote).path] = Difference.new(local, remote, @local_store)
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_differences(local_features, remote_features)
|
33
|
+
remote_features.each do | path, remote |
|
34
|
+
local = local_features[path]
|
35
|
+
if local
|
36
|
+
add(local, remote) unless local.version == remote.version
|
37
|
+
else
|
38
|
+
add(nil, remote)
|
39
|
+
end
|
40
|
+
local_features.delete(path)
|
41
|
+
end
|
42
|
+
local_features.each do | path, local |
|
43
|
+
add(local, nil)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/file_feature.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'feature'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
class FileFeature < Feature
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def initialize(dir, path)
|
8
|
+
@dir, @path = dir, path
|
9
|
+
@physical_path = File.join(dir, path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def title
|
13
|
+
File.basename(@path, ".feature")
|
14
|
+
end
|
15
|
+
|
16
|
+
def version
|
17
|
+
match = /#\s*version\s*:\s*(.+)/i.match(lines.first)
|
18
|
+
match && match[1].strip
|
19
|
+
end
|
20
|
+
|
21
|
+
def body
|
22
|
+
version ? lines[1..-1] : lines
|
23
|
+
end
|
24
|
+
|
25
|
+
def lines
|
26
|
+
File.readlines(@physical_path).map { |line| line.strip }.reject { |l| l == "" }
|
27
|
+
end
|
28
|
+
|
29
|
+
def write(contents, version)
|
30
|
+
FileUtils.mkdir_p(File.dirname(@physical_path))
|
31
|
+
File.open(@physical_path, 'w') { |f| f.write("#version: #{version}\n#{contents}") }
|
32
|
+
end
|
33
|
+
|
34
|
+
def patch_from(other)
|
35
|
+
write(other.reformat, other.version)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'file_feature'
|
2
|
+
|
3
|
+
class FileFeatureStore
|
4
|
+
def initialize(directory)
|
5
|
+
@directory = directory
|
6
|
+
end
|
7
|
+
|
8
|
+
def features
|
9
|
+
Dir.glob("#{@directory}/**/*.feature").map do | path |
|
10
|
+
FileFeature.new(@directory, path[(@directory.length+1)..-1])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_feature(path, version, contents)
|
15
|
+
FileFeature.new(@directory, path).write(contents, version)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
|
3
|
+
class GoogleAuthorization
|
4
|
+
|
5
|
+
def initialize(email, password)
|
6
|
+
@email, @password = email, password
|
7
|
+
end
|
8
|
+
|
9
|
+
def header
|
10
|
+
@header ||= "GoogleLogin auth=#{authorize}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def authorize
|
14
|
+
params = { "accountType" => "HOSTED_OR_GOOGLE", "Email" => @email, "Passwd" => @password, "service" => "writely" }
|
15
|
+
result = RestClient.post "https://www.google.com/accounts/ClientLogin", params
|
16
|
+
result.to_s[/Auth=(.*)/, 1]
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'google_authorization'
|
2
|
+
require 'google_resource'
|
3
|
+
|
4
|
+
class GoogleDocsClient
|
5
|
+
def initialize(email, password)
|
6
|
+
@auth = GoogleAuthorization.new(email, password)
|
7
|
+
end
|
8
|
+
|
9
|
+
def documents_feed
|
10
|
+
GoogleResource.new(@auth, "http://docs.google.com/feeds/documents/private/full", :accept => "application/atom+xml")
|
11
|
+
end
|
12
|
+
|
13
|
+
def document_body(uri)
|
14
|
+
GoogleResource.new(@auth, uri, :accept => "text/html")
|
15
|
+
end
|
16
|
+
|
17
|
+
def folders
|
18
|
+
GoogleResource.new(@auth, "http://docs.google.com/feeds/documents/private/full/-/folder?showfolders=true", :accept => "application/atom+xml")
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'feature'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
class GoogleFeature < Feature
|
5
|
+
|
6
|
+
def initialize(client, entry, folder)
|
7
|
+
@client, @entry, @folder = client, entry, folder
|
8
|
+
end
|
9
|
+
|
10
|
+
def title
|
11
|
+
underscore(camelize(@entry.css("title")[0].text))
|
12
|
+
end
|
13
|
+
|
14
|
+
def version
|
15
|
+
@version ||= @entry.css("updated")[0].text
|
16
|
+
end
|
17
|
+
|
18
|
+
def body
|
19
|
+
@body ||= remove_html(download_body).map {|line| line.strip}.reject { |l| l == "" }
|
20
|
+
end
|
21
|
+
|
22
|
+
def body_url
|
23
|
+
@entry.css("content").first.attributes["src"].value.gsub("justBody=false", "justBody=true")
|
24
|
+
end
|
25
|
+
|
26
|
+
def path
|
27
|
+
"#{@folder}/#{title}.feature"
|
28
|
+
end
|
29
|
+
|
30
|
+
def download_body
|
31
|
+
@client.document_body(body_url).get.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove_html(html_body)
|
35
|
+
text = Nokogiri::HTML(html_body).at('body').inner_html
|
36
|
+
text.gsub(" ", " ").gsub("<br>", "\n").gsub("\n\n", "\n").gsub(/<\/?[^>]*>/, "")
|
37
|
+
end
|
38
|
+
|
39
|
+
def underscore(camel_cased_word)
|
40
|
+
camel_cased_word.to_s.gsub(/::/, '/').
|
41
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
42
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
43
|
+
tr("-", "_").
|
44
|
+
downcase
|
45
|
+
end
|
46
|
+
|
47
|
+
def camelize(lower_case_and_underscored_word)
|
48
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'google_feature'
|
3
|
+
|
4
|
+
class GoogleFeatureStore
|
5
|
+
PARENT_FOLDER_REL = "http://schemas.google.com/docs/2007#parent"
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
def features
|
12
|
+
feed = @client.documents_feed.get.to_s
|
13
|
+
folder_map = folders
|
14
|
+
Nokogiri::HTML(feed).css("entry").map do | entry |
|
15
|
+
parent_links = entry.xpath("link[@rel='#{PARENT_FOLDER_REL}']/@href")
|
16
|
+
href = parent_links.first.text.to_s
|
17
|
+
folder = folder_map[href]
|
18
|
+
GoogleFeature.new(@client, entry, folder)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def folders
|
23
|
+
feed = @client.folders.get.to_s
|
24
|
+
doc = Nokogiri::HTML(feed)
|
25
|
+
map = {}
|
26
|
+
doc.css("entry").each do | entry |
|
27
|
+
map[entry.css("id").text] = folder_path(doc, entry)
|
28
|
+
end
|
29
|
+
map
|
30
|
+
end
|
31
|
+
|
32
|
+
def folder_path(doc, entry)
|
33
|
+
title = entry.css("title").text
|
34
|
+
parent = parent_folder_entry(doc, entry)
|
35
|
+
((parent ? folder_path(doc, parent) : '') + '/' + title).gsub(/^\//, '')
|
36
|
+
end
|
37
|
+
|
38
|
+
def parent_folder_entry(doc, entry)
|
39
|
+
parent_link = entry.css("link[rel='#{PARENT_FOLDER_REL}']").first
|
40
|
+
return nil if parent_link.nil?
|
41
|
+
doc.xpath("//entry[id='#{parent_link.attributes['href']}']").first
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
|
3
|
+
class GoogleResource
|
4
|
+
def initialize(auth, url, headers={})
|
5
|
+
@auth, @url, @headers = auth, url, headers
|
6
|
+
end
|
7
|
+
|
8
|
+
def get(additional_headers={})
|
9
|
+
RestClient.get @url, @headers.merge(additional_headers).merge("Authorization" => @auth.header)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'file_feature_store'
|
2
|
+
require 'google_feature_store'
|
3
|
+
require 'google_docs_client'
|
4
|
+
require 'remote_features/dialogue'
|
5
|
+
|
6
|
+
module RemoteFeatures
|
7
|
+
module Cli
|
8
|
+
class Main
|
9
|
+
def self.execute(args)
|
10
|
+
new(args, $stdin, $stdout).execute
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(args, input, output)
|
14
|
+
if args.length != 2 || args[1] !~ /^(.+)\:(.+)@(.+)$/
|
15
|
+
output.puts "Usage: remotefeatures <directory> <user:password@host>"
|
16
|
+
Kernel.exit
|
17
|
+
return
|
18
|
+
end
|
19
|
+
@local_store = FileFeatureStore.new(args[0])
|
20
|
+
m = /^(.+)\:(.+)@(.+)$/.match(args[1])
|
21
|
+
@remote_store = GoogleFeatureStore.new(GoogleDocsClient.new(m[1], m[2]))
|
22
|
+
@input, @output = input, output
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute
|
26
|
+
RemoteFeatures::Dialogue.new(@input, @output, @local_store, @remote_store).start
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'feature_diff'
|
2
|
+
|
3
|
+
module RemoteFeatures
|
4
|
+
class Dialogue
|
5
|
+
def initialize(input, output, local_store, remote_store)
|
6
|
+
@input, @output, @local_store, @remote_store = input, output, local_store, remote_store
|
7
|
+
end
|
8
|
+
|
9
|
+
def start
|
10
|
+
diff = FeatureDiff.new(@local_store, @remote_store)
|
11
|
+
differences = diff.differences
|
12
|
+
if differences.empty?
|
13
|
+
@output.puts "No changes. Local store is up-to-date."
|
14
|
+
Kernel.exit
|
15
|
+
end
|
16
|
+
i = 0
|
17
|
+
lines = diff.differences.values.map do |d|
|
18
|
+
"#{(i += 1)}) #{d.change_description} - #{d.path}"
|
19
|
+
end
|
20
|
+
@output.puts lines.join("\n")
|
21
|
+
@output.puts "Enter changes to apply, 'all' to apply all, or blank to exit"
|
22
|
+
diff.patch(@input.gets.strip.split(/\s+/).map { |e| e.to_i })
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
Binary file
|