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