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