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.
Files changed (43) hide show
  1. data/README +0 -0
  2. data/Rakefile +35 -0
  3. data/VERSION +1 -0
  4. data/bin/remotefeatures +7 -0
  5. data/cucumber.yml +2 -0
  6. data/features/patch_local_files_from_gdocs.feature +33 -0
  7. data/features/step_definitions/diff_steps.rb +19 -0
  8. data/features/step_definitions/google_steps.rb +19 -0
  9. data/features/step_definitions/local_steps.rb +16 -0
  10. data/features/support/env.rb +5 -0
  11. data/gdocs_features.gemspec +97 -0
  12. data/lib/difference.rb +31 -0
  13. data/lib/feature.rb +26 -0
  14. data/lib/feature_diff.rb +46 -0
  15. data/lib/file_feature.rb +37 -0
  16. data/lib/file_feature_store.rb +17 -0
  17. data/lib/google_authorization.rb +18 -0
  18. data/lib/google_docs_client.rb +20 -0
  19. data/lib/google_feature.rb +51 -0
  20. data/lib/google_feature_store.rb +44 -0
  21. data/lib/google_resource.rb +11 -0
  22. data/lib/remote_features/cli/main.rb +30 -0
  23. data/lib/remote_features/dialogue.rb +25 -0
  24. data/pkg/gdocs_features-0.1.0.gem +0 -0
  25. data/spec/cli_spec.rb +69 -0
  26. data/spec/dialogue_spec.rb +69 -0
  27. data/spec/feature_diff_spec.rb +80 -0
  28. data/spec/feature_reformat_spec.rb +40 -0
  29. data/spec/file_feature_spec.rb +23 -0
  30. data/spec/file_feature_store_spec.rb +33 -0
  31. data/spec/google_authorization_spec.rb +26 -0
  32. data/spec/google_docs_client_spec.rb +32 -0
  33. data/spec/google_feature_spec.rb +46 -0
  34. data/spec/google_feature_store_spec.rb +64 -0
  35. data/spec/google_resource_spec.rb +25 -0
  36. data/spec/integration_spec.rb +42 -0
  37. data/spec/spec_helper.rb +5 -0
  38. data/spec/stubs/documents.atom.xml +36 -0
  39. data/spec/stubs/example.feature +16 -0
  40. data/spec/stubs/folders.atom.xml +69 -0
  41. data/spec/temp_file_system.rb +19 -0
  42. data/spec/temp_file_system_spec.rb +28 -0
  43. 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
@@ -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,2 @@
1
+ default: --format progress features
2
+ pending: --format pretty features-pending
@@ -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,5 @@
1
+ lib_path = File.expand_path("#{File.dirname(__FILE__)}/../../lib")
2
+ $LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
3
+
4
+ require 'rubygems'
5
+ require 'spec'
@@ -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
@@ -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
@@ -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("&nbsp;", " ").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