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