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