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