couchdiff 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gemtest ADDED
File without changes
data/History.txt ADDED
@@ -0,0 +1,9 @@
1
+ === 0.0.2 / 2012-01-15
2
+
3
+ * Bugfix to maintain attachment order when copying
4
+
5
+ === 0.0.1 / 2011-12-14
6
+
7
+ * Initial release
8
+
9
+ * Support for db.all_docs or db.view to limit scope of diff (e.g. "Users/all")
data/Manifest.txt ADDED
@@ -0,0 +1,9 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.md
4
+ Rakefile
5
+ bin/couchdiff
6
+ lib/couchdiff.rb
7
+ lib/copier.rb
8
+ spec/copier_spec.rb
9
+ spec/couchdiff_spec.rb
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # couchdiff
2
+
3
+ * http://github.com/mudynamics/couchdiff
4
+
5
+ ## DESCRIPTION
6
+
7
+ * Diff/Merge utility to compare & synchronize two database, using couchrest. As this tool does not use the CouchDB replication protocol it can be used to transfer data between incompatible CouchDB versions and allows you to move data when replication fails.
8
+
9
+ ## FEATURES
10
+
11
+ * Compare all docs or only docs matching a design view (must be present in both source and dst)
12
+ * Pluggable comparison logic (by default couchdiff compares all fields excluding "_rev" and "rev_pos")
13
+ * Some initial optimizations to avoid copying attachments that did not change.
14
+
15
+ ## SYNOPSIS
16
+
17
+ * Usage: couchdiff [options] src-url dst-url
18
+ * s, --scope [SCOPE] name of couch view (e.g. User/all) used to limit scope of diff/merge.
19
+ * -f, --field [FIELD] json field used to compare docs (Default: Compare all fields except "_rev")
20
+ * -a, --apply Apply changes to destination DB (add, update and delete docs to match source DB)
21
+ * -c, --compact Compact destination DB after changes are applied.
22
+ * -v, --verbose List documents that were added, updated or deleted.
23
+ * -h, --help Display this screen
24
+
25
+ ## REQUIREMENTS
26
+
27
+ * couchrest, progressbar, json or json_pure
28
+ * hoe (to build the gem)
29
+
30
+ ## INSTALL
31
+
32
+ * Via rubyforge: `gem install couchdiff`
33
+ * Local installation: `rake install_gem`
34
+
35
+ ## TODOs
36
+
37
+ * Optimize attachment copying speed
38
+
39
+ ## LICENSE
40
+
41
+ (The MIT License)
42
+
43
+ Copyright (c) 2012 Mu Dynamics
44
+
45
+ Permission is hereby granted, free of charge, to any person obtaining
46
+ a copy of this software and associated documentation files (the
47
+ 'Software'), to deal in the Software without restriction, including
48
+ without limitation the rights to use, copy, modify, merge, publish,
49
+ distribute, sublicense, and/or sell copies of the Software, and to
50
+ permit persons to whom the Software is furnished to do so, subject to
51
+ the following conditions:
52
+
53
+ The above copyright notice and this permission notice shall be
54
+ included in all copies or substantial portions of the Software.
55
+
56
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
57
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
58
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
59
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
60
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
61
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
62
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/couchdiff.rb'
6
+ require './lib/copier.rb'
7
+
8
+ Hoe.plugin :bundler
9
+ Hoe.spec('couchdiff') do |p|
10
+ p.version = "0.0.2"
11
+ p.developer('Jens Schmidt (Mu Dynamics)', 'jens@mudynamics.com')
12
+ p.rubyforge_name = 'couchdiff'
13
+ p.author = 'Jens Schmidt'
14
+ p.url = 'http://github.com/mudynamics/couchdiff'
15
+ p.extra_deps << ['couchrest', '~> 1.1.2']
16
+ p.extra_deps << ['progressbar', '~> 0.9']
17
+ p.extra_deps << ['json', '~> 1.6.0']
18
+ p.extra_dev_deps << ['rspec','~> 2.7.0']
19
+ end
data/bin/couchdiff ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+ require 'couchrest'
3
+ require 'couchdiff'
4
+ require 'copier'
5
+ require 'optparse'
6
+ require 'uri'
7
+
8
+ options = {}
9
+
10
+ optparse = OptionParser.new do|opts|
11
+ opts.banner = "Usage: couchdiff [options] src_url dst_url"
12
+
13
+ opts.on( '-s', '--scope [SCOPE]', 'Name of CouchDB View (e.g. User/all) used to limit scope of diff.' ) do|value|
14
+ options[:scope] = value
15
+ end
16
+
17
+ opts.on( '-f', '--field [FIELD]', 'JSON field used to compare docs (Default: Compare all fields except "_rev")' ) do|value|
18
+ options[:field] = value
19
+ end
20
+
21
+ opts.on( '-a', '--apply', 'Apply changes to destination DB (add, update and delete docs to match source DB)' ) do|value|
22
+ options[:apply] = value
23
+ end
24
+
25
+ opts.on( '-c', '--compact', 'Compact destination DB after changes are applied.' ) do
26
+ options[:compact] = true
27
+ end
28
+
29
+ opts.on( '-v', '--verbose', 'List documents that were added, updated or deleted.' ) do
30
+ options[:verbose] = true
31
+ end
32
+
33
+ opts.on( '--version', 'Print couchdiff version and exit.' ) do
34
+ options[:version] = true
35
+ end
36
+
37
+ opts.on_tail( '-h', '--help', 'Display this screen' ) do
38
+ puts opts
39
+ exit
40
+ end
41
+ end
42
+
43
+ optparse.parse!(ARGV)
44
+
45
+ # print version
46
+ (puts "Version: "+ CouchDiff::VERSION; exit) if (options[:version])
47
+
48
+ def error msg
49
+ puts "ERROR: #{msg}"
50
+ exit
51
+ end
52
+
53
+ FROM, TO = ARGV
54
+
55
+ # check arguments
56
+ (puts optparse; exit) if FROM == TO # handles null and same db cases
57
+ error("Database URLs (src and dst) are required") unless (FROM && TO)
58
+ FROM[URI.regexp] or error "Invalid src url syntax '#{FROM}'"
59
+ TO[URI.regexp] or error "Invalid dst url syntax '#{TO}'"
60
+
61
+ SCOPE = options[:scope]
62
+
63
+ puts "Generating diff\n between #{FROM}\n and #{TO}\n limited to '#{SCOPE ? SCOPE : 'all_docs'}'"
64
+
65
+ RE_SERVER = /(^.*)\//
66
+ RE_DB = /\w+$/
67
+
68
+ src_couch = CouchRest.new FROM[RE_SERVER, 1]
69
+ src_db = src_couch.database FROM[RE_DB]
70
+
71
+ dst_couch = CouchRest.new TO[RE_SERVER, 1]
72
+ dst_db = dst_couch.database TO[RE_DB]
73
+
74
+ # use a couch view as diff scope if one is provided, otherwise all_docs
75
+ fetcher = proc{|db| SCOPE ?
76
+ (db.view(SCOPE, :include_docs => true) rescue error "View '#{SCOPE}' does not exist in DB at #{db}"):
77
+ db.all_docs(:include_docs => true)
78
+ }
79
+
80
+ src_docs = fetcher.yield(src_db)['rows']
81
+ dst_docs = fetcher.yield(dst_db)['rows']
82
+
83
+ COMPARE_FIELD = options[:field]
84
+ if COMPARE_FIELD
85
+ puts "-> Using '#{COMPARE_FIELD}' to compare document versions."
86
+ diff = CouchDiff.new src_docs, dst_docs do |src, dst|
87
+ src[COMPARE_FIELD] && src[COMPARE_FIELD] != dst[COMPARE_FIELD]
88
+ end
89
+ else
90
+ diff = CouchDiff.new src_docs, dst_docs
91
+ end
92
+
93
+ VERBOSE = options[:verbose]
94
+
95
+ puts "ADDED: #{diff.added.size}"
96
+ VERBOSE && (puts diff.added * "\n")
97
+
98
+ puts "DELETED: #{diff.deleted.size}"
99
+ VERBOSE && (puts diff.deleted * "\n")
100
+
101
+ puts "UPDATED: #{diff.updated.size}"
102
+ VERBOSE && (puts diff.updated_as_diff * "\n")
103
+
104
+ puts "UNCHANGED: #{diff.unchanged.size}"
105
+ # VERBOSE && (puts diff.unchanged * "\n")
106
+
107
+ options[:apply] && diff.patch(src_db, dst_db)
108
+
109
+ options[:compact] && (puts "Compacting..."; dst_db.compact! )
data/lib/copier.rb ADDED
@@ -0,0 +1,87 @@
1
+ require 'couchrest'
2
+
3
+ class Copier
4
+ def initialize src_db, dst_db
5
+ @src_db, @dst_db = src_db, dst_db
6
+ end
7
+
8
+ # copy src_doc and all attachments, dst_doc is the current doc in the dst_db
9
+ def copy src, old_dst = nil
10
+
11
+ # get source doc if src is a string (key)
12
+ src_doc = src.is_a?(String) ? @src_db.get(src) :
13
+ (src.is_a?(Hash) ? CouchRest::Document.new(src) : src)
14
+ src_doc.database = @src_db # in case it's a hash, not a CouchRest::Document
15
+
16
+ raise "document not found '#{src}'" if src_doc.nil?
17
+
18
+ new_dst = src_doc.clone
19
+
20
+ attachments_changed = false
21
+
22
+ # if src has attachments, check whether they have changed or not
23
+ if src_doc['_attachments']
24
+ if (old_dst.nil? || old_dst["_attachments"] != src_doc['_attachments']) # attachments have changed
25
+ attachments_changed = true
26
+ # remove attachments from dst_doc - they are added back later
27
+ new_dst.delete "_attachments"
28
+ end
29
+ end
30
+
31
+ # set the rev to the previous rev or remove it if new
32
+ old_dst ? (new_dst["_rev"] = old_dst["_rev"]) : (new_dst.delete "_rev")
33
+
34
+ if attachments_changed
35
+ begin
36
+ # saving the doc removes all old attachments (if any)
37
+ # using regular save
38
+ @dst_db.save_doc(new_dst)
39
+ # copy attachments one by one
40
+ src_doc['_attachments'].keys.each {|file| copy_attachment(src_doc, new_dst, file)}
41
+
42
+ # attachment order might not be retained - reload doc again to make sure
43
+ reloaded_doc = @dst_db.get new_dst['_id']
44
+ if (reloaded_doc['_attachments'].keys != src_doc['_attachments'].keys)
45
+ puts "correcting attachment order..."
46
+ ordered_attachments = src_doc['_attachments'].keys.inject({}) do |memo, key|
47
+ memo[key] = reloaded_doc['_attachments'][key]
48
+ memo
49
+ end
50
+ reloaded_doc['_attachments'] = ordered_attachments
51
+ @dst_db.save_doc(reloaded_doc)
52
+ end
53
+ rescue RestClient::Conflict
54
+ puts "ERROR: document version conflict, skipping..."
55
+ return
56
+ end
57
+ else
58
+ # use bulk save if attachments have not changed
59
+ @dst_db.bulk_save_doc(new_dst)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def copy_attachment src_doc, dst_doc, filename
66
+ size = src_doc['_attachments'][filename]['length'].to_i
67
+ # puts " reading attachment '#{filename}' (#{format_size size}) ..."
68
+ data = src_doc.fetch_attachment filename
69
+ # puts " writing attachment '#{filename}' (#{format_size size}) ...'"
70
+ content_type = src_doc['_attachments'][filename]['content_type']
71
+ dst_doc.put_attachment filename, data, {
72
+ :raw => true,
73
+ :content_type => content_type,
74
+ "Content-Encoding" => "gzip",
75
+ "Accept-Encoding" => "gzip"
76
+ }
77
+ end
78
+
79
+ def format_size size_int
80
+ units = %w{B KB MB GB TB}
81
+ e = (Math.log(size_int)/Math.log(1024)).floor
82
+ s = "%.1f" % (size_int.to_f / 1024**e)
83
+ s.sub(/\.?0*$/, units[e])
84
+ end
85
+
86
+ end
87
+
data/lib/couchdiff.rb ADDED
@@ -0,0 +1,135 @@
1
+ require 'progressbar'
2
+ require 'json/ext'
3
+
4
+ # add "diff" method to Hash class. Exlude keys are used to exempt one or more keys (e.g. '_rev')
5
+ class Hash
6
+ def diff(other, keys_to_exclude = [])
7
+
8
+ # check that values for all keys in this and other hash or the same
9
+ result = ((self.keys - keys_to_exclude) + (other.keys - keys_to_exclude)).uniq.inject({}) do |memo, key|
10
+
11
+ # if we have hashes on both sides...
12
+ if self[key].kind_of?(Hash) && other[key].kind_of?(Hash)
13
+ memo[key] = self[key].diff(other[key], keys_to_exclude)
14
+ else
15
+ # compare values directly (arrays or simple Strings, Numbers, Objects etc.)
16
+ memo[key] = [self[key], other[key]] unless (self[key] == other[key])
17
+ end
18
+ # get rid of empty hashes
19
+ memo.delete_if { |k, v| v.kind_of?(Hash) && v.empty? }
20
+ memo
21
+ end
22
+
23
+ # Check that both hashes contain the same keys (in the same order)
24
+ # uses special identifier "_keys_changed"
25
+ result[:_keys_changed] = [self.keys, other.keys] if (self.keys != other.keys)
26
+ result
27
+ end
28
+ end
29
+
30
+
31
+ class CouchDiff
32
+
33
+ VERSION = '0.0.2'
34
+ EXCLUDED_KEYS = ['_rev','revpos']
35
+
36
+ attr_reader :added
37
+ attr_reader :updated
38
+ attr_reader :deleted
39
+ attr_reader :unchanged
40
+
41
+
42
+ def initialize src_docs, dst_docs, &changed
43
+ # default behavior is to compare all fields besides the _rev and revpos field
44
+ changed ||= proc{ |doc1, doc2| doc1.diff(doc2, EXCLUDED_KEYS).size > 0 }
45
+
46
+ @added, @updated, @deleted, @unchanged = [], [], [], []
47
+
48
+ dst_doc_map, src_doc_map = {}, {}
49
+
50
+ # create hash maps for fast lookup
51
+ src_docs.each {|doc| src_doc_map[doc['id']] = doc; assert_doc(doc)}
52
+ dst_docs.each {|doc| dst_doc_map[doc['id']] = doc; assert_doc(doc)}
53
+
54
+ # updated/added docs
55
+ src_docs.each do |doc|
56
+ id = doc['id']
57
+ if !(dst_doc = dst_doc_map[id]).nil?
58
+ if changed.yield(doc['doc'], dst_doc['doc']) || attachments_changed(doc['doc'], dst_doc['doc'])
59
+ @updated << [doc, dst_doc]
60
+ else
61
+ @unchanged << doc
62
+ end
63
+ else
64
+ @added << doc
65
+ end
66
+ end
67
+
68
+ # deleted docs
69
+ dst_docs.each do |doc|
70
+ id = doc['id']
71
+ @deleted << doc unless src_doc_map[id]
72
+ end
73
+ end
74
+
75
+ # determine if there are any added/deleted/modified attachments between src and dst
76
+ def attachments_changed src_doc, dst_doc
77
+ (src_doc['_attachments'] || {}).diff(dst_doc['_attachments'] || {}, EXCLUDED_KEYS).length > 0
78
+ end
79
+
80
+ def patch src_db, dst_db, copier = Copier.new(src_db, dst_db)
81
+
82
+ p_bar = ProgressBar.new('Adding docs', @added.size)
83
+ # ADD new docs
84
+ until @added.empty?
85
+ doc_ids = (@added.shift(20)).map {|src| src['id'] }
86
+ src_docs = src_db.get_bulk(doc_ids)['rows']
87
+ src_docs.each do |src_doc|
88
+ puts "[Adding #{src_doc['doc']['_id']}]"
89
+ copier.copy src_doc['doc'], nil
90
+ p_bar.inc
91
+ end
92
+ end
93
+ p_bar.finish
94
+
95
+ p_bar = ProgressBar.new('Updating docs', @updated.size)
96
+
97
+ # UPDATE docs (if they changed)
98
+ until @updated.empty?
99
+ # process 25 docs at a time
100
+ doc_ids = (@updated.shift(25)).map {|src, dst| src['id'] }
101
+ src_docs = src_db.get_bulk(doc_ids)['rows']
102
+ dst_docs = dst_db.get_bulk(doc_ids)['rows']
103
+
104
+ # update dst_docs content
105
+ src_docs.each_with_index do |src_doc, index|
106
+ dst_doc = dst_docs[index]
107
+
108
+ # puts "[Updating #{src_doc['doc']['_id']}]"
109
+ copier.copy src_doc['doc'], dst_doc['doc']
110
+ p_bar.inc
111
+ end
112
+ end
113
+ dst_db.bulk_save # flush any pending docs
114
+ p_bar.finish
115
+
116
+
117
+ # REMOVE deleted docs
118
+ p_bar = ProgressBar.new('Deleting docs', @deleted.size)
119
+ dst_db.bulk_save @deleted.map { |doc| {"_id" => doc["id"], "_rev" => doc['doc']['_rev'], "_deleted" => true } } unless @deleted.empty?
120
+ p_bar.finish
121
+
122
+ end
123
+
124
+ def updated_as_diff
125
+ @updated.map {|doc1, doc2| doc1['doc'].diff(doc2['doc'], EXCLUDED_KEYS).merge({'id' => doc1['id']}) }
126
+ end
127
+
128
+ private
129
+
130
+ # verify that doc is valid
131
+ def assert_doc doc
132
+ raise "document '#{doc['id']}' does not contain a 'doc' element. Did you set :include_docs => true?\n#{doc}" unless doc['doc']
133
+ end
134
+
135
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
@@ -0,0 +1,131 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+ require 'couchdiff.rb'
3
+ require 'json/ext'
4
+
5
+ describe CouchDiff do
6
+ describe "deleted doc" do
7
+ diff = CouchDiff.new [], [
8
+ doc1 = {'id' => '1', 'value' => {'rev' => '1-12345'}, 'doc' => { "a" => "b"}}
9
+ ]
10
+ it "should mark doc as deleted and not report additions or deletions" do
11
+ diff.deleted.should == [doc1]
12
+ diff.added.should be_empty
13
+ diff.updated.should be_empty
14
+ end
15
+ end
16
+
17
+ describe "added doc" do
18
+ diff = CouchDiff.new [
19
+ doc1 = {'id' => '1', 'value' => {'rev' => '1-12345'}, 'doc' => { "a" => "b"}}
20
+ ], []
21
+
22
+ it "should mark doc as added and not report updates or deletions" do
23
+ diff.added.should == [doc1]
24
+ diff.deleted.should be_empty
25
+ diff.updated.should be_empty
26
+ end
27
+ end
28
+
29
+ describe "updated content" do
30
+ it "should mark doc as updated and not report additions or updates" do
31
+ diff = CouchDiff.new [
32
+ doc1 = {'id' => '1', 'value' => {'rev' => '1-12345'}, 'doc' => { "a" => "b"}},
33
+ doc2 = {'id' => '2', 'value' => {'rev' => '1-11111'}, 'doc' => { "a" => "b"}}
34
+ ], [
35
+ doc1,
36
+ doc2updated = {'id' => '2', 'value' => {'rev' => '1-22222'}, 'doc' => { "a" => "c"}}
37
+ ]
38
+
39
+ diff.updated.should == [[doc2,doc2updated]]
40
+ diff.deleted.should be_empty
41
+ diff.added.should be_empty
42
+ end
43
+ end
44
+
45
+ describe "unchanged content" do
46
+ it "should not mark doc as updated" do
47
+ diff = CouchDiff.new [
48
+ doc1 = {'id' => '1', 'value' => {'rev' => '1-12345'}, 'doc' => {'_rev' => '1-12345', "a" => "b"} },
49
+ doc2 = {'id' => '2', 'value' => {'rev' => '1-11111'}, 'doc' => { "a" => "b"}}
50
+ ], [
51
+ # higher revision, but same content as doc1
52
+ {'id' => '1', 'value' => {'rev' => '3-45678'}, 'doc' => {'_rev' => '3-45678', "a" => "b"}},
53
+ doc2
54
+ ]
55
+
56
+ diff.unchanged.should == [doc1, doc2]
57
+ diff.added.should be_empty
58
+ diff.updated.should be_empty
59
+ diff.deleted.should be_empty
60
+ end
61
+ end
62
+
63
+ describe "custom 'changed' function" do
64
+ it "should update docs based on user condition" do
65
+ diff = CouchDiff.new [
66
+ doc1 = {'id' => '1', 'value' => {'rev' => '1'}, 'doc' => { "a" => "b", "version" => "1.0"}},
67
+ doc2 = {'id' => '2', 'value' => {'rev' => '1'}, 'doc' => { "a" => "b", "version" => "1.0"}}
68
+ ], [
69
+ # doc changed (a => "b->c"), but not the version ==> unchanged
70
+ {'id' => '1', 'value' => {'rev' => '2'}, 'doc' => { "a" => "c", "version" => "1.0"}},
71
+ # version changed ==> changed
72
+ doc2updated = {'id' => '2', 'value' => {'rev' => '2'}, 'doc' => { "a" => "b", "version" => "2.0"}}
73
+ ] do |src, dst|
74
+ # do not update if the doc's git revision has not changed
75
+ src['version'] != dst['version']
76
+ end
77
+
78
+ diff.added.should be_empty
79
+ diff.unchanged.should == [doc1]
80
+ diff.updated.should == [[doc2,doc2updated]]
81
+ diff.deleted.should be_empty
82
+
83
+ end
84
+
85
+ it "should always copy missing attachments" do
86
+ doc = {'id' => '1', 'value' => {'rev' => '1'}, 'doc' => {
87
+ "id"=>"1", "_rev" => "1-12345", "version" => "1.0",
88
+ "_attachments"=>{"file.txt"=>{}, "revpos" => "1"}
89
+ }}
90
+ doc_no_attachment = {'id' => '1', 'value' => {'rev' => '1'}, 'doc' => {
91
+ "id"=>"1", "_rev" => "1-12345", "version" => "1.0",
92
+ "_attachments"=>{}
93
+ }}
94
+
95
+ diff = CouchDiff.new [ doc ],[ doc_no_attachment ] do |src, dst|
96
+ # do not update if the doc's git revision has not changed
97
+ src['version'] != dst['version']
98
+ end
99
+
100
+ diff.added.should be_empty
101
+ diff.unchanged.should be_empty
102
+ diff.updated.should == [[doc,doc_no_attachment]]
103
+ diff.deleted.should be_empty
104
+ end
105
+ end
106
+
107
+ describe "diff" do
108
+
109
+ it "should include all fields in diff" do
110
+ doc1 = {"id"=>"foo", "_rev" => "1-12345", "_attachments"=> {"file.txt" => {"revpos" => "1"}}}
111
+ doc2 = {"id"=>"foo", "_rev" => "2-12345", "_attachments"=> {"file.txt" => {"revpos" => "2"}}}
112
+
113
+ doc1.diff(doc2).should == {"_rev"=>["1-12345", "2-12345"], "_attachments" => {"file.txt" => {"revpos"=>["1", "2"]}}}
114
+ end
115
+
116
+ it "should exclude fields specified as excluded" do
117
+ doc1 = {"id"=>"foo", "_rev" => "1-12345", "_attachments"=>{"file.txt"=>{ "revpos" => "1" }}}
118
+ doc2 = {"id"=>"foo", "_rev" => "2-12345", "_attachments"=>{"file.txt"=>{ "revpos" => "2" }}}
119
+ # without
120
+ doc1.diff(doc2, ["_rev", "revpos"]).should == {}
121
+ end
122
+
123
+ it "should identify when order of attachments (= order of keys in hash) changes" do
124
+ doc1 = {"id"=>"foo", "_rev" => "1-12345", "_attachments"=>{"file.txt"=>{"revpos" => "1"}, "file2.txt"=>{"revpos" => "1"}}}
125
+ doc2 = {"id"=>"foo", "_rev" => "2-12345", "_attachments"=>{"file2.txt"=>{"revpos" => "1"}, "file.txt"=>{"revpos" => "1"}}}
126
+ doc1.diff(doc2, ["_rev", "revpos"]).should == {"_attachments"=> {:_keys_changed=>[["file.txt", "file2.txt"], ["file2.txt", "file.txt"]]}}
127
+ end
128
+
129
+ end
130
+ end
131
+
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: couchdiff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jens Schmidt
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-23 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: couchrest
16
+ requirement: &2152664100 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2152664100
25
+ - !ruby/object:Gem::Dependency
26
+ name: progressbar
27
+ requirement: &2152662580 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '0.9'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2152662580
36
+ - !ruby/object:Gem::Dependency
37
+ name: json
38
+ requirement: &2152661060 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 1.6.0
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *2152661060
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: &2152659680 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 2.7.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2152659680
58
+ - !ruby/object:Gem::Dependency
59
+ name: hoe
60
+ requirement: &2152658280 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: '2.12'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2152658280
69
+ - !ruby/object:Gem::Dependency
70
+ name: rdoc
71
+ requirement: &2156945080 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '3.10'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2156945080
80
+ description: ''
81
+ email:
82
+ - jens@mudynamics.com
83
+ executables:
84
+ - couchdiff
85
+ extensions: []
86
+ extra_rdoc_files:
87
+ - History.txt
88
+ - Manifest.txt
89
+ files:
90
+ - History.txt
91
+ - Manifest.txt
92
+ - README.md
93
+ - Rakefile
94
+ - bin/couchdiff
95
+ - lib/couchdiff.rb
96
+ - lib/copier.rb
97
+ - spec/copier_spec.rb
98
+ - spec/couchdiff_spec.rb
99
+ - .gemtest
100
+ homepage: http://github.com/mudynamics/couchdiff
101
+ licenses: []
102
+ post_install_message:
103
+ rdoc_options:
104
+ - --main
105
+ - README.txt
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project: couchdiff
122
+ rubygems_version: 1.8.10
123
+ signing_key:
124
+ specification_version: 3
125
+ summary: ''
126
+ test_files: []