Files
littlefs/scripts/dbgrbyd.py
T
Christopher Haster 89d5a5ef80 Working implementation of B-tree name split/lookup with vestigial names
B-trees with names are now working, though this required a number of
changes to the B-tree layout:

1. B-tree no-longer require name entries (LFSR_TAG_MK) on each branch.
   This is a nice optimization to the design, since these name entries
   just waste space in purely weight-based B-trees, which are probably
   going to be most B-trees in the filesystem.

   If a name entry is missing, the struct entry, which is required,
   should have the effective weight of the entry.

   The first entry in every rbyd block is expected to be have no name
   entry, since this is the default path for B-tree lookups.

2. The first entry in every rbyd block _may_ have a name entry, which
   is ignored. I'm calling these "vestigial names" to make them sound
   cooler than they actually are.

   These vestigial names show up in a couple complicated B-tree
   operations:

   - During B-tree split, since pending attributes are calculated before
     the split, we need to play out pending attributes into the rbyd
     before deciding what name becomes the name of entry in the parent.
     This creates a vestigial name which we _could_ immediately remove,
     but the remove adds additional size to the must-fit split operation

   - During B-tree pop/merge, if we remove the leading no-name entry,
     the second, named entry becomes the leading entry. This creates a
     vestigial name that _looks_ easy enough to remove when making the
     pending attributes for pop/merge, but turns out the be surprisingly
     tricky if the parent undergoes a split/merge at the same time.

   It may be possible to remove all these vestigial names proactively,
   but this adds additional rbyd lookups to figure out the exact tag to
   remove, complicates things in a fragile way, and doesn't actually
   reduce storage costs until the rbyd is compacted.

   The main downside is that these B-trees may be a bit more confusing
   to debug.
2023-03-21 12:59:46 -05:00

847 lines
28 KiB
Python
Executable File

