comparison mercurial/patch.py @ 48411:6a454e7053a1

errors: return more detailed errors when failing to parse or apply patch This patch adds subclasses of `PatchError` so we can distinguish between failure to parse a patch from failure to apply it. It updates the callers to raise either `InputError` or `StateError` depending on which type of error occurred. Differential Revision: https://phab.mercurial-scm.org/D11824
author Martin von Zweigbergk <martinvonz@google.com>
date Fri, 19 Nov 2021 12:57:53 -0800
parents e0d566f3ffce
children 290f9c150f70
comparison
equal deleted inserted replaced
48410:7e6488aa1261 48411:6a454e7053a1
53 wordsplitter = re.compile( 53 wordsplitter = re.compile(
54 br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|[^ \ta-zA-Z0-9_\x80-\xff])' 54 br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|[^ \ta-zA-Z0-9_\x80-\xff])'
55 ) 55 )
56 56
57 PatchError = error.PatchError 57 PatchError = error.PatchError
58 PatchParseError = error.PatchParseError
59 PatchApplicationError = error.PatchApplicationError
58 60
59 # public functions 61 # public functions
60 62
61 63
62 def split(stream): 64 def split(stream):
551 553
552 def _checkknown(self, fname): 554 def _checkknown(self, fname):
553 if not self.repo.dirstate.get_entry(fname).any_tracked and self.exists( 555 if not self.repo.dirstate.get_entry(fname).any_tracked and self.exists(
554 fname 556 fname
555 ): 557 ):
556 raise PatchError(_(b'cannot patch %s: file is not tracked') % fname) 558 raise PatchApplicationError(
559 _(b'cannot patch %s: file is not tracked') % fname
560 )
557 561
558 def setfile(self, fname, data, mode, copysource): 562 def setfile(self, fname, data, mode, copysource):
559 self._checkknown(fname) 563 self._checkknown(fname)
560 super(workingbackend, self).setfile(fname, data, mode, copysource) 564 super(workingbackend, self).setfile(fname, data, mode, copysource)
561 if copysource is not None: 565 if copysource is not None:
635 self.removed = set() 639 self.removed = set()
636 self.copied = {} 640 self.copied = {}
637 641
638 def _checkknown(self, fname): 642 def _checkknown(self, fname):
639 if fname not in self.ctx: 643 if fname not in self.ctx:
640 raise PatchError(_(b'cannot patch %s: file is not tracked') % fname) 644 raise PatchApplicationError(
645 _(b'cannot patch %s: file is not tracked') % fname
646 )
641 647
642 def getfile(self, fname): 648 def getfile(self, fname):
643 try: 649 try:
644 fctx = self.ctx[fname] 650 fctx = self.ctx[fname]
645 except error.LookupError: 651 except error.LookupError:
791 lines.append(b'\n' + diffhelper.MISSING_NEWLINE_MARKER) 797 lines.append(b'\n' + diffhelper.MISSING_NEWLINE_MARKER)
792 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines) 798 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
793 799
794 def apply(self, h): 800 def apply(self, h):
795 if not h.complete(): 801 if not h.complete():
796 raise PatchError( 802 raise PatchParseError(
797 _(b"bad hunk #%d %s (%d %d %d %d)") 803 _(b"bad hunk #%d %s (%d %d %d %d)")
798 % (h.number, h.desc, len(h.a), h.lena, len(h.b), h.lenb) 804 % (h.number, h.desc, len(h.a), h.lena, len(h.b), h.lenb)
799 ) 805 )
800 806
801 self.hunks += 1 807 self.hunks += 1
1386 return nh 1392 return nh
1387 1393
1388 def read_unified_hunk(self, lr): 1394 def read_unified_hunk(self, lr):
1389 m = unidesc.match(self.desc) 1395 m = unidesc.match(self.desc)
1390 if not m: 1396 if not m:
1391 raise PatchError(_(b"bad hunk #%d") % self.number) 1397 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1392 self.starta, self.lena, self.startb, self.lenb = m.groups() 1398 self.starta, self.lena, self.startb, self.lenb = m.groups()
1393 if self.lena is None: 1399 if self.lena is None:
1394 self.lena = 1 1400 self.lena = 1
1395 else: 1401 else:
1396 self.lena = int(self.lena) 1402 self.lena = int(self.lena)
1403 try: 1409 try:
1404 diffhelper.addlines( 1410 diffhelper.addlines(
1405 lr, self.hunk, self.lena, self.lenb, self.a, self.b 1411 lr, self.hunk, self.lena, self.lenb, self.a, self.b
1406 ) 1412 )
1407 except error.ParseError as e: 1413 except error.ParseError as e:
1408 raise PatchError(_(b"bad hunk #%d: %s") % (self.number, e)) 1414 raise PatchParseError(_(b"bad hunk #%d: %s") % (self.number, e))
1409 # if we hit eof before finishing out the hunk, the last line will 1415 # if we hit eof before finishing out the hunk, the last line will
1410 # be zero length. Lets try to fix it up. 1416 # be zero length. Lets try to fix it up.
1411 while len(self.hunk[-1]) == 0: 1417 while len(self.hunk[-1]) == 0:
1412 del self.hunk[-1] 1418 del self.hunk[-1]
1413 del self.a[-1] 1419 del self.a[-1]
1418 1424
1419 def read_context_hunk(self, lr): 1425 def read_context_hunk(self, lr):
1420 self.desc = lr.readline() 1426 self.desc = lr.readline()
1421 m = contextdesc.match(self.desc) 1427 m = contextdesc.match(self.desc)
1422 if not m: 1428 if not m:
1423 raise PatchError(_(b"bad hunk #%d") % self.number) 1429 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1424 self.starta, aend = m.groups() 1430 self.starta, aend = m.groups()
1425 self.starta = int(self.starta) 1431 self.starta = int(self.starta)
1426 if aend is None: 1432 if aend is None:
1427 aend = self.starta 1433 aend = self.starta
1428 self.lena = int(aend) - self.starta 1434 self.lena = int(aend) - self.starta
1438 if l.startswith(b'- ') or l.startswith(b'! '): 1444 if l.startswith(b'- ') or l.startswith(b'! '):
1439 u = b'-' + s 1445 u = b'-' + s
1440 elif l.startswith(b' '): 1446 elif l.startswith(b' '):
1441 u = b' ' + s 1447 u = b' ' + s
1442 else: 1448 else:
1443 raise PatchError( 1449 raise PatchParseError(
1444 _(b"bad hunk #%d old text line %d") % (self.number, x) 1450 _(b"bad hunk #%d old text line %d") % (self.number, x)
1445 ) 1451 )
1446 self.a.append(u) 1452 self.a.append(u)
1447 self.hunk.append(u) 1453 self.hunk.append(u)
1448 1454
1452 self.a[-1] = s 1458 self.a[-1] = s
1453 self.hunk[-1] = s 1459 self.hunk[-1] = s
1454 l = lr.readline() 1460 l = lr.readline()
1455 m = contextdesc.match(l) 1461 m = contextdesc.match(l)
1456 if not m: 1462 if not m:
1457 raise PatchError(_(b"bad hunk #%d") % self.number) 1463 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1458 self.startb, bend = m.groups() 1464 self.startb, bend = m.groups()
1459 self.startb = int(self.startb) 1465 self.startb = int(self.startb)
1460 if bend is None: 1466 if bend is None:
1461 bend = self.startb 1467 bend = self.startb
1462 self.lenb = int(bend) - self.startb 1468 self.lenb = int(bend) - self.startb
1485 elif len(self.b) == 0: 1491 elif len(self.b) == 0:
1486 # line deletions, new block is empty 1492 # line deletions, new block is empty
1487 lr.push(l) 1493 lr.push(l)
1488 break 1494 break
1489 else: 1495 else:
1490 raise PatchError( 1496 raise PatchParseError(
1491 _(b"bad hunk #%d old text line %d") % (self.number, x) 1497 _(b"bad hunk #%d old text line %d") % (self.number, x)
1492 ) 1498 )
1493 self.b.append(s) 1499 self.b.append(s)
1494 while True: 1500 while True:
1495 if hunki >= len(self.hunk): 1501 if hunki >= len(self.hunk):
1599 return l.rstrip(b'\r\n') 1605 return l.rstrip(b'\r\n')
1600 1606
1601 while True: 1607 while True:
1602 line = getline(lr, self.hunk) 1608 line = getline(lr, self.hunk)
1603 if not line: 1609 if not line:
1604 raise PatchError( 1610 raise PatchParseError(
1605 _(b'could not extract "%s" binary data') % self._fname 1611 _(b'could not extract "%s" binary data') % self._fname
1606 ) 1612 )
1607 if line.startswith(b'literal '): 1613 if line.startswith(b'literal '):
1608 size = int(line[8:].rstrip()) 1614 size = int(line[8:].rstrip())
1609 break 1615 break
1620 else: 1626 else:
1621 l = ord(l) - ord(b'a') + 27 1627 l = ord(l) - ord(b'a') + 27
1622 try: 1628 try:
1623 dec.append(util.b85decode(line[1:])[:l]) 1629 dec.append(util.b85decode(line[1:])[:l])
1624 except ValueError as e: 1630 except ValueError as e:
1625 raise PatchError( 1631 raise PatchParseError(
1626 _(b'could not decode "%s" binary patch: %s') 1632 _(b'could not decode "%s" binary patch: %s')
1627 % (self._fname, stringutil.forcebytestr(e)) 1633 % (self._fname, stringutil.forcebytestr(e))
1628 ) 1634 )
1629 line = getline(lr, self.hunk) 1635 line = getline(lr, self.hunk)
1630 text = zlib.decompress(b''.join(dec)) 1636 text = zlib.decompress(b''.join(dec))
1631 if len(text) != size: 1637 if len(text) != size:
1632 raise PatchError( 1638 raise PatchParseError(
1633 _(b'"%s" length is %d bytes, should be %d') 1639 _(b'"%s" length is %d bytes, should be %d')
1634 % (self._fname, len(text), size) 1640 % (self._fname, len(text), size)
1635 ) 1641 )
1636 self.text = text 1642 self.text = text
1637 1643
1845 state = b'context' 1851 state = b'context'
1846 for newstate, data in scanpatch(fp): 1852 for newstate, data in scanpatch(fp):
1847 try: 1853 try:
1848 p.transitions[state][newstate](p, data) 1854 p.transitions[state][newstate](p, data)
1849 except KeyError: 1855 except KeyError:
1850 raise PatchError( 1856 raise PatchParseError(
1851 b'unhandled transition: %s -> %s' % (state, newstate) 1857 b'unhandled transition: %s -> %s' % (state, newstate)
1852 ) 1858 )
1853 state = newstate 1859 state = newstate
1854 del fp 1860 del fp
1855 return p.finished() 1861 return p.finished()
1872 ('', 'd/e/a/b/c') 1878 ('', 'd/e/a/b/c')
1873 >>> pathtransform(b' a//b/c ', 2, b'd/e/') 1879 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1874 ('a//b/', 'd/e/c') 1880 ('a//b/', 'd/e/c')
1875 >>> pathtransform(b'a/b/c', 3, b'') 1881 >>> pathtransform(b'a/b/c', 3, b'')
1876 Traceback (most recent call last): 1882 Traceback (most recent call last):
1877 PatchError: unable to strip away 1 of 3 dirs from a/b/c 1883 PatchApplicationError: unable to strip away 1 of 3 dirs from a/b/c
1878 """ 1884 """
1879 pathlen = len(path) 1885 pathlen = len(path)
1880 i = 0 1886 i = 0
1881 if strip == 0: 1887 if strip == 0:
1882 return b'', prefix + path.rstrip() 1888 return b'', prefix + path.rstrip()
1883 count = strip 1889 count = strip
1884 while count > 0: 1890 while count > 0:
1885 i = path.find(b'/', i) 1891 i = path.find(b'/', i)
1886 if i == -1: 1892 if i == -1:
1887 raise PatchError( 1893 raise PatchApplicationError(
1888 _(b"unable to strip away %d of %d dirs from %s") 1894 _(b"unable to strip away %d of %d dirs from %s")
1889 % (count, strip, path) 1895 % (count, strip, path)
1890 ) 1896 )
1891 i += 1 1897 i += 1
1892 # consume '//' in the path 1898 # consume '//' in the path
1945 else: 1951 else:
1946 fname = bfile 1952 fname = bfile
1947 elif not nulla: 1953 elif not nulla:
1948 fname = afile 1954 fname = afile
1949 else: 1955 else:
1950 raise PatchError(_(b"undefined source and destination files")) 1956 raise PatchParseError(_(b"undefined source and destination files"))
1951 1957
1952 gp = patchmeta(fname) 1958 gp = patchmeta(fname)
1953 if create: 1959 if create:
1954 gp.op = b'ADD' 1960 gp.op = b'ADD'
1955 elif remove: 1961 elif remove:
2095 b'b/' + gp.path, 2101 b'b/' + gp.path,
2096 None, 2102 None,
2097 gp.copy(), 2103 gp.copy(),
2098 ) 2104 )
2099 if not gitpatches: 2105 if not gitpatches:
2100 raise PatchError( 2106 raise PatchParseError(
2101 _(b'failed to synchronize metadata for "%s"') % afile[2:] 2107 _(b'failed to synchronize metadata for "%s"') % afile[2:]
2102 ) 2108 )
2103 newfile = True 2109 newfile = True
2104 elif x.startswith(b'---'): 2110 elif x.startswith(b'---'):
2105 # check for a unified diff 2111 # check for a unified diff
2191 elif cmd != 0: 2197 elif cmd != 0:
2192 offset_end = i + cmd 2198 offset_end = i + cmd
2193 out += binchunk[i:offset_end] 2199 out += binchunk[i:offset_end]
2194 i += cmd 2200 i += cmd
2195 else: 2201 else:
2196 raise PatchError(_(b'unexpected delta opcode 0')) 2202 raise PatchApplicationError(_(b'unexpected delta opcode 0'))
2197 return out 2203 return out
2198 2204
2199 2205
2200 def applydiff(ui, fp, backend, store, strip=1, prefix=b'', eolmode=b'strict'): 2206 def applydiff(ui, fp, backend, store, strip=1, prefix=b'', eolmode=b'strict'):
2201 """Reads a patch from fp and tries to apply it. 2207 """Reads a patch from fp and tries to apply it.
2268 data, mode = None, None 2274 data, mode = None, None
2269 if gp.op in (b'RENAME', b'COPY'): 2275 if gp.op in (b'RENAME', b'COPY'):
2270 data, mode = store.getfile(gp.oldpath)[:2] 2276 data, mode = store.getfile(gp.oldpath)[:2]
2271 if data is None: 2277 if data is None:
2272 # This means that the old path does not exist 2278 # This means that the old path does not exist
2273 raise PatchError( 2279 raise PatchApplicationError(
2274 _(b"source file '%s' does not exist") % gp.oldpath 2280 _(b"source file '%s' does not exist") % gp.oldpath
2275 ) 2281 )
2276 if gp.mode: 2282 if gp.mode:
2277 mode = gp.mode 2283 mode = gp.mode
2278 if gp.op == b'ADD': 2284 if gp.op == b'ADD':
2281 data = b'' 2287 data = b''
2282 if data or mode: 2288 if data or mode:
2283 if gp.op in (b'ADD', b'RENAME', b'COPY') and backend.exists( 2289 if gp.op in (b'ADD', b'RENAME', b'COPY') and backend.exists(
2284 gp.path 2290 gp.path
2285 ): 2291 ):
2286 raise PatchError( 2292 raise PatchApplicationError(
2287 _( 2293 _(
2288 b"cannot create %s: destination " 2294 b"cannot create %s: destination "
2289 b"already exists" 2295 b"already exists"
2290 ) 2296 )
2291 % gp.path 2297 % gp.path
2363 finally: 2369 finally:
2364 if files: 2370 if files:
2365 scmutil.marktouched(repo, files, similarity) 2371 scmutil.marktouched(repo, files, similarity)
2366 code = fp.close() 2372 code = fp.close()
2367 if code: 2373 if code:
2368 raise PatchError( 2374 raise PatchApplicationError(
2369 _(b"patch command failed: %s") % procutil.explainexit(code) 2375 _(b"patch command failed: %s") % procutil.explainexit(code)
2370 ) 2376 )
2371 return fuzz 2377 return fuzz
2372 2378
2373 2379
2395 if fp != patchobj: 2401 if fp != patchobj:
2396 fp.close() 2402 fp.close()
2397 files.update(backend.close()) 2403 files.update(backend.close())
2398 store.close() 2404 store.close()
2399 if ret < 0: 2405 if ret < 0:
2400 raise PatchError(_(b'patch failed to apply')) 2406 raise PatchApplicationError(_(b'patch failed to apply'))
2401 return ret > 0 2407 return ret > 0
2402 2408
2403 2409
2404 def internalpatch( 2410 def internalpatch(
2405 ui, 2411 ui,