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