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.
- data/History.txt +4 -0
- data/License.txt +14 -0
- data/Manifest.txt +35 -0
- data/PostInstall.txt +2 -0
- data/README.txt +293 -0
- data/Rakefile +4 -0
- data/config/hoe.rb +75 -0
- data/config/requirements.rb +15 -0
- data/lib/active_record/acts/list.rb +256 -0
- data/lib/acts_as_list.rb +2 -0
- data/lib/cloud_rcs.rb +7 -0
- data/lib/cloud_rcs/patch.rb +404 -0
- data/lib/cloud_rcs/patch_types/addfile.rb +74 -0
- data/lib/cloud_rcs/patch_types/binary.rb +232 -0
- data/lib/cloud_rcs/patch_types/hunk.rb +263 -0
- data/lib/cloud_rcs/patch_types/move.rb +89 -0
- data/lib/cloud_rcs/patch_types/rmfile.rb +63 -0
- data/lib/cloud_rcs/primitive_patch.rb +147 -0
- data/lib/cloudrcs.rb +12 -0
- data/lib/cloudrcs/version.rb +9 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +82 -0
- data/setup.rb +1585 -0
- data/tasks/deployment.rake +34 -0
- data/tasks/environment.rake +7 -0
- data/tasks/website.rake +17 -0
- data/test/test_cloudrcs.rb +11 -0
- data/test/test_helper.rb +2 -0
- data/website/index.html +141 -0
- data/website/index.txt +83 -0
- data/website/javascripts/rounded_corners_lite.inc.js +285 -0
- data/website/stylesheets/screen.css +138 -0
- data/website/template.html.erb +48 -0
- metadata +115 -0
@@ -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
|