diff --git a/README.md b/README.md index f68500c..f77623d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It supports mapping multiple local and remote paths, excluded path, and more. ## Commands -- `TransferInit` - create a config file and open it. Just edit if it already exists. +- `TransferInit [project|global]?` - create a project-level or global config file and open it. Just edit if it already exists. If the argument is omitted, the command is called with "project". - `DiffRemote` - open a diff view with the remote file. - `TransferRepeat` - repeat the last transfer command (except TransferInit, DiffRemote). - `TransferUpload [path]` - upload the given file or directory. @@ -29,16 +29,19 @@ It supports mapping multiple local and remote paths, excluded path, and more. - `TransferDirDiff [path]` - diff the directory with the remote one and display the changed files in the quickfix. ## Deployment config example +Run `TransferInit project` or `TransferInit global` to create or open the `deployment.lua` config file. +If there are both a project-level and a global config file, the settings in the project-level config file will overwrite the global settings. ```lua --- .nvim/deployment.lua +-- Project config file /.nvim/deployment.lua +-- or global config file ~/.local/share/nvim/deployment.lua return { ["example_name"] = { host = "myhost", username = "web", -- optional mappings = { { - ["local"] = "live", -- path relative to project root + ["local"] = "live", -- path relative to project root (for project config file) or absolute path (for global config file) ["remote"] = "/var/www/example.com", -- absolute path or relative to user home }, { @@ -47,7 +50,7 @@ return { }, }, excludedPaths = { -- optional - "live/src/", -- local path relative to project root + "live/src/", -- local path relative to project root (for project config file) or absolute path (for global config file) "test/src/", }, }, diff --git a/lua/transfer/commands.lua b/lua/transfer/commands.lua index b458273..8f8bb60 100644 --- a/lua/transfer/commands.lua +++ b/lua/transfer/commands.lua @@ -14,13 +14,34 @@ local function create_autocmd() }) end +---@param is_project_config boolean +---@return string +local function get_config_path(is_project_config) + if is_project_config then + local dir = vim.loop.cwd() .. "/.nvim" + if vim.fn.isdirectory(dir) == 0 then + vim.fn.mkdir(dir) + end + return dir .. "/deployment.lua" + end + return vim.fn.stdpath("data") .. "/deployment.lua" +end + M.setup = function() create_autocmd() - -- TransferInit - create a config file and open it. Just edit if it already exists - vim.api.nvim_create_user_command("TransferInit", function() + -- TransferInit - create a project-level or global config file and open it. Just edit if it already exists + vim.api.nvim_create_user_command("TransferInit", function(selected) + local arg = selected.fargs[1] or "local" + if arg ~= "project" and arg ~= "global" then + vim.notify("Invalid argument: %s" .. arg, vim.log.levels.WARN, { + title = "Transfer.nvim", + icon = "", + }) + return + end local config = require("transfer.config") - local template = config.options.config_template + local template = arg == "project" and config.options.config_template_local or config.options.config_template_global -- if template is a function, call it if type(template) == "function" then template = template() @@ -29,16 +50,13 @@ M.setup = function() if type(template) == "string" then template = vim.fn.split(template, "\n") end - local path = vim.loop.cwd() .. "/.nvim" - if vim.fn.isdirectory(path) == 0 then - vim.fn.mkdir(path) - end - path = path .. "/deployment.lua" + + local path = get_config_path(arg == "project") if vim.fn.filereadable(path) == 0 then vim.fn.writefile(template, path) end vim.cmd("edit " .. path) - end, { nargs = 0 }) + end, { nargs = "?" }) -- TransferRepeat - repeat the last transfer command vim.api.nvim_create_user_command("TransferRepeat", function() diff --git a/lua/transfer/config.lua b/lua/transfer/config.lua index 4964e7b..1ae5851 100644 --- a/lua/transfer/config.lua +++ b/lua/transfer/config.lua @@ -2,13 +2,13 @@ local M = {} M.defaults = { -- deployment config template: can be a string, a function or a table of lines - config_template = [[ + config_template_local = [[ return { ["server1"] = { host = "server1", mappings = { { - ["local"] = "domains/example.com", + ["local"] = "domains/example.com", -- path relative to project root ["remote"] = "/var/www/example.com", }, }, @@ -17,6 +17,22 @@ return { -- }, }, } +]], + config_template_global = [[ +return { + ["server1"] = { + host = "server1", + mappings = { + { + ["local"] = "~/myproject/domains/example", -- absolute path to project root + ["remote"] = "/var/www/example.com", + }, + }, + -- excludedPaths = { + -- "/var/src", -- absolute path + -- }, + }, +} ]], close_diffview_mapping = "b", -- buffer related mapping to close diffview, set to nil to disable mapping upload_rsync_params = { diff --git a/lua/transfer/transfer.lua b/lua/transfer/transfer.lua index cfdf6b8..ce96b95 100644 --- a/lua/transfer/transfer.lua +++ b/lua/transfer/transfer.lua @@ -2,10 +2,29 @@ local config = require("transfer.config") local M = {} --- reloads the buffer after a transfer --- refreshes the neo-tree if the buffer is a neo-tree --- @param bufnr number --- @return void +local function file_exists(path) + return vim.fn.filereadable(path) == 1 +end + +local function load_config(path) + if file_exists(path) then + local success, result = pcall(dofile, path) + if success then + return result + else + vim.notify( + "Error loading config file: " .. path .. "\n" .. result, + vim.log.levels.ERROR, + { title = "Transfer.nvim" } + ) + end + end + return {} +end + +---reloads the buffer after a transfer +---refreshes the neo-tree if the buffer is a neo-tree +---@param bufnr number local function reload_buffer(bufnr) local filetype = vim.api.nvim_buf_get_option(bufnr, "filetype") if filetype == "neo-tree" then @@ -22,9 +41,9 @@ local function reload_buffer(bufnr) end end --- convert the given local absolute path to a relative project root path --- @param absolute_path string --- @return string +---convert the given local absolute path to a relative project root path +---@param absolute_path string +---@return string local function normalize_local_path(absolute_path) local cwd = vim.loop.cwd() local found, found_end = string.find(absolute_path, cwd, 1, true) @@ -35,31 +54,38 @@ local function normalize_local_path(absolute_path) return string.gsub(absolute_path, "^/", "") end --- check if the given path matches the given pattern --- @param path string --- @param pattern string --- @return boolean -local function path_matches(path, pattern) +---check if the given path matches the given pattern +---@param path string +---@param pattern string +---@param is_relative boolean +---@return boolean +local function path_matches(path, pattern, is_relative) pattern = string.gsub(pattern, "/$", "") path = string.gsub(path, "/$", "") - local s, e = string.find(path, pattern, 1, true) - if s ~= 1 then + if is_relative then + local s, e = string.find(path, pattern, 1, true) + if s ~= 1 then + return false + end + if e == #path then + return true + end + local next_char = string.sub(path, e + 1, e + 1) + if next_char == "/" then + return true + end return false + else + local absolute_path = vim.fs.normalize(vim.loop.cwd() .. "/" .. path) + local absolute_pattern = vim.fs.normalize(pattern) + return vim.startswith(absolute_path, absolute_pattern) end - if e == #path then - return true - end - local next_char = string.sub(path, e + 1, e + 1) - if next_char == "/" then - return true - end - return false end --- get the remote path for scp --- @param deployment table --- @param remote_file string --- @return string +---get the remote path for scp +---@param deployment table +---@param remote_file string +---@return string local function build_scp_path(deployment, remote_file) local remote_path = "scp://" if deployment.username then @@ -70,18 +96,20 @@ local function build_scp_path(deployment, remote_file) return remote_path end --- get the excluded paths for the given directory --- @param deployment table --- @param dir string --- @return table +---get the excluded paths for the given directory +---@param deployment table +---@param dir string +---@return table function M.excluded_paths_for_dir(deployment, dir) local excludedPaths = {} if deployment and deployment.excludedPaths and #deployment.excludedPaths > 0 then + local is_project_config = deployment._is_project_config + local local_paths = { project = normalize_local_path(dir), global = dir } -- remove cwd from local file path local local_path = normalize_local_path(dir) for _, excluded in pairs(deployment.excludedPaths) do excluded = string.gsub(excluded, "^/", "") - if path_matches(excluded, local_path) then + if path_matches(excluded, local_path, local_paths[is]) then local s, e = string.find(excluded, local_path, 1, true) if s then excluded = string.sub(excluded, e + 1) @@ -97,15 +125,27 @@ function M.excluded_paths_for_dir(deployment, dir) return excludedPaths end --- get the remote path for scp --- @param local_path string --- @return string +---get the remote path for scp +---@param local_path string +---@return string? function M.remote_scp_path(local_path) - local cwd = vim.loop.cwd() - local config_file = cwd .. "/.nvim/deployment.lua" - if vim.fn.filereadable(config_file) ~= 1 then + local project_config_file = vim.loop.cwd() .. "/.nvim/deployment.lua" + local global_config_file = vim.fn.stdpath("data") .. "/deployment.lua" + local project_deployment_conf = load_config(project_config_file) + local global_deployment_conf = load_config(global_config_file) + local merged_deployment_conf = global_deployment_conf + for _, deployment in pairs(merged_deployment_conf) do + deployment._config_type = "global" + end + -- Merge configurations, project overrides global + for name, deployment in pairs(project_deployment_conf) do + merged_deployment_conf[name] = deployment + deployment._config_type = "project" + end + + if vim.tbl_isempty(merged_deployment_conf) then vim.notify( - "No deployment config found in \n" .. config_file .. "\n\nRun `:TransferInit` to create it", + "No deployment config found in \n" .. project_config_file .. " or " .. global_config_file, vim.log.levels.WARN, { title = "Transfer.nvim", @@ -115,19 +155,25 @@ function M.remote_scp_path(local_path) ) return nil end - local deployment_conf = dofile(config_file) - -- remove cwd from local file path - local_path = normalize_local_path(local_path) + + -- Local path is relative to project root for project config, absolute otherwise + local local_paths = { project = normalize_local_path(local_path), global = local_path } local skip_reason - for name, deployment in pairs(deployment_conf) do + for name, deployment in pairs(merged_deployment_conf) do + local config_type = deployment._config_type + local local_path = local_paths[config_type] + if config_type == "project" then + local_path = normalize_local_path(local_path) + end local skip = false if deployment.excludedPaths ~= nil then for _, excluded in pairs(deployment.excludedPaths) do excluded = string.gsub(excluded, "^/", "") - if path_matches(local_path, excluded) then + if path_matches(local_path, excluded, config_type == "project") then skip_reason = "File is excluded from deployment\non " .. name .. " by rule: " .. excluded skip = true + break end end end @@ -148,7 +194,7 @@ function M.remote_scp_path(local_path) end return build_scp_path(deployment, remote_file), deployment else - if path_matches(local_path, mapped) then + if path_matches(local_path, mapped, config_type == "project") then if local_path:sub(-1) == "/" and mapped:sub(-1) ~= "/" then -- if local_path ends with a slash, and mapped does not, add it mapped = mapped .. "/" @@ -183,9 +229,9 @@ function M.remote_scp_path(local_path) return nil end --- get the remote path for rsync --- @param local_path string --- @return string +---get the remote path for rsync +---@param local_path string +---@return string function M.remote_rsync_path(local_path) local remote_path, deployment = M.remote_scp_path(local_path) if remote_path == nil then @@ -198,9 +244,9 @@ function M.remote_rsync_path(local_path) return remote_path, deployment end --- upload the given file --- @param local_path string --- @return void +---upload the given file +---@param local_path string +---@return void function M.upload_file(local_path) if local_path == nil then local_path = vim.fn.expand("%:p") @@ -249,8 +295,8 @@ function M.upload_file(local_path) }) end --- Replace local file with remote copy --- @param local_path string|nil +---Replace local file with remote copy +---@param local_path string|nil function M.download_file(local_path) if local_path == nil then local_path = vim.fn.expand("%:p") @@ -305,9 +351,9 @@ function M.download_file(local_path) }) end --- Sync local and remote directory --- @param dir string --- @param upload boolean +---Sync local and remote directory +---@param dir string +---@param upload boolean function M.sync_dir(dir, upload) local remote_path, deployment = M.remote_rsync_path(dir) if remote_path == nil then