mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-06 09:20:57 +01:00
314 lines
11 KiB
Python
314 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tools for diff'ing two xonsh history files in a meaningful fashion."""
|
|
import difflib
|
|
import datetime
|
|
import itertools
|
|
|
|
from xonsh.lazyjson import LazyJSON
|
|
from xonsh.tools import print_color
|
|
|
|
NO_COLOR = '{NO_COLOR}'
|
|
RED = '{RED}'
|
|
GREEN = '{GREEN}'
|
|
BOLD_RED = '{BOLD_RED}'
|
|
BOLD_GREEN = '{BOLD_GREEN}'
|
|
|
|
# intern some strings
|
|
REPLACE = 'replace'
|
|
DELETE = 'delete'
|
|
INSERT = 'insert'
|
|
EQUAL = 'equal'
|
|
|
|
|
|
def bold_str_diff(a, b, sm=None):
|
|
if sm is None:
|
|
sm = difflib.SequenceMatcher()
|
|
aline = RED + '- '
|
|
bline = GREEN + '+ '
|
|
sm.set_seqs(a, b)
|
|
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
|
if tag == REPLACE:
|
|
aline += BOLD_RED + a[i1:i2] + RED
|
|
bline += BOLD_GREEN + b[j1:j2] + GREEN
|
|
elif tag == DELETE:
|
|
aline += BOLD_RED + a[i1:i2] + RED
|
|
elif tag == INSERT:
|
|
bline += BOLD_GREEN + b[j1:j2] + GREEN
|
|
elif tag == EQUAL:
|
|
aline += a[i1:i2]
|
|
bline += b[j1:j2]
|
|
else:
|
|
raise RuntimeError('tag not understood')
|
|
return aline + NO_COLOR + '\n' + bline + NO_COLOR +'\n'
|
|
|
|
|
|
def redline(line):
|
|
return '{red}- {line}{no_color}\n'.format(red=RED, line=line, no_color=NO_COLOR)
|
|
|
|
|
|
def greenline(line):
|
|
return '{green}+ {line}{no_color}\n'.format(green=GREEN, line=line, no_color=NO_COLOR)
|
|
|
|
|
|
def highlighted_ndiff(a, b):
|
|
"""Returns a highlited string, with bold charaters where different."""
|
|
s = ''
|
|
sm = difflib.SequenceMatcher()
|
|
sm.set_seqs(a, b)
|
|
linesm = difflib.SequenceMatcher()
|
|
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
|
if tag == REPLACE:
|
|
for aline, bline in itertools.zip_longest(a[i1:i2], b[j1:j2]):
|
|
if bline is None:
|
|
s += redline(aline)
|
|
elif aline is None:
|
|
s += greenline(bline)
|
|
else:
|
|
s += bold_str_diff(aline, bline, sm=linesm)
|
|
elif tag == DELETE:
|
|
for aline in a[i1:i2]:
|
|
s += redline(aline)
|
|
elif tag == INSERT:
|
|
for bline in b[j1:j2]:
|
|
s += greenline(bline)
|
|
elif tag == EQUAL:
|
|
for aline in a[i1:i2]:
|
|
s += ' ' + aline + '\n'
|
|
else:
|
|
raise RuntimeError('tag not understood')
|
|
return s
|
|
|
|
|
|
class HistoryDiffer(object):
|
|
"""This class helps diff two xonsh history files."""
|
|
|
|
def __init__(self, afile, bfile, reopen=False, verbose=False):
|
|
"""
|
|
Parameters
|
|
----------
|
|
afile : file handle or str
|
|
The first file to diff
|
|
bfile : file handle or str
|
|
The second file to diff
|
|
reopen : bool, optional
|
|
Whether or not to reopen the file handles each time. The default here is
|
|
opposite from the LazyJSON default because we know that we will be doing
|
|
a lot of reading so it is best to keep the handles open.
|
|
verbose : bool, optional
|
|
Whether to print a verbose amount of information.
|
|
"""
|
|
self.a = LazyJSON(afile, reopen=reopen)
|
|
self.b = LazyJSON(bfile, reopen=reopen)
|
|
self.verbose = verbose
|
|
self.sm = difflib.SequenceMatcher(autojunk=False)
|
|
|
|
def __del__(self):
|
|
self.a.close()
|
|
self.b.close()
|
|
|
|
def __str__(self):
|
|
return self.format()
|
|
|
|
def _header_line(self, lj):
|
|
s = lj._f.name if hasattr(lj._f, 'name') else ''
|
|
s += ' (' + lj['sessionid'] + ')'
|
|
s += ' [locked]' if lj['locked'] else ' [unlocked]'
|
|
ts = lj['ts'].load()
|
|
ts0 = datetime.datetime.fromtimestamp(ts[0])
|
|
s += ' started: ' + ts0.isoformat(' ')
|
|
if ts[1] is not None:
|
|
ts1 = datetime.datetime.fromtimestamp(ts[1])
|
|
s += ' stopped: ' + ts1.isoformat(' ') + ' runtime: ' + str(ts1 - ts0)
|
|
return s
|
|
|
|
def header(self):
|
|
"""Computes a header string difference."""
|
|
s = ('{red}--- {aline}{no_color}\n'
|
|
'{green}+++ {bline}{no_color}')
|
|
s = s.format(aline=self._header_line(self.a), bline=self._header_line(self.b),
|
|
red=RED, green=GREEN, no_color=NO_COLOR)
|
|
return s
|
|
|
|
def _env_both_diff(self, in_both, aenv, benv):
|
|
sm = self.sm
|
|
s = ''
|
|
for key in sorted(in_both):
|
|
aval = aenv[key]
|
|
bval = benv[key]
|
|
if aval == bval:
|
|
continue
|
|
s += '{0!r} is in both, but differs\n'.format(key)
|
|
s += bold_str_diff(aval, bval, sm=sm) + '\n'
|
|
return s
|
|
|
|
def _env_in_one_diff(self, x, y, color, xid, xenv):
|
|
only_x = sorted(x - y)
|
|
if len(only_x) == 0:
|
|
return ''
|
|
if self.verbose:
|
|
xstr = ',\n'.join([' {0!r}: {1!r}'.format(key, xenv[key]) \
|
|
for key in only_x])
|
|
xstr = '\n' + xstr
|
|
else:
|
|
xstr = ', '.join(['{0!r}'.format(key) for key in only_x])
|
|
in_x = 'These vars are only in {color}{xid}{no_color}: {{{xstr}}}\n\n'
|
|
return in_x.format(xid=xid, color=color, no_color=NO_COLOR, xstr=xstr)
|
|
|
|
def envdiff(self):
|
|
"""Computes the difference between the environments."""
|
|
aenv = self.a['env'].load()
|
|
benv = self.b['env'].load()
|
|
akeys = frozenset(aenv)
|
|
bkeys = frozenset(benv)
|
|
in_both = akeys & bkeys
|
|
if len(in_both) == len(akeys) == len(bkeys):
|
|
keydiff = self._env_both_diff(in_both, aenv, benv)
|
|
if len(keydiff) == 0:
|
|
return ''
|
|
in_a = in_b = ''
|
|
else:
|
|
keydiff = self._env_both_diff(in_both, aenv, benv)
|
|
in_a = self._env_in_one_diff(akeys, bkeys, RED, self.a['sessionid'], aenv)
|
|
in_b = self._env_in_one_diff(bkeys, akeys, GREEN, self.b['sessionid'], benv)
|
|
s = 'Environment\n-----------\n' + in_a + keydiff + in_b
|
|
return s
|
|
|
|
def _cmd_in_one_diff(self, inp, i, xlj, xid, color):
|
|
s = 'cmd #{i} only in {color}{xid}{no_color}:\n'
|
|
s = s.format(i=i, color=color, xid=xid, no_color=NO_COLOR)
|
|
lines = inp.splitlines()
|
|
lt = '{color}{pre}{no_color} {line}\n'
|
|
s += lt.format(color=color, no_color=NO_COLOR, line=lines[0], pre='>>>')
|
|
for line in lines[1:]:
|
|
s += lt.format(color=color, no_color=NO_COLOR, line=line, pre='...')
|
|
if not self.verbose:
|
|
return s + '\n'
|
|
out = xlj['cmds'][0].get('out', 'Note: no output stored')
|
|
s += out.rstrip() + '\n\n'
|
|
return s
|
|
|
|
def _cmd_out_and_rtn_diff(self, i, j):
|
|
s = ''
|
|
aout = self.a['cmds'][i].get('out', None)
|
|
bout = self.b['cmds'][j].get('out', None)
|
|
if aout is None and bout is None:
|
|
#s += 'Note: neither output stored\n'
|
|
pass
|
|
elif bout is None:
|
|
aid = self.a['sessionid']
|
|
s += 'Note: only {red}{aid}{no_color} output stored\n'.format(red=RED,
|
|
aid=aid, no_color=NO_COLOR)
|
|
elif aout is None:
|
|
bid = self.b['sessionid']
|
|
s += 'Note: only {green}{bid}{no_color} output stored\n'.format(green=GREEN,
|
|
bid=bid, no_color=NO_COLOR)
|
|
elif aout != bout:
|
|
s += 'Outputs differ\n'
|
|
s += highlighted_ndiff(aout.splitlines(), bout.splitlines())
|
|
else:
|
|
pass
|
|
artn = self.a['cmds'][i]['rtn']
|
|
brtn = self.b['cmds'][j]['rtn']
|
|
if artn != brtn:
|
|
s += ('Return vals {red}{artn}{no_color} & {green}{brtn}{no_color} differ\n'
|
|
).format(red=RED, green=GREEN, no_color=NO_COLOR, artn=artn, brtn=brtn)
|
|
return s
|
|
|
|
def _cmd_replace_diff(self, i, ainp, aid, j, binp, bid):
|
|
s = ('cmd #{i} in {red}{aid}{no_color} is replaced by \n'
|
|
'cmd #{j} in {green}{bid}{no_color}:\n')
|
|
s = s.format(i=i, aid=aid, j=j, bid=bid, red=RED, green=GREEN, no_color=NO_COLOR)
|
|
s += highlighted_ndiff(ainp.splitlines(), binp.splitlines())
|
|
if not self.verbose:
|
|
return s + '\n'
|
|
s += self._cmd_out_and_rtn_diff(i, j)
|
|
return s + '\n'
|
|
|
|
def cmdsdiff(self):
|
|
"""Computes the difference of the commands themselves."""
|
|
aid = self.a['sessionid']
|
|
bid = self.b['sessionid']
|
|
ainps = [c['inp'] for c in self.a['cmds']]
|
|
binps = [c['inp'] for c in self.b['cmds']]
|
|
sm = self.sm
|
|
sm.set_seqs(ainps, binps)
|
|
s = ''
|
|
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
|
if tag == REPLACE:
|
|
zipper = itertools.zip_longest
|
|
for i, ainp, j, binp in zipper(range(i1, i2), ainps[i1:i2],
|
|
range(j1, j2), binps[j1:j2]):
|
|
if j is None:
|
|
s += self._cmd_in_one_diff(ainp, i, self.a, aid, RED)
|
|
elif i is None:
|
|
s += self._cmd_in_one_diff(binp, j, self.b, bid, GREEN)
|
|
else:
|
|
self._cmd_replace_diff(i, ainp, aid, j, binp, bid)
|
|
elif tag == DELETE:
|
|
for i, inp in enumerate(ainps[i1:i2], i1):
|
|
s += self._cmd_in_one_diff(inp, i, self.a, aid, RED)
|
|
elif tag == INSERT:
|
|
for j, inp in enumerate(binps[j1:j2], j1):
|
|
s += self._cmd_in_one_diff(inp, j, self.b, bid, GREEN)
|
|
elif tag == EQUAL:
|
|
for i, j, in zip(range(i1, i2), range(j1, j2)):
|
|
odiff = self._cmd_out_and_rtn_diff(i, j)
|
|
if len(odiff) > 0:
|
|
h = ('cmd #{i} in {red}{aid}{no_color} input is the same as \n'
|
|
'cmd #{j} in {green}{bid}{no_color}, but output differs:\n')
|
|
s += h.format(i=i, aid=aid, j=j, bid=bid, red=RED, green=GREEN,
|
|
no_color=NO_COLOR)
|
|
s += odiff + '\n'
|
|
else:
|
|
raise RuntimeError('tag not understood')
|
|
if len(s) == 0:
|
|
return s
|
|
return 'Commands\n--------\n' + s
|
|
|
|
def format(self):
|
|
"""Formats the difference between the two history files."""
|
|
s = self.header()
|
|
ed = self.envdiff()
|
|
if len(ed) > 0:
|
|
s += '\n\n' + ed
|
|
cd = self.cmdsdiff()
|
|
if len(cd) > 0:
|
|
s += '\n\n' + cd
|
|
return s.rstrip()
|
|
|
|
|
|
_HD_PARSER = None
|
|
|
|
def _dh_create_parser(p=None):
|
|
global _HD_PARSER
|
|
p_was_none = (p is None)
|
|
if _HD_PARSER is not None and p_was_none:
|
|
return _HD_PARSER
|
|
if p_was_none:
|
|
from argparse import ArgumentParser
|
|
p = ArgumentParser('diff-history', description='diffs two xonsh history files')
|
|
p.add_argument('--reopen', dest='reopen', default=False, action='store_true',
|
|
help='make lazy file loading reopen files each time')
|
|
p.add_argument('-v', '--verbose', dest='verbose', default=False, action='store_true',
|
|
help='whether to print even more information')
|
|
p.add_argument('a', help='first file in diff')
|
|
p.add_argument('b', help='second file in diff')
|
|
if p_was_none:
|
|
_HD_PARSER = p
|
|
return p
|
|
|
|
|
|
def _dh_main_action(ns, hist=None):
|
|
hd = HistoryDiffer(ns.a, ns.b, reopen=ns.reopen, verbose=ns.verbose)
|
|
print_color(hd.format())
|
|
|
|
|
|
def diff_history_main(args=None, stdin=None):
|
|
"""Main entry point for history diff'ing"""
|
|
parser = _create_parser()
|
|
ns = parser.parse_args(args)
|
|
_dh_main_action(ns)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
diff_history_main()
|