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