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