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