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