##// END OF EJS Templates
Merge with stable
Brendan Cully -
r10884:4fb1bafd merge default
parent child Browse files
Show More
@@ -1,1694 +1,1697 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 or any later version.
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
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 split(stream):
45 45 '''return an iterator of individual patches from a stream'''
46 46 def isheader(line, inheader):
47 47 if inheader and line[0] in (' ', '\t'):
48 48 # continuation
49 49 return True
50 if line[0] in (' ', '-', '+'):
51 # diff line - don't check for header pattern in there
52 return False
50 53 l = line.split(': ', 1)
51 54 return len(l) == 2 and ' ' not in l[0]
52 55
53 56 def chunk(lines):
54 57 return cStringIO.StringIO(''.join(lines))
55 58
56 59 def hgsplit(stream, cur):
57 60 inheader = True
58 61
59 62 for line in stream:
60 63 if not line.strip():
61 64 inheader = False
62 65 if not inheader and line.startswith('# HG changeset patch'):
63 66 yield chunk(cur)
64 67 cur = []
65 68 inheader = True
66 69
67 70 cur.append(line)
68 71
69 72 if cur:
70 73 yield chunk(cur)
71 74
72 75 def mboxsplit(stream, cur):
73 76 for line in stream:
74 77 if line.startswith('From '):
75 78 for c in split(chunk(cur[1:])):
76 79 yield c
77 80 cur = []
78 81
79 82 cur.append(line)
80 83
81 84 if cur:
82 85 for c in split(chunk(cur[1:])):
83 86 yield c
84 87
85 88 def mimesplit(stream, cur):
86 89 def msgfp(m):
87 90 fp = cStringIO.StringIO()
88 91 g = email.Generator.Generator(fp, mangle_from_=False)
89 92 g.flatten(m)
90 93 fp.seek(0)
91 94 return fp
92 95
93 96 for line in stream:
94 97 cur.append(line)
95 98 c = chunk(cur)
96 99
97 100 m = email.Parser.Parser().parse(c)
98 101 if not m.is_multipart():
99 102 yield msgfp(m)
100 103 else:
101 104 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
102 105 for part in m.walk():
103 106 ct = part.get_content_type()
104 107 if ct not in ok_types:
105 108 continue
106 109 yield msgfp(part)
107 110
108 111 def headersplit(stream, cur):
109 112 inheader = False
110 113
111 114 for line in stream:
112 115 if not inheader and isheader(line, inheader):
113 116 yield chunk(cur)
114 117 cur = []
115 118 inheader = True
116 119 if inheader and not isheader(line, inheader):
117 120 inheader = False
118 121
119 122 cur.append(line)
120 123
121 124 if cur:
122 125 yield chunk(cur)
123 126
124 127 def remainder(cur):
125 128 yield chunk(cur)
126 129
127 130 class fiter(object):
128 131 def __init__(self, fp):
129 132 self.fp = fp
130 133
131 134 def __iter__(self):
132 135 return self
133 136
134 137 def next(self):
135 138 l = self.fp.readline()
136 139 if not l:
137 140 raise StopIteration
138 141 return l
139 142
140 143 inheader = False
141 144 cur = []
142 145
143 146 mimeheaders = ['content-type']
144 147
145 148 if not hasattr(stream, 'next'):
146 149 # http responses, for example, have readline but not next
147 150 stream = fiter(stream)
148 151
149 152 for line in stream:
150 153 cur.append(line)
151 154 if line.startswith('# HG changeset patch'):
152 155 return hgsplit(stream, cur)
153 156 elif line.startswith('From '):
154 157 return mboxsplit(stream, cur)
155 158 elif isheader(line, inheader):
156 159 inheader = True
157 160 if line.split(':', 1)[0].lower() in mimeheaders:
158 161 # let email parser handle this
159 162 return mimesplit(stream, cur)
160 163 elif line.startswith('--- ') and inheader:
161 164 # No evil headers seen by diff start, split by hand
162 165 return headersplit(stream, cur)
163 166 # Not enough info, keep reading
164 167
165 168 # if we are here, we have a very plain patch
166 169 return remainder(cur)
167 170
168 171 def extract(ui, fileobj):
169 172 '''extract patch from data read from fileobj.
170 173
171 174 patch can be a normal patch or contained in an email message.
172 175
173 176 return tuple (filename, message, user, date, node, p1, p2).
174 177 Any item in the returned tuple can be None. If filename is None,
175 178 fileobj did not contain a patch. Caller must unlink filename when done.'''
176 179
177 180 # attempt to detect the start of a patch
178 181 # (this heuristic is borrowed from quilt)
179 182 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
180 183 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
181 184 r'---[ \t].*?^\+\+\+[ \t]|'
182 185 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
183 186
184 187 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
185 188 tmpfp = os.fdopen(fd, 'w')
186 189 try:
187 190 msg = email.Parser.Parser().parse(fileobj)
188 191
189 192 subject = msg['Subject']
190 193 user = msg['From']
191 194 if not subject and not user:
192 195 # Not an email, restore parsed headers if any
193 196 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
194 197
195 198 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
196 199 # should try to parse msg['Date']
197 200 date = None
198 201 nodeid = None
199 202 branch = None
200 203 parents = []
201 204
202 205 if subject:
203 206 if subject.startswith('[PATCH'):
204 207 pend = subject.find(']')
205 208 if pend >= 0:
206 209 subject = subject[pend + 1:].lstrip()
207 210 subject = subject.replace('\n\t', ' ')
208 211 ui.debug('Subject: %s\n' % subject)
209 212 if user:
210 213 ui.debug('From: %s\n' % user)
211 214 diffs_seen = 0
212 215 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
213 216 message = ''
214 217 for part in msg.walk():
215 218 content_type = part.get_content_type()
216 219 ui.debug('Content-Type: %s\n' % content_type)
217 220 if content_type not in ok_types:
218 221 continue
219 222 payload = part.get_payload(decode=True)
220 223 m = diffre.search(payload)
221 224 if m:
222 225 hgpatch = False
223 226 ignoretext = False
224 227
225 228 ui.debug('found patch at byte %d\n' % m.start(0))
226 229 diffs_seen += 1
227 230 cfp = cStringIO.StringIO()
228 231 for line in payload[:m.start(0)].splitlines():
229 232 if line.startswith('# HG changeset patch'):
230 233 ui.debug('patch generated by hg export\n')
231 234 hgpatch = True
232 235 # drop earlier commit message content
233 236 cfp.seek(0)
234 237 cfp.truncate()
235 238 subject = None
236 239 elif hgpatch:
237 240 if line.startswith('# User '):
238 241 user = line[7:]
239 242 ui.debug('From: %s\n' % user)
240 243 elif line.startswith("# Date "):
241 244 date = line[7:]
242 245 elif line.startswith("# Branch "):
243 246 branch = line[9:]
244 247 elif line.startswith("# Node ID "):
245 248 nodeid = line[10:]
246 249 elif line.startswith("# Parent "):
247 250 parents.append(line[10:])
248 251 elif line == '---' and gitsendmail:
249 252 ignoretext = True
250 253 if not line.startswith('# ') and not ignoretext:
251 254 cfp.write(line)
252 255 cfp.write('\n')
253 256 message = cfp.getvalue()
254 257 if tmpfp:
255 258 tmpfp.write(payload)
256 259 if not payload.endswith('\n'):
257 260 tmpfp.write('\n')
258 261 elif not diffs_seen and message and content_type == 'text/plain':
259 262 message += '\n' + payload
260 263 except:
261 264 tmpfp.close()
262 265 os.unlink(tmpname)
263 266 raise
264 267
265 268 if subject and not message.startswith(subject):
266 269 message = '%s\n%s' % (subject, message)
267 270 tmpfp.close()
268 271 if not diffs_seen:
269 272 os.unlink(tmpname)
270 273 return None, message, user, date, branch, None, None, None
271 274 p1 = parents and parents.pop(0) or None
272 275 p2 = parents and parents.pop(0) or None
273 276 return tmpname, message, user, date, branch, nodeid, p1, p2
274 277
275 278 GP_PATCH = 1 << 0 # we have to run patch
276 279 GP_FILTER = 1 << 1 # there's some copy/rename operation
277 280 GP_BINARY = 1 << 2 # there's a binary patch
278 281
279 282 class patchmeta(object):
280 283 """Patched file metadata
281 284
282 285 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
283 286 or COPY. 'path' is patched file path. 'oldpath' is set to the
284 287 origin file when 'op' is either COPY or RENAME, None otherwise. If
285 288 file mode is changed, 'mode' is a tuple (islink, isexec) where
286 289 'islink' is True if the file is a symlink and 'isexec' is True if
287 290 the file is executable. Otherwise, 'mode' is None.
288 291 """
289 292 def __init__(self, path):
290 293 self.path = path
291 294 self.oldpath = None
292 295 self.mode = None
293 296 self.op = 'MODIFY'
294 297 self.lineno = 0
295 298 self.binary = False
296 299
297 300 def setmode(self, mode):
298 301 islink = mode & 020000
299 302 isexec = mode & 0100
300 303 self.mode = (islink, isexec)
301 304
302 305 def readgitpatch(lr):
303 306 """extract git-style metadata about patches from <patchname>"""
304 307
305 308 # Filter patch for git information
306 309 gp = None
307 310 gitpatches = []
308 311 # Can have a git patch with only metadata, causing patch to complain
309 312 dopatch = 0
310 313
311 314 lineno = 0
312 315 for line in lr:
313 316 lineno += 1
314 317 line = line.rstrip(' \r\n')
315 318 if line.startswith('diff --git'):
316 319 m = gitre.match(line)
317 320 if m:
318 321 if gp:
319 322 gitpatches.append(gp)
320 323 dst = m.group(2)
321 324 gp = patchmeta(dst)
322 325 gp.lineno = lineno
323 326 elif gp:
324 327 if line.startswith('--- '):
325 328 if gp.op in ('COPY', 'RENAME'):
326 329 dopatch |= GP_FILTER
327 330 gitpatches.append(gp)
328 331 gp = None
329 332 dopatch |= GP_PATCH
330 333 continue
331 334 if line.startswith('rename from '):
332 335 gp.op = 'RENAME'
333 336 gp.oldpath = line[12:]
334 337 elif line.startswith('rename to '):
335 338 gp.path = line[10:]
336 339 elif line.startswith('copy from '):
337 340 gp.op = 'COPY'
338 341 gp.oldpath = line[10:]
339 342 elif line.startswith('copy to '):
340 343 gp.path = line[8:]
341 344 elif line.startswith('deleted file'):
342 345 gp.op = 'DELETE'
343 346 # is the deleted file a symlink?
344 347 gp.setmode(int(line[-6:], 8))
345 348 elif line.startswith('new file mode '):
346 349 gp.op = 'ADD'
347 350 gp.setmode(int(line[-6:], 8))
348 351 elif line.startswith('new mode '):
349 352 gp.setmode(int(line[-6:], 8))
350 353 elif line.startswith('GIT binary patch'):
351 354 dopatch |= GP_BINARY
352 355 gp.binary = True
353 356 if gp:
354 357 gitpatches.append(gp)
355 358
356 359 if not gitpatches:
357 360 dopatch = GP_PATCH
358 361
359 362 return (dopatch, gitpatches)
360 363
361 364 class linereader(object):
362 365 # simple class to allow pushing lines back into the input stream
363 366 def __init__(self, fp, textmode=False):
364 367 self.fp = fp
365 368 self.buf = []
366 369 self.textmode = textmode
367 370 self.eol = None
368 371
369 372 def push(self, line):
370 373 if line is not None:
371 374 self.buf.append(line)
372 375
373 376 def readline(self):
374 377 if self.buf:
375 378 l = self.buf[0]
376 379 del self.buf[0]
377 380 return l
378 381 l = self.fp.readline()
379 382 if not self.eol:
380 383 if l.endswith('\r\n'):
381 384 self.eol = '\r\n'
382 385 elif l.endswith('\n'):
383 386 self.eol = '\n'
384 387 if self.textmode and l.endswith('\r\n'):
385 388 l = l[:-2] + '\n'
386 389 return l
387 390
388 391 def __iter__(self):
389 392 while 1:
390 393 l = self.readline()
391 394 if not l:
392 395 break
393 396 yield l
394 397
395 398 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
396 399 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
397 400 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
398 401 eolmodes = ['strict', 'crlf', 'lf', 'auto']
399 402
400 403 class patchfile(object):
401 404 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
402 405 self.fname = fname
403 406 self.eolmode = eolmode
404 407 self.eol = None
405 408 self.opener = opener
406 409 self.ui = ui
407 410 self.lines = []
408 411 self.exists = False
409 412 self.missing = missing
410 413 if not missing:
411 414 try:
412 415 self.lines = self.readlines(fname)
413 416 self.exists = True
414 417 except IOError:
415 418 pass
416 419 else:
417 420 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
418 421
419 422 self.hash = {}
420 423 self.dirty = 0
421 424 self.offset = 0
422 425 self.skew = 0
423 426 self.rej = []
424 427 self.fileprinted = False
425 428 self.printfile(False)
426 429 self.hunks = 0
427 430
428 431 def readlines(self, fname):
429 432 if os.path.islink(fname):
430 433 return [os.readlink(fname)]
431 434 fp = self.opener(fname, 'r')
432 435 try:
433 436 lr = linereader(fp, self.eolmode != 'strict')
434 437 lines = list(lr)
435 438 self.eol = lr.eol
436 439 return lines
437 440 finally:
438 441 fp.close()
439 442
440 443 def writelines(self, fname, lines):
441 444 # Ensure supplied data ends in fname, being a regular file or
442 445 # a symlink. updatedir() will -too magically- take care of
443 446 # setting it to the proper type afterwards.
444 447 islink = os.path.islink(fname)
445 448 if islink:
446 449 fp = cStringIO.StringIO()
447 450 else:
448 451 fp = self.opener(fname, 'w')
449 452 try:
450 453 if self.eolmode == 'auto':
451 454 eol = self.eol
452 455 elif self.eolmode == 'crlf':
453 456 eol = '\r\n'
454 457 else:
455 458 eol = '\n'
456 459
457 460 if self.eolmode != 'strict' and eol and eol != '\n':
458 461 for l in lines:
459 462 if l and l[-1] == '\n':
460 463 l = l[:-1] + eol
461 464 fp.write(l)
462 465 else:
463 466 fp.writelines(lines)
464 467 if islink:
465 468 self.opener.symlink(fp.getvalue(), fname)
466 469 finally:
467 470 fp.close()
468 471
469 472 def unlink(self, fname):
470 473 os.unlink(fname)
471 474
472 475 def printfile(self, warn):
473 476 if self.fileprinted:
474 477 return
475 478 if warn or self.ui.verbose:
476 479 self.fileprinted = True
477 480 s = _("patching file %s\n") % self.fname
478 481 if warn:
479 482 self.ui.warn(s)
480 483 else:
481 484 self.ui.note(s)
482 485
483 486
484 487 def findlines(self, l, linenum):
485 488 # looks through the hash and finds candidate lines. The
486 489 # result is a list of line numbers sorted based on distance
487 490 # from linenum
488 491
489 492 cand = self.hash.get(l, [])
490 493 if len(cand) > 1:
491 494 # resort our list of potentials forward then back.
492 495 cand.sort(key=lambda x: abs(x - linenum))
493 496 return cand
494 497
495 498 def hashlines(self):
496 499 self.hash = {}
497 500 for x, s in enumerate(self.lines):
498 501 self.hash.setdefault(s, []).append(x)
499 502
500 503 def write_rej(self):
501 504 # our rejects are a little different from patch(1). This always
502 505 # creates rejects in the same form as the original patch. A file
503 506 # header is inserted so that you can run the reject through patch again
504 507 # without having to type the filename.
505 508
506 509 if not self.rej:
507 510 return
508 511
509 512 fname = self.fname + ".rej"
510 513 self.ui.warn(
511 514 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
512 515 (len(self.rej), self.hunks, fname))
513 516
514 517 def rejlines():
515 518 base = os.path.basename(self.fname)
516 519 yield "--- %s\n+++ %s\n" % (base, base)
517 520 for x in self.rej:
518 521 for l in x.hunk:
519 522 yield l
520 523 if l[-1] != '\n':
521 524 yield "\n\ No newline at end of file\n"
522 525
523 526 self.writelines(fname, rejlines())
524 527
525 528 def write(self, dest=None):
526 529 if not self.dirty:
527 530 return
528 531 if not dest:
529 532 dest = self.fname
530 533 self.writelines(dest, self.lines)
531 534
532 535 def close(self):
533 536 self.write()
534 537 self.write_rej()
535 538
536 539 def apply(self, h):
537 540 if not h.complete():
538 541 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
539 542 (h.number, h.desc, len(h.a), h.lena, len(h.b),
540 543 h.lenb))
541 544
542 545 self.hunks += 1
543 546
544 547 if self.missing:
545 548 self.rej.append(h)
546 549 return -1
547 550
548 551 if self.exists and h.createfile():
549 552 self.ui.warn(_("file %s already exists\n") % self.fname)
550 553 self.rej.append(h)
551 554 return -1
552 555
553 556 if isinstance(h, binhunk):
554 557 if h.rmfile():
555 558 self.unlink(self.fname)
556 559 else:
557 560 self.lines[:] = h.new()
558 561 self.offset += len(h.new())
559 562 self.dirty = 1
560 563 return 0
561 564
562 565 horig = h
563 566 if (self.eolmode in ('crlf', 'lf')
564 567 or self.eolmode == 'auto' and self.eol):
565 568 # If new eols are going to be normalized, then normalize
566 569 # hunk data before patching. Otherwise, preserve input
567 570 # line-endings.
568 571 h = h.getnormalized()
569 572
570 573 # fast case first, no offsets, no fuzz
571 574 old = h.old()
572 575 # patch starts counting at 1 unless we are adding the file
573 576 if h.starta == 0:
574 577 start = 0
575 578 else:
576 579 start = h.starta + self.offset - 1
577 580 orig_start = start
578 581 # if there's skew we want to emit the "(offset %d lines)" even
579 582 # when the hunk cleanly applies at start + skew, so skip the
580 583 # fast case code
581 584 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
582 585 if h.rmfile():
583 586 self.unlink(self.fname)
584 587 else:
585 588 self.lines[start : start + h.lena] = h.new()
586 589 self.offset += h.lenb - h.lena
587 590 self.dirty = 1
588 591 return 0
589 592
590 593 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
591 594 self.hashlines()
592 595 if h.hunk[-1][0] != ' ':
593 596 # if the hunk tried to put something at the bottom of the file
594 597 # override the start line and use eof here
595 598 search_start = len(self.lines)
596 599 else:
597 600 search_start = orig_start + self.skew
598 601
599 602 for fuzzlen in xrange(3):
600 603 for toponly in [True, False]:
601 604 old = h.old(fuzzlen, toponly)
602 605
603 606 cand = self.findlines(old[0][1:], search_start)
604 607 for l in cand:
605 608 if diffhelpers.testhunk(old, self.lines, l) == 0:
606 609 newlines = h.new(fuzzlen, toponly)
607 610 self.lines[l : l + len(old)] = newlines
608 611 self.offset += len(newlines) - len(old)
609 612 self.skew = l - orig_start
610 613 self.dirty = 1
611 614 offset = l - orig_start - fuzzlen
612 615 if fuzzlen:
613 616 msg = _("Hunk #%d succeeded at %d "
614 617 "with fuzz %d "
615 618 "(offset %d lines).\n")
616 619 self.printfile(True)
617 620 self.ui.warn(msg %
618 621 (h.number, l + 1, fuzzlen, offset))
619 622 else:
620 623 msg = _("Hunk #%d succeeded at %d "
621 624 "(offset %d lines).\n")
622 625 self.ui.note(msg % (h.number, l + 1, offset))
623 626 return fuzzlen
624 627 self.printfile(True)
625 628 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
626 629 self.rej.append(horig)
627 630 return -1
628 631
629 632 class hunk(object):
630 633 def __init__(self, desc, num, lr, context, create=False, remove=False):
631 634 self.number = num
632 635 self.desc = desc
633 636 self.hunk = [desc]
634 637 self.a = []
635 638 self.b = []
636 639 self.starta = self.lena = None
637 640 self.startb = self.lenb = None
638 641 if lr is not None:
639 642 if context:
640 643 self.read_context_hunk(lr)
641 644 else:
642 645 self.read_unified_hunk(lr)
643 646 self.create = create
644 647 self.remove = remove and not create
645 648
646 649 def getnormalized(self):
647 650 """Return a copy with line endings normalized to LF."""
648 651
649 652 def normalize(lines):
650 653 nlines = []
651 654 for line in lines:
652 655 if line.endswith('\r\n'):
653 656 line = line[:-2] + '\n'
654 657 nlines.append(line)
655 658 return nlines
656 659
657 660 # Dummy object, it is rebuilt manually
658 661 nh = hunk(self.desc, self.number, None, None, False, False)
659 662 nh.number = self.number
660 663 nh.desc = self.desc
661 664 nh.hunk = self.hunk
662 665 nh.a = normalize(self.a)
663 666 nh.b = normalize(self.b)
664 667 nh.starta = self.starta
665 668 nh.startb = self.startb
666 669 nh.lena = self.lena
667 670 nh.lenb = self.lenb
668 671 nh.create = self.create
669 672 nh.remove = self.remove
670 673 return nh
671 674
672 675 def read_unified_hunk(self, lr):
673 676 m = unidesc.match(self.desc)
674 677 if not m:
675 678 raise PatchError(_("bad hunk #%d") % self.number)
676 679 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
677 680 if self.lena is None:
678 681 self.lena = 1
679 682 else:
680 683 self.lena = int(self.lena)
681 684 if self.lenb is None:
682 685 self.lenb = 1
683 686 else:
684 687 self.lenb = int(self.lenb)
685 688 self.starta = int(self.starta)
686 689 self.startb = int(self.startb)
687 690 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
688 691 # if we hit eof before finishing out the hunk, the last line will
689 692 # be zero length. Lets try to fix it up.
690 693 while len(self.hunk[-1]) == 0:
691 694 del self.hunk[-1]
692 695 del self.a[-1]
693 696 del self.b[-1]
694 697 self.lena -= 1
695 698 self.lenb -= 1
696 699
697 700 def read_context_hunk(self, lr):
698 701 self.desc = lr.readline()
699 702 m = contextdesc.match(self.desc)
700 703 if not m:
701 704 raise PatchError(_("bad hunk #%d") % self.number)
702 705 foo, self.starta, foo2, aend, foo3 = m.groups()
703 706 self.starta = int(self.starta)
704 707 if aend is None:
705 708 aend = self.starta
706 709 self.lena = int(aend) - self.starta
707 710 if self.starta:
708 711 self.lena += 1
709 712 for x in xrange(self.lena):
710 713 l = lr.readline()
711 714 if l.startswith('---'):
712 715 lr.push(l)
713 716 break
714 717 s = l[2:]
715 718 if l.startswith('- ') or l.startswith('! '):
716 719 u = '-' + s
717 720 elif l.startswith(' '):
718 721 u = ' ' + s
719 722 else:
720 723 raise PatchError(_("bad hunk #%d old text line %d") %
721 724 (self.number, x))
722 725 self.a.append(u)
723 726 self.hunk.append(u)
724 727
725 728 l = lr.readline()
726 729 if l.startswith('\ '):
727 730 s = self.a[-1][:-1]
728 731 self.a[-1] = s
729 732 self.hunk[-1] = s
730 733 l = lr.readline()
731 734 m = contextdesc.match(l)
732 735 if not m:
733 736 raise PatchError(_("bad hunk #%d") % self.number)
734 737 foo, self.startb, foo2, bend, foo3 = m.groups()
735 738 self.startb = int(self.startb)
736 739 if bend is None:
737 740 bend = self.startb
738 741 self.lenb = int(bend) - self.startb
739 742 if self.startb:
740 743 self.lenb += 1
741 744 hunki = 1
742 745 for x in xrange(self.lenb):
743 746 l = lr.readline()
744 747 if l.startswith('\ '):
745 748 s = self.b[-1][:-1]
746 749 self.b[-1] = s
747 750 self.hunk[hunki - 1] = s
748 751 continue
749 752 if not l:
750 753 lr.push(l)
751 754 break
752 755 s = l[2:]
753 756 if l.startswith('+ ') or l.startswith('! '):
754 757 u = '+' + s
755 758 elif l.startswith(' '):
756 759 u = ' ' + s
757 760 elif len(self.b) == 0:
758 761 # this can happen when the hunk does not add any lines
759 762 lr.push(l)
760 763 break
761 764 else:
762 765 raise PatchError(_("bad hunk #%d old text line %d") %
763 766 (self.number, x))
764 767 self.b.append(s)
765 768 while True:
766 769 if hunki >= len(self.hunk):
767 770 h = ""
768 771 else:
769 772 h = self.hunk[hunki]
770 773 hunki += 1
771 774 if h == u:
772 775 break
773 776 elif h.startswith('-'):
774 777 continue
775 778 else:
776 779 self.hunk.insert(hunki - 1, u)
777 780 break
778 781
779 782 if not self.a:
780 783 # this happens when lines were only added to the hunk
781 784 for x in self.hunk:
782 785 if x.startswith('-') or x.startswith(' '):
783 786 self.a.append(x)
784 787 if not self.b:
785 788 # this happens when lines were only deleted from the hunk
786 789 for x in self.hunk:
787 790 if x.startswith('+') or x.startswith(' '):
788 791 self.b.append(x[1:])
789 792 # @@ -start,len +start,len @@
790 793 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
791 794 self.startb, self.lenb)
792 795 self.hunk[0] = self.desc
793 796
794 797 def fix_newline(self):
795 798 diffhelpers.fix_newline(self.hunk, self.a, self.b)
796 799
797 800 def complete(self):
798 801 return len(self.a) == self.lena and len(self.b) == self.lenb
799 802
800 803 def createfile(self):
801 804 return self.starta == 0 and self.lena == 0 and self.create
802 805
803 806 def rmfile(self):
804 807 return self.startb == 0 and self.lenb == 0 and self.remove
805 808
806 809 def fuzzit(self, l, fuzz, toponly):
807 810 # this removes context lines from the top and bottom of list 'l'. It
808 811 # checks the hunk to make sure only context lines are removed, and then
809 812 # returns a new shortened list of lines.
810 813 fuzz = min(fuzz, len(l)-1)
811 814 if fuzz:
812 815 top = 0
813 816 bot = 0
814 817 hlen = len(self.hunk)
815 818 for x in xrange(hlen - 1):
816 819 # the hunk starts with the @@ line, so use x+1
817 820 if self.hunk[x + 1][0] == ' ':
818 821 top += 1
819 822 else:
820 823 break
821 824 if not toponly:
822 825 for x in xrange(hlen - 1):
823 826 if self.hunk[hlen - bot - 1][0] == ' ':
824 827 bot += 1
825 828 else:
826 829 break
827 830
828 831 # top and bot now count context in the hunk
829 832 # adjust them if either one is short
830 833 context = max(top, bot, 3)
831 834 if bot < context:
832 835 bot = max(0, fuzz - (context - bot))
833 836 else:
834 837 bot = min(fuzz, bot)
835 838 if top < context:
836 839 top = max(0, fuzz - (context - top))
837 840 else:
838 841 top = min(fuzz, top)
839 842
840 843 return l[top:len(l)-bot]
841 844 return l
842 845
843 846 def old(self, fuzz=0, toponly=False):
844 847 return self.fuzzit(self.a, fuzz, toponly)
845 848
846 849 def new(self, fuzz=0, toponly=False):
847 850 return self.fuzzit(self.b, fuzz, toponly)
848 851
849 852 class binhunk:
850 853 'A binary patch file. Only understands literals so far.'
851 854 def __init__(self, gitpatch):
852 855 self.gitpatch = gitpatch
853 856 self.text = None
854 857 self.hunk = ['GIT binary patch\n']
855 858
856 859 def createfile(self):
857 860 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
858 861
859 862 def rmfile(self):
860 863 return self.gitpatch.op == 'DELETE'
861 864
862 865 def complete(self):
863 866 return self.text is not None
864 867
865 868 def new(self):
866 869 return [self.text]
867 870
868 871 def extract(self, lr):
869 872 line = lr.readline()
870 873 self.hunk.append(line)
871 874 while line and not line.startswith('literal '):
872 875 line = lr.readline()
873 876 self.hunk.append(line)
874 877 if not line:
875 878 raise PatchError(_('could not extract binary patch'))
876 879 size = int(line[8:].rstrip())
877 880 dec = []
878 881 line = lr.readline()
879 882 self.hunk.append(line)
880 883 while len(line) > 1:
881 884 l = line[0]
882 885 if l <= 'Z' and l >= 'A':
883 886 l = ord(l) - ord('A') + 1
884 887 else:
885 888 l = ord(l) - ord('a') + 27
886 889 dec.append(base85.b85decode(line[1:-1])[:l])
887 890 line = lr.readline()
888 891 self.hunk.append(line)
889 892 text = zlib.decompress(''.join(dec))
890 893 if len(text) != size:
891 894 raise PatchError(_('binary patch is %d bytes, not %d') %
892 895 len(text), size)
893 896 self.text = text
894 897
895 898 def parsefilename(str):
896 899 # --- filename \t|space stuff
897 900 s = str[4:].rstrip('\r\n')
898 901 i = s.find('\t')
899 902 if i < 0:
900 903 i = s.find(' ')
901 904 if i < 0:
902 905 return s
903 906 return s[:i]
904 907
905 908 def selectfile(afile_orig, bfile_orig, hunk, strip):
906 909 def pathstrip(path, count=1):
907 910 pathlen = len(path)
908 911 i = 0
909 912 if count == 0:
910 913 return '', path.rstrip()
911 914 while count > 0:
912 915 i = path.find('/', i)
913 916 if i == -1:
914 917 raise PatchError(_("unable to strip away %d dirs from %s") %
915 918 (count, path))
916 919 i += 1
917 920 # consume '//' in the path
918 921 while i < pathlen - 1 and path[i] == '/':
919 922 i += 1
920 923 count -= 1
921 924 return path[:i].lstrip(), path[i:].rstrip()
922 925
923 926 nulla = afile_orig == "/dev/null"
924 927 nullb = bfile_orig == "/dev/null"
925 928 abase, afile = pathstrip(afile_orig, strip)
926 929 gooda = not nulla and util.lexists(afile)
927 930 bbase, bfile = pathstrip(bfile_orig, strip)
928 931 if afile == bfile:
929 932 goodb = gooda
930 933 else:
931 934 goodb = not nullb and os.path.exists(bfile)
932 935 createfunc = hunk.createfile
933 936 missing = not goodb and not gooda and not createfunc()
934 937
935 938 # some diff programs apparently produce create patches where the
936 939 # afile is not /dev/null, but afile starts with bfile
937 940 abasedir = afile[:afile.rfind('/') + 1]
938 941 bbasedir = bfile[:bfile.rfind('/') + 1]
939 942 if missing and abasedir == bbasedir and afile.startswith(bfile):
940 943 # this isn't very pretty
941 944 hunk.create = True
942 945 if createfunc():
943 946 missing = False
944 947 else:
945 948 hunk.create = False
946 949
947 950 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
948 951 # diff is between a file and its backup. In this case, the original
949 952 # file should be patched (see original mpatch code).
950 953 isbackup = (abase == bbase and bfile.startswith(afile))
951 954 fname = None
952 955 if not missing:
953 956 if gooda and goodb:
954 957 fname = isbackup and afile or bfile
955 958 elif gooda:
956 959 fname = afile
957 960
958 961 if not fname:
959 962 if not nullb:
960 963 fname = isbackup and afile or bfile
961 964 elif not nulla:
962 965 fname = afile
963 966 else:
964 967 raise PatchError(_("undefined source and destination files"))
965 968
966 969 return fname, missing
967 970
968 971 def scangitpatch(lr, firstline):
969 972 """
970 973 Git patches can emit:
971 974 - rename a to b
972 975 - change b
973 976 - copy a to c
974 977 - change c
975 978
976 979 We cannot apply this sequence as-is, the renamed 'a' could not be
977 980 found for it would have been renamed already. And we cannot copy
978 981 from 'b' instead because 'b' would have been changed already. So
979 982 we scan the git patch for copy and rename commands so we can
980 983 perform the copies ahead of time.
981 984 """
982 985 pos = 0
983 986 try:
984 987 pos = lr.fp.tell()
985 988 fp = lr.fp
986 989 except IOError:
987 990 fp = cStringIO.StringIO(lr.fp.read())
988 991 gitlr = linereader(fp, lr.textmode)
989 992 gitlr.push(firstline)
990 993 (dopatch, gitpatches) = readgitpatch(gitlr)
991 994 fp.seek(pos)
992 995 return dopatch, gitpatches
993 996
994 997 def iterhunks(ui, fp, sourcefile=None):
995 998 """Read a patch and yield the following events:
996 999 - ("file", afile, bfile, firsthunk): select a new target file.
997 1000 - ("hunk", hunk): a new hunk is ready to be applied, follows a
998 1001 "file" event.
999 1002 - ("git", gitchanges): current diff is in git format, gitchanges
1000 1003 maps filenames to gitpatch records. Unique event.
1001 1004 """
1002 1005 changed = {}
1003 1006 current_hunk = None
1004 1007 afile = ""
1005 1008 bfile = ""
1006 1009 state = None
1007 1010 hunknum = 0
1008 1011 emitfile = False
1009 1012 git = False
1010 1013
1011 1014 # our states
1012 1015 BFILE = 1
1013 1016 context = None
1014 1017 lr = linereader(fp)
1015 1018 # gitworkdone is True if a git operation (copy, rename, ...) was
1016 1019 # performed already for the current file. Useful when the file
1017 1020 # section may have no hunk.
1018 1021 gitworkdone = False
1019 1022 empty = None
1020 1023
1021 1024 while True:
1022 1025 newfile = newgitfile = False
1023 1026 x = lr.readline()
1024 1027 if not x:
1025 1028 break
1026 1029 if current_hunk:
1027 1030 if x.startswith('\ '):
1028 1031 current_hunk.fix_newline()
1029 1032 yield 'hunk', current_hunk
1030 1033 current_hunk = None
1031 1034 empty = False
1032 1035 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1033 1036 ((context is not False) and x.startswith('***************')))):
1034 1037 try:
1035 1038 if context is None and x.startswith('***************'):
1036 1039 context = True
1037 1040 gpatch = changed.get(bfile)
1038 1041 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1039 1042 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1040 1043 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1041 1044 except PatchError, err:
1042 1045 ui.debug(err)
1043 1046 current_hunk = None
1044 1047 continue
1045 1048 hunknum += 1
1046 1049 if emitfile:
1047 1050 emitfile = False
1048 1051 yield 'file', (afile, bfile, current_hunk)
1049 1052 empty = False
1050 1053 elif state == BFILE and x.startswith('GIT binary patch'):
1051 1054 current_hunk = binhunk(changed[bfile])
1052 1055 hunknum += 1
1053 1056 if emitfile:
1054 1057 emitfile = False
1055 1058 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1056 1059 empty = False
1057 1060 current_hunk.extract(lr)
1058 1061 elif x.startswith('diff --git'):
1059 1062 # check for git diff, scanning the whole patch file if needed
1060 1063 m = gitre.match(x)
1061 1064 gitworkdone = False
1062 1065 if m:
1063 1066 afile, bfile = m.group(1, 2)
1064 1067 if not git:
1065 1068 git = True
1066 1069 gitpatches = scangitpatch(lr, x)[1]
1067 1070 yield 'git', gitpatches
1068 1071 for gp in gitpatches:
1069 1072 changed[gp.path] = gp
1070 1073 # else error?
1071 1074 # copy/rename + modify should modify target, not source
1072 1075 gp = changed.get(bfile)
1073 1076 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1074 1077 or gp.mode):
1075 1078 afile = bfile
1076 1079 gitworkdone = True
1077 1080 newgitfile = True
1078 1081 elif x.startswith('---'):
1079 1082 # check for a unified diff
1080 1083 l2 = lr.readline()
1081 1084 if not l2.startswith('+++'):
1082 1085 lr.push(l2)
1083 1086 continue
1084 1087 newfile = True
1085 1088 context = False
1086 1089 afile = parsefilename(x)
1087 1090 bfile = parsefilename(l2)
1088 1091 elif x.startswith('***'):
1089 1092 # check for a context diff
1090 1093 l2 = lr.readline()
1091 1094 if not l2.startswith('---'):
1092 1095 lr.push(l2)
1093 1096 continue
1094 1097 l3 = lr.readline()
1095 1098 lr.push(l3)
1096 1099 if not l3.startswith("***************"):
1097 1100 lr.push(l2)
1098 1101 continue
1099 1102 newfile = True
1100 1103 context = True
1101 1104 afile = parsefilename(x)
1102 1105 bfile = parsefilename(l2)
1103 1106
1104 1107 if newfile:
1105 1108 if empty:
1106 1109 raise NoHunks
1107 1110 empty = not gitworkdone
1108 1111 gitworkdone = False
1109 1112
1110 1113 if newgitfile or newfile:
1111 1114 emitfile = True
1112 1115 state = BFILE
1113 1116 hunknum = 0
1114 1117 if current_hunk:
1115 1118 if current_hunk.complete():
1116 1119 yield 'hunk', current_hunk
1117 1120 empty = False
1118 1121 else:
1119 1122 raise PatchError(_("malformed patch %s %s") % (afile,
1120 1123 current_hunk.desc))
1121 1124
1122 1125 if (empty is None and not gitworkdone) or empty:
1123 1126 raise NoHunks
1124 1127
1125 1128 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1126 1129 """
1127 1130 Reads a patch from fp and tries to apply it.
1128 1131
1129 1132 The dict 'changed' is filled in with all of the filenames changed
1130 1133 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1131 1134 found and 1 if there was any fuzz.
1132 1135
1133 1136 If 'eolmode' is 'strict', the patch content and patched file are
1134 1137 read in binary mode. Otherwise, line endings are ignored when
1135 1138 patching then normalized according to 'eolmode'.
1136 1139 """
1137 1140 rejects = 0
1138 1141 err = 0
1139 1142 current_file = None
1140 1143 gitpatches = None
1141 1144 opener = util.opener(os.getcwd())
1142 1145
1143 1146 def closefile():
1144 1147 if not current_file:
1145 1148 return 0
1146 1149 current_file.close()
1147 1150 return len(current_file.rej)
1148 1151
1149 1152 for state, values in iterhunks(ui, fp, sourcefile):
1150 1153 if state == 'hunk':
1151 1154 if not current_file:
1152 1155 continue
1153 1156 current_hunk = values
1154 1157 ret = current_file.apply(current_hunk)
1155 1158 if ret >= 0:
1156 1159 changed.setdefault(current_file.fname, None)
1157 1160 if ret > 0:
1158 1161 err = 1
1159 1162 elif state == 'file':
1160 1163 rejects += closefile()
1161 1164 afile, bfile, first_hunk = values
1162 1165 try:
1163 1166 if sourcefile:
1164 1167 current_file = patchfile(ui, sourcefile, opener,
1165 1168 eolmode=eolmode)
1166 1169 else:
1167 1170 current_file, missing = selectfile(afile, bfile,
1168 1171 first_hunk, strip)
1169 1172 current_file = patchfile(ui, current_file, opener,
1170 1173 missing, eolmode)
1171 1174 except PatchError, err:
1172 1175 ui.warn(str(err) + '\n')
1173 1176 current_file, current_hunk = None, None
1174 1177 rejects += 1
1175 1178 continue
1176 1179 elif state == 'git':
1177 1180 gitpatches = values
1178 1181 cwd = os.getcwd()
1179 1182 for gp in gitpatches:
1180 1183 if gp.op in ('COPY', 'RENAME'):
1181 1184 copyfile(gp.oldpath, gp.path, cwd)
1182 1185 changed[gp.path] = gp
1183 1186 else:
1184 1187 raise util.Abort(_('unsupported parser state: %s') % state)
1185 1188
1186 1189 rejects += closefile()
1187 1190
1188 1191 if rejects:
1189 1192 return -1
1190 1193 return err
1191 1194
1192 1195 def updatedir(ui, repo, patches, similarity=0):
1193 1196 '''Update dirstate after patch application according to metadata'''
1194 1197 if not patches:
1195 1198 return
1196 1199 copies = []
1197 1200 removes = set()
1198 1201 cfiles = patches.keys()
1199 1202 cwd = repo.getcwd()
1200 1203 if cwd:
1201 1204 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1202 1205 for f in patches:
1203 1206 gp = patches[f]
1204 1207 if not gp:
1205 1208 continue
1206 1209 if gp.op == 'RENAME':
1207 1210 copies.append((gp.oldpath, gp.path))
1208 1211 removes.add(gp.oldpath)
1209 1212 elif gp.op == 'COPY':
1210 1213 copies.append((gp.oldpath, gp.path))
1211 1214 elif gp.op == 'DELETE':
1212 1215 removes.add(gp.path)
1213 1216 for src, dst in copies:
1214 1217 repo.copy(src, dst)
1215 1218 if (not similarity) and removes:
1216 1219 repo.remove(sorted(removes), True)
1217 1220 for f in patches:
1218 1221 gp = patches[f]
1219 1222 if gp and gp.mode:
1220 1223 islink, isexec = gp.mode
1221 1224 dst = repo.wjoin(gp.path)
1222 1225 # patch won't create empty files
1223 1226 if gp.op == 'ADD' and not os.path.exists(dst):
1224 1227 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1225 1228 repo.wwrite(gp.path, '', flags)
1226 1229 elif gp.op != 'DELETE':
1227 1230 util.set_flags(dst, islink, isexec)
1228 1231 cmdutil.addremove(repo, cfiles, similarity=similarity)
1229 1232 files = patches.keys()
1230 1233 files.extend([r for r in removes if r not in files])
1231 1234 return sorted(files)
1232 1235
1233 1236 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1234 1237 """use <patcher> to apply <patchname> to the working directory.
1235 1238 returns whether patch was applied with fuzz factor."""
1236 1239
1237 1240 fuzz = False
1238 1241 if cwd:
1239 1242 args.append('-d %s' % util.shellquote(cwd))
1240 1243 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1241 1244 util.shellquote(patchname)))
1242 1245
1243 1246 for line in fp:
1244 1247 line = line.rstrip()
1245 1248 ui.note(line + '\n')
1246 1249 if line.startswith('patching file '):
1247 1250 pf = util.parse_patch_output(line)
1248 1251 printed_file = False
1249 1252 files.setdefault(pf, None)
1250 1253 elif line.find('with fuzz') >= 0:
1251 1254 fuzz = True
1252 1255 if not printed_file:
1253 1256 ui.warn(pf + '\n')
1254 1257 printed_file = True
1255 1258 ui.warn(line + '\n')
1256 1259 elif line.find('saving rejects to file') >= 0:
1257 1260 ui.warn(line + '\n')
1258 1261 elif line.find('FAILED') >= 0:
1259 1262 if not printed_file:
1260 1263 ui.warn(pf + '\n')
1261 1264 printed_file = True
1262 1265 ui.warn(line + '\n')
1263 1266 code = fp.close()
1264 1267 if code:
1265 1268 raise PatchError(_("patch command failed: %s") %
1266 1269 util.explain_exit(code)[0])
1267 1270 return fuzz
1268 1271
1269 1272 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1270 1273 """use builtin patch to apply <patchobj> to the working directory.
1271 1274 returns whether patch was applied with fuzz factor."""
1272 1275
1273 1276 if files is None:
1274 1277 files = {}
1275 1278 if eolmode is None:
1276 1279 eolmode = ui.config('patch', 'eol', 'strict')
1277 1280 if eolmode.lower() not in eolmodes:
1278 1281 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1279 1282 eolmode = eolmode.lower()
1280 1283
1281 1284 try:
1282 1285 fp = open(patchobj, 'rb')
1283 1286 except TypeError:
1284 1287 fp = patchobj
1285 1288 if cwd:
1286 1289 curdir = os.getcwd()
1287 1290 os.chdir(cwd)
1288 1291 try:
1289 1292 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1290 1293 finally:
1291 1294 if cwd:
1292 1295 os.chdir(curdir)
1293 1296 if fp != patchobj:
1294 1297 fp.close()
1295 1298 if ret < 0:
1296 1299 raise PatchError
1297 1300 return ret > 0
1298 1301
1299 1302 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1300 1303 """Apply <patchname> to the working directory.
1301 1304
1302 1305 'eolmode' specifies how end of lines should be handled. It can be:
1303 1306 - 'strict': inputs are read in binary mode, EOLs are preserved
1304 1307 - 'crlf': EOLs are ignored when patching and reset to CRLF
1305 1308 - 'lf': EOLs are ignored when patching and reset to LF
1306 1309 - None: get it from user settings, default to 'strict'
1307 1310 'eolmode' is ignored when using an external patcher program.
1308 1311
1309 1312 Returns whether patch was applied with fuzz factor.
1310 1313 """
1311 1314 patcher = ui.config('ui', 'patch')
1312 1315 args = []
1313 1316 if files is None:
1314 1317 files = {}
1315 1318 try:
1316 1319 if patcher:
1317 1320 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1318 1321 files)
1319 1322 else:
1320 1323 try:
1321 1324 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1322 1325 except NoHunks:
1323 1326 ui.warn(_('internal patcher failed\n'
1324 1327 'please report details to '
1325 1328 'http://mercurial.selenic.com/bts/\n'
1326 1329 'or mercurial@selenic.com\n'))
1327 1330 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1328 1331 or 'patch')
1329 1332 ui.debug('no valid hunks found; trying with %r instead\n' %
1330 1333 patcher)
1331 1334 if util.needbinarypatch():
1332 1335 args.append('--binary')
1333 1336 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1334 1337 files)
1335 1338 except PatchError, err:
1336 1339 s = str(err)
1337 1340 if s:
1338 1341 raise util.Abort(s)
1339 1342 else:
1340 1343 raise util.Abort(_('patch failed to apply'))
1341 1344
1342 1345 def b85diff(to, tn):
1343 1346 '''print base85-encoded binary diff'''
1344 1347 def gitindex(text):
1345 1348 if not text:
1346 1349 return '0' * 40
1347 1350 l = len(text)
1348 1351 s = util.sha1('blob %d\0' % l)
1349 1352 s.update(text)
1350 1353 return s.hexdigest()
1351 1354
1352 1355 def fmtline(line):
1353 1356 l = len(line)
1354 1357 if l <= 26:
1355 1358 l = chr(ord('A') + l - 1)
1356 1359 else:
1357 1360 l = chr(l - 26 + ord('a') - 1)
1358 1361 return '%c%s\n' % (l, base85.b85encode(line, True))
1359 1362
1360 1363 def chunk(text, csize=52):
1361 1364 l = len(text)
1362 1365 i = 0
1363 1366 while i < l:
1364 1367 yield text[i:i + csize]
1365 1368 i += csize
1366 1369
1367 1370 tohash = gitindex(to)
1368 1371 tnhash = gitindex(tn)
1369 1372 if tohash == tnhash:
1370 1373 return ""
1371 1374
1372 1375 # TODO: deltas
1373 1376 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1374 1377 (tohash, tnhash, len(tn))]
1375 1378 for l in chunk(zlib.compress(tn)):
1376 1379 ret.append(fmtline(l))
1377 1380 ret.append('\n')
1378 1381 return ''.join(ret)
1379 1382
1380 1383 class GitDiffRequired(Exception):
1381 1384 pass
1382 1385
1383 1386 def diffopts(ui, opts=None, untrusted=False):
1384 1387 def get(key, name=None, getter=ui.configbool):
1385 1388 return ((opts and opts.get(key)) or
1386 1389 getter('diff', name or key, None, untrusted=untrusted))
1387 1390 return mdiff.diffopts(
1388 1391 text=opts and opts.get('text'),
1389 1392 git=get('git'),
1390 1393 nodates=get('nodates'),
1391 1394 showfunc=get('show_function', 'showfunc'),
1392 1395 ignorews=get('ignore_all_space', 'ignorews'),
1393 1396 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1394 1397 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1395 1398 context=get('unified', getter=ui.config))
1396 1399
1397 1400 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1398 1401 losedatafn=None):
1399 1402 '''yields diff of changes to files between two nodes, or node and
1400 1403 working directory.
1401 1404
1402 1405 if node1 is None, use first dirstate parent instead.
1403 1406 if node2 is None, compare node1 with working directory.
1404 1407
1405 1408 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1406 1409 every time some change cannot be represented with the current
1407 1410 patch format. Return False to upgrade to git patch format, True to
1408 1411 accept the loss or raise an exception to abort the diff. It is
1409 1412 called with the name of current file being diffed as 'fn'. If set
1410 1413 to None, patches will always be upgraded to git format when
1411 1414 necessary.
1412 1415 '''
1413 1416
1414 1417 if opts is None:
1415 1418 opts = mdiff.defaultopts
1416 1419
1417 1420 if not node1 and not node2:
1418 1421 node1 = repo.dirstate.parents()[0]
1419 1422
1420 1423 def lrugetfilectx():
1421 1424 cache = {}
1422 1425 order = []
1423 1426 def getfilectx(f, ctx):
1424 1427 fctx = ctx.filectx(f, filelog=cache.get(f))
1425 1428 if f not in cache:
1426 1429 if len(cache) > 20:
1427 1430 del cache[order.pop(0)]
1428 1431 cache[f] = fctx.filelog()
1429 1432 else:
1430 1433 order.remove(f)
1431 1434 order.append(f)
1432 1435 return fctx
1433 1436 return getfilectx
1434 1437 getfilectx = lrugetfilectx()
1435 1438
1436 1439 ctx1 = repo[node1]
1437 1440 ctx2 = repo[node2]
1438 1441
1439 1442 if not changes:
1440 1443 changes = repo.status(ctx1, ctx2, match=match)
1441 1444 modified, added, removed = changes[:3]
1442 1445
1443 1446 if not modified and not added and not removed:
1444 1447 return []
1445 1448
1446 1449 revs = None
1447 1450 if not repo.ui.quiet:
1448 1451 hexfunc = repo.ui.debugflag and hex or short
1449 1452 revs = [hexfunc(node) for node in [node1, node2] if node]
1450 1453
1451 1454 copy = {}
1452 1455 if opts.git or opts.upgrade:
1453 1456 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1454 1457
1455 1458 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1456 1459 modified, added, removed, copy, getfilectx, opts, losedata)
1457 1460 if opts.upgrade and not opts.git:
1458 1461 try:
1459 1462 def losedata(fn):
1460 1463 if not losedatafn or not losedatafn(fn=fn):
1461 1464 raise GitDiffRequired()
1462 1465 # Buffer the whole output until we are sure it can be generated
1463 1466 return list(difffn(opts.copy(git=False), losedata))
1464 1467 except GitDiffRequired:
1465 1468 return difffn(opts.copy(git=True), None)
1466 1469 else:
1467 1470 return difffn(opts, None)
1468 1471
1469 1472 def difflabel(func, *args, **kw):
1470 1473 '''yields 2-tuples of (output, label) based on the output of func()'''
1471 1474 prefixes = [('diff', 'diff.diffline'),
1472 1475 ('copy', 'diff.extended'),
1473 1476 ('rename', 'diff.extended'),
1474 1477 ('old', 'diff.extended'),
1475 1478 ('new', 'diff.extended'),
1476 1479 ('deleted', 'diff.extended'),
1477 1480 ('---', 'diff.file_a'),
1478 1481 ('+++', 'diff.file_b'),
1479 1482 ('@@', 'diff.hunk'),
1480 1483 ('-', 'diff.deleted'),
1481 1484 ('+', 'diff.inserted')]
1482 1485
1483 1486 for chunk in func(*args, **kw):
1484 1487 lines = chunk.split('\n')
1485 1488 for i, line in enumerate(lines):
1486 1489 if i != 0:
1487 1490 yield ('\n', '')
1488 1491 stripline = line
1489 1492 if line and line[0] in '+-':
1490 1493 # highlight trailing whitespace, but only in changed lines
1491 1494 stripline = line.rstrip()
1492 1495 for prefix, label in prefixes:
1493 1496 if stripline.startswith(prefix):
1494 1497 yield (stripline, label)
1495 1498 break
1496 1499 else:
1497 1500 yield (line, '')
1498 1501 if line != stripline:
1499 1502 yield (line[len(stripline):], 'diff.trailingwhitespace')
1500 1503
1501 1504 def diffui(*args, **kw):
1502 1505 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1503 1506 return difflabel(diff, *args, **kw)
1504 1507
1505 1508
1506 1509 def _addmodehdr(header, omode, nmode):
1507 1510 if omode != nmode:
1508 1511 header.append('old mode %s\n' % omode)
1509 1512 header.append('new mode %s\n' % nmode)
1510 1513
1511 1514 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1512 1515 copy, getfilectx, opts, losedatafn):
1513 1516
1514 1517 date1 = util.datestr(ctx1.date())
1515 1518 man1 = ctx1.manifest()
1516 1519
1517 1520 gone = set()
1518 1521 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1519 1522
1520 1523 copyto = dict([(v, k) for k, v in copy.items()])
1521 1524
1522 1525 if opts.git:
1523 1526 revs = None
1524 1527
1525 1528 for f in sorted(modified + added + removed):
1526 1529 to = None
1527 1530 tn = None
1528 1531 dodiff = True
1529 1532 header = []
1530 1533 if f in man1:
1531 1534 to = getfilectx(f, ctx1).data()
1532 1535 if f not in removed:
1533 1536 tn = getfilectx(f, ctx2).data()
1534 1537 a, b = f, f
1535 1538 if opts.git or losedatafn:
1536 1539 if f in added:
1537 1540 mode = gitmode[ctx2.flags(f)]
1538 1541 if f in copy or f in copyto:
1539 1542 if opts.git:
1540 1543 if f in copy:
1541 1544 a = copy[f]
1542 1545 else:
1543 1546 a = copyto[f]
1544 1547 omode = gitmode[man1.flags(a)]
1545 1548 _addmodehdr(header, omode, mode)
1546 1549 if a in removed and a not in gone:
1547 1550 op = 'rename'
1548 1551 gone.add(a)
1549 1552 else:
1550 1553 op = 'copy'
1551 1554 header.append('%s from %s\n' % (op, a))
1552 1555 header.append('%s to %s\n' % (op, f))
1553 1556 to = getfilectx(a, ctx1).data()
1554 1557 else:
1555 1558 losedatafn(f)
1556 1559 else:
1557 1560 if opts.git:
1558 1561 header.append('new file mode %s\n' % mode)
1559 1562 elif ctx2.flags(f):
1560 1563 losedatafn(f)
1561 1564 if util.binary(tn):
1562 1565 if opts.git:
1563 1566 dodiff = 'binary'
1564 1567 else:
1565 1568 losedatafn(f)
1566 1569 if not opts.git and not tn:
1567 1570 # regular diffs cannot represent new empty file
1568 1571 losedatafn(f)
1569 1572 elif f in removed:
1570 1573 if opts.git:
1571 1574 # have we already reported a copy above?
1572 1575 if ((f in copy and copy[f] in added
1573 1576 and copyto[copy[f]] == f) or
1574 1577 (f in copyto and copyto[f] in added
1575 1578 and copy[copyto[f]] == f)):
1576 1579 dodiff = False
1577 1580 else:
1578 1581 header.append('deleted file mode %s\n' %
1579 1582 gitmode[man1.flags(f)])
1580 1583 elif not to:
1581 1584 # regular diffs cannot represent empty file deletion
1582 1585 losedatafn(f)
1583 1586 else:
1584 1587 oflag = man1.flags(f)
1585 1588 nflag = ctx2.flags(f)
1586 1589 binary = util.binary(to) or util.binary(tn)
1587 1590 if opts.git:
1588 1591 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1589 1592 if binary:
1590 1593 dodiff = 'binary'
1591 1594 elif binary or nflag != oflag:
1592 1595 losedatafn(f)
1593 1596 if opts.git:
1594 1597 header.insert(0, mdiff.diffline(revs, a, b, opts))
1595 1598
1596 1599 if dodiff:
1597 1600 if dodiff == 'binary':
1598 1601 text = b85diff(to, tn)
1599 1602 else:
1600 1603 text = mdiff.unidiff(to, date1,
1601 1604 # ctx2 date may be dynamic
1602 1605 tn, util.datestr(ctx2.date()),
1603 1606 a, b, revs, opts=opts)
1604 1607 if header and (text or len(header) > 1):
1605 1608 yield ''.join(header)
1606 1609 if text:
1607 1610 yield text
1608 1611
1609 1612 def diffstatdata(lines):
1610 1613 filename, adds, removes = None, 0, 0
1611 1614 for line in lines:
1612 1615 if line.startswith('diff'):
1613 1616 if filename:
1614 1617 isbinary = adds == 0 and removes == 0
1615 1618 yield (filename, adds, removes, isbinary)
1616 1619 # set numbers to 0 anyway when starting new file
1617 1620 adds, removes = 0, 0
1618 1621 if line.startswith('diff --git'):
1619 1622 filename = gitre.search(line).group(1)
1620 1623 else:
1621 1624 # format: "diff -r ... -r ... filename"
1622 1625 filename = line.split(None, 5)[-1]
1623 1626 elif line.startswith('+') and not line.startswith('+++'):
1624 1627 adds += 1
1625 1628 elif line.startswith('-') and not line.startswith('---'):
1626 1629 removes += 1
1627 1630 if filename:
1628 1631 isbinary = adds == 0 and removes == 0
1629 1632 yield (filename, adds, removes, isbinary)
1630 1633
1631 1634 def diffstat(lines, width=80, git=False):
1632 1635 output = []
1633 1636 stats = list(diffstatdata(lines))
1634 1637
1635 1638 maxtotal, maxname = 0, 0
1636 1639 totaladds, totalremoves = 0, 0
1637 1640 hasbinary = False
1638 1641 for filename, adds, removes, isbinary in stats:
1639 1642 totaladds += adds
1640 1643 totalremoves += removes
1641 1644 maxname = max(maxname, len(filename))
1642 1645 maxtotal = max(maxtotal, adds + removes)
1643 1646 if isbinary:
1644 1647 hasbinary = True
1645 1648
1646 1649 countwidth = len(str(maxtotal))
1647 1650 if hasbinary and countwidth < 3:
1648 1651 countwidth = 3
1649 1652 graphwidth = width - countwidth - maxname - 6
1650 1653 if graphwidth < 10:
1651 1654 graphwidth = 10
1652 1655
1653 1656 def scale(i):
1654 1657 if maxtotal <= graphwidth:
1655 1658 return i
1656 1659 # If diffstat runs out of room it doesn't print anything,
1657 1660 # which isn't very useful, so always print at least one + or -
1658 1661 # if there were at least some changes.
1659 1662 return max(i * graphwidth // maxtotal, int(bool(i)))
1660 1663
1661 1664 for filename, adds, removes, isbinary in stats:
1662 1665 if git and isbinary:
1663 1666 count = 'Bin'
1664 1667 else:
1665 1668 count = adds + removes
1666 1669 pluses = '+' * scale(adds)
1667 1670 minuses = '-' * scale(removes)
1668 1671 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1669 1672 count, pluses, minuses))
1670 1673
1671 1674 if stats:
1672 1675 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1673 1676 % (len(stats), totaladds, totalremoves))
1674 1677
1675 1678 return ''.join(output)
1676 1679
1677 1680 def diffstatui(*args, **kw):
1678 1681 '''like diffstat(), but yields 2-tuples of (output, label) for
1679 1682 ui.write()
1680 1683 '''
1681 1684
1682 1685 for line in diffstat(*args, **kw).splitlines():
1683 1686 if line and line[-1] in '+-':
1684 1687 name, graph = line.rsplit(' ', 1)
1685 1688 yield (name + ' ', '')
1686 1689 m = re.search(r'\++', graph)
1687 1690 if m:
1688 1691 yield (m.group(0), 'diffstat.inserted')
1689 1692 m = re.search(r'-+', graph)
1690 1693 if m:
1691 1694 yield (m.group(0), 'diffstat.deleted')
1692 1695 else:
1693 1696 yield (line, '')
1694 1697 yield ('\n', '')
@@ -1,476 +1,497 b''
1 1 #!/bin/sh
2 2
3 3 hg init a
4 4 mkdir a/d1
5 5 mkdir a/d1/d2
6 6 echo line 1 > a/a
7 7 echo line 1 > a/d1/d2/a
8 8 hg --cwd a ci -Ama
9 9
10 10 echo line 2 >> a/a
11 11 hg --cwd a ci -u someone -d '1 0' -m'second change'
12 12
13 13 echo % import exported patch
14 14 hg clone -r0 a b
15 15 hg --cwd a export tip > tip.patch
16 16 hg --cwd b import ../tip.patch
17 17 echo % message should be same
18 18 hg --cwd b tip | grep 'second change'
19 19 echo % committer should be same
20 20 hg --cwd b tip | grep someone
21 21 rm -r b
22 22
23 23 echo % import exported patch with external patcher
24 24 cat > dummypatch.py <<EOF
25 25 print 'patching file a'
26 26 file('a', 'wb').write('line2\n')
27 27 EOF
28 28 chmod +x dummypatch.py
29 29 hg clone -r0 a b
30 30 hg --cwd a export tip > tip.patch
31 31 hg --config ui.patch='python ../dummypatch.py' --cwd b import ../tip.patch
32 32 cat b/a
33 33 rm -r b
34 34
35 35 echo % import of plain diff should fail without message
36 36 hg clone -r0 a b
37 37 hg --cwd a diff -r0:1 > tip.patch
38 38 hg --cwd b import ../tip.patch
39 39 rm -r b
40 40
41 41 echo % import of plain diff should be ok with message
42 42 hg clone -r0 a b
43 43 hg --cwd a diff -r0:1 > tip.patch
44 44 hg --cwd b import -mpatch ../tip.patch
45 45 rm -r b
46 46
47 47 echo % import of plain diff with specific date and user
48 48 hg clone -r0 a b
49 49 hg --cwd a diff -r0:1 > tip.patch
50 50 hg --cwd b import -mpatch -d '1 0' -u 'user@nowhere.net' ../tip.patch
51 51 hg -R b tip -pv
52 52 rm -r b
53 53
54 54 echo % import of plain diff should be ok with --no-commit
55 55 hg clone -r0 a b
56 56 hg --cwd a diff -r0:1 > tip.patch
57 57 hg --cwd b import --no-commit ../tip.patch
58 58 hg --cwd b diff --nodates
59 59 rm -r b
60 60
61 61 echo % hg -R repo import
62 62 # put the clone in a subdir - having a directory named "a"
63 63 # used to hide a bug.
64 64 mkdir dir
65 65 hg clone -r0 a dir/b
66 66 hg --cwd a export tip > dir/tip.patch
67 67 cd dir
68 68 hg -R b import tip.patch
69 69 cd ..
70 70 rm -r dir
71 71
72 72 echo % import from stdin
73 73 hg clone -r0 a b
74 74 hg --cwd a export tip | hg --cwd b import -
75 75 rm -r b
76 76
77 77 echo % import two patches in one stream
78 78 hg init b
79 79 hg --cwd a export 0:tip | hg --cwd b import -
80 80 hg --cwd a id
81 81 hg --cwd b id
82 82 rm -r b
83 83
84 84 echo % override commit message
85 85 hg clone -r0 a b
86 86 hg --cwd a export tip | hg --cwd b import -m 'override' -
87 87 hg --cwd b tip | grep override
88 88 rm -r b
89 89
90 90 cat > mkmsg.py <<EOF
91 91 import email.Message, sys
92 92 msg = email.Message.Message()
93 93 msg.set_payload('email commit message\n' + open('tip.patch', 'rb').read())
94 94 msg['Subject'] = 'email patch'
95 95 msg['From'] = 'email patcher'
96 96 sys.stdout.write(msg.as_string())
97 97 EOF
98 98
99 99 echo % plain diff in email, subject, message body
100 100 hg clone -r0 a b
101 101 hg --cwd a diff -r0:1 > tip.patch
102 102 python mkmsg.py > msg.patch
103 103 hg --cwd b import ../msg.patch
104 104 hg --cwd b tip | grep email
105 105 rm -r b
106 106
107 107 echo % plain diff in email, no subject, message body
108 108 hg clone -r0 a b
109 109 grep -v '^Subject:' msg.patch | hg --cwd b import -
110 110 rm -r b
111 111
112 112 echo % plain diff in email, subject, no message body
113 113 hg clone -r0 a b
114 114 grep -v '^email ' msg.patch | hg --cwd b import -
115 115 rm -r b
116 116
117 117 echo % plain diff in email, no subject, no message body, should fail
118 118 hg clone -r0 a b
119 119 egrep -v '^(Subject|email)' msg.patch | hg --cwd b import -
120 120 rm -r b
121 121
122 122 echo % hg export in email, should use patch header
123 123 hg clone -r0 a b
124 124 hg --cwd a export tip > tip.patch
125 125 python mkmsg.py | hg --cwd b import -
126 126 hg --cwd b tip | grep second
127 127 rm -r b
128 128
129 129 # subject: duplicate detection, removal of [PATCH]
130 130 # The '---' tests the gitsendmail handling without proper mail headers
131 131 cat > mkmsg2.py <<EOF
132 132 import email.Message, sys
133 133 msg = email.Message.Message()
134 134 msg.set_payload('email patch\n\nnext line\n---\n' + open('tip.patch').read())
135 135 msg['Subject'] = '[PATCH] email patch'
136 136 msg['From'] = 'email patcher'
137 137 sys.stdout.write(msg.as_string())
138 138 EOF
139 139
140 140 echo '% plain diff in email, [PATCH] subject, message body with subject'
141 141 hg clone -r0 a b
142 142 hg --cwd a diff -r0:1 > tip.patch
143 143 python mkmsg2.py | hg --cwd b import -
144 144 hg --cwd b tip --template '{desc}\n'
145 145 rm -r b
146 146
147 147 # We weren't backing up the correct dirstate file when importing many patches
148 148 # (issue963)
149 149 echo '% import patch1 patch2; rollback'
150 150 echo line 3 >> a/a
151 151 hg --cwd a ci -m'third change'
152 152 hg --cwd a export -o '../patch%R' 1 2
153 153 hg clone -qr0 a b
154 154 hg --cwd b parents --template 'parent: {rev}\n'
155 155 hg --cwd b import ../patch1 ../patch2
156 156 hg --cwd b rollback
157 157 hg --cwd b parents --template 'parent: {rev}\n'
158 158 rm -r b
159 159
160 160 # bug non regression test
161 161 # importing a patch in a subdirectory failed at the commit stage
162 162 echo line 2 >> a/d1/d2/a
163 163 hg --cwd a ci -u someoneelse -d '1 0' -m'subdir change'
164 164 echo % hg import in a subdirectory
165 165 hg clone -r0 a b
166 166 hg --cwd a export tip | sed -e 's/d1\/d2\///' > tip.patch
167 167 dir=`pwd`
168 168 cd b/d1/d2 2>&1 > /dev/null
169 169 hg import ../../../tip.patch
170 170 cd "$dir"
171 171 echo "% message should be 'subdir change'"
172 172 hg --cwd b tip | grep 'subdir change'
173 173 echo "% committer should be 'someoneelse'"
174 174 hg --cwd b tip | grep someoneelse
175 175 echo "% should be empty"
176 176 hg --cwd b status
177 177
178 178
179 179 # Test fuzziness (ambiguous patch location, fuzz=2)
180 180 echo % test fuzziness
181 181 hg init fuzzy
182 182 cd fuzzy
183 183 echo line1 > a
184 184 echo line0 >> a
185 185 echo line3 >> a
186 186 hg ci -Am adda
187 187 echo line1 > a
188 188 echo line2 >> a
189 189 echo line0 >> a
190 190 echo line3 >> a
191 191 hg ci -m change a
192 192 hg export tip > tip.patch
193 193 hg up -C 0
194 194 echo line1 > a
195 195 echo line0 >> a
196 196 echo line1 >> a
197 197 echo line0 >> a
198 198 hg ci -m brancha
199 199 hg import --no-commit -v tip.patch
200 200 hg revert -a
201 201 echo '% test fuzziness with eol=auto'
202 202 hg --config patch.eol=auto import --no-commit -v tip.patch
203 203 cd ..
204 204
205 205 # Test hunk touching empty files (issue906)
206 206 hg init empty
207 207 cd empty
208 208 touch a
209 209 touch b1
210 210 touch c1
211 211 echo d > d
212 212 hg ci -Am init
213 213 echo a > a
214 214 echo b > b1
215 215 hg mv b1 b2
216 216 echo c > c1
217 217 hg copy c1 c2
218 218 rm d
219 219 touch d
220 220 hg diff --git
221 221 hg ci -m empty
222 222 hg export --git tip > empty.diff
223 223 hg up -C 0
224 224 hg import empty.diff
225 225 for name in a b1 b2 c1 c2 d;
226 226 do
227 227 echo % $name file
228 228 test -f $name && cat $name
229 229 done
230 230 cd ..
231 231
232 232 # Test importing a patch ending with a binary file removal
233 233 echo % test trailing binary removal
234 234 hg init binaryremoval
235 235 cd binaryremoval
236 236 echo a > a
237 237 python -c "file('b', 'wb').write('a\x00b')"
238 238 hg ci -Am addall
239 239 hg rm a
240 240 hg rm b
241 241 hg st
242 242 hg ci -m remove
243 243 hg export --git . > remove.diff
244 244 cat remove.diff | grep git
245 245 hg up -C 0
246 246 hg import remove.diff
247 247 hg manifest
248 248 cd ..
249 249
250 250 echo % 'test update+rename with common name (issue 927)'
251 251 hg init t
252 252 cd t
253 253 touch a
254 254 hg ci -Am t
255 255 echo a > a
256 256 # Here, bfile.startswith(afile)
257 257 hg copy a a2
258 258 hg ci -m copya
259 259 hg export --git tip > copy.diff
260 260 hg up -C 0
261 261 hg import copy.diff
262 262 echo % view a
263 263 # a should contain an 'a'
264 264 cat a
265 265 echo % view a2
266 266 # and a2 should have duplicated it
267 267 cat a2
268 268 cd ..
269 269
270 270 echo % 'test -p0'
271 271 hg init p0
272 272 cd p0
273 273 echo a > a
274 274 hg ci -Am t
275 275 hg import -p0 - << EOF
276 276 foobar
277 277 --- a Sat Apr 12 22:43:58 2008 -0400
278 278 +++ a Sat Apr 12 22:44:05 2008 -0400
279 279 @@ -1,1 +1,1 @@
280 280 -a
281 281 +bb
282 282 EOF
283 283 hg status
284 284 cat a
285 285 cd ..
286 286
287 287 echo % 'test paths outside repo root'
288 288 mkdir outside
289 289 touch outside/foo
290 290 hg init inside
291 291 cd inside
292 292 hg import - <<EOF
293 293 diff --git a/a b/b
294 294 rename from ../outside/foo
295 295 rename to bar
296 296 EOF
297 297 cd ..
298 298
299 299 echo '% test import with similarity (issue295)'
300 300 hg init sim
301 301 cd sim
302 302 echo 'this is a test' > a
303 303 hg ci -Ama
304 304 cat > ../rename.diff <<EOF
305 305 diff --git a/a b/a
306 306 deleted file mode 100644
307 307 --- a/a
308 308 +++ /dev/null
309 309 @@ -1,1 +0,0 @@
310 310 -this is a test
311 311 diff --git a/b b/b
312 312 new file mode 100644
313 313 --- /dev/null
314 314 +++ b/b
315 315 @@ -0,0 +1,2 @@
316 316 +this is a test
317 317 +foo
318 318 EOF
319 319 hg import --no-commit -v -s 1 ../rename.diff
320 320 hg st -C
321 321 hg revert -a
322 322 rm b
323 323 hg import --no-commit -v -s 100 ../rename.diff
324 324 hg st -C
325 325 cd ..
326 326
327 327
328 328 echo '% add empty file from the end of patch (issue 1495)'
329 329 hg init addemptyend
330 330 cd addemptyend
331 331 touch a
332 332 hg addremove
333 333 hg ci -m "commit"
334 334 cat > a.patch <<EOF
335 335 diff --git a/a b/a
336 336 --- a/a
337 337 +++ b/a
338 338 @@ -0,0 +1,1 @@
339 339 +a
340 340 diff --git a/b b/b
341 341 new file mode 100644
342 342 EOF
343 343 hg import --no-commit a.patch
344 344 cd ..
345 345
346 346 echo '% create file when source is not /dev/null'
347 347 cat > create.patch <<EOF
348 348 diff -Naur proj-orig/foo proj-new/foo
349 349 --- proj-orig/foo 1969-12-31 16:00:00.000000000 -0800
350 350 +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
351 351 @@ -0,0 +1,1 @@
352 352 +a
353 353 EOF
354 354 # some people have patches like the following too
355 355 cat > create2.patch <<EOF
356 356 diff -Naur proj-orig/foo proj-new/foo
357 357 --- proj-orig/foo.orig 1969-12-31 16:00:00.000000000 -0800
358 358 +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
359 359 @@ -0,0 +1,1 @@
360 360 +a
361 361 EOF
362 362 hg init oddcreate
363 363 cd oddcreate
364 364 hg import --no-commit ../create.patch
365 365 cat foo
366 366 rm foo
367 367 hg revert foo
368 368 hg import --no-commit ../create2.patch
369 369 cat foo
370 370
371 371 echo % 'first line mistaken for email headers (issue 1859)'
372 372 hg init emailconfusion
373 373 cd emailconfusion
374 374 cat > a.patch <<EOF
375 375 module: summary
376 376
377 377 description
378 378
379 379
380 380 diff -r 000000000000 -r 9b4c1e343b55 test.txt
381 381 --- /dev/null
382 382 +++ b/a
383 383 @@ -0,0 +1,1 @@
384 384 +a
385 385 EOF
386 386 hg import -d '0 0' a.patch
387 387 hg parents -v
388 388 cd ..
389 389
390 390 echo % '--- in commit message'
391 391 hg init commitconfusion
392 392 cd commitconfusion
393 393 cat > a.patch <<EOF
394 394 module: summary
395 395
396 396 --- description
397 397
398 398 diff --git a/a b/a
399 399 new file mode 100644
400 400 --- /dev/null
401 401 +++ b/a
402 402 @@ -0,0 +1,1 @@
403 403 +a
404 404 EOF
405 405 hg import -d '0 0' a.patch
406 406 hg parents -v
407 407 cd ..
408 408
409 409 echo '% tricky header splitting'
410 410 cat > trickyheaders.patch <<EOF
411 411 From: User A <user@a>
412 412 Subject: [PATCH] from: tricky!
413 413
414 414 # HG changeset patch
415 415 # User User B
416 416 # Date 1266264441 18000
417 417 # Branch stable
418 418 # Node ID f2be6a1170ac83bf31cb4ae0bad00d7678115bc0
419 419 # Parent 0000000000000000000000000000000000000000
420 420 from: tricky!
421 421
422 422 That is not a header.
423 423
424 424 diff -r 000000000000 -r f2be6a1170ac foo
425 425 --- /dev/null
426 426 +++ b/foo
427 427 @@ -0,0 +1,1 @@
428 428 +foo
429 429 EOF
430 430
431 431 hg init trickyheaders
432 432 cd trickyheaders
433 433 hg import -d '0 0' ../trickyheaders.patch
434 434 hg export --git tip
435 435 cd ..
436 436
437 437 echo '% issue2102'
438 438 hg init issue2102
439 439 cd issue2102
440 440 mkdir -p src/cmd/gc
441 441 touch src/cmd/gc/mksys.bash
442 442 hg ci -Am init
443 443 hg import - <<EOF
444 444 # HG changeset patch
445 445 # User Rob Pike
446 446 # Date 1216685449 25200
447 447 # Node ID 03aa2b206f499ad6eb50e6e207b9e710d6409c98
448 448 # Parent 93d10138ad8df586827ca90b4ddb5033e21a3a84
449 449 help management of empty pkg and lib directories in perforce
450 450
451 451 R=gri
452 452 DELTA=4 (4 added, 0 deleted, 0 changed)
453 453 OCL=13328
454 454 CL=13328
455 455
456 456 diff --git a/lib/place-holder b/lib/place-holder
457 457 new file mode 100644
458 458 --- /dev/null
459 459 +++ b/lib/place-holder
460 460 @@ -0,0 +1,2 @@
461 461 +perforce does not maintain empty directories.
462 462 +this file helps.
463 463 diff --git a/pkg/place-holder b/pkg/place-holder
464 464 new file mode 100644
465 465 --- /dev/null
466 466 +++ b/pkg/place-holder
467 467 @@ -0,0 +1,2 @@
468 468 +perforce does not maintain empty directories.
469 469 +this file helps.
470 470 diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
471 471 old mode 100644
472 472 new mode 100755
473 473 EOF
474 474 hg sum
475 475 hg diff --git -c tip
476 476 cd ..
477
478 echo '% diff lines looking like headers'
479 hg init difflineslikeheaders
480 cd difflineslikeheaders
481 echo a >a
482 echo b >b
483 echo c >c
484 hg ci -Am1
485
486 echo "key: value" >>a
487 echo "key: value" >>b
488 echo "foo" >>c
489 hg ci -m2
490
491 hg up -C 0
492 hg diff --git -c1 >want
493 hg diff -c1 | hg import --no-commit -
494 hg diff --git >have
495 diff want have
496 cd ..
497
@@ -1,375 +1,381 b''
1 1 adding a
2 2 adding d1/d2/a
3 3 % import exported patch
4 4 requesting all changes
5 5 adding changesets
6 6 adding manifests
7 7 adding file changes
8 8 added 1 changesets with 2 changes to 2 files
9 9 updating to branch default
10 10 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
11 11 applying ../tip.patch
12 12 % message should be same
13 13 summary: second change
14 14 % committer should be same
15 15 user: someone
16 16 % import exported patch with external patcher
17 17 requesting all changes
18 18 adding changesets
19 19 adding manifests
20 20 adding file changes
21 21 added 1 changesets with 2 changes to 2 files
22 22 updating to branch default
23 23 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
24 24 applying ../tip.patch
25 25 line2
26 26 % import of plain diff should fail without message
27 27 requesting all changes
28 28 adding changesets
29 29 adding manifests
30 30 adding file changes
31 31 added 1 changesets with 2 changes to 2 files
32 32 updating to branch default
33 33 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 34 applying ../tip.patch
35 35 abort: empty commit message
36 36 % import of plain diff should be ok with message
37 37 requesting all changes
38 38 adding changesets
39 39 adding manifests
40 40 adding file changes
41 41 added 1 changesets with 2 changes to 2 files
42 42 updating to branch default
43 43 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
44 44 applying ../tip.patch
45 45 % import of plain diff with specific date and user
46 46 requesting all changes
47 47 adding changesets
48 48 adding manifests
49 49 adding file changes
50 50 added 1 changesets with 2 changes to 2 files
51 51 updating to branch default
52 52 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 53 applying ../tip.patch
54 54 changeset: 1:ca68f19f3a40
55 55 tag: tip
56 56 user: user@nowhere.net
57 57 date: Thu Jan 01 00:00:01 1970 +0000
58 58 files: a
59 59 description:
60 60 patch
61 61
62 62
63 63 diff -r 80971e65b431 -r ca68f19f3a40 a
64 64 --- a/a Thu Jan 01 00:00:00 1970 +0000
65 65 +++ b/a Thu Jan 01 00:00:01 1970 +0000
66 66 @@ -1,1 +1,2 @@
67 67 line 1
68 68 +line 2
69 69
70 70 % import of plain diff should be ok with --no-commit
71 71 requesting all changes
72 72 adding changesets
73 73 adding manifests
74 74 adding file changes
75 75 added 1 changesets with 2 changes to 2 files
76 76 updating to branch default
77 77 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 78 applying ../tip.patch
79 79 diff -r 80971e65b431 a
80 80 --- a/a
81 81 +++ b/a
82 82 @@ -1,1 +1,2 @@
83 83 line 1
84 84 +line 2
85 85 % hg -R repo import
86 86 requesting all changes
87 87 adding changesets
88 88 adding manifests
89 89 adding file changes
90 90 added 1 changesets with 2 changes to 2 files
91 91 updating to branch default
92 92 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
93 93 applying tip.patch
94 94 % import from stdin
95 95 requesting all changes
96 96 adding changesets
97 97 adding manifests
98 98 adding file changes
99 99 added 1 changesets with 2 changes to 2 files
100 100 updating to branch default
101 101 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 102 applying patch from stdin
103 103 % import two patches in one stream
104 104 applying patch from stdin
105 105 applied 80971e65b431
106 106 1d4bd90af0e4 tip
107 107 1d4bd90af0e4 tip
108 108 % override commit message
109 109 requesting all changes
110 110 adding changesets
111 111 adding manifests
112 112 adding file changes
113 113 added 1 changesets with 2 changes to 2 files
114 114 updating to branch default
115 115 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
116 116 applying patch from stdin
117 117 summary: override
118 118 % plain diff in email, subject, message body
119 119 requesting all changes
120 120 adding changesets
121 121 adding manifests
122 122 adding file changes
123 123 added 1 changesets with 2 changes to 2 files
124 124 updating to branch default
125 125 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
126 126 applying ../msg.patch
127 127 user: email patcher
128 128 summary: email patch
129 129 % plain diff in email, no subject, message body
130 130 requesting all changes
131 131 adding changesets
132 132 adding manifests
133 133 adding file changes
134 134 added 1 changesets with 2 changes to 2 files
135 135 updating to branch default
136 136 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
137 137 applying patch from stdin
138 138 % plain diff in email, subject, no message body
139 139 requesting all changes
140 140 adding changesets
141 141 adding manifests
142 142 adding file changes
143 143 added 1 changesets with 2 changes to 2 files
144 144 updating to branch default
145 145 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
146 146 applying patch from stdin
147 147 % plain diff in email, no subject, no message body, should fail
148 148 requesting all changes
149 149 adding changesets
150 150 adding manifests
151 151 adding file changes
152 152 added 1 changesets with 2 changes to 2 files
153 153 updating to branch default
154 154 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
155 155 applying patch from stdin
156 156 abort: empty commit message
157 157 % hg export in email, should use patch header
158 158 requesting all changes
159 159 adding changesets
160 160 adding manifests
161 161 adding file changes
162 162 added 1 changesets with 2 changes to 2 files
163 163 updating to branch default
164 164 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
165 165 applying patch from stdin
166 166 summary: second change
167 167 % plain diff in email, [PATCH] subject, message body with subject
168 168 requesting all changes
169 169 adding changesets
170 170 adding manifests
171 171 adding file changes
172 172 added 1 changesets with 2 changes to 2 files
173 173 updating to branch default
174 174 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
175 175 applying patch from stdin
176 176 email patch
177 177
178 178 next line
179 179 ---
180 180 % import patch1 patch2; rollback
181 181 parent: 0
182 182 applying ../patch1
183 183 applying ../patch2
184 184 applied 1d4bd90af0e4
185 185 rolling back commit to revision 2
186 186 parent: 1
187 187 % hg import in a subdirectory
188 188 requesting all changes
189 189 adding changesets
190 190 adding manifests
191 191 adding file changes
192 192 added 1 changesets with 2 changes to 2 files
193 193 updating to branch default
194 194 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
195 195 applying ../../../tip.patch
196 196 % message should be 'subdir change'
197 197 summary: subdir change
198 198 % committer should be 'someoneelse'
199 199 user: someoneelse
200 200 % should be empty
201 201 % test fuzziness
202 202 adding a
203 203 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
204 204 created new head
205 205 applying tip.patch
206 206 patching file a
207 207 Hunk #1 succeeded at 1 with fuzz 2 (offset -2 lines).
208 208 reverting a
209 209 % test fuzziness with eol=auto
210 210 applying tip.patch
211 211 patching file a
212 212 Hunk #1 succeeded at 1 with fuzz 2 (offset -2 lines).
213 213 adding a
214 214 adding b1
215 215 adding c1
216 216 adding d
217 217 diff --git a/a b/a
218 218 --- a/a
219 219 +++ b/a
220 220 @@ -0,0 +1,1 @@
221 221 +a
222 222 diff --git a/b1 b/b2
223 223 rename from b1
224 224 rename to b2
225 225 --- a/b1
226 226 +++ b/b2
227 227 @@ -0,0 +1,1 @@
228 228 +b
229 229 diff --git a/c1 b/c1
230 230 --- a/c1
231 231 +++ b/c1
232 232 @@ -0,0 +1,1 @@
233 233 +c
234 234 diff --git a/c1 b/c2
235 235 copy from c1
236 236 copy to c2
237 237 --- a/c1
238 238 +++ b/c2
239 239 @@ -0,0 +1,1 @@
240 240 +c
241 241 diff --git a/d b/d
242 242 --- a/d
243 243 +++ b/d
244 244 @@ -1,1 +0,0 @@
245 245 -d
246 246 4 files updated, 0 files merged, 2 files removed, 0 files unresolved
247 247 applying empty.diff
248 248 % a file
249 249 a
250 250 % b1 file
251 251 % b2 file
252 252 b
253 253 % c1 file
254 254 c
255 255 % c2 file
256 256 c
257 257 % d file
258 258 % test trailing binary removal
259 259 adding a
260 260 adding b
261 261 R a
262 262 R b
263 263 diff --git a/a b/a
264 264 diff --git a/b b/b
265 265 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
266 266 applying remove.diff
267 267 % test update+rename with common name (issue 927)
268 268 adding a
269 269 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
270 270 applying copy.diff
271 271 % view a
272 272 a
273 273 % view a2
274 274 a
275 275 % test -p0
276 276 adding a
277 277 applying patch from stdin
278 278 bb
279 279 % test paths outside repo root
280 280 applying patch from stdin
281 281 abort: ../outside/foo not under root
282 282 % test import with similarity (issue295)
283 283 adding a
284 284 applying ../rename.diff
285 285 patching file a
286 286 patching file b
287 287 removing a
288 288 adding b
289 289 recording removal of a as rename to b (88% similar)
290 290 A b
291 291 a
292 292 R a
293 293 undeleting a
294 294 forgetting b
295 295 applying ../rename.diff
296 296 patching file a
297 297 patching file b
298 298 removing a
299 299 adding b
300 300 A b
301 301 R a
302 302 % add empty file from the end of patch (issue 1495)
303 303 adding a
304 304 applying a.patch
305 305 % create file when source is not /dev/null
306 306 applying ../create.patch
307 307 a
308 308 applying ../create2.patch
309 309 a
310 310 % first line mistaken for email headers (issue 1859)
311 311 applying a.patch
312 312 changeset: 0:5a681217c0ad
313 313 tag: tip
314 314 user: test
315 315 date: Thu Jan 01 00:00:00 1970 +0000
316 316 files: a
317 317 description:
318 318 module: summary
319 319
320 320 description
321 321
322 322
323 323 % --- in commit message
324 324 applying a.patch
325 325 changeset: 0:f34d9187897d
326 326 tag: tip
327 327 user: test
328 328 date: Thu Jan 01 00:00:00 1970 +0000
329 329 files: a
330 330 description:
331 331 module: summary
332 332
333 333
334 334 % tricky header splitting
335 335 applying ../trickyheaders.patch
336 336 # HG changeset patch
337 337 # User User B
338 338 # Date 0 0
339 339 # Node ID eb56ab91903632294ac504838508cb370c0901d2
340 340 # Parent 0000000000000000000000000000000000000000
341 341 from: tricky!
342 342
343 343 That is not a header.
344 344
345 345 diff --git a/foo b/foo
346 346 new file mode 100644
347 347 --- /dev/null
348 348 +++ b/foo
349 349 @@ -0,0 +1,1 @@
350 350 +foo
351 351 % issue2102
352 352 adding src/cmd/gc/mksys.bash
353 353 applying patch from stdin
354 354 parent: 1:d59915696727 tip
355 355 help management of empty pkg and lib directories in perforce
356 356 branch: default
357 357 commit: (clean)
358 358 update: (current)
359 359 diff --git a/lib/place-holder b/lib/place-holder
360 360 new file mode 100644
361 361 --- /dev/null
362 362 +++ b/lib/place-holder
363 363 @@ -0,0 +1,2 @@
364 364 +perforce does not maintain empty directories.
365 365 +this file helps.
366 366 diff --git a/pkg/place-holder b/pkg/place-holder
367 367 new file mode 100644
368 368 --- /dev/null
369 369 +++ b/pkg/place-holder
370 370 @@ -0,0 +1,2 @@
371 371 +perforce does not maintain empty directories.
372 372 +this file helps.
373 373 diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
374 374 old mode 100644
375 375 new mode 100755
376 % diff lines looking like headers
377 adding a
378 adding b
379 adding c
380 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
381 applying patch from stdin
General Comments 0
You need to be logged in to leave comments. Login now