diff --git a/docs/changelog.rst b/docs/changelog.rst index a7822d6de..2cc0940ff 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -62,6 +62,9 @@ Detailed list of changes - ssh kitten: Allow ssh kitten to work from inside tmux, provided the tmux session inherits the correct KITTY env vars (:iss:`5227`) +- ssh kitten: A new option :code:`--symlink-strategy` to control how symlinks + are copied to the remote machine (:iss:`5249`) + 0.25.2 [2022-06-07] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kittens/ssh/copy.py b/kittens/ssh/copy.py index 98a17b0a5..1f863df8c 100644 --- a/kittens/ssh/copy.py +++ b/kittens/ssh/copy.py @@ -39,6 +39,17 @@ A glob pattern. Files with names matching this pattern are excluded from being transferred. Useful when adding directories. Can be specified multiple times, if any of the patterns match the file will be excluded. + + +--symlink-strategy +default=preserve +choices=preserve,resolve,keep-path +Control what happens if the specified path is a symlink. The default is to preserve +the symlink, re-creating it on the remote machine. Setting this to :code:`resolve` +will cause the symlink to be followed and its target used as the file/directory to copy. +The value of :code:`keep-path` is the same as :code:`resolve` except that the remote +file path is derived from the symlink's path instead of the path of the symlink's target. +Note that this option does not apply to symlinks encountered while recursively copying directories. ''' @@ -100,5 +111,9 @@ def parse_copy_instructions(val: str, current_val: Dict[str, str]) -> Iterable[T raise CopyCLIError('Specifying a remote location with more than one file is not supported') home = home_path() for loc in locations: - arcname = get_arcname(loc, opts.dest, home) - yield str(uuid.uuid4()), CopyInstruction(loc, arcname, tuple(opts.exclude)) + if opts.symlink_strategy != 'preserve': + rp = os.path.realpath(loc) + else: + rp = loc + arcname = get_arcname(rp if opts.symlink_strategy == 'resolve' else loc, opts.dest, home) + yield str(uuid.uuid4()), CopyInstruction(rp, arcname, tuple(opts.exclude)) diff --git a/kitty_tests/ssh.py b/kitty_tests/ssh.py index 0b5c5bcc6..7b3bdbebb 100644 --- a/kitty_tests/ssh.py +++ b/kitty_tests/ssh.py @@ -106,9 +106,13 @@ print(' '.join(map(str, buf)))'''), lines=13, cols=77) touch('d1/d2/x') touch('d1/d2/w.exclude') os.symlink('d2/x', f'{local_home}/d1/y') + os.symlink('simple-file', f'{local_home}/s1') + os.symlink('simple-file', f'{local_home}/s2') conf = '''\ copy simple-file +copy s1 +copy --symlink-strategy=keep-name s2 copy --dest=a/sfa simple-file copy --glob g.* copy --exclude */w.* d1 @@ -124,12 +128,14 @@ copy --exclude */w.* d1 self.assertTrue(os.path.lexists(f'{remote_home}/{tname}/78')) self.assertTrue(os.path.exists(f'{remote_home}/{tname}/78/xterm-kitty')) self.assertTrue(os.path.exists(f'{remote_home}/{tname}/x/xterm-kitty')) - for w in ('simple-file', 'a/sfa'): + for w in ('simple-file', 'a/sfa', 's2'): with open(os.path.join(remote_home, w), 'r') as f: self.ae(f.read(), simple_data) + self.assertFalse(os.path.islink(f.name)) self.assertTrue(os.path.lexists(f'{remote_home}/d1/y')) self.assertTrue(os.path.exists(f'{remote_home}/d1/y')) self.ae(os.readlink(f'{remote_home}/d1/y'), 'd2/x') + self.ae(os.readlink(f'{remote_home}/s1'), 'simple-file') contents = set(files_in(remote_home)) contents.discard('.zshrc') # added by check_bootstrap() # depending on platform one of these is a symlink and hence @@ -137,7 +143,7 @@ copy --exclude */w.* d1 contents.discard(f'{tname}/x/xterm-kitty') contents.discard(f'{tname}/78/xterm-kitty') self.ae(contents, { - 'g.1', 'g.2', f'{tname}/kitty.terminfo', 'simple-file', 'd1/d2/x', 'd1/y', 'a/sfa', + 'g.1', 'g.2', f'{tname}/kitty.terminfo', 'simple-file', 'd1/d2/x', 'd1/y', 'a/sfa', 's1', 's2', '.local/share/kitty-ssh-kitten/kitty/version', '.local/share/kitty-ssh-kitten/kitty/bin/kitty' }) self.ae(len(glob.glob(f'{remote_home}/{tname}/*/xterm-kitty')), 2)