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