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