# debugcommands.py - debug logic for remotefilelog # # Copyright 2013 Facebook, Inc. # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from __future__ import annotations import os import zlib from mercurial.node import ( bin, hex, sha1nodeconstants, short, ) from mercurial.i18n import _ from mercurial.pycompat import open from mercurial import ( error, filelog, lock as lockmod, pycompat, revlog, ) from mercurial.utils import hashutil from . import ( constants, datapack, fileserverclient, historypack, repack, shallowutil, ) def debugremotefilelog(ui, path, **opts) -> None: decompress = opts.get('decompress') size, firstnode, mapping = parsefileblob(path, decompress) ui.status(_(b"size: %d bytes\n") % size) ui.status(_(b"path: %s \n") % path) ui.status(_(b"key: %s \n") % (short(firstnode))) ui.status(_(b"\n")) ui.status( _(b"%12s => %12s %13s %13s %12s\n") % (b"node", b"p1", b"p2", b"linknode", b"copyfrom") ) queue = [firstnode] while queue: node = queue.pop(0) p1, p2, linknode, copyfrom = mapping[node] ui.status( _(b"%s => %s %s %s %s\n") % (short(node), short(p1), short(p2), short(linknode), copyfrom) ) if p1 != sha1nodeconstants.nullid: queue.append(p1) if p2 != sha1nodeconstants.nullid: queue.append(p2) def buildtemprevlog(repo, file) -> filelog.filelog: # get filename key filekey = hex(hashutil.sha1(file).digest()) filedir = os.path.join(repo.path, b'store/data', filekey) # sort all entries based on linkrev fctxs = [] for filenode in os.listdir(filedir): if b'_old' not in filenode: fctxs.append(repo.filectx(file, fileid=bin(filenode))) fctxs = sorted(fctxs, key=lambda x: x.linkrev()) # add to revlog temppath = repo.sjoin(b'data/temprevlog.i') if os.path.exists(temppath): os.remove(temppath) r = filelog.filelog(repo.svfs, b'temprevlog') class faket: def add(self, a, b, c): pass t = faket() for fctx in fctxs: if fctx.node() not in repo: continue p = fctx.filelog().parents(fctx.filenode()) meta = {} if fctx.renamed(): meta[b'copy'] = fctx.renamed()[0] meta[b'copyrev'] = hex(fctx.renamed()[1]) r.add(fctx.data(), meta, t, fctx.linkrev(), p[0], p[1]) return r def debugindex(orig, ui, repo, file_=None, **opts): """dump the contents of an index file""" if ( opts.get('changelog') or opts.get('manifest') or opts.get('dir') or not shallowutil.isenabled(repo) or not repo.shallowmatch(file_) ): return orig(ui, repo, file_, **opts) r = buildtemprevlog(repo, file_) # debugindex like normal format = opts.get('format', 0) if format not in (0, 1): raise error.Abort(_(b"unknown format %d") % format) generaldelta = r.get_revlog()._format_flags & revlog.FLAG_GENERALDELTA if generaldelta: basehdr = b' delta' else: basehdr = b' base' if format == 0: ui.write( ( b" rev offset length " + basehdr + b" linkrev" b" nodeid p1 p2\n" ) ) elif format == 1: ui.write( ( b" rev flag offset length" b" size " + basehdr + b" link p1 p2" b" nodeid\n" ) ) for i in r: node = r.node(i) if generaldelta: base = r.get_revlog().deltaparent(i) else: base = r.get_revlog().chainbase(i) if format == 0: try: pp = r.parents(node) except Exception: pp = [repo.nullid, repo.nullid] ui.write( b"% 6d % 9d % 7d % 6d % 7d %s %s %s\n" % ( i, r.get_revlog().start(i), r.get_revlog().length(i), base, r.linkrev(i), short(node), short(pp[0]), short(pp[1]), ) ) elif format == 1: pr = r.parentrevs(i) ui.write( b"% 6d %04x % 8d % 8d % 8d % 6d % 6d % 6d % 6d %s\n" % ( i, r.get_revlog().flags(i), r.get_revlog().start(i), r.get_revlog().length(i), r.get_revlog().rawsize(i), base, r.linkrev(i), pr[0], pr[1], short(node), ) ) def debugindexdot(orig, ui, repo, file_): """dump an index DAG as a graphviz dot file""" if not shallowutil.isenabled(repo): return orig(ui, repo, file_) r = buildtemprevlog(repo, os.path.basename(file_)[:-2]) ui.writenoi18n(b"digraph G {\n") for i in r: node = r.node(i) pp = r.parents(node) ui.write(b"\t%d -> %d\n" % (r.rev(pp[0]), i)) if pp[1] != repo.nullid: ui.write(b"\t%d -> %d\n" % (r.rev(pp[1]), i)) ui.write(b"}\n") def verifyremotefilelog(ui, path, **opts): decompress = opts.get('decompress') for root, dirs, files in os.walk(path): for file in files: if file == b"repos": continue filepath = os.path.join(root, file) size, firstnode, mapping = parsefileblob(filepath, decompress) for p1, p2, linknode, copyfrom in mapping.values(): if linknode == sha1nodeconstants.nullid: actualpath = os.path.relpath(root, path) key = fileserverclient.getcachekey( b"reponame", actualpath, file ) ui.status( b"%s %s\n" % (key, os.path.relpath(filepath, path)) ) def _decompressblob(raw): return zlib.decompress(raw) def parsefileblob(path, decompress): f = open(path, b"rb") try: raw = f.read() finally: f.close() if decompress: raw = _decompressblob(raw) offset, size, flags = shallowutil.parsesizeflags(raw) start = offset + size firstnode = None mapping = {} while start < len(raw): divider = raw.index(b'\0', start + 80) currentnode = raw[start : (start + 20)] if not firstnode: firstnode = currentnode p1 = raw[(start + 20) : (start + 40)] p2 = raw[(start + 40) : (start + 60)] linknode = raw[(start + 60) : (start + 80)] copyfrom = raw[(start + 80) : divider] mapping[currentnode] = (p1, p2, linknode, copyfrom) start = divider + 1 return size, firstnode, mapping def debugdatapack(ui, *paths, **opts): for path in paths: if b'.data' in path: path = path[: path.index(b'.data')] ui.write(b"%s:\n" % path) dpack = datapack.datapack(path) node = opts.get('node') if node: deltachain = dpack.getdeltachain(b'', bin(node)) dumpdeltachain(ui, deltachain, **opts) return if opts.get('long'): hashformatter = hex hashlen = 42 else: hashformatter = short hashlen = 14 lastfilename = None totaldeltasize = 0 totalblobsize = 0 def printtotals(): if lastfilename is not None: ui.write(b"\n") if not totaldeltasize or not totalblobsize: return difference = totalblobsize - totaldeltasize deltastr = b"%0.1f%% %s" % ( (100.0 * abs(difference) / totalblobsize), (b"smaller" if difference > 0 else b"bigger"), ) ui.writenoi18n( b"Total:%s%s %s (%s)\n" % ( b"".ljust(2 * hashlen - len(b"Total:")), (b'%d' % totaldeltasize).ljust(12), (b'%d' % totalblobsize).ljust(9), deltastr, ) ) bases = {} nodes = set() failures = 0 for filename, node, deltabase, deltalen in dpack.iterentries(): bases[node] = deltabase if node in nodes: ui.write((b"Bad entry: %s appears twice\n" % short(node))) failures += 1 nodes.add(node) if filename != lastfilename: printtotals() name = b'(empty name)' if filename == b'' else filename ui.write(b"%s:\n" % name) ui.write( b"%s%s%s%s\n" % ( b"Node".ljust(hashlen), b"Delta Base".ljust(hashlen), b"Delta Length".ljust(14), b"Blob Size".ljust(9), ) ) lastfilename = filename totalblobsize = 0 totaldeltasize = 0 # Metadata could be missing, in which case it will be an empty dict. meta = dpack.getmeta(filename, node) if constants.METAKEYSIZE in meta: blobsize = meta[constants.METAKEYSIZE] totaldeltasize += deltalen totalblobsize += blobsize else: blobsize = b"(missing)" ui.write( b"%s %s %s%s\n" % ( hashformatter(node), hashformatter(deltabase), (b'%d' % deltalen).ljust(14), pycompat.bytestr(blobsize), ) ) if filename is not None: printtotals() failures += _sanitycheck(ui, set(nodes), bases) if failures > 1: ui.warn((b"%d failures\n" % failures)) return 1 def _sanitycheck(ui, nodes, bases): """ Does some basic sanity checking on a packfiles with ``nodes`` ``bases`` (a mapping of node->base): - Each deltabase must itself be a node elsewhere in the pack - There must be no cycles """ failures = 0 for node in nodes: seen = set() current = node deltabase = bases[current] while deltabase != sha1nodeconstants.nullid: if deltabase not in nodes: ui.warn( ( b"Bad entry: %s has an unknown deltabase (%s)\n" % (short(node), short(deltabase)) ) ) failures += 1 break if deltabase in seen: ui.warn( ( b"Bad entry: %s has a cycle (at %s)\n" % (short(node), short(deltabase)) ) ) failures += 1 break current = deltabase seen.add(current) deltabase = bases[current] # Since ``node`` begins a valid chain, reset/memoize its base to nullid # so we don't traverse it again. bases[node] = sha1nodeconstants.nullid return failures def dumpdeltachain(ui, deltachain, **opts): hashformatter = hex hashlen = 40 lastfilename = None for filename, node, filename, deltabasenode, delta in deltachain: if filename != lastfilename: ui.write(b"\n%s\n" % filename) lastfilename = filename ui.write( b"%s %s %s %s\n" % ( b"Node".ljust(hashlen), b"Delta Base".ljust(hashlen), b"Delta SHA1".ljust(hashlen), b"Delta Length".ljust(6), ) ) ui.write( b"%s %s %s %d\n" % ( hashformatter(node), hashformatter(deltabasenode), hex(hashutil.sha1(delta).digest()), len(delta), ) ) def debughistorypack(ui, path): if b'.hist' in path: path = path[: path.index(b'.hist')] hpack = historypack.historypack(path) lastfilename = None for entry in hpack.iterentries(): filename, node, p1node, p2node, linknode, copyfrom = entry if filename != lastfilename: ui.write(b"\n%s\n" % filename) ui.write( b"%s%s%s%s%s\n" % ( b"Node".ljust(14), b"P1 Node".ljust(14), b"P2 Node".ljust(14), b"Link Node".ljust(14), b"Copy From", ) ) lastfilename = filename ui.write( b"%s %s %s %s %s\n" % ( short(node), short(p1node), short(p2node), short(linknode), copyfrom, ) ) def debugwaitonrepack(repo): with lockmod.lock(repack.repacklockvfs(repo), b"repacklock", timeout=-1): return def debugwaitonprefetch(repo): with repo._lock( repo.svfs, b"prefetchlock", True, None, None, _(b'prefetching in %s') % repo.origroot, ): pass