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