diff options
author | V1K1NGbg <victor@ilchev.com> | 2024-08-03 14:56:20 +0300 |
---|---|---|
committer | V1K1NGbg <victor@ilchev.com> | 2024-08-05 20:51:31 +0300 |
commit | 417107d9d9661d4ab0ba6a63239374b200814fce (patch) | |
tree | 3d75b5e81a02c15ef61bb735432d85ee314e8bad | |
parent | a22cc0d426e52254cca0ed623f1514b8391b48b3 (diff) | |
download | luarocks-417107d9d9661d4ab0ba6a63239374b200814fce.tar.gz luarocks-417107d9d9661d4ab0ba6a63239374b200814fce.tar.bz2 luarocks-417107d9d9661d4ab0ba6a63239374b200814fce.zip |
patch
-rw-r--r-- | src/luarocks/tools/patch-original.lua | 716 | ||||
-rw-r--r-- | src/luarocks/tools/patch.lua | 1235 | ||||
-rw-r--r-- | src/luarocks/tools/patch.tl | 19 |
3 files changed, 1363 insertions, 607 deletions
diff --git a/src/luarocks/tools/patch-original.lua b/src/luarocks/tools/patch-original.lua new file mode 100644 index 00000000..6f36d713 --- /dev/null +++ b/src/luarocks/tools/patch-original.lua | |||
@@ -0,0 +1,716 @@ | |||
1 | --- Patch utility to apply unified diffs. | ||
2 | -- | ||
3 | -- http://lua-users.org/wiki/LuaPatch | ||
4 | -- | ||
5 | -- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license). | ||
6 | -- Code is heavily based on the Python-based patch.py version 8.06-1 | ||
7 | -- Copyright (c) 2008 rainforce.org, MIT License | ||
8 | -- Project home: http://code.google.com/p/python-patch/ . | ||
9 | -- Version 0.1 | ||
10 | |||
11 | local patch = {} | ||
12 | |||
13 | local fs = require("luarocks.fs") | ||
14 | local fun = require("luarocks.fun") | ||
15 | |||
16 | local io = io | ||
17 | local os = os | ||
18 | local string = string | ||
19 | local table = table | ||
20 | local format = string.format | ||
21 | |||
22 | -- logging | ||
23 | local debugmode = false | ||
24 | local function debug(_) end | ||
25 | local function info(_) end | ||
26 | local function warning(s) io.stderr:write(s .. '\n') end | ||
27 | |||
28 | -- Returns boolean whether string s2 starts with string s. | ||
29 | local function startswith(s, s2) | ||
30 | return s:sub(1, #s2) == s2 | ||
31 | end | ||
32 | |||
33 | -- Returns boolean whether string s2 ends with string s. | ||
34 | local function endswith(s, s2) | ||
35 | return #s >= #s2 and s:sub(#s-#s2+1) == s2 | ||
36 | end | ||
37 | |||
38 | -- Returns string s after filtering out any new-line characters from end. | ||
39 | local function endlstrip(s) | ||
40 | return s:gsub('[\r\n]+$', '') | ||
41 | end | ||
42 | |||
43 | -- Returns shallow copy of table t. | ||
44 | local function table_copy(t) | ||
45 | local t2 = {} | ||
46 | for k,v in pairs(t) do t2[k] = v end | ||
47 | return t2 | ||
48 | end | ||
49 | |||
50 | local function exists(filename) | ||
51 | local fh = io.open(filename) | ||
52 | local result = fh ~= nil | ||
53 | if fh then fh:close() end | ||
54 | return result | ||
55 | end | ||
56 | local function isfile() return true end --FIX? | ||
57 | |||
58 | local function string_as_file(s) | ||
59 | return { | ||
60 | at = 0, | ||
61 | str = s, | ||
62 | len = #s, | ||
63 | eof = false, | ||
64 | read = function(self, n) | ||
65 | if self.eof then return nil end | ||
66 | local chunk = self.str:sub(self.at, self.at + n - 1) | ||
67 | self.at = self.at + n | ||
68 | if self.at > self.len then | ||
69 | self.eof = true | ||
70 | end | ||
71 | return chunk | ||
72 | end, | ||
73 | close = function(self) | ||
74 | self.eof = true | ||
75 | end, | ||
76 | } | ||
77 | end | ||
78 | |||
79 | -- | ||
80 | -- file_lines(f) is similar to f:lines() for file f. | ||
81 | -- The main difference is that read_lines includes | ||
82 | -- new-line character sequences ("\n", "\r\n", "\r"), | ||
83 | -- if any, at the end of each line. Embedded "\0" are also handled. | ||
84 | -- Caution: The newline behavior can depend on whether f is opened | ||
85 | -- in binary or ASCII mode. | ||
86 | -- (file_lines - version 20080913) | ||
87 | -- | ||
88 | local function file_lines(f) | ||
89 | local CHUNK_SIZE = 1024 | ||
90 | local buffer = "" | ||
91 | local pos_beg = 1 | ||
92 | return function() | ||
93 | local pos, chars | ||
94 | while 1 do | ||
95 | pos, chars = buffer:match('()([\r\n].)', pos_beg) | ||
96 | if pos or not f then | ||
97 | break | ||
98 | elseif f then | ||
99 | local chunk = f:read(CHUNK_SIZE) | ||
100 | if chunk then | ||
101 | buffer = buffer:sub(pos_beg) .. chunk | ||
102 | pos_beg = 1 | ||
103 | else | ||
104 | f = nil | ||
105 | end | ||
106 | end | ||
107 | end | ||
108 | if not pos then | ||
109 | pos = #buffer | ||
110 | elseif chars == '\r\n' then | ||
111 | pos = pos + 1 | ||
112 | end | ||
113 | local line = buffer:sub(pos_beg, pos) | ||
114 | pos_beg = pos + 1 | ||
115 | if #line > 0 then | ||
116 | return line | ||
117 | end | ||
118 | end | ||
119 | end | ||
120 | |||
121 | local function match_linerange(line) | ||
122 | local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)") | ||
123 | if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end | ||
124 | if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end | ||
125 | if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end | ||
126 | return m1, m2, m3, m4 | ||
127 | end | ||
128 | |||
129 | local function match_epoch(str) | ||
130 | return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]") | ||
131 | end | ||
132 | |||
133 | function patch.read_patch(filename, data) | ||
134 | -- define possible file regions that will direct the parser flow | ||
135 | local state = 'header' | ||
136 | -- 'header' - comments before the patch body | ||
137 | -- 'filenames' - lines starting with --- and +++ | ||
138 | -- 'hunkhead' - @@ -R +R @@ sequence | ||
139 | -- 'hunkbody' | ||
140 | -- 'hunkskip' - skipping invalid hunk mode | ||
141 | |||
142 | local all_ok = true | ||
143 | local lineends = {lf=0, crlf=0, cr=0} | ||
144 | local files = {source={}, target={}, epoch={}, hunks={}, fileends={}, hunkends={}} | ||
145 | local nextfileno = 0 | ||
146 | local nexthunkno = 0 --: even if index starts with 0 user messages | ||
147 | -- number hunks from 1 | ||
148 | |||
149 | -- hunkinfo holds parsed values, hunkactual - calculated | ||
150 | local hunkinfo = { | ||
151 | startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil, | ||
152 | invalid=false, text={} | ||
153 | } | ||
154 | local hunkactual = {linessrc=nil, linestgt=nil} | ||
155 | |||
156 | info(format("reading patch %s", filename)) | ||
157 | |||
158 | local fp | ||
159 | if data then | ||
160 | fp = string_as_file(data) | ||
161 | else | ||
162 | fp = filename == '-' and io.stdin or assert(io.open(filename, "rb")) | ||
163 | end | ||
164 | local lineno = 0 | ||
165 | |||
166 | for line in file_lines(fp) do | ||
167 | lineno = lineno + 1 | ||
168 | if state == 'header' then | ||
169 | if startswith(line, "--- ") then | ||
170 | state = 'filenames' | ||
171 | end | ||
172 | -- state is 'header' or 'filenames' | ||
173 | end | ||
174 | if state == 'hunkbody' then | ||
175 | -- skip hunkskip and hunkbody code until definition of hunkhead read | ||
176 | |||
177 | if line:match"^[\r\n]*$" then | ||
178 | -- prepend space to empty lines to interpret them as context properly | ||
179 | line = " " .. line | ||
180 | end | ||
181 | |||
182 | -- process line first | ||
183 | if line:match"^[- +\\]" then | ||
184 | -- gather stats about line endings | ||
185 | local he = files.hunkends[nextfileno] | ||
186 | if endswith(line, "\r\n") then | ||
187 | he.crlf = he.crlf + 1 | ||
188 | elseif endswith(line, "\n") then | ||
189 | he.lf = he.lf + 1 | ||
190 | elseif endswith(line, "\r") then | ||
191 | he.cr = he.cr + 1 | ||
192 | end | ||
193 | if startswith(line, "-") then | ||
194 | hunkactual.linessrc = hunkactual.linessrc + 1 | ||
195 | elseif startswith(line, "+") then | ||
196 | hunkactual.linestgt = hunkactual.linestgt + 1 | ||
197 | elseif startswith(line, "\\") then | ||
198 | -- nothing | ||
199 | else | ||
200 | hunkactual.linessrc = hunkactual.linessrc + 1 | ||
201 | hunkactual.linestgt = hunkactual.linestgt + 1 | ||
202 | end | ||
203 | table.insert(hunkinfo.text, line) | ||
204 | -- todo: handle \ No newline cases | ||
205 | else | ||
206 | warning(format("invalid hunk no.%d at %d for target file %s", | ||
207 | nexthunkno, lineno, files.target[nextfileno])) | ||
208 | -- add hunk status node | ||
209 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | ||
210 | files.hunks[nextfileno][nexthunkno].invalid = true | ||
211 | all_ok = false | ||
212 | state = 'hunkskip' | ||
213 | end | ||
214 | |||
215 | -- check exit conditions | ||
216 | if hunkactual.linessrc > hunkinfo.linessrc or | ||
217 | hunkactual.linestgt > hunkinfo.linestgt | ||
218 | then | ||
219 | warning(format("extra hunk no.%d lines at %d for target %s", | ||
220 | nexthunkno, lineno, files.target[nextfileno])) | ||
221 | -- add hunk status node | ||
222 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | ||
223 | files.hunks[nextfileno][nexthunkno].invalid = true | ||
224 | state = 'hunkskip' | ||
225 | elseif hunkinfo.linessrc == hunkactual.linessrc and | ||
226 | hunkinfo.linestgt == hunkactual.linestgt | ||
227 | then | ||
228 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | ||
229 | state = 'hunkskip' | ||
230 | |||
231 | -- detect mixed window/unix line ends | ||
232 | local ends = files.hunkends[nextfileno] | ||
233 | if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) + | ||
234 | (ends.lf~=0 and 1 or 0) > 1 | ||
235 | then | ||
236 | warning(format("inconsistent line ends in patch hunks for %s", | ||
237 | files.source[nextfileno])) | ||
238 | end | ||
239 | end | ||
240 | -- state is 'hunkbody' or 'hunkskip' | ||
241 | end | ||
242 | |||
243 | if state == 'hunkskip' then | ||
244 | if match_linerange(line) then | ||
245 | state = 'hunkhead' | ||
246 | elseif startswith(line, "--- ") then | ||
247 | state = 'filenames' | ||
248 | if debugmode and #files.source > 0 then | ||
249 | debug(format("- %2d hunks for %s", #files.hunks[nextfileno], | ||
250 | files.source[nextfileno])) | ||
251 | end | ||
252 | end | ||
253 | -- state is 'hunkskip', 'hunkhead', or 'filenames' | ||
254 | end | ||
255 | local advance | ||
256 | if state == 'filenames' then | ||
257 | if startswith(line, "--- ") then | ||
258 | if fun.contains(files.source, nextfileno) then | ||
259 | all_ok = false | ||
260 | warning(format("skipping invalid patch for %s", | ||
261 | files.source[nextfileno+1])) | ||
262 | table.remove(files.source, nextfileno+1) | ||
263 | -- double source filename line is encountered | ||
264 | -- attempt to restart from this second line | ||
265 | end | ||
266 | -- Accept a space as a terminator, like GNU patch does. | ||
267 | -- Breaks patches containing filenames with spaces... | ||
268 | -- FIXME Figure out what does GNU patch do in those cases. | ||
269 | local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)") | ||
270 | if not match then | ||
271 | all_ok = false | ||
272 | warning(format("skipping invalid filename at line %d", lineno+1)) | ||
273 | state = 'header' | ||
274 | else | ||
275 | if match_epoch(rest) then | ||
276 | files.epoch[nextfileno + 1] = true | ||
277 | end | ||
278 | table.insert(files.source, match) | ||
279 | end | ||
280 | elseif not startswith(line, "+++ ") then | ||
281 | if fun.contains(files.source, nextfileno) then | ||
282 | all_ok = false | ||
283 | warning(format("skipping invalid patch with no target for %s", | ||
284 | files.source[nextfileno+1])) | ||
285 | table.remove(files.source, nextfileno+1) | ||
286 | else | ||
287 | -- this should be unreachable | ||
288 | warning("skipping invalid target patch") | ||
289 | end | ||
290 | state = 'header' | ||
291 | else | ||
292 | if fun.contains(files.target, nextfileno) then | ||
293 | all_ok = false | ||
294 | warning(format("skipping invalid patch - double target at line %d", | ||
295 | lineno+1)) | ||
296 | table.remove(files.source, nextfileno+1) | ||
297 | table.remove(files.target, nextfileno+1) | ||
298 | nextfileno = nextfileno - 1 | ||
299 | -- double target filename line is encountered | ||
300 | -- switch back to header state | ||
301 | state = 'header' | ||
302 | else | ||
303 | -- Accept a space as a terminator, like GNU patch does. | ||
304 | -- Breaks patches containing filenames with spaces... | ||
305 | -- FIXME Figure out what does GNU patch do in those cases. | ||
306 | local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$" | ||
307 | local match, rest = line:match(re_filename) | ||
308 | if not match then | ||
309 | all_ok = false | ||
310 | warning(format( | ||
311 | "skipping invalid patch - no target filename at line %d", | ||
312 | lineno+1)) | ||
313 | state = 'header' | ||
314 | else | ||
315 | table.insert(files.target, match) | ||
316 | nextfileno = nextfileno + 1 | ||
317 | if match_epoch(rest) then | ||
318 | files.epoch[nextfileno] = true | ||
319 | end | ||
320 | nexthunkno = 0 | ||
321 | table.insert(files.hunks, {}) | ||
322 | table.insert(files.hunkends, table_copy(lineends)) | ||
323 | table.insert(files.fileends, table_copy(lineends)) | ||
324 | state = 'hunkhead' | ||
325 | advance = true | ||
326 | end | ||
327 | end | ||
328 | end | ||
329 | -- state is 'filenames', 'header', or ('hunkhead' with advance) | ||
330 | end | ||
331 | if not advance and state == 'hunkhead' then | ||
332 | local m1, m2, m3, m4 = match_linerange(line) | ||
333 | if not m1 then | ||
334 | if not fun.contains(files.hunks, nextfileno-1) then | ||
335 | all_ok = false | ||
336 | warning(format("skipping invalid patch with no hunks for file %s", | ||
337 | files.target[nextfileno])) | ||
338 | end | ||
339 | state = 'header' | ||
340 | else | ||
341 | hunkinfo.startsrc = tonumber(m1) | ||
342 | hunkinfo.linessrc = tonumber(m2 or 1) | ||
343 | hunkinfo.starttgt = tonumber(m3) | ||
344 | hunkinfo.linestgt = tonumber(m4 or 1) | ||
345 | hunkinfo.invalid = false | ||
346 | hunkinfo.text = {} | ||
347 | |||
348 | hunkactual.linessrc = 0 | ||
349 | hunkactual.linestgt = 0 | ||
350 | |||
351 | state = 'hunkbody' | ||
352 | nexthunkno = nexthunkno + 1 | ||
353 | end | ||
354 | -- state is 'header' or 'hunkbody' | ||
355 | end | ||
356 | end | ||
357 | if state ~= 'hunkskip' then | ||
358 | warning(format("patch file incomplete - %s", filename)) | ||
359 | all_ok = false | ||
360 | -- os.exit(?) | ||
361 | else | ||
362 | -- duplicated message when an eof is reached | ||
363 | if debugmode and #files.source > 0 then | ||
364 | debug(format("- %2d hunks for %s", #files.hunks[nextfileno], | ||
365 | files.source[nextfileno])) | ||
366 | end | ||
367 | end | ||
368 | |||
369 | local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end | ||
370 | info(format("total files: %d total hunks: %d", #files.source, sum)) | ||
371 | fp:close() | ||
372 | return files, all_ok | ||
373 | end | ||
374 | |||
375 | local function find_hunk(file, h, hno) | ||
376 | for fuzz=0,2 do | ||
377 | local lineno = h.startsrc | ||
378 | for i=0,#file do | ||
379 | local found = true | ||
380 | local location = lineno | ||
381 | for l, hline in ipairs(h.text) do | ||
382 | if l > fuzz then | ||
383 | -- todo: \ No newline at the end of file | ||
384 | if startswith(hline, " ") or startswith(hline, "-") then | ||
385 | local line = file[lineno] | ||
386 | lineno = lineno + 1 | ||
387 | if not line or #line == 0 then | ||
388 | found = false | ||
389 | break | ||
390 | end | ||
391 | if endlstrip(line) ~= endlstrip(hline:sub(2)) then | ||
392 | found = false | ||
393 | break | ||
394 | end | ||
395 | end | ||
396 | end | ||
397 | end | ||
398 | if found then | ||
399 | local offset = location - h.startsrc - fuzz | ||
400 | if offset ~= 0 then | ||
401 | warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz))) | ||
402 | end | ||
403 | h.startsrc = location | ||
404 | h.starttgt = h.starttgt + offset | ||
405 | for _=1,fuzz do | ||
406 | table.remove(h.text, 1) | ||
407 | table.remove(h.text, #h.text) | ||
408 | end | ||
409 | return true | ||
410 | end | ||
411 | lineno = i | ||
412 | end | ||
413 | end | ||
414 | return false | ||
415 | end | ||
416 | |||
417 | local function load_file(filename) | ||
418 | local fp = assert(io.open(filename)) | ||
419 | local file = {} | ||
420 | local readline = file_lines(fp) | ||
421 | while true do | ||
422 | local line = readline() | ||
423 | if not line then break end | ||
424 | table.insert(file, line) | ||
425 | end | ||
426 | fp:close() | ||
427 | return file | ||
428 | end | ||
429 | |||
430 | local function find_hunks(file, hunks) | ||
431 | for hno, h in ipairs(hunks) do | ||
432 | find_hunk(file, h, hno) | ||
433 | end | ||
434 | end | ||
435 | |||
436 | local function check_patched(file, hunks) | ||
437 | local lineno = 1 | ||
438 | local ok, err = pcall(function() | ||
439 | if #file == 0 then | ||
440 | error('nomatch', 0) | ||
441 | end | ||
442 | for hno, h in ipairs(hunks) do | ||
443 | -- skip to line just before hunk starts | ||
444 | if #file < h.starttgt then | ||
445 | error('nomatch', 0) | ||
446 | end | ||
447 | lineno = h.starttgt | ||
448 | for _, hline in ipairs(h.text) do | ||
449 | -- todo: \ No newline at the end of file | ||
450 | if not startswith(hline, "-") and not startswith(hline, "\\") then | ||
451 | local line = file[lineno] | ||
452 | lineno = lineno + 1 | ||
453 | if #line == 0 then | ||
454 | error('nomatch', 0) | ||
455 | end | ||
456 | if endlstrip(line) ~= endlstrip(hline:sub(2)) then | ||
457 | warning(format("file is not patched - failed hunk: %d", hno)) | ||
458 | error('nomatch', 0) | ||
459 | end | ||
460 | end | ||
461 | end | ||
462 | end | ||
463 | end) | ||
464 | -- todo: display failed hunk, i.e. expected/found | ||
465 | return err ~= 'nomatch' | ||
466 | end | ||
467 | |||
468 | local function patch_hunks(srcname, tgtname, hunks) | ||
469 | local src = assert(io.open(srcname, "rb")) | ||
470 | local tgt = assert(io.open(tgtname, "wb")) | ||
471 | |||
472 | local src_readline = file_lines(src) | ||
473 | |||
474 | -- todo: detect linefeeds early - in apply_files routine | ||
475 | -- to handle cases when patch starts right from the first | ||
476 | -- line and no lines are processed. At the moment substituted | ||
477 | -- lineends may not be the same at the start and at the end | ||
478 | -- of patching. Also issue a warning about mixed lineends | ||
479 | |||
480 | local srclineno = 1 | ||
481 | local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0} | ||
482 | for hno, h in ipairs(hunks) do | ||
483 | debug(format("processing hunk %d for file %s", hno, tgtname)) | ||
484 | -- skip to line just before hunk starts | ||
485 | while srclineno < h.startsrc do | ||
486 | local line = src_readline() | ||
487 | -- Python 'U' mode works only with text files | ||
488 | if endswith(line, "\r\n") then | ||
489 | lineends["\r\n"] = lineends["\r\n"] + 1 | ||
490 | elseif endswith(line, "\n") then | ||
491 | lineends["\n"] = lineends["\n"] + 1 | ||
492 | elseif endswith(line, "\r") then | ||
493 | lineends["\r"] = lineends["\r"] + 1 | ||
494 | end | ||
495 | tgt:write(line) | ||
496 | srclineno = srclineno + 1 | ||
497 | end | ||
498 | |||
499 | for _,hline in ipairs(h.text) do | ||
500 | -- todo: check \ No newline at the end of file | ||
501 | if startswith(hline, "-") or startswith(hline, "\\") then | ||
502 | src_readline() | ||
503 | srclineno = srclineno + 1 | ||
504 | else | ||
505 | if not startswith(hline, "+") then | ||
506 | src_readline() | ||
507 | srclineno = srclineno + 1 | ||
508 | end | ||
509 | local line2write = hline:sub(2) | ||
510 | -- detect if line ends are consistent in source file | ||
511 | local sum = 0 | ||
512 | for _,v in pairs(lineends) do if v > 0 then sum=sum+1 end end | ||
513 | if sum == 1 then | ||
514 | local newline | ||
515 | for k,v in pairs(lineends) do if v ~= 0 then newline = k end end | ||
516 | tgt:write(endlstrip(line2write) .. newline) | ||
517 | else -- newlines are mixed or unknown | ||
518 | tgt:write(line2write) | ||
519 | end | ||
520 | end | ||
521 | end | ||
522 | end | ||
523 | for line in src_readline do | ||
524 | tgt:write(line) | ||
525 | end | ||
526 | tgt:close() | ||
527 | src:close() | ||
528 | return true | ||
529 | end | ||
530 | |||
531 | local function strip_dirs(filename, strip) | ||
532 | if strip == nil then return filename end | ||
533 | for _=1,strip do | ||
534 | filename=filename:gsub("^[^/]*/", "") | ||
535 | end | ||
536 | return filename | ||
537 | end | ||
538 | |||
539 | local function write_new_file(filename, hunk) | ||
540 | local fh = io.open(filename, "wb") | ||
541 | if not fh then return false end | ||
542 | for _, hline in ipairs(hunk.text) do | ||
543 | local c = hline:sub(1,1) | ||
544 | if c ~= "+" and c ~= "-" and c ~= " " then | ||
545 | return false, "malformed patch" | ||
546 | end | ||
547 | fh:write(hline:sub(2)) | ||
548 | end | ||
549 | fh:close() | ||
550 | return true | ||
551 | end | ||
552 | |||
553 | local function patch_file(source, target, epoch, hunks, strip, create_delete) | ||
554 | local create_file = false | ||
555 | if create_delete then | ||
556 | local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0 | ||
557 | if is_src_epoch or source == "/dev/null" then | ||
558 | info(format("will create %s", target)) | ||
559 | create_file = true | ||
560 | end | ||
561 | end | ||
562 | if create_file then | ||
563 | return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1]) | ||
564 | end | ||
565 | source = strip_dirs(source, strip) | ||
566 | local f2patch = source | ||
567 | if not exists(f2patch) then | ||
568 | f2patch = strip_dirs(target, strip) | ||
569 | f2patch = fs.absolute_name(f2patch) | ||
570 | if not exists(f2patch) then --FIX:if f2patch nil | ||
571 | warning(format("source/target file does not exist\n--- %s\n+++ %s", | ||
572 | source, f2patch)) | ||
573 | return false | ||
574 | end | ||
575 | end | ||
576 | if not isfile(f2patch) then | ||
577 | warning(format("not a file - %s", f2patch)) | ||
578 | return false | ||
579 | end | ||
580 | |||
581 | source = f2patch | ||
582 | |||
583 | -- validate before patching | ||
584 | local file = load_file(source) | ||
585 | local hunkno = 1 | ||
586 | local hunk = hunks[hunkno] | ||
587 | local hunkfind = {} | ||
588 | local validhunks = 0 | ||
589 | local canpatch = false | ||
590 | local hunklineno | ||
591 | if not file then | ||
592 | return nil, "failed reading file " .. source | ||
593 | end | ||
594 | |||
595 | if create_delete then | ||
596 | if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then | ||
597 | local ok = os.remove(source) | ||
598 | if not ok then | ||
599 | return false | ||
600 | end | ||
601 | info(format("successfully removed %s", source)) | ||
602 | return true | ||
603 | end | ||
604 | end | ||
605 | |||
606 | find_hunks(file, hunks) | ||
607 | |||
608 | local function process_line(line, lineno) | ||
609 | if not hunk or lineno < hunk.startsrc then | ||
610 | return false | ||
611 | end | ||
612 | if lineno == hunk.startsrc then | ||
613 | hunkfind = {} | ||
614 | for _,x in ipairs(hunk.text) do | ||
615 | if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then | ||
616 | hunkfind[#hunkfind+1] = endlstrip(x:sub(2)) | ||
617 | end | ||
618 | end | ||
619 | hunklineno = 1 | ||
620 | |||
621 | -- todo \ No newline at end of file | ||
622 | end | ||
623 | -- check hunks in source file | ||
624 | if lineno < hunk.startsrc + #hunkfind - 1 then | ||
625 | if endlstrip(line) == hunkfind[hunklineno] then | ||
626 | hunklineno = hunklineno + 1 | ||
627 | else | ||
628 | debug(format("hunk no.%d doesn't match source file %s", | ||
629 | hunkno, source)) | ||
630 | -- file may be already patched, but check other hunks anyway | ||
631 | hunkno = hunkno + 1 | ||
632 | if hunkno <= #hunks then | ||
633 | hunk = hunks[hunkno] | ||
634 | return false | ||
635 | else | ||
636 | return true | ||
637 | end | ||
638 | end | ||
639 | end | ||
640 | -- check if processed line is the last line | ||
641 | if lineno == hunk.startsrc + #hunkfind - 1 then | ||
642 | debug(format("file %s hunk no.%d -- is ready to be patched", | ||
643 | source, hunkno)) | ||
644 | hunkno = hunkno + 1 | ||
645 | validhunks = validhunks + 1 | ||
646 | if hunkno <= #hunks then | ||
647 | hunk = hunks[hunkno] | ||
648 | else | ||
649 | if validhunks == #hunks then | ||
650 | -- patch file | ||
651 | canpatch = true | ||
652 | return true | ||
653 | end | ||
654 | end | ||
655 | end | ||
656 | return false | ||
657 | end | ||
658 | |||
659 | local done = false | ||
660 | for lineno, line in ipairs(file) do | ||
661 | done = process_line(line, lineno) | ||
662 | if done then | ||
663 | break | ||
664 | end | ||
665 | end | ||
666 | if not done then | ||
667 | if hunkno <= #hunks and not create_file then | ||
668 | warning(format("premature end of source file %s at hunk %d", | ||
669 | source, hunkno)) | ||
670 | return false | ||
671 | end | ||
672 | end | ||
673 | if validhunks < #hunks then | ||
674 | if check_patched(file, hunks) then | ||
675 | warning(format("already patched %s", source)) | ||
676 | elseif not create_file then | ||
677 | warning(format("source file is different - %s", source)) | ||
678 | return false | ||
679 | end | ||
680 | end | ||
681 | if not canpatch then | ||
682 | return true | ||
683 | end | ||
684 | local backupname = source .. ".orig" | ||
685 | if exists(backupname) then | ||
686 | warning(format("can't backup original file to %s - aborting", | ||
687 | backupname)) | ||
688 | return false | ||
689 | end | ||
690 | local ok = os.rename(source, backupname) | ||
691 | if not ok then | ||
692 | warning(format("failed backing up %s when patching", source)) | ||
693 | return false | ||
694 | end | ||
695 | patch_hunks(backupname, source, hunks) | ||
696 | info(format("successfully patched %s", source)) | ||
697 | os.remove(backupname) | ||
698 | return true | ||
699 | end | ||
700 | |||
701 | function patch.apply_patch(the_patch, strip, create_delete) | ||
702 | local all_ok = true | ||
703 | local total = #the_patch.source | ||
704 | for fileno, source in ipairs(the_patch.source) do | ||
705 | local target = the_patch.target[fileno] | ||
706 | local hunks = the_patch.hunks[fileno] | ||
707 | local epoch = the_patch.epoch[fileno] | ||
708 | info(format("processing %d/%d:\t %s", fileno, total, source)) | ||
709 | local ok = patch_file(source, target, epoch, hunks, strip, create_delete) | ||
710 | all_ok = all_ok and ok | ||
711 | end | ||
712 | -- todo: check for premature eof | ||
713 | return all_ok | ||
714 | end | ||
715 | |||
716 | return patch | ||
diff --git a/src/luarocks/tools/patch.lua b/src/luarocks/tools/patch.lua index 6f36d713..0b109313 100644 --- a/src/luarocks/tools/patch.lua +++ b/src/luarocks/tools/patch.lua | |||
@@ -1,59 +1,88 @@ | |||
1 | --- Patch utility to apply unified diffs. | 1 | local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local assert = _tl_compat and _tl_compat.assert or assert; local debug = _tl_compat and _tl_compat.debug or debug; local io = _tl_compat and _tl_compat.io or io; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local math = _tl_compat and _tl_compat.math or math; local os = _tl_compat and _tl_compat.os or os; local pairs = _tl_compat and _tl_compat.pairs or pairs; local pcall = _tl_compat and _tl_compat.pcall or pcall; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table |
2 | -- | 2 | |
3 | -- http://lua-users.org/wiki/LuaPatch | 3 | |
4 | -- | 4 | |
5 | -- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license). | 5 | |
6 | -- Code is heavily based on the Python-based patch.py version 8.06-1 | 6 | |
7 | -- Copyright (c) 2008 rainforce.org, MIT License | 7 | |
8 | -- Project home: http://code.google.com/p/python-patch/ . | 8 | |
9 | -- Version 0.1 | 9 | |
10 | 10 | ||
11 | local patch = {} | 11 | local patch = {Lineends = {}, Hunk = {}, File = {}, Files = {}, } |
12 | |||
13 | |||
14 | |||
15 | |||
16 | |||
17 | |||
18 | |||
19 | |||
20 | |||
21 | |||
22 | |||
23 | |||
24 | |||
25 | |||
26 | |||
27 | |||
28 | |||
29 | |||
30 | |||
31 | |||
32 | |||
33 | |||
34 | |||
35 | |||
36 | |||
37 | |||
38 | |||
39 | |||
40 | |||
41 | |||
12 | 42 | ||
13 | local fs = require("luarocks.fs") | 43 | local fs = require("luarocks.fs") |
14 | local fun = require("luarocks.fun") | 44 | local fun = require("luarocks.fun") |
15 | 45 | ||
16 | local io = io | ||
17 | local os = os | ||
18 | local string = string | ||
19 | local table = table | ||
20 | local format = string.format | ||
21 | 46 | ||
22 | -- logging | 47 | |
48 | |||
49 | |||
50 | |||
51 | |||
23 | local debugmode = false | 52 | local debugmode = false |
24 | local function debug(_) end | 53 | local function debug(_) end |
25 | local function info(_) end | 54 | local function info(_) end |
26 | local function warning(s) io.stderr:write(s .. '\n') end | 55 | local function warning(s) io.stderr:write(s .. '\n') end |
27 | 56 | ||
28 | -- Returns boolean whether string s2 starts with string s. | 57 | |
29 | local function startswith(s, s2) | 58 | local function startswith(s, s2) |
30 | return s:sub(1, #s2) == s2 | 59 | return s:sub(1, #s2) == s2 |
31 | end | 60 | end |
32 | 61 | ||
33 | -- Returns boolean whether string s2 ends with string s. | 62 | |
34 | local function endswith(s, s2) | 63 | local function endswith(s, s2) |
35 | return #s >= #s2 and s:sub(#s-#s2+1) == s2 | 64 | return #s >= #s2 and s:sub(#s - #s2 + 1) == s2 |
36 | end | 65 | end |
37 | 66 | ||
38 | -- Returns string s after filtering out any new-line characters from end. | 67 | |
39 | local function endlstrip(s) | 68 | local function endlstrip(s) |
40 | return s:gsub('[\r\n]+$', '') | 69 | return s:gsub('[\r\n]+$', '') |
41 | end | 70 | end |
42 | 71 | ||
43 | -- Returns shallow copy of table t. | 72 | |
44 | local function table_copy(t) | 73 | local function table_copy(t) |
45 | local t2 = {} | 74 | local t2 = {} |
46 | for k,v in pairs(t) do t2[k] = v end | 75 | for k, v in pairs(t) do t2[k] = v end |
47 | return t2 | 76 | return t2 |
48 | end | 77 | end |
49 | 78 | ||
50 | local function exists(filename) | 79 | local function exists(filename) |
51 | local fh = io.open(filename) | 80 | local fh = io.open(filename) |
52 | local result = fh ~= nil | 81 | local result = fh ~= nil |
53 | if fh then fh:close() end | 82 | if fh then fh:close() end |
54 | return result | 83 | return result |
55 | end | 84 | end |
56 | local function isfile() return true end --FIX? | 85 | local function isfile() return true end |
57 | 86 | ||
58 | local function string_as_file(s) | 87 | local function string_as_file(s) |
59 | return { | 88 | return { |
@@ -76,641 +105,643 @@ local function string_as_file(s) | |||
76 | } | 105 | } |
77 | end | 106 | end |
78 | 107 | ||
79 | -- | 108 | |
80 | -- file_lines(f) is similar to f:lines() for file f. | 109 | |
81 | -- The main difference is that read_lines includes | 110 | |
82 | -- new-line character sequences ("\n", "\r\n", "\r"), | 111 | |
83 | -- if any, at the end of each line. Embedded "\0" are also handled. | 112 | |
84 | -- Caution: The newline behavior can depend on whether f is opened | 113 | |
85 | -- in binary or ASCII mode. | 114 | |
86 | -- (file_lines - version 20080913) | 115 | |
87 | -- | 116 | |
88 | local function file_lines(f) | 117 | local function file_lines(f) |
89 | local CHUNK_SIZE = 1024 | 118 | local CHUNK_SIZE = 1024 |
90 | local buffer = "" | 119 | local buffer = "" |
91 | local pos_beg = 1 | 120 | local pos_beg = 1 |
92 | return function() | 121 | return function() |
93 | local pos, chars | 122 | local pos, chars |
94 | while 1 do | 123 | while 1 do |
95 | pos, chars = buffer:match('()([\r\n].)', pos_beg) | 124 | pos, chars = buffer:match('()([\r\n].)', pos_beg) |
96 | if pos or not f then | 125 | if pos or not f then |
97 | break | 126 | break |
98 | elseif f then | 127 | elseif f then |
99 | local chunk = f:read(CHUNK_SIZE) | 128 | local chunk = f:read(CHUNK_SIZE) |
100 | if chunk then | 129 | if chunk then |
101 | buffer = buffer:sub(pos_beg) .. chunk | 130 | buffer = buffer:sub(pos_beg) .. chunk |
102 | pos_beg = 1 | 131 | pos_beg = 1 |
103 | else | 132 | else |
104 | f = nil | 133 | f = nil |
105 | end | 134 | end |
135 | end | ||
136 | end | ||
137 | local posi = math.tointeger(pos) | ||
138 | if not posi then | ||
139 | posi = #buffer | ||
140 | elseif chars == '\r\n' then | ||
141 | posi = posi + 1 | ||
142 | end | ||
143 | local line = buffer:sub(pos_beg, posi) | ||
144 | pos_beg = posi + 1 | ||
145 | if #line > 0 then | ||
146 | return line | ||
106 | end | 147 | end |
107 | end | 148 | end |
108 | if not pos then | ||
109 | pos = #buffer | ||
110 | elseif chars == '\r\n' then | ||
111 | pos = pos + 1 | ||
112 | end | ||
113 | local line = buffer:sub(pos_beg, pos) | ||
114 | pos_beg = pos + 1 | ||
115 | if #line > 0 then | ||
116 | return line | ||
117 | end | ||
118 | end | ||
119 | end | 149 | end |
120 | 150 | ||
121 | local function match_linerange(line) | 151 | local function match_linerange(line) |
122 | local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)") | 152 | local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)") |
123 | if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end | 153 | if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end |
124 | if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end | 154 | if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end |
125 | if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end | 155 | if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end |
126 | return m1, m2, m3, m4 | 156 | return m1, m2, m3, m4 |
127 | end | 157 | end |
128 | 158 | ||
129 | local function match_epoch(str) | 159 | local function match_epoch(str) |
130 | return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]") | 160 | return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]") |
131 | end | 161 | end |
132 | 162 | ||
133 | function patch.read_patch(filename, data) | 163 | function patch.read_patch(filename, data) |
134 | -- define possible file regions that will direct the parser flow | ||
135 | local state = 'header' | ||
136 | -- 'header' - comments before the patch body | ||
137 | -- 'filenames' - lines starting with --- and +++ | ||
138 | -- 'hunkhead' - @@ -R +R @@ sequence | ||
139 | -- 'hunkbody' | ||
140 | -- 'hunkskip' - skipping invalid hunk mode | ||
141 | |||
142 | local all_ok = true | ||
143 | local lineends = {lf=0, crlf=0, cr=0} | ||
144 | local files = {source={}, target={}, epoch={}, hunks={}, fileends={}, hunkends={}} | ||
145 | local nextfileno = 0 | ||
146 | local nexthunkno = 0 --: even if index starts with 0 user messages | ||
147 | -- number hunks from 1 | ||
148 | |||
149 | -- hunkinfo holds parsed values, hunkactual - calculated | ||
150 | local hunkinfo = { | ||
151 | startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil, | ||
152 | invalid=false, text={} | ||
153 | } | ||
154 | local hunkactual = {linessrc=nil, linestgt=nil} | ||
155 | |||
156 | info(format("reading patch %s", filename)) | ||
157 | |||
158 | local fp | ||
159 | if data then | ||
160 | fp = string_as_file(data) | ||
161 | else | ||
162 | fp = filename == '-' and io.stdin or assert(io.open(filename, "rb")) | ||
163 | end | ||
164 | local lineno = 0 | ||
165 | |||
166 | for line in file_lines(fp) do | ||
167 | lineno = lineno + 1 | ||
168 | if state == 'header' then | ||
169 | if startswith(line, "--- ") then | ||
170 | state = 'filenames' | ||
171 | end | ||
172 | -- state is 'header' or 'filenames' | ||
173 | end | ||
174 | if state == 'hunkbody' then | ||
175 | -- skip hunkskip and hunkbody code until definition of hunkhead read | ||
176 | |||
177 | if line:match"^[\r\n]*$" then | ||
178 | -- prepend space to empty lines to interpret them as context properly | ||
179 | line = " " .. line | ||
180 | end | ||
181 | 164 | ||
182 | -- process line first | 165 | local state = 'header' |
183 | if line:match"^[- +\\]" then | 166 | |
184 | -- gather stats about line endings | 167 | |
185 | local he = files.hunkends[nextfileno] | 168 | |
186 | if endswith(line, "\r\n") then | 169 | |
187 | he.crlf = he.crlf + 1 | 170 | |
188 | elseif endswith(line, "\n") then | 171 | |
189 | he.lf = he.lf + 1 | 172 | local all_ok = true |
190 | elseif endswith(line, "\r") then | 173 | local lineends = { lf = 0, crlf = 0, cr = 0 } |
191 | he.cr = he.cr + 1 | 174 | local files = { source = {}, target = {}, epoch = {}, hunks = {}, fileends = {}, hunkends = {} } |
192 | end | 175 | local nextfileno = 0 |
193 | if startswith(line, "-") then | 176 | local nexthunkno = 0 |
194 | hunkactual.linessrc = hunkactual.linessrc + 1 | 177 | |
195 | elseif startswith(line, "+") then | 178 | |
196 | hunkactual.linestgt = hunkactual.linestgt + 1 | 179 | |
197 | elseif startswith(line, "\\") then | 180 | local hunkinfo = { |
198 | -- nothing | 181 | startsrc = nil, linessrc = nil, starttgt = nil, linestgt = nil, |
199 | else | 182 | invalid = false, text = {}, |
200 | hunkactual.linessrc = hunkactual.linessrc + 1 | 183 | } |
201 | hunkactual.linestgt = hunkactual.linestgt + 1 | 184 | local hunkactual = { linessrc = nil, linestgt = nil } |
202 | end | 185 | |
203 | table.insert(hunkinfo.text, line) | 186 | info(string.format("reading patch %s", filename)) |
204 | -- todo: handle \ No newline cases | 187 | |
205 | else | 188 | local fp |
206 | warning(format("invalid hunk no.%d at %d for target file %s", | 189 | if data then |
207 | nexthunkno, lineno, files.target[nextfileno])) | 190 | fp = string_as_file(data) |
208 | -- add hunk status node | 191 | else |
209 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | 192 | fp = filename == '-' and io.stdin or assert(io.open(filename, "rb")) |
210 | files.hunks[nextfileno][nexthunkno].invalid = true | 193 | end |
211 | all_ok = false | 194 | local lineno = 0 |
212 | state = 'hunkskip' | 195 | |
196 | for line in file_lines(fp) do | ||
197 | lineno = lineno + 1 | ||
198 | if state == 'header' then | ||
199 | if startswith(line, "--- ") then | ||
200 | state = 'filenames' | ||
201 | end | ||
202 | |||
213 | end | 203 | end |
204 | if state == 'hunkbody' then | ||
205 | |||
206 | |||
207 | if line:match("^[\r\n]*$") then | ||
208 | |||
209 | line = " " .. line | ||
210 | end | ||
211 | |||
212 | |||
213 | if line:match("^[- +\\]") then | ||
214 | |||
215 | local he = files.hunkends[nextfileno] | ||
216 | if endswith(line, "\r\n") then | ||
217 | he.crlf = he.crlf + 1 | ||
218 | elseif endswith(line, "\n") then | ||
219 | he.lf = he.lf + 1 | ||
220 | elseif endswith(line, "\r") then | ||
221 | he.cr = he.cr + 1 | ||
222 | end | ||
223 | if startswith(line, "-") then | ||
224 | hunkactual.linessrc = hunkactual.linessrc + 1 | ||
225 | elseif startswith(line, "+") then | ||
226 | hunkactual.linestgt = hunkactual.linestgt + 1 | ||
227 | elseif startswith(line, "\\") then | ||
228 | |||
229 | else | ||
230 | hunkactual.linessrc = hunkactual.linessrc + 1 | ||
231 | hunkactual.linestgt = hunkactual.linestgt + 1 | ||
232 | end | ||
233 | table.insert(hunkinfo.text, line) | ||
234 | |||
235 | else | ||
236 | warning(string.format("invalid hunk no.%d at %d for target file %s", | ||
237 | nexthunkno, lineno, files.target[nextfileno])) | ||
238 | |||
239 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | ||
240 | files.hunks[nextfileno][nexthunkno].invalid = true | ||
241 | all_ok = false | ||
242 | state = 'hunkskip' | ||
243 | end | ||
244 | |||
245 | |||
246 | if hunkactual.linessrc > hunkinfo.linessrc or | ||
247 | hunkactual.linestgt > hunkinfo.linestgt then | ||
248 | |||
249 | warning(string.format("extra hunk no.%d lines at %d for target %s", | ||
250 | nexthunkno, lineno, files.target[nextfileno])) | ||
251 | |||
252 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | ||
253 | files.hunks[nextfileno][nexthunkno].invalid = true | ||
254 | state = 'hunkskip' | ||
255 | elseif hunkinfo.linessrc == hunkactual.linessrc and | ||
256 | hunkinfo.linestgt == hunkactual.linestgt then | ||
257 | |||
258 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | ||
259 | state = 'hunkskip' | ||
260 | |||
261 | |||
262 | local ends = files.hunkends[nextfileno] | ||
263 | if (ends.cr ~= 0 and 1 or 0) + (ends.crlf ~= 0 and 1 or 0) + | ||
264 | (ends.lf ~= 0 and 1 or 0) > 1 then | ||
265 | |||
266 | warning(string.format("inconsistent line ends in patch hunks for %s", | ||
267 | files.source[nextfileno])) | ||
268 | end | ||
269 | end | ||
214 | 270 | ||
215 | -- check exit conditions | ||
216 | if hunkactual.linessrc > hunkinfo.linessrc or | ||
217 | hunkactual.linestgt > hunkinfo.linestgt | ||
218 | then | ||
219 | warning(format("extra hunk no.%d lines at %d for target %s", | ||
220 | nexthunkno, lineno, files.target[nextfileno])) | ||
221 | -- add hunk status node | ||
222 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | ||
223 | files.hunks[nextfileno][nexthunkno].invalid = true | ||
224 | state = 'hunkskip' | ||
225 | elseif hunkinfo.linessrc == hunkactual.linessrc and | ||
226 | hunkinfo.linestgt == hunkactual.linestgt | ||
227 | then | ||
228 | table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) | ||
229 | state = 'hunkskip' | ||
230 | |||
231 | -- detect mixed window/unix line ends | ||
232 | local ends = files.hunkends[nextfileno] | ||
233 | if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) + | ||
234 | (ends.lf~=0 and 1 or 0) > 1 | ||
235 | then | ||
236 | warning(format("inconsistent line ends in patch hunks for %s", | ||
237 | files.source[nextfileno])) | ||
238 | end | ||
239 | end | 271 | end |
240 | -- state is 'hunkbody' or 'hunkskip' | 272 | |
241 | end | 273 | if state == 'hunkskip' then |
242 | 274 | if match_linerange(line) then | |
243 | if state == 'hunkskip' then | 275 | state = 'hunkhead' |
244 | if match_linerange(line) then | 276 | elseif startswith(line, "--- ") then |
245 | state = 'hunkhead' | 277 | state = 'filenames' |
246 | elseif startswith(line, "--- ") then | 278 | if debugmode and #files.source > 0 then |
247 | state = 'filenames' | 279 | debug(string.format("- %2d hunks for %s", #files.hunks[nextfileno], |
248 | if debugmode and #files.source > 0 then | 280 | files.source[nextfileno])) |
249 | debug(format("- %2d hunks for %s", #files.hunks[nextfileno], | 281 | end |
250 | files.source[nextfileno])) | 282 | end |
251 | end | 283 | |
252 | end | 284 | end |
253 | -- state is 'hunkskip', 'hunkhead', or 'filenames' | 285 | local advance |
254 | end | 286 | if state == 'filenames' then |
255 | local advance | 287 | if startswith(line, "--- ") then |
256 | if state == 'filenames' then | 288 | if fun.contains(files.source, nextfileno) then |
257 | if startswith(line, "--- ") then | 289 | all_ok = false |
258 | if fun.contains(files.source, nextfileno) then | 290 | warning(string.format("skipping invalid patch for %s", |
259 | all_ok = false | 291 | files.source[nextfileno + 1])) |
260 | warning(format("skipping invalid patch for %s", | 292 | table.remove(files.source, nextfileno + 1) |
261 | files.source[nextfileno+1])) | 293 | |
262 | table.remove(files.source, nextfileno+1) | 294 | |
263 | -- double source filename line is encountered | 295 | end |
264 | -- attempt to restart from this second line | 296 | |
265 | end | 297 | |
266 | -- Accept a space as a terminator, like GNU patch does. | 298 | |
267 | -- Breaks patches containing filenames with spaces... | 299 | local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)") |
268 | -- FIXME Figure out what does GNU patch do in those cases. | 300 | if not match then |
269 | local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)") | 301 | all_ok = false |
270 | if not match then | 302 | warning(string.format("skipping invalid filename at line %d", lineno + 1)) |
271 | all_ok = false | 303 | state = 'header' |
272 | warning(format("skipping invalid filename at line %d", lineno+1)) | 304 | else |
273 | state = 'header' | 305 | if match_epoch(rest) then |
274 | else | 306 | files.epoch[nextfileno + 1] = true |
275 | if match_epoch(rest) then | 307 | end |
276 | files.epoch[nextfileno + 1] = true | 308 | table.insert(files.source, match) |
277 | end | 309 | end |
278 | table.insert(files.source, match) | 310 | elseif not startswith(line, "+++ ") then |
279 | end | 311 | if fun.contains(files.source, nextfileno) then |
280 | elseif not startswith(line, "+++ ") then | 312 | all_ok = false |
281 | if fun.contains(files.source, nextfileno) then | 313 | warning(string.format("skipping invalid patch with no target for %s", |
282 | all_ok = false | 314 | files.source[nextfileno + 1])) |
283 | warning(format("skipping invalid patch with no target for %s", | 315 | table.remove(files.source, nextfileno + 1) |
284 | files.source[nextfileno+1])) | 316 | else |
285 | table.remove(files.source, nextfileno+1) | 317 | |
286 | else | 318 | warning("skipping invalid target patch") |
287 | -- this should be unreachable | 319 | end |
288 | warning("skipping invalid target patch") | ||
289 | end | ||
290 | state = 'header' | ||
291 | else | ||
292 | if fun.contains(files.target, nextfileno) then | ||
293 | all_ok = false | ||
294 | warning(format("skipping invalid patch - double target at line %d", | ||
295 | lineno+1)) | ||
296 | table.remove(files.source, nextfileno+1) | ||
297 | table.remove(files.target, nextfileno+1) | ||
298 | nextfileno = nextfileno - 1 | ||
299 | -- double target filename line is encountered | ||
300 | -- switch back to header state | ||
301 | state = 'header' | ||
302 | else | ||
303 | -- Accept a space as a terminator, like GNU patch does. | ||
304 | -- Breaks patches containing filenames with spaces... | ||
305 | -- FIXME Figure out what does GNU patch do in those cases. | ||
306 | local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$" | ||
307 | local match, rest = line:match(re_filename) | ||
308 | if not match then | ||
309 | all_ok = false | ||
310 | warning(format( | ||
311 | "skipping invalid patch - no target filename at line %d", | ||
312 | lineno+1)) | ||
313 | state = 'header' | 320 | state = 'header' |
314 | else | 321 | else |
315 | table.insert(files.target, match) | 322 | if fun.contains(files.target, nextfileno) then |
316 | nextfileno = nextfileno + 1 | 323 | all_ok = false |
317 | if match_epoch(rest) then | 324 | warning(string.format("skipping invalid patch - double target at line %d", |
318 | files.epoch[nextfileno] = true | 325 | lineno + 1)) |
326 | table.remove(files.source, nextfileno + 1) | ||
327 | table.remove(files.target, nextfileno + 1) | ||
328 | nextfileno = nextfileno - 1 | ||
329 | |||
330 | |||
331 | state = 'header' | ||
332 | else | ||
333 | |||
334 | |||
335 | |||
336 | local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$" | ||
337 | local match, rest = line:match(re_filename) | ||
338 | if not match then | ||
339 | all_ok = false | ||
340 | warning(string.format( | ||
341 | "skipping invalid patch - no target filename at line %d", | ||
342 | lineno + 1)) | ||
343 | state = 'header' | ||
344 | else | ||
345 | table.insert(files.target, match) | ||
346 | nextfileno = nextfileno + 1 | ||
347 | if match_epoch(rest) then | ||
348 | files.epoch[nextfileno] = true | ||
349 | end | ||
350 | nexthunkno = 0 | ||
351 | table.insert(files.hunks, {}) | ||
352 | table.insert(files.hunkends, table_copy(lineends)) | ||
353 | table.insert(files.fileends, table_copy(lineends)) | ||
354 | state = 'hunkhead' | ||
355 | advance = true | ||
356 | end | ||
319 | end | 357 | end |
320 | nexthunkno = 0 | 358 | end |
321 | table.insert(files.hunks, {}) | 359 | |
322 | table.insert(files.hunkends, table_copy(lineends)) | ||
323 | table.insert(files.fileends, table_copy(lineends)) | ||
324 | state = 'hunkhead' | ||
325 | advance = true | ||
326 | end | ||
327 | end | ||
328 | end | 360 | end |
329 | -- state is 'filenames', 'header', or ('hunkhead' with advance) | 361 | if not advance and state == 'hunkhead' then |
330 | end | 362 | local m1, m2, m3, m4 = match_linerange(line) |
331 | if not advance and state == 'hunkhead' then | 363 | if not m1 then |
332 | local m1, m2, m3, m4 = match_linerange(line) | 364 | if not fun.contains(files.hunks, nextfileno - 1) then |
333 | if not m1 then | 365 | all_ok = false |
334 | if not fun.contains(files.hunks, nextfileno-1) then | 366 | warning(string.format("skipping invalid patch with no hunks for file %s", |
335 | all_ok = false | 367 | files.target[nextfileno])) |
336 | warning(format("skipping invalid patch with no hunks for file %s", | 368 | end |
337 | files.target[nextfileno])) | 369 | state = 'header' |
338 | end | 370 | else |
339 | state = 'header' | 371 | hunkinfo.startsrc = tonumber(m1) |
340 | else | 372 | hunkinfo.linessrc = tonumber(tonumber(m2) or 1) |
341 | hunkinfo.startsrc = tonumber(m1) | 373 | hunkinfo.starttgt = tonumber(m3) |
342 | hunkinfo.linessrc = tonumber(m2 or 1) | 374 | hunkinfo.linestgt = tonumber(tonumber(m4) or 1) |
343 | hunkinfo.starttgt = tonumber(m3) | 375 | hunkinfo.invalid = false |
344 | hunkinfo.linestgt = tonumber(m4 or 1) | 376 | hunkinfo.text = {} |
345 | hunkinfo.invalid = false | 377 | |
346 | hunkinfo.text = {} | 378 | hunkactual.linessrc = 0 |
347 | 379 | hunkactual.linestgt = 0 | |
348 | hunkactual.linessrc = 0 | 380 | |
349 | hunkactual.linestgt = 0 | 381 | state = 'hunkbody' |
350 | 382 | nexthunkno = nexthunkno + 1 | |
351 | state = 'hunkbody' | 383 | end |
352 | nexthunkno = nexthunkno + 1 | 384 | |
385 | end | ||
386 | end | ||
387 | if state ~= 'hunkskip' then | ||
388 | warning(string.format("patch file incomplete - %s", filename)) | ||
389 | all_ok = false | ||
390 | |||
391 | else | ||
392 | |||
393 | if debugmode and #files.source > 0 then | ||
394 | debug(string.format("- %2d hunks for %s", #files.hunks[nextfileno], | ||
395 | files.source[nextfileno])) | ||
353 | end | 396 | end |
354 | -- state is 'header' or 'hunkbody' | 397 | end |
355 | end | 398 | |
356 | end | 399 | local sum = 0; for _, hset in ipairs(files.hunks) do sum = sum + #hset end |
357 | if state ~= 'hunkskip' then | 400 | info(string.format("total files: %d total hunks: %d", #files.source, sum)) |
358 | warning(format("patch file incomplete - %s", filename)) | 401 | fp:close() |
359 | all_ok = false | 402 | return files, all_ok |
360 | -- os.exit(?) | ||
361 | else | ||
362 | -- duplicated message when an eof is reached | ||
363 | if debugmode and #files.source > 0 then | ||
364 | debug(format("- %2d hunks for %s", #files.hunks[nextfileno], | ||
365 | files.source[nextfileno])) | ||
366 | end | ||
367 | end | ||
368 | |||
369 | local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end | ||
370 | info(format("total files: %d total hunks: %d", #files.source, sum)) | ||
371 | fp:close() | ||
372 | return files, all_ok | ||
373 | end | 403 | end |
374 | 404 | ||
375 | local function find_hunk(file, h, hno) | 405 | local function find_hunk(file, h, hno) |
376 | for fuzz=0,2 do | 406 | for fuzz = 0, 2 do |
377 | local lineno = h.startsrc | 407 | local lineno = h.startsrc |
378 | for i=0,#file do | 408 | for i = 0, #file do |
379 | local found = true | 409 | local found = true |
380 | local location = lineno | 410 | local location = lineno |
381 | for l, hline in ipairs(h.text) do | 411 | for l, hline in ipairs(h.text) do |
382 | if l > fuzz then | 412 | if l > fuzz then |
383 | -- todo: \ No newline at the end of file | 413 | |
384 | if startswith(hline, " ") or startswith(hline, "-") then | 414 | if startswith(hline, " ") or startswith(hline, "-") then |
385 | local line = file[lineno] | 415 | local line = file[lineno] |
386 | lineno = lineno + 1 | 416 | lineno = lineno + 1 |
387 | if not line or #line == 0 then | 417 | if not line or #line == 0 then |
388 | found = false | 418 | found = false |
389 | break | 419 | break |
420 | end | ||
421 | if endlstrip(line) ~= endlstrip(hline:sub(2)) then | ||
422 | found = false | ||
423 | break | ||
424 | end | ||
425 | end | ||
426 | end | ||
427 | end | ||
428 | if found then | ||
429 | local offset = location - h.startsrc - fuzz | ||
430 | if offset ~= 0 then | ||
431 | warning(string.format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or string.format(" (fuzz %d)", fuzz))) | ||
390 | end | 432 | end |
391 | if endlstrip(line) ~= endlstrip(hline:sub(2)) then | 433 | h.startsrc = location |
392 | found = false | 434 | h.starttgt = h.starttgt + offset |
393 | break | 435 | for _ = 1, fuzz do |
436 | table.remove(h.text, 1) | ||
437 | table.remove(h.text, #h.text) | ||
394 | end | 438 | end |
395 | end | 439 | return true |
396 | end | 440 | end |
397 | end | 441 | lineno = i |
398 | if found then | ||
399 | local offset = location - h.startsrc - fuzz | ||
400 | if offset ~= 0 then | ||
401 | warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz))) | ||
402 | end | ||
403 | h.startsrc = location | ||
404 | h.starttgt = h.starttgt + offset | ||
405 | for _=1,fuzz do | ||
406 | table.remove(h.text, 1) | ||
407 | table.remove(h.text, #h.text) | ||
408 | end | ||
409 | return true | ||
410 | end | 442 | end |
411 | lineno = i | 443 | end |
412 | end | 444 | return false |
413 | end | ||
414 | return false | ||
415 | end | 445 | end |
416 | 446 | ||
417 | local function load_file(filename) | 447 | local function load_file(filename) |
418 | local fp = assert(io.open(filename)) | 448 | local fp = assert(io.open(filename)) |
419 | local file = {} | 449 | local file = {} |
420 | local readline = file_lines(fp) | 450 | local readline = file_lines(fp) |
421 | while true do | 451 | while true do |
422 | local line = readline() | 452 | local line = readline() |
423 | if not line then break end | 453 | if not line then break end |
424 | table.insert(file, line) | 454 | table.insert(file, line) |
425 | end | 455 | end |
426 | fp:close() | 456 | fp:close() |
427 | return file | 457 | return file |
428 | end | 458 | end |
429 | 459 | ||
430 | local function find_hunks(file, hunks) | 460 | local function find_hunks(file, hunks) |
431 | for hno, h in ipairs(hunks) do | 461 | for hno, h in ipairs(hunks) do |
432 | find_hunk(file, h, hno) | 462 | find_hunk(file, h, hno) |
433 | end | 463 | end |
434 | end | 464 | end |
435 | 465 | ||
436 | local function check_patched(file, hunks) | 466 | local function check_patched(file, hunks) |
437 | local lineno = 1 | 467 | local lineno = 1 |
438 | local ok, err = pcall(function() | 468 | local ok, err = pcall(function() |
439 | if #file == 0 then | 469 | if #file == 0 then |
440 | error('nomatch', 0) | 470 | error('nomatch', 0) |
441 | end | ||
442 | for hno, h in ipairs(hunks) do | ||
443 | -- skip to line just before hunk starts | ||
444 | if #file < h.starttgt then | ||
445 | error('nomatch', 0) | ||
446 | end | 471 | end |
447 | lineno = h.starttgt | 472 | for hno, h in ipairs(hunks) do |
448 | for _, hline in ipairs(h.text) do | 473 | |
449 | -- todo: \ No newline at the end of file | 474 | if #file < h.starttgt then |
450 | if not startswith(hline, "-") and not startswith(hline, "\\") then | ||
451 | local line = file[lineno] | ||
452 | lineno = lineno + 1 | ||
453 | if #line == 0 then | ||
454 | error('nomatch', 0) | ||
455 | end | ||
456 | if endlstrip(line) ~= endlstrip(hline:sub(2)) then | ||
457 | warning(format("file is not patched - failed hunk: %d", hno)) | ||
458 | error('nomatch', 0) | 475 | error('nomatch', 0) |
459 | end | 476 | end |
460 | end | 477 | lineno = h.starttgt |
478 | for _, hline in ipairs(h.text) do | ||
479 | |||
480 | if not startswith(hline, "-") and not startswith(hline, "\\") then | ||
481 | local line = file[lineno] | ||
482 | lineno = lineno + 1 | ||
483 | if #line == 0 then | ||
484 | error('nomatch', 0) | ||
485 | end | ||
486 | if endlstrip(line) ~= endlstrip(hline:sub(2)) then | ||
487 | warning(string.format("file is not patched - failed hunk: %d", hno)) | ||
488 | error('nomatch', 0) | ||
489 | end | ||
490 | end | ||
491 | end | ||
461 | end | 492 | end |
462 | end | 493 | end) |
463 | end) | 494 | |
464 | -- todo: display failed hunk, i.e. expected/found | 495 | return err ~= 'nomatch' |
465 | return err ~= 'nomatch' | ||
466 | end | 496 | end |
467 | 497 | ||
468 | local function patch_hunks(srcname, tgtname, hunks) | 498 | local function patch_hunks(srcname, tgtname, hunks) |
469 | local src = assert(io.open(srcname, "rb")) | 499 | local src = assert(io.open(srcname, "rb")) |
470 | local tgt = assert(io.open(tgtname, "wb")) | 500 | local tgt = assert(io.open(tgtname, "wb")) |
471 | 501 | ||
472 | local src_readline = file_lines(src) | 502 | local src_readline = file_lines(src) |
473 | 503 | ||
474 | -- todo: detect linefeeds early - in apply_files routine | 504 | |
475 | -- to handle cases when patch starts right from the first | 505 | |
476 | -- line and no lines are processed. At the moment substituted | 506 | |
477 | -- lineends may not be the same at the start and at the end | 507 | |
478 | -- of patching. Also issue a warning about mixed lineends | 508 | |
479 | 509 | ||
480 | local srclineno = 1 | 510 | local srclineno = 1 |
481 | local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0} | 511 | local lineends = { ['\n'] = 0, ['\r\n'] = 0, ['\r'] = 0 } |
482 | for hno, h in ipairs(hunks) do | 512 | for hno, h in ipairs(hunks) do |
483 | debug(format("processing hunk %d for file %s", hno, tgtname)) | 513 | debug(string.format("processing hunk %d for file %s", hno, tgtname)) |
484 | -- skip to line just before hunk starts | 514 | |
485 | while srclineno < h.startsrc do | 515 | while srclineno < h.startsrc do |
486 | local line = src_readline() | 516 | local line = src_readline() |
487 | -- Python 'U' mode works only with text files | 517 | |
488 | if endswith(line, "\r\n") then | 518 | if endswith(line, "\r\n") then |
489 | lineends["\r\n"] = lineends["\r\n"] + 1 | 519 | lineends["\r\n"] = lineends["\r\n"] + 1 |
490 | elseif endswith(line, "\n") then | 520 | elseif endswith(line, "\n") then |
491 | lineends["\n"] = lineends["\n"] + 1 | 521 | lineends["\n"] = lineends["\n"] + 1 |
492 | elseif endswith(line, "\r") then | 522 | elseif endswith(line, "\r") then |
493 | lineends["\r"] = lineends["\r"] + 1 | 523 | lineends["\r"] = lineends["\r"] + 1 |
524 | end | ||
525 | tgt:write(line) | ||
526 | srclineno = srclineno + 1 | ||
494 | end | 527 | end |
495 | tgt:write(line) | 528 | |
496 | srclineno = srclineno + 1 | 529 | for _, hline in ipairs(h.text) do |
497 | end | 530 | |
498 | 531 | if startswith(hline, "-") or startswith(hline, "\\") then | |
499 | for _,hline in ipairs(h.text) do | 532 | src_readline() |
500 | -- todo: check \ No newline at the end of file | 533 | srclineno = srclineno + 1 |
501 | if startswith(hline, "-") or startswith(hline, "\\") then | 534 | else |
502 | src_readline() | 535 | if not startswith(hline, "+") then |
503 | srclineno = srclineno + 1 | 536 | src_readline() |
504 | else | 537 | srclineno = srclineno + 1 |
505 | if not startswith(hline, "+") then | 538 | end |
506 | src_readline() | 539 | local line2write = hline:sub(2) |
507 | srclineno = srclineno + 1 | 540 | |
508 | end | 541 | local sum = 0 |
509 | local line2write = hline:sub(2) | 542 | for _, v in pairs(lineends) do if v > 0 then sum = sum + 1 end end |
510 | -- detect if line ends are consistent in source file | 543 | if sum == 1 then |
511 | local sum = 0 | 544 | local newline |
512 | for _,v in pairs(lineends) do if v > 0 then sum=sum+1 end end | 545 | for k, v in pairs(lineends) do if v ~= 0 then newline = k end end |
513 | if sum == 1 then | 546 | tgt:write(endlstrip(line2write) .. newline) |
514 | local newline | 547 | else |
515 | for k,v in pairs(lineends) do if v ~= 0 then newline = k end end | 548 | tgt:write(line2write) |
516 | tgt:write(endlstrip(line2write) .. newline) | 549 | end |
517 | else -- newlines are mixed or unknown | 550 | end |
518 | tgt:write(line2write) | ||
519 | end | ||
520 | end | 551 | end |
521 | end | 552 | end |
522 | end | 553 | for line in src_readline do |
523 | for line in src_readline do | 554 | tgt:write(line) |
524 | tgt:write(line) | 555 | end |
525 | end | 556 | tgt:close() |
526 | tgt:close() | 557 | src:close() |
527 | src:close() | 558 | return true |
528 | return true | ||
529 | end | 559 | end |
530 | 560 | ||
531 | local function strip_dirs(filename, strip) | 561 | local function strip_dirs(filename, strip) |
532 | if strip == nil then return filename end | 562 | if strip == nil then return filename end |
533 | for _=1,strip do | 563 | for _ = 1, strip do |
534 | filename=filename:gsub("^[^/]*/", "") | 564 | filename = filename:gsub("^[^/]*/", "") |
535 | end | 565 | end |
536 | return filename | 566 | return filename |
537 | end | 567 | end |
538 | 568 | ||
539 | local function write_new_file(filename, hunk) | 569 | local function write_new_file(filename, hunk) |
540 | local fh = io.open(filename, "wb") | 570 | local fh = io.open(filename, "wb") |
541 | if not fh then return false end | 571 | if not fh then return false end |
542 | for _, hline in ipairs(hunk.text) do | 572 | for _, hline in ipairs(hunk.text) do |
543 | local c = hline:sub(1,1) | 573 | local c = hline:sub(1, 1) |
544 | if c ~= "+" and c ~= "-" and c ~= " " then | 574 | if c ~= "+" and c ~= "-" and c ~= " " then |
545 | return false, "malformed patch" | 575 | return false, "malformed patch" |
546 | end | 576 | end |
547 | fh:write(hline:sub(2)) | 577 | fh:write(hline:sub(2)) |
548 | end | 578 | end |
549 | fh:close() | 579 | fh:close() |
550 | return true | 580 | return true |
551 | end | 581 | end |
552 | 582 | ||
553 | local function patch_file(source, target, epoch, hunks, strip, create_delete) | 583 | local function patch_file(source, target, epoch, hunks, strip, create_delete) |
554 | local create_file = false | 584 | local create_file = false |
555 | if create_delete then | 585 | if create_delete then |
556 | local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0 | 586 | local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0 |
557 | if is_src_epoch or source == "/dev/null" then | 587 | if is_src_epoch or source == "/dev/null" then |
558 | info(format("will create %s", target)) | 588 | info(string.format("will create %s", target)) |
559 | create_file = true | 589 | create_file = true |
560 | end | 590 | end |
561 | end | 591 | end |
562 | if create_file then | 592 | if create_file then |
563 | return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1]) | 593 | return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1]) |
564 | end | 594 | end |
565 | source = strip_dirs(source, strip) | 595 | source = strip_dirs(source, strip) |
566 | local f2patch = source | 596 | local f2patch = source |
567 | if not exists(f2patch) then | 597 | if not exists(f2patch) then |
568 | f2patch = strip_dirs(target, strip) | 598 | f2patch = strip_dirs(target, strip) |
569 | f2patch = fs.absolute_name(f2patch) | 599 | f2patch = fs.absolute_name(f2patch) |
570 | if not exists(f2patch) then --FIX:if f2patch nil | 600 | if not exists(f2patch) then |
571 | warning(format("source/target file does not exist\n--- %s\n+++ %s", | 601 | warning(string.format("source/target file does not exist\n--- %s\n+++ %s", |
572 | source, f2patch)) | 602 | source, f2patch)) |
603 | return false | ||
604 | end | ||
605 | end | ||
606 | |||
607 | if not isfile() then | ||
608 | warning(string.format("not a file - %s", f2patch)) | ||
573 | return false | 609 | return false |
574 | end | 610 | end |
575 | end | 611 | |
576 | if not isfile(f2patch) then | 612 | source = f2patch |
577 | warning(format("not a file - %s", f2patch)) | 613 | |
578 | return false | 614 | |
579 | end | 615 | local file = load_file(source) |
580 | 616 | local hunkno = 1 | |
581 | source = f2patch | 617 | local hunk = hunks[hunkno] |
582 | 618 | local hunkfind = {} | |
583 | -- validate before patching | 619 | local validhunks = 0 |
584 | local file = load_file(source) | 620 | local canpatch = false |
585 | local hunkno = 1 | 621 | local hunklineno |
586 | local hunk = hunks[hunkno] | 622 | if not file then |
587 | local hunkfind = {} | 623 | return nil, "failed reading file " .. source |
588 | local validhunks = 0 | 624 | end |
589 | local canpatch = false | 625 | |
590 | local hunklineno | 626 | if create_delete then |
591 | if not file then | 627 | if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then |
592 | return nil, "failed reading file " .. source | 628 | local ok = os.remove(source) |
593 | end | 629 | if not ok then |
594 | 630 | return false | |
595 | if create_delete then | 631 | end |
596 | if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then | 632 | info(string.format("successfully removed %s", source)) |
597 | local ok = os.remove(source) | 633 | return true |
598 | if not ok then | ||
599 | return false | ||
600 | end | 634 | end |
601 | info(format("successfully removed %s", source)) | 635 | end |
602 | return true | ||
603 | end | ||
604 | end | ||
605 | 636 | ||
606 | find_hunks(file, hunks) | 637 | find_hunks(file, hunks) |
607 | 638 | ||
608 | local function process_line(line, lineno) | 639 | local function process_line(line, lineno) |
609 | if not hunk or lineno < hunk.startsrc then | 640 | if not hunk or lineno < hunk.startsrc then |
641 | return false | ||
642 | end | ||
643 | if lineno == hunk.startsrc then | ||
644 | hunkfind = {} | ||
645 | for _, x in ipairs(hunk.text) do | ||
646 | if x:sub(1, 1) == ' ' or x:sub(1, 1) == '-' then | ||
647 | hunkfind[#hunkfind + 1] = endlstrip(x:sub(2)) | ||
648 | end | ||
649 | end | ||
650 | hunklineno = 1 | ||
651 | |||
652 | |||
653 | end | ||
654 | |||
655 | if lineno < hunk.startsrc + #hunkfind - 1 then | ||
656 | if endlstrip(line) == hunkfind[hunklineno] then | ||
657 | hunklineno = hunklineno + 1 | ||
658 | else | ||
659 | debug(string.format("hunk no.%d doesn't match source file %s", | ||
660 | hunkno, source)) | ||
661 | |||
662 | hunkno = hunkno + 1 | ||
663 | if hunkno <= #hunks then | ||
664 | hunk = hunks[hunkno] | ||
665 | return false | ||
666 | else | ||
667 | return true | ||
668 | end | ||
669 | end | ||
670 | end | ||
671 | |||
672 | if lineno == hunk.startsrc + #hunkfind - 1 then | ||
673 | debug(string.format("file %s hunk no.%d -- is ready to be patched", | ||
674 | source, hunkno)) | ||
675 | hunkno = hunkno + 1 | ||
676 | validhunks = validhunks + 1 | ||
677 | if hunkno <= #hunks then | ||
678 | hunk = hunks[hunkno] | ||
679 | else | ||
680 | if validhunks == #hunks then | ||
681 | |||
682 | canpatch = true | ||
683 | return true | ||
684 | end | ||
685 | end | ||
686 | end | ||
610 | return false | 687 | return false |
611 | end | 688 | end |
612 | if lineno == hunk.startsrc then | 689 | |
613 | hunkfind = {} | 690 | local done = false |
614 | for _,x in ipairs(hunk.text) do | 691 | for lineno, line in ipairs(file) do |
615 | if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then | 692 | done = process_line(line, lineno) |
616 | hunkfind[#hunkfind+1] = endlstrip(x:sub(2)) | 693 | if done then |
617 | end | 694 | break |
618 | end | 695 | end |
619 | hunklineno = 1 | 696 | end |
620 | 697 | if not done then | |
621 | -- todo \ No newline at end of file | 698 | if hunkno <= #hunks and not create_file then |
622 | end | 699 | warning(string.format("premature end of source file %s at hunk %d", |
623 | -- check hunks in source file | 700 | source, hunkno)) |
624 | if lineno < hunk.startsrc + #hunkfind - 1 then | 701 | return false |
625 | if endlstrip(line) == hunkfind[hunklineno] then | ||
626 | hunklineno = hunklineno + 1 | ||
627 | else | ||
628 | debug(format("hunk no.%d doesn't match source file %s", | ||
629 | hunkno, source)) | ||
630 | -- file may be already patched, but check other hunks anyway | ||
631 | hunkno = hunkno + 1 | ||
632 | if hunkno <= #hunks then | ||
633 | hunk = hunks[hunkno] | ||
634 | return false | ||
635 | else | ||
636 | return true | ||
637 | end | ||
638 | end | 702 | end |
639 | end | 703 | end |
640 | -- check if processed line is the last line | 704 | if validhunks < #hunks then |
641 | if lineno == hunk.startsrc + #hunkfind - 1 then | 705 | if check_patched(file, hunks) then |
642 | debug(format("file %s hunk no.%d -- is ready to be patched", | 706 | warning(string.format("already patched %s", source)) |
643 | source, hunkno)) | 707 | elseif not create_file then |
644 | hunkno = hunkno + 1 | 708 | warning(string.format("source file is different - %s", source)) |
645 | validhunks = validhunks + 1 | 709 | return false |
646 | if hunkno <= #hunks then | ||
647 | hunk = hunks[hunkno] | ||
648 | else | ||
649 | if validhunks == #hunks then | ||
650 | -- patch file | ||
651 | canpatch = true | ||
652 | return true | ||
653 | end | ||
654 | end | 710 | end |
655 | end | 711 | end |
656 | return false | 712 | if not canpatch then |
657 | end | 713 | return true |
658 | 714 | end | |
659 | local done = false | 715 | local backupname = source .. ".orig" |
660 | for lineno, line in ipairs(file) do | 716 | if exists(backupname) then |
661 | done = process_line(line, lineno) | 717 | warning(string.format("can't backup original file to %s - aborting", |
662 | if done then | 718 | backupname)) |
663 | break | ||
664 | end | ||
665 | end | ||
666 | if not done then | ||
667 | if hunkno <= #hunks and not create_file then | ||
668 | warning(format("premature end of source file %s at hunk %d", | ||
669 | source, hunkno)) | ||
670 | return false | 719 | return false |
671 | end | 720 | end |
672 | end | 721 | local ok = os.rename(source, backupname) |
673 | if validhunks < #hunks then | 722 | if not ok then |
674 | if check_patched(file, hunks) then | 723 | warning(string.format("failed backing up %s when patching", source)) |
675 | warning(format("already patched %s", source)) | ||
676 | elseif not create_file then | ||
677 | warning(format("source file is different - %s", source)) | ||
678 | return false | 724 | return false |
679 | end | 725 | end |
680 | end | 726 | patch_hunks(backupname, source, hunks) |
681 | if not canpatch then | 727 | info(string.format("successfully patched %s", source)) |
682 | return true | 728 | os.remove(backupname) |
683 | end | 729 | return true |
684 | local backupname = source .. ".orig" | ||
685 | if exists(backupname) then | ||
686 | warning(format("can't backup original file to %s - aborting", | ||
687 | backupname)) | ||
688 | return false | ||
689 | end | ||
690 | local ok = os.rename(source, backupname) | ||
691 | if not ok then | ||
692 | warning(format("failed backing up %s when patching", source)) | ||
693 | return false | ||
694 | end | ||
695 | patch_hunks(backupname, source, hunks) | ||
696 | info(format("successfully patched %s", source)) | ||
697 | os.remove(backupname) | ||
698 | return true | ||
699 | end | 730 | end |
700 | 731 | ||
701 | function patch.apply_patch(the_patch, strip, create_delete) | 732 | function patch.apply_patch(the_patch, strip, create_delete) |
702 | local all_ok = true | 733 | local all_ok = true |
703 | local total = #the_patch.source | 734 | local total = #the_patch.source |
704 | for fileno, source in ipairs(the_patch.source) do | 735 | for fileno, source in ipairs(the_patch.source) do |
705 | local target = the_patch.target[fileno] | 736 | local target = the_patch.target[fileno] |
706 | local hunks = the_patch.hunks[fileno] | 737 | local hunks = the_patch.hunks[fileno] |
707 | local epoch = the_patch.epoch[fileno] | 738 | local epoch = the_patch.epoch[fileno] |
708 | info(format("processing %d/%d:\t %s", fileno, total, source)) | 739 | info(string.format("processing %d/%d:\t %s", fileno, total, source)) |
709 | local ok = patch_file(source, target, epoch, hunks, strip, create_delete) | 740 | local ok = patch_file(tostring(source), tostring(target), epoch, hunks, strip, create_delete) |
710 | all_ok = all_ok and ok | 741 | all_ok = all_ok and ok |
711 | end | 742 | end |
712 | -- todo: check for premature eof | 743 | |
713 | return all_ok | 744 | return all_ok |
714 | end | 745 | end |
715 | 746 | ||
716 | return patch | 747 | return patch |
diff --git a/src/luarocks/tools/patch.tl b/src/luarocks/tools/patch.tl index bd6c3d63..84de28b7 100644 --- a/src/luarocks/tools/patch.tl +++ b/src/luarocks/tools/patch.tl | |||
@@ -22,6 +22,14 @@ local record patch | |||
22 | invalid: boolean | 22 | invalid: boolean |
23 | text: {string} | 23 | text: {string} |
24 | end | 24 | end |
25 | record File --! | ||
26 | at: integer | ||
27 | str: string | ||
28 | len: integer | ||
29 | eof: boolean | ||
30 | read: function(File, integer): string | ||
31 | close: function(File): boolean | ||
32 | end | ||
25 | record Files | 33 | record Files |
26 | source: {string | number} --! | 34 | source: {string | number} --! |
27 | target: {string | number} | 35 | target: {string | number} |
@@ -38,6 +46,7 @@ local fun = require("luarocks.fun") | |||
38 | local type Lineends = patch.Lineends | 46 | local type Lineends = patch.Lineends |
39 | local type Hunk = patch.Hunk | 47 | local type Hunk = patch.Hunk |
40 | local type Files = patch.Files | 48 | local type Files = patch.Files |
49 | local type File = patch.File | ||
41 | 50 | ||
42 | -- logging | 51 | -- logging |
43 | local debugmode = false | 52 | local debugmode = false |
@@ -75,13 +84,13 @@ local function exists(filename: string): boolean | |||
75 | end | 84 | end |
76 | local function isfile(): boolean return true end --FIX? --! | 85 | local function isfile(): boolean return true end --FIX? --! |
77 | 86 | ||
78 | local function string_as_file(s: string): FILE --! | 87 | local function string_as_file(s: string): File --! |
79 | return { | 88 | return { |
80 | at = 0, | 89 | at = 0, |
81 | str = s, | 90 | str = s, |
82 | len = #s, | 91 | len = #s, |
83 | eof = false, | 92 | eof = false, |
84 | read = function(self: FILE, n: number): string | 93 | read = function(self: File, n: integer): string |
85 | if self.eof then return nil end | 94 | if self.eof then return nil end |
86 | local chunk = self.str:sub(self.at, self.at + n - 1) | 95 | local chunk = self.str:sub(self.at, self.at + n - 1) |
87 | self.at = self.at + n | 96 | self.at = self.at + n |
@@ -90,10 +99,10 @@ local function string_as_file(s: string): FILE --! | |||
90 | end | 99 | end |
91 | return chunk | 100 | return chunk |
92 | end, | 101 | end, |
93 | close = function(self: FILE): boolean | 102 | close = function(self: File): boolean |
94 | self.eof = true | 103 | self.eof = true |
95 | end, | 104 | end, |
96 | } as FILE | 105 | } as File |
97 | end | 106 | end |
98 | 107 | ||
99 | -- | 108 | -- |
@@ -178,7 +187,7 @@ function patch.read_patch(filename: string, data: string): Files, boolean | |||
178 | 187 | ||
179 | local fp: FILE | 188 | local fp: FILE |
180 | if data then | 189 | if data then |
181 | fp = string_as_file(data) | 190 | fp = string_as_file(data) as FILE --! cast |
182 | else | 191 | else |
183 | fp = filename == '-' and io.stdin or assert(io.open(filename, "rb") as (FILE, string)) --! use of cast | 192 | fp = filename == '-' and io.stdin or assert(io.open(filename, "rb") as (FILE, string)) --! use of cast |
184 | end | 193 | end |