gdocs_features 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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