diff --git a/dist/wt b/dist/wt index 878afa2cbb2fcc3417f4a16ec35edd93318b4622..216f9597e961fda766bcf610f2fc6ea98da590ff 100755 --- a/dist/wt +++ b/dist/wt @@ -197,40 +197,32 @@ end return M ]] - -if _VERSION < "Lua 5.2" then - io.stderr:write("error: wt requires Lua 5.2 or later\n") - os.exit(1) -end - -local exit = require("wt.exit") -local EXIT_SUCCESS = exit.EXIT_SUCCESS -local EXIT_USER_ERROR = exit.EXIT_USER_ERROR -local EXIT_SYSTEM_ERROR = exit.EXIT_SYSTEM_ERROR +_EMBEDDED_MODULES["wt.git"] = [[-- SPDX-FileCopyrightText: Amolith +-- +-- SPDX-License-Identifier: GPL-3.0-or-later local shell = require("wt.shell") local run_cmd = shell.run_cmd local run_cmd_silent = shell.run_cmd_silent local get_cwd = shell.get_cwd -local die = shell.die -local path_mod = require("wt.path") -local branch_to_path = path_mod.branch_to_path -local split_path = path_mod.split_path -local relative_path = path_mod.relative_path -local path_inside = path_mod.path_inside -local escape_pattern = path_mod.escape_pattern +---@class wt.git +local M = {} ---Walk up from cwd looking for .git file pointing to .bare, or .bare/ directory +---@param cwd_override? string optional starting directory (defaults to pwd) ---@return string|nil root ---@return string|nil error -local function find_project_root() - local handle = io.popen("pwd") - if not handle then - return nil, "failed to get current directory" +function M.find_project_root(cwd_override) + local cwd = cwd_override + if not cwd then + local handle = io.popen("pwd") + if not handle then + return nil, "failed to get current directory" + end + cwd = handle:read("*l") + handle:close() end - local cwd = handle:read("*l") - handle:close() if not cwd then return nil, "failed to get current directory" @@ -262,11 +254,177 @@ local function find_project_root() return nil, "not in a wt-managed repository" end +---Check if cwd is inside a worktree (has .git file, not at project root) +---@param root string +---@param cwd_override? string optional current directory (defaults to get_cwd()) +---@return string|nil source_worktree path if inside worktree, nil if at project root +function M.detect_source_worktree(root, cwd_override) + local cwd = cwd_override or get_cwd() + if not cwd then + return nil + end + -- If cwd is the project root, no source worktree + if cwd == root then + return nil + end + -- Check if cwd has a .git file (indicating it's a worktree) + local git_file = io.open(cwd .. "/.git", "r") + if git_file then + git_file:close() + return cwd + end + -- Walk up to find worktree root + ---@type string|nil + local path = cwd + while path and path ~= "" and path ~= "/" and path ~= root do + local gf = io.open(path .. "/.git", "r") + if gf then + gf:close() + return path + end + path = path:match("(.+)/[^/]+$") + end + return nil +end + +---Check if branch exists locally +---@param git_dir string +---@param branch string +---@return boolean +function M.branch_exists_local(git_dir, branch) + return run_cmd_silent("GIT_DIR=" .. git_dir .. " git show-ref --verify --quiet refs/heads/" .. branch) +end + +---Parse git branch -r output to extract remotes containing a branch +---@param output string git branch -r output +---@param branch string branch name to find +---@return string[] remote names +function M.parse_branch_remotes(output, branch) + local remotes = {} + for line in output:gmatch("[^\n]+") do + -- Match: " origin/branch-name" or " upstream/feature/foo" + -- For branch "feature/foo", we want remote "origin", not "origin/feature" + -- The remote name is everything before the LAST occurrence of /branch + local trimmed = line:match("^%s*(.-)%s*$") + if trimmed then + -- Check if line ends with /branch + local suffix = "/" .. branch + if trimmed:sub(-#suffix) == suffix then + local remote = trimmed:sub(1, #trimmed - #suffix) + -- Simple remote name (no slashes) - this is what we want + -- Remote names with slashes (e.g., "forks/alice") are ambiguous + -- and skipped for safety + if remote ~= "" and not remote:match("/") then + table.insert(remotes, remote) + end + end + end + end + return remotes +end + +---Find which remotes have the branch +---@param git_dir string +---@param branch string +---@return string[] remote names +function M.find_branch_remotes(git_dir, branch) + local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git branch -r --list '*/" .. branch .. "'") + if code ~= 0 then + return {} + end + return M.parse_branch_remotes(output, branch) +end + +---Detect default branch from cloned bare repo's HEAD +---@param git_dir string +---@return string branch name +function M.detect_cloned_default_branch(git_dir) + -- First try the bare repo's own HEAD (set during clone) + local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git symbolic-ref HEAD") + if code == 0 and output ~= "" then + local branch = output:match("refs/heads/(.+)") + if branch then + return (branch:gsub("%s+$", "")) + end + end + return "main" +end + +---Get default branch name from git config, fallback to "main" +---@return string +function M.get_default_branch() + local output, code = run_cmd("git config --get init.defaultBranch") + if code == 0 and output ~= "" then + return (output:gsub("%s+$", "")) + end + return "main" +end + +---Parse git worktree list --porcelain output +---@param output string +---@return {path: string, branch?: string, bare?: boolean, detached?: boolean, head?: string}[] +function M.parse_worktree_list(output) + local worktrees = {} + ---@type {path: string, branch?: string, bare?: boolean, detached?: boolean, head?: string}|nil + local current = nil + for line in output:gmatch("[^\n]+") do + local key, value = line:match("^(%S+)%s*(.*)$") + if key == "worktree" and value then + if current then + table.insert(worktrees, current) + end + current = { path = value } + elseif current then + if key == "branch" and value then + current.branch = value:gsub("^refs/heads/", "") + elseif key == "bare" then + current.bare = true + elseif key == "detached" then + current.detached = true + elseif key == "HEAD" then + current.head = value + end + end + end + if current then + table.insert(worktrees, current) + end + return worktrees +end + +---Check if branch is checked out in any worktree +---@param git_dir string +---@param branch string +---@return string|nil path if checked out, nil otherwise +function M.branch_checked_out_at(git_dir, branch) + local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain") + if code ~= 0 then + return nil + end + local worktrees = M.parse_worktree_list(output) + for _, wt in ipairs(worktrees) do + if wt.branch == branch then + return wt.path + end + end + return nil +end + +return M +]] + +_EMBEDDED_MODULES["wt.config"] = [[-- SPDX-FileCopyrightText: Amolith +-- +-- SPDX-License-Identifier: GPL-3.0-or-later + +---@class wt.config +local M = {} + ---Substitute ${project} in template string ---@param template string ---@param project_name string ---@return string -local function resolve_url_template(template, project_name) +function M.resolve_url_template(template, project_name) local escaped = project_name:gsub("%%", "%%%%") return (template:gsub("%${project}", escaped)) end @@ -274,7 +432,7 @@ end ---Parse git URLs to extract project name ---@param url string ---@return string|nil -local function _extract_project_name(url) -- luacheck: ignore 211 +function M.extract_project_name(url) if not url or url == "" then return nil end @@ -302,6 +460,116 @@ local function _extract_project_name(url) -- luacheck: ignore 211 return name end +---Load global config from ~/.config/wt/config.lua +---@return table +function M.load_global_config() + local home = os.getenv("HOME") + if not home then + return {} + end + local config_path = home .. "/.config/wt/config.lua" + local f = io.open(config_path, "r") + if not f then + return {} + end + local content = f:read("*a") + f:close() + local chunk, err = load(content, config_path, "t", {}) + if not chunk then + chunk, err = load("return " .. content, config_path, "t", {}) + end + if not chunk then + io.stderr:write("warning: config syntax error in " .. config_path .. ": " .. err .. "\n") + return {} + end + local ok, result = pcall(chunk) + if not ok then + io.stderr:write("warning: config execution error in " .. config_path .. ": " .. result .. "\n") + return {} + end + if type(result) ~= "table" then + io.stderr:write("warning: config must return a table in " .. config_path .. "\n") + return {} + end + return result +end + +---Load project config from /.wt.lua +---@param root string +---@return {hooks?: {copy?: string[], symlink?: string[], run?: string[]}} +function M.load_project_config(root) + local config_path = root .. "/.wt.lua" + local f = io.open(config_path, "r") + if not f then + return {} + end + local content = f:read("*a") + f:close() + + local chunk, err = load(content, config_path, "t", {}) + if not chunk then + chunk, err = load("return " .. content, config_path, "t", {}) + end + if not chunk then + io.stderr:write("warning: config syntax error in " .. config_path .. ": " .. err .. "\n") + return {} + end + local ok, result = pcall(chunk) + if not ok then + io.stderr:write("warning: config execution error in " .. config_path .. ": " .. result .. "\n") + return {} + end + if type(result) ~= "table" then + io.stderr:write("warning: config must return a table in " .. config_path .. "\n") + return {} + end + return result +end + +return M +]] + + +if _VERSION < "Lua 5.2" then + io.stderr:write("error: wt requires Lua 5.2 or later\n") + os.exit(1) +end + +local exit = require("wt.exit") +local EXIT_SUCCESS = exit.EXIT_SUCCESS +local EXIT_USER_ERROR = exit.EXIT_USER_ERROR +local EXIT_SYSTEM_ERROR = exit.EXIT_SYSTEM_ERROR + +local shell = require("wt.shell") +local run_cmd = shell.run_cmd +local run_cmd_silent = shell.run_cmd_silent +local get_cwd = shell.get_cwd +local die = shell.die + +local path_mod = require("wt.path") +local branch_to_path = path_mod.branch_to_path +local split_path = path_mod.split_path +local relative_path = path_mod.relative_path +local path_inside = path_mod.path_inside +local escape_pattern = path_mod.escape_pattern + +local git_mod = require("wt.git") +local find_project_root = git_mod.find_project_root +local detect_source_worktree = git_mod.detect_source_worktree +local branch_exists_local = git_mod.branch_exists_local +local find_branch_remotes = git_mod.find_branch_remotes +local detect_cloned_default_branch = git_mod.detect_cloned_default_branch +local get_default_branch = git_mod.get_default_branch +local parse_branch_remotes = git_mod.parse_branch_remotes +local parse_worktree_list = git_mod.parse_worktree_list +local branch_checked_out_at = git_mod.branch_checked_out_at + +local config_mod = require("wt.config") +local resolve_url_template = config_mod.resolve_url_template +local extract_project_name = config_mod.extract_project_name +local load_global_config = config_mod.load_global_config +local load_project_config = config_mod.load_project_config + ---Print usage information local function print_usage() print("wt - git worktree manager") @@ -444,208 +712,6 @@ local function show_command_help(cmd) os.exit(EXIT_SUCCESS) end ----Parse git URLs to extract project name (exported version) ----@param url string ----@return string|nil -local function extract_project_name(url) - if not url or url == "" then - return nil - end - - url = url:gsub("[?#].*$", "") - url = url:gsub("/+$", "") - - if url == "" or url == "/" then - return nil - end - - url = url:gsub("%.git$", "") - - if not url:match("://") then - local scp_path = url:match("^[^@]+@[^:]+:(.+)$") - if scp_path and scp_path ~= "" then - url = scp_path - end - end - - local name = url:match("([^/]+)$") or url:match("([^:]+)$") - if not name or name == "" then - return nil - end - return name -end - ----Detect default branch from cloned bare repo ----@param git_dir string ----@return string -local function detect_cloned_default_branch(git_dir) - -- First try the bare repo's own HEAD (set during clone) - local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git symbolic-ref HEAD") - if code == 0 and output ~= "" then - local branch = output:match("refs/heads/(.+)") - if branch then - return (branch:gsub("%s+$", "")) - end - end - return "main" -end - ----Get default branch name from git config, fallback to "main" ----@return string -local function get_default_branch() - local output, code = run_cmd("git config --get init.defaultBranch") - if code == 0 and output ~= "" then - return (output:gsub("%s+$", "")) - end - return "main" -end - ----Load global config from ~/.config/wt/config.lua ----@return {branch_path_style?: string, flat_separator?: string, remotes?: table, default_remotes?: string[]|string} -local function load_global_config() - local home = os.getenv("HOME") - if not home then - return {} - end - local config_path = home .. "/.config/wt/config.lua" - local f = io.open(config_path, "r") - if not f then - return {} - end - local content = f:read("*a") - f:close() - local chunk, err = load(content, config_path, "t", {}) - if not chunk then - chunk, err = load("return " .. content, config_path, "t", {}) - end - if not chunk then - io.stderr:write("warning: config syntax error in " .. config_path .. ": " .. err .. "\n") - return {} - end - local ok, result = pcall(chunk) - if not ok then - io.stderr:write("warning: config execution error in " .. config_path .. ": " .. result .. "\n") - return {} - end - if type(result) ~= "table" then - io.stderr:write("warning: config must return a table in " .. config_path .. "\n") - return {} - end - return result -end - ----Load project config from /.wt.lua ----@param root string ----@return {hooks?: {copy?: string[], symlink?: string[], run?: string[]}} -local function load_project_config(root) - local config_path = root .. "/.wt.lua" - local f = io.open(config_path, "r") - if not f then - return {} - end - local content = f:read("*a") - f:close() - - local chunk, err = load(content, config_path, "t", {}) - if not chunk then - chunk, err = load("return " .. content, config_path, "t", {}) - end - if not chunk then - io.stderr:write("warning: config syntax error in " .. config_path .. ": " .. err .. "\n") - return {} - end - local ok, result = pcall(chunk) - if not ok then - io.stderr:write("warning: config execution error in " .. config_path .. ": " .. result .. "\n") - return {} - end - if type(result) ~= "table" then - io.stderr:write("warning: config must return a table in " .. config_path .. "\n") - return {} - end - return result -end - ----Check if cwd is inside a worktree (has .git file, not at project root) ----@param root string ----@return string|nil source_worktree path if inside worktree, nil if at project root -local function detect_source_worktree(root) - local cwd = get_cwd() - if not cwd then - return nil - end - -- If cwd is the project root, no source worktree - if cwd == root then - return nil - end - -- Check if cwd has a .git file (indicating it's a worktree) - local git_file = io.open(cwd .. "/.git", "r") - if git_file then - git_file:close() - return cwd - end - -- Walk up to find worktree root - ---@type string|nil - local path = cwd - while path and path ~= "" and path ~= "/" and path ~= root do - local gf = io.open(path .. "/.git", "r") - if gf then - gf:close() - return path - end - path = path:match("(.+)/[^/]+$") - end - return nil -end - ----Check if branch exists locally ----@param git_dir string ----@param branch string ----@return boolean -local function branch_exists_local(git_dir, branch) - return run_cmd_silent("GIT_DIR=" .. git_dir .. " git show-ref --verify --quiet refs/heads/" .. branch) -end - ----Parse git branch -r output to extract remotes containing a branch ----@param output string git branch -r output ----@param branch string branch name to find ----@return string[] remote names -local function parse_branch_remotes(output, branch) - local remotes = {} - for line in output:gmatch("[^\n]+") do - -- Match: " origin/branch-name" or " upstream/feature/foo" - -- For branch "feature/foo", we want remote "origin", not "origin/feature" - -- The remote name is everything before the LAST occurrence of /branch - local trimmed = line:match("^%s*(.-)%s*$") - if trimmed then - -- Check if line ends with /branch - local suffix = "/" .. branch - if trimmed:sub(-#suffix) == suffix then - local remote = trimmed:sub(1, #trimmed - #suffix) - -- Simple remote name (no slashes) - this is what we want - -- Remote names with slashes (e.g., "forks/alice") are ambiguous - -- and skipped for safety - if remote ~= "" and not remote:match("/") then - table.insert(remotes, remote) - end - end - end - end - return remotes -end - ----Find which remotes have the branch ----@param git_dir string ----@param branch string ----@return string[] remote names -local function find_branch_remotes(git_dir, branch) - local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git branch -r --list '*/" .. branch .. "'") - if code ~= 0 then - return {} - end - return parse_branch_remotes(output, branch) -end - ---Load hook permissions from ~/.local/share/wt/hook-dirs.lua ---@return table local function load_hook_permissions() @@ -1341,56 +1407,6 @@ local function get_bare_head(git_dir) return (output:gsub("%s+$", "")) end ----Parse git worktree list --porcelain output ----@param output string git worktree list --porcelain output ----@return table[] array of {path: string, branch?: string, bare?: boolean, detached?: boolean} -local function parse_worktree_list(output) - local worktrees = {} - ---@type {path: string, branch?: string, bare?: boolean, detached?: boolean, head?: string}|nil - local current = nil - for line in output:gmatch("[^\n]+") do - local key, value = line:match("^(%S+)%s*(.*)$") - if key == "worktree" and value then - if current then - table.insert(worktrees, current) - end - current = { path = value } - elseif current then - if key == "branch" and value then - current.branch = value:gsub("^refs/heads/", "") - elseif key == "bare" then - current.bare = true - elseif key == "detached" then - current.detached = true - elseif key == "HEAD" then - current.head = value - end - end - end - if current then - table.insert(worktrees, current) - end - return worktrees -end - ----Check if branch is checked out in any worktree ----@param git_dir string ----@param branch string ----@return string|nil path if checked out, nil otherwise -local function branch_checked_out_at(git_dir, branch) - local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain") - if code ~= 0 then - return nil - end - local worktrees = parse_worktree_list(output) - for _, wt in ipairs(worktrees) do - if wt.branch == branch then - return wt.path - end - end - return nil -end - ---@param args string[] local function cmd_remove(args) -- Parse arguments: [-b] [-f] @@ -1987,55 +2003,9 @@ if pcall(debug.getlocal, 4, 1) then end end end, - -- Project root detection - find_project_root = function(cwd_override) - local cwd = cwd_override or get_cwd() - if not cwd then - return nil, "failed to get current directory" - end - local path = cwd - while path and path ~= "" and path ~= "/" do - local bare_check = io.open(path .. "/.bare/HEAD", "r") - if bare_check then - bare_check:close() - return path, nil - end - local git_file = io.open(path .. "/.git", "r") - if git_file then - local content = git_file:read("*a") - git_file:close() - if content and content:match("gitdir:%s*%.?/?%.bare") then - return path, nil - end - end - path = path:match("(.+)/[^/]+$") - end - return nil, "not in a wt-managed repository" - end, - detect_source_worktree = function(root, cwd_override) - local cwd = cwd_override or get_cwd() - if not cwd then - return nil - end - if cwd == root then - return nil - end - local git_file = io.open(cwd .. "/.git", "r") - if git_file then - git_file:close() - return cwd - end - local path = cwd - while path and path ~= "" and path ~= "/" and path ~= root do - local gf = io.open(path .. "/.git", "r") - if gf then - gf:close() - return path - end - path = path:match("(.+)/[^/]+$") - end - return nil - end, + -- Project root detection (re-exported from wt.git) + find_project_root = find_project_root, + detect_source_worktree = detect_source_worktree, -- Command execution (for integration tests) run_cmd = run_cmd, run_cmd_silent = run_cmd_silent, diff --git a/src/main.lua b/src/main.lua index 6b79b3a67e85b65447d8740cfb84efa733372e95..34f9fea55cf54b45dce033497025798251b29bce 100644 --- a/src/main.lua +++ b/src/main.lua @@ -38,45 +38,11 @@ local parse_branch_remotes = git_mod.parse_branch_remotes local parse_worktree_list = git_mod.parse_worktree_list local branch_checked_out_at = git_mod.branch_checked_out_at ----Substitute ${project} in template string ----@param template string ----@param project_name string ----@return string -local function resolve_url_template(template, project_name) - local escaped = project_name:gsub("%%", "%%%%") - return (template:gsub("%${project}", escaped)) -end - ----Parse git URLs to extract project name ----@param url string ----@return string|nil -local function _extract_project_name(url) -- luacheck: ignore 211 - if not url or url == "" then - return nil - end - - url = url:gsub("[?#].*$", "") - url = url:gsub("/+$", "") - - if url == "" or url == "/" then - return nil - end - - url = url:gsub("%.git$", "") - - if not url:match("://") then - local scp_path = url:match("^[^@]+@[^:]+:(.+)$") - if scp_path and scp_path ~= "" then - url = scp_path - end - end - - local name = url:match("([^/]+)$") or url:match("([^:]+)$") - if not name or name == "" then - return nil - end - return name -end +local config_mod = require("wt.config") +local resolve_url_template = config_mod.resolve_url_template +local extract_project_name = config_mod.extract_project_name +local load_global_config = config_mod.load_global_config +local load_project_config = config_mod.load_project_config ---Print usage information local function print_usage() @@ -220,103 +186,6 @@ local function show_command_help(cmd) os.exit(EXIT_SUCCESS) end ----Parse git URLs to extract project name (exported version) ----@param url string ----@return string|nil -local function extract_project_name(url) - if not url or url == "" then - return nil - end - - url = url:gsub("[?#].*$", "") - url = url:gsub("/+$", "") - - if url == "" or url == "/" then - return nil - end - - url = url:gsub("%.git$", "") - - if not url:match("://") then - local scp_path = url:match("^[^@]+@[^:]+:(.+)$") - if scp_path and scp_path ~= "" then - url = scp_path - end - end - - local name = url:match("([^/]+)$") or url:match("([^:]+)$") - if not name or name == "" then - return nil - end - return name -end - ----Load global config from ~/.config/wt/config.lua ----@return {branch_path_style?: string, flat_separator?: string, remotes?: table, default_remotes?: string[]|string} -local function load_global_config() - local home = os.getenv("HOME") - if not home then - return {} - end - local config_path = home .. "/.config/wt/config.lua" - local f = io.open(config_path, "r") - if not f then - return {} - end - local content = f:read("*a") - f:close() - local chunk, err = load(content, config_path, "t", {}) - if not chunk then - chunk, err = load("return " .. content, config_path, "t", {}) - end - if not chunk then - io.stderr:write("warning: config syntax error in " .. config_path .. ": " .. err .. "\n") - return {} - end - local ok, result = pcall(chunk) - if not ok then - io.stderr:write("warning: config execution error in " .. config_path .. ": " .. result .. "\n") - return {} - end - if type(result) ~= "table" then - io.stderr:write("warning: config must return a table in " .. config_path .. "\n") - return {} - end - return result -end - ----Load project config from /.wt.lua ----@param root string ----@return {hooks?: {copy?: string[], symlink?: string[], run?: string[]}} -local function load_project_config(root) - local config_path = root .. "/.wt.lua" - local f = io.open(config_path, "r") - if not f then - return {} - end - local content = f:read("*a") - f:close() - - local chunk, err = load(content, config_path, "t", {}) - if not chunk then - chunk, err = load("return " .. content, config_path, "t", {}) - end - if not chunk then - io.stderr:write("warning: config syntax error in " .. config_path .. ": " .. err .. "\n") - return {} - end - local ok, result = pcall(chunk) - if not ok then - io.stderr:write("warning: config execution error in " .. config_path .. ": " .. result .. "\n") - return {} - end - if type(result) ~= "table" then - io.stderr:write("warning: config must return a table in " .. config_path .. "\n") - return {} - end - return result -end - ---Load hook permissions from ~/.local/share/wt/hook-dirs.lua ---@return table local function load_hook_permissions() diff --git a/src/wt/config.lua b/src/wt/config.lua new file mode 100644 index 0000000000000000000000000000000000000000..98509fdf2a54a0612a74a5e39a9fda49949d6a8d --- /dev/null +++ b/src/wt/config.lua @@ -0,0 +1,114 @@ +-- SPDX-FileCopyrightText: Amolith +-- +-- SPDX-License-Identifier: GPL-3.0-or-later + +---@class wt.config +local M = {} + +---Substitute ${project} in template string +---@param template string +---@param project_name string +---@return string +function M.resolve_url_template(template, project_name) + local escaped = project_name:gsub("%%", "%%%%") + return (template:gsub("%${project}", escaped)) +end + +---Parse git URLs to extract project name +---@param url string +---@return string|nil +function M.extract_project_name(url) + if not url or url == "" then + return nil + end + + url = url:gsub("[?#].*$", "") + url = url:gsub("/+$", "") + + if url == "" or url == "/" then + return nil + end + + url = url:gsub("%.git$", "") + + if not url:match("://") then + local scp_path = url:match("^[^@]+@[^:]+:(.+)$") + if scp_path and scp_path ~= "" then + url = scp_path + end + end + + local name = url:match("([^/]+)$") or url:match("([^:]+)$") + if not name or name == "" then + return nil + end + return name +end + +---Load global config from ~/.config/wt/config.lua +---@return table +function M.load_global_config() + local home = os.getenv("HOME") + if not home then + return {} + end + local config_path = home .. "/.config/wt/config.lua" + local f = io.open(config_path, "r") + if not f then + return {} + end + local content = f:read("*a") + f:close() + local chunk, err = load(content, config_path, "t", {}) + if not chunk then + chunk, err = load("return " .. content, config_path, "t", {}) + end + if not chunk then + io.stderr:write("warning: config syntax error in " .. config_path .. ": " .. err .. "\n") + return {} + end + local ok, result = pcall(chunk) + if not ok then + io.stderr:write("warning: config execution error in " .. config_path .. ": " .. result .. "\n") + return {} + end + if type(result) ~= "table" then + io.stderr:write("warning: config must return a table in " .. config_path .. "\n") + return {} + end + return result +end + +---Load project config from /.wt.lua +---@param root string +---@return {hooks?: {copy?: string[], symlink?: string[], run?: string[]}} +function M.load_project_config(root) + local config_path = root .. "/.wt.lua" + local f = io.open(config_path, "r") + if not f then + return {} + end + local content = f:read("*a") + f:close() + + local chunk, err = load(content, config_path, "t", {}) + if not chunk then + chunk, err = load("return " .. content, config_path, "t", {}) + end + if not chunk then + io.stderr:write("warning: config syntax error in " .. config_path .. ": " .. err .. "\n") + return {} + end + local ok, result = pcall(chunk) + if not ok then + io.stderr:write("warning: config execution error in " .. config_path .. ": " .. result .. "\n") + return {} + end + if type(result) ~= "table" then + io.stderr:write("warning: config must return a table in " .. config_path .. "\n") + return {} + end + return result +end + +return M