Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(120)

Side by Side Diff: buildbot/patch.py

Issue 7745047: Enable browser tests with glibc. (Closed) Base URL: svn://svn.chromium.org/native_client/trunk/src/native_client/
Patch Set: '' Created 9 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « buildbot/buildbot_standard.py ('k') | ppapi.patch » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 """ Patch utility to apply unified diffs
2
3 Brute-force line-by-line non-recursive parsing
4
5 Copyright (c) 2008-2011 anatoly techtonik
6 Available under the terms of MIT license
7
8 Project home: http://code.google.com/p/python-patch/
9
10
11 $Id: patch.py 117 2011-01-09 16:38:03Z techtonik $
12 $HeadURL: https://python-patch.googlecode.com/svn/trunk/patch.py $
13 """
14
15 __author__ = "techtonik.rainforce.org"
16 __version__ = "11.01"
17
18 import copy
19 import logging
20 import re
21 # cStringIO doesn't support unicode in 2.5
22 from StringIO import StringIO
23 import urllib2
24
25 from os.path import exists, isfile, abspath
26 from os import unlink
27
28
29 #------------------------------------------------
30 # Logging is controlled by "python_patch" logger
31
32 debugmode = False
33
34 logger = logging.getLogger("python_patch")
35 loghandler = logging.StreamHandler()
36 logger.addHandler(loghandler)
37
38 debug = logger.debug
39 info = logger.info
40 warning = logger.warning
41
42 #: disable library logging by default
43 logger.setLevel(logging.CRITICAL)
44
45 #------------------------------------------------
46
47 # constants for patch types
48
49 DIFF = PLAIN = "plain"
50 HG = MERCURIAL = "mercurial"
51 SVN = SUBVERSION = "svn"
52
53
54 def fromfile(filename):
55 """ Parse patch file and return Patch() object
56 """
57 debug("reading %s" % filename)
58 fp = open(filename, "rb")
59 patch = Patch(fp)
60 fp.close()
61 return patch
62
63
64 def fromstring(s):
65 """ Parse text string and return Patch() object
66 """
67 return Patch( StringIO(s) )
68
69
70 def fromurl(url):
71 """ Read patch from URL
72 """
73 return Patch( urllib2.urlopen(url) )
74
75
76 class Hunk(object):
77 """ Parsed hunk data container (hunk starts with @@ -R +R @@) """
78
79 def __init__(self):
80 self.startsrc=None #: line count starts with 1
81 self.linessrc=None
82 self.starttgt=None
83 self.linestgt=None
84 self.invalid=False
85 self.text=[]
86
87 def copy(self):
88 return copy.copy(self)
89
90 # def apply(self, estream):
91 # """ write hunk data into enumerable stream
92 # return strings one by one until hunk is
93 # over
94 #
95 # enumerable stream are tuples (lineno, line)
96 # where lineno starts with 0
97 # """
98 # pass
99
100
101
102 class Patch(object):
103
104 def __init__(self, stream=None):
105
106 # define Patch data members
107 # table with a row for every source file
108
109 #: list of source filenames
110 self.source=None
111 self.target=None
112 #: list of lists of hunks
113 self.hunks=None
114 #: file endings statistics for every hunk
115 self.hunkends=None
116 #: headers for each file
117 self.header=None
118
119 #: patch type - one of constants
120 self.type = None
121
122 if stream:
123 self.parse(stream)
124
125 def copy(self):
126 return copy.copy(self)
127
128 def parse(self, stream):
129 """ parse unified diff """
130 self.header = []
131
132 self.source = []
133 self.target = []
134 self.hunks = []
135 self.hunkends = []
136
137 lineends = dict(lf=0, crlf=0, cr=0)
138 nextfileno = 0
139 nexthunkno = 0 #: even if index starts with 0 user messages number hunks from 1
140
141 # hunkinfo variable holds parsed values, hunkactual - calculated
142 hunkinfo = Hunk()
143 hunkactual = dict(linessrc=None, linestgt=None)
144
145
146 class wrapumerate(enumerate):
147 """Enumerate wrapper that uses boolean end of stream status instead of
148 StopIteration exception, and properties to access line information.
149 """
150
151 def __init__(self, *args, **kwargs):
152 # we don't call parent, it is magically created by __new__ method
153
154 self._exhausted = False
155 self._lineno = False # after end of stream equal to the num of lines
156 self._line = False # will be reset to False after end of stream
157
158 def next(self):
159 """Try to read the next line and return True if it is available,
160 False if end of stream is reached."""
161 if self._exhausted:
162 return False
163
164 try:
165 self._lineno, self._line = super(wrapumerate, self).next()
166 except StopIteration:
167 self._exhausted = True
168 self._line = False
169 return False
170 return True
171
172 @property
173 def is_empty(self):
174 return self._exhausted
175
176 @property
177 def line(self):
178 return self._line
179
180 @property
181 def lineno(self):
182 return self._lineno
183
184 # define states (possible file regions) that direct parse flow
185 headscan = True # start with scanning header
186 filenames = False # lines starting with --- and +++
187
188 hunkhead = False # @@ -R +R @@ sequence
189 hunkbody = False #
190 hunkskip = False # skipping invalid hunk mode
191
192 hunkparsed = False # state after successfully parsed hunk
193
194 # regexp to match start of hunk, used groups - 1,3,4,6
195 re_hunk_start = re.compile("^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))?")
196
197
198 # start of main cycle
199 # each parsing block already has line available in fe.line
200 fe = wrapumerate(stream)
201 while fe.next():
202
203 # -- deciders: these only switch state to decide who should process
204 # -- line fetched at the start of this cycle
205 if hunkparsed:
206 hunkparsed = False
207 if re_hunk_start.match(fe.line):
208 hunkhead = True
209 elif fe.line.startswith("--- "):
210 filenames = True
211 else:
212 headscan = True
213 # -- ------------------------------------
214
215 # read out header
216 if headscan:
217 header = ''
218 while not fe.is_empty and not fe.line.startswith("--- "):
219 header += fe.line
220 fe.next()
221 if fe.is_empty:
222 if len(self.source) == 0:
223 warning("warning: no patch data is found")
224 else:
225 info("%d unparsed bytes left at the end of stream" % len(header))
226 # this is actually a loop exit
227 continue
228 self.header.append(header)
229
230 headscan = False
231 # switch to filenames state
232 filenames = True
233
234 line = fe.line
235 lineno = fe.lineno
236
237
238 # hunkskip and hunkbody code skipped until definition of hunkhead is parse d
239 if hunkbody:
240 # process line first
241 if re.match(r"^[- \+\\]", line):
242 # gather stats about line endings
243 if line.endswith("\r\n"):
244 self.hunkends[nextfileno-1]["crlf"] += 1
245 elif line.endswith("\n"):
246 self.hunkends[nextfileno-1]["lf"] += 1
247 elif line.endswith("\r"):
248 self.hunkends[nextfileno-1]["cr"] += 1
249
250 if line.startswith("-"):
251 hunkactual["linessrc"] += 1
252 elif line.startswith("+"):
253 hunkactual["linestgt"] += 1
254 elif not line.startswith("\\"):
255 hunkactual["linessrc"] += 1
256 hunkactual["linestgt"] += 1
257 hunkinfo.text.append(line)
258 # todo: handle \ No newline cases
259 else:
260 warning("invalid hunk no.%d at %d for target file %s" % (nexthunkno, lineno+1, self.target[nextfileno-1]))
261 # add hunk status node
262 self.hunks[nextfileno-1].append(hunkinfo.copy())
263 self.hunks[nextfileno-1][nexthunkno-1]["invalid"] = True
264 # switch to hunkskip state
265 hunkbody = False
266 hunkskip = True
267
268 # check exit conditions
269 if hunkactual["linessrc"] > hunkinfo.linessrc or hunkactual["linestgt"] > hunkinfo.linestgt:
270 warning("extra lines for hunk no.%d at %d for target %s" % (nexthunk no, lineno+1, self.target[nextfileno-1]))
271 # add hunk status node
272 self.hunks[nextfileno-1].append(hunkinfo.copy())
273 self.hunks[nextfileno-1][nexthunkno-1]["invalid"] = True
274 # switch to hunkskip state
275 hunkbody = False
276 hunkskip = True
277 elif hunkinfo.linessrc == hunkactual["linessrc"] and hunkinfo.linestgt = = hunkactual["linestgt"]:
278 # hunk parsed successfully
279 self.hunks[nextfileno-1].append(hunkinfo.copy())
280 # switch to hunkparsed state
281 hunkbody = False
282 hunkparsed = True
283
284 # detect mixed window/unix line ends
285 ends = self.hunkends[nextfileno-1]
286 if ((ends["cr"]!=0) + (ends["crlf"]!=0) + (ends["lf"]!=0)) > 1:
287 warning("inconsistent line ends in patch hunks for %s" % self.sour ce[nextfileno-1])
288 if debugmode:
289 debuglines = dict(ends)
290 debuglines.update(file=self.target[nextfileno-1], hunk=nexthunkno)
291 debug("crlf: %(crlf)d lf: %(lf)d cr: %(cr)d\t - file: %(file)s h unk: %(hunk)d" % debuglines)
292 # fetch next line
293 continue
294
295 if hunkskip:
296 if re_hunk_start.match(line):
297 # switch to hunkhead state
298 hunkskip = False
299 hunkhead = True
300 elif line.startswith("--- "):
301 # switch to filenames state
302 hunkskip = False
303 filenames = True
304 if debugmode and len(self.source) > 0:
305 debug("- %2d hunks for %s" % (len(self.hunks[nextfileno-1]), self.so urce[nextfileno-1]))
306
307 if filenames:
308 if line.startswith("--- "):
309 if nextfileno in self.source:
310 warning("skipping invalid patch for %s" % self.source[nextfileno])
311 del self.source[nextfileno]
312 # double source filename line is encountered
313 # attempt to restart from this second line
314 re_filename = "^--- ([^\t]+)"
315 match = re.match(re_filename, line)
316 # todo: support spaces in filenames
317 if match:
318 self.source.append(match.group(1).strip())
319 else:
320 warning("skipping invalid filename at line %d" % lineno)
321 # switch back to headscan state
322 filenames = False
323 headscan = True
324 elif not line.startswith("+++ "):
325 if nextfileno in self.source:
326 warning("skipping invalid patch with no target for %s" % self.source [nextfileno])
327 del self.source[nextfileno]
328 else:
329 # this should be unreachable
330 warning("skipping invalid target patch")
331 filenames = False
332 headscan = True
333 else:
334 if nextfileno in self.target:
335 warning("skipping invalid patch - double target at line %d" % lineno )
336 del self.source[nextfileno]
337 del self.target[nextfileno]
338 nextfileno -= 1
339 # double target filename line is encountered
340 # switch back to headscan state
341 filenames = False
342 headscan = True
343 else:
344 re_filename = "^\+\+\+ ([^\t]+)"
345 match = re.match(re_filename, line)
346 if not match:
347 warning("skipping invalid patch - no target filename at line %d" % lineno)
348 # switch back to headscan state
349 filenames = False
350 headscan = True
351 else:
352 self.target.append(match.group(1).strip())
353 nextfileno += 1
354 # switch to hunkhead state
355 filenames = False
356 hunkhead = True
357 nexthunkno = 0
358 self.hunks.append([])
359 self.hunkends.append(lineends.copy())
360 continue
361
362 if hunkhead:
363 match = re.match("^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))?", line)
364 if not match:
365 if nextfileno-1 not in self.hunks:
366 warning("skipping invalid patch with no hunks for file %s" % self.ta rget[nextfileno-1])
367 # switch to headscan state
368 hunkhead = False
369 headscan = True
370 continue
371 else:
372 # switch to headscan state
373 hunkhead = False
374 headscan = True
375 else:
376 hunkinfo.startsrc = int(match.group(1))
377 hunkinfo.linessrc = 1
378 if match.group(3): hunkinfo.linessrc = int(match.group(3))
379 hunkinfo.starttgt = int(match.group(4))
380 hunkinfo.linestgt = 1
381 if match.group(6): hunkinfo.linestgt = int(match.group(6))
382 hunkinfo.invalid = False
383 hunkinfo.text = []
384
385 hunkactual["linessrc"] = hunkactual["linestgt"] = 0
386
387 # switch to hunkbody state
388 hunkhead = False
389 hunkbody = True
390 nexthunkno += 1
391 continue
392
393
394 if not hunkparsed:
395 if hunkskip:
396 warning("warning: finished with warnings, some hunks may be invalid")
397 elif headscan:
398 if len(self.source) == 0:
399 warning("error: no patch data found!")
400 # ? sys.exit(-1)
401 else: # extra data at the end of file
402 pass
403 else:
404 warning("error: patch stream is incomplete!")
405
406 if debugmode and len(self.source) > 0:
407 debug("- %2d hunks for %s" % (len(self.hunks[nextfileno-1]), self.source [nextfileno-1]))
408
409 debug("total files: %d total hunks: %d" % (len(self.source), sum(len(hset) for hset in self.hunks)))
410
411
412 def apply(self):
413 """ apply parsed patch
414 return True on success
415 """
416
417 total = len(self.source)
418 errors = 0
419 for fileno, filename in enumerate(self.source):
420
421 f2patch = filename
422 if not exists(f2patch):
423 f2patch = self.target[fileno]
424 if not exists(f2patch):
425 warning("source/target file does not exist\n--- %s\n+++ %s" % (filenam e, f2patch))
426 errors += 1
427 continue
428 if not isfile(f2patch):
429 warning("not a file - %s" % f2patch)
430 errors += 1
431 continue
432 filename = f2patch
433
434 debug("processing %d/%d:\t %s" % (fileno+1, total, filename))
435
436 # validate before patching
437 f2fp = open(filename)
438 hunkno = 0
439 hunk = self.hunks[fileno][hunkno]
440 hunkfind = []
441 hunkreplace = []
442 validhunks = 0
443 canpatch = False
444 for lineno, line in enumerate(f2fp):
445 if lineno+1 < hunk.startsrc:
446 continue
447 elif lineno+1 == hunk.startsrc:
448 hunkfind = [x[1:].rstrip("\r\n") for x in hunk.text if x[0] in " -"]
449 hunkreplace = [x[1:].rstrip("\r\n") for x in hunk.text if x[0] in " +" ]
450 #pprint(hunkreplace)
451 hunklineno = 0
452
453 # todo \ No newline at end of file
454
455 # check hunks in source file
456 if lineno+1 < hunk.startsrc+len(hunkfind)-1:
457 if line.rstrip("\r\n") == hunkfind[hunklineno]:
458 hunklineno+=1
459 else:
460 info("file %d/%d:\t %s" % (fileno+1, total, filename))
461 info(" hunk no.%d doesn't match source file at line %d" % (hunkno+1, lineno))
462 info(" expected: %s" % hunkfind[hunklineno])
463 info(" actual : %s" % line.rstrip("\r\n"))
464 # not counting this as error, because file may already be patched.
465 # check if file is already patched is done after the number of
466 # invalid hunks if found
467 # TODO: check hunks against source/target file in one pass
468 # API - check(stream, srchunks, tgthunks)
469 # return tuple (srcerrs, tgterrs)
470
471 # continue to check other hunks for completeness
472 hunkno += 1
473 if hunkno < len(self.hunks[fileno]):
474 hunk = self.hunks[fileno][hunkno]
475 continue
476 else:
477 break
478
479 # check if processed line is the last line
480 if lineno+1 == hunk.startsrc+len(hunkfind)-1:
481 debug(" hunk no.%d for file %s -- is ready to be patched" % (hunkno+1 , filename))
482 hunkno+=1
483 validhunks+=1
484 if hunkno < len(self.hunks[fileno]):
485 hunk = self.hunks[fileno][hunkno]
486 else:
487 if validhunks == len(self.hunks[fileno]):
488 # patch file
489 canpatch = True
490 break
491 else:
492 if hunkno < len(self.hunks[fileno]):
493 warning("premature end of source file %s at hunk %d" % (filename, hunk no+1))
494 errors += 1
495
496 f2fp.close()
497
498 if validhunks < len(self.hunks[fileno]):
499 if self._match_file_hunks(filename, self.hunks[fileno]):
500 warning("already patched %s" % filename)
501 else:
502 warning("source file is different - %s" % filename)
503 errors += 1
504 if canpatch:
505 backupname = filename+".orig"
506 if exists(backupname):
507 warning("can't backup original file to %s - aborting" % backupname)
508 else:
509 import shutil
510 shutil.move(filename, backupname)
511 if self.write_hunks(backupname, filename, self.hunks[fileno]):
512 info("successfully patched %d/%d:\t %s" % (fileno+1, total, filename ))
513 unlink(backupname)
514 else:
515 errors += 1
516 warning("error patching file %s" % filename)
517 shutil.copy(filename, filename+".invalid")
518 warning("invalid version is saved to %s" % filename+".invalid")
519 # todo: proper rejects
520 shutil.move(backupname, filename)
521
522 # todo: check for premature eof
523 return (errors == 0)
524
525
526 def can_patch(self, filename):
527 """ Check if specified filename can be patched. Returns None if file can
528 not be found among source filenames. False if patch can not be applied
529 clearly. True otherwise.
530
531 :returns: True, False or None
532 """
533 idx = self._get_file_idx(filename, source=True)
534 if idx == None:
535 return None
536 return self._match_file_hunks(filename, self.hunks[idx])
537
538
539 def _match_file_hunks(self, filepath, hunks):
540 matched = True
541 fp = open(abspath(filepath))
542
543 class NoMatch(Exception):
544 pass
545
546 lineno = 1
547 line = fp.readline()
548 hno = None
549 try:
550 for hno, h in enumerate(hunks):
551 # skip to first line of the hunk
552 while lineno < h.starttgt:
553 if not len(line): # eof
554 debug("check failed - premature eof before hunk: %d" % (hno+1))
555 raise NoMatch
556 line = fp.readline()
557 lineno += 1
558 for hline in h.text:
559 if hline.startswith("-"):
560 continue
561 if not len(line):
562 debug("check failed - premature eof on hunk: %d" % (hno+1))
563 # todo: \ No newline at the end of file
564 raise NoMatch
565 if line.rstrip("\r\n") != hline[1:].rstrip("\r\n"):
566 debug("file is not patched - failed hunk: %d" % (hno+1))
567 raise NoMatch
568 line = fp.readline()
569 lineno += 1
570
571 except NoMatch:
572 matched = False
573 # todo: display failed hunk, i.e. expected/found
574
575 fp.close()
576 return matched
577
578
579 def patch_stream(self, instream, hunks):
580 """ Generator that yields stream patched with hunks iterable
581
582 Converts lineends in hunk lines to the best suitable format
583 autodetected from input
584 """
585
586 # todo: At the moment substituted lineends may not be the same
587 # at the start and at the end of patching. Also issue a
588 # warning/throw about mixed lineends (is it really needed?)
589
590 hunks = iter(hunks)
591
592 srclineno = 1
593
594 lineends = {'\n':0, '\r\n':0, '\r':0}
595 def get_line():
596 """
597 local utility function - return line from source stream
598 collecting line end statistics on the way
599 """
600 line = instream.readline()
601 # 'U' mode works only with text files
602 if line.endswith("\r\n"):
603 lineends["\r\n"] += 1
604 elif line.endswith("\n"):
605 lineends["\n"] += 1
606 elif line.endswith("\r"):
607 lineends["\r"] += 1
608 return line
609
610 for hno, h in enumerate(hunks):
611 debug("hunk %d" % (hno+1))
612 # skip to line just before hunk starts
613 while srclineno < h.startsrc:
614 yield get_line()
615 srclineno += 1
616
617 for hline in h.text:
618 # todo: check \ No newline at the end of file
619 if hline.startswith("-") or hline.startswith("\\"):
620 get_line()
621 srclineno += 1
622 continue
623 else:
624 if not hline.startswith("+"):
625 get_line()
626 srclineno += 1
627 line2write = hline[1:]
628 # detect if line ends are consistent in source file
629 if sum([bool(lineends[x]) for x in lineends]) == 1:
630 newline = [x for x in lineends if lineends[x] != 0][0]
631 yield line2write.rstrip("\r\n")+newline
632 else: # newlines are mixed
633 yield line2write
634
635 for line in instream:
636 yield line
637
638
639 def write_hunks(self, srcname, tgtname, hunks):
640 src = open(srcname, "rb")
641 tgt = open(tgtname, "wb")
642
643 debug("processing target file %s" % tgtname)
644
645 tgt.writelines(self.patch_stream(src, hunks))
646
647 tgt.close()
648 src.close()
649 return True
650
651
652 def _get_file_idx(self, filename, source=None):
653 """ Detect index of given filename within patch.
654
655 :param filename:
656 :param source: search filename among sources (True),
657 targets (False), or both (None)
658 :returns: int or None
659 """
660 filename = abspath(filename)
661 if source == True or source == None:
662 for i,fnm in enumerate(self.source):
663 if filename == abspath(fnm):
664 return i
665 if source == False or source == None:
666 for i,fnm in enumerate(self.target):
667 if filename == abspath(fnm):
668 return i
669
670
671
672
673 if __name__ == "__main__":
674 from optparse import OptionParser
675 from os.path import exists
676 import sys
677
678 opt = OptionParser(usage="1. %prog [options] unipatch-file\n"
679 " 2. %prog [options] http://host/patch",
680 version="python-patch %s" % __version__)
681 opt.add_option("-q", "--quiet", action="store_const", dest="verbosity",
682 const=0, help="print only warnings and errors" , default=1)
683 opt.add_option("-v", "--verbose", action="store_const", dest="verbosity",
684 const=2, help="be verbose")
685 opt.add_option("--debug", action="store_true", dest="debugmode", help="debug m ode")
686 (options, args) = opt.parse_args()
687
688 if not args:
689 opt.print_version()
690 opt.print_help()
691 sys.exit()
692 debugmode = options.debugmode
693
694 verbosity_levels = {0:logging.WARNING, 1:logging.INFO, 2:logging.DEBUG}
695 loglevel = verbosity_levels[options.verbosity]
696 logformat = "%(message)s"
697 if debugmode:
698 loglevel = logging.DEBUG
699 logformat = "%(levelname)8s %(message)s"
700 logger.setLevel(loglevel)
701 loghandler.setFormatter(logging.Formatter(logformat))
702
703
704 patchfile = args[0]
705 urltest = patchfile.split(':')[0]
706 if (':' in patchfile and urltest.isalpha()
707 and len(urltest) > 1): # one char before : is a windows drive letter
708 patch = fromurl(patchfile)
709 else:
710 if not exists(patchfile) or not isfile(patchfile):
711 sys.exit("patch file does not exist - %s" % patchfile)
712 patch = fromfile(patchfile)
713
714 #pprint(patch)
715 patch.apply() or sys.exit(-1)
716
717 # todo: document and test line ends handling logic - patch.py detects proper l ine-endings
718 # for inserted hunks and issues a warning if patched file has incosisten t line ends
OLDNEW
« no previous file with comments | « buildbot/buildbot_standard.py ('k') | ppapi.patch » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698