parent
36ca3838a6
commit
df89266c03
@ -11,6 +11,8 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
|||||||
used by full screen terminal programs and even games, see
|
used by full screen terminal programs and even games, see
|
||||||
:doc:`keyboard-protocol` (:iss:`3248`)
|
:doc:`keyboard-protocol` (:iss:`3248`)
|
||||||
|
|
||||||
|
- diff kitten: Implement recursive diff over SSH (:iss:`3268`)
|
||||||
|
|
||||||
- Add support for the color settings stack that XTerm copied from us without
|
- Add support for the color settings stack that XTerm copied from us without
|
||||||
acknowledgement and decided to use incompatible escape codes for.
|
acknowledgement and decided to use incompatible escape codes for.
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,11 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
path_name_map: Dict[str, str] = {}
|
path_name_map: Dict[str, str] = {}
|
||||||
|
remote_dirs: Dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def add_remote_dir(val: str) -> None:
|
||||||
|
remote_dirs[val] = os.path.basename(val).rpartition('-')[-1]
|
||||||
|
|
||||||
|
|
||||||
class Segment:
|
class Segment:
|
||||||
@ -88,6 +93,20 @@ class Collection:
|
|||||||
return len(self.all_paths)
|
return len(self.all_paths)
|
||||||
|
|
||||||
|
|
||||||
|
def remote_hostname(path: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
for q in remote_dirs:
|
||||||
|
if path.startswith(q):
|
||||||
|
return q, remote_dirs[q]
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_remote_name(path: str, default: str) -> str:
|
||||||
|
remote_dir, rh = remote_hostname(path)
|
||||||
|
if remote_dir and rh:
|
||||||
|
return rh + ':' + os.path.relpath(path, remote_dir)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def collect_files(collection: Collection, left: str, right: str) -> None:
|
def collect_files(collection: Collection, left: str, right: str) -> None:
|
||||||
left_names: Set[str] = set()
|
left_names: Set[str] = set()
|
||||||
right_names: Set[str] = set()
|
right_names: Set[str] = set()
|
||||||
@ -184,8 +203,8 @@ def create_collection(left: str, right: str) -> Collection:
|
|||||||
collect_files(collection, left, right)
|
collect_files(collection, left, right)
|
||||||
else:
|
else:
|
||||||
pl, pr = os.path.abspath(left), os.path.abspath(right)
|
pl, pr = os.path.abspath(left), os.path.abspath(right)
|
||||||
path_name_map[pl] = left
|
path_name_map[pl] = resolve_remote_name(pl, left)
|
||||||
path_name_map[pr] = right
|
path_name_map[pr] = resolve_remote_name(pr, right)
|
||||||
collection.add_change(pl, pr)
|
collection.add_change(pl, pr)
|
||||||
collection.finalize()
|
collection.finalize()
|
||||||
return collection
|
return collection
|
||||||
|
|||||||
@ -22,7 +22,7 @@ from kitty.cli_stub import DiffCLIOptions
|
|||||||
from kitty.conf.utils import KittensKeyAction
|
from kitty.conf.utils import KittensKeyAction
|
||||||
from kitty.constants import appname
|
from kitty.constants import appname
|
||||||
from kitty.fast_data_types import wcswidth
|
from kitty.fast_data_types import wcswidth
|
||||||
from kitty.key_encoding import KeyEvent, EventType
|
from kitty.key_encoding import EventType, KeyEvent
|
||||||
from kitty.options_stub import DiffOptions
|
from kitty.options_stub import DiffOptions
|
||||||
from kitty.utils import ScreenSize
|
from kitty.utils import ScreenSize
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ from ..tui.operations import styled
|
|||||||
from . import global_data
|
from . import global_data
|
||||||
from .collect import (
|
from .collect import (
|
||||||
Collection, create_collection, data_for_path, lines_for_path, sanitize,
|
Collection, create_collection, data_for_path, lines_for_path, sanitize,
|
||||||
set_highlight_data
|
set_highlight_data, add_remote_dir
|
||||||
)
|
)
|
||||||
from .config import init_config
|
from .config import init_config
|
||||||
from .patch import Differ, Patch, set_diff_command, worker_processes
|
from .patch import Differ, Patch, set_diff_command, worker_processes
|
||||||
@ -567,17 +567,39 @@ def terminate_processes(processes: Iterable[int]) -> None:
|
|||||||
os.kill(pid, signal.SIGKILL)
|
os.kill(pid, signal.SIGKILL)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ssh_file(hostname: str, rpath: str) -> str:
|
||||||
|
import io
|
||||||
|
import shutil
|
||||||
|
import tarfile
|
||||||
|
tdir = tempfile.mkdtemp(suffix=f'-{hostname}')
|
||||||
|
add_remote_dir(tdir)
|
||||||
|
atexit.register(shutil.rmtree, tdir)
|
||||||
|
is_abs = rpath.startswith('/')
|
||||||
|
rpath = rpath.lstrip('/')
|
||||||
|
cmd = ['ssh', hostname, 'tar', '-c', '-f', '-']
|
||||||
|
if is_abs:
|
||||||
|
cmd.extend(('-C', '/'))
|
||||||
|
cmd.append(rpath)
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||||
|
assert p.stdout is not None
|
||||||
|
raw = p.stdout.read()
|
||||||
|
if p.wait() != 0:
|
||||||
|
raise SystemExit(p.returncode)
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as tf:
|
||||||
|
members = tf.getmembers()
|
||||||
|
tf.extractall(tdir)
|
||||||
|
if len(members) == 1:
|
||||||
|
for root, dirs, files in os.walk(tdir):
|
||||||
|
if files:
|
||||||
|
return os.path.join(root, files[0])
|
||||||
|
return os.path.abspath(os.path.join(tdir, rpath))
|
||||||
|
|
||||||
|
|
||||||
def get_remote_file(path: str) -> str:
|
def get_remote_file(path: str) -> str:
|
||||||
if path.startswith('ssh:'):
|
if path.startswith('ssh:'):
|
||||||
parts = path.split(':', 2)
|
parts = path.split(':', 2)
|
||||||
if len(parts) == 3:
|
if len(parts) == 3:
|
||||||
hostname, rpath = parts[1:]
|
return get_ssh_file(parts[1], parts[2])
|
||||||
with tempfile.NamedTemporaryFile(suffix='-' + os.path.basename(rpath), prefix='remote:', delete=False) as tf:
|
|
||||||
atexit.register(os.remove, tf.name)
|
|
||||||
p = subprocess.Popen(['ssh', hostname, 'cat', rpath], stdout=tf)
|
|
||||||
if p.wait() != 0:
|
|
||||||
raise SystemExit(p.returncode)
|
|
||||||
return tf.name
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
@ -588,12 +610,12 @@ def main(args: List[str]) -> None:
|
|||||||
raise SystemExit('You must specify exactly two files/directories to compare')
|
raise SystemExit('You must specify exactly two files/directories to compare')
|
||||||
left, right = items
|
left, right = items
|
||||||
global_data.title = _('{} vs. {}').format(left, right)
|
global_data.title = _('{} vs. {}').format(left, right)
|
||||||
if os.path.isdir(left) != os.path.isdir(right):
|
|
||||||
raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.')
|
|
||||||
opts = init_config(cli_opts)
|
opts = init_config(cli_opts)
|
||||||
set_diff_command(opts.diff_cmd)
|
set_diff_command(opts.diff_cmd)
|
||||||
lines_for_path.replace_tab_by = opts.replace_tab_by
|
lines_for_path.replace_tab_by = opts.replace_tab_by
|
||||||
left, right = map(get_remote_file, (left, right))
|
left, right = map(get_remote_file, (left, right))
|
||||||
|
if os.path.isdir(left) != os.path.isdir(right):
|
||||||
|
raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.')
|
||||||
for f in left, right:
|
for f in left, right:
|
||||||
if not os.path.exists(f):
|
if not os.path.exists(f):
|
||||||
raise SystemExit('{} does not exist'.format(f))
|
raise SystemExit('{} does not exist'.format(f))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user