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