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