##// END OF EJS Templates
patch: move copyfile() into backends, abstract basedir
Patrick Mezard -
r14350:00da6624 default
parent child Browse files
Show More
@@ -1,1740 +1,1745 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import cStringIO, email.Parser, os, errno, re
10 10 import tempfile, zlib
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
15 15
16 16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17 17
18 18 class PatchError(Exception):
19 19 pass
20 20
21 # helper functions
22
23 def copyfile(src, dst, basedir):
24 abssrc, absdst = [scmutil.canonpath(basedir, basedir, x)
25 for x in [src, dst]]
26 if os.path.lexists(absdst):
27 raise util.Abort(_("cannot create %s: destination already exists") %
28 dst)
29
30 dstdir = os.path.dirname(absdst)
31 if dstdir and not os.path.isdir(dstdir):
32 try:
33 os.makedirs(dstdir)
34 except IOError:
35 raise util.Abort(
36 _("cannot create %s: unable to create destination directory")
37 % dst)
38
39 util.copyfile(abssrc, absdst)
40 21
41 22 # public functions
42 23
43 24 def split(stream):
44 25 '''return an iterator of individual patches from a stream'''
45 26 def isheader(line, inheader):
46 27 if inheader and line[0] in (' ', '\t'):
47 28 # continuation
48 29 return True
49 30 if line[0] in (' ', '-', '+'):
50 31 # diff line - don't check for header pattern in there
51 32 return False
52 33 l = line.split(': ', 1)
53 34 return len(l) == 2 and ' ' not in l[0]
54 35
55 36 def chunk(lines):
56 37 return cStringIO.StringIO(''.join(lines))
57 38
58 39 def hgsplit(stream, cur):
59 40 inheader = True
60 41
61 42 for line in stream:
62 43 if not line.strip():
63 44 inheader = False
64 45 if not inheader and line.startswith('# HG changeset patch'):
65 46 yield chunk(cur)
66 47 cur = []
67 48 inheader = True
68 49
69 50 cur.append(line)
70 51
71 52 if cur:
72 53 yield chunk(cur)
73 54
74 55 def mboxsplit(stream, cur):
75 56 for line in stream:
76 57 if line.startswith('From '):
77 58 for c in split(chunk(cur[1:])):
78 59 yield c
79 60 cur = []
80 61
81 62 cur.append(line)
82 63
83 64 if cur:
84 65 for c in split(chunk(cur[1:])):
85 66 yield c
86 67
87 68 def mimesplit(stream, cur):
88 69 def msgfp(m):
89 70 fp = cStringIO.StringIO()
90 71 g = email.Generator.Generator(fp, mangle_from_=False)
91 72 g.flatten(m)
92 73 fp.seek(0)
93 74 return fp
94 75
95 76 for line in stream:
96 77 cur.append(line)
97 78 c = chunk(cur)
98 79
99 80 m = email.Parser.Parser().parse(c)
100 81 if not m.is_multipart():
101 82 yield msgfp(m)
102 83 else:
103 84 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
104 85 for part in m.walk():
105 86 ct = part.get_content_type()
106 87 if ct not in ok_types:
107 88 continue
108 89 yield msgfp(part)
109 90
110 91 def headersplit(stream, cur):
111 92 inheader = False
112 93
113 94 for line in stream:
114 95 if not inheader and isheader(line, inheader):
115 96 yield chunk(cur)
116 97 cur = []
117 98 inheader = True
118 99 if inheader and not isheader(line, inheader):
119 100 inheader = False
120 101
121 102 cur.append(line)
122 103
123 104 if cur:
124 105 yield chunk(cur)
125 106
126 107 def remainder(cur):
127 108 yield chunk(cur)
128 109
129 110 class fiter(object):
130 111 def __init__(self, fp):
131 112 self.fp = fp
132 113
133 114 def __iter__(self):
134 115 return self
135 116
136 117 def next(self):
137 118 l = self.fp.readline()
138 119 if not l:
139 120 raise StopIteration
140 121 return l
141 122
142 123 inheader = False
143 124 cur = []
144 125
145 126 mimeheaders = ['content-type']
146 127
147 128 if not hasattr(stream, 'next'):
148 129 # http responses, for example, have readline but not next
149 130 stream = fiter(stream)
150 131
151 132 for line in stream:
152 133 cur.append(line)
153 134 if line.startswith('# HG changeset patch'):
154 135 return hgsplit(stream, cur)
155 136 elif line.startswith('From '):
156 137 return mboxsplit(stream, cur)
157 138 elif isheader(line, inheader):
158 139 inheader = True
159 140 if line.split(':', 1)[0].lower() in mimeheaders:
160 141 # let email parser handle this
161 142 return mimesplit(stream, cur)
162 143 elif line.startswith('--- ') and inheader:
163 144 # No evil headers seen by diff start, split by hand
164 145 return headersplit(stream, cur)
165 146 # Not enough info, keep reading
166 147
167 148 # if we are here, we have a very plain patch
168 149 return remainder(cur)
169 150
170 151 def extract(ui, fileobj):
171 152 '''extract patch from data read from fileobj.
172 153
173 154 patch can be a normal patch or contained in an email message.
174 155
175 156 return tuple (filename, message, user, date, branch, node, p1, p2).
176 157 Any item in the returned tuple can be None. If filename is None,
177 158 fileobj did not contain a patch. Caller must unlink filename when done.'''
178 159
179 160 # attempt to detect the start of a patch
180 161 # (this heuristic is borrowed from quilt)
181 162 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
182 163 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
183 164 r'---[ \t].*?^\+\+\+[ \t]|'
184 165 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
185 166
186 167 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
187 168 tmpfp = os.fdopen(fd, 'w')
188 169 try:
189 170 msg = email.Parser.Parser().parse(fileobj)
190 171
191 172 subject = msg['Subject']
192 173 user = msg['From']
193 174 if not subject and not user:
194 175 # Not an email, restore parsed headers if any
195 176 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
196 177
197 178 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
198 179 # should try to parse msg['Date']
199 180 date = None
200 181 nodeid = None
201 182 branch = None
202 183 parents = []
203 184
204 185 if subject:
205 186 if subject.startswith('[PATCH'):
206 187 pend = subject.find(']')
207 188 if pend >= 0:
208 189 subject = subject[pend + 1:].lstrip()
209 190 subject = subject.replace('\n\t', ' ')
210 191 ui.debug('Subject: %s\n' % subject)
211 192 if user:
212 193 ui.debug('From: %s\n' % user)
213 194 diffs_seen = 0
214 195 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
215 196 message = ''
216 197 for part in msg.walk():
217 198 content_type = part.get_content_type()
218 199 ui.debug('Content-Type: %s\n' % content_type)
219 200 if content_type not in ok_types:
220 201 continue
221 202 payload = part.get_payload(decode=True)
222 203 m = diffre.search(payload)
223 204 if m:
224 205 hgpatch = False
225 206 hgpatchheader = False
226 207 ignoretext = False
227 208
228 209 ui.debug('found patch at byte %d\n' % m.start(0))
229 210 diffs_seen += 1
230 211 cfp = cStringIO.StringIO()
231 212 for line in payload[:m.start(0)].splitlines():
232 213 if line.startswith('# HG changeset patch') and not hgpatch:
233 214 ui.debug('patch generated by hg export\n')
234 215 hgpatch = True
235 216 hgpatchheader = True
236 217 # drop earlier commit message content
237 218 cfp.seek(0)
238 219 cfp.truncate()
239 220 subject = None
240 221 elif hgpatchheader:
241 222 if line.startswith('# User '):
242 223 user = line[7:]
243 224 ui.debug('From: %s\n' % user)
244 225 elif line.startswith("# Date "):
245 226 date = line[7:]
246 227 elif line.startswith("# Branch "):
247 228 branch = line[9:]
248 229 elif line.startswith("# Node ID "):
249 230 nodeid = line[10:]
250 231 elif line.startswith("# Parent "):
251 232 parents.append(line[10:])
252 233 elif not line.startswith("# "):
253 234 hgpatchheader = False
254 235 elif line == '---' and gitsendmail:
255 236 ignoretext = True
256 237 if not hgpatchheader and not ignoretext:
257 238 cfp.write(line)
258 239 cfp.write('\n')
259 240 message = cfp.getvalue()
260 241 if tmpfp:
261 242 tmpfp.write(payload)
262 243 if not payload.endswith('\n'):
263 244 tmpfp.write('\n')
264 245 elif not diffs_seen and message and content_type == 'text/plain':
265 246 message += '\n' + payload
266 247 except:
267 248 tmpfp.close()
268 249 os.unlink(tmpname)
269 250 raise
270 251
271 252 if subject and not message.startswith(subject):
272 253 message = '%s\n%s' % (subject, message)
273 254 tmpfp.close()
274 255 if not diffs_seen:
275 256 os.unlink(tmpname)
276 257 return None, message, user, date, branch, None, None, None
277 258 p1 = parents and parents.pop(0) or None
278 259 p2 = parents and parents.pop(0) or None
279 260 return tmpname, message, user, date, branch, nodeid, p1, p2
280 261
281 262 class patchmeta(object):
282 263 """Patched file metadata
283 264
284 265 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
285 266 or COPY. 'path' is patched file path. 'oldpath' is set to the
286 267 origin file when 'op' is either COPY or RENAME, None otherwise. If
287 268 file mode is changed, 'mode' is a tuple (islink, isexec) where
288 269 'islink' is True if the file is a symlink and 'isexec' is True if
289 270 the file is executable. Otherwise, 'mode' is None.
290 271 """
291 272 def __init__(self, path):
292 273 self.path = path
293 274 self.oldpath = None
294 275 self.mode = None
295 276 self.op = 'MODIFY'
296 277 self.binary = False
297 278
298 279 def setmode(self, mode):
299 280 islink = mode & 020000
300 281 isexec = mode & 0100
301 282 self.mode = (islink, isexec)
302 283
303 284 def __repr__(self):
304 285 return "<patchmeta %s %r>" % (self.op, self.path)
305 286
306 287 def readgitpatch(lr):
307 288 """extract git-style metadata about patches from <patchname>"""
308 289
309 290 # Filter patch for git information
310 291 gp = None
311 292 gitpatches = []
312 293 for line in lr:
313 294 line = line.rstrip(' \r\n')
314 295 if line.startswith('diff --git'):
315 296 m = gitre.match(line)
316 297 if m:
317 298 if gp:
318 299 gitpatches.append(gp)
319 300 dst = m.group(2)
320 301 gp = patchmeta(dst)
321 302 elif gp:
322 303 if line.startswith('--- '):
323 304 gitpatches.append(gp)
324 305 gp = None
325 306 continue
326 307 if line.startswith('rename from '):
327 308 gp.op = 'RENAME'
328 309 gp.oldpath = line[12:]
329 310 elif line.startswith('rename to '):
330 311 gp.path = line[10:]
331 312 elif line.startswith('copy from '):
332 313 gp.op = 'COPY'
333 314 gp.oldpath = line[10:]
334 315 elif line.startswith('copy to '):
335 316 gp.path = line[8:]
336 317 elif line.startswith('deleted file'):
337 318 gp.op = 'DELETE'
338 319 elif line.startswith('new file mode '):
339 320 gp.op = 'ADD'
340 321 gp.setmode(int(line[-6:], 8))
341 322 elif line.startswith('new mode '):
342 323 gp.setmode(int(line[-6:], 8))
343 324 elif line.startswith('GIT binary patch'):
344 325 gp.binary = True
345 326 if gp:
346 327 gitpatches.append(gp)
347 328
348 329 return gitpatches
349 330
350 331 class linereader(object):
351 332 # simple class to allow pushing lines back into the input stream
352 333 def __init__(self, fp, textmode=False):
353 334 self.fp = fp
354 335 self.buf = []
355 336 self.textmode = textmode
356 337 self.eol = None
357 338
358 339 def push(self, line):
359 340 if line is not None:
360 341 self.buf.append(line)
361 342
362 343 def readline(self):
363 344 if self.buf:
364 345 l = self.buf[0]
365 346 del self.buf[0]
366 347 return l
367 348 l = self.fp.readline()
368 349 if not self.eol:
369 350 if l.endswith('\r\n'):
370 351 self.eol = '\r\n'
371 352 elif l.endswith('\n'):
372 353 self.eol = '\n'
373 354 if self.textmode and l.endswith('\r\n'):
374 355 l = l[:-2] + '\n'
375 356 return l
376 357
377 358 def __iter__(self):
378 359 while 1:
379 360 l = self.readline()
380 361 if not l:
381 362 break
382 363 yield l
383 364
384 365 class abstractbackend(object):
385 366 def __init__(self, ui):
386 367 self.ui = ui
387 368
388 369 def readlines(self, fname):
389 370 """Return target file lines, or its content as a single line
390 371 for symlinks.
391 372 """
392 373 raise NotImplementedError
393 374
394 375 def writelines(self, fname, lines):
395 376 """Write lines to target file."""
396 377 raise NotImplementedError
397 378
398 379 def unlink(self, fname):
399 380 """Unlink target file."""
400 381 raise NotImplementedError
401 382
402 383 def writerej(self, fname, failed, total, lines):
403 384 """Write rejected lines for fname. total is the number of hunks
404 385 which failed to apply and total the total number of hunks for this
405 386 files.
406 387 """
407 388 pass
408 389
390 def copy(self, src, dst):
391 """Copy src file into dst file. Create intermediate directories if
392 necessary. Files are specified relatively to the patching base
393 directory.
394 """
395 raise NotImplementedError
396
409 397 class fsbackend(abstractbackend):
410 def __init__(self, ui, opener):
398 def __init__(self, ui, basedir):
411 399 super(fsbackend, self).__init__(ui)
412 self.opener = opener
400 self.opener = scmutil.opener(basedir)
413 401
414 402 def readlines(self, fname):
415 403 if os.path.islink(fname):
416 404 return [os.readlink(fname)]
417 405 fp = self.opener(fname, 'r')
418 406 try:
419 407 return list(fp)
420 408 finally:
421 409 fp.close()
422 410
423 411 def writelines(self, fname, lines):
424 412 # Ensure supplied data ends in fname, being a regular file or
425 413 # a symlink. _updatedir will -too magically- take care
426 414 # of setting it to the proper type afterwards.
427 415 st_mode = None
428 416 islink = os.path.islink(fname)
429 417 if islink:
430 418 fp = cStringIO.StringIO()
431 419 else:
432 420 try:
433 421 st_mode = os.lstat(fname).st_mode & 0777
434 422 except OSError, e:
435 423 if e.errno != errno.ENOENT:
436 424 raise
437 425 fp = self.opener(fname, 'w')
438 426 try:
439 427 fp.writelines(lines)
440 428 if islink:
441 429 self.opener.symlink(fp.getvalue(), fname)
442 430 if st_mode is not None:
443 431 os.chmod(fname, st_mode)
444 432 finally:
445 433 fp.close()
446 434
447 435 def unlink(self, fname):
448 436 os.unlink(fname)
449 437
450 438 def writerej(self, fname, failed, total, lines):
451 439 fname = fname + ".rej"
452 440 self.ui.warn(
453 441 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
454 442 (failed, total, fname))
455 443 fp = self.opener(fname, 'w')
456 444 fp.writelines(lines)
457 445 fp.close()
458 446
447 def copy(self, src, dst):
448 basedir = self.opener.base
449 abssrc, absdst = [scmutil.canonpath(basedir, basedir, x)
450 for x in [src, dst]]
451 if os.path.lexists(absdst):
452 raise util.Abort(_("cannot create %s: destination already exists")
453 % dst)
454 dstdir = os.path.dirname(absdst)
455 if dstdir and not os.path.isdir(dstdir):
456 try:
457 os.makedirs(dstdir)
458 except IOError:
459 raise util.Abort(
460 _("cannot create %s: unable to create destination directory")
461 % dst)
462 util.copyfile(abssrc, absdst)
463
459 464 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
460 465 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
461 466 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
462 467 eolmodes = ['strict', 'crlf', 'lf', 'auto']
463 468
464 469 class patchfile(object):
465 470 def __init__(self, ui, fname, backend, missing=False, eolmode='strict'):
466 471 self.fname = fname
467 472 self.eolmode = eolmode
468 473 self.eol = None
469 474 self.backend = backend
470 475 self.ui = ui
471 476 self.lines = []
472 477 self.exists = False
473 478 self.missing = missing
474 479 if not missing:
475 480 try:
476 481 self.lines = self.backend.readlines(fname)
477 482 if self.lines:
478 483 # Normalize line endings
479 484 if self.lines[0].endswith('\r\n'):
480 485 self.eol = '\r\n'
481 486 elif self.lines[0].endswith('\n'):
482 487 self.eol = '\n'
483 488 if eolmode != 'strict':
484 489 nlines = []
485 490 for l in self.lines:
486 491 if l.endswith('\r\n'):
487 492 l = l[:-2] + '\n'
488 493 nlines.append(l)
489 494 self.lines = nlines
490 495 self.exists = True
491 496 except IOError:
492 497 pass
493 498 else:
494 499 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
495 500
496 501 self.hash = {}
497 502 self.dirty = 0
498 503 self.offset = 0
499 504 self.skew = 0
500 505 self.rej = []
501 506 self.fileprinted = False
502 507 self.printfile(False)
503 508 self.hunks = 0
504 509
505 510 def writelines(self, fname, lines):
506 511 if self.eolmode == 'auto':
507 512 eol = self.eol
508 513 elif self.eolmode == 'crlf':
509 514 eol = '\r\n'
510 515 else:
511 516 eol = '\n'
512 517
513 518 if self.eolmode != 'strict' and eol and eol != '\n':
514 519 rawlines = []
515 520 for l in lines:
516 521 if l and l[-1] == '\n':
517 522 l = l[:-1] + eol
518 523 rawlines.append(l)
519 524 lines = rawlines
520 525
521 526 self.backend.writelines(fname, lines)
522 527
523 528 def printfile(self, warn):
524 529 if self.fileprinted:
525 530 return
526 531 if warn or self.ui.verbose:
527 532 self.fileprinted = True
528 533 s = _("patching file %s\n") % self.fname
529 534 if warn:
530 535 self.ui.warn(s)
531 536 else:
532 537 self.ui.note(s)
533 538
534 539
535 540 def findlines(self, l, linenum):
536 541 # looks through the hash and finds candidate lines. The
537 542 # result is a list of line numbers sorted based on distance
538 543 # from linenum
539 544
540 545 cand = self.hash.get(l, [])
541 546 if len(cand) > 1:
542 547 # resort our list of potentials forward then back.
543 548 cand.sort(key=lambda x: abs(x - linenum))
544 549 return cand
545 550
546 551 def write_rej(self):
547 552 # our rejects are a little different from patch(1). This always
548 553 # creates rejects in the same form as the original patch. A file
549 554 # header is inserted so that you can run the reject through patch again
550 555 # without having to type the filename.
551 556 if not self.rej:
552 557 return
553 558 base = os.path.basename(self.fname)
554 559 lines = ["--- %s\n+++ %s\n" % (base, base)]
555 560 for x in self.rej:
556 561 for l in x.hunk:
557 562 lines.append(l)
558 563 if l[-1] != '\n':
559 564 lines.append("\n\ No newline at end of file\n")
560 565 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
561 566
562 567 def apply(self, h):
563 568 if not h.complete():
564 569 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
565 570 (h.number, h.desc, len(h.a), h.lena, len(h.b),
566 571 h.lenb))
567 572
568 573 self.hunks += 1
569 574
570 575 if self.missing:
571 576 self.rej.append(h)
572 577 return -1
573 578
574 579 if self.exists and h.createfile():
575 580 self.ui.warn(_("file %s already exists\n") % self.fname)
576 581 self.rej.append(h)
577 582 return -1
578 583
579 584 if isinstance(h, binhunk):
580 585 if h.rmfile():
581 586 self.backend.unlink(self.fname)
582 587 else:
583 588 self.lines[:] = h.new()
584 589 self.offset += len(h.new())
585 590 self.dirty = True
586 591 return 0
587 592
588 593 horig = h
589 594 if (self.eolmode in ('crlf', 'lf')
590 595 or self.eolmode == 'auto' and self.eol):
591 596 # If new eols are going to be normalized, then normalize
592 597 # hunk data before patching. Otherwise, preserve input
593 598 # line-endings.
594 599 h = h.getnormalized()
595 600
596 601 # fast case first, no offsets, no fuzz
597 602 old = h.old()
598 603 # patch starts counting at 1 unless we are adding the file
599 604 if h.starta == 0:
600 605 start = 0
601 606 else:
602 607 start = h.starta + self.offset - 1
603 608 orig_start = start
604 609 # if there's skew we want to emit the "(offset %d lines)" even
605 610 # when the hunk cleanly applies at start + skew, so skip the
606 611 # fast case code
607 612 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
608 613 if h.rmfile():
609 614 self.backend.unlink(self.fname)
610 615 else:
611 616 self.lines[start : start + h.lena] = h.new()
612 617 self.offset += h.lenb - h.lena
613 618 self.dirty = True
614 619 return 0
615 620
616 621 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
617 622 self.hash = {}
618 623 for x, s in enumerate(self.lines):
619 624 self.hash.setdefault(s, []).append(x)
620 625 if h.hunk[-1][0] != ' ':
621 626 # if the hunk tried to put something at the bottom of the file
622 627 # override the start line and use eof here
623 628 search_start = len(self.lines)
624 629 else:
625 630 search_start = orig_start + self.skew
626 631
627 632 for fuzzlen in xrange(3):
628 633 for toponly in [True, False]:
629 634 old = h.old(fuzzlen, toponly)
630 635
631 636 cand = self.findlines(old[0][1:], search_start)
632 637 for l in cand:
633 638 if diffhelpers.testhunk(old, self.lines, l) == 0:
634 639 newlines = h.new(fuzzlen, toponly)
635 640 self.lines[l : l + len(old)] = newlines
636 641 self.offset += len(newlines) - len(old)
637 642 self.skew = l - orig_start
638 643 self.dirty = True
639 644 offset = l - orig_start - fuzzlen
640 645 if fuzzlen:
641 646 msg = _("Hunk #%d succeeded at %d "
642 647 "with fuzz %d "
643 648 "(offset %d lines).\n")
644 649 self.printfile(True)
645 650 self.ui.warn(msg %
646 651 (h.number, l + 1, fuzzlen, offset))
647 652 else:
648 653 msg = _("Hunk #%d succeeded at %d "
649 654 "(offset %d lines).\n")
650 655 self.ui.note(msg % (h.number, l + 1, offset))
651 656 return fuzzlen
652 657 self.printfile(True)
653 658 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
654 659 self.rej.append(horig)
655 660 return -1
656 661
657 662 def close(self):
658 663 if self.dirty:
659 664 self.writelines(self.fname, self.lines)
660 665 self.write_rej()
661 666 return len(self.rej)
662 667
663 668 class hunk(object):
664 669 def __init__(self, desc, num, lr, context, create=False, remove=False):
665 670 self.number = num
666 671 self.desc = desc
667 672 self.hunk = [desc]
668 673 self.a = []
669 674 self.b = []
670 675 self.starta = self.lena = None
671 676 self.startb = self.lenb = None
672 677 if lr is not None:
673 678 if context:
674 679 self.read_context_hunk(lr)
675 680 else:
676 681 self.read_unified_hunk(lr)
677 682 self.create = create
678 683 self.remove = remove and not create
679 684
680 685 def getnormalized(self):
681 686 """Return a copy with line endings normalized to LF."""
682 687
683 688 def normalize(lines):
684 689 nlines = []
685 690 for line in lines:
686 691 if line.endswith('\r\n'):
687 692 line = line[:-2] + '\n'
688 693 nlines.append(line)
689 694 return nlines
690 695
691 696 # Dummy object, it is rebuilt manually
692 697 nh = hunk(self.desc, self.number, None, None, False, False)
693 698 nh.number = self.number
694 699 nh.desc = self.desc
695 700 nh.hunk = self.hunk
696 701 nh.a = normalize(self.a)
697 702 nh.b = normalize(self.b)
698 703 nh.starta = self.starta
699 704 nh.startb = self.startb
700 705 nh.lena = self.lena
701 706 nh.lenb = self.lenb
702 707 nh.create = self.create
703 708 nh.remove = self.remove
704 709 return nh
705 710
706 711 def read_unified_hunk(self, lr):
707 712 m = unidesc.match(self.desc)
708 713 if not m:
709 714 raise PatchError(_("bad hunk #%d") % self.number)
710 715 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
711 716 if self.lena is None:
712 717 self.lena = 1
713 718 else:
714 719 self.lena = int(self.lena)
715 720 if self.lenb is None:
716 721 self.lenb = 1
717 722 else:
718 723 self.lenb = int(self.lenb)
719 724 self.starta = int(self.starta)
720 725 self.startb = int(self.startb)
721 726 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
722 727 # if we hit eof before finishing out the hunk, the last line will
723 728 # be zero length. Lets try to fix it up.
724 729 while len(self.hunk[-1]) == 0:
725 730 del self.hunk[-1]
726 731 del self.a[-1]
727 732 del self.b[-1]
728 733 self.lena -= 1
729 734 self.lenb -= 1
730 735 self._fixnewline(lr)
731 736
732 737 def read_context_hunk(self, lr):
733 738 self.desc = lr.readline()
734 739 m = contextdesc.match(self.desc)
735 740 if not m:
736 741 raise PatchError(_("bad hunk #%d") % self.number)
737 742 foo, self.starta, foo2, aend, foo3 = m.groups()
738 743 self.starta = int(self.starta)
739 744 if aend is None:
740 745 aend = self.starta
741 746 self.lena = int(aend) - self.starta
742 747 if self.starta:
743 748 self.lena += 1
744 749 for x in xrange(self.lena):
745 750 l = lr.readline()
746 751 if l.startswith('---'):
747 752 # lines addition, old block is empty
748 753 lr.push(l)
749 754 break
750 755 s = l[2:]
751 756 if l.startswith('- ') or l.startswith('! '):
752 757 u = '-' + s
753 758 elif l.startswith(' '):
754 759 u = ' ' + s
755 760 else:
756 761 raise PatchError(_("bad hunk #%d old text line %d") %
757 762 (self.number, x))
758 763 self.a.append(u)
759 764 self.hunk.append(u)
760 765
761 766 l = lr.readline()
762 767 if l.startswith('\ '):
763 768 s = self.a[-1][:-1]
764 769 self.a[-1] = s
765 770 self.hunk[-1] = s
766 771 l = lr.readline()
767 772 m = contextdesc.match(l)
768 773 if not m:
769 774 raise PatchError(_("bad hunk #%d") % self.number)
770 775 foo, self.startb, foo2, bend, foo3 = m.groups()
771 776 self.startb = int(self.startb)
772 777 if bend is None:
773 778 bend = self.startb
774 779 self.lenb = int(bend) - self.startb
775 780 if self.startb:
776 781 self.lenb += 1
777 782 hunki = 1
778 783 for x in xrange(self.lenb):
779 784 l = lr.readline()
780 785 if l.startswith('\ '):
781 786 # XXX: the only way to hit this is with an invalid line range.
782 787 # The no-eol marker is not counted in the line range, but I
783 788 # guess there are diff(1) out there which behave differently.
784 789 s = self.b[-1][:-1]
785 790 self.b[-1] = s
786 791 self.hunk[hunki - 1] = s
787 792 continue
788 793 if not l:
789 794 # line deletions, new block is empty and we hit EOF
790 795 lr.push(l)
791 796 break
792 797 s = l[2:]
793 798 if l.startswith('+ ') or l.startswith('! '):
794 799 u = '+' + s
795 800 elif l.startswith(' '):
796 801 u = ' ' + s
797 802 elif len(self.b) == 0:
798 803 # line deletions, new block is empty
799 804 lr.push(l)
800 805 break
801 806 else:
802 807 raise PatchError(_("bad hunk #%d old text line %d") %
803 808 (self.number, x))
804 809 self.b.append(s)
805 810 while True:
806 811 if hunki >= len(self.hunk):
807 812 h = ""
808 813 else:
809 814 h = self.hunk[hunki]
810 815 hunki += 1
811 816 if h == u:
812 817 break
813 818 elif h.startswith('-'):
814 819 continue
815 820 else:
816 821 self.hunk.insert(hunki - 1, u)
817 822 break
818 823
819 824 if not self.a:
820 825 # this happens when lines were only added to the hunk
821 826 for x in self.hunk:
822 827 if x.startswith('-') or x.startswith(' '):
823 828 self.a.append(x)
824 829 if not self.b:
825 830 # this happens when lines were only deleted from the hunk
826 831 for x in self.hunk:
827 832 if x.startswith('+') or x.startswith(' '):
828 833 self.b.append(x[1:])
829 834 # @@ -start,len +start,len @@
830 835 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
831 836 self.startb, self.lenb)
832 837 self.hunk[0] = self.desc
833 838 self._fixnewline(lr)
834 839
835 840 def _fixnewline(self, lr):
836 841 l = lr.readline()
837 842 if l.startswith('\ '):
838 843 diffhelpers.fix_newline(self.hunk, self.a, self.b)
839 844 else:
840 845 lr.push(l)
841 846
842 847 def complete(self):
843 848 return len(self.a) == self.lena and len(self.b) == self.lenb
844 849
845 850 def createfile(self):
846 851 return self.starta == 0 and self.lena == 0 and self.create
847 852
848 853 def rmfile(self):
849 854 return self.startb == 0 and self.lenb == 0 and self.remove
850 855
851 856 def fuzzit(self, l, fuzz, toponly):
852 857 # this removes context lines from the top and bottom of list 'l'. It
853 858 # checks the hunk to make sure only context lines are removed, and then
854 859 # returns a new shortened list of lines.
855 860 fuzz = min(fuzz, len(l)-1)
856 861 if fuzz:
857 862 top = 0
858 863 bot = 0
859 864 hlen = len(self.hunk)
860 865 for x in xrange(hlen - 1):
861 866 # the hunk starts with the @@ line, so use x+1
862 867 if self.hunk[x + 1][0] == ' ':
863 868 top += 1
864 869 else:
865 870 break
866 871 if not toponly:
867 872 for x in xrange(hlen - 1):
868 873 if self.hunk[hlen - bot - 1][0] == ' ':
869 874 bot += 1
870 875 else:
871 876 break
872 877
873 878 # top and bot now count context in the hunk
874 879 # adjust them if either one is short
875 880 context = max(top, bot, 3)
876 881 if bot < context:
877 882 bot = max(0, fuzz - (context - bot))
878 883 else:
879 884 bot = min(fuzz, bot)
880 885 if top < context:
881 886 top = max(0, fuzz - (context - top))
882 887 else:
883 888 top = min(fuzz, top)
884 889
885 890 return l[top:len(l)-bot]
886 891 return l
887 892
888 893 def old(self, fuzz=0, toponly=False):
889 894 return self.fuzzit(self.a, fuzz, toponly)
890 895
891 896 def new(self, fuzz=0, toponly=False):
892 897 return self.fuzzit(self.b, fuzz, toponly)
893 898
894 899 class binhunk:
895 900 'A binary patch file. Only understands literals so far.'
896 901 def __init__(self, gitpatch):
897 902 self.gitpatch = gitpatch
898 903 self.text = None
899 904 self.hunk = ['GIT binary patch\n']
900 905
901 906 def createfile(self):
902 907 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
903 908
904 909 def rmfile(self):
905 910 return self.gitpatch.op == 'DELETE'
906 911
907 912 def complete(self):
908 913 return self.text is not None
909 914
910 915 def new(self):
911 916 return [self.text]
912 917
913 918 def extract(self, lr):
914 919 line = lr.readline()
915 920 self.hunk.append(line)
916 921 while line and not line.startswith('literal '):
917 922 line = lr.readline()
918 923 self.hunk.append(line)
919 924 if not line:
920 925 raise PatchError(_('could not extract binary patch'))
921 926 size = int(line[8:].rstrip())
922 927 dec = []
923 928 line = lr.readline()
924 929 self.hunk.append(line)
925 930 while len(line) > 1:
926 931 l = line[0]
927 932 if l <= 'Z' and l >= 'A':
928 933 l = ord(l) - ord('A') + 1
929 934 else:
930 935 l = ord(l) - ord('a') + 27
931 936 dec.append(base85.b85decode(line[1:-1])[:l])
932 937 line = lr.readline()
933 938 self.hunk.append(line)
934 939 text = zlib.decompress(''.join(dec))
935 940 if len(text) != size:
936 941 raise PatchError(_('binary patch is %d bytes, not %d') %
937 942 len(text), size)
938 943 self.text = text
939 944
940 945 def parsefilename(str):
941 946 # --- filename \t|space stuff
942 947 s = str[4:].rstrip('\r\n')
943 948 i = s.find('\t')
944 949 if i < 0:
945 950 i = s.find(' ')
946 951 if i < 0:
947 952 return s
948 953 return s[:i]
949 954
950 955 def pathstrip(path, strip):
951 956 pathlen = len(path)
952 957 i = 0
953 958 if strip == 0:
954 959 return '', path.rstrip()
955 960 count = strip
956 961 while count > 0:
957 962 i = path.find('/', i)
958 963 if i == -1:
959 964 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
960 965 (count, strip, path))
961 966 i += 1
962 967 # consume '//' in the path
963 968 while i < pathlen - 1 and path[i] == '/':
964 969 i += 1
965 970 count -= 1
966 971 return path[:i].lstrip(), path[i:].rstrip()
967 972
968 973 def selectfile(afile_orig, bfile_orig, hunk, strip):
969 974 nulla = afile_orig == "/dev/null"
970 975 nullb = bfile_orig == "/dev/null"
971 976 abase, afile = pathstrip(afile_orig, strip)
972 977 gooda = not nulla and os.path.lexists(afile)
973 978 bbase, bfile = pathstrip(bfile_orig, strip)
974 979 if afile == bfile:
975 980 goodb = gooda
976 981 else:
977 982 goodb = not nullb and os.path.lexists(bfile)
978 983 createfunc = hunk.createfile
979 984 missing = not goodb and not gooda and not createfunc()
980 985
981 986 # some diff programs apparently produce patches where the afile is
982 987 # not /dev/null, but afile starts with bfile
983 988 abasedir = afile[:afile.rfind('/') + 1]
984 989 bbasedir = bfile[:bfile.rfind('/') + 1]
985 990 if missing and abasedir == bbasedir and afile.startswith(bfile):
986 991 # this isn't very pretty
987 992 hunk.create = True
988 993 if createfunc():
989 994 missing = False
990 995 else:
991 996 hunk.create = False
992 997
993 998 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
994 999 # diff is between a file and its backup. In this case, the original
995 1000 # file should be patched (see original mpatch code).
996 1001 isbackup = (abase == bbase and bfile.startswith(afile))
997 1002 fname = None
998 1003 if not missing:
999 1004 if gooda and goodb:
1000 1005 fname = isbackup and afile or bfile
1001 1006 elif gooda:
1002 1007 fname = afile
1003 1008
1004 1009 if not fname:
1005 1010 if not nullb:
1006 1011 fname = isbackup and afile or bfile
1007 1012 elif not nulla:
1008 1013 fname = afile
1009 1014 else:
1010 1015 raise PatchError(_("undefined source and destination files"))
1011 1016
1012 1017 return fname, missing
1013 1018
1014 1019 def scangitpatch(lr, firstline):
1015 1020 """
1016 1021 Git patches can emit:
1017 1022 - rename a to b
1018 1023 - change b
1019 1024 - copy a to c
1020 1025 - change c
1021 1026
1022 1027 We cannot apply this sequence as-is, the renamed 'a' could not be
1023 1028 found for it would have been renamed already. And we cannot copy
1024 1029 from 'b' instead because 'b' would have been changed already. So
1025 1030 we scan the git patch for copy and rename commands so we can
1026 1031 perform the copies ahead of time.
1027 1032 """
1028 1033 pos = 0
1029 1034 try:
1030 1035 pos = lr.fp.tell()
1031 1036 fp = lr.fp
1032 1037 except IOError:
1033 1038 fp = cStringIO.StringIO(lr.fp.read())
1034 1039 gitlr = linereader(fp, lr.textmode)
1035 1040 gitlr.push(firstline)
1036 1041 gitpatches = readgitpatch(gitlr)
1037 1042 fp.seek(pos)
1038 1043 return gitpatches
1039 1044
1040 1045 def iterhunks(fp):
1041 1046 """Read a patch and yield the following events:
1042 1047 - ("file", afile, bfile, firsthunk): select a new target file.
1043 1048 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1044 1049 "file" event.
1045 1050 - ("git", gitchanges): current diff is in git format, gitchanges
1046 1051 maps filenames to gitpatch records. Unique event.
1047 1052 """
1048 1053 changed = {}
1049 1054 afile = ""
1050 1055 bfile = ""
1051 1056 state = None
1052 1057 hunknum = 0
1053 1058 emitfile = newfile = False
1054 1059 git = False
1055 1060
1056 1061 # our states
1057 1062 BFILE = 1
1058 1063 context = None
1059 1064 lr = linereader(fp)
1060 1065
1061 1066 while True:
1062 1067 x = lr.readline()
1063 1068 if not x:
1064 1069 break
1065 1070 if (state == BFILE and ((not context and x[0] == '@') or
1066 1071 ((context is not False) and x.startswith('***************')))):
1067 1072 if context is None and x.startswith('***************'):
1068 1073 context = True
1069 1074 gpatch = changed.get(bfile)
1070 1075 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1071 1076 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1072 1077 h = hunk(x, hunknum + 1, lr, context, create, remove)
1073 1078 hunknum += 1
1074 1079 if emitfile:
1075 1080 emitfile = False
1076 1081 yield 'file', (afile, bfile, h)
1077 1082 yield 'hunk', h
1078 1083 elif state == BFILE and x.startswith('GIT binary patch'):
1079 1084 h = binhunk(changed[bfile])
1080 1085 hunknum += 1
1081 1086 if emitfile:
1082 1087 emitfile = False
1083 1088 yield 'file', ('a/' + afile, 'b/' + bfile, h)
1084 1089 h.extract(lr)
1085 1090 yield 'hunk', h
1086 1091 elif x.startswith('diff --git'):
1087 1092 # check for git diff, scanning the whole patch file if needed
1088 1093 m = gitre.match(x)
1089 1094 if m:
1090 1095 afile, bfile = m.group(1, 2)
1091 1096 if not git:
1092 1097 git = True
1093 1098 gitpatches = scangitpatch(lr, x)
1094 1099 yield 'git', gitpatches
1095 1100 for gp in gitpatches:
1096 1101 changed[gp.path] = gp
1097 1102 # else error?
1098 1103 # copy/rename + modify should modify target, not source
1099 1104 gp = changed.get(bfile)
1100 1105 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1101 1106 or gp.mode):
1102 1107 afile = bfile
1103 1108 newfile = True
1104 1109 elif x.startswith('---'):
1105 1110 # check for a unified diff
1106 1111 l2 = lr.readline()
1107 1112 if not l2.startswith('+++'):
1108 1113 lr.push(l2)
1109 1114 continue
1110 1115 newfile = True
1111 1116 context = False
1112 1117 afile = parsefilename(x)
1113 1118 bfile = parsefilename(l2)
1114 1119 elif x.startswith('***'):
1115 1120 # check for a context diff
1116 1121 l2 = lr.readline()
1117 1122 if not l2.startswith('---'):
1118 1123 lr.push(l2)
1119 1124 continue
1120 1125 l3 = lr.readline()
1121 1126 lr.push(l3)
1122 1127 if not l3.startswith("***************"):
1123 1128 lr.push(l2)
1124 1129 continue
1125 1130 newfile = True
1126 1131 context = True
1127 1132 afile = parsefilename(x)
1128 1133 bfile = parsefilename(l2)
1129 1134
1130 1135 if newfile:
1131 1136 newfile = False
1132 1137 emitfile = True
1133 1138 state = BFILE
1134 1139 hunknum = 0
1135 1140
1136 1141 def applydiff(ui, fp, changed, strip=1, eolmode='strict'):
1137 1142 """Reads a patch from fp and tries to apply it.
1138 1143
1139 1144 The dict 'changed' is filled in with all of the filenames changed
1140 1145 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1141 1146 found and 1 if there was any fuzz.
1142 1147
1143 1148 If 'eolmode' is 'strict', the patch content and patched file are
1144 1149 read in binary mode. Otherwise, line endings are ignored when
1145 1150 patching then normalized according to 'eolmode'.
1146 1151
1147 1152 Callers probably want to call '_updatedir' after this to
1148 1153 apply certain categories of changes not done by this function.
1149 1154 """
1150 return _applydiff(ui, fp, patchfile, copyfile, changed, strip=strip,
1155 return _applydiff(ui, fp, patchfile, changed, strip=strip,
1151 1156 eolmode=eolmode)
1152 1157
1153 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1, eolmode='strict'):
1158 def _applydiff(ui, fp, patcher, changed, strip=1, eolmode='strict'):
1154 1159 rejects = 0
1155 1160 err = 0
1156 1161 current_file = None
1157 1162 cwd = os.getcwd()
1158 backend = fsbackend(ui, scmutil.opener(cwd))
1163 backend = fsbackend(ui, os.getcwd())
1159 1164
1160 1165 for state, values in iterhunks(fp):
1161 1166 if state == 'hunk':
1162 1167 if not current_file:
1163 1168 continue
1164 1169 ret = current_file.apply(values)
1165 1170 if ret >= 0:
1166 1171 changed.setdefault(current_file.fname, None)
1167 1172 if ret > 0:
1168 1173 err = 1
1169 1174 elif state == 'file':
1170 1175 if current_file:
1171 1176 rejects += current_file.close()
1172 1177 afile, bfile, first_hunk = values
1173 1178 try:
1174 1179 current_file, missing = selectfile(afile, bfile,
1175 1180 first_hunk, strip)
1176 1181 current_file = patcher(ui, current_file, backend,
1177 1182 missing=missing, eolmode=eolmode)
1178 1183 except PatchError, inst:
1179 1184 ui.warn(str(inst) + '\n')
1180 1185 current_file = None
1181 1186 rejects += 1
1182 1187 continue
1183 1188 elif state == 'git':
1184 1189 for gp in values:
1185 1190 gp.path = pathstrip(gp.path, strip - 1)[1]
1186 1191 if gp.oldpath:
1187 1192 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1188 1193 # Binary patches really overwrite target files, copying them
1189 1194 # will just make it fails with "target file exists"
1190 1195 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1191 copyfn(gp.oldpath, gp.path, cwd)
1196 backend.copy(gp.oldpath, gp.path)
1192 1197 changed[gp.path] = gp
1193 1198 else:
1194 1199 raise util.Abort(_('unsupported parser state: %s') % state)
1195 1200
1196 1201 if current_file:
1197 1202 rejects += current_file.close()
1198 1203
1199 1204 if rejects:
1200 1205 return -1
1201 1206 return err
1202 1207
1203 1208 def _updatedir(ui, repo, patches, similarity=0):
1204 1209 '''Update dirstate after patch application according to metadata'''
1205 1210 if not patches:
1206 1211 return []
1207 1212 copies = []
1208 1213 removes = set()
1209 1214 cfiles = patches.keys()
1210 1215 cwd = repo.getcwd()
1211 1216 if cwd:
1212 1217 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1213 1218 for f in patches:
1214 1219 gp = patches[f]
1215 1220 if not gp:
1216 1221 continue
1217 1222 if gp.op == 'RENAME':
1218 1223 copies.append((gp.oldpath, gp.path))
1219 1224 removes.add(gp.oldpath)
1220 1225 elif gp.op == 'COPY':
1221 1226 copies.append((gp.oldpath, gp.path))
1222 1227 elif gp.op == 'DELETE':
1223 1228 removes.add(gp.path)
1224 1229
1225 1230 wctx = repo[None]
1226 1231 for src, dst in copies:
1227 1232 scmutil.dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
1228 1233 if (not similarity) and removes:
1229 1234 wctx.remove(sorted(removes), True)
1230 1235
1231 1236 for f in patches:
1232 1237 gp = patches[f]
1233 1238 if gp and gp.mode:
1234 1239 islink, isexec = gp.mode
1235 1240 dst = repo.wjoin(gp.path)
1236 1241 # patch won't create empty files
1237 1242 if gp.op == 'ADD' and not os.path.lexists(dst):
1238 1243 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1239 1244 repo.wwrite(gp.path, '', flags)
1240 1245 util.setflags(dst, islink, isexec)
1241 1246 scmutil.addremove(repo, cfiles, similarity=similarity)
1242 1247 files = patches.keys()
1243 1248 files.extend([r for r in removes if r not in files])
1244 1249 return sorted(files)
1245 1250
1246 1251 def _externalpatch(patcher, patchname, ui, strip, cwd, files):
1247 1252 """use <patcher> to apply <patchname> to the working directory.
1248 1253 returns whether patch was applied with fuzz factor."""
1249 1254
1250 1255 fuzz = False
1251 1256 args = []
1252 1257 if cwd:
1253 1258 args.append('-d %s' % util.shellquote(cwd))
1254 1259 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1255 1260 util.shellquote(patchname)))
1256 1261
1257 1262 for line in fp:
1258 1263 line = line.rstrip()
1259 1264 ui.note(line + '\n')
1260 1265 if line.startswith('patching file '):
1261 1266 pf = util.parsepatchoutput(line)
1262 1267 printed_file = False
1263 1268 files.setdefault(pf, None)
1264 1269 elif line.find('with fuzz') >= 0:
1265 1270 fuzz = True
1266 1271 if not printed_file:
1267 1272 ui.warn(pf + '\n')
1268 1273 printed_file = True
1269 1274 ui.warn(line + '\n')
1270 1275 elif line.find('saving rejects to file') >= 0:
1271 1276 ui.warn(line + '\n')
1272 1277 elif line.find('FAILED') >= 0:
1273 1278 if not printed_file:
1274 1279 ui.warn(pf + '\n')
1275 1280 printed_file = True
1276 1281 ui.warn(line + '\n')
1277 1282 code = fp.close()
1278 1283 if code:
1279 1284 raise PatchError(_("patch command failed: %s") %
1280 1285 util.explainexit(code)[0])
1281 1286 return fuzz
1282 1287
1283 1288 def internalpatch(ui, repo, patchobj, strip, cwd, files=None, eolmode='strict',
1284 1289 similarity=0):
1285 1290 """use builtin patch to apply <patchobj> to the working directory.
1286 1291 returns whether patch was applied with fuzz factor."""
1287 1292
1288 1293 if files is None:
1289 1294 files = {}
1290 1295 if eolmode is None:
1291 1296 eolmode = ui.config('patch', 'eol', 'strict')
1292 1297 if eolmode.lower() not in eolmodes:
1293 1298 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1294 1299 eolmode = eolmode.lower()
1295 1300
1296 1301 try:
1297 1302 fp = open(patchobj, 'rb')
1298 1303 except TypeError:
1299 1304 fp = patchobj
1300 1305 if cwd:
1301 1306 curdir = os.getcwd()
1302 1307 os.chdir(cwd)
1303 1308 try:
1304 1309 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1305 1310 finally:
1306 1311 if cwd:
1307 1312 os.chdir(curdir)
1308 1313 if fp != patchobj:
1309 1314 fp.close()
1310 1315 touched = _updatedir(ui, repo, files, similarity)
1311 1316 files.update(dict.fromkeys(touched))
1312 1317 if ret < 0:
1313 1318 raise PatchError(_('patch failed to apply'))
1314 1319 return ret > 0
1315 1320
1316 1321 def patch(ui, repo, patchname, strip=1, cwd=None, files=None, eolmode='strict',
1317 1322 similarity=0):
1318 1323 """Apply <patchname> to the working directory.
1319 1324
1320 1325 'eolmode' specifies how end of lines should be handled. It can be:
1321 1326 - 'strict': inputs are read in binary mode, EOLs are preserved
1322 1327 - 'crlf': EOLs are ignored when patching and reset to CRLF
1323 1328 - 'lf': EOLs are ignored when patching and reset to LF
1324 1329 - None: get it from user settings, default to 'strict'
1325 1330 'eolmode' is ignored when using an external patcher program.
1326 1331
1327 1332 Returns whether patch was applied with fuzz factor.
1328 1333 """
1329 1334 patcher = ui.config('ui', 'patch')
1330 1335 if files is None:
1331 1336 files = {}
1332 1337 try:
1333 1338 if patcher:
1334 1339 try:
1335 1340 return _externalpatch(patcher, patchname, ui, strip, cwd,
1336 1341 files)
1337 1342 finally:
1338 1343 touched = _updatedir(ui, repo, files, similarity)
1339 1344 files.update(dict.fromkeys(touched))
1340 1345 return internalpatch(ui, repo, patchname, strip, cwd, files, eolmode,
1341 1346 similarity)
1342 1347 except PatchError, err:
1343 1348 raise util.Abort(str(err))
1344 1349
1345 1350 def changedfiles(patchpath, strip=1):
1346 1351 fp = open(patchpath, 'rb')
1347 1352 try:
1348 1353 changed = set()
1349 1354 for state, values in iterhunks(fp):
1350 1355 if state == 'hunk':
1351 1356 continue
1352 1357 elif state == 'file':
1353 1358 afile, bfile, first_hunk = values
1354 1359 current_file, missing = selectfile(afile, bfile,
1355 1360 first_hunk, strip)
1356 1361 changed.add(current_file)
1357 1362 elif state == 'git':
1358 1363 for gp in values:
1359 1364 gp.path = pathstrip(gp.path, strip - 1)[1]
1360 1365 changed.add(gp.path)
1361 1366 if gp.oldpath:
1362 1367 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1363 1368 if gp.op == 'RENAME':
1364 1369 changed.add(gp.oldpath)
1365 1370 else:
1366 1371 raise util.Abort(_('unsupported parser state: %s') % state)
1367 1372 return changed
1368 1373 finally:
1369 1374 fp.close()
1370 1375
1371 1376 def b85diff(to, tn):
1372 1377 '''print base85-encoded binary diff'''
1373 1378 def gitindex(text):
1374 1379 if not text:
1375 1380 return hex(nullid)
1376 1381 l = len(text)
1377 1382 s = util.sha1('blob %d\0' % l)
1378 1383 s.update(text)
1379 1384 return s.hexdigest()
1380 1385
1381 1386 def fmtline(line):
1382 1387 l = len(line)
1383 1388 if l <= 26:
1384 1389 l = chr(ord('A') + l - 1)
1385 1390 else:
1386 1391 l = chr(l - 26 + ord('a') - 1)
1387 1392 return '%c%s\n' % (l, base85.b85encode(line, True))
1388 1393
1389 1394 def chunk(text, csize=52):
1390 1395 l = len(text)
1391 1396 i = 0
1392 1397 while i < l:
1393 1398 yield text[i:i + csize]
1394 1399 i += csize
1395 1400
1396 1401 tohash = gitindex(to)
1397 1402 tnhash = gitindex(tn)
1398 1403 if tohash == tnhash:
1399 1404 return ""
1400 1405
1401 1406 # TODO: deltas
1402 1407 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1403 1408 (tohash, tnhash, len(tn))]
1404 1409 for l in chunk(zlib.compress(tn)):
1405 1410 ret.append(fmtline(l))
1406 1411 ret.append('\n')
1407 1412 return ''.join(ret)
1408 1413
1409 1414 class GitDiffRequired(Exception):
1410 1415 pass
1411 1416
1412 1417 def diffopts(ui, opts=None, untrusted=False):
1413 1418 def get(key, name=None, getter=ui.configbool):
1414 1419 return ((opts and opts.get(key)) or
1415 1420 getter('diff', name or key, None, untrusted=untrusted))
1416 1421 return mdiff.diffopts(
1417 1422 text=opts and opts.get('text'),
1418 1423 git=get('git'),
1419 1424 nodates=get('nodates'),
1420 1425 showfunc=get('show_function', 'showfunc'),
1421 1426 ignorews=get('ignore_all_space', 'ignorews'),
1422 1427 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1423 1428 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1424 1429 context=get('unified', getter=ui.config))
1425 1430
1426 1431 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1427 1432 losedatafn=None, prefix=''):
1428 1433 '''yields diff of changes to files between two nodes, or node and
1429 1434 working directory.
1430 1435
1431 1436 if node1 is None, use first dirstate parent instead.
1432 1437 if node2 is None, compare node1 with working directory.
1433 1438
1434 1439 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1435 1440 every time some change cannot be represented with the current
1436 1441 patch format. Return False to upgrade to git patch format, True to
1437 1442 accept the loss or raise an exception to abort the diff. It is
1438 1443 called with the name of current file being diffed as 'fn'. If set
1439 1444 to None, patches will always be upgraded to git format when
1440 1445 necessary.
1441 1446
1442 1447 prefix is a filename prefix that is prepended to all filenames on
1443 1448 display (used for subrepos).
1444 1449 '''
1445 1450
1446 1451 if opts is None:
1447 1452 opts = mdiff.defaultopts
1448 1453
1449 1454 if not node1 and not node2:
1450 1455 node1 = repo.dirstate.p1()
1451 1456
1452 1457 def lrugetfilectx():
1453 1458 cache = {}
1454 1459 order = []
1455 1460 def getfilectx(f, ctx):
1456 1461 fctx = ctx.filectx(f, filelog=cache.get(f))
1457 1462 if f not in cache:
1458 1463 if len(cache) > 20:
1459 1464 del cache[order.pop(0)]
1460 1465 cache[f] = fctx.filelog()
1461 1466 else:
1462 1467 order.remove(f)
1463 1468 order.append(f)
1464 1469 return fctx
1465 1470 return getfilectx
1466 1471 getfilectx = lrugetfilectx()
1467 1472
1468 1473 ctx1 = repo[node1]
1469 1474 ctx2 = repo[node2]
1470 1475
1471 1476 if not changes:
1472 1477 changes = repo.status(ctx1, ctx2, match=match)
1473 1478 modified, added, removed = changes[:3]
1474 1479
1475 1480 if not modified and not added and not removed:
1476 1481 return []
1477 1482
1478 1483 revs = None
1479 1484 if not repo.ui.quiet:
1480 1485 hexfunc = repo.ui.debugflag and hex or short
1481 1486 revs = [hexfunc(node) for node in [node1, node2] if node]
1482 1487
1483 1488 copy = {}
1484 1489 if opts.git or opts.upgrade:
1485 1490 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1486 1491
1487 1492 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1488 1493 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1489 1494 if opts.upgrade and not opts.git:
1490 1495 try:
1491 1496 def losedata(fn):
1492 1497 if not losedatafn or not losedatafn(fn=fn):
1493 1498 raise GitDiffRequired()
1494 1499 # Buffer the whole output until we are sure it can be generated
1495 1500 return list(difffn(opts.copy(git=False), losedata))
1496 1501 except GitDiffRequired:
1497 1502 return difffn(opts.copy(git=True), None)
1498 1503 else:
1499 1504 return difffn(opts, None)
1500 1505
1501 1506 def difflabel(func, *args, **kw):
1502 1507 '''yields 2-tuples of (output, label) based on the output of func()'''
1503 1508 prefixes = [('diff', 'diff.diffline'),
1504 1509 ('copy', 'diff.extended'),
1505 1510 ('rename', 'diff.extended'),
1506 1511 ('old', 'diff.extended'),
1507 1512 ('new', 'diff.extended'),
1508 1513 ('deleted', 'diff.extended'),
1509 1514 ('---', 'diff.file_a'),
1510 1515 ('+++', 'diff.file_b'),
1511 1516 ('@@', 'diff.hunk'),
1512 1517 ('-', 'diff.deleted'),
1513 1518 ('+', 'diff.inserted')]
1514 1519
1515 1520 for chunk in func(*args, **kw):
1516 1521 lines = chunk.split('\n')
1517 1522 for i, line in enumerate(lines):
1518 1523 if i != 0:
1519 1524 yield ('\n', '')
1520 1525 stripline = line
1521 1526 if line and line[0] in '+-':
1522 1527 # highlight trailing whitespace, but only in changed lines
1523 1528 stripline = line.rstrip()
1524 1529 for prefix, label in prefixes:
1525 1530 if stripline.startswith(prefix):
1526 1531 yield (stripline, label)
1527 1532 break
1528 1533 else:
1529 1534 yield (line, '')
1530 1535 if line != stripline:
1531 1536 yield (line[len(stripline):], 'diff.trailingwhitespace')
1532 1537
1533 1538 def diffui(*args, **kw):
1534 1539 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1535 1540 return difflabel(diff, *args, **kw)
1536 1541
1537 1542
1538 1543 def _addmodehdr(header, omode, nmode):
1539 1544 if omode != nmode:
1540 1545 header.append('old mode %s\n' % omode)
1541 1546 header.append('new mode %s\n' % nmode)
1542 1547
1543 1548 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1544 1549 copy, getfilectx, opts, losedatafn, prefix):
1545 1550
1546 1551 def join(f):
1547 1552 return os.path.join(prefix, f)
1548 1553
1549 1554 date1 = util.datestr(ctx1.date())
1550 1555 man1 = ctx1.manifest()
1551 1556
1552 1557 gone = set()
1553 1558 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1554 1559
1555 1560 copyto = dict([(v, k) for k, v in copy.items()])
1556 1561
1557 1562 if opts.git:
1558 1563 revs = None
1559 1564
1560 1565 for f in sorted(modified + added + removed):
1561 1566 to = None
1562 1567 tn = None
1563 1568 dodiff = True
1564 1569 header = []
1565 1570 if f in man1:
1566 1571 to = getfilectx(f, ctx1).data()
1567 1572 if f not in removed:
1568 1573 tn = getfilectx(f, ctx2).data()
1569 1574 a, b = f, f
1570 1575 if opts.git or losedatafn:
1571 1576 if f in added:
1572 1577 mode = gitmode[ctx2.flags(f)]
1573 1578 if f in copy or f in copyto:
1574 1579 if opts.git:
1575 1580 if f in copy:
1576 1581 a = copy[f]
1577 1582 else:
1578 1583 a = copyto[f]
1579 1584 omode = gitmode[man1.flags(a)]
1580 1585 _addmodehdr(header, omode, mode)
1581 1586 if a in removed and a not in gone:
1582 1587 op = 'rename'
1583 1588 gone.add(a)
1584 1589 else:
1585 1590 op = 'copy'
1586 1591 header.append('%s from %s\n' % (op, join(a)))
1587 1592 header.append('%s to %s\n' % (op, join(f)))
1588 1593 to = getfilectx(a, ctx1).data()
1589 1594 else:
1590 1595 losedatafn(f)
1591 1596 else:
1592 1597 if opts.git:
1593 1598 header.append('new file mode %s\n' % mode)
1594 1599 elif ctx2.flags(f):
1595 1600 losedatafn(f)
1596 1601 # In theory, if tn was copied or renamed we should check
1597 1602 # if the source is binary too but the copy record already
1598 1603 # forces git mode.
1599 1604 if util.binary(tn):
1600 1605 if opts.git:
1601 1606 dodiff = 'binary'
1602 1607 else:
1603 1608 losedatafn(f)
1604 1609 if not opts.git and not tn:
1605 1610 # regular diffs cannot represent new empty file
1606 1611 losedatafn(f)
1607 1612 elif f in removed:
1608 1613 if opts.git:
1609 1614 # have we already reported a copy above?
1610 1615 if ((f in copy and copy[f] in added
1611 1616 and copyto[copy[f]] == f) or
1612 1617 (f in copyto and copyto[f] in added
1613 1618 and copy[copyto[f]] == f)):
1614 1619 dodiff = False
1615 1620 else:
1616 1621 header.append('deleted file mode %s\n' %
1617 1622 gitmode[man1.flags(f)])
1618 1623 elif not to or util.binary(to):
1619 1624 # regular diffs cannot represent empty file deletion
1620 1625 losedatafn(f)
1621 1626 else:
1622 1627 oflag = man1.flags(f)
1623 1628 nflag = ctx2.flags(f)
1624 1629 binary = util.binary(to) or util.binary(tn)
1625 1630 if opts.git:
1626 1631 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1627 1632 if binary:
1628 1633 dodiff = 'binary'
1629 1634 elif binary or nflag != oflag:
1630 1635 losedatafn(f)
1631 1636 if opts.git:
1632 1637 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1633 1638
1634 1639 if dodiff:
1635 1640 if dodiff == 'binary':
1636 1641 text = b85diff(to, tn)
1637 1642 else:
1638 1643 text = mdiff.unidiff(to, date1,
1639 1644 # ctx2 date may be dynamic
1640 1645 tn, util.datestr(ctx2.date()),
1641 1646 join(a), join(b), revs, opts=opts)
1642 1647 if header and (text or len(header) > 1):
1643 1648 yield ''.join(header)
1644 1649 if text:
1645 1650 yield text
1646 1651
1647 1652 def diffstatdata(lines):
1648 1653 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1649 1654
1650 1655 filename, adds, removes = None, 0, 0
1651 1656 for line in lines:
1652 1657 if line.startswith('diff'):
1653 1658 if filename:
1654 1659 isbinary = adds == 0 and removes == 0
1655 1660 yield (filename, adds, removes, isbinary)
1656 1661 # set numbers to 0 anyway when starting new file
1657 1662 adds, removes = 0, 0
1658 1663 if line.startswith('diff --git'):
1659 1664 filename = gitre.search(line).group(1)
1660 1665 elif line.startswith('diff -r'):
1661 1666 # format: "diff -r ... -r ... filename"
1662 1667 filename = diffre.search(line).group(1)
1663 1668 elif line.startswith('+') and not line.startswith('+++'):
1664 1669 adds += 1
1665 1670 elif line.startswith('-') and not line.startswith('---'):
1666 1671 removes += 1
1667 1672 if filename:
1668 1673 isbinary = adds == 0 and removes == 0
1669 1674 yield (filename, adds, removes, isbinary)
1670 1675
1671 1676 def diffstat(lines, width=80, git=False):
1672 1677 output = []
1673 1678 stats = list(diffstatdata(lines))
1674 1679
1675 1680 maxtotal, maxname = 0, 0
1676 1681 totaladds, totalremoves = 0, 0
1677 1682 hasbinary = False
1678 1683
1679 1684 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1680 1685 for filename, adds, removes, isbinary in stats]
1681 1686
1682 1687 for filename, adds, removes, isbinary, namewidth in sized:
1683 1688 totaladds += adds
1684 1689 totalremoves += removes
1685 1690 maxname = max(maxname, namewidth)
1686 1691 maxtotal = max(maxtotal, adds + removes)
1687 1692 if isbinary:
1688 1693 hasbinary = True
1689 1694
1690 1695 countwidth = len(str(maxtotal))
1691 1696 if hasbinary and countwidth < 3:
1692 1697 countwidth = 3
1693 1698 graphwidth = width - countwidth - maxname - 6
1694 1699 if graphwidth < 10:
1695 1700 graphwidth = 10
1696 1701
1697 1702 def scale(i):
1698 1703 if maxtotal <= graphwidth:
1699 1704 return i
1700 1705 # If diffstat runs out of room it doesn't print anything,
1701 1706 # which isn't very useful, so always print at least one + or -
1702 1707 # if there were at least some changes.
1703 1708 return max(i * graphwidth // maxtotal, int(bool(i)))
1704 1709
1705 1710 for filename, adds, removes, isbinary, namewidth in sized:
1706 1711 if git and isbinary:
1707 1712 count = 'Bin'
1708 1713 else:
1709 1714 count = adds + removes
1710 1715 pluses = '+' * scale(adds)
1711 1716 minuses = '-' * scale(removes)
1712 1717 output.append(' %s%s | %*s %s%s\n' %
1713 1718 (filename, ' ' * (maxname - namewidth),
1714 1719 countwidth, count,
1715 1720 pluses, minuses))
1716 1721
1717 1722 if stats:
1718 1723 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1719 1724 % (len(stats), totaladds, totalremoves))
1720 1725
1721 1726 return ''.join(output)
1722 1727
1723 1728 def diffstatui(*args, **kw):
1724 1729 '''like diffstat(), but yields 2-tuples of (output, label) for
1725 1730 ui.write()
1726 1731 '''
1727 1732
1728 1733 for line in diffstat(*args, **kw).splitlines():
1729 1734 if line and line[-1] in '+-':
1730 1735 name, graph = line.rsplit(' ', 1)
1731 1736 yield (name + ' ', '')
1732 1737 m = re.search(r'\++', graph)
1733 1738 if m:
1734 1739 yield (m.group(0), 'diffstat.inserted')
1735 1740 m = re.search(r'-+', graph)
1736 1741 if m:
1737 1742 yield (m.group(0), 'diffstat.deleted')
1738 1743 else:
1739 1744 yield (line, '')
1740 1745 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now