##// END OF EJS Templates
patch: simplify directory creation
Patrick Mezard -
r5653:1b35bc1c default
parent child Browse files
Show More
@@ -1,1389 +1,1384 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 dirs = dirname.split(os.path.sep)
325 d = ""
326 for x in dirs:
327 d = os.path.join(d, x)
328 if not os.path.isdir(d):
329 os.mkdir(d)
324 os.makedirs(dirname)
330 325
331 326 self.hash = {}
332 327 self.dirty = 0
333 328 self.offset = 0
334 329 self.rej = []
335 330 self.fileprinted = False
336 331 self.printfile(False)
337 332 self.hunks = 0
338 333
339 334 def printfile(self, warn):
340 335 if self.fileprinted:
341 336 return
342 337 if warn or self.ui.verbose:
343 338 self.fileprinted = True
344 339 s = _("patching file %s\n") % self.fname
345 340 if warn:
346 341 self.ui.warn(s)
347 342 else:
348 343 self.ui.note(s)
349 344
350 345
351 346 def findlines(self, l, linenum):
352 347 # looks through the hash and finds candidate lines. The
353 348 # result is a list of line numbers sorted based on distance
354 349 # from linenum
355 350 def sorter(a, b):
356 351 vala = abs(a - linenum)
357 352 valb = abs(b - linenum)
358 353 return cmp(vala, valb)
359 354
360 355 try:
361 356 cand = self.hash[l]
362 357 except:
363 358 return []
364 359
365 360 if len(cand) > 1:
366 361 # resort our list of potentials forward then back.
367 362 cand.sort(sorter)
368 363 return cand
369 364
370 365 def hashlines(self):
371 366 self.hash = {}
372 367 for x in xrange(len(self.lines)):
373 368 s = self.lines[x]
374 369 self.hash.setdefault(s, []).append(x)
375 370
376 371 def write_rej(self):
377 372 # our rejects are a little different from patch(1). This always
378 373 # creates rejects in the same form as the original patch. A file
379 374 # header is inserted so that you can run the reject through patch again
380 375 # without having to type the filename.
381 376
382 377 if not self.rej:
383 378 return
384 379 if self.hunks != 1:
385 380 hunkstr = "s"
386 381 else:
387 382 hunkstr = ""
388 383
389 384 fname = self.fname + ".rej"
390 385 self.ui.warn(
391 386 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
392 387 (len(self.rej), self.hunks, hunkstr, fname))
393 388 try: os.unlink(fname)
394 389 except:
395 390 pass
396 391 fp = file(fname, 'wb')
397 392 base = os.path.basename(self.fname)
398 393 fp.write("--- %s\n+++ %s\n" % (base, base))
399 394 for x in self.rej:
400 395 for l in x.hunk:
401 396 fp.write(l)
402 397 if l[-1] != '\n':
403 398 fp.write("\n\ No newline at end of file\n")
404 399
405 400 def write(self, dest=None):
406 401 if self.dirty:
407 402 if not dest:
408 403 dest = self.fname
409 404 st = None
410 405 try:
411 406 st = os.lstat(dest)
412 407 except OSError, inst:
413 408 if inst.errno != errno.ENOENT:
414 409 raise
415 410 if st and st.st_nlink > 1:
416 411 os.unlink(dest)
417 412 fp = file(dest, 'wb')
418 413 if st and st.st_nlink > 1:
419 414 os.chmod(dest, st.st_mode)
420 415 fp.writelines(self.lines)
421 416 fp.close()
422 417
423 418 def close(self):
424 419 self.write()
425 420 self.write_rej()
426 421
427 422 def apply(self, h, reverse):
428 423 if not h.complete():
429 424 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
430 425 (h.number, h.desc, len(h.a), h.lena, len(h.b),
431 426 h.lenb))
432 427
433 428 self.hunks += 1
434 429 if reverse:
435 430 h.reverse()
436 431
437 432 if self.missing:
438 433 self.rej.append(h)
439 434 return -1
440 435
441 436 if self.exists and h.createfile():
442 437 self.ui.warn(_("file %s already exists\n") % self.fname)
443 438 self.rej.append(h)
444 439 return -1
445 440
446 441 if isinstance(h, binhunk):
447 442 if h.rmfile():
448 443 os.unlink(self.fname)
449 444 else:
450 445 self.lines[:] = h.new()
451 446 self.offset += len(h.new())
452 447 self.dirty = 1
453 448 return 0
454 449
455 450 # fast case first, no offsets, no fuzz
456 451 old = h.old()
457 452 # patch starts counting at 1 unless we are adding the file
458 453 if h.starta == 0:
459 454 start = 0
460 455 else:
461 456 start = h.starta + self.offset - 1
462 457 orig_start = start
463 458 if diffhelpers.testhunk(old, self.lines, start) == 0:
464 459 if h.rmfile():
465 460 os.unlink(self.fname)
466 461 else:
467 462 self.lines[start : start + h.lena] = h.new()
468 463 self.offset += h.lenb - h.lena
469 464 self.dirty = 1
470 465 return 0
471 466
472 467 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
473 468 self.hashlines()
474 469 if h.hunk[-1][0] != ' ':
475 470 # if the hunk tried to put something at the bottom of the file
476 471 # override the start line and use eof here
477 472 search_start = len(self.lines)
478 473 else:
479 474 search_start = orig_start
480 475
481 476 for fuzzlen in xrange(3):
482 477 for toponly in [ True, False ]:
483 478 old = h.old(fuzzlen, toponly)
484 479
485 480 cand = self.findlines(old[0][1:], search_start)
486 481 for l in cand:
487 482 if diffhelpers.testhunk(old, self.lines, l) == 0:
488 483 newlines = h.new(fuzzlen, toponly)
489 484 self.lines[l : l + len(old)] = newlines
490 485 self.offset += len(newlines) - len(old)
491 486 self.dirty = 1
492 487 if fuzzlen:
493 488 fuzzstr = "with fuzz %d " % fuzzlen
494 489 f = self.ui.warn
495 490 self.printfile(True)
496 491 else:
497 492 fuzzstr = ""
498 493 f = self.ui.note
499 494 offset = l - orig_start - fuzzlen
500 495 if offset == 1:
501 496 linestr = "line"
502 497 else:
503 498 linestr = "lines"
504 499 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
505 500 (h.number, l+1, fuzzstr, offset, linestr))
506 501 return fuzzlen
507 502 self.printfile(True)
508 503 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
509 504 self.rej.append(h)
510 505 return -1
511 506
512 507 class hunk:
513 508 def __init__(self, desc, num, lr, context):
514 509 self.number = num
515 510 self.desc = desc
516 511 self.hunk = [ desc ]
517 512 self.a = []
518 513 self.b = []
519 514 if context:
520 515 self.read_context_hunk(lr)
521 516 else:
522 517 self.read_unified_hunk(lr)
523 518
524 519 def read_unified_hunk(self, lr):
525 520 m = unidesc.match(self.desc)
526 521 if not m:
527 522 raise PatchError(_("bad hunk #%d") % self.number)
528 523 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
529 524 if self.lena == None:
530 525 self.lena = 1
531 526 else:
532 527 self.lena = int(self.lena)
533 528 if self.lenb == None:
534 529 self.lenb = 1
535 530 else:
536 531 self.lenb = int(self.lenb)
537 532 self.starta = int(self.starta)
538 533 self.startb = int(self.startb)
539 534 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
540 535 # if we hit eof before finishing out the hunk, the last line will
541 536 # be zero length. Lets try to fix it up.
542 537 while len(self.hunk[-1]) == 0:
543 538 del self.hunk[-1]
544 539 del self.a[-1]
545 540 del self.b[-1]
546 541 self.lena -= 1
547 542 self.lenb -= 1
548 543
549 544 def read_context_hunk(self, lr):
550 545 self.desc = lr.readline()
551 546 m = contextdesc.match(self.desc)
552 547 if not m:
553 548 raise PatchError(_("bad hunk #%d") % self.number)
554 549 foo, self.starta, foo2, aend, foo3 = m.groups()
555 550 self.starta = int(self.starta)
556 551 if aend == None:
557 552 aend = self.starta
558 553 self.lena = int(aend) - self.starta
559 554 if self.starta:
560 555 self.lena += 1
561 556 for x in xrange(self.lena):
562 557 l = lr.readline()
563 558 if l.startswith('---'):
564 559 lr.push(l)
565 560 break
566 561 s = l[2:]
567 562 if l.startswith('- ') or l.startswith('! '):
568 563 u = '-' + s
569 564 elif l.startswith(' '):
570 565 u = ' ' + s
571 566 else:
572 567 raise PatchError(_("bad hunk #%d old text line %d") %
573 568 (self.number, x))
574 569 self.a.append(u)
575 570 self.hunk.append(u)
576 571
577 572 l = lr.readline()
578 573 if l.startswith('\ '):
579 574 s = self.a[-1][:-1]
580 575 self.a[-1] = s
581 576 self.hunk[-1] = s
582 577 l = lr.readline()
583 578 m = contextdesc.match(l)
584 579 if not m:
585 580 raise PatchError(_("bad hunk #%d") % self.number)
586 581 foo, self.startb, foo2, bend, foo3 = m.groups()
587 582 self.startb = int(self.startb)
588 583 if bend == None:
589 584 bend = self.startb
590 585 self.lenb = int(bend) - self.startb
591 586 if self.startb:
592 587 self.lenb += 1
593 588 hunki = 1
594 589 for x in xrange(self.lenb):
595 590 l = lr.readline()
596 591 if l.startswith('\ '):
597 592 s = self.b[-1][:-1]
598 593 self.b[-1] = s
599 594 self.hunk[hunki-1] = s
600 595 continue
601 596 if not l:
602 597 lr.push(l)
603 598 break
604 599 s = l[2:]
605 600 if l.startswith('+ ') or l.startswith('! '):
606 601 u = '+' + s
607 602 elif l.startswith(' '):
608 603 u = ' ' + s
609 604 elif len(self.b) == 0:
610 605 # this can happen when the hunk does not add any lines
611 606 lr.push(l)
612 607 break
613 608 else:
614 609 raise PatchError(_("bad hunk #%d old text line %d") %
615 610 (self.number, x))
616 611 self.b.append(s)
617 612 while True:
618 613 if hunki >= len(self.hunk):
619 614 h = ""
620 615 else:
621 616 h = self.hunk[hunki]
622 617 hunki += 1
623 618 if h == u:
624 619 break
625 620 elif h.startswith('-'):
626 621 continue
627 622 else:
628 623 self.hunk.insert(hunki-1, u)
629 624 break
630 625
631 626 if not self.a:
632 627 # this happens when lines were only added to the hunk
633 628 for x in self.hunk:
634 629 if x.startswith('-') or x.startswith(' '):
635 630 self.a.append(x)
636 631 if not self.b:
637 632 # this happens when lines were only deleted from the hunk
638 633 for x in self.hunk:
639 634 if x.startswith('+') or x.startswith(' '):
640 635 self.b.append(x[1:])
641 636 # @@ -start,len +start,len @@
642 637 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
643 638 self.startb, self.lenb)
644 639 self.hunk[0] = self.desc
645 640
646 641 def reverse(self):
647 642 origlena = self.lena
648 643 origstarta = self.starta
649 644 self.lena = self.lenb
650 645 self.starta = self.startb
651 646 self.lenb = origlena
652 647 self.startb = origstarta
653 648 self.a = []
654 649 self.b = []
655 650 # self.hunk[0] is the @@ description
656 651 for x in xrange(1, len(self.hunk)):
657 652 o = self.hunk[x]
658 653 if o.startswith('-'):
659 654 n = '+' + o[1:]
660 655 self.b.append(o[1:])
661 656 elif o.startswith('+'):
662 657 n = '-' + o[1:]
663 658 self.a.append(n)
664 659 else:
665 660 n = o
666 661 self.b.append(o[1:])
667 662 self.a.append(o)
668 663 self.hunk[x] = o
669 664
670 665 def fix_newline(self):
671 666 diffhelpers.fix_newline(self.hunk, self.a, self.b)
672 667
673 668 def complete(self):
674 669 return len(self.a) == self.lena and len(self.b) == self.lenb
675 670
676 671 def createfile(self):
677 672 return self.starta == 0 and self.lena == 0
678 673
679 674 def rmfile(self):
680 675 return self.startb == 0 and self.lenb == 0
681 676
682 677 def fuzzit(self, l, fuzz, toponly):
683 678 # this removes context lines from the top and bottom of list 'l'. It
684 679 # checks the hunk to make sure only context lines are removed, and then
685 680 # returns a new shortened list of lines.
686 681 fuzz = min(fuzz, len(l)-1)
687 682 if fuzz:
688 683 top = 0
689 684 bot = 0
690 685 hlen = len(self.hunk)
691 686 for x in xrange(hlen-1):
692 687 # the hunk starts with the @@ line, so use x+1
693 688 if self.hunk[x+1][0] == ' ':
694 689 top += 1
695 690 else:
696 691 break
697 692 if not toponly:
698 693 for x in xrange(hlen-1):
699 694 if self.hunk[hlen-bot-1][0] == ' ':
700 695 bot += 1
701 696 else:
702 697 break
703 698
704 699 # top and bot now count context in the hunk
705 700 # adjust them if either one is short
706 701 context = max(top, bot, 3)
707 702 if bot < context:
708 703 bot = max(0, fuzz - (context - bot))
709 704 else:
710 705 bot = min(fuzz, bot)
711 706 if top < context:
712 707 top = max(0, fuzz - (context - top))
713 708 else:
714 709 top = min(fuzz, top)
715 710
716 711 return l[top:len(l)-bot]
717 712 return l
718 713
719 714 def old(self, fuzz=0, toponly=False):
720 715 return self.fuzzit(self.a, fuzz, toponly)
721 716
722 717 def newctrl(self):
723 718 res = []
724 719 for x in self.hunk:
725 720 c = x[0]
726 721 if c == ' ' or c == '+':
727 722 res.append(x)
728 723 return res
729 724
730 725 def new(self, fuzz=0, toponly=False):
731 726 return self.fuzzit(self.b, fuzz, toponly)
732 727
733 728 class binhunk:
734 729 'A binary patch file. Only understands literals so far.'
735 730 def __init__(self, gitpatch):
736 731 self.gitpatch = gitpatch
737 732 self.text = None
738 733 self.hunk = ['GIT binary patch\n']
739 734
740 735 def createfile(self):
741 736 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
742 737
743 738 def rmfile(self):
744 739 return self.gitpatch.op == 'DELETE'
745 740
746 741 def complete(self):
747 742 return self.text is not None
748 743
749 744 def new(self):
750 745 return [self.text]
751 746
752 747 def extract(self, fp):
753 748 line = fp.readline()
754 749 self.hunk.append(line)
755 750 while line and not line.startswith('literal '):
756 751 line = fp.readline()
757 752 self.hunk.append(line)
758 753 if not line:
759 754 raise PatchError(_('could not extract binary patch'))
760 755 size = int(line[8:].rstrip())
761 756 dec = []
762 757 line = fp.readline()
763 758 self.hunk.append(line)
764 759 while len(line) > 1:
765 760 l = line[0]
766 761 if l <= 'Z' and l >= 'A':
767 762 l = ord(l) - ord('A') + 1
768 763 else:
769 764 l = ord(l) - ord('a') + 27
770 765 dec.append(base85.b85decode(line[1:-1])[:l])
771 766 line = fp.readline()
772 767 self.hunk.append(line)
773 768 text = zlib.decompress(''.join(dec))
774 769 if len(text) != size:
775 770 raise PatchError(_('binary patch is %d bytes, not %d') %
776 771 len(text), size)
777 772 self.text = text
778 773
779 774 def parsefilename(str):
780 775 # --- filename \t|space stuff
781 776 s = str[4:]
782 777 i = s.find('\t')
783 778 if i < 0:
784 779 i = s.find(' ')
785 780 if i < 0:
786 781 return s
787 782 return s[:i]
788 783
789 784 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
790 785 def pathstrip(path, count=1):
791 786 pathlen = len(path)
792 787 i = 0
793 788 if count == 0:
794 789 return path.rstrip()
795 790 while count > 0:
796 791 i = path.find('/', i)
797 792 if i == -1:
798 793 raise PatchError(_("unable to strip away %d dirs from %s") %
799 794 (count, path))
800 795 i += 1
801 796 # consume '//' in the path
802 797 while i < pathlen - 1 and path[i] == '/':
803 798 i += 1
804 799 count -= 1
805 800 return path[i:].rstrip()
806 801
807 802 nulla = afile_orig == "/dev/null"
808 803 nullb = bfile_orig == "/dev/null"
809 804 afile = pathstrip(afile_orig, strip)
810 805 gooda = not nulla and os.path.exists(afile)
811 806 bfile = pathstrip(bfile_orig, strip)
812 807 if afile == bfile:
813 808 goodb = gooda
814 809 else:
815 810 goodb = not nullb and os.path.exists(bfile)
816 811 createfunc = hunk.createfile
817 812 if reverse:
818 813 createfunc = hunk.rmfile
819 814 missing = not goodb and not gooda and not createfunc()
820 815 fname = None
821 816 if not missing:
822 817 if gooda and goodb:
823 818 fname = (afile in bfile) and afile or bfile
824 819 elif gooda:
825 820 fname = afile
826 821
827 822 if not fname:
828 823 if not nullb:
829 824 fname = (afile in bfile) and afile or bfile
830 825 elif not nulla:
831 826 fname = afile
832 827 else:
833 828 raise PatchError(_("undefined source and destination files"))
834 829
835 830 return fname, missing
836 831
837 832 class linereader:
838 833 # simple class to allow pushing lines back into the input stream
839 834 def __init__(self, fp):
840 835 self.fp = fp
841 836 self.buf = []
842 837
843 838 def push(self, line):
844 839 self.buf.append(line)
845 840
846 841 def readline(self):
847 842 if self.buf:
848 843 l = self.buf[0]
849 844 del self.buf[0]
850 845 return l
851 846 return self.fp.readline()
852 847
853 848 def iterhunks(ui, fp, sourcefile=None):
854 849 """Read a patch and yield the following events:
855 850 - ("file", afile, bfile, firsthunk): select a new target file.
856 851 - ("hunk", hunk): a new hunk is ready to be applied, follows a
857 852 "file" event.
858 853 - ("git", gitchanges): current diff is in git format, gitchanges
859 854 maps filenames to gitpatch records. Unique event.
860 855 """
861 856
862 857 def scangitpatch(fp, firstline):
863 858 '''git patches can modify a file, then copy that file to
864 859 a new file, but expect the source to be the unmodified form.
865 860 So we scan the patch looking for that case so we can do
866 861 the copies ahead of time.'''
867 862
868 863 pos = 0
869 864 try:
870 865 pos = fp.tell()
871 866 except IOError:
872 867 fp = cStringIO.StringIO(fp.read())
873 868
874 869 (dopatch, gitpatches) = readgitpatch(fp, firstline)
875 870 fp.seek(pos)
876 871
877 872 return fp, dopatch, gitpatches
878 873
879 874 changed = {}
880 875 current_hunk = None
881 876 afile = ""
882 877 bfile = ""
883 878 state = None
884 879 hunknum = 0
885 880 emitfile = False
886 881
887 882 git = False
888 883 gitre = re.compile('diff --git (a/.*) (b/.*)')
889 884
890 885 # our states
891 886 BFILE = 1
892 887 context = None
893 888 lr = linereader(fp)
894 889 dopatch = True
895 890 gitworkdone = False
896 891
897 892 while True:
898 893 newfile = False
899 894 x = lr.readline()
900 895 if not x:
901 896 break
902 897 if current_hunk:
903 898 if x.startswith('\ '):
904 899 current_hunk.fix_newline()
905 900 yield 'hunk', current_hunk
906 901 current_hunk = None
907 902 gitworkdone = False
908 903 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
909 904 ((context or context == None) and x.startswith('***************')))):
910 905 try:
911 906 if context == None and x.startswith('***************'):
912 907 context = True
913 908 current_hunk = hunk(x, hunknum + 1, lr, context)
914 909 except PatchError, err:
915 910 ui.debug(err)
916 911 current_hunk = None
917 912 continue
918 913 hunknum += 1
919 914 if emitfile:
920 915 emitfile = False
921 916 yield 'file', (afile, bfile, current_hunk)
922 917 elif state == BFILE and x.startswith('GIT binary patch'):
923 918 current_hunk = binhunk(changed[bfile[2:]][1])
924 919 hunknum += 1
925 920 if emitfile:
926 921 emitfile = False
927 922 yield 'file', (afile, bfile, current_hunk)
928 923 current_hunk.extract(fp)
929 924 elif x.startswith('diff --git'):
930 925 # check for git diff, scanning the whole patch file if needed
931 926 m = gitre.match(x)
932 927 if m:
933 928 afile, bfile = m.group(1, 2)
934 929 if not git:
935 930 git = True
936 931 fp, dopatch, gitpatches = scangitpatch(fp, x)
937 932 yield 'git', gitpatches
938 933 for gp in gitpatches:
939 934 changed[gp.path] = (gp.op, gp)
940 935 # else error?
941 936 # copy/rename + modify should modify target, not source
942 937 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
943 938 'RENAME'):
944 939 afile = bfile
945 940 gitworkdone = True
946 941 newfile = True
947 942 elif x.startswith('---'):
948 943 # check for a unified diff
949 944 l2 = lr.readline()
950 945 if not l2.startswith('+++'):
951 946 lr.push(l2)
952 947 continue
953 948 newfile = True
954 949 context = False
955 950 afile = parsefilename(x)
956 951 bfile = parsefilename(l2)
957 952 elif x.startswith('***'):
958 953 # check for a context diff
959 954 l2 = lr.readline()
960 955 if not l2.startswith('---'):
961 956 lr.push(l2)
962 957 continue
963 958 l3 = lr.readline()
964 959 lr.push(l3)
965 960 if not l3.startswith("***************"):
966 961 lr.push(l2)
967 962 continue
968 963 newfile = True
969 964 context = True
970 965 afile = parsefilename(x)
971 966 bfile = parsefilename(l2)
972 967
973 968 if newfile:
974 969 emitfile = True
975 970 state = BFILE
976 971 hunknum = 0
977 972 if current_hunk:
978 973 if current_hunk.complete():
979 974 yield 'hunk', current_hunk
980 975 else:
981 976 raise PatchError(_("malformed patch %s %s") % (afile,
982 977 current_hunk.desc))
983 978
984 979 if hunknum == 0 and dopatch and not gitworkdone:
985 980 raise NoHunks
986 981
987 982 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
988 983 rejmerge=None, updatedir=None):
989 984 """reads a patch from fp and tries to apply it. The dict 'changed' is
990 985 filled in with all of the filenames changed by the patch. Returns 0
991 986 for a clean patch, -1 if any rejects were found and 1 if there was
992 987 any fuzz."""
993 988
994 989 rejects = 0
995 990 err = 0
996 991 current_file = None
997 992 gitpatches = None
998 993
999 994 def closefile():
1000 995 if not current_file:
1001 996 return 0
1002 997 current_file.close()
1003 998 if rejmerge:
1004 999 rejmerge(current_file)
1005 1000 return len(current_file.rej)
1006 1001
1007 1002 for state, values in iterhunks(ui, fp, sourcefile):
1008 1003 if state == 'hunk':
1009 1004 if not current_file:
1010 1005 continue
1011 1006 current_hunk = values
1012 1007 ret = current_file.apply(current_hunk, reverse)
1013 1008 if ret >= 0:
1014 1009 changed.setdefault(current_file.fname, (None, None))
1015 1010 if ret > 0:
1016 1011 err = 1
1017 1012 elif state == 'file':
1018 1013 rejects += closefile()
1019 1014 afile, bfile, first_hunk = values
1020 1015 try:
1021 1016 if sourcefile:
1022 1017 current_file = patchfile(ui, sourcefile)
1023 1018 else:
1024 1019 current_file, missing = selectfile(afile, bfile, first_hunk,
1025 1020 strip, reverse)
1026 1021 current_file = patchfile(ui, current_file, missing)
1027 1022 except PatchError, err:
1028 1023 ui.warn(str(err) + '\n')
1029 1024 current_file, current_hunk = None, None
1030 1025 rejects += 1
1031 1026 continue
1032 1027 elif state == 'git':
1033 1028 gitpatches = values
1034 1029 for gp in gitpatches:
1035 1030 if gp.op in ('COPY', 'RENAME'):
1036 1031 copyfile(gp.oldpath, gp.path)
1037 1032 changed[gp.path] = (gp.op, gp)
1038 1033 else:
1039 1034 raise util.Abort(_('unsupported parser state: %s') % state)
1040 1035
1041 1036 rejects += closefile()
1042 1037
1043 1038 if updatedir and gitpatches:
1044 1039 updatedir(gitpatches)
1045 1040 if rejects:
1046 1041 return -1
1047 1042 return err
1048 1043
1049 1044 def diffopts(ui, opts={}, untrusted=False):
1050 1045 def get(key, name=None):
1051 1046 return (opts.get(key) or
1052 1047 ui.configbool('diff', name or key, None, untrusted=untrusted))
1053 1048 return mdiff.diffopts(
1054 1049 text=opts.get('text'),
1055 1050 git=get('git'),
1056 1051 nodates=get('nodates'),
1057 1052 showfunc=get('show_function', 'showfunc'),
1058 1053 ignorews=get('ignore_all_space', 'ignorews'),
1059 1054 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1060 1055 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
1061 1056
1062 1057 def updatedir(ui, repo, patches):
1063 1058 '''Update dirstate after patch application according to metadata'''
1064 1059 if not patches:
1065 1060 return
1066 1061 copies = []
1067 1062 removes = {}
1068 1063 cfiles = patches.keys()
1069 1064 cwd = repo.getcwd()
1070 1065 if cwd:
1071 1066 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1072 1067 for f in patches:
1073 1068 ctype, gp = patches[f]
1074 1069 if ctype == 'RENAME':
1075 1070 copies.append((gp.oldpath, gp.path))
1076 1071 removes[gp.oldpath] = 1
1077 1072 elif ctype == 'COPY':
1078 1073 copies.append((gp.oldpath, gp.path))
1079 1074 elif ctype == 'DELETE':
1080 1075 removes[gp.path] = 1
1081 1076 for src, dst in copies:
1082 1077 repo.copy(src, dst)
1083 1078 removes = removes.keys()
1084 1079 if removes:
1085 1080 removes.sort()
1086 1081 repo.remove(removes, True)
1087 1082 for f in patches:
1088 1083 ctype, gp = patches[f]
1089 1084 if gp and gp.mode:
1090 1085 x = gp.mode & 0100 != 0
1091 1086 l = gp.mode & 020000 != 0
1092 1087 dst = os.path.join(repo.root, gp.path)
1093 1088 # patch won't create empty files
1094 1089 if ctype == 'ADD' and not os.path.exists(dst):
1095 1090 repo.wwrite(gp.path, '', x and 'x' or '')
1096 1091 else:
1097 1092 util.set_link(dst, l)
1098 1093 if not l:
1099 1094 util.set_exec(dst, x)
1100 1095 cmdutil.addremove(repo, cfiles)
1101 1096 files = patches.keys()
1102 1097 files.extend([r for r in removes if r not in files])
1103 1098 files.sort()
1104 1099
1105 1100 return files
1106 1101
1107 1102 def b85diff(to, tn):
1108 1103 '''print base85-encoded binary diff'''
1109 1104 def gitindex(text):
1110 1105 if not text:
1111 1106 return '0' * 40
1112 1107 l = len(text)
1113 1108 s = sha.new('blob %d\0' % l)
1114 1109 s.update(text)
1115 1110 return s.hexdigest()
1116 1111
1117 1112 def fmtline(line):
1118 1113 l = len(line)
1119 1114 if l <= 26:
1120 1115 l = chr(ord('A') + l - 1)
1121 1116 else:
1122 1117 l = chr(l - 26 + ord('a') - 1)
1123 1118 return '%c%s\n' % (l, base85.b85encode(line, True))
1124 1119
1125 1120 def chunk(text, csize=52):
1126 1121 l = len(text)
1127 1122 i = 0
1128 1123 while i < l:
1129 1124 yield text[i:i+csize]
1130 1125 i += csize
1131 1126
1132 1127 tohash = gitindex(to)
1133 1128 tnhash = gitindex(tn)
1134 1129 if tohash == tnhash:
1135 1130 return ""
1136 1131
1137 1132 # TODO: deltas
1138 1133 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1139 1134 (tohash, tnhash, len(tn))]
1140 1135 for l in chunk(zlib.compress(tn)):
1141 1136 ret.append(fmtline(l))
1142 1137 ret.append('\n')
1143 1138 return ''.join(ret)
1144 1139
1145 1140 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1146 1141 fp=None, changes=None, opts=None):
1147 1142 '''print diff of changes to files between two nodes, or node and
1148 1143 working directory.
1149 1144
1150 1145 if node1 is None, use first dirstate parent instead.
1151 1146 if node2 is None, compare node1 with working directory.'''
1152 1147
1153 1148 if opts is None:
1154 1149 opts = mdiff.defaultopts
1155 1150 if fp is None:
1156 1151 fp = repo.ui
1157 1152
1158 1153 if not node1:
1159 1154 node1 = repo.dirstate.parents()[0]
1160 1155
1161 1156 ccache = {}
1162 1157 def getctx(r):
1163 1158 if r not in ccache:
1164 1159 ccache[r] = context.changectx(repo, r)
1165 1160 return ccache[r]
1166 1161
1167 1162 flcache = {}
1168 1163 def getfilectx(f, ctx):
1169 1164 flctx = ctx.filectx(f, filelog=flcache.get(f))
1170 1165 if f not in flcache:
1171 1166 flcache[f] = flctx._filelog
1172 1167 return flctx
1173 1168
1174 1169 # reading the data for node1 early allows it to play nicely
1175 1170 # with repo.status and the revlog cache.
1176 1171 ctx1 = context.changectx(repo, node1)
1177 1172 # force manifest reading
1178 1173 man1 = ctx1.manifest()
1179 1174 date1 = util.datestr(ctx1.date())
1180 1175
1181 1176 if not changes:
1182 1177 changes = repo.status(node1, node2, files, match=match)[:5]
1183 1178 modified, added, removed, deleted, unknown = changes
1184 1179
1185 1180 if not modified and not added and not removed:
1186 1181 return
1187 1182
1188 1183 if node2:
1189 1184 ctx2 = context.changectx(repo, node2)
1190 1185 execf2 = ctx2.manifest().execf
1191 1186 linkf2 = ctx2.manifest().linkf
1192 1187 else:
1193 1188 ctx2 = context.workingctx(repo)
1194 1189 execf2 = util.execfunc(repo.root, None)
1195 1190 linkf2 = util.linkfunc(repo.root, None)
1196 1191 if execf2 is None:
1197 1192 mc = ctx2.parents()[0].manifest().copy()
1198 1193 execf2 = mc.execf
1199 1194 linkf2 = mc.linkf
1200 1195
1201 1196 # returns False if there was no rename between ctx1 and ctx2
1202 1197 # returns None if the file was created between ctx1 and ctx2
1203 1198 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1204 1199 # This will only really work if c1 is the Nth 1st parent of c2.
1205 1200 def renamed(c1, c2, man, f):
1206 1201 startrev = c1.rev()
1207 1202 c = c2
1208 1203 crev = c.rev()
1209 1204 if crev is None:
1210 1205 crev = repo.changelog.count()
1211 1206 orig = f
1212 1207 files = (f,)
1213 1208 while crev > startrev:
1214 1209 if f in files:
1215 1210 try:
1216 1211 src = getfilectx(f, c).renamed()
1217 1212 except revlog.LookupError:
1218 1213 return None
1219 1214 if src:
1220 1215 f = src[0]
1221 1216 crev = c.parents()[0].rev()
1222 1217 # try to reuse
1223 1218 c = getctx(crev)
1224 1219 files = c.files()
1225 1220 if f not in man:
1226 1221 return None
1227 1222 if f == orig:
1228 1223 return False
1229 1224 return f
1230 1225
1231 1226 if repo.ui.quiet:
1232 1227 r = None
1233 1228 else:
1234 1229 hexfunc = repo.ui.debugflag and hex or short
1235 1230 r = [hexfunc(node) for node in [node1, node2] if node]
1236 1231
1237 1232 if opts.git:
1238 1233 copied = {}
1239 1234 c1, c2 = ctx1, ctx2
1240 1235 files = added
1241 1236 man = man1
1242 1237 if node2 and ctx1.rev() >= ctx2.rev():
1243 1238 # renamed() starts at c2 and walks back in history until c1.
1244 1239 # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to
1245 1240 # detect (inverted) copies.
1246 1241 c1, c2 = ctx2, ctx1
1247 1242 files = removed
1248 1243 man = ctx2.manifest()
1249 1244 for f in files:
1250 1245 src = renamed(c1, c2, man, f)
1251 1246 if src:
1252 1247 copied[f] = src
1253 1248 if ctx1 == c2:
1254 1249 # invert the copied dict
1255 1250 copied = dict([(v, k) for (k, v) in copied.iteritems()])
1256 1251 # If we've renamed file foo to bar (copied['bar'] = 'foo'),
1257 1252 # avoid showing a diff for foo if we're going to show
1258 1253 # the rename to bar.
1259 1254 srcs = [x[1] for x in copied.iteritems() if x[0] in added]
1260 1255
1261 1256 all = modified + added + removed
1262 1257 all.sort()
1263 1258 gone = {}
1264 1259
1265 1260 for f in all:
1266 1261 to = None
1267 1262 tn = None
1268 1263 dodiff = True
1269 1264 header = []
1270 1265 if f in man1:
1271 1266 to = getfilectx(f, ctx1).data()
1272 1267 if f not in removed:
1273 1268 tn = getfilectx(f, ctx2).data()
1274 1269 a, b = f, f
1275 1270 if opts.git:
1276 1271 def gitmode(x, l):
1277 1272 return l and '120000' or (x and '100755' or '100644')
1278 1273 def addmodehdr(header, omode, nmode):
1279 1274 if omode != nmode:
1280 1275 header.append('old mode %s\n' % omode)
1281 1276 header.append('new mode %s\n' % nmode)
1282 1277
1283 1278 if f in added:
1284 1279 mode = gitmode(execf2(f), linkf2(f))
1285 1280 if f in copied:
1286 1281 a = copied[f]
1287 1282 omode = gitmode(man1.execf(a), man1.linkf(a))
1288 1283 addmodehdr(header, omode, mode)
1289 1284 if a in removed and a not in gone:
1290 1285 op = 'rename'
1291 1286 gone[a] = 1
1292 1287 else:
1293 1288 op = 'copy'
1294 1289 header.append('%s from %s\n' % (op, a))
1295 1290 header.append('%s to %s\n' % (op, f))
1296 1291 to = getfilectx(a, ctx1).data()
1297 1292 else:
1298 1293 header.append('new file mode %s\n' % mode)
1299 1294 if util.binary(tn):
1300 1295 dodiff = 'binary'
1301 1296 elif f in removed:
1302 1297 if f in srcs:
1303 1298 dodiff = False
1304 1299 else:
1305 1300 mode = gitmode(man1.execf(f), man1.linkf(f))
1306 1301 header.append('deleted file mode %s\n' % mode)
1307 1302 else:
1308 1303 omode = gitmode(man1.execf(f), man1.linkf(f))
1309 1304 nmode = gitmode(execf2(f), linkf2(f))
1310 1305 addmodehdr(header, omode, nmode)
1311 1306 if util.binary(to) or util.binary(tn):
1312 1307 dodiff = 'binary'
1313 1308 r = None
1314 1309 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1315 1310 if dodiff:
1316 1311 if dodiff == 'binary':
1317 1312 text = b85diff(to, tn)
1318 1313 else:
1319 1314 text = mdiff.unidiff(to, date1,
1320 1315 # ctx2 date may be dynamic
1321 1316 tn, util.datestr(ctx2.date()),
1322 1317 a, b, r, opts=opts)
1323 1318 if text or len(header) > 1:
1324 1319 fp.write(''.join(header))
1325 1320 fp.write(text)
1326 1321
1327 1322 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1328 1323 opts=None):
1329 1324 '''export changesets as hg patches.'''
1330 1325
1331 1326 total = len(revs)
1332 1327 revwidth = max([len(str(rev)) for rev in revs])
1333 1328
1334 1329 def single(rev, seqno, fp):
1335 1330 ctx = repo.changectx(rev)
1336 1331 node = ctx.node()
1337 1332 parents = [p.node() for p in ctx.parents() if p]
1338 1333 branch = ctx.branch()
1339 1334 if switch_parent:
1340 1335 parents.reverse()
1341 1336 prev = (parents and parents[0]) or nullid
1342 1337
1343 1338 if not fp:
1344 1339 fp = cmdutil.make_file(repo, template, node, total=total,
1345 1340 seqno=seqno, revwidth=revwidth)
1346 1341 if fp != sys.stdout and hasattr(fp, 'name'):
1347 1342 repo.ui.note("%s\n" % fp.name)
1348 1343
1349 1344 fp.write("# HG changeset patch\n")
1350 1345 fp.write("# User %s\n" % ctx.user())
1351 1346 fp.write("# Date %d %d\n" % ctx.date())
1352 1347 if branch and (branch != 'default'):
1353 1348 fp.write("# Branch %s\n" % branch)
1354 1349 fp.write("# Node ID %s\n" % hex(node))
1355 1350 fp.write("# Parent %s\n" % hex(prev))
1356 1351 if len(parents) > 1:
1357 1352 fp.write("# Parent %s\n" % hex(parents[1]))
1358 1353 fp.write(ctx.description().rstrip())
1359 1354 fp.write("\n\n")
1360 1355
1361 1356 diff(repo, prev, node, fp=fp, opts=opts)
1362 1357 if fp not in (sys.stdout, repo.ui):
1363 1358 fp.close()
1364 1359
1365 1360 for seqno, rev in enumerate(revs):
1366 1361 single(rev, seqno+1, fp)
1367 1362
1368 1363 def diffstat(patchlines):
1369 1364 if not util.find_exe('diffstat'):
1370 1365 return
1371 1366 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1372 1367 try:
1373 1368 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1374 1369 try:
1375 1370 for line in patchlines: print >> p.tochild, line
1376 1371 p.tochild.close()
1377 1372 if p.wait(): return
1378 1373 fp = os.fdopen(fd, 'r')
1379 1374 stat = []
1380 1375 for line in fp: stat.append(line.lstrip())
1381 1376 last = stat.pop()
1382 1377 stat.insert(0, last)
1383 1378 stat = ''.join(stat)
1384 1379 if stat.startswith('0 files'): raise ValueError
1385 1380 return stat
1386 1381 except: raise
1387 1382 finally:
1388 1383 try: os.unlink(name)
1389 1384 except: pass
General Comments 0
You need to be logged in to leave comments. Login now