diff --git a/doc/oil.txt b/doc/oil.txt index 6691db0..b0807df 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -175,6 +175,21 @@ The full list of options with their defaults: extra_s3_args = {}, -- Extra arguments to pass to curl for FTP operations extra_curl_args = {}, + -- Per-host SCP arg overrides. Args are appended after global extra_scp_args. + -- Keys are exact hostnames as they appear in the SSH URL. + ssh_hosts = { + -- ["myserver.com"] = { extra_scp_args = { "-O" } }, + }, + -- Per-bucket S3 arg overrides. Args are appended after global extra_s3_args. + -- Keys are exact S3 bucket names. + s3_buckets = { + -- ["my-r2-bucket"] = { extra_s3_args = { "--endpoint-url", "https://..." } }, + }, + -- Per-host curl arg overrides for FTP. Args are appended after global extra_curl_args. + -- Keys are exact hostnames as they appear in the FTP URL. + ftp_hosts = { + -- ["ftp.internal.com"] = { extra_curl_args = { "--insecure" } }, + }, -- EXPERIMENTAL support for performing file operations with git git = { -- Return true to automatically git add/mv/rm files @@ -337,6 +352,35 @@ preview_win.max_file_size *oil.preview_win* than this limit will show a placeholder message instead of being loaded. Set to `nil` to disable the limit. +ssh_hosts *oil.ssh_hosts* + type: `table` default: `{}` + Per-host SCP argument overrides. Keys are exact hostnames as they appear + in the SSH URL. Per-host args are appended after the global + `extra_scp_args`. Example: >lua + ssh_hosts = { + ["nas.local"] = { extra_scp_args = { "-O" } }, + } +< + +s3_buckets *oil.s3_buckets* + type: `table` default: `{}` + Per-bucket S3 argument overrides. Keys are exact S3 bucket names. + Per-bucket args are appended after the global `extra_s3_args`. Example: >lua + s3_buckets = { + ["my-r2-bucket"] = { extra_s3_args = { "--endpoint-url", "https://..." } }, + } +< + +ftp_hosts *oil.ftp_hosts* + type: `table` default: `{}` + Per-host curl argument overrides for FTP operations. Keys are exact + hostnames as they appear in the FTP URL. Per-host args are appended after + the global `extra_curl_args`. Example: >lua + ftp_hosts = { + ["ftp.internal.com"] = { extra_curl_args = { "--insecure" } }, + } +< + -------------------------------------------------------------------------------- API *oil-api* diff --git a/doc/upstream.md b/doc/upstream.md index 2f6dfb0..9e1b9a9 100644 --- a/doc/upstream.md +++ b/doc/upstream.md @@ -99,7 +99,7 @@ issues against this fork. | [#578](https://github.com/stevearc/oil.nvim/issues/578) | Hidden file dimming recipe | fixed | | [#587](https://github.com/stevearc/oil.nvim/issues/587) | Alt+h keymap | not actionable — user config issue | | [#599](https://github.com/stevearc/oil.nvim/issues/599) | user:group display and manipulation | consolidated into [#126](https://github.com/barrettruth/canola.nvim/issues/126) | -| [#607](https://github.com/stevearc/oil.nvim/issues/607) | Per-host SCP args | open | +| [#607](https://github.com/stevearc/oil.nvim/issues/607) | Per-host SCP args | fixed | | [#609](https://github.com/stevearc/oil.nvim/issues/609) | Cursor placement via Snacks picker | not actionable — Windows-only | | [#612](https://github.com/stevearc/oil.nvim/issues/612) | Delete buffers on file delete | fixed | | [#615](https://github.com/stevearc/oil.nvim/issues/615) | Cursor at name column on o/O | fixed ([#72](https://github.com/barrettruth/canola.nvim/pull/72)) | diff --git a/lua/oil/adapters/ftp.lua b/lua/oil/adapters/ftp.lua index 36b2857..9ba0248 100644 --- a/lua/oil/adapters/ftp.lua +++ b/lua/oil/adapters/ftp.lua @@ -106,6 +106,17 @@ local function curl_ftp_url(url) return table.concat(pieces, '') end +---@param host string +---@return string[] +local function resolved_curl_args(host) + local extra = vim.deepcopy(config.extra_curl_args) + local host_cfg = config.ftp_hosts[host] + if host_cfg and host_cfg.extra_curl_args then + vim.list_extend(extra, host_cfg.extra_curl_args) + end + return extra +end + ---@param url oil.ftpUrl ---@param py_lines string[] ---@param cb fun(err: nil|string) @@ -113,8 +124,8 @@ local function ftpcmd(url, py_lines, cb) local lines = {} local use_tls = url.scheme == 'oil-ftps://' if use_tls then - local insecure = vim.tbl_contains(config.extra_curl_args, '--insecure') - or vim.tbl_contains(config.extra_curl_args, '-k') + local curl_args = resolved_curl_args(url.host) + local insecure = vim.tbl_contains(curl_args, '--insecure') or vim.tbl_contains(curl_args, '-k') table.insert(lines, 'import ftplib, ssl') table.insert(lines, 'ctx = ssl.create_default_context()') if insecure then @@ -170,7 +181,7 @@ local function curl(url, extra_args, opts, cb) end local cmd = { 'curl', '-sS', '--netrc-optional' } vim.list_extend(cmd, ssl_args(url)) - vim.list_extend(cmd, config.extra_curl_args) + vim.list_extend(cmd, resolved_curl_args(url.host)) vim.list_extend(cmd, extra_args) shell.run(cmd, opts, cb) end diff --git a/lua/oil/adapters/s3/s3fs.lua b/lua/oil/adapters/s3/s3fs.lua index a81f10c..e3b60ef 100644 --- a/lua/oil/adapters/s3/s3fs.lua +++ b/lua/oil/adapters/s3/s3fs.lua @@ -46,8 +46,22 @@ end ---@param cmd string[] cmd and flags ---@return string[] Shell command to run local function create_s3_command(cmd) + local bucket + for _, arg in ipairs(cmd) do + bucket = arg:match('^s3://([^/]+)') + if bucket then + break + end + end + local extra = vim.deepcopy(config.extra_s3_args) + if bucket then + local bucket_cfg = config.s3_buckets[bucket] + if bucket_cfg and bucket_cfg.extra_s3_args then + vim.list_extend(extra, bucket_cfg.extra_s3_args) + end + end local full_cmd = vim.list_extend({ 'aws', 's3' }, cmd) - return vim.list_extend(full_cmd, config.extra_s3_args) + return vim.list_extend(full_cmd, extra) end ---@param url string diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua index a125c6c..a87855b 100644 --- a/lua/oil/adapters/ssh.lua +++ b/lua/oil/adapters/ssh.lua @@ -21,9 +21,21 @@ local FIELD_META = constants.FIELD_META ---@field port nil|integer ---@field path string +---@param hosts string[] ---@param args string[] -local function scp(args, ...) - local cmd = vim.list_extend({ 'scp', '-C' }, config.extra_scp_args) +local function scp(hosts, args, ...) + local extra = vim.deepcopy(config.extra_scp_args) + local seen = {} + for _, host in ipairs(hosts) do + if not seen[host] then + seen[host] = true + local host_cfg = config.ssh_hosts[host] + if host_cfg and host_cfg.extra_scp_args then + vim.list_extend(extra, host_cfg.extra_scp_args) + end + end + end + local cmd = vim.list_extend({ 'scp', '-C' }, extra) vim.list_extend(cmd, args) shell.run(cmd, ...) end @@ -320,12 +332,16 @@ M.perform_action = function(action, cb) local src_conn = get_connection(action.src_url) local dest_conn = get_connection(action.dest_url) if src_conn ~= dest_conn then - scp({ '-r', url_to_scp(src_res), url_to_scp(dest_res) }, function(err) - if err then - return cb(err) + scp( + { src_res.host, dest_res.host }, + { '-r', url_to_scp(src_res), url_to_scp(dest_res) }, + function(err) + if err then + return cb(err) + end + src_conn:rm(src_res.path, cb) end - src_conn:rm(src_res.path, cb) - end) + ) else src_conn:mv(src_res.path, dest_res.path, cb) end @@ -339,26 +355,31 @@ M.perform_action = function(action, cb) local src_res = M.parse_url(action.src_url) local dest_res = M.parse_url(action.dest_url) if not url_hosts_equal(src_res, dest_res) then - scp({ '-r', url_to_scp(src_res), url_to_scp(dest_res) }, cb) + scp( + { src_res.host, dest_res.host }, + { '-r', url_to_scp(src_res), url_to_scp(dest_res) }, + cb + ) else local src_conn = get_connection(action.src_url) src_conn:cp(src_res.path, dest_res.path, cb) end else - local src_arg - local dest_arg if src_adapter == M then - src_arg = url_to_scp(M.parse_url(action.src_url)) + local src_res = M.parse_url(action.src_url) + local src_arg = url_to_scp(src_res) local _, path = util.parse_url(action.dest_url) assert(path) - dest_arg = fs.posix_to_os_path(path) + local dest_arg = fs.posix_to_os_path(path) + scp({ src_res.host }, { '-r', src_arg, dest_arg }, cb) else local _, path = util.parse_url(action.src_url) assert(path) - src_arg = fs.posix_to_os_path(path) - dest_arg = url_to_scp(M.parse_url(action.dest_url)) + local src_arg = fs.posix_to_os_path(path) + local dest_res = M.parse_url(action.dest_url) + local dest_arg = url_to_scp(dest_res) + scp({ dest_res.host }, { '-r', src_arg, dest_arg }, cb) end - scp({ '-r', src_arg, dest_arg }, cb) end else cb(string.format('Bad action type: %s', action.type)) @@ -384,7 +405,7 @@ M.read_file = function(bufnr) end local tmp_bufnr = vim.fn.bufadd(tmpfile) - scp({ scp_url, tmpfile }, function(err) + scp({ url.host }, { scp_url, tmpfile }, function(err) loading.set_loading(bufnr, false) vim.bo[bufnr].modifiable = true vim.cmd.doautocmd({ args = { 'BufReadPre', bufname }, mods = { silent = true } }) @@ -426,7 +447,7 @@ M.write_file = function(bufnr) vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } }) local tmp_bufnr = vim.fn.bufadd(tmpfile) - scp({ tmpfile, scp_url }, function(err) + scp({ url.host }, { tmpfile, scp_url }, function(err) vim.bo[bufnr].modifiable = true if err then vim.notify(string.format('Error writing file: %s', err), vim.log.levels.ERROR) diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 048412d..1d556cd 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -118,6 +118,9 @@ local default_config = { extra_s3_args = {}, -- Extra arguments to pass to curl for FTP operations extra_curl_args = {}, + ssh_hosts = {}, + s3_buckets = {}, + ftp_hosts = {}, -- EXPERIMENTAL support for performing file operations with git git = { -- Return true to automatically git add/mv/rm files @@ -233,6 +236,15 @@ default_config.adapter_aliases = {} -- here we can get some performance wins default_config.view_options.highlight_filename = nil +---@class (exact) oil.SshHostConfig +---@field extra_scp_args? string[] + +---@class (exact) oil.S3BucketConfig +---@field extra_s3_args? string[] + +---@class (exact) oil.FtpHostConfig +---@field extra_curl_args? string[] + ---@class oil.Config ---@field adapters table Hidden from SetupOpts ---@field adapter_aliases table Hidden from SetupOpts @@ -259,6 +271,9 @@ default_config.view_options.highlight_filename = nil ---@field extra_scp_args string[] ---@field extra_s3_args string[] ---@field extra_curl_args string[] +---@field ssh_hosts table +---@field s3_buckets table +---@field ftp_hosts table ---@field git oil.GitOptions ---@field float oil.FloatWindowConfig ---@field preview_win oil.PreviewWindowConfig @@ -294,6 +309,9 @@ local M = {} ---@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH ---@field extra_s3_args? string[] Extra arguments to pass to aws s3 when moving/copying files using aws s3 ---@field extra_curl_args? string[] Extra arguments to pass to curl for FTP operations +---@field ssh_hosts? table Per-host SCP arg overrides +---@field s3_buckets? table Per-bucket S3 arg overrides +---@field ftp_hosts? table Per-host curl arg overrides ---@field git? oil.SetupGitOptions EXPERIMENTAL support for performing file operations with git ---@field float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float ---@field preview_win? oil.SetupPreviewWindowConfig Configuration for the file preview window diff --git a/spec/per_host_args_spec.lua b/spec/per_host_args_spec.lua new file mode 100644 index 0000000..f2f5fce --- /dev/null +++ b/spec/per_host_args_spec.lua @@ -0,0 +1,97 @@ +local config = require('oil.config') +local test_util = require('spec.test_util') + +describe('per-host/bucket arg overrides', function() + after_each(function() + test_util.reset_editor() + end) + + describe('ssh_hosts', function() + it('stores ssh_hosts from setup', function() + config.setup({ ssh_hosts = { ['myserver.com'] = { extra_scp_args = { '-O' } } } }) + assert.are.equal('-O', config.ssh_hosts['myserver.com'].extra_scp_args[1]) + end) + + it('defaults to empty table when not set', function() + config.setup({}) + assert.are.same({}, config.ssh_hosts) + end) + + it('returns nil for unknown host', function() + config.setup({ ssh_hosts = { ['known.host'] = { extra_scp_args = { '-O' } } } }) + assert.is_nil(config.ssh_hosts['unknown.host']) + end) + end) + + describe('s3_buckets', function() + it('stores s3_buckets from setup', function() + config.setup({ + s3_buckets = { + ['my-r2-bucket'] = { extra_s3_args = { '--endpoint-url', 'https://r2.example.com' } }, + }, + }) + assert.are.equal('--endpoint-url', config.s3_buckets['my-r2-bucket'].extra_s3_args[1]) + assert.are.equal('https://r2.example.com', config.s3_buckets['my-r2-bucket'].extra_s3_args[2]) + end) + + it('defaults to empty table when not set', function() + config.setup({}) + assert.are.same({}, config.s3_buckets) + end) + + it('returns nil for unknown bucket', function() + config.setup({ + s3_buckets = { ['known-bucket'] = { extra_s3_args = { '--no-sign-request' } } }, + }) + assert.is_nil(config.s3_buckets['unknown-bucket']) + end) + end) + + describe('ftp_hosts', function() + it('stores ftp_hosts from setup', function() + config.setup({ ftp_hosts = { ['ftp.internal.com'] = { extra_curl_args = { '--insecure' } } } }) + assert.are.equal('--insecure', config.ftp_hosts['ftp.internal.com'].extra_curl_args[1]) + end) + + it('defaults to empty table when not set', function() + config.setup({}) + assert.are.same({}, config.ftp_hosts) + end) + + it('returns nil for unknown host', function() + config.setup({ ftp_hosts = { ['known.host'] = { extra_curl_args = { '--insecure' } } } }) + assert.is_nil(config.ftp_hosts['unknown.host']) + end) + + it('reflects --insecure in per-host curl args', function() + config.setup({ ftp_hosts = { ['ftp.internal.com'] = { extra_curl_args = { '--insecure' } } } }) + local host_cfg = config.ftp_hosts['ftp.internal.com'] + assert.is_truthy(vim.tbl_contains(host_cfg.extra_curl_args, '--insecure')) + end) + end) + + describe('merge semantics', function() + it('per-host ssh args supplement global args', function() + config.setup({ + extra_scp_args = { '-C' }, + ssh_hosts = { ['myserver.com'] = { extra_scp_args = { '-O' } } }, + }) + assert.are.same({ '-C' }, config.extra_scp_args) + assert.are.same({ '-O' }, config.ssh_hosts['myserver.com'].extra_scp_args) + end) + + it('per-bucket s3 args supplement global args', function() + config.setup({ + extra_s3_args = { '--sse', 'aws:kms' }, + s3_buckets = { + ['special-bucket'] = { extra_s3_args = { '--endpoint-url', 'https://...' } }, + }, + }) + assert.are.same({ '--sse', 'aws:kms' }, config.extra_s3_args) + assert.are.same( + { '--endpoint-url', 'https://...' }, + config.s3_buckets['special-bucket'].extra_s3_args + ) + end) + end) +end)