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