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