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