hallettj-cloudrcs 0.0.1

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.
@@ -0,0 +1,74 @@
1
+ module CloudRCS
2
+
3
+ # A primitive patch type that represents a new or an undeleted file.
4
+ class Addfile < PrimitivePatch
5
+ validates_presence_of :path
6
+
7
+ def after_initialize
8
+ verify_path_prefix
9
+ end
10
+
11
+ def to_s
12
+ "addfile #{self.class.escape_path(path)}"
13
+ end
14
+
15
+ def inverse
16
+ Rmfile.new(:path => path)
17
+ end
18
+
19
+ def commute(patch)
20
+ if patch.is_a? Addfile and patch.path == self.path
21
+ raise CommuteException(true, "Conflict: cannot create two files with the same path.")
22
+ elsif patch.is_a? Rmfile and patch.path == self.path
23
+ raise CommuteException(true, "Conflict: commuting addfile with rmfile in this case would cause file to be removed before it is created.")
24
+ elsif patch.is_a? Move and patch.original_path == self.path
25
+ raise CommuteException(true, "Conflict: commuting addfile with move in this case would cause file to be moved before it is created.")
26
+ else
27
+ patch1 = patch.clone
28
+ patch2 = self.clone
29
+ end
30
+ return patch1, patch2
31
+ end
32
+
33
+ def apply_to(file)
34
+ return file unless file.nil?
35
+ if patch.respond_to? :owner
36
+ new_file = self.class.file_class.new(:owner => patch.owner,
37
+ :contents => "",
38
+ :content_type => "text/plain")
39
+ else
40
+ new_file = self.class.file_class.new(:contents => "",
41
+ :content_type => "text/plain")
42
+ end
43
+ new_file.path = path
44
+ return new_file
45
+ end
46
+
47
+ class << self
48
+
49
+ # Addfile has a low priority so that it will appear before patches
50
+ # that are likely to depend on it - such as Hunk patches.
51
+ def priority
52
+ 10
53
+ end
54
+
55
+ def generate(orig_file, changed_file)
56
+ if orig_file.nil? and not changed_file.nil?
57
+ return Addfile.new(:path => changed_file.path, :contents => changed_file.content_type)
58
+ end
59
+ end
60
+
61
+ def parse(contents)
62
+ unless contents =~ /^addfile\s+(\S+)\s*$/
63
+ raise "Failed to parse addfile patch: #{contents}"
64
+ end
65
+ Addfile.new(:path => unescape_path($1))
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ PATCH_TYPES << Addfile
73
+
74
+ end
@@ -0,0 +1,232 @@
1
+ module CloudRCS
2
+
3
+ class Binary < PrimitivePatch
4
+ serialize :contents, Array
5
+
6
+ validates_presence_of :path, :contents, :position
7
+ validates_numericality_of :position, :only_integer => true, :greater_than_or_equal_to => 0
8
+
9
+ def apply_to(file)
10
+ return file unless file.path == path
11
+
12
+ hex_contents = Binary.binary_to_hex(file.contents)
13
+
14
+ # Check that the patch matches the file contents
15
+ unless hex_contents[position...position+lengthold] == removed
16
+ raise ApplyException.new(true), "Portion of binary patch marked for removal does not match existing contents in file. Existing contents at position #{position}: '#{hex_contents[position...position+lengthold]}' ; marked for removal: '#{removed}'"
17
+ end
18
+
19
+ # Then, remove stuff
20
+ unless removed.blank?
21
+ hex_contents[position...position+lengthold] = ""
22
+ end
23
+
24
+ # Finally, add stuff
25
+ unless added.blank?
26
+ hex_contents.insert(position, added)
27
+ end
28
+
29
+ file.contents = Binary.hex_to_binary(hex_contents)
30
+ return file
31
+ end
32
+
33
+ def inverse
34
+ Binary.new(:path => path,
35
+ :position => position,
36
+ :contents => [added, removed],
37
+ :inverted => true)
38
+ end
39
+
40
+ def commute(patch)
41
+ if patch.is_a? Binary and patch.path == self.path
42
+
43
+ # self is applied first and precedes patch in the file
44
+ if self.position + self.lengthnew < patch.position
45
+ patch1 = Binary.new(:path => patch.path,
46
+ :position => (patch.position - self.lengthnew + self.lengthold),
47
+ :contents => patch.contents)
48
+ patch2 = Binary.new(:path => self.path,
49
+ :position => self.position,
50
+ :contents => self.contents)
51
+
52
+ # self is applied first, but is preceded by patch in the file
53
+ elsif patch.position + patch.lengthold < self.position
54
+ patch1 = Binary.new(:path => patch.path,
55
+ :position => patch.position,
56
+ :contents => patch.contents)
57
+ patch2 = Binary.new(:path => self.path,
58
+ :position => (self.position + patch.lengthnew - patch.lengthold),
59
+ :contents => self.contents)
60
+
61
+ # patch precedes self in file, but bumps up against it
62
+ elsif patch.position + patch.lengthnew == self.position and
63
+ self.lengthold != 0 and patch.lengthold != 0 and
64
+ self.lengthnew != 0 and patch.lengthnew != 0
65
+ patch1 = Binary.new(:path => patch.path,
66
+ :position => patch.position,
67
+ :contents => patch.contents)
68
+ patch2 = Binary.new(:path => self.path,
69
+ :position => (self.position - patch.lengthnew + patch.lengthold),
70
+ :contents => self.contents)
71
+
72
+ # self precedes patch in file, but bumps up against it
73
+ elsif self.position + self.lengthold == patch.position and
74
+ self.lengthold != 0 and patch.lengthold != 0 and
75
+ self.lengthnew != 0 and patch.lengthnew != 0
76
+ patch1 = Binary.new(:path => patch.path,
77
+ :position => patch.position,
78
+ :contents => patch.contents)
79
+ patch2 = Binary.new(:path => self.path,
80
+ :position => (self.position + patch.lengthnew - patch.lengthold),
81
+ :contents => self.contents)
82
+
83
+ # Patches overlap. This is a conflict scenario
84
+ else
85
+ raise CommuteException.new(true), "Conflict: binary patches overlap."
86
+ end
87
+
88
+ elsif patch.is_a? Rmfile and patch.path == self.path
89
+ raise CommuteException.new(true), "Conflict: cannot modify a file after it is removed."
90
+
91
+ elsif patch.is_a? Move and self.path == patch.original_path
92
+ patch1 = patch.clone
93
+ patch2 = self.clone
94
+ patch2.path = patch.new_path
95
+
96
+ # Commutation is trivial
97
+ else
98
+ patch1, patch2 = patch, self
99
+ end
100
+
101
+ return patch1, patch2
102
+ end
103
+
104
+ def to_s
105
+ header = "binary #{self.class.escape_path(path)} #{position}"
106
+ old = removed.scan(/.{1,78}/).collect { |c| '-' + c }.join("\n")
107
+ new = added.scan(/.{1,78}/).collect { |c| '+' + c }.join("\n")
108
+ return [header, old, new].delete_if { |e| e.blank? }.join("\n")
109
+ end
110
+
111
+ def removed
112
+ contents.first
113
+ end
114
+
115
+ def added
116
+ contents.last
117
+ end
118
+
119
+ def lengthold
120
+ removed.length
121
+ end
122
+
123
+ def lengthnew
124
+ added.length
125
+ end
126
+
127
+ class << self
128
+
129
+ # Use a low priority so that the binary patch generating method
130
+ # will be called before the hunk patch generating method
131
+ def priority
132
+ 20
133
+ end
134
+
135
+ def generate(orig_file, changed_file)
136
+ return if orig_file.nil? and changed_file.nil?
137
+ return unless (orig_file and orig_file.contents.is_binary_data?) or
138
+ (changed_file and changed_file.contents.is_binary_data?)
139
+
140
+ # Convert binary data to hexadecimal for storage in a text
141
+ # file
142
+ orig_hex = orig_file ? binary_to_hex(orig_file.contents).scan(/.{2}/) : []
143
+ changed_hex = changed_file ? binary_to_hex(changed_file.contents).scan(/.{2}/) : []
144
+
145
+ file_path = orig_file ? orig_file.path : changed_file.path
146
+
147
+ diffs = Diff::LCS.diff(orig_hex, changed_hex)
148
+ chunks = []
149
+ offset = 0
150
+ diffs.each do |d|
151
+
152
+ # We need to recalculate positions for removals - just as in
153
+ # hunk generation.
154
+ unless chunks.empty?
155
+ offset += chunks.last.lengthnew - chunks.last.lengthold
156
+ end
157
+ d.collect! do |l|
158
+ if l.action == '-'
159
+ Diff::LCS::Change.new(l.action, l.position + (offset / 2), l.element)
160
+ else
161
+ l
162
+ end
163
+ end
164
+
165
+ position = d.first.position * 2
166
+
167
+ removed = d.find_all { |l| l.action == '-' }.collect { |l| l.element }.join
168
+ added = d.find_all { |l| l.action == '+' }.collect { |l| l.element }.join
169
+
170
+ unless removed.blank? and added.blank?
171
+ chunks << Binary.new(:contents => [removed, added],
172
+ :position => position,
173
+ :path => file_path)
174
+ end
175
+
176
+ end
177
+
178
+ return chunks
179
+ end
180
+
181
+ def parse(contents)
182
+ unless contents =~ /^binary\s+(\S+)\s+(\d+)\s+(.*)$/m
183
+ raise ParseException.new(true), "Failed to parse binary patch: \"#{contents}\""
184
+ end
185
+ file_path = unescape_path($1)
186
+ starting_position = $2.to_i
187
+ contents = $3
188
+
189
+ removed, added = [], []
190
+ removed_offset = 0
191
+ added_offset = 0
192
+ contents.split("\n").each do |line|
193
+ if line =~ /^-([\S]*)\s*$/
194
+ removed << $1
195
+ removed_offset += 1
196
+ elsif line =~ /^\+([\S]*)\s*$/
197
+ added << $1
198
+ added_offset += 1
199
+ else
200
+ raise "Failed to parse a line in binary patch: \"#{line}\""
201
+ end
202
+ end
203
+
204
+ removed = removed.join
205
+ added = added.join
206
+
207
+ return Binary.new(:path => file_path,
208
+ :position => starting_position,
209
+ :contents => [removed, added])
210
+ end
211
+
212
+ # We want to store the contents of a binary file encoded as a
213
+ # hexidecimal value. These two methods allow for translating
214
+ # between binary and hexidecimal.
215
+ #
216
+ # Code borrowed from:
217
+ # http://4thmouse.com/index.php/2008/02/18/converting-hex-to-binary-in-4-languages/
218
+ def hex_to_binary(hex)
219
+ hex.to_a.pack("H*")
220
+ end
221
+
222
+ def binary_to_hex(bin)
223
+ bin.unpack("H*").first
224
+ end
225
+
226
+ end
227
+
228
+ end
229
+
230
+ PATCH_TYPES << Binary
231
+
232
+ end
@@ -0,0 +1,263 @@
1
+ module CloudRCS
2
+
3
+ # Hunk is one type of primitive patch. It represents a deletion or
4
+ # an insertion, or a combination of both, in a text file.
5
+ #
6
+ # A Hunk is constructed using the path of a file, the first line
7
+ # modifications to the file, and a set of diffs, each of which
8
+ # represents a line added to or deleted from the file.
9
+
10
+ class Hunk < PrimitivePatch
11
+ serialize :contents, Array
12
+
13
+ validates_presence_of :path, :contents, :position
14
+ validates_numericality_of :position, :only_integer => true, :greater_than_or_equal_to => 1
15
+
16
+ def validate
17
+ # Make sure diffs only contain the actions '+' and '-'
18
+ if contents.respond_to? :each
19
+ contents.each do |d|
20
+ unless ['+','-'].include? d.action
21
+ errors.add(:contents, "contains an unknown action.")
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # def after_initialize
28
+ # verify_path_prefix
29
+ # starting_line ||= contents.first.position
30
+ # end
31
+
32
+ def to_s
33
+ "hunk #{self.class.escape_path(path)} #{position}\n" + contents.collect do |d|
34
+ "#{d.action}#{d.element}"
35
+ end.join("\n")
36
+ end
37
+
38
+ # The inverse of a Hunk simply swaps adds and deletes.
39
+ def inverse
40
+ new_removals = added_lines.collect do |d|
41
+ Diff::LCS::Change.new('-', d.position, d.element)
42
+ end
43
+ new_adds = removed_lines.collect do |d|
44
+ Diff::LCS::Change.new('+', d.position, d.element)
45
+ end
46
+ Hunk.new(:path => path, :position => position, :contents => (new_removals + new_adds))
47
+ end
48
+
49
+ # Given another patch, generates two new patches that have the
50
+ # same effect as the original two, but with the order of the
51
+ # analogous patches reversed. The message receiver is the first
52
+ # patch, and the argument is the second; so after commuting the
53
+ # analog of this patch will be second.
54
+ def commute(patch)
55
+ if patch.is_a? Hunk and patch.path == self.path
56
+
57
+ # self is applied first and precedes patch in the file
58
+ if self.position + self.lengthnew < patch.position
59
+ patch1 = Hunk.new(:path => patch.path,
60
+ :position => (patch.position - self.lengthnew + self.lengthold),
61
+ :contents => patch.contents)
62
+ patch2 = Hunk.new(:path => self.path, :position => self.position, :contents => self.contents)
63
+
64
+ # self is applied first, but is preceded by patch in the file
65
+ elsif patch.position + patch.lengthold < self.position
66
+ patch1 = Hunk.new(:path => patch.path, :position => patch.position, :contents => patch.contents)
67
+ patch2 = Hunk.new(:path => self.path,
68
+ :position => (self.position + patch.lengthnew - patch.lengthold),
69
+ :contents => self.contents)
70
+
71
+ # patch precedes self in file, but bumps up against it
72
+ elsif patch.position + patch.lengthnew == self.position and
73
+ self.lengthold != 0 and patch.lengthold != 0 and
74
+ self.lengthnew != 0 and patch.lengthnew != 0
75
+ patch1 = Hunk.new(:path => patch.path, :position => patch.position, :contents => patch.contents)
76
+ patch2 = Hunk.new(:path => self.path,
77
+ :position => (self.position - patch.lengthnew + patch.lengthold),
78
+ :contents => self.contents)
79
+
80
+ # self precedes patch in file, but bumps up against it
81
+ elsif self.position + self.lengthold == patch.position and
82
+ self.lengthold != 0 and patch.lengthold != 0 and
83
+ self.lengthnew != 0 and patch.lengthnew != 0
84
+ patch1 = Hunk.new(:path => patch.path, :position => patch.position, :contents => patch.contents)
85
+ patch2 = Hunk.new(:path => self.path,
86
+ :position => (self.position + patch.lengthnew - patch.lengthold),
87
+ :contents => self.contents)
88
+
89
+ # Patches overlap. This is a conflict scenario
90
+ else
91
+ raise CommuteException.new(true, "Conflict: hunk patches overlap.")
92
+ end
93
+
94
+ elsif patch.is_a? Rmfile and patch.path == self.path
95
+ raise CommuteException.new(true, "Conflict: cannot modify a file after it is removed.")
96
+
97
+ elsif patch.is_a? Move and self.path == patch.original_path
98
+ patch1 = patch.clone
99
+ patch2 = self.clone
100
+ patch2.path = patch.new_path
101
+
102
+ # Commutation is trivial
103
+ else
104
+ patch1, patch2 = patch, self
105
+ end
106
+
107
+ return patch1, patch2
108
+ end
109
+
110
+ def apply_to(file)
111
+ return file unless file.path == path
112
+
113
+ # Passing a negative number as the second argument of split
114
+ # preserves trailing newline characters at the end of the file
115
+ # when the lines are re-joined.
116
+ lines = file.contents.split("\n",-1)
117
+
118
+ # First, remove lines
119
+ removed_lines.each do |d|
120
+ if lines[position-1] == d.element.sub(/(\s+)\$\s*$/) { $1 }
121
+ lines.delete_at(position-1)
122
+ else
123
+ raise ApplyException.new(true), "Line in hunk marked for removal does not match contents of existing line in file\nfile contents: #{position} -'#{lines[position-1]}'\nline to be removed: #{d.position} -'#{d.element}'"
124
+ end
125
+ end
126
+
127
+ # Next, add lines
128
+ added_lines.each_with_index do |d,i|
129
+ lines.insert(position - 1 + i, d.element.sub(/(\s+)\$\s*$/) { $1 })
130
+ end
131
+
132
+ file.contents = lines.join("\n")
133
+ return file
134
+ end
135
+
136
+ # Returns the number of lines added by the hunk patch
137
+ def lengthnew
138
+ added_lines.length
139
+ end
140
+
141
+ # Returns the number of lines removed by the hunk patch
142
+ def lengthold
143
+ removed_lines.length
144
+ end
145
+
146
+ def removed_lines
147
+ contents.find_all { |d| d.action == '-' } # .sort { |a,b| a.position <=> b.position }
148
+ end
149
+
150
+ def added_lines
151
+ contents.find_all { |d| d.action == '+' } # .sort { |a,b| a.position <=> b.position }
152
+ end
153
+
154
+ class << self
155
+
156
+ # Given a list of files, determine whether this patch type
157
+ # describes the changes between the files and generate patches
158
+ # accordingly.
159
+ #
160
+ # In this case we use the Diff::LCS algorithm to generate Change
161
+ # objects representing each changed line between two files. The
162
+ # changesets are automatically nested into a two dimensional
163
+ # Array, where each row represents a changed portion of the file
164
+ # that is separated from the other rows by an unchanged portion
165
+ # of the file. So we split that dimension of the Array into
166
+ # separate Hunk patches and return the resulting list.
167
+ def generate(orig_file, changed_file)
168
+ return if orig_file.nil? and changed_file.nil?
169
+ return if (orig_file and orig_file.contents.is_binary_data?) or
170
+ (changed_file and changed_file.contents.is_binary_data?)
171
+
172
+ # If the original or the changed file is nil, the hunk should
173
+ # contain the entirety of the other file. This is so that a
174
+ # record is kept of a file that is deleted; and so that the
175
+ # contents of a file is added to it after it is created.
176
+ orig_lines = orig_file ? orig_file.contents.split("\n",-1) : []
177
+ changed_lines = changed_file ? changed_file.contents.split("\n",-1) : []
178
+
179
+ # Insert end-of-line tokens to preserve white space at the end
180
+ # of lines. This is part of the darcs patch format.
181
+ orig_lines.each { |l| l += "$" if l =~ /\s+$/ }
182
+ changed_lines.each { |l| l += "$" if l =~ /\s+$/ }
183
+
184
+ file_path = orig_file ? orig_file.path : changed_file.path
185
+
186
+ diffs = Diff::LCS.diff(orig_lines, changed_lines)
187
+ hunks = []
188
+ offset = 0
189
+ diffs.each do |d|
190
+
191
+ # Diff::LCS assumes that removed lines from all hunks will be
192
+ # removed from file before new lines are added. Unfortunately,
193
+ # in this implementation we remove and add lines from each
194
+ # hunk in order. So the position values for removed lines will
195
+ # be off in all but the first hunk. So we need to adjust those
196
+ # position values before we create the hunk patch.
197
+ unless hunks.empty?
198
+ offset += hunks.last.lengthnew - hunks.last.lengthold
199
+ end
200
+ d.collect! do |l|
201
+ if l.action == '-'
202
+ Diff::LCS::Change.new(l.action, l.position + offset, l.element)
203
+ else
204
+ l
205
+ end
206
+ end
207
+
208
+ # The darcs patch format counts lines starting from 1; whereas
209
+ # Diff::LCS counts lines starting from 0. So we add 1 to the
210
+ # position of the first changed line to get the
211
+ # darcs-compatible starting line number for the Hunk patch.
212
+ position = d.first.position + 1
213
+
214
+ hunks << Hunk.new(:path => file_path, :position => position, :contents => d)
215
+ end
216
+ return hunks
217
+ end
218
+
219
+ # Parse hunk info from a file and convert into a Hunk object.
220
+ def parse(contents)
221
+ unless contents =~ /^hunk\s+(\S+)\s+(\d+)\s+(.*)$/m
222
+ raise ParseException.new(true), "Failed to parse hunk patch: \"#{contents}\""
223
+ end
224
+ file_path = unescape_path($1)
225
+ starting_position = $2.to_i
226
+ contents = $3
227
+
228
+ last_action = nil
229
+ line_offset = 0
230
+
231
+ diffs = []
232
+ add_line_offset = 0
233
+ del_line_offset = 0
234
+ contents.split("\n").each do |line|
235
+ # These regular expressions ensure that each line ends with a
236
+ # non-whitespace character, or is empty. A dollar sign is
237
+ # added during patch generation to the end of lines that end
238
+ # in whitespace; so parsing this way will not cut off
239
+ # whitespace that is supposed to be added to any patched file.
240
+ #
241
+ # If the line is empty, $1 will be nil. So it is important to
242
+ # pass $1.to_s instead of just $1 to change nil to "".
243
+ if line =~ /^\+(.*[\S\$])?\s*$/
244
+ diffs << Diff::LCS::Change.new('+', starting_position + add_line_offset, $1.to_s)
245
+ add_line_offset += 1
246
+ elsif line =~ /^-(.*[\S\$])?\s*$/
247
+ diffs << Diff::LCS::Change.new('-', starting_position + del_line_offset, $1.to_s)
248
+ del_line_offset += 1
249
+ else
250
+ raise "Failed to parse a line in hunk: \"#{line}\""
251
+ end
252
+ end
253
+
254
+ return Hunk.new(:path => file_path, :position => starting_position, :contents => diffs)
255
+ end
256
+
257
+ end
258
+
259
+ end
260
+
261
+ PATCH_TYPES << Hunk
262
+
263
+ end