Mercurial > public > mercurial-scm > hg-stable
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, |