|
|
# 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
|
|
|
|