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