#!/usr/bin/env python3
import bisect
import itertools as it
import math as m
import os
import struct
COLORS = [
'34', # blue
'31', # red
'32', # green
'35', # purple
'33', # yellow
'36', # cyan
]
def blocklim(s):
if '.' in s:
s = s.strip()
b = 10
if s.startswith('0x') or s.startswith('0X'):
s = s[2:]
b = 16
elif s.startswith('0o') or s.startswith('0O'):
s = s[2:]
b = 8
elif s.startswith('0b') or s.startswith('0B'):
s = s[2:]
b = 2
s0, s1 = s.split('.', 1)
return int(s0, b), int(s1, b)
else:
return int(s, 0)
def crc32c(data, crc=0):
crc ^= 0xffffffff
for b in data:
crc ^= b
for j in range(8):
crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78)
return 0xffffffff ^ crc
def fromleb128(data):
word = 0
for i, b in enumerate(data):
word |= ((b & 0x7f) << 7*i)
word &= 0xffffffff
if not b & 0x80:
return word, i+1
return word, len(data)
def fromtag(data):
tag, delta = fromleb128(data)
id, delta_ = fromleb128(data[delta:])
size, delta__ = fromleb128(data[delta+delta_:])
return tag&1, tag&~1, id if tag&0x8 else id-1, size, delta+delta_+delta__
def popc(x):
return bin(x).count('1')
def xxd(data, width=16, crc=False):
for i in range(0, len(data), width):
yield '%-*s %-*s' % (
3*width,
' '.join('%02x' % b for b in data[i:i+width]),
width,
''.join(
b if b >= ' ' and b <= '~' else '.'
for b in map(chr, data[i:i+width])))
def tagrepr(tag, id, size, off=None):
if (tag & ~0x3f0) == 0x0400:
return '%smk%s id%d %d' % (
'rm' if tag & 0x2 else '',
'branch' if ((tag & 0x3f0) >> 4) == 0x00
else 'reg' if ((tag & 0x3f0) >> 4) == 0x01
else 'dir' if ((tag & 0x3f0) >> 4) == 0x02
else ' 0x%02x' % ((tag & 0x3f0) >> 4),
id,
size)
elif tag == 0x0800:
return 'inlined id%d %d' % (id, size)
elif tag == 0x0810:
return 'block id%d %d' % (id, size)
elif tag == 0x0820:
return 'btree id%d %d' % (id, size)
elif tag == 0x0830:
return 'branch id%d %d' % (id, size)
elif (tag & ~0xff2) == 0x2000:
return '%suattr 0x%02x%s%s' % (
'rm' if tag & 0x2 else '',
(tag & 0xff0) >> 4,
' id%d' % id if id != -1 else '',
' %d' % size if not tag & 0x2 or size else '')
elif tag == 0x0006:
return 'grow id%d w%d' % (
id,
size)
elif tag == 0x0016:
return 'shrink id%d w%d' % (
id,
size)
elif (tag & ~0x10) == 0x0004:
return 'crc%x%s %d' % (
1 if tag & 0x10 else 0,
' 0x%02x' % id if id != -1 else '',
size)
elif tag == 0x0024:
return 'fcrc%s %d' % (
' 0x%02x' % id if id != -1 else '',
size)
elif tag & 0x8:
return 'alt%s%s 0x%x w%d %s' % (
'r' if tag & 0x2 else 'b',
'gt' if tag & 0x4 else 'le',
tag & 0x3ff0,
id,
'0x%x' % (0xffffffff & (off-size))
if off is not None
else '-%d' % off)
else:
return '0x%04x id%d %d' % (tag, id, size)
def show_log(block_size, data, rev, off, *,
color=False,
**args):
crc = crc32c(data[0:4])
# preprocess jumps
if args.get('jumps'):
jumps = []
j_ = 4
while j_ < (block_size if args.get('all') else off):
j = j_
v, tag, id, size, delta = fromtag(data[j_:])
j_ += delta
if (tag & 0xe) <= 0x4:
j_ += size
if tag & 0x8:
# figure out which alt color
if tag & 0x2:
_, ntag, _, _, _ = fromtag(data[j_:])
if ntag & 0x2:
jumps.append((j, j-size, 0, 'y'))
else:
jumps.append((j, j-size, 0, 'r'))
else:
jumps.append((j, j-size, 0, 'b'))
# figure out x-offsets to avoid collisions between jumps
for j in range(len(jumps)):
a, b, _, c = jumps[j]
x = 0
while any(
max(a, b) >= min(a_, b_)
and max(a_, b_) >= min(a, b)
and x == x_
for a_, b_, x_, _ in jumps[:j]):
x += 1
jumps[j] = a, b, x, c
def jumprepr(j):
# render jumps
chars = {}
for a, b, x, c in jumps:
c_start = (
'\x1b[33m' if color and c == 'y'
else '\x1b[31m' if color and c == 'r'
else '\x1b[90m' if color
else '')
c_stop = '\x1b[m' if color else ''
if j == a:
for x_ in range(2*x+1):
chars[x_] = '%s-%s' % (c_start, c_stop)
chars[2*x+1] = '%s\'%s' % (c_start, c_stop)
elif j == b:
for x_ in range(2*x+1):
chars[x_] = '%s-%s' % (c_start, c_stop)
chars[2*x+1] = '%s.%s' % (c_start, c_stop)
chars[0] = '%s<%s' % (c_start, c_stop)
elif j >= min(a, b) and j <= max(a, b):
chars[2*x+1] = '%s|%s' % (c_start, c_stop)
return ''.join(chars.get(x, ' ')
for x in range(max(chars.keys(), default=0)+1))
# preprocess lifetimes
lifetime_width = 0
if args.get('lifetimes'):
class Lifetime:
color_i = 0
def __init__(self, j):
self.origin = j
self.tags = set()
self.color = COLORS[self.__class__.color_i]
self.__class__.color_i = (
self.__class__.color_i + 1) % len(COLORS)
def add(self, j):
self.tags.add(j)
def __bool__(self):
return bool(self.tags)
# first figure out where each id comes from
weights = []
lifetimes = []
def index(weights, id):
for i, w in enumerate(weights):
if id < w:
return i, id
id -= w
return len(weights), 0
checkpoint_js = []
checkpoints = []
def checkpoint(j, weights, lifetimes, grows, shrinks, tags):
checkpoint_js.append(j)
checkpoints.append((
weights.copy(), lifetimes.copy(),
grows, shrinks, tags))
j_ = 4
while j_ < (block_size if args.get('all') else off):
j = j_
v, tag, id, size, delta = fromtag(data[j_:])
j_ += delta
if (tag & 0xe) <= 0x4:
j_ += size
# note we ignore out-of-bounds here for debugging
if tag == 0x0006:
# grow lifetimes
i, id_ = index(weights, id)
if id_ > 0:
weights[i:i+1] = [id_, size, weights[i]-id_]
lifetimes[i:i+1] = [lifetimes[i], Lifetime(j), lifetimes[i]]
else:
weights[i:i] = [size]
lifetimes[i:i] = [Lifetime(j)]
checkpoint(j, weights, lifetimes, {i}, set(), {i})
elif tag == 0x0016:
# shrink lifetimes
i, id_ = index(weights, id)
size_ = size
weights_ = weights.copy()
lifetimes_ = lifetimes.copy()
shrinks = set()
while size_ > 0 and i < len(weights_):
if id_ > 0:
diff = min(size_, weights_[i]-id_)
size_ -= diff
weights_[i] -= diff
i += 1
id_ = 0
elif weights_[i] > size_:
weights_[i] -= size_
size_ = 0
else:
size_ -= weights_[i]
weights_[i:i+1] = []
lifetimes_[i:i+1] = []
shrinks.add(i + len(shrinks))
checkpoint(j, weights, lifetimes, set(), shrinks, {i})
weights = weights_
lifetimes = lifetimes_
elif (tag & 0xc) == 0x0:
# attach tag to lifetime
i, id_ = index(weights, id)
if i < len(weights):
lifetimes[i].add(j)
checkpoint(j, weights, lifetimes, set(), set(), {i})
lifetime_width = 2*max((
sum(1 for lifetime in lifetimes if lifetime)
for _, lifetimes, _, _, _ in checkpoints),
default=0)
def lifetimerepr(j):
x = bisect.bisect(checkpoint_js, j)-1
j_ = checkpoint_js[x]
weights, lifetimes, grows, shrinks, tags = checkpoints[x]
reprs = []
colors = []
was = None
for i, (w, lifetime) in enumerate(zip(weights, lifetimes)):
# skip lifetimes with no tags and shrinks
if not lifetime or (j != j_ and i in shrinks):
if i in grows or i in shrinks or i in tags:
tags = tags.copy()
tags.add(i+1)
continue
if j == j_ and i in grows:
reprs.append('.')
was = 'grow'
elif j == j_ and i in shrinks:
reprs.append('\'')
was = 'shrink'
elif j == j_ and i in tags:
reprs.append('* ')
elif was == 'grow':
reprs.append('\\ ')
elif was == 'shrink':
reprs.append('/ ')
else:
reprs.append('| ')
colors.append(lifetime.color)
return '%s%*s' % (
''.join('%s%s%s' % (
'\x1b[%sm' % c if color else '',
r,
'\x1b[m' if color else '')
for r, c in zip(reprs, colors)),
lifetime_width - sum(len(r) for r in reprs), '')
# print header
print('%-8s %*s%-22s %s' % (
'off',
lifetime_width, '',
'tag',
'data (truncated)'
if not args.get('no_truncate') else ''))
# print revision count
if args.get('raw'):
print('%8s: %s' % ('%04x' % 0, next(xxd(data[0:4]))))
# print tags
j_ = 4
while j_ < (block_size if args.get('all') else off):
notes = []
j = j_
v, tag, id, size, delta = fromtag(data[j_:])
if v != (popc(crc) & 1):
notes.append('v!=%x' % (popc(crc) & 1))
tag &= ~1
crc = crc32c(data[j_:j_+delta], crc)
j_ += delta
if (tag & 0xe) <= 0x4:
if (tag & ~0x10) != 0x04:
crc = crc32c(data[j_:j_+size], crc)
# found a crc?
else:
crc_, = struct.unpack('<I', data[j_:j_+4].ljust(4, b'\0'))
if crc != crc_:
notes.append('crc!=%08x' % crc)
j_ += size
# show human-readable tag representation
print('%s%08x:%s %s%s%-57s%s%s' % (
'\x1b[90m' if color and j >= off else '',
j,
'\x1b[m' if color and j >= off else '',
lifetimerepr(j) if args.get('lifetimes') else '',
'\x1b[90m' if color and j >= off else '',
'%-22s%s' % (
tagrepr(tag, id, size, j),
' %s' % next(xxd(
data[j+delta:j+delta+min(size, 8)], 8), '')
if not args.get('no_truncate')
and (tag & 0xe) <= 0x4 else ''),
'\x1b[m' if color and j >= off else '',
' (%s)' % ', '.join(notes) if notes
else ' %s' % jumprepr(j)
if args.get('jumps')
else ''))
if args.get('raw'):
# show on-disk encoding of tags
for o, line in enumerate(xxd(data[j:j+delta])):
print('%s%8s: %s%s' % (
'\x1b[90m' if color and j >= off else '',
'%04x' % (j + o*16),
line,
'\x1b[m' if color and j >= off else ''))
# show in-device representation, including some extra
# crc/parity info
if args.get('device'):
print('%s%8s %*s%-47s %08x %x%s' % (
'\x1b[90m' if color and j >= off else '',
'',
lifetime_width, '',
'%-22s%s' % (
'%04x %08x %07x' % (tag, 0xffffffff & id, size),
' %s' % ' '.join(
'%08x' % struct.unpack('<I',
data[j+delta+i*4:j+delta+min(i*4+4,size)]
.ljust(4, b'\0'))
for i in range(min(m.ceil(size/4), 3)))[:23]
if not args.get('no_truncate')
and (tag & 0xe) <= 0x4 else ''),
crc,
popc(crc) & 1,
'\x1b[m' if color and j >= off else ''))
if (tag & 0xe) <= 0x4:
# show on-disk encoding of data
if args.get('raw') or args.get('no_truncate'):
for o, line in enumerate(xxd(data[j+delta:j+delta+size])):
print('%s%8s: %s%s' % (
'\x1b[90m' if color and j >= off else '',
'%04x' % (j+delta + o*16),
line,
'\x1b[m' if color and j >= off else ''))
def show_tree(block_size, data, rev, trunk, weight, *,
color=False,
**args):
if trunk is None:
return
# lookup a tag, returning also the search path for decoration
# purposes
def lookup(tag, id):
lower = -1
upper = weight
path = []
# descend down tree
j = trunk
while True:
_, alt, weight_, jump, delta = fromtag(data[j:])
# found an alt?
if alt & 0x8:
# follow?
if ((id, tag & ~0xf) > (upper-weight_-1, alt & ~0xf)
if alt & 0x4
else ((id, tag & ~0xf) <= (lower+weight_, alt & ~0xf))):
lower += upper-lower-1-weight_ if alt & 0x4 else 0
upper -= upper-lower-1-weight_ if not alt & 0x4 else 0
j = j - jump
if args.get('tree'):
# figure out which color
if alt & 0x2:
_, nalt, _, _, _ = fromtag(data[j+jump+delta:])
if nalt & 0x2:
path.append((j+jump, j, 'y'))
else:
path.append((j+jump, j, 'r'))
else:
path.append((j+jump, j, 'b'))
# stay on path
else:
lower += weight_ if not alt & 0x4 else 0
upper -= weight_ if alt & 0x4 else 0
j = j + delta
if args.get('tree'):
# figure out which color
if alt & 0x2:
_, nalt, _, _, _ = fromtag(data[j:])
if nalt & 0x2:
path.append((j-delta, j, 'y'))
else:
path.append((j-delta, j, 'r'))
else:
path.append((j-delta, j, 'b'))
# found tag
else:
tag_ = alt
id_ = upper-1
w_ = id_-lower
done = (id_, tag_) < (id, tag) or tag_ & 2
return done, tag_, id_, w_, j, delta, jump, path
# precompute tree
tree_width = 0
if args.get('tree'):
tags = []
paths = {}
tag, id = 0, -1
while True:
done, tag, id, w, j, delta, size, path = lookup(tag+0x10, id)
# found end of tree?
if done:
break
tags.append((j, tag, id))
for x, (a, b, c) in enumerate(path):
paths[a, b, x] = c
# align paths to nearest tag
tags.sort()
paths = {(
tags[bisect.bisect_left(tags, (a, 0, -1), hi=len(tags)-1)],
tags[bisect.bisect_left(tags, (b, 0, -1), hi=len(tags)-1)],
x): c for (a, b, x), c in paths.items()}
# also find the maximum depth
depth = max((x+1 for _, _, x in paths.keys()), default=0)
if depth > 0:
tree_width = 2*depth + 2
def treerepr(j):
if depth == 0:
return ''
_, tag, id = tags[bisect.bisect_left(
tags, (j, 0, -1), hi=len(tags)-1)]
def c_start(c):
return ('\x1b[33m' if color and c == 'y'
else '\x1b[31m' if color and c == 'r'
else '\x1b[90m' if color
else '')
def c_stop(c):
return '\x1b[m' if color else ''
path = []
seen = None
for x in range(depth):
if any(x == x_ and tag == a_tag and id == a_id
for (_, a_tag, a_id), _, x_ in paths.keys()):
c = next(c
for ((_, a_tag, a_id), _, x_), c in paths.items()
if x == x_ and tag == a_tag and id == a_id)
path.append('%s+%s' % (c_start(c), c_stop(c)))
elif any(x == x_ and tag == b_tag and id == b_id
for _, (_, b_tag, b_id), x_ in paths.keys()):
a_tag, a_id, c = next((a_tag, a_id, c)
for ((_, a_tag, a_id), (_, b_tag, b_id), x_), c
in paths.items()
if x == x_ and tag == b_tag and id == b_id)
if (a_id, a_tag) < (id, tag):
path.append('%s\'%s' % (c_start(c), c_stop(c)))
else:
path.append('%s.%s' % (c_start(c), c_stop(c)))
elif any(x == x_
and (id, tag) >= min((a_id, a_tag), (b_id, b_tag))
and (id, tag) <= max((a_id, a_tag), (b_id, b_tag))
for (_, a_tag, a_id), (_, b_tag, b_id), x_
in paths.keys()):
c = next(c
for ((_, a_tag, a_id), (_, b_tag, b_id), x_), c
in paths.items()
if x == x_
and (id, tag) >= min((a_id, a_tag), (b_id, b_tag))
and (id, tag) <= max((a_id, a_tag), (b_id, b_tag)))
path.append('%s|%s' % (c_start(c), c_stop(c)))
elif seen:
path.append('%s-%s' % (c_start(seen), c_stop(seen)))
else:
path.append(' ')
if any(x == x_ and tag == b_tag and id == b_id
for _, (_, b_tag, b_id), x_ in paths.keys()):
c = next(c
for (_, (_, b_tag, b_id), x_), c in paths.items()
if x == x_ and tag == b_tag and id == b_id)
seen = c
if seen and x == depth-1:
path.append('%s->%s' % (c_start(seen), c_stop(seen)))
elif seen:
path.append('%s-%s' % (c_start(seen), c_stop(seen)))
else:
path.append(' ')
return ' %s' % ''.join(path)
# print header
w_width = 2*m.ceil(m.log10(max(1, weight)+1))+1
print('%-8s %*s%-*s %-22s %s' % (
'off',
tree_width, '',
w_width, 'ids',
'tag',
'data (truncated)'
if not args.get('no_truncate') else ''))
tag, id = 0, -1
while True:
done, tag, id, w, j, delta, size, path = lookup(tag+0x10, id)
# found end of tree?
if done:
break
# show human-readable tag representation
print('%08x:%s %*s %-57s' % (
j,
treerepr(j) if args.get('tree') else '',
w_width, '%d-%d' % (id-(w-1), id)
if w > 1 else id
if w > 0 else '',
'%-22s%s' % (
tagrepr(tag, id, size, j),
' %s' % next(xxd(
data[j+delta:j+delta+min(size, 8)], 8), '')
if not args.get('no_truncate')
and (tag & 0xe) <= 0x4 else '')))
# show in-device representation
if args.get('device'):
print('%8s %*s%*s %s' % (
'',
tree_width, '',
w_width, '',
'%-22s%s' % (
'%04x %08x %07x' % (tag, 0xffffffff & id, size),
' %s' % ' '.join(
'%08x' % struct.unpack('<I',
data[j+delta+i*4:j+delta+min(i*4+4,size)]
.ljust(4, b'\0'))
for i in range(min(m.ceil(size/4), 3)))[:23]
if not args.get('no_truncate')
and (tag & 0xe) <= 0x4 else '')))
if args.get('raw'):
# show on-disk encoding of tags
for o, line in enumerate(xxd(data[j:j+delta])):
print('%8s: %s' % (
'%04x' % (j + o*16),
line))
if (tag & 0xe) <= 0x4:
# show on-disk encoding of data
if args.get('raw') or args.get('no_truncate'):
for o, line in enumerate(xxd(data[j+delta:j+delta+size])):
print('%8s: %s' % (
'%04x' % (j+delta + o*16),
line))
def main(disk, block_size=None, block1=0, block2=None, *,
limit=None,
trunk=None,
color='auto',
**args):
# figure out what color should be
if color == 'auto':
color = sys.stdout.isatty()
elif color == 'always':
color = True
else:
color = False
with open(disk, 'rb') as f:
# if block_size is omitted, assume the block device is one big block
if block_size is None:
f.seek(0, os.SEEK_END)
block_size = f.tell()
# blocks may also encode limits
blocks = [
block[0] if isinstance(block, tuple) else block
for block in [block1, block2]
if block is not None]
limits = [
limit if limit is not None
else block[1] if isinstance(block, tuple)
else None
for block in [block1, block2]
if block is not None]
# read each block
datas = []
for block, limit in zip(blocks, limits):
f.seek(block * block_size)
datas.append(f.read(limit if limit is not None else block_size))
# first figure out which block as the most recent revision
def fetch(data):
rev, = struct.unpack('<I', data[0:4].ljust(4, b'\0'))
crc = crc32c(data[0:4])
off = 0
j_ = 4
trunk = None
trunk_ = None
weight = 0
weight_ = 0
wastrunk = False
while j_ < len(data):
v, tag, id, size, delta = fromtag(data[j_:])
if v != (popc(crc) & 1):
break
crc = crc32c(data[j_:j_+delta], crc)
j_ += delta
# find trunk
if not wastrunk and (tag & 0xe) != 0x4:
trunk_ = j_ - delta
wastrunk = not not tag & 0x8
# keep track of weight
if tag == 0x0006:
weight_ += size
elif tag == 0x0016:
weight_ = max(weight_ - size, 0)
# take care of crcs
if (tag & 0xe) <= 0x4:
if (tag & ~0x10) != 0x04:
crc = crc32c(data[j_:j_+size], crc)
# found a crc?
else:
crc_, = struct.unpack('<I', data[j_:j_+4].ljust(4, b'\0'))
if crc != crc_:
break
# commit what we have
off = j_ + size
trunk = trunk_
weight = weight_
j_ += size
return rev, off, trunk, weight
revs, offs, trunks, weights = [], [], [], []
i = 0
for data in datas:
rev, off, trunk_, weight = fetch(data)
revs.append(rev)
offs.append(off)
trunks.append(trunk_)
weights.append(weight)
# compare with sequence arithmetic
if off and ((rev - revs[i]) & 0x80000000):
i = len(revs)-1
# print contents of the winning metadata block
block, limit, data, rev, off, trunk, weight = (
blocks[i], limits[i], datas[i], revs[i], offs[i],
trunk if trunk is not None else trunks[i],
weights[i])
print('rbyd 0x%x%s, rev %d, size %d, weight %d%s' % (
block, '.%x' % limit if limit is not None else '',
rev, off, weight,
' (was 0x%x%s, %d, %d, %d)' % (
blocks[~i], '.%x' % limits[~i] if limits[~i] is not None else '',
revs[~i], offs[~i], weights[~i])
if len(blocks) > 1 else ''))
if args.get('log'):
show_log(block_size, data, rev, off,
color=color,
**args)
else:
show_tree(block_size, data, rev, trunk, weight,
color=color,
**args)
if args.get('error_on_corrupt') and off == 0:
sys.exit(2)
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="Debug rbyd metadata.",
allow_abbrev=False)
parser.add_argument(
'disk',
help="File containing the block device.")
parser.add_argument(
'block1',
nargs='?',
type=blocklim,
help="Block address of the first metadata block.")
parser.add_argument(
'block2',
nargs='?',
type=blocklim,
help="Block address of the second metadata block.")
parser.add_argument(
'-B', '--block-size',
type=lambda x: int(x, 0),
help="Block size in bytes.")
parser.add_argument(
'-L', '--limit',
type=lambda x: int(x, 0),
help="Use this offset as the rbyd limit.")
parser.add_argument(
'--trunk',
type=lambda x: int(x, 0),
help="Use this offset as the trunk of the tree.")
parser.add_argument(
'--color',
choices=['never', 'always', 'auto'],
default='auto',
help="When to use terminal colors. Defaults to 'auto'.")
parser.add_argument(
'-a', '--all',
action='store_true',
help="Don't stop parsing on bad commits.")
parser.add_argument(
'-l', '--log',
action='store_true',
help="Show the raw tags as they appear in the log.")
parser.add_argument(
'-r', '--raw',
action='store_true',
help="Show the raw data including tag encodings.")
parser.add_argument(
'-x', '--device',
action='store_true',
help="Show the device-side representation of tags.")
parser.add_argument(
'-T', '--no-truncate',
action='store_true',
help="Don't truncate, show the full contents.")
parser.add_argument(
'-t', '--tree',
action='store_true',
help="Show the rbyd tree.")
parser.add_argument(
'-j', '--jumps',
action='store_true',
help="Show alt pointer jumps in the margin.")
parser.add_argument(
'-g', '--lifetimes',
action='store_true',
help="Show inserts/deletes of ids in the margin.")
parser.add_argument(
'-e', '--error-on-corrupt',
action='store_true',
help="Error if no valid commit is found.")
sys.exit(main(**{k: v
for k, v in vars(parser.parse_intermixed_args()).items()
if v is not None}))