rpatch 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/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jiang Xin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Rpatch
2
+
3
+ rpatch: a patch utility support regexp in patchfile.
4
+
5
+ Rpatch is a patch utility, more tolerant than GNU patch. It will ignore
6
+ changes of blank lines and white spaces, and what's more you can write
7
+ regexp ("RE: " and "RE:-") in patch file to match lines.
8
+
9
+ * Use "RE: " with a regexp to match unchanged line.
10
+ * Use "RE:-" with a regexp to match removed line.
11
+ * Use "@@" to starts a new patch hunk only and use it's text as
12
+ description for the hunk only.
13
+
14
+ For example:
15
+
16
+ * Origninal file:
17
+
18
+ When I hack files using GNU patch,
19
+ sometimes fail because of the change of upstream files.
20
+ blah blah blah...
21
+ So comes rpatch.
22
+
23
+ Happy hacking.
24
+
25
+ * Patch file:
26
+
27
+ diff -ru a/README b/README
28
+ --- a/readme.txt 2013-11-03 22:17:02.000000000 +0800
29
+ +++ b/readme.txt 2013-11-03 21:10:46.000000000 +0800
30
+ @@ add copyright at the beginning
31
+ +# Copyright (c) 2013 Jiang Xin
32
+ +
33
+ When I hack files using GNU patch,
34
+ RE: sometimes fail because .*
35
+ RE:-(blah\s*){3}
36
+ @@ add notes
37
+ +If patch can ignore blank lines, support regex patterns
38
+ +in patch, it will be nice.
39
+ So comes rpatch.
40
+ @@ add signature
41
+ Happy hacking.
42
+ +--
43
+ +jiangxin
44
+
45
+ * And the result would be:
46
+
47
+ # Copyright (c) 2013 Jiang Xin
48
+
49
+ When I hack files using GNU patch,
50
+ sometimes fail because of the change of upstream files.
51
+ If patch can ignore blank lines, support regex patterns
52
+ in patch, it will be nice.
53
+ So comes rpatch.
54
+
55
+ Happy hacking.
56
+ --
57
+ jiangxin
58
+
59
+ ## Installation
60
+
61
+ Install using rubygems:
62
+
63
+ $ gem install rpatch
64
+
65
+ Or install from source:
66
+
67
+ $ rake build
68
+ $ rake install
69
+
70
+ ## Usage
71
+
72
+ Rpatch is a GNU patch likely utilty, patch original file using patch file like this:
73
+
74
+ $ patch originalfile patchfile
75
+
76
+ Or patch files under the current directory, and read patchfile from STDIN:
77
+
78
+ $ patch -p1 < patchfile
79
+
80
+ Rpatch can also read a series patches in quilt format.
81
+
82
+ $ patch -p1 . patches
83
+
84
+ ## Contributing
85
+
86
+ 1. Fork it
87
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
88
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
89
+ 4. Push to the branch (`git push origin my-new-feature`)
90
+ 5. Create new Pull Request
data/bin/rpatch ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ begin
5
+ require 'rpatch/runner'
6
+ Rpatch::Runner.runner
7
+ rescue LoadError
8
+ $stderr.puts <<-EOS
9
+ #{'*'*50}
10
+ Could not require 'rpatch/patch'
11
+
12
+ This may happen if you're using rubygems as your package manager, but it is not
13
+ being required through some mechanism before executing the rpatch command.
14
+
15
+ You may need to do one of the following in your shell:
16
+
17
+ # for bash/zsh
18
+ export RUBYOPT=rubygems
19
+
20
+ # for csh, etc.
21
+ set RUBYOPT=rubygems
22
+
23
+ For background, please see http://gist.github.com/54177.
24
+ #{'*'*50}
25
+ EOS
26
+ exit(1)
27
+ end
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ require 'stringio'
5
+ require 'rpatch/error'
6
+ require 'rpatch/hunk'
7
+
8
+ module Rpatch
9
+ class PatchEntry
10
+ attr_reader :level
11
+
12
+ def initialize(old, new, level)
13
+ @entry_old = old
14
+ @entry_new = new
15
+ @level = level
16
+ @hunks = []
17
+ end
18
+
19
+ def oldfile
20
+ @oldfile ||= path_strip_prefix(@entry_old, @level)
21
+ end
22
+
23
+ def newfile
24
+ @newfile ||= path_strip_prefix(@entry_new, @level)
25
+ end
26
+
27
+ def feed_line(line)
28
+ if line =~ /^@@/
29
+ hunk = PatchHunk.new line, @hunks.size + 1
30
+ @hunks << hunk
31
+ elsif @hunks.any?
32
+ @hunks[-1].feed_line line
33
+ else
34
+ raise PatchFormatError, "Feed lines must start with @@, not: #{line}"
35
+ end
36
+ end
37
+
38
+ # Used in test cases.
39
+ def patch_on_message(text)
40
+ lines = text.split("\n").map{|line| line.chomp}
41
+
42
+ @hunks.each do |hunk|
43
+ hunk.patch(lines)
44
+ end
45
+
46
+ if lines.size > 0
47
+ lines * "\n" + "\n"
48
+ else
49
+ ''
50
+ end
51
+ rescue Exception => e
52
+ STDERR.puts "Error: #{e.message}"
53
+ end
54
+
55
+ def patch_on_file(input, output=nil)
56
+ lines = []
57
+ output ||= input
58
+ if output.is_a? IO or output.is_a? StringIO
59
+ filename = "<#{newfile}>"
60
+ else
61
+ filename = output
62
+ end
63
+
64
+ if input.is_a? IO or input.is_a? StringIO
65
+ lines = input.readlines.map{|line| line.chomp}
66
+ elsif File.file? input
67
+ File.open(input) do |io|
68
+ lines = io.readlines.map{|line| line.chomp}
69
+ end
70
+ end
71
+ lines_dup = lines.dup
72
+
73
+ patch_applied = false
74
+ patch_status = true
75
+ @hunks.each do |hunk|
76
+ begin
77
+ hunk.patch(lines)
78
+ rescue AlreadyPatchedError => e
79
+ STDERR.puts "#{filename}: #{e.message}"
80
+ rescue Exception => e
81
+ STDERR.puts "ERROR: #{filename}: #{e.message}"
82
+ patch_status = false
83
+ else
84
+ patch_applied = true
85
+ end
86
+ end
87
+
88
+ if output.is_a? IO or output.is_a? StringIO
89
+ output.write lines * "\n" + "\n"
90
+ elsif not patch_applied
91
+ STDERR.puts "#{filename}: nothing changed"
92
+ if input != output
93
+ File.open(output, "w") do |io|
94
+ io.write lines * "\n" + "\n"
95
+ end
96
+ end
97
+ else
98
+ unless patch_status
99
+ STDERR.puts "Warning: saved orignal file as \"#{output}.orig\"."
100
+ File.open("#{output}.orig", "w") do |io|
101
+ io.write lines_dup * "\n" + "\n"
102
+ end
103
+ end
104
+ if lines.size > 0
105
+ STDERR.puts "Patched \"#{output}\"."
106
+ File.open(output, "w") do |io|
107
+ io.write lines * "\n" + "\n"
108
+ end
109
+ else
110
+ STDERR.puts "Remove \"#{output}\"."
111
+ File.unlink output
112
+ end
113
+ end
114
+ return patch_status
115
+ end
116
+
117
+ def patch_on_directory(inputdir, outputdir=nil)
118
+ outputdir ||=inputdir
119
+ input = oldfile.start_with?('/') ? oldfile : File.join(inputdir, oldfile)
120
+ output = outputdir
121
+ if File.directory? outputdir
122
+ output = newfile.start_with?('/') ? newfile : File.join(outputdir, newfile)
123
+ end
124
+ patch_on_file(input, output)
125
+ end
126
+
127
+ private
128
+
129
+ def path_strip_prefix(name, level)
130
+ filename = name.dup
131
+ unless filename.start_with? '/'
132
+ level.times {filename.sub!(/^[^\/]+\//, '')}
133
+ end
134
+ filename
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,8 @@
1
+ module Rpatch
2
+ class AlreadyPatchedError < Exception; end
3
+ class PatchHunkError < Exception; end
4
+ class PatchFormatError < Exception; end
5
+ class FileNotExistError < Exception; end
6
+ class PatchFailNotify < Exception; end
7
+ class PatchOneWithManyError < Exception; end
8
+ end
@@ -0,0 +1,271 @@
1
+ require 'rpatch/error'
2
+
3
+ module Rpatch
4
+ class PatchHunk
5
+ attr_reader :title, :diffs, :num
6
+
7
+ def initialize(text, num=nil)
8
+ @title = text.chomp.sub(/^@@/, '').strip
9
+ @num = num || "#"
10
+ @diffs = []
11
+ end
12
+
13
+ def feed_line(line)
14
+ diffs << line.chomp
15
+ end
16
+
17
+ # Patch lines in place.
18
+ def patch(lines)
19
+ matched_before = match_before_patch lines
20
+ matched_after = match_after_patch lines
21
+
22
+ if patterns_before_patch.size > patterns_after_patch.size
23
+ if not matched_before
24
+ if matched_after
25
+ raise AlreadyPatchedError.new("Hunk #{num} (#{title}) is already patched.")
26
+ else
27
+ raise PatchHunkError, "Hunk #{num} (#{title}) FAILED to apply. Match failed."
28
+ end
29
+ end
30
+ else
31
+ if matched_after
32
+ raise AlreadyPatchedError.new("Hunk #{num} (#{title}) is already patched.")
33
+ elsif not matched_before
34
+ raise PatchHunkError, "Hunk #{num} (#{title}) FAILED to apply. Match failed."
35
+ end
36
+ end
37
+
38
+ n, size = matched_before
39
+ lines[n...(n+size)] = convert lines[n...(n+size)]
40
+ lines
41
+ end
42
+
43
+ # Apply patch on lines.
44
+ # Return array of strings contain result of applying patch.
45
+ def convert(lines)
46
+ lines_dup = lines.dup
47
+ result = []
48
+ i = 0
49
+ while i < @diffs.size
50
+ case @diffs[i]
51
+ when /^( |RE: )/
52
+ while true
53
+ match = match_line lines_dup.first, patterns[i]
54
+ if not match
55
+ raise PatchFormatError.new("Hunk #{num} (#{title}) FAILED to apply. No match \"#{patterns[i]}\" with #{lines_dup.first.inspect}.")
56
+ elsif match > 0
57
+ result << lines_dup.shift
58
+ break
59
+ elsif match == 0
60
+ result << lines_dup.shift
61
+ end
62
+ end
63
+ when /^(-|RE:-)/
64
+ while true
65
+ match = match_line lines_dup.first, patterns[i]
66
+ if not match
67
+ raise PatchFormatError.new("Hunk #{num} (#{title}) FAILED to apply. No match pattern \"#{patterns[i]}\" against #{lines_dup.first.inspect}.")
68
+ elsif match > 0
69
+ lines_dup.shift
70
+ break
71
+ elsif match == 0
72
+ lines_dup.shift
73
+ end
74
+ end
75
+ when /^\+/
76
+ result << @diffs[i][1..-1]
77
+ else
78
+ raise PatchFormatError.new("Hunk #{num} (#{title}) FAILED to apply. Unknow syntax in hunk: #{@diffs[i]}")
79
+ end
80
+ i += 1
81
+ end
82
+ result
83
+ end
84
+
85
+ # Return [location, +num_of_lines], which seems already patched, and
86
+ # match start with location and +num lines are matched.
87
+ # Return nil, if nothing matched.
88
+ def match_after_patch(lines)
89
+ i = 0
90
+ if patterns_after_patch.size == 0
91
+ return [0, 0]
92
+ end
93
+
94
+ loop_n = lines.size
95
+ loop_n = loop_n - patterns_after_patch.size + 1 if patterns_after_patch.size > 0
96
+ while i < loop_n
97
+ matched_line_no = match_beginning(lines[i..-1], patterns_after_patch)
98
+ if matched_line_no
99
+ return [i, matched_line_no]
100
+ else
101
+ i += 1
102
+ end
103
+ end
104
+ nil
105
+ end
106
+
107
+ # Return [location, +num_of_lines], which could apply patch at
108
+ # at location, and +num lines would be replaced.
109
+ # Return nil, if nothing matched.
110
+ def match_before_patch(lines)
111
+ i = 0
112
+ if patterns_before_patch.size == 0
113
+ return [0, 0]
114
+ end
115
+
116
+ loop_n = lines.size
117
+ loop_n = loop_n - patterns_before_patch.size + 1 if patterns_before_patch.size > 0
118
+
119
+ while i < loop_n
120
+ matched_size = match_beginning(lines[i..-1], patterns_before_patch)
121
+ if matched_size
122
+ return [i, matched_size]
123
+ else
124
+ i += 1
125
+ end
126
+ end
127
+ nil
128
+ end
129
+
130
+ # Test whether patterns match against the beginning of lines
131
+ # Return nil if not match, or return number of lines matched
132
+ # with patterns (would be replaced later).
133
+ def match_beginning(lines, patterns)
134
+ i = 0
135
+ found = true
136
+ patterns.each do |pattern|
137
+ unless lines[i]
138
+ found = false
139
+ break
140
+ end
141
+
142
+ while true
143
+ match = match_line lines[i], pattern
144
+ if not match
145
+ found = false
146
+ break
147
+ # Match precisely for the first line of pattern (i==0),
148
+ # Not pass blank lines.
149
+ elsif match == 0 and i == 0
150
+ found = false
151
+ break
152
+ # Matched.
153
+ elsif match > 0
154
+ break
155
+ # Match next line if this line is blank.
156
+ elsif match == 0
157
+ i += 1
158
+ unless lines[i]
159
+ found = false
160
+ break
161
+ end
162
+ # Never comes here.
163
+ else
164
+ found = false
165
+ break
166
+ end
167
+ end
168
+
169
+ break unless found
170
+ i += 1
171
+ end
172
+
173
+ if found
174
+ i
175
+ else
176
+ nil
177
+ end
178
+ end
179
+
180
+ def match_line(message , pattern)
181
+ return nil unless message
182
+ line = nil
183
+ match = nil
184
+ while true
185
+ if pattern.is_a? Regexp
186
+ # When match with regexp, do not twick message
187
+ line ||= message.dup
188
+ if pattern.match(line)
189
+ match = 1
190
+ end
191
+ else
192
+ line ||= message.strip.gsub(/\s+/, ' ')
193
+ if pattern == line
194
+ match = 1
195
+ end
196
+ end
197
+
198
+ if match
199
+ break
200
+ elsif line.empty?
201
+ match = 0
202
+ break
203
+ elsif line.start_with? "#"
204
+ while line.start_with? "#"
205
+ line = line[1..-1]
206
+ line = line.strip unless pattern.is_a? Regexp
207
+ end
208
+ else
209
+ break
210
+ end
211
+ end
212
+ match
213
+ end
214
+
215
+ def patterns_before_patch
216
+ @patterns_before_patch ||= begin
217
+ result = []
218
+ @diffs.each do |line|
219
+ case line
220
+ when /^( |-)/
221
+ result << line[1..-1].strip.gsub(/\s+/, ' ')
222
+ when /^(RE: |RE:-)/
223
+ result << Regexp.new(line[4..-1].strip)
224
+ when /^\+/
225
+ next
226
+ else
227
+ raise PatchFormatError.new("Unknown pattern in diffs: #{line}")
228
+ end
229
+ end
230
+ result
231
+ end
232
+ end
233
+
234
+ def patterns_after_patch
235
+ @patterns_after_patch ||= begin
236
+ result = []
237
+ @diffs.each do |line|
238
+ case line
239
+ when /^( |\+)/
240
+ result << line[1..-1].strip.gsub(/\s+/, ' ')
241
+ when /^(RE: )/
242
+ result << Regexp.new(line[4..-1].strip)
243
+ when /^(-|RE:-)/
244
+ next
245
+ else
246
+ raise PatchFormatError.new("Unknown pattern in diffs: #{line}")
247
+ end
248
+ end
249
+ result
250
+ end
251
+ end
252
+
253
+ def patterns
254
+ @patterns ||= begin
255
+ result = []
256
+ @diffs.each do |line|
257
+ case line
258
+ when /^( |\+|-)/
259
+ result << line[1..-1].strip.gsub(/\s+/, ' ')
260
+ when /^(RE: |RE:-)/
261
+ result << Regexp.new(line[4..-1].strip)
262
+ else
263
+ raise PatchFormatError.new("Unknown pattern in diffs: #{line}")
264
+ end
265
+ end
266
+ result
267
+ end
268
+ end
269
+
270
+ end
271
+ end
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ require 'rpatch/error'
5
+ require 'rpatch/entry'
6
+
7
+ module Rpatch
8
+ class Patch
9
+ attr_reader :name, :level, :patch_entries
10
+
11
+ class <<self
12
+ def apply(path, patches, patch_level)
13
+ unless File.exist? path
14
+ raise FileNotExistError, "File or directory \"#{path}\" does not exist"
15
+ end
16
+ patch_status = true
17
+ patches.each do |patch_file|
18
+ if patch_file.is_a? IO or patch_file.is_a? StringIO
19
+ apply_one(path, patch_file, patch_level) || patch_status = false
20
+ elsif File.file? patch_file
21
+ apply_one(path, patch_file, patch_level) || patch_status = false
22
+ elsif File.directory? patch_file
23
+ apply_quilt(path, patch_file, patch_level) || patch_status = false
24
+ else
25
+ raise FileNotExistError, "Can not find patch file: #{patch_file}"
26
+ end
27
+ end
28
+ patch_status
29
+ rescue Exception => e
30
+ STDERR.puts "Error: #{e.message}"
31
+ patch_status = false
32
+ end
33
+ end
34
+
35
+ def initialize(file, level)
36
+ @name = file.is_a?(String) ? file : "<#{file.class.to_s}>"
37
+ @patch = file
38
+ @level = level
39
+ @patch_entries = []
40
+ load_patch
41
+ end
42
+
43
+ def apply_to(input, output=nil)
44
+ patch_status = true
45
+ patch_entries.each do |patch_entry|
46
+ begin
47
+ # input maybe a IO, StringIO, directory, file, or in-exist file.
48
+ if input.is_a? String and File.directory? input
49
+ patch_entry.patch_on_directory(input, output) || patch_status = false
50
+ else
51
+ if patch_entries.size > 1
52
+ raise PatchOneWithManyError, "Multiple patch entries (#{patch_entries.size}) have been found in patch #{name}"
53
+ end
54
+ # a IO, StringIO, file, or inexist file.
55
+ patch_entry.patch_on_file(input, output) || patch_status = false
56
+ end
57
+ rescue Exception => e
58
+ STDERR.puts "Error: #{e.message}"
59
+ patch_status = false
60
+ end
61
+ end
62
+ patch_status
63
+ end
64
+
65
+ private
66
+
67
+ class <<self
68
+ def apply_one(path, patch_file, patch_level)
69
+ patch = Patch.new(patch_file, patch_level)
70
+ patch.apply_to(path)
71
+ end
72
+
73
+ def apply_quilt(path, quilt_dir, patch_level)
74
+ patch_status = true
75
+ if File.exist?("#{quilt_dir}/series")
76
+ File.open("#{quilt_dir}/series") do |io|
77
+ io.readlines.each do |line|
78
+ line.strip!
79
+ filename = line
80
+ level = patch_level
81
+ if line =~ /^(.*?)[\s]+-p([0-9]*)$/
82
+ filename = $1
83
+ level = $2
84
+ end
85
+ unless filename.start_with? '#'
86
+ apply_one(path, filename, level) || patch_status = false
87
+ end
88
+ end
89
+ end
90
+ else
91
+ raise FileNotExistError, "Can not find (quilt) patchs in dir: #{quilt_dir}"
92
+ end
93
+ patch_status
94
+ end
95
+ end
96
+
97
+ def load_patch
98
+ i=0
99
+ lines = get_patch
100
+ while i < lines.size
101
+ entry_names = find_new_entry(lines[i..i+6])
102
+ if entry_names
103
+ @patch_entries << PatchEntry.new(entry_names[0], entry_names[1], @level)
104
+ while not lines[i] =~ /^@@ / and lines[i]
105
+ i += 1
106
+ end
107
+ break unless i < lines.size
108
+ end
109
+
110
+ if @patch_entries.any?
111
+ if lines[i] =~ /^(@@| |-|\+|RE: |RE:-)/
112
+ @patch_entries.last.feed_line lines[i]
113
+ elsif lines[i] =~ /^(Binary files |Only in)/
114
+ # ignore
115
+ else
116
+ raise PatchFormatError, "Line #{i} of patch \"#{name}\" is invalid.\n\t=> #{lines[i].inspect}"
117
+ end
118
+ end
119
+
120
+ i += 1
121
+ end
122
+ end
123
+
124
+ def get_patch
125
+ if @patch.is_a? IO or @patch.is_a? StringIO
126
+ @patch.readlines.map {|line| line.chomp}
127
+ else
128
+ File.open(@patch) do |io|
129
+ io.readlines.map {|line| line.chomp}
130
+ end
131
+ end
132
+ end
133
+
134
+ def find_new_entry(lines)
135
+ return nil if lines[0] =~ /^(@@| |-|\+|RE: |RE:-)/
136
+ old = new = nil
137
+ lines_dup = lines.dup
138
+ line = lines_dup.shift
139
+ line = lines_dup.shift if line =~ /^diff /
140
+ line = lines_dup.shift if line =~ /^new file mode/
141
+ line = lines_dup.shift if line =~ /^index /
142
+ if line =~ /^--- (.+?)([\s]+[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.*)?$/
143
+ old = $1
144
+ line = lines_dup.shift
145
+ end
146
+ if line =~ /^\+\+\+ (.+?)([\s]+[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.*)?$/
147
+ new = $1
148
+ line = lines_dup.shift
149
+ end
150
+ if line =~ /^@@ / and old and new
151
+ [old, new]
152
+ else
153
+ nil
154
+ end
155
+ end
156
+ end
157
+
158
+ end