##// END OF EJS Templates
patch: error out if reached to EOF while reading hunk...
Yuya Nishihara -
r37591:49b82cdb default
parent child Browse files
Show More
@@ -1,69 +1,77 b''
1 1 # diffhelpers.py - helper routines for patch
2 2 #
3 3 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 from .i18n import _
11
12 from . import (
13 error,
14 )
15
10 16 def addlines(fp, hunk, lena, lenb, a, b):
11 17 """Read lines from fp into the hunk
12 18
13 19 The hunk is parsed into two arrays, a and b. a gets the old state of
14 20 the text, b gets the new state. The control char from the hunk is saved
15 21 when inserting into a, but not b (for performance while deleting files.)
16 22 """
17 23 while True:
18 24 todoa = lena - len(a)
19 25 todob = lenb - len(b)
20 26 num = max(todoa, todob)
21 27 if num == 0:
22 28 break
23 29 for i in xrange(num):
24 30 s = fp.readline()
31 if not s:
32 raise error.ParseError(_('incomplete hunk'))
25 33 if s == "\\ No newline at end of file\n":
26 34 fixnewline(hunk, a, b)
27 35 continue
28 36 if s == "\n":
29 37 # Some patches may be missing the control char
30 38 # on empty lines. Supply a leading space.
31 39 s = " \n"
32 40 hunk.append(s)
33 41 if s.startswith('+'):
34 42 b.append(s[1:])
35 43 elif s.startswith('-'):
36 44 a.append(s)
37 45 else:
38 46 b.append(s[1:])
39 47 a.append(s)
40 48
41 49 def fixnewline(hunk, a, b):
42 50 """Fix up the last lines of a and b when the patch has no newline at EOF"""
43 51 l = hunk[-1]
44 52 # tolerate CRLF in last line
45 53 if l.endswith('\r\n'):
46 54 hline = l[:-2]
47 55 else:
48 56 hline = l[:-1]
49 57
50 58 if hline.startswith((' ', '+')):
51 59 b[-1] = hline[1:]
52 60 if hline.startswith((' ', '-')):
53 61 a[-1] = hline
54 62 hunk[-1] = hline
55 63
56 64 def testhunk(a, b, bstart):
57 65 """Compare the lines in a with the lines in b
58 66
59 67 a is assumed to have a control char at the start of each line, this char
60 68 is ignored in the compare.
61 69 """
62 70 alen = len(a)
63 71 blen = len(b)
64 72 if alen > blen - bstart:
65 73 return -1
66 74 for i in xrange(alen):
67 75 if a[i][1:] != b[i + bstart]:
68 76 return -1
69 77 return 0
@@ -1,2913 +1,2916 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import, print_function
10 10
11 11 import collections
12 12 import copy
13 13 import difflib
14 14 import email
15 15 import errno
16 16 import hashlib
17 17 import os
18 18 import posixpath
19 19 import re
20 20 import shutil
21 21 import tempfile
22 22 import zlib
23 23
24 24 from .i18n import _
25 25 from .node import (
26 26 hex,
27 27 short,
28 28 )
29 29 from . import (
30 30 copies,
31 31 diffhelpers,
32 32 encoding,
33 33 error,
34 34 mail,
35 35 mdiff,
36 36 pathutil,
37 37 pycompat,
38 38 scmutil,
39 39 similar,
40 40 util,
41 41 vfs as vfsmod,
42 42 )
43 43 from .utils import (
44 44 dateutil,
45 45 procutil,
46 46 stringutil,
47 47 )
48 48
49 49 stringio = util.stringio
50 50
51 51 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
52 52 tabsplitter = re.compile(br'(\t+|[^\t]+)')
53 53 _nonwordre = re.compile(br'([^a-zA-Z0-9_\x80-\xff])')
54 54
55 55 PatchError = error.PatchError
56 56
57 57 # public functions
58 58
59 59 def split(stream):
60 60 '''return an iterator of individual patches from a stream'''
61 61 def isheader(line, inheader):
62 62 if inheader and line.startswith((' ', '\t')):
63 63 # continuation
64 64 return True
65 65 if line.startswith((' ', '-', '+')):
66 66 # diff line - don't check for header pattern in there
67 67 return False
68 68 l = line.split(': ', 1)
69 69 return len(l) == 2 and ' ' not in l[0]
70 70
71 71 def chunk(lines):
72 72 return stringio(''.join(lines))
73 73
74 74 def hgsplit(stream, cur):
75 75 inheader = True
76 76
77 77 for line in stream:
78 78 if not line.strip():
79 79 inheader = False
80 80 if not inheader and line.startswith('# HG changeset patch'):
81 81 yield chunk(cur)
82 82 cur = []
83 83 inheader = True
84 84
85 85 cur.append(line)
86 86
87 87 if cur:
88 88 yield chunk(cur)
89 89
90 90 def mboxsplit(stream, cur):
91 91 for line in stream:
92 92 if line.startswith('From '):
93 93 for c in split(chunk(cur[1:])):
94 94 yield c
95 95 cur = []
96 96
97 97 cur.append(line)
98 98
99 99 if cur:
100 100 for c in split(chunk(cur[1:])):
101 101 yield c
102 102
103 103 def mimesplit(stream, cur):
104 104 def msgfp(m):
105 105 fp = stringio()
106 106 g = email.Generator.Generator(fp, mangle_from_=False)
107 107 g.flatten(m)
108 108 fp.seek(0)
109 109 return fp
110 110
111 111 for line in stream:
112 112 cur.append(line)
113 113 c = chunk(cur)
114 114
115 115 m = pycompat.emailparser().parse(c)
116 116 if not m.is_multipart():
117 117 yield msgfp(m)
118 118 else:
119 119 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
120 120 for part in m.walk():
121 121 ct = part.get_content_type()
122 122 if ct not in ok_types:
123 123 continue
124 124 yield msgfp(part)
125 125
126 126 def headersplit(stream, cur):
127 127 inheader = False
128 128
129 129 for line in stream:
130 130 if not inheader and isheader(line, inheader):
131 131 yield chunk(cur)
132 132 cur = []
133 133 inheader = True
134 134 if inheader and not isheader(line, inheader):
135 135 inheader = False
136 136
137 137 cur.append(line)
138 138
139 139 if cur:
140 140 yield chunk(cur)
141 141
142 142 def remainder(cur):
143 143 yield chunk(cur)
144 144
145 145 class fiter(object):
146 146 def __init__(self, fp):
147 147 self.fp = fp
148 148
149 149 def __iter__(self):
150 150 return self
151 151
152 152 def next(self):
153 153 l = self.fp.readline()
154 154 if not l:
155 155 raise StopIteration
156 156 return l
157 157
158 158 __next__ = next
159 159
160 160 inheader = False
161 161 cur = []
162 162
163 163 mimeheaders = ['content-type']
164 164
165 165 if not util.safehasattr(stream, 'next'):
166 166 # http responses, for example, have readline but not next
167 167 stream = fiter(stream)
168 168
169 169 for line in stream:
170 170 cur.append(line)
171 171 if line.startswith('# HG changeset patch'):
172 172 return hgsplit(stream, cur)
173 173 elif line.startswith('From '):
174 174 return mboxsplit(stream, cur)
175 175 elif isheader(line, inheader):
176 176 inheader = True
177 177 if line.split(':', 1)[0].lower() in mimeheaders:
178 178 # let email parser handle this
179 179 return mimesplit(stream, cur)
180 180 elif line.startswith('--- ') and inheader:
181 181 # No evil headers seen by diff start, split by hand
182 182 return headersplit(stream, cur)
183 183 # Not enough info, keep reading
184 184
185 185 # if we are here, we have a very plain patch
186 186 return remainder(cur)
187 187
188 188 ## Some facility for extensible patch parsing:
189 189 # list of pairs ("header to match", "data key")
190 190 patchheadermap = [('Date', 'date'),
191 191 ('Branch', 'branch'),
192 192 ('Node ID', 'nodeid'),
193 193 ]
194 194
195 195 def extract(ui, fileobj):
196 196 '''extract patch from data read from fileobj.
197 197
198 198 patch can be a normal patch or contained in an email message.
199 199
200 200 return a dictionary. Standard keys are:
201 201 - filename,
202 202 - message,
203 203 - user,
204 204 - date,
205 205 - branch,
206 206 - node,
207 207 - p1,
208 208 - p2.
209 209 Any item can be missing from the dictionary. If filename is missing,
210 210 fileobj did not contain a patch. Caller must unlink filename when done.'''
211 211
212 212 # attempt to detect the start of a patch
213 213 # (this heuristic is borrowed from quilt)
214 214 diffre = re.compile(br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
215 215 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
216 216 br'---[ \t].*?^\+\+\+[ \t]|'
217 217 br'\*\*\*[ \t].*?^---[ \t])',
218 218 re.MULTILINE | re.DOTALL)
219 219
220 220 data = {}
221 221 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
222 222 tmpfp = os.fdopen(fd, r'wb')
223 223 try:
224 224 msg = pycompat.emailparser().parse(fileobj)
225 225
226 226 subject = msg[r'Subject'] and mail.headdecode(msg[r'Subject'])
227 227 data['user'] = msg[r'From'] and mail.headdecode(msg[r'From'])
228 228 if not subject and not data['user']:
229 229 # Not an email, restore parsed headers if any
230 230 subject = '\n'.join(': '.join(map(encoding.strtolocal, h))
231 231 for h in msg.items()) + '\n'
232 232
233 233 # should try to parse msg['Date']
234 234 parents = []
235 235
236 236 if subject:
237 237 if subject.startswith('[PATCH'):
238 238 pend = subject.find(']')
239 239 if pend >= 0:
240 240 subject = subject[pend + 1:].lstrip()
241 241 subject = re.sub(br'\n[ \t]+', ' ', subject)
242 242 ui.debug('Subject: %s\n' % subject)
243 243 if data['user']:
244 244 ui.debug('From: %s\n' % data['user'])
245 245 diffs_seen = 0
246 246 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
247 247 message = ''
248 248 for part in msg.walk():
249 249 content_type = pycompat.bytestr(part.get_content_type())
250 250 ui.debug('Content-Type: %s\n' % content_type)
251 251 if content_type not in ok_types:
252 252 continue
253 253 payload = part.get_payload(decode=True)
254 254 m = diffre.search(payload)
255 255 if m:
256 256 hgpatch = False
257 257 hgpatchheader = False
258 258 ignoretext = False
259 259
260 260 ui.debug('found patch at byte %d\n' % m.start(0))
261 261 diffs_seen += 1
262 262 cfp = stringio()
263 263 for line in payload[:m.start(0)].splitlines():
264 264 if line.startswith('# HG changeset patch') and not hgpatch:
265 265 ui.debug('patch generated by hg export\n')
266 266 hgpatch = True
267 267 hgpatchheader = True
268 268 # drop earlier commit message content
269 269 cfp.seek(0)
270 270 cfp.truncate()
271 271 subject = None
272 272 elif hgpatchheader:
273 273 if line.startswith('# User '):
274 274 data['user'] = line[7:]
275 275 ui.debug('From: %s\n' % data['user'])
276 276 elif line.startswith("# Parent "):
277 277 parents.append(line[9:].lstrip())
278 278 elif line.startswith("# "):
279 279 for header, key in patchheadermap:
280 280 prefix = '# %s ' % header
281 281 if line.startswith(prefix):
282 282 data[key] = line[len(prefix):]
283 283 else:
284 284 hgpatchheader = False
285 285 elif line == '---':
286 286 ignoretext = True
287 287 if not hgpatchheader and not ignoretext:
288 288 cfp.write(line)
289 289 cfp.write('\n')
290 290 message = cfp.getvalue()
291 291 if tmpfp:
292 292 tmpfp.write(payload)
293 293 if not payload.endswith('\n'):
294 294 tmpfp.write('\n')
295 295 elif not diffs_seen and message and content_type == 'text/plain':
296 296 message += '\n' + payload
297 297 except: # re-raises
298 298 tmpfp.close()
299 299 os.unlink(tmpname)
300 300 raise
301 301
302 302 if subject and not message.startswith(subject):
303 303 message = '%s\n%s' % (subject, message)
304 304 data['message'] = message
305 305 tmpfp.close()
306 306 if parents:
307 307 data['p1'] = parents.pop(0)
308 308 if parents:
309 309 data['p2'] = parents.pop(0)
310 310
311 311 if diffs_seen:
312 312 data['filename'] = tmpname
313 313 else:
314 314 os.unlink(tmpname)
315 315 return data
316 316
317 317 class patchmeta(object):
318 318 """Patched file metadata
319 319
320 320 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
321 321 or COPY. 'path' is patched file path. 'oldpath' is set to the
322 322 origin file when 'op' is either COPY or RENAME, None otherwise. If
323 323 file mode is changed, 'mode' is a tuple (islink, isexec) where
324 324 'islink' is True if the file is a symlink and 'isexec' is True if
325 325 the file is executable. Otherwise, 'mode' is None.
326 326 """
327 327 def __init__(self, path):
328 328 self.path = path
329 329 self.oldpath = None
330 330 self.mode = None
331 331 self.op = 'MODIFY'
332 332 self.binary = False
333 333
334 334 def setmode(self, mode):
335 335 islink = mode & 0o20000
336 336 isexec = mode & 0o100
337 337 self.mode = (islink, isexec)
338 338
339 339 def copy(self):
340 340 other = patchmeta(self.path)
341 341 other.oldpath = self.oldpath
342 342 other.mode = self.mode
343 343 other.op = self.op
344 344 other.binary = self.binary
345 345 return other
346 346
347 347 def _ispatchinga(self, afile):
348 348 if afile == '/dev/null':
349 349 return self.op == 'ADD'
350 350 return afile == 'a/' + (self.oldpath or self.path)
351 351
352 352 def _ispatchingb(self, bfile):
353 353 if bfile == '/dev/null':
354 354 return self.op == 'DELETE'
355 355 return bfile == 'b/' + self.path
356 356
357 357 def ispatching(self, afile, bfile):
358 358 return self._ispatchinga(afile) and self._ispatchingb(bfile)
359 359
360 360 def __repr__(self):
361 361 return "<patchmeta %s %r>" % (self.op, self.path)
362 362
363 363 def readgitpatch(lr):
364 364 """extract git-style metadata about patches from <patchname>"""
365 365
366 366 # Filter patch for git information
367 367 gp = None
368 368 gitpatches = []
369 369 for line in lr:
370 370 line = line.rstrip(' \r\n')
371 371 if line.startswith('diff --git a/'):
372 372 m = gitre.match(line)
373 373 if m:
374 374 if gp:
375 375 gitpatches.append(gp)
376 376 dst = m.group(2)
377 377 gp = patchmeta(dst)
378 378 elif gp:
379 379 if line.startswith('--- '):
380 380 gitpatches.append(gp)
381 381 gp = None
382 382 continue
383 383 if line.startswith('rename from '):
384 384 gp.op = 'RENAME'
385 385 gp.oldpath = line[12:]
386 386 elif line.startswith('rename to '):
387 387 gp.path = line[10:]
388 388 elif line.startswith('copy from '):
389 389 gp.op = 'COPY'
390 390 gp.oldpath = line[10:]
391 391 elif line.startswith('copy to '):
392 392 gp.path = line[8:]
393 393 elif line.startswith('deleted file'):
394 394 gp.op = 'DELETE'
395 395 elif line.startswith('new file mode '):
396 396 gp.op = 'ADD'
397 397 gp.setmode(int(line[-6:], 8))
398 398 elif line.startswith('new mode '):
399 399 gp.setmode(int(line[-6:], 8))
400 400 elif line.startswith('GIT binary patch'):
401 401 gp.binary = True
402 402 if gp:
403 403 gitpatches.append(gp)
404 404
405 405 return gitpatches
406 406
407 407 class linereader(object):
408 408 # simple class to allow pushing lines back into the input stream
409 409 def __init__(self, fp):
410 410 self.fp = fp
411 411 self.buf = []
412 412
413 413 def push(self, line):
414 414 if line is not None:
415 415 self.buf.append(line)
416 416
417 417 def readline(self):
418 418 if self.buf:
419 419 l = self.buf[0]
420 420 del self.buf[0]
421 421 return l
422 422 return self.fp.readline()
423 423
424 424 def __iter__(self):
425 425 return iter(self.readline, '')
426 426
427 427 class abstractbackend(object):
428 428 def __init__(self, ui):
429 429 self.ui = ui
430 430
431 431 def getfile(self, fname):
432 432 """Return target file data and flags as a (data, (islink,
433 433 isexec)) tuple. Data is None if file is missing/deleted.
434 434 """
435 435 raise NotImplementedError
436 436
437 437 def setfile(self, fname, data, mode, copysource):
438 438 """Write data to target file fname and set its mode. mode is a
439 439 (islink, isexec) tuple. If data is None, the file content should
440 440 be left unchanged. If the file is modified after being copied,
441 441 copysource is set to the original file name.
442 442 """
443 443 raise NotImplementedError
444 444
445 445 def unlink(self, fname):
446 446 """Unlink target file."""
447 447 raise NotImplementedError
448 448
449 449 def writerej(self, fname, failed, total, lines):
450 450 """Write rejected lines for fname. total is the number of hunks
451 451 which failed to apply and total the total number of hunks for this
452 452 files.
453 453 """
454 454
455 455 def exists(self, fname):
456 456 raise NotImplementedError
457 457
458 458 def close(self):
459 459 raise NotImplementedError
460 460
461 461 class fsbackend(abstractbackend):
462 462 def __init__(self, ui, basedir):
463 463 super(fsbackend, self).__init__(ui)
464 464 self.opener = vfsmod.vfs(basedir)
465 465
466 466 def getfile(self, fname):
467 467 if self.opener.islink(fname):
468 468 return (self.opener.readlink(fname), (True, False))
469 469
470 470 isexec = False
471 471 try:
472 472 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
473 473 except OSError as e:
474 474 if e.errno != errno.ENOENT:
475 475 raise
476 476 try:
477 477 return (self.opener.read(fname), (False, isexec))
478 478 except IOError as e:
479 479 if e.errno != errno.ENOENT:
480 480 raise
481 481 return None, None
482 482
483 483 def setfile(self, fname, data, mode, copysource):
484 484 islink, isexec = mode
485 485 if data is None:
486 486 self.opener.setflags(fname, islink, isexec)
487 487 return
488 488 if islink:
489 489 self.opener.symlink(data, fname)
490 490 else:
491 491 self.opener.write(fname, data)
492 492 if isexec:
493 493 self.opener.setflags(fname, False, True)
494 494
495 495 def unlink(self, fname):
496 496 self.opener.unlinkpath(fname, ignoremissing=True)
497 497
498 498 def writerej(self, fname, failed, total, lines):
499 499 fname = fname + ".rej"
500 500 self.ui.warn(
501 501 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
502 502 (failed, total, fname))
503 503 fp = self.opener(fname, 'w')
504 504 fp.writelines(lines)
505 505 fp.close()
506 506
507 507 def exists(self, fname):
508 508 return self.opener.lexists(fname)
509 509
510 510 class workingbackend(fsbackend):
511 511 def __init__(self, ui, repo, similarity):
512 512 super(workingbackend, self).__init__(ui, repo.root)
513 513 self.repo = repo
514 514 self.similarity = similarity
515 515 self.removed = set()
516 516 self.changed = set()
517 517 self.copied = []
518 518
519 519 def _checkknown(self, fname):
520 520 if self.repo.dirstate[fname] == '?' and self.exists(fname):
521 521 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
522 522
523 523 def setfile(self, fname, data, mode, copysource):
524 524 self._checkknown(fname)
525 525 super(workingbackend, self).setfile(fname, data, mode, copysource)
526 526 if copysource is not None:
527 527 self.copied.append((copysource, fname))
528 528 self.changed.add(fname)
529 529
530 530 def unlink(self, fname):
531 531 self._checkknown(fname)
532 532 super(workingbackend, self).unlink(fname)
533 533 self.removed.add(fname)
534 534 self.changed.add(fname)
535 535
536 536 def close(self):
537 537 wctx = self.repo[None]
538 538 changed = set(self.changed)
539 539 for src, dst in self.copied:
540 540 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
541 541 if self.removed:
542 542 wctx.forget(sorted(self.removed))
543 543 for f in self.removed:
544 544 if f not in self.repo.dirstate:
545 545 # File was deleted and no longer belongs to the
546 546 # dirstate, it was probably marked added then
547 547 # deleted, and should not be considered by
548 548 # marktouched().
549 549 changed.discard(f)
550 550 if changed:
551 551 scmutil.marktouched(self.repo, changed, self.similarity)
552 552 return sorted(self.changed)
553 553
554 554 class filestore(object):
555 555 def __init__(self, maxsize=None):
556 556 self.opener = None
557 557 self.files = {}
558 558 self.created = 0
559 559 self.maxsize = maxsize
560 560 if self.maxsize is None:
561 561 self.maxsize = 4*(2**20)
562 562 self.size = 0
563 563 self.data = {}
564 564
565 565 def setfile(self, fname, data, mode, copied=None):
566 566 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
567 567 self.data[fname] = (data, mode, copied)
568 568 self.size += len(data)
569 569 else:
570 570 if self.opener is None:
571 571 root = tempfile.mkdtemp(prefix='hg-patch-')
572 572 self.opener = vfsmod.vfs(root)
573 573 # Avoid filename issues with these simple names
574 574 fn = '%d' % self.created
575 575 self.opener.write(fn, data)
576 576 self.created += 1
577 577 self.files[fname] = (fn, mode, copied)
578 578
579 579 def getfile(self, fname):
580 580 if fname in self.data:
581 581 return self.data[fname]
582 582 if not self.opener or fname not in self.files:
583 583 return None, None, None
584 584 fn, mode, copied = self.files[fname]
585 585 return self.opener.read(fn), mode, copied
586 586
587 587 def close(self):
588 588 if self.opener:
589 589 shutil.rmtree(self.opener.base)
590 590
591 591 class repobackend(abstractbackend):
592 592 def __init__(self, ui, repo, ctx, store):
593 593 super(repobackend, self).__init__(ui)
594 594 self.repo = repo
595 595 self.ctx = ctx
596 596 self.store = store
597 597 self.changed = set()
598 598 self.removed = set()
599 599 self.copied = {}
600 600
601 601 def _checkknown(self, fname):
602 602 if fname not in self.ctx:
603 603 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
604 604
605 605 def getfile(self, fname):
606 606 try:
607 607 fctx = self.ctx[fname]
608 608 except error.LookupError:
609 609 return None, None
610 610 flags = fctx.flags()
611 611 return fctx.data(), ('l' in flags, 'x' in flags)
612 612
613 613 def setfile(self, fname, data, mode, copysource):
614 614 if copysource:
615 615 self._checkknown(copysource)
616 616 if data is None:
617 617 data = self.ctx[fname].data()
618 618 self.store.setfile(fname, data, mode, copysource)
619 619 self.changed.add(fname)
620 620 if copysource:
621 621 self.copied[fname] = copysource
622 622
623 623 def unlink(self, fname):
624 624 self._checkknown(fname)
625 625 self.removed.add(fname)
626 626
627 627 def exists(self, fname):
628 628 return fname in self.ctx
629 629
630 630 def close(self):
631 631 return self.changed | self.removed
632 632
633 633 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
634 634 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
635 635 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
636 636 eolmodes = ['strict', 'crlf', 'lf', 'auto']
637 637
638 638 class patchfile(object):
639 639 def __init__(self, ui, gp, backend, store, eolmode='strict'):
640 640 self.fname = gp.path
641 641 self.eolmode = eolmode
642 642 self.eol = None
643 643 self.backend = backend
644 644 self.ui = ui
645 645 self.lines = []
646 646 self.exists = False
647 647 self.missing = True
648 648 self.mode = gp.mode
649 649 self.copysource = gp.oldpath
650 650 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
651 651 self.remove = gp.op == 'DELETE'
652 652 if self.copysource is None:
653 653 data, mode = backend.getfile(self.fname)
654 654 else:
655 655 data, mode = store.getfile(self.copysource)[:2]
656 656 if data is not None:
657 657 self.exists = self.copysource is None or backend.exists(self.fname)
658 658 self.missing = False
659 659 if data:
660 660 self.lines = mdiff.splitnewlines(data)
661 661 if self.mode is None:
662 662 self.mode = mode
663 663 if self.lines:
664 664 # Normalize line endings
665 665 if self.lines[0].endswith('\r\n'):
666 666 self.eol = '\r\n'
667 667 elif self.lines[0].endswith('\n'):
668 668 self.eol = '\n'
669 669 if eolmode != 'strict':
670 670 nlines = []
671 671 for l in self.lines:
672 672 if l.endswith('\r\n'):
673 673 l = l[:-2] + '\n'
674 674 nlines.append(l)
675 675 self.lines = nlines
676 676 else:
677 677 if self.create:
678 678 self.missing = False
679 679 if self.mode is None:
680 680 self.mode = (False, False)
681 681 if self.missing:
682 682 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
683 683 self.ui.warn(_("(use '--prefix' to apply patch relative to the "
684 684 "current directory)\n"))
685 685
686 686 self.hash = {}
687 687 self.dirty = 0
688 688 self.offset = 0
689 689 self.skew = 0
690 690 self.rej = []
691 691 self.fileprinted = False
692 692 self.printfile(False)
693 693 self.hunks = 0
694 694
695 695 def writelines(self, fname, lines, mode):
696 696 if self.eolmode == 'auto':
697 697 eol = self.eol
698 698 elif self.eolmode == 'crlf':
699 699 eol = '\r\n'
700 700 else:
701 701 eol = '\n'
702 702
703 703 if self.eolmode != 'strict' and eol and eol != '\n':
704 704 rawlines = []
705 705 for l in lines:
706 706 if l and l[-1] == '\n':
707 707 l = l[:-1] + eol
708 708 rawlines.append(l)
709 709 lines = rawlines
710 710
711 711 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
712 712
713 713 def printfile(self, warn):
714 714 if self.fileprinted:
715 715 return
716 716 if warn or self.ui.verbose:
717 717 self.fileprinted = True
718 718 s = _("patching file %s\n") % self.fname
719 719 if warn:
720 720 self.ui.warn(s)
721 721 else:
722 722 self.ui.note(s)
723 723
724 724
725 725 def findlines(self, l, linenum):
726 726 # looks through the hash and finds candidate lines. The
727 727 # result is a list of line numbers sorted based on distance
728 728 # from linenum
729 729
730 730 cand = self.hash.get(l, [])
731 731 if len(cand) > 1:
732 732 # resort our list of potentials forward then back.
733 733 cand.sort(key=lambda x: abs(x - linenum))
734 734 return cand
735 735
736 736 def write_rej(self):
737 737 # our rejects are a little different from patch(1). This always
738 738 # creates rejects in the same form as the original patch. A file
739 739 # header is inserted so that you can run the reject through patch again
740 740 # without having to type the filename.
741 741 if not self.rej:
742 742 return
743 743 base = os.path.basename(self.fname)
744 744 lines = ["--- %s\n+++ %s\n" % (base, base)]
745 745 for x in self.rej:
746 746 for l in x.hunk:
747 747 lines.append(l)
748 748 if l[-1:] != '\n':
749 749 lines.append("\n\ No newline at end of file\n")
750 750 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
751 751
752 752 def apply(self, h):
753 753 if not h.complete():
754 754 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
755 755 (h.number, h.desc, len(h.a), h.lena, len(h.b),
756 756 h.lenb))
757 757
758 758 self.hunks += 1
759 759
760 760 if self.missing:
761 761 self.rej.append(h)
762 762 return -1
763 763
764 764 if self.exists and self.create:
765 765 if self.copysource:
766 766 self.ui.warn(_("cannot create %s: destination already "
767 767 "exists\n") % self.fname)
768 768 else:
769 769 self.ui.warn(_("file %s already exists\n") % self.fname)
770 770 self.rej.append(h)
771 771 return -1
772 772
773 773 if isinstance(h, binhunk):
774 774 if self.remove:
775 775 self.backend.unlink(self.fname)
776 776 else:
777 777 l = h.new(self.lines)
778 778 self.lines[:] = l
779 779 self.offset += len(l)
780 780 self.dirty = True
781 781 return 0
782 782
783 783 horig = h
784 784 if (self.eolmode in ('crlf', 'lf')
785 785 or self.eolmode == 'auto' and self.eol):
786 786 # If new eols are going to be normalized, then normalize
787 787 # hunk data before patching. Otherwise, preserve input
788 788 # line-endings.
789 789 h = h.getnormalized()
790 790
791 791 # fast case first, no offsets, no fuzz
792 792 old, oldstart, new, newstart = h.fuzzit(0, False)
793 793 oldstart += self.offset
794 794 orig_start = oldstart
795 795 # if there's skew we want to emit the "(offset %d lines)" even
796 796 # when the hunk cleanly applies at start + skew, so skip the
797 797 # fast case code
798 798 if (self.skew == 0 and
799 799 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
800 800 if self.remove:
801 801 self.backend.unlink(self.fname)
802 802 else:
803 803 self.lines[oldstart:oldstart + len(old)] = new
804 804 self.offset += len(new) - len(old)
805 805 self.dirty = True
806 806 return 0
807 807
808 808 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
809 809 self.hash = {}
810 810 for x, s in enumerate(self.lines):
811 811 self.hash.setdefault(s, []).append(x)
812 812
813 813 for fuzzlen in xrange(self.ui.configint("patch", "fuzz") + 1):
814 814 for toponly in [True, False]:
815 815 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
816 816 oldstart = oldstart + self.offset + self.skew
817 817 oldstart = min(oldstart, len(self.lines))
818 818 if old:
819 819 cand = self.findlines(old[0][1:], oldstart)
820 820 else:
821 821 # Only adding lines with no or fuzzed context, just
822 822 # take the skew in account
823 823 cand = [oldstart]
824 824
825 825 for l in cand:
826 826 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
827 827 self.lines[l : l + len(old)] = new
828 828 self.offset += len(new) - len(old)
829 829 self.skew = l - orig_start
830 830 self.dirty = True
831 831 offset = l - orig_start - fuzzlen
832 832 if fuzzlen:
833 833 msg = _("Hunk #%d succeeded at %d "
834 834 "with fuzz %d "
835 835 "(offset %d lines).\n")
836 836 self.printfile(True)
837 837 self.ui.warn(msg %
838 838 (h.number, l + 1, fuzzlen, offset))
839 839 else:
840 840 msg = _("Hunk #%d succeeded at %d "
841 841 "(offset %d lines).\n")
842 842 self.ui.note(msg % (h.number, l + 1, offset))
843 843 return fuzzlen
844 844 self.printfile(True)
845 845 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
846 846 self.rej.append(horig)
847 847 return -1
848 848
849 849 def close(self):
850 850 if self.dirty:
851 851 self.writelines(self.fname, self.lines, self.mode)
852 852 self.write_rej()
853 853 return len(self.rej)
854 854
855 855 class header(object):
856 856 """patch header
857 857 """
858 858 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
859 859 diff_re = re.compile('diff -r .* (.*)$')
860 860 allhunks_re = re.compile('(?:index|deleted file) ')
861 861 pretty_re = re.compile('(?:new file|deleted file) ')
862 862 special_re = re.compile('(?:index|deleted|copy|rename) ')
863 863 newfile_re = re.compile('(?:new file)')
864 864
865 865 def __init__(self, header):
866 866 self.header = header
867 867 self.hunks = []
868 868
869 869 def binary(self):
870 870 return any(h.startswith('index ') for h in self.header)
871 871
872 872 def pretty(self, fp):
873 873 for h in self.header:
874 874 if h.startswith('index '):
875 875 fp.write(_('this modifies a binary file (all or nothing)\n'))
876 876 break
877 877 if self.pretty_re.match(h):
878 878 fp.write(h)
879 879 if self.binary():
880 880 fp.write(_('this is a binary file\n'))
881 881 break
882 882 if h.startswith('---'):
883 883 fp.write(_('%d hunks, %d lines changed\n') %
884 884 (len(self.hunks),
885 885 sum([max(h.added, h.removed) for h in self.hunks])))
886 886 break
887 887 fp.write(h)
888 888
889 889 def write(self, fp):
890 890 fp.write(''.join(self.header))
891 891
892 892 def allhunks(self):
893 893 return any(self.allhunks_re.match(h) for h in self.header)
894 894
895 895 def files(self):
896 896 match = self.diffgit_re.match(self.header[0])
897 897 if match:
898 898 fromfile, tofile = match.groups()
899 899 if fromfile == tofile:
900 900 return [fromfile]
901 901 return [fromfile, tofile]
902 902 else:
903 903 return self.diff_re.match(self.header[0]).groups()
904 904
905 905 def filename(self):
906 906 return self.files()[-1]
907 907
908 908 def __repr__(self):
909 909 return '<header %s>' % (' '.join(map(repr, self.files())))
910 910
911 911 def isnewfile(self):
912 912 return any(self.newfile_re.match(h) for h in self.header)
913 913
914 914 def special(self):
915 915 # Special files are shown only at the header level and not at the hunk
916 916 # level for example a file that has been deleted is a special file.
917 917 # The user cannot change the content of the operation, in the case of
918 918 # the deleted file he has to take the deletion or not take it, he
919 919 # cannot take some of it.
920 920 # Newly added files are special if they are empty, they are not special
921 921 # if they have some content as we want to be able to change it
922 922 nocontent = len(self.header) == 2
923 923 emptynewfile = self.isnewfile() and nocontent
924 924 return emptynewfile or \
925 925 any(self.special_re.match(h) for h in self.header)
926 926
927 927 class recordhunk(object):
928 928 """patch hunk
929 929
930 930 XXX shouldn't we merge this with the other hunk class?
931 931 """
932 932
933 933 def __init__(self, header, fromline, toline, proc, before, hunk, after,
934 934 maxcontext=None):
935 935 def trimcontext(lines, reverse=False):
936 936 if maxcontext is not None:
937 937 delta = len(lines) - maxcontext
938 938 if delta > 0:
939 939 if reverse:
940 940 return delta, lines[delta:]
941 941 else:
942 942 return delta, lines[:maxcontext]
943 943 return 0, lines
944 944
945 945 self.header = header
946 946 trimedbefore, self.before = trimcontext(before, True)
947 947 self.fromline = fromline + trimedbefore
948 948 self.toline = toline + trimedbefore
949 949 _trimedafter, self.after = trimcontext(after, False)
950 950 self.proc = proc
951 951 self.hunk = hunk
952 952 self.added, self.removed = self.countchanges(self.hunk)
953 953
954 954 def __eq__(self, v):
955 955 if not isinstance(v, recordhunk):
956 956 return False
957 957
958 958 return ((v.hunk == self.hunk) and
959 959 (v.proc == self.proc) and
960 960 (self.fromline == v.fromline) and
961 961 (self.header.files() == v.header.files()))
962 962
963 963 def __hash__(self):
964 964 return hash((tuple(self.hunk),
965 965 tuple(self.header.files()),
966 966 self.fromline,
967 967 self.proc))
968 968
969 969 def countchanges(self, hunk):
970 970 """hunk -> (n+,n-)"""
971 971 add = len([h for h in hunk if h.startswith('+')])
972 972 rem = len([h for h in hunk if h.startswith('-')])
973 973 return add, rem
974 974
975 975 def reversehunk(self):
976 976 """return another recordhunk which is the reverse of the hunk
977 977
978 978 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
979 979 that, swap fromline/toline and +/- signs while keep other things
980 980 unchanged.
981 981 """
982 982 m = {'+': '-', '-': '+', '\\': '\\'}
983 983 hunk = ['%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
984 984 return recordhunk(self.header, self.toline, self.fromline, self.proc,
985 985 self.before, hunk, self.after)
986 986
987 987 def write(self, fp):
988 988 delta = len(self.before) + len(self.after)
989 989 if self.after and self.after[-1] == '\\ No newline at end of file\n':
990 990 delta -= 1
991 991 fromlen = delta + self.removed
992 992 tolen = delta + self.added
993 993 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
994 994 (self.fromline, fromlen, self.toline, tolen,
995 995 self.proc and (' ' + self.proc)))
996 996 fp.write(''.join(self.before + self.hunk + self.after))
997 997
998 998 pretty = write
999 999
1000 1000 def filename(self):
1001 1001 return self.header.filename()
1002 1002
1003 1003 def __repr__(self):
1004 1004 return '<hunk %r@%d>' % (self.filename(), self.fromline)
1005 1005
1006 1006 def getmessages():
1007 1007 return {
1008 1008 'multiple': {
1009 1009 'apply': _("apply change %d/%d to '%s'?"),
1010 1010 'discard': _("discard change %d/%d to '%s'?"),
1011 1011 'record': _("record change %d/%d to '%s'?"),
1012 1012 },
1013 1013 'single': {
1014 1014 'apply': _("apply this change to '%s'?"),
1015 1015 'discard': _("discard this change to '%s'?"),
1016 1016 'record': _("record this change to '%s'?"),
1017 1017 },
1018 1018 'help': {
1019 1019 'apply': _('[Ynesfdaq?]'
1020 1020 '$$ &Yes, apply this change'
1021 1021 '$$ &No, skip this change'
1022 1022 '$$ &Edit this change manually'
1023 1023 '$$ &Skip remaining changes to this file'
1024 1024 '$$ Apply remaining changes to this &file'
1025 1025 '$$ &Done, skip remaining changes and files'
1026 1026 '$$ Apply &all changes to all remaining files'
1027 1027 '$$ &Quit, applying no changes'
1028 1028 '$$ &? (display help)'),
1029 1029 'discard': _('[Ynesfdaq?]'
1030 1030 '$$ &Yes, discard this change'
1031 1031 '$$ &No, skip this change'
1032 1032 '$$ &Edit this change manually'
1033 1033 '$$ &Skip remaining changes to this file'
1034 1034 '$$ Discard remaining changes to this &file'
1035 1035 '$$ &Done, skip remaining changes and files'
1036 1036 '$$ Discard &all changes to all remaining files'
1037 1037 '$$ &Quit, discarding no changes'
1038 1038 '$$ &? (display help)'),
1039 1039 'record': _('[Ynesfdaq?]'
1040 1040 '$$ &Yes, record this change'
1041 1041 '$$ &No, skip this change'
1042 1042 '$$ &Edit this change manually'
1043 1043 '$$ &Skip remaining changes to this file'
1044 1044 '$$ Record remaining changes to this &file'
1045 1045 '$$ &Done, skip remaining changes and files'
1046 1046 '$$ Record &all changes to all remaining files'
1047 1047 '$$ &Quit, recording no changes'
1048 1048 '$$ &? (display help)'),
1049 1049 }
1050 1050 }
1051 1051
1052 1052 def filterpatch(ui, headers, operation=None):
1053 1053 """Interactively filter patch chunks into applied-only chunks"""
1054 1054 messages = getmessages()
1055 1055
1056 1056 if operation is None:
1057 1057 operation = 'record'
1058 1058
1059 1059 def prompt(skipfile, skipall, query, chunk):
1060 1060 """prompt query, and process base inputs
1061 1061
1062 1062 - y/n for the rest of file
1063 1063 - y/n for the rest
1064 1064 - ? (help)
1065 1065 - q (quit)
1066 1066
1067 1067 Return True/False and possibly updated skipfile and skipall.
1068 1068 """
1069 1069 newpatches = None
1070 1070 if skipall is not None:
1071 1071 return skipall, skipfile, skipall, newpatches
1072 1072 if skipfile is not None:
1073 1073 return skipfile, skipfile, skipall, newpatches
1074 1074 while True:
1075 1075 resps = messages['help'][operation]
1076 1076 r = ui.promptchoice("%s %s" % (query, resps))
1077 1077 ui.write("\n")
1078 1078 if r == 8: # ?
1079 1079 for c, t in ui.extractchoices(resps)[1]:
1080 1080 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1081 1081 continue
1082 1082 elif r == 0: # yes
1083 1083 ret = True
1084 1084 elif r == 1: # no
1085 1085 ret = False
1086 1086 elif r == 2: # Edit patch
1087 1087 if chunk is None:
1088 1088 ui.write(_('cannot edit patch for whole file'))
1089 1089 ui.write("\n")
1090 1090 continue
1091 1091 if chunk.header.binary():
1092 1092 ui.write(_('cannot edit patch for binary file'))
1093 1093 ui.write("\n")
1094 1094 continue
1095 1095 # Patch comment based on the Git one (based on comment at end of
1096 1096 # https://mercurial-scm.org/wiki/RecordExtension)
1097 1097 phelp = '---' + _("""
1098 1098 To remove '-' lines, make them ' ' lines (context).
1099 1099 To remove '+' lines, delete them.
1100 1100 Lines starting with # will be removed from the patch.
1101 1101
1102 1102 If the patch applies cleanly, the edited hunk will immediately be
1103 1103 added to the record list. If it does not apply cleanly, a rejects
1104 1104 file will be generated: you can use that when you try again. If
1105 1105 all lines of the hunk are removed, then the edit is aborted and
1106 1106 the hunk is left unchanged.
1107 1107 """)
1108 1108 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1109 1109 suffix=".diff")
1110 1110 ncpatchfp = None
1111 1111 try:
1112 1112 # Write the initial patch
1113 1113 f = util.nativeeolwriter(os.fdopen(patchfd, r'wb'))
1114 1114 chunk.header.write(f)
1115 1115 chunk.write(f)
1116 1116 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1117 1117 f.close()
1118 1118 # Start the editor and wait for it to complete
1119 1119 editor = ui.geteditor()
1120 1120 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1121 1121 environ={'HGUSER': ui.username()},
1122 1122 blockedtag='filterpatch')
1123 1123 if ret != 0:
1124 1124 ui.warn(_("editor exited with exit code %d\n") % ret)
1125 1125 continue
1126 1126 # Remove comment lines
1127 1127 patchfp = open(patchfn, r'rb')
1128 1128 ncpatchfp = stringio()
1129 1129 for line in util.iterfile(patchfp):
1130 1130 line = util.fromnativeeol(line)
1131 1131 if not line.startswith('#'):
1132 1132 ncpatchfp.write(line)
1133 1133 patchfp.close()
1134 1134 ncpatchfp.seek(0)
1135 1135 newpatches = parsepatch(ncpatchfp)
1136 1136 finally:
1137 1137 os.unlink(patchfn)
1138 1138 del ncpatchfp
1139 1139 # Signal that the chunk shouldn't be applied as-is, but
1140 1140 # provide the new patch to be used instead.
1141 1141 ret = False
1142 1142 elif r == 3: # Skip
1143 1143 ret = skipfile = False
1144 1144 elif r == 4: # file (Record remaining)
1145 1145 ret = skipfile = True
1146 1146 elif r == 5: # done, skip remaining
1147 1147 ret = skipall = False
1148 1148 elif r == 6: # all
1149 1149 ret = skipall = True
1150 1150 elif r == 7: # quit
1151 1151 raise error.Abort(_('user quit'))
1152 1152 return ret, skipfile, skipall, newpatches
1153 1153
1154 1154 seen = set()
1155 1155 applied = {} # 'filename' -> [] of chunks
1156 1156 skipfile, skipall = None, None
1157 1157 pos, total = 1, sum(len(h.hunks) for h in headers)
1158 1158 for h in headers:
1159 1159 pos += len(h.hunks)
1160 1160 skipfile = None
1161 1161 fixoffset = 0
1162 1162 hdr = ''.join(h.header)
1163 1163 if hdr in seen:
1164 1164 continue
1165 1165 seen.add(hdr)
1166 1166 if skipall is None:
1167 1167 h.pretty(ui)
1168 1168 msg = (_('examine changes to %s?') %
1169 1169 _(' and ').join("'%s'" % f for f in h.files()))
1170 1170 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1171 1171 if not r:
1172 1172 continue
1173 1173 applied[h.filename()] = [h]
1174 1174 if h.allhunks():
1175 1175 applied[h.filename()] += h.hunks
1176 1176 continue
1177 1177 for i, chunk in enumerate(h.hunks):
1178 1178 if skipfile is None and skipall is None:
1179 1179 chunk.pretty(ui)
1180 1180 if total == 1:
1181 1181 msg = messages['single'][operation] % chunk.filename()
1182 1182 else:
1183 1183 idx = pos - len(h.hunks) + i
1184 1184 msg = messages['multiple'][operation] % (idx, total,
1185 1185 chunk.filename())
1186 1186 r, skipfile, skipall, newpatches = prompt(skipfile,
1187 1187 skipall, msg, chunk)
1188 1188 if r:
1189 1189 if fixoffset:
1190 1190 chunk = copy.copy(chunk)
1191 1191 chunk.toline += fixoffset
1192 1192 applied[chunk.filename()].append(chunk)
1193 1193 elif newpatches is not None:
1194 1194 for newpatch in newpatches:
1195 1195 for newhunk in newpatch.hunks:
1196 1196 if fixoffset:
1197 1197 newhunk.toline += fixoffset
1198 1198 applied[newhunk.filename()].append(newhunk)
1199 1199 else:
1200 1200 fixoffset += chunk.removed - chunk.added
1201 1201 return (sum([h for h in applied.itervalues()
1202 1202 if h[0].special() or len(h) > 1], []), {})
1203 1203 class hunk(object):
1204 1204 def __init__(self, desc, num, lr, context):
1205 1205 self.number = num
1206 1206 self.desc = desc
1207 1207 self.hunk = [desc]
1208 1208 self.a = []
1209 1209 self.b = []
1210 1210 self.starta = self.lena = None
1211 1211 self.startb = self.lenb = None
1212 1212 if lr is not None:
1213 1213 if context:
1214 1214 self.read_context_hunk(lr)
1215 1215 else:
1216 1216 self.read_unified_hunk(lr)
1217 1217
1218 1218 def getnormalized(self):
1219 1219 """Return a copy with line endings normalized to LF."""
1220 1220
1221 1221 def normalize(lines):
1222 1222 nlines = []
1223 1223 for line in lines:
1224 1224 if line.endswith('\r\n'):
1225 1225 line = line[:-2] + '\n'
1226 1226 nlines.append(line)
1227 1227 return nlines
1228 1228
1229 1229 # Dummy object, it is rebuilt manually
1230 1230 nh = hunk(self.desc, self.number, None, None)
1231 1231 nh.number = self.number
1232 1232 nh.desc = self.desc
1233 1233 nh.hunk = self.hunk
1234 1234 nh.a = normalize(self.a)
1235 1235 nh.b = normalize(self.b)
1236 1236 nh.starta = self.starta
1237 1237 nh.startb = self.startb
1238 1238 nh.lena = self.lena
1239 1239 nh.lenb = self.lenb
1240 1240 return nh
1241 1241
1242 1242 def read_unified_hunk(self, lr):
1243 1243 m = unidesc.match(self.desc)
1244 1244 if not m:
1245 1245 raise PatchError(_("bad hunk #%d") % self.number)
1246 1246 self.starta, self.lena, self.startb, self.lenb = m.groups()
1247 1247 if self.lena is None:
1248 1248 self.lena = 1
1249 1249 else:
1250 1250 self.lena = int(self.lena)
1251 1251 if self.lenb is None:
1252 1252 self.lenb = 1
1253 1253 else:
1254 1254 self.lenb = int(self.lenb)
1255 1255 self.starta = int(self.starta)
1256 1256 self.startb = int(self.startb)
1257 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1258 self.b)
1257 try:
1258 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb,
1259 self.a, self.b)
1260 except error.ParseError as e:
1261 raise PatchError(_("bad hunk #%d: %s") % (self.number, e))
1259 1262 # if we hit eof before finishing out the hunk, the last line will
1260 1263 # be zero length. Lets try to fix it up.
1261 1264 while len(self.hunk[-1]) == 0:
1262 1265 del self.hunk[-1]
1263 1266 del self.a[-1]
1264 1267 del self.b[-1]
1265 1268 self.lena -= 1
1266 1269 self.lenb -= 1
1267 1270 self._fixnewline(lr)
1268 1271
1269 1272 def read_context_hunk(self, lr):
1270 1273 self.desc = lr.readline()
1271 1274 m = contextdesc.match(self.desc)
1272 1275 if not m:
1273 1276 raise PatchError(_("bad hunk #%d") % self.number)
1274 1277 self.starta, aend = m.groups()
1275 1278 self.starta = int(self.starta)
1276 1279 if aend is None:
1277 1280 aend = self.starta
1278 1281 self.lena = int(aend) - self.starta
1279 1282 if self.starta:
1280 1283 self.lena += 1
1281 1284 for x in xrange(self.lena):
1282 1285 l = lr.readline()
1283 1286 if l.startswith('---'):
1284 1287 # lines addition, old block is empty
1285 1288 lr.push(l)
1286 1289 break
1287 1290 s = l[2:]
1288 1291 if l.startswith('- ') or l.startswith('! '):
1289 1292 u = '-' + s
1290 1293 elif l.startswith(' '):
1291 1294 u = ' ' + s
1292 1295 else:
1293 1296 raise PatchError(_("bad hunk #%d old text line %d") %
1294 1297 (self.number, x))
1295 1298 self.a.append(u)
1296 1299 self.hunk.append(u)
1297 1300
1298 1301 l = lr.readline()
1299 1302 if l.startswith('\ '):
1300 1303 s = self.a[-1][:-1]
1301 1304 self.a[-1] = s
1302 1305 self.hunk[-1] = s
1303 1306 l = lr.readline()
1304 1307 m = contextdesc.match(l)
1305 1308 if not m:
1306 1309 raise PatchError(_("bad hunk #%d") % self.number)
1307 1310 self.startb, bend = m.groups()
1308 1311 self.startb = int(self.startb)
1309 1312 if bend is None:
1310 1313 bend = self.startb
1311 1314 self.lenb = int(bend) - self.startb
1312 1315 if self.startb:
1313 1316 self.lenb += 1
1314 1317 hunki = 1
1315 1318 for x in xrange(self.lenb):
1316 1319 l = lr.readline()
1317 1320 if l.startswith('\ '):
1318 1321 # XXX: the only way to hit this is with an invalid line range.
1319 1322 # The no-eol marker is not counted in the line range, but I
1320 1323 # guess there are diff(1) out there which behave differently.
1321 1324 s = self.b[-1][:-1]
1322 1325 self.b[-1] = s
1323 1326 self.hunk[hunki - 1] = s
1324 1327 continue
1325 1328 if not l:
1326 1329 # line deletions, new block is empty and we hit EOF
1327 1330 lr.push(l)
1328 1331 break
1329 1332 s = l[2:]
1330 1333 if l.startswith('+ ') or l.startswith('! '):
1331 1334 u = '+' + s
1332 1335 elif l.startswith(' '):
1333 1336 u = ' ' + s
1334 1337 elif len(self.b) == 0:
1335 1338 # line deletions, new block is empty
1336 1339 lr.push(l)
1337 1340 break
1338 1341 else:
1339 1342 raise PatchError(_("bad hunk #%d old text line %d") %
1340 1343 (self.number, x))
1341 1344 self.b.append(s)
1342 1345 while True:
1343 1346 if hunki >= len(self.hunk):
1344 1347 h = ""
1345 1348 else:
1346 1349 h = self.hunk[hunki]
1347 1350 hunki += 1
1348 1351 if h == u:
1349 1352 break
1350 1353 elif h.startswith('-'):
1351 1354 continue
1352 1355 else:
1353 1356 self.hunk.insert(hunki - 1, u)
1354 1357 break
1355 1358
1356 1359 if not self.a:
1357 1360 # this happens when lines were only added to the hunk
1358 1361 for x in self.hunk:
1359 1362 if x.startswith('-') or x.startswith(' '):
1360 1363 self.a.append(x)
1361 1364 if not self.b:
1362 1365 # this happens when lines were only deleted from the hunk
1363 1366 for x in self.hunk:
1364 1367 if x.startswith('+') or x.startswith(' '):
1365 1368 self.b.append(x[1:])
1366 1369 # @@ -start,len +start,len @@
1367 1370 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1368 1371 self.startb, self.lenb)
1369 1372 self.hunk[0] = self.desc
1370 1373 self._fixnewline(lr)
1371 1374
1372 1375 def _fixnewline(self, lr):
1373 1376 l = lr.readline()
1374 1377 if l.startswith('\ '):
1375 1378 diffhelpers.fixnewline(self.hunk, self.a, self.b)
1376 1379 else:
1377 1380 lr.push(l)
1378 1381
1379 1382 def complete(self):
1380 1383 return len(self.a) == self.lena and len(self.b) == self.lenb
1381 1384
1382 1385 def _fuzzit(self, old, new, fuzz, toponly):
1383 1386 # this removes context lines from the top and bottom of list 'l'. It
1384 1387 # checks the hunk to make sure only context lines are removed, and then
1385 1388 # returns a new shortened list of lines.
1386 1389 fuzz = min(fuzz, len(old))
1387 1390 if fuzz:
1388 1391 top = 0
1389 1392 bot = 0
1390 1393 hlen = len(self.hunk)
1391 1394 for x in xrange(hlen - 1):
1392 1395 # the hunk starts with the @@ line, so use x+1
1393 1396 if self.hunk[x + 1].startswith(' '):
1394 1397 top += 1
1395 1398 else:
1396 1399 break
1397 1400 if not toponly:
1398 1401 for x in xrange(hlen - 1):
1399 1402 if self.hunk[hlen - bot - 1].startswith(' '):
1400 1403 bot += 1
1401 1404 else:
1402 1405 break
1403 1406
1404 1407 bot = min(fuzz, bot)
1405 1408 top = min(fuzz, top)
1406 1409 return old[top:len(old) - bot], new[top:len(new) - bot], top
1407 1410 return old, new, 0
1408 1411
1409 1412 def fuzzit(self, fuzz, toponly):
1410 1413 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1411 1414 oldstart = self.starta + top
1412 1415 newstart = self.startb + top
1413 1416 # zero length hunk ranges already have their start decremented
1414 1417 if self.lena and oldstart > 0:
1415 1418 oldstart -= 1
1416 1419 if self.lenb and newstart > 0:
1417 1420 newstart -= 1
1418 1421 return old, oldstart, new, newstart
1419 1422
1420 1423 class binhunk(object):
1421 1424 'A binary patch file.'
1422 1425 def __init__(self, lr, fname):
1423 1426 self.text = None
1424 1427 self.delta = False
1425 1428 self.hunk = ['GIT binary patch\n']
1426 1429 self._fname = fname
1427 1430 self._read(lr)
1428 1431
1429 1432 def complete(self):
1430 1433 return self.text is not None
1431 1434
1432 1435 def new(self, lines):
1433 1436 if self.delta:
1434 1437 return [applybindelta(self.text, ''.join(lines))]
1435 1438 return [self.text]
1436 1439
1437 1440 def _read(self, lr):
1438 1441 def getline(lr, hunk):
1439 1442 l = lr.readline()
1440 1443 hunk.append(l)
1441 1444 return l.rstrip('\r\n')
1442 1445
1443 1446 size = 0
1444 1447 while True:
1445 1448 line = getline(lr, self.hunk)
1446 1449 if not line:
1447 1450 raise PatchError(_('could not extract "%s" binary data')
1448 1451 % self._fname)
1449 1452 if line.startswith('literal '):
1450 1453 size = int(line[8:].rstrip())
1451 1454 break
1452 1455 if line.startswith('delta '):
1453 1456 size = int(line[6:].rstrip())
1454 1457 self.delta = True
1455 1458 break
1456 1459 dec = []
1457 1460 line = getline(lr, self.hunk)
1458 1461 while len(line) > 1:
1459 1462 l = line[0:1]
1460 1463 if l <= 'Z' and l >= 'A':
1461 1464 l = ord(l) - ord('A') + 1
1462 1465 else:
1463 1466 l = ord(l) - ord('a') + 27
1464 1467 try:
1465 1468 dec.append(util.b85decode(line[1:])[:l])
1466 1469 except ValueError as e:
1467 1470 raise PatchError(_('could not decode "%s" binary patch: %s')
1468 1471 % (self._fname, stringutil.forcebytestr(e)))
1469 1472 line = getline(lr, self.hunk)
1470 1473 text = zlib.decompress(''.join(dec))
1471 1474 if len(text) != size:
1472 1475 raise PatchError(_('"%s" length is %d bytes, should be %d')
1473 1476 % (self._fname, len(text), size))
1474 1477 self.text = text
1475 1478
1476 1479 def parsefilename(str):
1477 1480 # --- filename \t|space stuff
1478 1481 s = str[4:].rstrip('\r\n')
1479 1482 i = s.find('\t')
1480 1483 if i < 0:
1481 1484 i = s.find(' ')
1482 1485 if i < 0:
1483 1486 return s
1484 1487 return s[:i]
1485 1488
1486 1489 def reversehunks(hunks):
1487 1490 '''reverse the signs in the hunks given as argument
1488 1491
1489 1492 This function operates on hunks coming out of patch.filterpatch, that is
1490 1493 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1491 1494
1492 1495 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1493 1496 ... --- a/folder1/g
1494 1497 ... +++ b/folder1/g
1495 1498 ... @@ -1,7 +1,7 @@
1496 1499 ... +firstline
1497 1500 ... c
1498 1501 ... 1
1499 1502 ... 2
1500 1503 ... + 3
1501 1504 ... -4
1502 1505 ... 5
1503 1506 ... d
1504 1507 ... +lastline"""
1505 1508 >>> hunks = parsepatch([rawpatch])
1506 1509 >>> hunkscomingfromfilterpatch = []
1507 1510 >>> for h in hunks:
1508 1511 ... hunkscomingfromfilterpatch.append(h)
1509 1512 ... hunkscomingfromfilterpatch.extend(h.hunks)
1510 1513
1511 1514 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1512 1515 >>> from . import util
1513 1516 >>> fp = util.stringio()
1514 1517 >>> for c in reversedhunks:
1515 1518 ... c.write(fp)
1516 1519 >>> fp.seek(0) or None
1517 1520 >>> reversedpatch = fp.read()
1518 1521 >>> print(pycompat.sysstr(reversedpatch))
1519 1522 diff --git a/folder1/g b/folder1/g
1520 1523 --- a/folder1/g
1521 1524 +++ b/folder1/g
1522 1525 @@ -1,4 +1,3 @@
1523 1526 -firstline
1524 1527 c
1525 1528 1
1526 1529 2
1527 1530 @@ -2,6 +1,6 @@
1528 1531 c
1529 1532 1
1530 1533 2
1531 1534 - 3
1532 1535 +4
1533 1536 5
1534 1537 d
1535 1538 @@ -6,3 +5,2 @@
1536 1539 5
1537 1540 d
1538 1541 -lastline
1539 1542
1540 1543 '''
1541 1544
1542 1545 newhunks = []
1543 1546 for c in hunks:
1544 1547 if util.safehasattr(c, 'reversehunk'):
1545 1548 c = c.reversehunk()
1546 1549 newhunks.append(c)
1547 1550 return newhunks
1548 1551
1549 1552 def parsepatch(originalchunks, maxcontext=None):
1550 1553 """patch -> [] of headers -> [] of hunks
1551 1554
1552 1555 If maxcontext is not None, trim context lines if necessary.
1553 1556
1554 1557 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1555 1558 ... --- a/folder1/g
1556 1559 ... +++ b/folder1/g
1557 1560 ... @@ -1,8 +1,10 @@
1558 1561 ... 1
1559 1562 ... 2
1560 1563 ... -3
1561 1564 ... 4
1562 1565 ... 5
1563 1566 ... 6
1564 1567 ... +6.1
1565 1568 ... +6.2
1566 1569 ... 7
1567 1570 ... 8
1568 1571 ... +9'''
1569 1572 >>> out = util.stringio()
1570 1573 >>> headers = parsepatch([rawpatch], maxcontext=1)
1571 1574 >>> for header in headers:
1572 1575 ... header.write(out)
1573 1576 ... for hunk in header.hunks:
1574 1577 ... hunk.write(out)
1575 1578 >>> print(pycompat.sysstr(out.getvalue()))
1576 1579 diff --git a/folder1/g b/folder1/g
1577 1580 --- a/folder1/g
1578 1581 +++ b/folder1/g
1579 1582 @@ -2,3 +2,2 @@
1580 1583 2
1581 1584 -3
1582 1585 4
1583 1586 @@ -6,2 +5,4 @@
1584 1587 6
1585 1588 +6.1
1586 1589 +6.2
1587 1590 7
1588 1591 @@ -8,1 +9,2 @@
1589 1592 8
1590 1593 +9
1591 1594 """
1592 1595 class parser(object):
1593 1596 """patch parsing state machine"""
1594 1597 def __init__(self):
1595 1598 self.fromline = 0
1596 1599 self.toline = 0
1597 1600 self.proc = ''
1598 1601 self.header = None
1599 1602 self.context = []
1600 1603 self.before = []
1601 1604 self.hunk = []
1602 1605 self.headers = []
1603 1606
1604 1607 def addrange(self, limits):
1605 1608 fromstart, fromend, tostart, toend, proc = limits
1606 1609 self.fromline = int(fromstart)
1607 1610 self.toline = int(tostart)
1608 1611 self.proc = proc
1609 1612
1610 1613 def addcontext(self, context):
1611 1614 if self.hunk:
1612 1615 h = recordhunk(self.header, self.fromline, self.toline,
1613 1616 self.proc, self.before, self.hunk, context, maxcontext)
1614 1617 self.header.hunks.append(h)
1615 1618 self.fromline += len(self.before) + h.removed
1616 1619 self.toline += len(self.before) + h.added
1617 1620 self.before = []
1618 1621 self.hunk = []
1619 1622 self.context = context
1620 1623
1621 1624 def addhunk(self, hunk):
1622 1625 if self.context:
1623 1626 self.before = self.context
1624 1627 self.context = []
1625 1628 self.hunk = hunk
1626 1629
1627 1630 def newfile(self, hdr):
1628 1631 self.addcontext([])
1629 1632 h = header(hdr)
1630 1633 self.headers.append(h)
1631 1634 self.header = h
1632 1635
1633 1636 def addother(self, line):
1634 1637 pass # 'other' lines are ignored
1635 1638
1636 1639 def finished(self):
1637 1640 self.addcontext([])
1638 1641 return self.headers
1639 1642
1640 1643 transitions = {
1641 1644 'file': {'context': addcontext,
1642 1645 'file': newfile,
1643 1646 'hunk': addhunk,
1644 1647 'range': addrange},
1645 1648 'context': {'file': newfile,
1646 1649 'hunk': addhunk,
1647 1650 'range': addrange,
1648 1651 'other': addother},
1649 1652 'hunk': {'context': addcontext,
1650 1653 'file': newfile,
1651 1654 'range': addrange},
1652 1655 'range': {'context': addcontext,
1653 1656 'hunk': addhunk},
1654 1657 'other': {'other': addother},
1655 1658 }
1656 1659
1657 1660 p = parser()
1658 1661 fp = stringio()
1659 1662 fp.write(''.join(originalchunks))
1660 1663 fp.seek(0)
1661 1664
1662 1665 state = 'context'
1663 1666 for newstate, data in scanpatch(fp):
1664 1667 try:
1665 1668 p.transitions[state][newstate](p, data)
1666 1669 except KeyError:
1667 1670 raise PatchError('unhandled transition: %s -> %s' %
1668 1671 (state, newstate))
1669 1672 state = newstate
1670 1673 del fp
1671 1674 return p.finished()
1672 1675
1673 1676 def pathtransform(path, strip, prefix):
1674 1677 '''turn a path from a patch into a path suitable for the repository
1675 1678
1676 1679 prefix, if not empty, is expected to be normalized with a / at the end.
1677 1680
1678 1681 Returns (stripped components, path in repository).
1679 1682
1680 1683 >>> pathtransform(b'a/b/c', 0, b'')
1681 1684 ('', 'a/b/c')
1682 1685 >>> pathtransform(b' a/b/c ', 0, b'')
1683 1686 ('', ' a/b/c')
1684 1687 >>> pathtransform(b' a/b/c ', 2, b'')
1685 1688 ('a/b/', 'c')
1686 1689 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1687 1690 ('', 'd/e/a/b/c')
1688 1691 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1689 1692 ('a//b/', 'd/e/c')
1690 1693 >>> pathtransform(b'a/b/c', 3, b'')
1691 1694 Traceback (most recent call last):
1692 1695 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1693 1696 '''
1694 1697 pathlen = len(path)
1695 1698 i = 0
1696 1699 if strip == 0:
1697 1700 return '', prefix + path.rstrip()
1698 1701 count = strip
1699 1702 while count > 0:
1700 1703 i = path.find('/', i)
1701 1704 if i == -1:
1702 1705 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1703 1706 (count, strip, path))
1704 1707 i += 1
1705 1708 # consume '//' in the path
1706 1709 while i < pathlen - 1 and path[i:i + 1] == '/':
1707 1710 i += 1
1708 1711 count -= 1
1709 1712 return path[:i].lstrip(), prefix + path[i:].rstrip()
1710 1713
1711 1714 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1712 1715 nulla = afile_orig == "/dev/null"
1713 1716 nullb = bfile_orig == "/dev/null"
1714 1717 create = nulla and hunk.starta == 0 and hunk.lena == 0
1715 1718 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1716 1719 abase, afile = pathtransform(afile_orig, strip, prefix)
1717 1720 gooda = not nulla and backend.exists(afile)
1718 1721 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1719 1722 if afile == bfile:
1720 1723 goodb = gooda
1721 1724 else:
1722 1725 goodb = not nullb and backend.exists(bfile)
1723 1726 missing = not goodb and not gooda and not create
1724 1727
1725 1728 # some diff programs apparently produce patches where the afile is
1726 1729 # not /dev/null, but afile starts with bfile
1727 1730 abasedir = afile[:afile.rfind('/') + 1]
1728 1731 bbasedir = bfile[:bfile.rfind('/') + 1]
1729 1732 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1730 1733 and hunk.starta == 0 and hunk.lena == 0):
1731 1734 create = True
1732 1735 missing = False
1733 1736
1734 1737 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1735 1738 # diff is between a file and its backup. In this case, the original
1736 1739 # file should be patched (see original mpatch code).
1737 1740 isbackup = (abase == bbase and bfile.startswith(afile))
1738 1741 fname = None
1739 1742 if not missing:
1740 1743 if gooda and goodb:
1741 1744 if isbackup:
1742 1745 fname = afile
1743 1746 else:
1744 1747 fname = bfile
1745 1748 elif gooda:
1746 1749 fname = afile
1747 1750
1748 1751 if not fname:
1749 1752 if not nullb:
1750 1753 if isbackup:
1751 1754 fname = afile
1752 1755 else:
1753 1756 fname = bfile
1754 1757 elif not nulla:
1755 1758 fname = afile
1756 1759 else:
1757 1760 raise PatchError(_("undefined source and destination files"))
1758 1761
1759 1762 gp = patchmeta(fname)
1760 1763 if create:
1761 1764 gp.op = 'ADD'
1762 1765 elif remove:
1763 1766 gp.op = 'DELETE'
1764 1767 return gp
1765 1768
1766 1769 def scanpatch(fp):
1767 1770 """like patch.iterhunks, but yield different events
1768 1771
1769 1772 - ('file', [header_lines + fromfile + tofile])
1770 1773 - ('context', [context_lines])
1771 1774 - ('hunk', [hunk_lines])
1772 1775 - ('range', (-start,len, +start,len, proc))
1773 1776 """
1774 1777 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1775 1778 lr = linereader(fp)
1776 1779
1777 1780 def scanwhile(first, p):
1778 1781 """scan lr while predicate holds"""
1779 1782 lines = [first]
1780 1783 for line in iter(lr.readline, ''):
1781 1784 if p(line):
1782 1785 lines.append(line)
1783 1786 else:
1784 1787 lr.push(line)
1785 1788 break
1786 1789 return lines
1787 1790
1788 1791 for line in iter(lr.readline, ''):
1789 1792 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1790 1793 def notheader(line):
1791 1794 s = line.split(None, 1)
1792 1795 return not s or s[0] not in ('---', 'diff')
1793 1796 header = scanwhile(line, notheader)
1794 1797 fromfile = lr.readline()
1795 1798 if fromfile.startswith('---'):
1796 1799 tofile = lr.readline()
1797 1800 header += [fromfile, tofile]
1798 1801 else:
1799 1802 lr.push(fromfile)
1800 1803 yield 'file', header
1801 1804 elif line.startswith(' '):
1802 1805 cs = (' ', '\\')
1803 1806 yield 'context', scanwhile(line, lambda l: l.startswith(cs))
1804 1807 elif line.startswith(('-', '+')):
1805 1808 cs = ('-', '+', '\\')
1806 1809 yield 'hunk', scanwhile(line, lambda l: l.startswith(cs))
1807 1810 else:
1808 1811 m = lines_re.match(line)
1809 1812 if m:
1810 1813 yield 'range', m.groups()
1811 1814 else:
1812 1815 yield 'other', line
1813 1816
1814 1817 def scangitpatch(lr, firstline):
1815 1818 """
1816 1819 Git patches can emit:
1817 1820 - rename a to b
1818 1821 - change b
1819 1822 - copy a to c
1820 1823 - change c
1821 1824
1822 1825 We cannot apply this sequence as-is, the renamed 'a' could not be
1823 1826 found for it would have been renamed already. And we cannot copy
1824 1827 from 'b' instead because 'b' would have been changed already. So
1825 1828 we scan the git patch for copy and rename commands so we can
1826 1829 perform the copies ahead of time.
1827 1830 """
1828 1831 pos = 0
1829 1832 try:
1830 1833 pos = lr.fp.tell()
1831 1834 fp = lr.fp
1832 1835 except IOError:
1833 1836 fp = stringio(lr.fp.read())
1834 1837 gitlr = linereader(fp)
1835 1838 gitlr.push(firstline)
1836 1839 gitpatches = readgitpatch(gitlr)
1837 1840 fp.seek(pos)
1838 1841 return gitpatches
1839 1842
1840 1843 def iterhunks(fp):
1841 1844 """Read a patch and yield the following events:
1842 1845 - ("file", afile, bfile, firsthunk): select a new target file.
1843 1846 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1844 1847 "file" event.
1845 1848 - ("git", gitchanges): current diff is in git format, gitchanges
1846 1849 maps filenames to gitpatch records. Unique event.
1847 1850 """
1848 1851 afile = ""
1849 1852 bfile = ""
1850 1853 state = None
1851 1854 hunknum = 0
1852 1855 emitfile = newfile = False
1853 1856 gitpatches = None
1854 1857
1855 1858 # our states
1856 1859 BFILE = 1
1857 1860 context = None
1858 1861 lr = linereader(fp)
1859 1862
1860 1863 for x in iter(lr.readline, ''):
1861 1864 if state == BFILE and (
1862 1865 (not context and x.startswith('@'))
1863 1866 or (context is not False and x.startswith('***************'))
1864 1867 or x.startswith('GIT binary patch')):
1865 1868 gp = None
1866 1869 if (gitpatches and
1867 1870 gitpatches[-1].ispatching(afile, bfile)):
1868 1871 gp = gitpatches.pop()
1869 1872 if x.startswith('GIT binary patch'):
1870 1873 h = binhunk(lr, gp.path)
1871 1874 else:
1872 1875 if context is None and x.startswith('***************'):
1873 1876 context = True
1874 1877 h = hunk(x, hunknum + 1, lr, context)
1875 1878 hunknum += 1
1876 1879 if emitfile:
1877 1880 emitfile = False
1878 1881 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1879 1882 yield 'hunk', h
1880 1883 elif x.startswith('diff --git a/'):
1881 1884 m = gitre.match(x.rstrip(' \r\n'))
1882 1885 if not m:
1883 1886 continue
1884 1887 if gitpatches is None:
1885 1888 # scan whole input for git metadata
1886 1889 gitpatches = scangitpatch(lr, x)
1887 1890 yield 'git', [g.copy() for g in gitpatches
1888 1891 if g.op in ('COPY', 'RENAME')]
1889 1892 gitpatches.reverse()
1890 1893 afile = 'a/' + m.group(1)
1891 1894 bfile = 'b/' + m.group(2)
1892 1895 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1893 1896 gp = gitpatches.pop()
1894 1897 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1895 1898 if not gitpatches:
1896 1899 raise PatchError(_('failed to synchronize metadata for "%s"')
1897 1900 % afile[2:])
1898 1901 gp = gitpatches[-1]
1899 1902 newfile = True
1900 1903 elif x.startswith('---'):
1901 1904 # check for a unified diff
1902 1905 l2 = lr.readline()
1903 1906 if not l2.startswith('+++'):
1904 1907 lr.push(l2)
1905 1908 continue
1906 1909 newfile = True
1907 1910 context = False
1908 1911 afile = parsefilename(x)
1909 1912 bfile = parsefilename(l2)
1910 1913 elif x.startswith('***'):
1911 1914 # check for a context diff
1912 1915 l2 = lr.readline()
1913 1916 if not l2.startswith('---'):
1914 1917 lr.push(l2)
1915 1918 continue
1916 1919 l3 = lr.readline()
1917 1920 lr.push(l3)
1918 1921 if not l3.startswith("***************"):
1919 1922 lr.push(l2)
1920 1923 continue
1921 1924 newfile = True
1922 1925 context = True
1923 1926 afile = parsefilename(x)
1924 1927 bfile = parsefilename(l2)
1925 1928
1926 1929 if newfile:
1927 1930 newfile = False
1928 1931 emitfile = True
1929 1932 state = BFILE
1930 1933 hunknum = 0
1931 1934
1932 1935 while gitpatches:
1933 1936 gp = gitpatches.pop()
1934 1937 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1935 1938
1936 1939 def applybindelta(binchunk, data):
1937 1940 """Apply a binary delta hunk
1938 1941 The algorithm used is the algorithm from git's patch-delta.c
1939 1942 """
1940 1943 def deltahead(binchunk):
1941 1944 i = 0
1942 1945 for c in binchunk:
1943 1946 i += 1
1944 1947 if not (ord(c) & 0x80):
1945 1948 return i
1946 1949 return i
1947 1950 out = ""
1948 1951 s = deltahead(binchunk)
1949 1952 binchunk = binchunk[s:]
1950 1953 s = deltahead(binchunk)
1951 1954 binchunk = binchunk[s:]
1952 1955 i = 0
1953 1956 while i < len(binchunk):
1954 1957 cmd = ord(binchunk[i])
1955 1958 i += 1
1956 1959 if (cmd & 0x80):
1957 1960 offset = 0
1958 1961 size = 0
1959 1962 if (cmd & 0x01):
1960 1963 offset = ord(binchunk[i])
1961 1964 i += 1
1962 1965 if (cmd & 0x02):
1963 1966 offset |= ord(binchunk[i]) << 8
1964 1967 i += 1
1965 1968 if (cmd & 0x04):
1966 1969 offset |= ord(binchunk[i]) << 16
1967 1970 i += 1
1968 1971 if (cmd & 0x08):
1969 1972 offset |= ord(binchunk[i]) << 24
1970 1973 i += 1
1971 1974 if (cmd & 0x10):
1972 1975 size = ord(binchunk[i])
1973 1976 i += 1
1974 1977 if (cmd & 0x20):
1975 1978 size |= ord(binchunk[i]) << 8
1976 1979 i += 1
1977 1980 if (cmd & 0x40):
1978 1981 size |= ord(binchunk[i]) << 16
1979 1982 i += 1
1980 1983 if size == 0:
1981 1984 size = 0x10000
1982 1985 offset_end = offset + size
1983 1986 out += data[offset:offset_end]
1984 1987 elif cmd != 0:
1985 1988 offset_end = i + cmd
1986 1989 out += binchunk[i:offset_end]
1987 1990 i += cmd
1988 1991 else:
1989 1992 raise PatchError(_('unexpected delta opcode 0'))
1990 1993 return out
1991 1994
1992 1995 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1993 1996 """Reads a patch from fp and tries to apply it.
1994 1997
1995 1998 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1996 1999 there was any fuzz.
1997 2000
1998 2001 If 'eolmode' is 'strict', the patch content and patched file are
1999 2002 read in binary mode. Otherwise, line endings are ignored when
2000 2003 patching then normalized according to 'eolmode'.
2001 2004 """
2002 2005 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
2003 2006 prefix=prefix, eolmode=eolmode)
2004 2007
2005 2008 def _canonprefix(repo, prefix):
2006 2009 if prefix:
2007 2010 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2008 2011 if prefix != '':
2009 2012 prefix += '/'
2010 2013 return prefix
2011 2014
2012 2015 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
2013 2016 eolmode='strict'):
2014 2017 prefix = _canonprefix(backend.repo, prefix)
2015 2018 def pstrip(p):
2016 2019 return pathtransform(p, strip - 1, prefix)[1]
2017 2020
2018 2021 rejects = 0
2019 2022 err = 0
2020 2023 current_file = None
2021 2024
2022 2025 for state, values in iterhunks(fp):
2023 2026 if state == 'hunk':
2024 2027 if not current_file:
2025 2028 continue
2026 2029 ret = current_file.apply(values)
2027 2030 if ret > 0:
2028 2031 err = 1
2029 2032 elif state == 'file':
2030 2033 if current_file:
2031 2034 rejects += current_file.close()
2032 2035 current_file = None
2033 2036 afile, bfile, first_hunk, gp = values
2034 2037 if gp:
2035 2038 gp.path = pstrip(gp.path)
2036 2039 if gp.oldpath:
2037 2040 gp.oldpath = pstrip(gp.oldpath)
2038 2041 else:
2039 2042 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2040 2043 prefix)
2041 2044 if gp.op == 'RENAME':
2042 2045 backend.unlink(gp.oldpath)
2043 2046 if not first_hunk:
2044 2047 if gp.op == 'DELETE':
2045 2048 backend.unlink(gp.path)
2046 2049 continue
2047 2050 data, mode = None, None
2048 2051 if gp.op in ('RENAME', 'COPY'):
2049 2052 data, mode = store.getfile(gp.oldpath)[:2]
2050 2053 if data is None:
2051 2054 # This means that the old path does not exist
2052 2055 raise PatchError(_("source file '%s' does not exist")
2053 2056 % gp.oldpath)
2054 2057 if gp.mode:
2055 2058 mode = gp.mode
2056 2059 if gp.op == 'ADD':
2057 2060 # Added files without content have no hunk and
2058 2061 # must be created
2059 2062 data = ''
2060 2063 if data or mode:
2061 2064 if (gp.op in ('ADD', 'RENAME', 'COPY')
2062 2065 and backend.exists(gp.path)):
2063 2066 raise PatchError(_("cannot create %s: destination "
2064 2067 "already exists") % gp.path)
2065 2068 backend.setfile(gp.path, data, mode, gp.oldpath)
2066 2069 continue
2067 2070 try:
2068 2071 current_file = patcher(ui, gp, backend, store,
2069 2072 eolmode=eolmode)
2070 2073 except PatchError as inst:
2071 2074 ui.warn(str(inst) + '\n')
2072 2075 current_file = None
2073 2076 rejects += 1
2074 2077 continue
2075 2078 elif state == 'git':
2076 2079 for gp in values:
2077 2080 path = pstrip(gp.oldpath)
2078 2081 data, mode = backend.getfile(path)
2079 2082 if data is None:
2080 2083 # The error ignored here will trigger a getfile()
2081 2084 # error in a place more appropriate for error
2082 2085 # handling, and will not interrupt the patching
2083 2086 # process.
2084 2087 pass
2085 2088 else:
2086 2089 store.setfile(path, data, mode)
2087 2090 else:
2088 2091 raise error.Abort(_('unsupported parser state: %s') % state)
2089 2092
2090 2093 if current_file:
2091 2094 rejects += current_file.close()
2092 2095
2093 2096 if rejects:
2094 2097 return -1
2095 2098 return err
2096 2099
2097 2100 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2098 2101 similarity):
2099 2102 """use <patcher> to apply <patchname> to the working directory.
2100 2103 returns whether patch was applied with fuzz factor."""
2101 2104
2102 2105 fuzz = False
2103 2106 args = []
2104 2107 cwd = repo.root
2105 2108 if cwd:
2106 2109 args.append('-d %s' % procutil.shellquote(cwd))
2107 2110 cmd = ('%s %s -p%d < %s'
2108 2111 % (patcher, ' '.join(args), strip, procutil.shellquote(patchname)))
2109 2112 fp = procutil.popen(cmd, 'rb')
2110 2113 try:
2111 2114 for line in util.iterfile(fp):
2112 2115 line = line.rstrip()
2113 2116 ui.note(line + '\n')
2114 2117 if line.startswith('patching file '):
2115 2118 pf = util.parsepatchoutput(line)
2116 2119 printed_file = False
2117 2120 files.add(pf)
2118 2121 elif line.find('with fuzz') >= 0:
2119 2122 fuzz = True
2120 2123 if not printed_file:
2121 2124 ui.warn(pf + '\n')
2122 2125 printed_file = True
2123 2126 ui.warn(line + '\n')
2124 2127 elif line.find('saving rejects to file') >= 0:
2125 2128 ui.warn(line + '\n')
2126 2129 elif line.find('FAILED') >= 0:
2127 2130 if not printed_file:
2128 2131 ui.warn(pf + '\n')
2129 2132 printed_file = True
2130 2133 ui.warn(line + '\n')
2131 2134 finally:
2132 2135 if files:
2133 2136 scmutil.marktouched(repo, files, similarity)
2134 2137 code = fp.close()
2135 2138 if code:
2136 2139 raise PatchError(_("patch command failed: %s") %
2137 2140 procutil.explainexit(code))
2138 2141 return fuzz
2139 2142
2140 2143 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2141 2144 eolmode='strict'):
2142 2145 if files is None:
2143 2146 files = set()
2144 2147 if eolmode is None:
2145 2148 eolmode = ui.config('patch', 'eol')
2146 2149 if eolmode.lower() not in eolmodes:
2147 2150 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2148 2151 eolmode = eolmode.lower()
2149 2152
2150 2153 store = filestore()
2151 2154 try:
2152 2155 fp = open(patchobj, 'rb')
2153 2156 except TypeError:
2154 2157 fp = patchobj
2155 2158 try:
2156 2159 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2157 2160 eolmode=eolmode)
2158 2161 finally:
2159 2162 if fp != patchobj:
2160 2163 fp.close()
2161 2164 files.update(backend.close())
2162 2165 store.close()
2163 2166 if ret < 0:
2164 2167 raise PatchError(_('patch failed to apply'))
2165 2168 return ret > 0
2166 2169
2167 2170 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2168 2171 eolmode='strict', similarity=0):
2169 2172 """use builtin patch to apply <patchobj> to the working directory.
2170 2173 returns whether patch was applied with fuzz factor."""
2171 2174 backend = workingbackend(ui, repo, similarity)
2172 2175 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2173 2176
2174 2177 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2175 2178 eolmode='strict'):
2176 2179 backend = repobackend(ui, repo, ctx, store)
2177 2180 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2178 2181
2179 2182 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2180 2183 similarity=0):
2181 2184 """Apply <patchname> to the working directory.
2182 2185
2183 2186 'eolmode' specifies how end of lines should be handled. It can be:
2184 2187 - 'strict': inputs are read in binary mode, EOLs are preserved
2185 2188 - 'crlf': EOLs are ignored when patching and reset to CRLF
2186 2189 - 'lf': EOLs are ignored when patching and reset to LF
2187 2190 - None: get it from user settings, default to 'strict'
2188 2191 'eolmode' is ignored when using an external patcher program.
2189 2192
2190 2193 Returns whether patch was applied with fuzz factor.
2191 2194 """
2192 2195 patcher = ui.config('ui', 'patch')
2193 2196 if files is None:
2194 2197 files = set()
2195 2198 if patcher:
2196 2199 return _externalpatch(ui, repo, patcher, patchname, strip,
2197 2200 files, similarity)
2198 2201 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2199 2202 similarity)
2200 2203
2201 2204 def changedfiles(ui, repo, patchpath, strip=1, prefix=''):
2202 2205 backend = fsbackend(ui, repo.root)
2203 2206 prefix = _canonprefix(repo, prefix)
2204 2207 with open(patchpath, 'rb') as fp:
2205 2208 changed = set()
2206 2209 for state, values in iterhunks(fp):
2207 2210 if state == 'file':
2208 2211 afile, bfile, first_hunk, gp = values
2209 2212 if gp:
2210 2213 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2211 2214 if gp.oldpath:
2212 2215 gp.oldpath = pathtransform(gp.oldpath, strip - 1,
2213 2216 prefix)[1]
2214 2217 else:
2215 2218 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2216 2219 prefix)
2217 2220 changed.add(gp.path)
2218 2221 if gp.op == 'RENAME':
2219 2222 changed.add(gp.oldpath)
2220 2223 elif state not in ('hunk', 'git'):
2221 2224 raise error.Abort(_('unsupported parser state: %s') % state)
2222 2225 return changed
2223 2226
2224 2227 class GitDiffRequired(Exception):
2225 2228 pass
2226 2229
2227 2230 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2228 2231 '''return diffopts with all features supported and parsed'''
2229 2232 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2230 2233 git=True, whitespace=True, formatchanging=True)
2231 2234
2232 2235 diffopts = diffallopts
2233 2236
2234 2237 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2235 2238 whitespace=False, formatchanging=False):
2236 2239 '''return diffopts with only opted-in features parsed
2237 2240
2238 2241 Features:
2239 2242 - git: git-style diffs
2240 2243 - whitespace: whitespace options like ignoreblanklines and ignorews
2241 2244 - formatchanging: options that will likely break or cause correctness issues
2242 2245 with most diff parsers
2243 2246 '''
2244 2247 def get(key, name=None, getter=ui.configbool, forceplain=None):
2245 2248 if opts:
2246 2249 v = opts.get(key)
2247 2250 # diffopts flags are either None-default (which is passed
2248 2251 # through unchanged, so we can identify unset values), or
2249 2252 # some other falsey default (eg --unified, which defaults
2250 2253 # to an empty string). We only want to override the config
2251 2254 # entries from hgrc with command line values if they
2252 2255 # appear to have been set, which is any truthy value,
2253 2256 # True, or False.
2254 2257 if v or isinstance(v, bool):
2255 2258 return v
2256 2259 if forceplain is not None and ui.plain():
2257 2260 return forceplain
2258 2261 return getter(section, name or key, untrusted=untrusted)
2259 2262
2260 2263 # core options, expected to be understood by every diff parser
2261 2264 buildopts = {
2262 2265 'nodates': get('nodates'),
2263 2266 'showfunc': get('show_function', 'showfunc'),
2264 2267 'context': get('unified', getter=ui.config),
2265 2268 }
2266 2269 buildopts['worddiff'] = ui.configbool('experimental', 'worddiff')
2267 2270 buildopts['xdiff'] = ui.configbool('experimental', 'xdiff')
2268 2271
2269 2272 if git:
2270 2273 buildopts['git'] = get('git')
2271 2274
2272 2275 # since this is in the experimental section, we need to call
2273 2276 # ui.configbool directory
2274 2277 buildopts['showsimilarity'] = ui.configbool('experimental',
2275 2278 'extendedheader.similarity')
2276 2279
2277 2280 # need to inspect the ui object instead of using get() since we want to
2278 2281 # test for an int
2279 2282 hconf = ui.config('experimental', 'extendedheader.index')
2280 2283 if hconf is not None:
2281 2284 hlen = None
2282 2285 try:
2283 2286 # the hash config could be an integer (for length of hash) or a
2284 2287 # word (e.g. short, full, none)
2285 2288 hlen = int(hconf)
2286 2289 if hlen < 0 or hlen > 40:
2287 2290 msg = _("invalid length for extendedheader.index: '%d'\n")
2288 2291 ui.warn(msg % hlen)
2289 2292 except ValueError:
2290 2293 # default value
2291 2294 if hconf == 'short' or hconf == '':
2292 2295 hlen = 12
2293 2296 elif hconf == 'full':
2294 2297 hlen = 40
2295 2298 elif hconf != 'none':
2296 2299 msg = _("invalid value for extendedheader.index: '%s'\n")
2297 2300 ui.warn(msg % hconf)
2298 2301 finally:
2299 2302 buildopts['index'] = hlen
2300 2303
2301 2304 if whitespace:
2302 2305 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2303 2306 buildopts['ignorewsamount'] = get('ignore_space_change',
2304 2307 'ignorewsamount')
2305 2308 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2306 2309 'ignoreblanklines')
2307 2310 buildopts['ignorewseol'] = get('ignore_space_at_eol', 'ignorewseol')
2308 2311 if formatchanging:
2309 2312 buildopts['text'] = opts and opts.get('text')
2310 2313 binary = None if opts is None else opts.get('binary')
2311 2314 buildopts['nobinary'] = (not binary if binary is not None
2312 2315 else get('nobinary', forceplain=False))
2313 2316 buildopts['noprefix'] = get('noprefix', forceplain=False)
2314 2317
2315 2318 return mdiff.diffopts(**pycompat.strkwargs(buildopts))
2316 2319
2317 2320 def diff(repo, node1=None, node2=None, match=None, changes=None,
2318 2321 opts=None, losedatafn=None, prefix='', relroot='', copy=None,
2319 2322 hunksfilterfn=None):
2320 2323 '''yields diff of changes to files between two nodes, or node and
2321 2324 working directory.
2322 2325
2323 2326 if node1 is None, use first dirstate parent instead.
2324 2327 if node2 is None, compare node1 with working directory.
2325 2328
2326 2329 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2327 2330 every time some change cannot be represented with the current
2328 2331 patch format. Return False to upgrade to git patch format, True to
2329 2332 accept the loss or raise an exception to abort the diff. It is
2330 2333 called with the name of current file being diffed as 'fn'. If set
2331 2334 to None, patches will always be upgraded to git format when
2332 2335 necessary.
2333 2336
2334 2337 prefix is a filename prefix that is prepended to all filenames on
2335 2338 display (used for subrepos).
2336 2339
2337 2340 relroot, if not empty, must be normalized with a trailing /. Any match
2338 2341 patterns that fall outside it will be ignored.
2339 2342
2340 2343 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2341 2344 information.
2342 2345
2343 2346 hunksfilterfn, if not None, should be a function taking a filectx and
2344 2347 hunks generator that may yield filtered hunks.
2345 2348 '''
2346 2349 for fctx1, fctx2, hdr, hunks in diffhunks(
2347 2350 repo, node1=node1, node2=node2,
2348 2351 match=match, changes=changes, opts=opts,
2349 2352 losedatafn=losedatafn, prefix=prefix, relroot=relroot, copy=copy,
2350 2353 ):
2351 2354 if hunksfilterfn is not None:
2352 2355 # If the file has been removed, fctx2 is None; but this should
2353 2356 # not occur here since we catch removed files early in
2354 2357 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2355 2358 assert fctx2 is not None, \
2356 2359 'fctx2 unexpectly None in diff hunks filtering'
2357 2360 hunks = hunksfilterfn(fctx2, hunks)
2358 2361 text = ''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2359 2362 if hdr and (text or len(hdr) > 1):
2360 2363 yield '\n'.join(hdr) + '\n'
2361 2364 if text:
2362 2365 yield text
2363 2366
2364 2367 def diffhunks(repo, node1=None, node2=None, match=None, changes=None,
2365 2368 opts=None, losedatafn=None, prefix='', relroot='', copy=None):
2366 2369 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2367 2370 where `header` is a list of diff headers and `hunks` is an iterable of
2368 2371 (`hunkrange`, `hunklines`) tuples.
2369 2372
2370 2373 See diff() for the meaning of parameters.
2371 2374 """
2372 2375
2373 2376 if opts is None:
2374 2377 opts = mdiff.defaultopts
2375 2378
2376 2379 if not node1 and not node2:
2377 2380 node1 = repo.dirstate.p1()
2378 2381
2379 2382 def lrugetfilectx():
2380 2383 cache = {}
2381 2384 order = collections.deque()
2382 2385 def getfilectx(f, ctx):
2383 2386 fctx = ctx.filectx(f, filelog=cache.get(f))
2384 2387 if f not in cache:
2385 2388 if len(cache) > 20:
2386 2389 del cache[order.popleft()]
2387 2390 cache[f] = fctx.filelog()
2388 2391 else:
2389 2392 order.remove(f)
2390 2393 order.append(f)
2391 2394 return fctx
2392 2395 return getfilectx
2393 2396 getfilectx = lrugetfilectx()
2394 2397
2395 2398 ctx1 = repo[node1]
2396 2399 ctx2 = repo[node2]
2397 2400
2398 2401 relfiltered = False
2399 2402 if relroot != '' and match.always():
2400 2403 # as a special case, create a new matcher with just the relroot
2401 2404 pats = [relroot]
2402 2405 match = scmutil.match(ctx2, pats, default='path')
2403 2406 relfiltered = True
2404 2407
2405 2408 if not changes:
2406 2409 changes = repo.status(ctx1, ctx2, match=match)
2407 2410 modified, added, removed = changes[:3]
2408 2411
2409 2412 if not modified and not added and not removed:
2410 2413 return []
2411 2414
2412 2415 if repo.ui.debugflag:
2413 2416 hexfunc = hex
2414 2417 else:
2415 2418 hexfunc = short
2416 2419 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2417 2420
2418 2421 if copy is None:
2419 2422 copy = {}
2420 2423 if opts.git or opts.upgrade:
2421 2424 copy = copies.pathcopies(ctx1, ctx2, match=match)
2422 2425
2423 2426 if relroot is not None:
2424 2427 if not relfiltered:
2425 2428 # XXX this would ideally be done in the matcher, but that is
2426 2429 # generally meant to 'or' patterns, not 'and' them. In this case we
2427 2430 # need to 'and' all the patterns from the matcher with relroot.
2428 2431 def filterrel(l):
2429 2432 return [f for f in l if f.startswith(relroot)]
2430 2433 modified = filterrel(modified)
2431 2434 added = filterrel(added)
2432 2435 removed = filterrel(removed)
2433 2436 relfiltered = True
2434 2437 # filter out copies where either side isn't inside the relative root
2435 2438 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2436 2439 if dst.startswith(relroot)
2437 2440 and src.startswith(relroot)))
2438 2441
2439 2442 modifiedset = set(modified)
2440 2443 addedset = set(added)
2441 2444 removedset = set(removed)
2442 2445 for f in modified:
2443 2446 if f not in ctx1:
2444 2447 # Fix up added, since merged-in additions appear as
2445 2448 # modifications during merges
2446 2449 modifiedset.remove(f)
2447 2450 addedset.add(f)
2448 2451 for f in removed:
2449 2452 if f not in ctx1:
2450 2453 # Merged-in additions that are then removed are reported as removed.
2451 2454 # They are not in ctx1, so We don't want to show them in the diff.
2452 2455 removedset.remove(f)
2453 2456 modified = sorted(modifiedset)
2454 2457 added = sorted(addedset)
2455 2458 removed = sorted(removedset)
2456 2459 for dst, src in list(copy.items()):
2457 2460 if src not in ctx1:
2458 2461 # Files merged in during a merge and then copied/renamed are
2459 2462 # reported as copies. We want to show them in the diff as additions.
2460 2463 del copy[dst]
2461 2464
2462 2465 def difffn(opts, losedata):
2463 2466 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2464 2467 copy, getfilectx, opts, losedata, prefix, relroot)
2465 2468 if opts.upgrade and not opts.git:
2466 2469 try:
2467 2470 def losedata(fn):
2468 2471 if not losedatafn or not losedatafn(fn=fn):
2469 2472 raise GitDiffRequired
2470 2473 # Buffer the whole output until we are sure it can be generated
2471 2474 return list(difffn(opts.copy(git=False), losedata))
2472 2475 except GitDiffRequired:
2473 2476 return difffn(opts.copy(git=True), None)
2474 2477 else:
2475 2478 return difffn(opts, None)
2476 2479
2477 2480 def difflabel(func, *args, **kw):
2478 2481 '''yields 2-tuples of (output, label) based on the output of func()'''
2479 2482 inlinecolor = False
2480 2483 if kw.get(r'opts'):
2481 2484 inlinecolor = kw[r'opts'].worddiff
2482 2485 headprefixes = [('diff', 'diff.diffline'),
2483 2486 ('copy', 'diff.extended'),
2484 2487 ('rename', 'diff.extended'),
2485 2488 ('old', 'diff.extended'),
2486 2489 ('new', 'diff.extended'),
2487 2490 ('deleted', 'diff.extended'),
2488 2491 ('index', 'diff.extended'),
2489 2492 ('similarity', 'diff.extended'),
2490 2493 ('---', 'diff.file_a'),
2491 2494 ('+++', 'diff.file_b')]
2492 2495 textprefixes = [('@', 'diff.hunk'),
2493 2496 ('-', 'diff.deleted'),
2494 2497 ('+', 'diff.inserted')]
2495 2498 head = False
2496 2499 for chunk in func(*args, **kw):
2497 2500 lines = chunk.split('\n')
2498 2501 matches = {}
2499 2502 if inlinecolor:
2500 2503 matches = _findmatches(lines)
2501 2504 for i, line in enumerate(lines):
2502 2505 if i != 0:
2503 2506 yield ('\n', '')
2504 2507 if head:
2505 2508 if line.startswith('@'):
2506 2509 head = False
2507 2510 else:
2508 2511 if line and not line.startswith((' ', '+', '-', '@', '\\')):
2509 2512 head = True
2510 2513 stripline = line
2511 2514 diffline = False
2512 2515 if not head and line and line.startswith(('+', '-')):
2513 2516 # highlight tabs and trailing whitespace, but only in
2514 2517 # changed lines
2515 2518 stripline = line.rstrip()
2516 2519 diffline = True
2517 2520
2518 2521 prefixes = textprefixes
2519 2522 if head:
2520 2523 prefixes = headprefixes
2521 2524 for prefix, label in prefixes:
2522 2525 if stripline.startswith(prefix):
2523 2526 if diffline:
2524 2527 if i in matches:
2525 2528 for t, l in _inlinediff(lines[i].rstrip(),
2526 2529 lines[matches[i]].rstrip(),
2527 2530 label):
2528 2531 yield (t, l)
2529 2532 else:
2530 2533 for token in tabsplitter.findall(stripline):
2531 2534 if token.startswith('\t'):
2532 2535 yield (token, 'diff.tab')
2533 2536 else:
2534 2537 yield (token, label)
2535 2538 else:
2536 2539 yield (stripline, label)
2537 2540 break
2538 2541 else:
2539 2542 yield (line, '')
2540 2543 if line != stripline:
2541 2544 yield (line[len(stripline):], 'diff.trailingwhitespace')
2542 2545
2543 2546 def _findmatches(slist):
2544 2547 '''Look for insertion matches to deletion and returns a dict of
2545 2548 correspondences.
2546 2549 '''
2547 2550 lastmatch = 0
2548 2551 matches = {}
2549 2552 for i, line in enumerate(slist):
2550 2553 if line == '':
2551 2554 continue
2552 2555 if line.startswith('-'):
2553 2556 lastmatch = max(lastmatch, i)
2554 2557 newgroup = False
2555 2558 for j, newline in enumerate(slist[lastmatch + 1:]):
2556 2559 if newline == '':
2557 2560 continue
2558 2561 if newline.startswith('-') and newgroup: # too far, no match
2559 2562 break
2560 2563 if newline.startswith('+'): # potential match
2561 2564 newgroup = True
2562 2565 sim = difflib.SequenceMatcher(None, line, newline).ratio()
2563 2566 if sim > 0.7:
2564 2567 lastmatch = lastmatch + 1 + j
2565 2568 matches[i] = lastmatch
2566 2569 matches[lastmatch] = i
2567 2570 break
2568 2571 return matches
2569 2572
2570 2573 def _inlinediff(s1, s2, operation):
2571 2574 '''Perform string diff to highlight specific changes.'''
2572 2575 operation_skip = ('+', '?') if operation == 'diff.deleted' else ('-', '?')
2573 2576 if operation == 'diff.deleted':
2574 2577 s2, s1 = s1, s2
2575 2578
2576 2579 buff = []
2577 2580 # we never want to higlight the leading +-
2578 2581 if operation == 'diff.deleted' and s2.startswith('-'):
2579 2582 label = operation
2580 2583 token = '-'
2581 2584 s2 = s2[1:]
2582 2585 s1 = s1[1:]
2583 2586 elif operation == 'diff.inserted' and s1.startswith('+'):
2584 2587 label = operation
2585 2588 token = '+'
2586 2589 s2 = s2[1:]
2587 2590 s1 = s1[1:]
2588 2591 else:
2589 2592 raise error.ProgrammingError("Case not expected, operation = %s" %
2590 2593 operation)
2591 2594
2592 2595 s = difflib.ndiff(_nonwordre.split(s2), _nonwordre.split(s1))
2593 2596 for part in s:
2594 2597 if part.startswith(operation_skip) or len(part) == 2:
2595 2598 continue
2596 2599 l = operation + '.highlight'
2597 2600 if part.startswith(' '):
2598 2601 l = operation
2599 2602 if part[2:] == '\t':
2600 2603 l = 'diff.tab'
2601 2604 if l == label: # contiguous token with same label
2602 2605 token += part[2:]
2603 2606 continue
2604 2607 else:
2605 2608 buff.append((token, label))
2606 2609 label = l
2607 2610 token = part[2:]
2608 2611 buff.append((token, label))
2609 2612
2610 2613 return buff
2611 2614
2612 2615 def diffui(*args, **kw):
2613 2616 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2614 2617 return difflabel(diff, *args, **kw)
2615 2618
2616 2619 def _filepairs(modified, added, removed, copy, opts):
2617 2620 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2618 2621 before and f2 is the the name after. For added files, f1 will be None,
2619 2622 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2620 2623 or 'rename' (the latter two only if opts.git is set).'''
2621 2624 gone = set()
2622 2625
2623 2626 copyto = dict([(v, k) for k, v in copy.items()])
2624 2627
2625 2628 addedset, removedset = set(added), set(removed)
2626 2629
2627 2630 for f in sorted(modified + added + removed):
2628 2631 copyop = None
2629 2632 f1, f2 = f, f
2630 2633 if f in addedset:
2631 2634 f1 = None
2632 2635 if f in copy:
2633 2636 if opts.git:
2634 2637 f1 = copy[f]
2635 2638 if f1 in removedset and f1 not in gone:
2636 2639 copyop = 'rename'
2637 2640 gone.add(f1)
2638 2641 else:
2639 2642 copyop = 'copy'
2640 2643 elif f in removedset:
2641 2644 f2 = None
2642 2645 if opts.git:
2643 2646 # have we already reported a copy above?
2644 2647 if (f in copyto and copyto[f] in addedset
2645 2648 and copy[copyto[f]] == f):
2646 2649 continue
2647 2650 yield f1, f2, copyop
2648 2651
2649 2652 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2650 2653 copy, getfilectx, opts, losedatafn, prefix, relroot):
2651 2654 '''given input data, generate a diff and yield it in blocks
2652 2655
2653 2656 If generating a diff would lose data like flags or binary data and
2654 2657 losedatafn is not None, it will be called.
2655 2658
2656 2659 relroot is removed and prefix is added to every path in the diff output.
2657 2660
2658 2661 If relroot is not empty, this function expects every path in modified,
2659 2662 added, removed and copy to start with it.'''
2660 2663
2661 2664 def gitindex(text):
2662 2665 if not text:
2663 2666 text = ""
2664 2667 l = len(text)
2665 2668 s = hashlib.sha1('blob %d\0' % l)
2666 2669 s.update(text)
2667 2670 return hex(s.digest())
2668 2671
2669 2672 if opts.noprefix:
2670 2673 aprefix = bprefix = ''
2671 2674 else:
2672 2675 aprefix = 'a/'
2673 2676 bprefix = 'b/'
2674 2677
2675 2678 def diffline(f, revs):
2676 2679 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2677 2680 return 'diff %s %s' % (revinfo, f)
2678 2681
2679 2682 def isempty(fctx):
2680 2683 return fctx is None or fctx.size() == 0
2681 2684
2682 2685 date1 = dateutil.datestr(ctx1.date())
2683 2686 date2 = dateutil.datestr(ctx2.date())
2684 2687
2685 2688 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2686 2689
2687 2690 if relroot != '' and (repo.ui.configbool('devel', 'all-warnings')
2688 2691 or repo.ui.configbool('devel', 'check-relroot')):
2689 2692 for f in modified + added + removed + list(copy) + list(copy.values()):
2690 2693 if f is not None and not f.startswith(relroot):
2691 2694 raise AssertionError(
2692 2695 "file %s doesn't start with relroot %s" % (f, relroot))
2693 2696
2694 2697 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2695 2698 content1 = None
2696 2699 content2 = None
2697 2700 fctx1 = None
2698 2701 fctx2 = None
2699 2702 flag1 = None
2700 2703 flag2 = None
2701 2704 if f1:
2702 2705 fctx1 = getfilectx(f1, ctx1)
2703 2706 if opts.git or losedatafn:
2704 2707 flag1 = ctx1.flags(f1)
2705 2708 if f2:
2706 2709 fctx2 = getfilectx(f2, ctx2)
2707 2710 if opts.git or losedatafn:
2708 2711 flag2 = ctx2.flags(f2)
2709 2712 # if binary is True, output "summary" or "base85", but not "text diff"
2710 2713 if opts.text:
2711 2714 binary = False
2712 2715 else:
2713 2716 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
2714 2717
2715 2718 if losedatafn and not opts.git:
2716 2719 if (binary or
2717 2720 # copy/rename
2718 2721 f2 in copy or
2719 2722 # empty file creation
2720 2723 (not f1 and isempty(fctx2)) or
2721 2724 # empty file deletion
2722 2725 (isempty(fctx1) and not f2) or
2723 2726 # create with flags
2724 2727 (not f1 and flag2) or
2725 2728 # change flags
2726 2729 (f1 and f2 and flag1 != flag2)):
2727 2730 losedatafn(f2 or f1)
2728 2731
2729 2732 path1 = f1 or f2
2730 2733 path2 = f2 or f1
2731 2734 path1 = posixpath.join(prefix, path1[len(relroot):])
2732 2735 path2 = posixpath.join(prefix, path2[len(relroot):])
2733 2736 header = []
2734 2737 if opts.git:
2735 2738 header.append('diff --git %s%s %s%s' %
2736 2739 (aprefix, path1, bprefix, path2))
2737 2740 if not f1: # added
2738 2741 header.append('new file mode %s' % gitmode[flag2])
2739 2742 elif not f2: # removed
2740 2743 header.append('deleted file mode %s' % gitmode[flag1])
2741 2744 else: # modified/copied/renamed
2742 2745 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2743 2746 if mode1 != mode2:
2744 2747 header.append('old mode %s' % mode1)
2745 2748 header.append('new mode %s' % mode2)
2746 2749 if copyop is not None:
2747 2750 if opts.showsimilarity:
2748 2751 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
2749 2752 header.append('similarity index %d%%' % sim)
2750 2753 header.append('%s from %s' % (copyop, path1))
2751 2754 header.append('%s to %s' % (copyop, path2))
2752 2755 elif revs and not repo.ui.quiet:
2753 2756 header.append(diffline(path1, revs))
2754 2757
2755 2758 # fctx.is | diffopts | what to | is fctx.data()
2756 2759 # binary() | text nobinary git index | output? | outputted?
2757 2760 # ------------------------------------|----------------------------
2758 2761 # yes | no no no * | summary | no
2759 2762 # yes | no no yes * | base85 | yes
2760 2763 # yes | no yes no * | summary | no
2761 2764 # yes | no yes yes 0 | summary | no
2762 2765 # yes | no yes yes >0 | summary | semi [1]
2763 2766 # yes | yes * * * | text diff | yes
2764 2767 # no | * * * * | text diff | yes
2765 2768 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
2766 2769 if binary and (not opts.git or (opts.git and opts.nobinary and not
2767 2770 opts.index)):
2768 2771 # fast path: no binary content will be displayed, content1 and
2769 2772 # content2 are only used for equivalent test. cmp() could have a
2770 2773 # fast path.
2771 2774 if fctx1 is not None:
2772 2775 content1 = b'\0'
2773 2776 if fctx2 is not None:
2774 2777 if fctx1 is not None and not fctx1.cmp(fctx2):
2775 2778 content2 = b'\0' # not different
2776 2779 else:
2777 2780 content2 = b'\0\0'
2778 2781 else:
2779 2782 # normal path: load contents
2780 2783 if fctx1 is not None:
2781 2784 content1 = fctx1.data()
2782 2785 if fctx2 is not None:
2783 2786 content2 = fctx2.data()
2784 2787
2785 2788 if binary and opts.git and not opts.nobinary:
2786 2789 text = mdiff.b85diff(content1, content2)
2787 2790 if text:
2788 2791 header.append('index %s..%s' %
2789 2792 (gitindex(content1), gitindex(content2)))
2790 2793 hunks = (None, [text]),
2791 2794 else:
2792 2795 if opts.git and opts.index > 0:
2793 2796 flag = flag1
2794 2797 if flag is None:
2795 2798 flag = flag2
2796 2799 header.append('index %s..%s %s' %
2797 2800 (gitindex(content1)[0:opts.index],
2798 2801 gitindex(content2)[0:opts.index],
2799 2802 gitmode[flag]))
2800 2803
2801 2804 uheaders, hunks = mdiff.unidiff(content1, date1,
2802 2805 content2, date2,
2803 2806 path1, path2,
2804 2807 binary=binary, opts=opts)
2805 2808 header.extend(uheaders)
2806 2809 yield fctx1, fctx2, header, hunks
2807 2810
2808 2811 def diffstatsum(stats):
2809 2812 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2810 2813 for f, a, r, b in stats:
2811 2814 maxfile = max(maxfile, encoding.colwidth(f))
2812 2815 maxtotal = max(maxtotal, a + r)
2813 2816 addtotal += a
2814 2817 removetotal += r
2815 2818 binary = binary or b
2816 2819
2817 2820 return maxfile, maxtotal, addtotal, removetotal, binary
2818 2821
2819 2822 def diffstatdata(lines):
2820 2823 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2821 2824
2822 2825 results = []
2823 2826 filename, adds, removes, isbinary = None, 0, 0, False
2824 2827
2825 2828 def addresult():
2826 2829 if filename:
2827 2830 results.append((filename, adds, removes, isbinary))
2828 2831
2829 2832 # inheader is used to track if a line is in the
2830 2833 # header portion of the diff. This helps properly account
2831 2834 # for lines that start with '--' or '++'
2832 2835 inheader = False
2833 2836
2834 2837 for line in lines:
2835 2838 if line.startswith('diff'):
2836 2839 addresult()
2837 2840 # starting a new file diff
2838 2841 # set numbers to 0 and reset inheader
2839 2842 inheader = True
2840 2843 adds, removes, isbinary = 0, 0, False
2841 2844 if line.startswith('diff --git a/'):
2842 2845 filename = gitre.search(line).group(2)
2843 2846 elif line.startswith('diff -r'):
2844 2847 # format: "diff -r ... -r ... filename"
2845 2848 filename = diffre.search(line).group(1)
2846 2849 elif line.startswith('@@'):
2847 2850 inheader = False
2848 2851 elif line.startswith('+') and not inheader:
2849 2852 adds += 1
2850 2853 elif line.startswith('-') and not inheader:
2851 2854 removes += 1
2852 2855 elif (line.startswith('GIT binary patch') or
2853 2856 line.startswith('Binary file')):
2854 2857 isbinary = True
2855 2858 addresult()
2856 2859 return results
2857 2860
2858 2861 def diffstat(lines, width=80):
2859 2862 output = []
2860 2863 stats = diffstatdata(lines)
2861 2864 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2862 2865
2863 2866 countwidth = len(str(maxtotal))
2864 2867 if hasbinary and countwidth < 3:
2865 2868 countwidth = 3
2866 2869 graphwidth = width - countwidth - maxname - 6
2867 2870 if graphwidth < 10:
2868 2871 graphwidth = 10
2869 2872
2870 2873 def scale(i):
2871 2874 if maxtotal <= graphwidth:
2872 2875 return i
2873 2876 # If diffstat runs out of room it doesn't print anything,
2874 2877 # which isn't very useful, so always print at least one + or -
2875 2878 # if there were at least some changes.
2876 2879 return max(i * graphwidth // maxtotal, int(bool(i)))
2877 2880
2878 2881 for filename, adds, removes, isbinary in stats:
2879 2882 if isbinary:
2880 2883 count = 'Bin'
2881 2884 else:
2882 2885 count = '%d' % (adds + removes)
2883 2886 pluses = '+' * scale(adds)
2884 2887 minuses = '-' * scale(removes)
2885 2888 output.append(' %s%s | %*s %s%s\n' %
2886 2889 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2887 2890 countwidth, count, pluses, minuses))
2888 2891
2889 2892 if stats:
2890 2893 output.append(_(' %d files changed, %d insertions(+), '
2891 2894 '%d deletions(-)\n')
2892 2895 % (len(stats), totaladds, totalremoves))
2893 2896
2894 2897 return ''.join(output)
2895 2898
2896 2899 def diffstatui(*args, **kw):
2897 2900 '''like diffstat(), but yields 2-tuples of (output, label) for
2898 2901 ui.write()
2899 2902 '''
2900 2903
2901 2904 for line in diffstat(*args, **kw).splitlines():
2902 2905 if line and line[-1] in '+-':
2903 2906 name, graph = line.rsplit(' ', 1)
2904 2907 yield (name + ' ', '')
2905 2908 m = re.search(br'\++', graph)
2906 2909 if m:
2907 2910 yield (m.group(0), 'diffstat.inserted')
2908 2911 m = re.search(br'-+', graph)
2909 2912 if m:
2910 2913 yield (m.group(0), 'diffstat.deleted')
2911 2914 else:
2912 2915 yield (line, '')
2913 2916 yield ('\n', '')
@@ -1,1919 +1,1942 b''
1 1 $ hg init a
2 2 $ mkdir a/d1
3 3 $ mkdir a/d1/d2
4 4 $ echo line 1 > a/a
5 5 $ echo line 1 > a/d1/d2/a
6 6 $ hg --cwd a ci -Ama
7 7 adding a
8 8 adding d1/d2/a
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 import with no args:
14 14
15 15 $ hg --cwd a import
16 16 abort: need at least one patch to import
17 17 [255]
18 18
19 19 generate patches for the test
20 20
21 21 $ hg --cwd a export tip > exported-tip.patch
22 22 $ hg --cwd a diff -r0:1 > diffed-tip.patch
23 23
24 24
25 25 import exported patch
26 26 (this also tests that editor is not invoked, if the patch contains the
27 27 commit message and '--edit' is not specified)
28 28
29 29 $ hg clone -r0 a b
30 30 adding changesets
31 31 adding manifests
32 32 adding file changes
33 33 added 1 changesets with 2 changes to 2 files
34 34 new changesets 80971e65b431
35 35 updating to branch default
36 36 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
37 37 $ HGEDITOR=cat hg --cwd b import ../exported-tip.patch
38 38 applying ../exported-tip.patch
39 39
40 40 message and committer and date should be same
41 41
42 42 $ hg --cwd b tip
43 43 changeset: 1:1d4bd90af0e4
44 44 tag: tip
45 45 user: someone
46 46 date: Thu Jan 01 00:00:01 1970 +0000
47 47 summary: second change
48 48
49 49 $ rm -r b
50 50
51 51
52 52 import exported patch with external patcher
53 53 (this also tests that editor is invoked, if the '--edit' is specified,
54 54 regardless of the commit message in the patch)
55 55
56 56 $ cat > dummypatch.py <<EOF
57 57 > from __future__ import print_function
58 58 > print('patching file a')
59 59 > open('a', 'wb').write(b'line2\n')
60 60 > EOF
61 61 $ hg clone -r0 a b
62 62 adding changesets
63 63 adding manifests
64 64 adding file changes
65 65 added 1 changesets with 2 changes to 2 files
66 66 new changesets 80971e65b431
67 67 updating to branch default
68 68 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
69 69 $ HGEDITOR=cat hg --config ui.patch="$PYTHON ../dummypatch.py" --cwd b import --edit ../exported-tip.patch
70 70 applying ../exported-tip.patch
71 71 second change
72 72
73 73
74 74 HG: Enter commit message. Lines beginning with 'HG:' are removed.
75 75 HG: Leave message empty to abort commit.
76 76 HG: --
77 77 HG: user: someone
78 78 HG: branch 'default'
79 79 HG: changed a
80 80 $ cat b/a
81 81 line2
82 82 $ rm -r b
83 83
84 84
85 85 import of plain diff should fail without message
86 86 (this also tests that editor is invoked, if the patch doesn't contain
87 87 the commit message, regardless of '--edit')
88 88
89 89 $ hg clone -r0 a b
90 90 adding changesets
91 91 adding manifests
92 92 adding file changes
93 93 added 1 changesets with 2 changes to 2 files
94 94 new changesets 80971e65b431
95 95 updating to branch default
96 96 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
97 97 $ cat > $TESTTMP/editor.sh <<EOF
98 98 > env | grep HGEDITFORM
99 99 > cat \$1
100 100 > EOF
101 101 $ HGEDITOR="sh $TESTTMP/editor.sh" hg --cwd b import ../diffed-tip.patch
102 102 applying ../diffed-tip.patch
103 103 HGEDITFORM=import.normal.normal
104 104
105 105
106 106 HG: Enter commit message. Lines beginning with 'HG:' are removed.
107 107 HG: Leave message empty to abort commit.
108 108 HG: --
109 109 HG: user: test
110 110 HG: branch 'default'
111 111 HG: changed a
112 112 abort: empty commit message
113 113 [255]
114 114
115 115 Test avoiding editor invocation at applying the patch with --exact,
116 116 even if commit message is empty
117 117
118 118 $ echo a >> b/a
119 119 $ hg --cwd b commit -m ' '
120 120 $ hg --cwd b tip -T "{node}\n"
121 121 d8804f3f5396d800812f579c8452796a5993bdb2
122 122 $ hg --cwd b export -o ../empty-log.diff .
123 123 $ hg --cwd b update -q -C ".^1"
124 124 $ hg --cwd b --config extensions.strip= strip -q tip
125 125 $ HGEDITOR=cat hg --cwd b import --exact ../empty-log.diff
126 126 applying ../empty-log.diff
127 127 $ hg --cwd b tip -T "{node}\n"
128 128 d8804f3f5396d800812f579c8452796a5993bdb2
129 129
130 130 $ rm -r b
131 131
132 132
133 133 import of plain diff should be ok with message
134 134
135 135 $ hg clone -r0 a b
136 136 adding changesets
137 137 adding manifests
138 138 adding file changes
139 139 added 1 changesets with 2 changes to 2 files
140 140 new changesets 80971e65b431
141 141 updating to branch default
142 142 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
143 143 $ hg --cwd b import -mpatch ../diffed-tip.patch
144 144 applying ../diffed-tip.patch
145 145 $ rm -r b
146 146
147 147
148 148 import of plain diff with specific date and user
149 149 (this also tests that editor is not invoked, if
150 150 '--message'/'--logfile' is specified and '--edit' is not)
151 151
152 152 $ hg clone -r0 a b
153 153 adding changesets
154 154 adding manifests
155 155 adding file changes
156 156 added 1 changesets with 2 changes to 2 files
157 157 new changesets 80971e65b431
158 158 updating to branch default
159 159 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
160 160 $ hg --cwd b import -mpatch -d '1 0' -u 'user@nowhere.net' ../diffed-tip.patch
161 161 applying ../diffed-tip.patch
162 162 $ hg -R b tip -pv
163 163 changeset: 1:ca68f19f3a40
164 164 tag: tip
165 165 user: user@nowhere.net
166 166 date: Thu Jan 01 00:00:01 1970 +0000
167 167 files: a
168 168 description:
169 169 patch
170 170
171 171
172 172 diff -r 80971e65b431 -r ca68f19f3a40 a
173 173 --- a/a Thu Jan 01 00:00:00 1970 +0000
174 174 +++ b/a Thu Jan 01 00:00:01 1970 +0000
175 175 @@ -1,1 +1,2 @@
176 176 line 1
177 177 +line 2
178 178
179 179 $ rm -r b
180 180
181 181
182 182 import of plain diff should be ok with --no-commit
183 183 (this also tests that editor is not invoked, if '--no-commit' is
184 184 specified, regardless of '--edit')
185 185
186 186 $ hg clone -r0 a b
187 187 adding changesets
188 188 adding manifests
189 189 adding file changes
190 190 added 1 changesets with 2 changes to 2 files
191 191 new changesets 80971e65b431
192 192 updating to branch default
193 193 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
194 194 $ HGEDITOR=cat hg --cwd b import --no-commit --edit ../diffed-tip.patch
195 195 applying ../diffed-tip.patch
196 196 $ hg --cwd b diff --nodates
197 197 diff -r 80971e65b431 a
198 198 --- a/a
199 199 +++ b/a
200 200 @@ -1,1 +1,2 @@
201 201 line 1
202 202 +line 2
203 203 $ rm -r b
204 204
205 205
206 206 import of malformed plain diff should fail
207 207
208 208 $ hg clone -r0 a b
209 209 adding changesets
210 210 adding manifests
211 211 adding file changes
212 212 added 1 changesets with 2 changes to 2 files
213 213 new changesets 80971e65b431
214 214 updating to branch default
215 215 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
216 216 $ sed 's/1,1/foo/' < diffed-tip.patch > broken.patch
217 217 $ hg --cwd b import -mpatch ../broken.patch
218 218 applying ../broken.patch
219 219 abort: bad hunk #1
220 220 [255]
221 221 $ rm -r b
222 222
223 223
224 224 hg -R repo import
225 225 put the clone in a subdir - having a directory named "a"
226 226 used to hide a bug.
227 227
228 228 $ mkdir dir
229 229 $ hg clone -r0 a dir/b
230 230 adding changesets
231 231 adding manifests
232 232 adding file changes
233 233 added 1 changesets with 2 changes to 2 files
234 234 new changesets 80971e65b431
235 235 updating to branch default
236 236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 237 $ cd dir
238 238 $ hg -R b import ../exported-tip.patch
239 239 applying ../exported-tip.patch
240 240 $ cd ..
241 241 $ rm -r dir
242 242
243 243
244 244 import from stdin
245 245
246 246 $ hg clone -r0 a b
247 247 adding changesets
248 248 adding manifests
249 249 adding file changes
250 250 added 1 changesets with 2 changes to 2 files
251 251 new changesets 80971e65b431
252 252 updating to branch default
253 253 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 254 $ hg --cwd b import - < exported-tip.patch
255 255 applying patch from stdin
256 256 $ rm -r b
257 257
258 258
259 259 import two patches in one stream
260 260
261 261 $ hg init b
262 262 $ hg --cwd a export 0:tip | hg --cwd b import -
263 263 applying patch from stdin
264 264 $ hg --cwd a id
265 265 1d4bd90af0e4 tip
266 266 $ hg --cwd b id
267 267 1d4bd90af0e4 tip
268 268 $ rm -r b
269 269
270 270
271 271 override commit message
272 272
273 273 $ hg clone -r0 a b
274 274 adding changesets
275 275 adding manifests
276 276 adding file changes
277 277 added 1 changesets with 2 changes to 2 files
278 278 new changesets 80971e65b431
279 279 updating to branch default
280 280 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
281 281 $ hg --cwd b import -m 'override' - < exported-tip.patch
282 282 applying patch from stdin
283 283 $ hg --cwd b tip | grep override
284 284 summary: override
285 285 $ rm -r b
286 286
287 287 $ cat > mkmsg.py <<EOF
288 288 > import email.message, sys
289 289 > msg = email.message.Message()
290 290 > patch = open(sys.argv[1], 'rb').read()
291 291 > msg.set_payload(b'email commit message\n' + patch)
292 292 > msg['Subject'] = 'email patch'
293 293 > msg['From'] = 'email patcher'
294 294 > open(sys.argv[2], 'wb').write(bytes(msg))
295 295 > EOF
296 296
297 297
298 298 plain diff in email, subject, message body
299 299
300 300 $ hg clone -r0 a b
301 301 adding changesets
302 302 adding manifests
303 303 adding file changes
304 304 added 1 changesets with 2 changes to 2 files
305 305 new changesets 80971e65b431
306 306 updating to branch default
307 307 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
308 308 $ $PYTHON mkmsg.py diffed-tip.patch msg.patch
309 309 $ hg --cwd b import ../msg.patch
310 310 applying ../msg.patch
311 311 $ hg --cwd b tip | grep email
312 312 user: email patcher
313 313 summary: email patch
314 314 $ rm -r b
315 315
316 316
317 317 plain diff in email, no subject, message body
318 318
319 319 $ hg clone -r0 a b
320 320 adding changesets
321 321 adding manifests
322 322 adding file changes
323 323 added 1 changesets with 2 changes to 2 files
324 324 new changesets 80971e65b431
325 325 updating to branch default
326 326 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
327 327 $ grep -v '^Subject:' msg.patch | hg --cwd b import -
328 328 applying patch from stdin
329 329 $ rm -r b
330 330
331 331
332 332 plain diff in email, subject, no message body
333 333
334 334 $ hg clone -r0 a b
335 335 adding changesets
336 336 adding manifests
337 337 adding file changes
338 338 added 1 changesets with 2 changes to 2 files
339 339 new changesets 80971e65b431
340 340 updating to branch default
341 341 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
342 342 $ grep -v '^email ' msg.patch | hg --cwd b import -
343 343 applying patch from stdin
344 344 $ rm -r b
345 345
346 346
347 347 plain diff in email, no subject, no message body, should fail
348 348
349 349 $ hg clone -r0 a b
350 350 adding changesets
351 351 adding manifests
352 352 adding file changes
353 353 added 1 changesets with 2 changes to 2 files
354 354 new changesets 80971e65b431
355 355 updating to branch default
356 356 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
357 357 $ egrep -v '^(Subject|email)' msg.patch | hg --cwd b import -
358 358 applying patch from stdin
359 359 abort: empty commit message
360 360 [255]
361 361 $ rm -r b
362 362
363 363
364 364 hg export in email, should use patch header
365 365
366 366 $ hg clone -r0 a b
367 367 adding changesets
368 368 adding manifests
369 369 adding file changes
370 370 added 1 changesets with 2 changes to 2 files
371 371 new changesets 80971e65b431
372 372 updating to branch default
373 373 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
374 374 $ $PYTHON mkmsg.py exported-tip.patch msg.patch
375 375 $ cat msg.patch | hg --cwd b import -
376 376 applying patch from stdin
377 377 $ hg --cwd b tip | grep second
378 378 summary: second change
379 379 $ rm -r b
380 380
381 381
382 382 subject: duplicate detection, removal of [PATCH]
383 383 The '---' tests the gitsendmail handling without proper mail headers
384 384
385 385 $ cat > mkmsg2.py <<EOF
386 386 > import email.message, sys
387 387 > msg = email.message.Message()
388 388 > patch = open(sys.argv[1], 'rb').read()
389 389 > msg.set_payload(b'email patch\n\nnext line\n---\n' + patch)
390 390 > msg['Subject'] = '[PATCH] email patch'
391 391 > msg['From'] = 'email patcher'
392 392 > open(sys.argv[2], 'wb').write(bytes(msg))
393 393 > EOF
394 394
395 395
396 396 plain diff in email, [PATCH] subject, message body with subject
397 397
398 398 $ hg clone -r0 a b
399 399 adding changesets
400 400 adding manifests
401 401 adding file changes
402 402 added 1 changesets with 2 changes to 2 files
403 403 new changesets 80971e65b431
404 404 updating to branch default
405 405 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
406 406 $ $PYTHON mkmsg2.py diffed-tip.patch msg.patch
407 407 $ cat msg.patch | hg --cwd b import -
408 408 applying patch from stdin
409 409 $ hg --cwd b tip --template '{desc}\n'
410 410 email patch
411 411
412 412 next line
413 413 $ rm -r b
414 414
415 415
416 416 Issue963: Parent of working dir incorrect after import of multiple
417 417 patches and rollback
418 418
419 419 We weren't backing up the correct dirstate file when importing many
420 420 patches: import patch1 patch2; rollback
421 421
422 422 $ echo line 3 >> a/a
423 423 $ hg --cwd a ci -m'third change'
424 424 $ hg --cwd a export -o '../patch%R' 1 2
425 425 $ hg clone -qr0 a b
426 426 $ hg --cwd b parents --template 'parent: {rev}\n'
427 427 parent: 0
428 428 $ hg --cwd b import -v ../patch1 ../patch2
429 429 applying ../patch1
430 430 patching file a
431 431 committing files:
432 432 a
433 433 committing manifest
434 434 committing changelog
435 435 created 1d4bd90af0e4
436 436 applying ../patch2
437 437 patching file a
438 438 committing files:
439 439 a
440 440 committing manifest
441 441 committing changelog
442 442 created 6d019af21222
443 443 $ hg --cwd b rollback
444 444 repository tip rolled back to revision 0 (undo import)
445 445 working directory now based on revision 0
446 446 $ hg --cwd b parents --template 'parent: {rev}\n'
447 447 parent: 0
448 448
449 449 Test that "hg rollback" doesn't restore dirstate to one at the
450 450 beginning of the rolled back transaction in not-"parent-gone" case.
451 451
452 452 invoking pretxncommit hook will cause marking '.hg/dirstate' as a file
453 453 to be restored when rolling back, after DirstateTransactionPlan (see wiki
454 454 page for detail).
455 455
456 456 $ hg --cwd b branch -q foobar
457 457 $ hg --cwd b commit -m foobar
458 458 $ hg --cwd b update 0 -q
459 459 $ hg --cwd b import ../patch1 ../patch2 --config hooks.pretxncommit=true
460 460 applying ../patch1
461 461 applying ../patch2
462 462 $ hg --cwd b update -q 1
463 463 $ hg --cwd b rollback -q
464 464 $ hg --cwd b parents --template 'parent: {rev}\n'
465 465 parent: 1
466 466
467 467 $ hg --cwd b update -q -C 0
468 468 $ hg --cwd b --config extensions.strip= strip -q 1
469 469
470 470 Test visibility of in-memory dirstate changes inside transaction to
471 471 external process
472 472
473 473 $ echo foo > a/foo
474 474 $ hg --cwd a commit -A -m 'adding foo' foo
475 475 $ hg --cwd a export -o '../patch%R' 3
476 476
477 477 $ cat > $TESTTMP/checkvisibility.sh <<EOF
478 478 > echo "===="
479 479 > hg parents --template "VISIBLE {rev}:{node|short}\n"
480 480 > hg status -amr
481 481 > # test that pending changes are hidden
482 482 > unset HG_PENDING
483 483 > hg parents --template "ACTUAL {rev}:{node|short}\n"
484 484 > hg status -amr
485 485 > echo "===="
486 486 > EOF
487 487
488 488 == test visibility to external editor
489 489
490 490 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
491 491 ====
492 492 VISIBLE 0:80971e65b431
493 493 ACTUAL 0:80971e65b431
494 494 ====
495 495
496 496 $ HGEDITOR="sh $TESTTMP/checkvisibility.sh" hg --cwd b import -v --edit ../patch1 ../patch2 ../patch3
497 497 applying ../patch1
498 498 patching file a
499 499 ====
500 500 VISIBLE 0:80971e65b431
501 501 M a
502 502 ACTUAL 0:80971e65b431
503 503 M a
504 504 ====
505 505 committing files:
506 506 a
507 507 committing manifest
508 508 committing changelog
509 509 created 1d4bd90af0e4
510 510 applying ../patch2
511 511 patching file a
512 512 ====
513 513 VISIBLE 1:1d4bd90af0e4
514 514 M a
515 515 ACTUAL 0:80971e65b431
516 516 M a
517 517 ====
518 518 committing files:
519 519 a
520 520 committing manifest
521 521 committing changelog
522 522 created 6d019af21222
523 523 applying ../patch3
524 524 patching file foo
525 525 adding foo
526 526 ====
527 527 VISIBLE 2:6d019af21222
528 528 A foo
529 529 ACTUAL 0:80971e65b431
530 530 M a
531 531 ====
532 532 committing files:
533 533 foo
534 534 committing manifest
535 535 committing changelog
536 536 created 55e3f75b2378
537 537
538 538 $ hg --cwd b rollback -q
539 539
540 540 (content of file "a" is already changed and it should be recognized as
541 541 "M", even though dirstate is restored to one before "hg import")
542 542
543 543 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
544 544 ====
545 545 VISIBLE 0:80971e65b431
546 546 M a
547 547 ACTUAL 0:80971e65b431
548 548 M a
549 549 ====
550 550 $ hg --cwd b revert --no-backup a
551 551 $ rm -f b/foo
552 552
553 553 == test visibility to precommit external hook
554 554
555 555 $ cat >> b/.hg/hgrc <<EOF
556 556 > [hooks]
557 557 > precommit.visibility = sh $TESTTMP/checkvisibility.sh
558 558 > EOF
559 559
560 560 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
561 561 ====
562 562 VISIBLE 0:80971e65b431
563 563 ACTUAL 0:80971e65b431
564 564 ====
565 565
566 566 $ hg --cwd b import ../patch1 ../patch2 ../patch3
567 567 applying ../patch1
568 568 ====
569 569 VISIBLE 0:80971e65b431
570 570 M a
571 571 ACTUAL 0:80971e65b431
572 572 M a
573 573 ====
574 574 applying ../patch2
575 575 ====
576 576 VISIBLE 1:1d4bd90af0e4
577 577 M a
578 578 ACTUAL 0:80971e65b431
579 579 M a
580 580 ====
581 581 applying ../patch3
582 582 ====
583 583 VISIBLE 2:6d019af21222
584 584 A foo
585 585 ACTUAL 0:80971e65b431
586 586 M a
587 587 ====
588 588
589 589 $ hg --cwd b rollback -q
590 590 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
591 591 ====
592 592 VISIBLE 0:80971e65b431
593 593 M a
594 594 ACTUAL 0:80971e65b431
595 595 M a
596 596 ====
597 597 $ hg --cwd b revert --no-backup a
598 598 $ rm -f b/foo
599 599
600 600 $ cat >> b/.hg/hgrc <<EOF
601 601 > [hooks]
602 602 > precommit.visibility =
603 603 > EOF
604 604
605 605 == test visibility to pretxncommit external hook
606 606
607 607 $ cat >> b/.hg/hgrc <<EOF
608 608 > [hooks]
609 609 > pretxncommit.visibility = sh $TESTTMP/checkvisibility.sh
610 610 > EOF
611 611
612 612 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
613 613 ====
614 614 VISIBLE 0:80971e65b431
615 615 ACTUAL 0:80971e65b431
616 616 ====
617 617
618 618 $ hg --cwd b import ../patch1 ../patch2 ../patch3
619 619 applying ../patch1
620 620 ====
621 621 VISIBLE 0:80971e65b431
622 622 M a
623 623 ACTUAL 0:80971e65b431
624 624 M a
625 625 ====
626 626 applying ../patch2
627 627 ====
628 628 VISIBLE 1:1d4bd90af0e4
629 629 M a
630 630 ACTUAL 0:80971e65b431
631 631 M a
632 632 ====
633 633 applying ../patch3
634 634 ====
635 635 VISIBLE 2:6d019af21222
636 636 A foo
637 637 ACTUAL 0:80971e65b431
638 638 M a
639 639 ====
640 640
641 641 $ hg --cwd b rollback -q
642 642 $ (cd b && sh "$TESTTMP/checkvisibility.sh")
643 643 ====
644 644 VISIBLE 0:80971e65b431
645 645 M a
646 646 ACTUAL 0:80971e65b431
647 647 M a
648 648 ====
649 649 $ hg --cwd b revert --no-backup a
650 650 $ rm -f b/foo
651 651
652 652 $ cat >> b/.hg/hgrc <<EOF
653 653 > [hooks]
654 654 > pretxncommit.visibility =
655 655 > EOF
656 656
657 657 $ rm -r b
658 658
659 659
660 660 importing a patch in a subdirectory failed at the commit stage
661 661
662 662 $ echo line 2 >> a/d1/d2/a
663 663 $ hg --cwd a ci -u someoneelse -d '1 0' -m'subdir change'
664 664
665 665 hg import in a subdirectory
666 666
667 667 $ hg clone -r0 a b
668 668 adding changesets
669 669 adding manifests
670 670 adding file changes
671 671 added 1 changesets with 2 changes to 2 files
672 672 new changesets 80971e65b431
673 673 updating to branch default
674 674 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
675 675 $ hg --cwd a export tip > tmp
676 676 $ sed -e 's/d1\/d2\///' < tmp > subdir-tip.patch
677 677 $ dir=`pwd`
678 678 $ cd b/d1/d2 2>&1 > /dev/null
679 679 $ hg import ../../../subdir-tip.patch
680 680 applying ../../../subdir-tip.patch
681 681 $ cd "$dir"
682 682
683 683 message should be 'subdir change'
684 684 committer should be 'someoneelse'
685 685
686 686 $ hg --cwd b tip
687 687 changeset: 1:3577f5aea227
688 688 tag: tip
689 689 user: someoneelse
690 690 date: Thu Jan 01 00:00:01 1970 +0000
691 691 summary: subdir change
692 692
693 693
694 694 should be empty
695 695
696 696 $ hg --cwd b status
697 697
698 698
699 699 Test fuzziness (ambiguous patch location, fuzz=2)
700 700
701 701 $ hg init fuzzy
702 702 $ cd fuzzy
703 703 $ echo line1 > a
704 704 $ echo line0 >> a
705 705 $ echo line3 >> a
706 706 $ hg ci -Am adda
707 707 adding a
708 708 $ echo line1 > a
709 709 $ echo line2 >> a
710 710 $ echo line0 >> a
711 711 $ echo line3 >> a
712 712 $ hg ci -m change a
713 713 $ hg export tip > fuzzy-tip.patch
714 714 $ hg up -C 0
715 715 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
716 716 $ echo line1 > a
717 717 $ echo line0 >> a
718 718 $ echo line1 >> a
719 719 $ echo line0 >> a
720 720 $ hg ci -m brancha
721 721 created new head
722 722 $ hg import --config patch.fuzz=0 -v fuzzy-tip.patch
723 723 applying fuzzy-tip.patch
724 724 patching file a
725 725 Hunk #1 FAILED at 0
726 726 1 out of 1 hunks FAILED -- saving rejects to file a.rej
727 727 abort: patch failed to apply
728 728 [255]
729 729 $ hg import --no-commit -v fuzzy-tip.patch
730 730 applying fuzzy-tip.patch
731 731 patching file a
732 732 Hunk #1 succeeded at 2 with fuzz 1 (offset 0 lines).
733 733 applied to working directory
734 734 $ hg revert -a
735 735 reverting a
736 736
737 737
738 738 import with --no-commit should have written .hg/last-message.txt
739 739
740 740 $ cat .hg/last-message.txt
741 741 change (no-eol)
742 742
743 743
744 744 test fuzziness with eol=auto
745 745
746 746 $ hg --config patch.eol=auto import --no-commit -v fuzzy-tip.patch
747 747 applying fuzzy-tip.patch
748 748 patching file a
749 749 Hunk #1 succeeded at 2 with fuzz 1 (offset 0 lines).
750 750 applied to working directory
751 751 $ cd ..
752 752
753 753
754 754 Test hunk touching empty files (issue906)
755 755
756 756 $ hg init empty
757 757 $ cd empty
758 758 $ touch a
759 759 $ touch b1
760 760 $ touch c1
761 761 $ echo d > d
762 762 $ hg ci -Am init
763 763 adding a
764 764 adding b1
765 765 adding c1
766 766 adding d
767 767 $ echo a > a
768 768 $ echo b > b1
769 769 $ hg mv b1 b2
770 770 $ echo c > c1
771 771 $ hg copy c1 c2
772 772 $ rm d
773 773 $ touch d
774 774 $ hg diff --git
775 775 diff --git a/a b/a
776 776 --- a/a
777 777 +++ b/a
778 778 @@ -0,0 +1,1 @@
779 779 +a
780 780 diff --git a/b1 b/b2
781 781 rename from b1
782 782 rename to b2
783 783 --- a/b1
784 784 +++ b/b2
785 785 @@ -0,0 +1,1 @@
786 786 +b
787 787 diff --git a/c1 b/c1
788 788 --- a/c1
789 789 +++ b/c1
790 790 @@ -0,0 +1,1 @@
791 791 +c
792 792 diff --git a/c1 b/c2
793 793 copy from c1
794 794 copy to c2
795 795 --- a/c1
796 796 +++ b/c2
797 797 @@ -0,0 +1,1 @@
798 798 +c
799 799 diff --git a/d b/d
800 800 --- a/d
801 801 +++ b/d
802 802 @@ -1,1 +0,0 @@
803 803 -d
804 804 $ hg ci -m empty
805 805 $ hg export --git tip > empty.diff
806 806 $ hg up -C 0
807 807 4 files updated, 0 files merged, 2 files removed, 0 files unresolved
808 808 $ hg import empty.diff
809 809 applying empty.diff
810 810 $ for name in a b1 b2 c1 c2 d; do
811 811 > echo % $name file
812 812 > test -f $name && cat $name
813 813 > done
814 814 % a file
815 815 a
816 816 % b1 file
817 817 % b2 file
818 818 b
819 819 % c1 file
820 820 c
821 821 % c2 file
822 822 c
823 823 % d file
824 824 $ cd ..
825 825
826 826
827 827 Test importing a patch ending with a binary file removal
828 828
829 829 $ hg init binaryremoval
830 830 $ cd binaryremoval
831 831 $ echo a > a
832 832 $ $PYTHON -c "open('b', 'wb').write(b'a\x00b')"
833 833 $ hg ci -Am addall
834 834 adding a
835 835 adding b
836 836 $ hg rm a
837 837 $ hg rm b
838 838 $ hg st
839 839 R a
840 840 R b
841 841 $ hg ci -m remove
842 842 $ hg export --git . > remove.diff
843 843 $ cat remove.diff | grep git
844 844 diff --git a/a b/a
845 845 diff --git a/b b/b
846 846 $ hg up -C 0
847 847 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
848 848 $ hg import remove.diff
849 849 applying remove.diff
850 850 $ hg manifest
851 851 $ cd ..
852 852
853 853
854 854 Issue927: test update+rename with common name
855 855
856 856 $ hg init t
857 857 $ cd t
858 858 $ touch a
859 859 $ hg ci -Am t
860 860 adding a
861 861 $ echo a > a
862 862
863 863 Here, bfile.startswith(afile)
864 864
865 865 $ hg copy a a2
866 866 $ hg ci -m copya
867 867 $ hg export --git tip > copy.diff
868 868 $ hg up -C 0
869 869 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
870 870 $ hg import copy.diff
871 871 applying copy.diff
872 872
873 873 a should contain an 'a'
874 874
875 875 $ cat a
876 876 a
877 877
878 878 and a2 should have duplicated it
879 879
880 880 $ cat a2
881 881 a
882 882 $ cd ..
883 883
884 884
885 885 test -p0
886 886
887 887 $ hg init p0
888 888 $ cd p0
889 889 $ echo a > a
890 890 $ hg ci -Am t
891 891 adding a
892 892 $ hg import -p foo
893 893 abort: invalid value 'foo' for option -p, expected int
894 894 [255]
895 895 $ hg import -p0 - << EOF
896 896 > foobar
897 897 > --- a Sat Apr 12 22:43:58 2008 -0400
898 898 > +++ a Sat Apr 12 22:44:05 2008 -0400
899 899 > @@ -1,1 +1,1 @@
900 900 > -a
901 901 > +bb
902 902 > EOF
903 903 applying patch from stdin
904 904 $ hg status
905 905 $ cat a
906 906 bb
907 907
908 908 test --prefix
909 909
910 910 $ mkdir -p dir/dir2
911 911 $ echo b > dir/dir2/b
912 912 $ hg ci -Am b
913 913 adding dir/dir2/b
914 914 $ hg import -p2 --prefix dir - << EOF
915 915 > foobar
916 916 > --- drop1/drop2/dir2/b
917 917 > +++ drop1/drop2/dir2/b
918 918 > @@ -1,1 +1,1 @@
919 919 > -b
920 920 > +cc
921 921 > EOF
922 922 applying patch from stdin
923 923 $ hg status
924 924 $ cat dir/dir2/b
925 925 cc
926 926 $ cd ..
927 927
928 928
929 929 test paths outside repo root
930 930
931 931 $ mkdir outside
932 932 $ touch outside/foo
933 933 $ hg init inside
934 934 $ cd inside
935 935 $ hg import - <<EOF
936 936 > diff --git a/a b/b
937 937 > rename from ../outside/foo
938 938 > rename to bar
939 939 > EOF
940 940 applying patch from stdin
941 941 abort: path contains illegal component: ../outside/foo
942 942 [255]
943 943 $ cd ..
944 944
945 945
946 946 test import with similarity and git and strip (issue295 et al.)
947 947
948 948 $ hg init sim
949 949 $ cd sim
950 950 $ echo 'this is a test' > a
951 951 $ hg ci -Ama
952 952 adding a
953 953 $ cat > ../rename.diff <<EOF
954 954 > diff --git a/foo/a b/foo/a
955 955 > deleted file mode 100644
956 956 > --- a/foo/a
957 957 > +++ /dev/null
958 958 > @@ -1,1 +0,0 @@
959 959 > -this is a test
960 960 > diff --git a/foo/b b/foo/b
961 961 > new file mode 100644
962 962 > --- /dev/null
963 963 > +++ b/foo/b
964 964 > @@ -0,0 +1,2 @@
965 965 > +this is a test
966 966 > +foo
967 967 > EOF
968 968 $ hg import --no-commit -v -s 1 ../rename.diff -p2
969 969 applying ../rename.diff
970 970 patching file a
971 971 patching file b
972 972 adding b
973 973 recording removal of a as rename to b (88% similar)
974 974 applied to working directory
975 975 $ echo 'mod b' > b
976 976 $ hg st -C
977 977 A b
978 978 a
979 979 R a
980 980 $ hg revert -a
981 981 undeleting a
982 982 forgetting b
983 983 $ cat b
984 984 mod b
985 985 $ rm b
986 986 $ hg import --no-commit -v -s 100 ../rename.diff -p2
987 987 applying ../rename.diff
988 988 patching file a
989 989 patching file b
990 990 adding b
991 991 applied to working directory
992 992 $ hg st -C
993 993 A b
994 994 R a
995 995 $ cd ..
996 996
997 997
998 998 Issue1495: add empty file from the end of patch
999 999
1000 1000 $ hg init addemptyend
1001 1001 $ cd addemptyend
1002 1002 $ touch a
1003 1003 $ hg addremove
1004 1004 adding a
1005 1005 $ hg ci -m "commit"
1006 1006 $ cat > a.patch <<EOF
1007 1007 > add a, b
1008 1008 > diff --git a/a b/a
1009 1009 > --- a/a
1010 1010 > +++ b/a
1011 1011 > @@ -0,0 +1,1 @@
1012 1012 > +a
1013 1013 > diff --git a/b b/b
1014 1014 > new file mode 100644
1015 1015 > EOF
1016 1016 $ hg import --no-commit a.patch
1017 1017 applying a.patch
1018 1018
1019 1019 apply a good patch followed by an empty patch (mainly to ensure
1020 1020 that dirstate is *not* updated when import crashes)
1021 1021 $ hg update -q -C .
1022 1022 $ rm b
1023 1023 $ touch empty.patch
1024 1024 $ hg import a.patch empty.patch
1025 1025 applying a.patch
1026 1026 applying empty.patch
1027 1027 transaction abort!
1028 1028 rollback completed
1029 1029 abort: empty.patch: no diffs found
1030 1030 [255]
1031 1031 $ hg tip --template '{rev} {desc|firstline}\n'
1032 1032 0 commit
1033 1033 $ hg -q status
1034 1034 M a
1035 1035 $ cd ..
1036 1036
1037 1037 create file when source is not /dev/null
1038 1038
1039 1039 $ cat > create.patch <<EOF
1040 1040 > diff -Naur proj-orig/foo proj-new/foo
1041 1041 > --- proj-orig/foo 1969-12-31 16:00:00.000000000 -0800
1042 1042 > +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
1043 1043 > @@ -0,0 +1,1 @@
1044 1044 > +a
1045 1045 > EOF
1046 1046
1047 1047 some people have patches like the following too
1048 1048
1049 1049 $ cat > create2.patch <<EOF
1050 1050 > diff -Naur proj-orig/foo proj-new/foo
1051 1051 > --- proj-orig/foo.orig 1969-12-31 16:00:00.000000000 -0800
1052 1052 > +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
1053 1053 > @@ -0,0 +1,1 @@
1054 1054 > +a
1055 1055 > EOF
1056 1056 $ hg init oddcreate
1057 1057 $ cd oddcreate
1058 1058 $ hg import --no-commit ../create.patch
1059 1059 applying ../create.patch
1060 1060 $ cat foo
1061 1061 a
1062 1062 $ rm foo
1063 1063 $ hg revert foo
1064 1064 $ hg import --no-commit ../create2.patch
1065 1065 applying ../create2.patch
1066 1066 $ cat foo
1067 1067 a
1068 1068
1069 1069 $ cd ..
1070 1070
1071 1071 Issue1859: first line mistaken for email headers
1072 1072
1073 1073 $ hg init emailconfusion
1074 1074 $ cd emailconfusion
1075 1075 $ cat > a.patch <<EOF
1076 1076 > module: summary
1077 1077 >
1078 1078 > description
1079 1079 >
1080 1080 >
1081 1081 > diff -r 000000000000 -r 9b4c1e343b55 test.txt
1082 1082 > --- /dev/null
1083 1083 > +++ b/a
1084 1084 > @@ -0,0 +1,1 @@
1085 1085 > +a
1086 1086 > EOF
1087 1087 $ hg import -d '0 0' a.patch
1088 1088 applying a.patch
1089 1089 $ hg parents -v
1090 1090 changeset: 0:5a681217c0ad
1091 1091 tag: tip
1092 1092 user: test
1093 1093 date: Thu Jan 01 00:00:00 1970 +0000
1094 1094 files: a
1095 1095 description:
1096 1096 module: summary
1097 1097
1098 1098 description
1099 1099
1100 1100
1101 1101 $ cd ..
1102 1102
1103 1103
1104 1104 in commit message
1105 1105
1106 1106 $ hg init commitconfusion
1107 1107 $ cd commitconfusion
1108 1108 $ cat > a.patch <<EOF
1109 1109 > module: summary
1110 1110 >
1111 1111 > --- description
1112 1112 >
1113 1113 > diff --git a/a b/a
1114 1114 > new file mode 100644
1115 1115 > --- /dev/null
1116 1116 > +++ b/a
1117 1117 > @@ -0,0 +1,1 @@
1118 1118 > +a
1119 1119 > EOF
1120 1120 > hg import -d '0 0' a.patch
1121 1121 > hg parents -v
1122 1122 > cd ..
1123 1123 >
1124 1124 > echo '% tricky header splitting'
1125 1125 > cat > trickyheaders.patch <<EOF
1126 1126 > From: User A <user@a>
1127 1127 > Subject: [PATCH] from: tricky!
1128 1128 >
1129 1129 > # HG changeset patch
1130 1130 > # User User B
1131 1131 > # Date 1266264441 18000
1132 1132 > # Branch stable
1133 1133 > # Node ID f2be6a1170ac83bf31cb4ae0bad00d7678115bc0
1134 1134 > # Parent 0000000000000000000000000000000000000000
1135 1135 > from: tricky!
1136 1136 >
1137 1137 > That is not a header.
1138 1138 >
1139 1139 > diff -r 000000000000 -r f2be6a1170ac foo
1140 1140 > --- /dev/null
1141 1141 > +++ b/foo
1142 1142 > @@ -0,0 +1,1 @@
1143 1143 > +foo
1144 1144 > EOF
1145 1145 applying a.patch
1146 1146 changeset: 0:f34d9187897d
1147 1147 tag: tip
1148 1148 user: test
1149 1149 date: Thu Jan 01 00:00:00 1970 +0000
1150 1150 files: a
1151 1151 description:
1152 1152 module: summary
1153 1153
1154 1154
1155 1155 % tricky header splitting
1156 1156
1157 1157 $ hg init trickyheaders
1158 1158 $ cd trickyheaders
1159 1159 $ hg import -d '0 0' ../trickyheaders.patch
1160 1160 applying ../trickyheaders.patch
1161 1161 $ hg export --git tip
1162 1162 # HG changeset patch
1163 1163 # User User B
1164 1164 # Date 0 0
1165 1165 # Thu Jan 01 00:00:00 1970 +0000
1166 1166 # Node ID eb56ab91903632294ac504838508cb370c0901d2
1167 1167 # Parent 0000000000000000000000000000000000000000
1168 1168 from: tricky!
1169 1169
1170 1170 That is not a header.
1171 1171
1172 1172 diff --git a/foo b/foo
1173 1173 new file mode 100644
1174 1174 --- /dev/null
1175 1175 +++ b/foo
1176 1176 @@ -0,0 +1,1 @@
1177 1177 +foo
1178 1178 $ cd ..
1179 1179
1180 1180
1181 1181 Issue2102: hg export and hg import speak different languages
1182 1182
1183 1183 $ hg init issue2102
1184 1184 $ cd issue2102
1185 1185 $ mkdir -p src/cmd/gc
1186 1186 $ touch src/cmd/gc/mksys.bash
1187 1187 $ hg ci -Am init
1188 1188 adding src/cmd/gc/mksys.bash
1189 1189 $ hg import - <<EOF
1190 1190 > # HG changeset patch
1191 1191 > # User Rob Pike
1192 1192 > # Date 1216685449 25200
1193 1193 > # Node ID 03aa2b206f499ad6eb50e6e207b9e710d6409c98
1194 1194 > # Parent 93d10138ad8df586827ca90b4ddb5033e21a3a84
1195 1195 > help management of empty pkg and lib directories in perforce
1196 1196 >
1197 1197 > R=gri
1198 1198 > DELTA=4 (4 added, 0 deleted, 0 changed)
1199 1199 > OCL=13328
1200 1200 > CL=13328
1201 1201 >
1202 1202 > diff --git a/lib/place-holder b/lib/place-holder
1203 1203 > new file mode 100644
1204 1204 > --- /dev/null
1205 1205 > +++ b/lib/place-holder
1206 1206 > @@ -0,0 +1,2 @@
1207 1207 > +perforce does not maintain empty directories.
1208 1208 > +this file helps.
1209 1209 > diff --git a/pkg/place-holder b/pkg/place-holder
1210 1210 > new file mode 100644
1211 1211 > --- /dev/null
1212 1212 > +++ b/pkg/place-holder
1213 1213 > @@ -0,0 +1,2 @@
1214 1214 > +perforce does not maintain empty directories.
1215 1215 > +this file helps.
1216 1216 > diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
1217 1217 > old mode 100644
1218 1218 > new mode 100755
1219 1219 > EOF
1220 1220 applying patch from stdin
1221 1221
1222 1222 #if execbit
1223 1223
1224 1224 $ hg sum
1225 1225 parent: 1:d59915696727 tip
1226 1226 help management of empty pkg and lib directories in perforce
1227 1227 branch: default
1228 1228 commit: (clean)
1229 1229 update: (current)
1230 1230 phases: 2 draft
1231 1231
1232 1232 $ hg diff --git -c tip
1233 1233 diff --git a/lib/place-holder b/lib/place-holder
1234 1234 new file mode 100644
1235 1235 --- /dev/null
1236 1236 +++ b/lib/place-holder
1237 1237 @@ -0,0 +1,2 @@
1238 1238 +perforce does not maintain empty directories.
1239 1239 +this file helps.
1240 1240 diff --git a/pkg/place-holder b/pkg/place-holder
1241 1241 new file mode 100644
1242 1242 --- /dev/null
1243 1243 +++ b/pkg/place-holder
1244 1244 @@ -0,0 +1,2 @@
1245 1245 +perforce does not maintain empty directories.
1246 1246 +this file helps.
1247 1247 diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
1248 1248 old mode 100644
1249 1249 new mode 100755
1250 1250
1251 1251 #else
1252 1252
1253 1253 $ hg sum
1254 1254 parent: 1:28f089cc9ccc tip
1255 1255 help management of empty pkg and lib directories in perforce
1256 1256 branch: default
1257 1257 commit: (clean)
1258 1258 update: (current)
1259 1259 phases: 2 draft
1260 1260
1261 1261 $ hg diff --git -c tip
1262 1262 diff --git a/lib/place-holder b/lib/place-holder
1263 1263 new file mode 100644
1264 1264 --- /dev/null
1265 1265 +++ b/lib/place-holder
1266 1266 @@ -0,0 +1,2 @@
1267 1267 +perforce does not maintain empty directories.
1268 1268 +this file helps.
1269 1269 diff --git a/pkg/place-holder b/pkg/place-holder
1270 1270 new file mode 100644
1271 1271 --- /dev/null
1272 1272 +++ b/pkg/place-holder
1273 1273 @@ -0,0 +1,2 @@
1274 1274 +perforce does not maintain empty directories.
1275 1275 +this file helps.
1276 1276
1277 1277 /* The mode change for mksys.bash is missing here, because on platforms */
1278 1278 /* that don't support execbits, mode changes in patches are ignored when */
1279 1279 /* they are imported. This is obviously also the reason for why the hash */
1280 1280 /* in the created changeset is different to the one you see above the */
1281 1281 /* #else clause */
1282 1282
1283 1283 #endif
1284 1284 $ cd ..
1285 1285
1286 1286
1287 1287 diff lines looking like headers
1288 1288
1289 1289 $ hg init difflineslikeheaders
1290 1290 $ cd difflineslikeheaders
1291 1291 $ echo a >a
1292 1292 $ echo b >b
1293 1293 $ echo c >c
1294 1294 $ hg ci -Am1
1295 1295 adding a
1296 1296 adding b
1297 1297 adding c
1298 1298
1299 1299 $ echo "key: value" >>a
1300 1300 $ echo "key: value" >>b
1301 1301 $ echo "foo" >>c
1302 1302 $ hg ci -m2
1303 1303
1304 1304 $ hg up -C 0
1305 1305 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1306 1306 $ hg diff --git -c1 >want
1307 1307 $ hg diff -c1 | hg import --no-commit -
1308 1308 applying patch from stdin
1309 1309 $ hg diff --git >have
1310 1310 $ diff want have
1311 1311 $ cd ..
1312 1312
1313 1313 import a unified diff with no lines of context (diff -U0)
1314 1314
1315 1315 $ hg init diffzero
1316 1316 $ cd diffzero
1317 1317 $ cat > f << EOF
1318 1318 > c2
1319 1319 > c4
1320 1320 > c5
1321 1321 > EOF
1322 1322 $ hg commit -Am0
1323 1323 adding f
1324 1324
1325 1325 $ hg import --no-commit - << EOF
1326 1326 > # HG changeset patch
1327 1327 > # User test
1328 1328 > # Date 0 0
1329 1329 > # Node ID f4974ab632f3dee767567b0576c0ec9a4508575c
1330 1330 > # Parent 8679a12a975b819fae5f7ad3853a2886d143d794
1331 1331 > 1
1332 1332 > diff -r 8679a12a975b -r f4974ab632f3 f
1333 1333 > --- a/f Thu Jan 01 00:00:00 1970 +0000
1334 1334 > +++ b/f Thu Jan 01 00:00:00 1970 +0000
1335 1335 > @@ -0,0 +1,1 @@
1336 1336 > +c1
1337 1337 > @@ -1,0 +3,1 @@
1338 1338 > +c3
1339 1339 > @@ -3,1 +4,0 @@
1340 1340 > -c5
1341 1341 > EOF
1342 1342 applying patch from stdin
1343 1343
1344 1344 $ cat f
1345 1345 c1
1346 1346 c2
1347 1347 c3
1348 1348 c4
1349 1349
1350 1350 $ cd ..
1351 1351
1352 1352 commit message that looks like a diff header (issue1879)
1353 1353
1354 1354 $ hg init headerlikemsg
1355 1355 $ cd headerlikemsg
1356 1356 $ touch empty
1357 1357 $ echo nonempty >> nonempty
1358 1358 $ hg ci -qAl - <<EOF
1359 1359 > blah blah
1360 1360 > diff blah
1361 1361 > blah blah
1362 1362 > EOF
1363 1363 $ hg --config diff.git=1 log -pv
1364 1364 changeset: 0:c6ef204ef767
1365 1365 tag: tip
1366 1366 user: test
1367 1367 date: Thu Jan 01 00:00:00 1970 +0000
1368 1368 files: empty nonempty
1369 1369 description:
1370 1370 blah blah
1371 1371 diff blah
1372 1372 blah blah
1373 1373
1374 1374
1375 1375 diff --git a/empty b/empty
1376 1376 new file mode 100644
1377 1377 diff --git a/nonempty b/nonempty
1378 1378 new file mode 100644
1379 1379 --- /dev/null
1380 1380 +++ b/nonempty
1381 1381 @@ -0,0 +1,1 @@
1382 1382 +nonempty
1383 1383
1384 1384
1385 1385 (without --git, empty file is lost, but commit message should be preserved)
1386 1386
1387 1387 $ hg init plain
1388 1388 $ hg export 0 | hg -R plain import -
1389 1389 applying patch from stdin
1390 1390 $ hg --config diff.git=1 -R plain log -pv
1391 1391 changeset: 0:60a2d231e71f
1392 1392 tag: tip
1393 1393 user: test
1394 1394 date: Thu Jan 01 00:00:00 1970 +0000
1395 1395 files: nonempty
1396 1396 description:
1397 1397 blah blah
1398 1398 diff blah
1399 1399 blah blah
1400 1400
1401 1401
1402 1402 diff --git a/nonempty b/nonempty
1403 1403 new file mode 100644
1404 1404 --- /dev/null
1405 1405 +++ b/nonempty
1406 1406 @@ -0,0 +1,1 @@
1407 1407 +nonempty
1408 1408
1409 1409
1410 1410 (with --git, patch contents should be fully preserved)
1411 1411
1412 1412 $ hg init git
1413 1413 $ hg --config diff.git=1 export 0 | hg -R git import -
1414 1414 applying patch from stdin
1415 1415 $ hg --config diff.git=1 -R git log -pv
1416 1416 changeset: 0:c6ef204ef767
1417 1417 tag: tip
1418 1418 user: test
1419 1419 date: Thu Jan 01 00:00:00 1970 +0000
1420 1420 files: empty nonempty
1421 1421 description:
1422 1422 blah blah
1423 1423 diff blah
1424 1424 blah blah
1425 1425
1426 1426
1427 1427 diff --git a/empty b/empty
1428 1428 new file mode 100644
1429 1429 diff --git a/nonempty b/nonempty
1430 1430 new file mode 100644
1431 1431 --- /dev/null
1432 1432 +++ b/nonempty
1433 1433 @@ -0,0 +1,1 @@
1434 1434 +nonempty
1435 1435
1436 1436
1437 1437 $ cd ..
1438 1438
1439 1439 no segfault while importing a unified diff which start line is zero but chunk
1440 1440 size is non-zero
1441 1441
1442 1442 $ hg init startlinezero
1443 1443 $ cd startlinezero
1444 1444 $ echo foo > foo
1445 1445 $ hg commit -Amfoo
1446 1446 adding foo
1447 1447
1448 1448 $ hg import --no-commit - << EOF
1449 1449 > diff a/foo b/foo
1450 1450 > --- a/foo
1451 1451 > +++ b/foo
1452 1452 > @@ -0,1 +0,1 @@
1453 1453 > foo
1454 1454 > EOF
1455 1455 applying patch from stdin
1456 1456
1457 1457 $ cd ..
1458 1458
1459 1459 Test corner case involving fuzz and skew
1460 1460
1461 1461 $ hg init morecornercases
1462 1462 $ cd morecornercases
1463 1463
1464 1464 $ cat > 01-no-context-beginning-of-file.diff <<EOF
1465 1465 > diff --git a/a b/a
1466 1466 > --- a/a
1467 1467 > +++ b/a
1468 1468 > @@ -1,0 +1,1 @@
1469 1469 > +line
1470 1470 > EOF
1471 1471
1472 1472 $ cat > 02-no-context-middle-of-file.diff <<EOF
1473 1473 > diff --git a/a b/a
1474 1474 > --- a/a
1475 1475 > +++ b/a
1476 1476 > @@ -1,1 +1,1 @@
1477 1477 > -2
1478 1478 > +add some skew
1479 1479 > @@ -2,0 +2,1 @@
1480 1480 > +line
1481 1481 > EOF
1482 1482
1483 1483 $ cat > 03-no-context-end-of-file.diff <<EOF
1484 1484 > diff --git a/a b/a
1485 1485 > --- a/a
1486 1486 > +++ b/a
1487 1487 > @@ -10,0 +10,1 @@
1488 1488 > +line
1489 1489 > EOF
1490 1490
1491 1491 $ cat > 04-middle-of-file-completely-fuzzed.diff <<EOF
1492 1492 > diff --git a/a b/a
1493 1493 > --- a/a
1494 1494 > +++ b/a
1495 1495 > @@ -1,1 +1,1 @@
1496 1496 > -2
1497 1497 > +add some skew
1498 1498 > @@ -2,2 +2,3 @@
1499 1499 > not matching, should fuzz
1500 1500 > ... a bit
1501 1501 > +line
1502 1502 > EOF
1503 1503
1504 1504 $ cat > a <<EOF
1505 1505 > 1
1506 1506 > 2
1507 1507 > 3
1508 1508 > 4
1509 1509 > EOF
1510 1510 $ hg ci -Am adda a
1511 1511 $ for p in *.diff; do
1512 1512 > hg import -v --no-commit $p
1513 1513 > cat a
1514 1514 > hg revert -aqC a
1515 1515 > # patch -p1 < $p
1516 1516 > # cat a
1517 1517 > # hg revert -aC a
1518 1518 > done
1519 1519 applying 01-no-context-beginning-of-file.diff
1520 1520 patching file a
1521 1521 applied to working directory
1522 1522 1
1523 1523 line
1524 1524 2
1525 1525 3
1526 1526 4
1527 1527 applying 02-no-context-middle-of-file.diff
1528 1528 patching file a
1529 1529 Hunk #1 succeeded at 2 (offset 1 lines).
1530 1530 Hunk #2 succeeded at 4 (offset 1 lines).
1531 1531 applied to working directory
1532 1532 1
1533 1533 add some skew
1534 1534 3
1535 1535 line
1536 1536 4
1537 1537 applying 03-no-context-end-of-file.diff
1538 1538 patching file a
1539 1539 Hunk #1 succeeded at 5 (offset -6 lines).
1540 1540 applied to working directory
1541 1541 1
1542 1542 2
1543 1543 3
1544 1544 4
1545 1545 line
1546 1546 applying 04-middle-of-file-completely-fuzzed.diff
1547 1547 patching file a
1548 1548 Hunk #1 succeeded at 2 (offset 1 lines).
1549 1549 Hunk #2 succeeded at 5 with fuzz 2 (offset 1 lines).
1550 1550 applied to working directory
1551 1551 1
1552 1552 add some skew
1553 1553 3
1554 1554 4
1555 1555 line
1556 1556 $ cd ..
1557 1557
1558 1558 Test partial application
1559 1559 ------------------------
1560 1560
1561 1561 prepare a stack of patches depending on each other
1562 1562
1563 1563 $ hg init partial
1564 1564 $ cd partial
1565 1565 $ cat << EOF > a
1566 1566 > one
1567 1567 > two
1568 1568 > three
1569 1569 > four
1570 1570 > five
1571 1571 > six
1572 1572 > seven
1573 1573 > EOF
1574 1574 $ hg add a
1575 1575 $ echo 'b' > b
1576 1576 $ hg add b
1577 1577 $ hg commit -m 'initial' -u Babar
1578 1578 $ cat << EOF > a
1579 1579 > one
1580 1580 > two
1581 1581 > 3
1582 1582 > four
1583 1583 > five
1584 1584 > six
1585 1585 > seven
1586 1586 > EOF
1587 1587 $ hg commit -m 'three' -u Celeste
1588 1588 $ cat << EOF > a
1589 1589 > one
1590 1590 > two
1591 1591 > 3
1592 1592 > 4
1593 1593 > five
1594 1594 > six
1595 1595 > seven
1596 1596 > EOF
1597 1597 $ hg commit -m 'four' -u Rataxes
1598 1598 $ cat << EOF > a
1599 1599 > one
1600 1600 > two
1601 1601 > 3
1602 1602 > 4
1603 1603 > 5
1604 1604 > six
1605 1605 > seven
1606 1606 > EOF
1607 1607 $ echo bb >> b
1608 1608 $ hg commit -m 'five' -u Arthur
1609 1609 $ echo 'Babar' > jungle
1610 1610 $ hg add jungle
1611 1611 $ hg ci -m 'jungle' -u Zephir
1612 1612 $ echo 'Celeste' >> jungle
1613 1613 $ hg ci -m 'extended jungle' -u Cornelius
1614 1614 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1615 1615 @ extended jungle [Cornelius] 1: +1/-0
1616 1616 |
1617 1617 o jungle [Zephir] 1: +1/-0
1618 1618 |
1619 1619 o five [Arthur] 2: +2/-1
1620 1620 |
1621 1621 o four [Rataxes] 1: +1/-1
1622 1622 |
1623 1623 o three [Celeste] 1: +1/-1
1624 1624 |
1625 1625 o initial [Babar] 2: +8/-0
1626 1626
1627 1627 Adding those config options should not change the output of diffstat. Bugfix #4755.
1628 1628
1629 1629 $ hg log -r . --template '{diffstat}\n'
1630 1630 1: +1/-0
1631 1631 $ hg log -r . --template '{diffstat}\n' --config diff.git=1 \
1632 1632 > --config diff.noprefix=1
1633 1633 1: +1/-0
1634 1634
1635 1635 Importing with some success and some errors:
1636 1636
1637 1637 $ hg update --rev 'desc(initial)'
1638 1638 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
1639 1639 $ hg export --rev 'desc(five)' | hg import --partial -
1640 1640 applying patch from stdin
1641 1641 patching file a
1642 1642 Hunk #1 FAILED at 1
1643 1643 1 out of 1 hunks FAILED -- saving rejects to file a.rej
1644 1644 patch applied partially
1645 1645 (fix the .rej files and run `hg commit --amend`)
1646 1646 [1]
1647 1647
1648 1648 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1649 1649 @ five [Arthur] 1: +1/-0
1650 1650 |
1651 1651 | o extended jungle [Cornelius] 1: +1/-0
1652 1652 | |
1653 1653 | o jungle [Zephir] 1: +1/-0
1654 1654 | |
1655 1655 | o five [Arthur] 2: +2/-1
1656 1656 | |
1657 1657 | o four [Rataxes] 1: +1/-1
1658 1658 | |
1659 1659 | o three [Celeste] 1: +1/-1
1660 1660 |/
1661 1661 o initial [Babar] 2: +8/-0
1662 1662
1663 1663 $ hg export
1664 1664 # HG changeset patch
1665 1665 # User Arthur
1666 1666 # Date 0 0
1667 1667 # Thu Jan 01 00:00:00 1970 +0000
1668 1668 # Node ID 26e6446bb2526e2be1037935f5fca2b2706f1509
1669 1669 # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
1670 1670 five
1671 1671
1672 1672 diff -r 8e4f0351909e -r 26e6446bb252 b
1673 1673 --- a/b Thu Jan 01 00:00:00 1970 +0000
1674 1674 +++ b/b Thu Jan 01 00:00:00 1970 +0000
1675 1675 @@ -1,1 +1,2 @@
1676 1676 b
1677 1677 +bb
1678 1678 $ hg status -c .
1679 1679 C a
1680 1680 C b
1681 1681 $ ls
1682 1682 a
1683 1683 a.rej
1684 1684 b
1685 1685
1686 1686 Importing with zero success:
1687 1687
1688 1688 $ hg update --rev 'desc(initial)'
1689 1689 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1690 1690 $ hg export --rev 'desc(four)' | hg import --partial -
1691 1691 applying patch from stdin
1692 1692 patching file a
1693 1693 Hunk #1 FAILED at 0
1694 1694 1 out of 1 hunks FAILED -- saving rejects to file a.rej
1695 1695 patch applied partially
1696 1696 (fix the .rej files and run `hg commit --amend`)
1697 1697 [1]
1698 1698
1699 1699 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1700 1700 @ four [Rataxes] 0: +0/-0
1701 1701 |
1702 1702 | o five [Arthur] 1: +1/-0
1703 1703 |/
1704 1704 | o extended jungle [Cornelius] 1: +1/-0
1705 1705 | |
1706 1706 | o jungle [Zephir] 1: +1/-0
1707 1707 | |
1708 1708 | o five [Arthur] 2: +2/-1
1709 1709 | |
1710 1710 | o four [Rataxes] 1: +1/-1
1711 1711 | |
1712 1712 | o three [Celeste] 1: +1/-1
1713 1713 |/
1714 1714 o initial [Babar] 2: +8/-0
1715 1715
1716 1716 $ hg export
1717 1717 # HG changeset patch
1718 1718 # User Rataxes
1719 1719 # Date 0 0
1720 1720 # Thu Jan 01 00:00:00 1970 +0000
1721 1721 # Node ID cb9b1847a74d9ad52e93becaf14b98dbcc274e1e
1722 1722 # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
1723 1723 four
1724 1724
1725 1725 $ hg status -c .
1726 1726 C a
1727 1727 C b
1728 1728 $ ls
1729 1729 a
1730 1730 a.rej
1731 1731 b
1732 1732
1733 1733 Importing with unknown file:
1734 1734
1735 1735 $ hg update --rev 'desc(initial)'
1736 1736 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1737 1737 $ hg export --rev 'desc("extended jungle")' | hg import --partial -
1738 1738 applying patch from stdin
1739 1739 unable to find 'jungle' for patching
1740 1740 (use '--prefix' to apply patch relative to the current directory)
1741 1741 1 out of 1 hunks FAILED -- saving rejects to file jungle.rej
1742 1742 patch applied partially
1743 1743 (fix the .rej files and run `hg commit --amend`)
1744 1744 [1]
1745 1745
1746 1746 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1747 1747 @ extended jungle [Cornelius] 0: +0/-0
1748 1748 |
1749 1749 | o four [Rataxes] 0: +0/-0
1750 1750 |/
1751 1751 | o five [Arthur] 1: +1/-0
1752 1752 |/
1753 1753 | o extended jungle [Cornelius] 1: +1/-0
1754 1754 | |
1755 1755 | o jungle [Zephir] 1: +1/-0
1756 1756 | |
1757 1757 | o five [Arthur] 2: +2/-1
1758 1758 | |
1759 1759 | o four [Rataxes] 1: +1/-1
1760 1760 | |
1761 1761 | o three [Celeste] 1: +1/-1
1762 1762 |/
1763 1763 o initial [Babar] 2: +8/-0
1764 1764
1765 1765 $ hg export
1766 1766 # HG changeset patch
1767 1767 # User Cornelius
1768 1768 # Date 0 0
1769 1769 # Thu Jan 01 00:00:00 1970 +0000
1770 1770 # Node ID 1fb1f86bef43c5a75918178f8d23c29fb0a7398d
1771 1771 # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e
1772 1772 extended jungle
1773 1773
1774 1774 $ hg status -c .
1775 1775 C a
1776 1776 C b
1777 1777 $ ls
1778 1778 a
1779 1779 a.rej
1780 1780 b
1781 1781 jungle.rej
1782 1782
1783 1783 Importing multiple failing patches:
1784 1784
1785 1785 $ hg update --rev 'desc(initial)'
1786 1786 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1787 1787 $ echo 'B' > b # just to make another commit
1788 1788 $ hg commit -m "a new base"
1789 1789 created new head
1790 1790 $ hg export --rev 'desc("four") + desc("extended jungle")' | hg import --partial -
1791 1791 applying patch from stdin
1792 1792 patching file a
1793 1793 Hunk #1 FAILED at 0
1794 1794 1 out of 1 hunks FAILED -- saving rejects to file a.rej
1795 1795 patch applied partially
1796 1796 (fix the .rej files and run `hg commit --amend`)
1797 1797 [1]
1798 1798 $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n'
1799 1799 @ four [Rataxes] 0: +0/-0
1800 1800 |
1801 1801 o a new base [test] 1: +1/-1
1802 1802 |
1803 1803 | o extended jungle [Cornelius] 0: +0/-0
1804 1804 |/
1805 1805 | o four [Rataxes] 0: +0/-0
1806 1806 |/
1807 1807 | o five [Arthur] 1: +1/-0
1808 1808 |/
1809 1809 | o extended jungle [Cornelius] 1: +1/-0
1810 1810 | |
1811 1811 | o jungle [Zephir] 1: +1/-0
1812 1812 | |
1813 1813 | o five [Arthur] 2: +2/-1
1814 1814 | |
1815 1815 | o four [Rataxes] 1: +1/-1
1816 1816 | |
1817 1817 | o three [Celeste] 1: +1/-1
1818 1818 |/
1819 1819 o initial [Babar] 2: +8/-0
1820 1820
1821 1821 $ hg export
1822 1822 # HG changeset patch
1823 1823 # User Rataxes
1824 1824 # Date 0 0
1825 1825 # Thu Jan 01 00:00:00 1970 +0000
1826 1826 # Node ID a9d7b6d0ffbb4eb12b7d5939250fcd42e8930a1d
1827 1827 # Parent f59f8d2e95a8ca5b1b4ca64320140da85f3b44fd
1828 1828 four
1829 1829
1830 1830 $ hg status -c .
1831 1831 C a
1832 1832 C b
1833 1833
1834 1834 Importing some extra header
1835 1835 ===========================
1836 1836
1837 1837 $ cat > $TESTTMP/parseextra.py <<EOF
1838 1838 > import mercurial.patch
1839 1839 > import mercurial.cmdutil
1840 1840 >
1841 1841 > def processfoo(repo, data, extra, opts):
1842 1842 > if b'foo' in data:
1843 1843 > extra[b'foo'] = data[b'foo']
1844 1844 > def postimport(ctx):
1845 1845 > if b'foo' in ctx.extra():
1846 1846 > ctx.repo().ui.write(b'imported-foo: %s\n' % ctx.extra()[b'foo'])
1847 1847 >
1848 1848 > mercurial.patch.patchheadermap.append((b'Foo', b'foo'))
1849 1849 > mercurial.cmdutil.extrapreimport.append(b'foo')
1850 1850 > mercurial.cmdutil.extrapreimportmap[b'foo'] = processfoo
1851 1851 > mercurial.cmdutil.extrapostimport.append(b'foo')
1852 1852 > mercurial.cmdutil.extrapostimportmap[b'foo'] = postimport
1853 1853 > EOF
1854 1854 $ cat >> $HGRCPATH <<EOF
1855 1855 > [extensions]
1856 1856 > parseextra=$TESTTMP/parseextra.py
1857 1857 > EOF
1858 1858 $ hg up -C tip
1859 1859 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1860 1860 $ cat > $TESTTMP/foo.patch <<EOF
1861 1861 > # HG changeset patch
1862 1862 > # User Rataxes
1863 1863 > # Date 0 0
1864 1864 > # Thu Jan 01 00:00:00 1970 +0000
1865 1865 > # Foo bar
1866 1866 > height
1867 1867 >
1868 1868 > --- a/a Thu Jan 01 00:00:00 1970 +0000
1869 1869 > +++ b/a Wed Oct 07 09:17:44 2015 +0000
1870 1870 > @@ -5,3 +5,4 @@
1871 1871 > five
1872 1872 > six
1873 1873 > seven
1874 1874 > +heigt
1875 1875 > EOF
1876 1876 $ hg import $TESTTMP/foo.patch
1877 1877 applying $TESTTMP/foo.patch
1878 1878 imported-foo: bar
1879 1879 $ hg log --debug -r . | grep extra
1880 1880 extra: branch=default
1881 1881 extra: foo=bar
1882 1882
1883 1883 Warn the user that paths are relative to the root of
1884 1884 repository when file not found for patching
1885 1885
1886 1886 $ mkdir filedir
1887 1887 $ echo "file1" >> filedir/file1
1888 1888 $ hg add filedir/file1
1889 1889 $ hg commit -m "file1"
1890 1890 $ cd filedir
1891 1891 $ hg import -p 2 - <<EOF
1892 1892 > # HG changeset patch
1893 1893 > # User test
1894 1894 > # Date 0 0
1895 1895 > file2
1896 1896 >
1897 1897 > diff --git a/filedir/file1 b/filedir/file1
1898 1898 > --- a/filedir/file1
1899 1899 > +++ b/filedir/file1
1900 1900 > @@ -1,1 +1,2 @@
1901 1901 > file1
1902 1902 > +file2
1903 1903 > EOF
1904 1904 applying patch from stdin
1905 1905 unable to find 'file1' for patching
1906 1906 (use '--prefix' to apply patch relative to the current directory)
1907 1907 1 out of 1 hunks FAILED -- saving rejects to file file1.rej
1908 1908 abort: patch failed to apply
1909 1909 [255]
1910 1910
1911 1911 test import crash (issue5375)
1912 1912 $ cd ..
1913 1913 $ hg init repo
1914 1914 $ cd repo
1915 1915 $ printf "diff --git a/a b/b\nrename from a\nrename to b" | hg import -
1916 1916 applying patch from stdin
1917 1917 a not tracked!
1918 1918 abort: source file 'a' does not exist
1919 1919 [255]
1920
1921 test immature end of hunk
1922
1923 $ hg import - <<'EOF'
1924 > diff --git a/foo b/foo
1925 > --- a/foo
1926 > --- b/foo
1927 > @@ -0,0 +1,1 @@
1928 > EOF
1929 applying patch from stdin
1930 abort: bad hunk #1: incomplete hunk
1931 [255]
1932
1933 $ hg import - <<'EOF'
1934 > diff --git a/foo b/foo
1935 > --- a/foo
1936 > --- b/foo
1937 > @@ -0,0 +1,1 @@
1938 > \ No newline at end of file
1939 > EOF
1940 applying patch from stdin
1941 abort: bad hunk #1: incomplete hunk
1942 [255]
General Comments 0
You need to be logged in to leave comments. Login now