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