gdocs_features 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|