rpatch 0.0.1

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