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