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