From b1c03afbe655f76d37f15fba112d0a42c4dcd6dd Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 18 Jan 2026 08:56:20 -0700 Subject: [PATCH] refactor(wt): extract hooks module Create src/wt/hooks.lua with load_hook_permissions, save_hook_permissions, summarize_hooks, check_hook_permission, and run_hooks functions. Update main.lua to import from the new module. Assisted-by: Claude Opus 4.5 via Amp --- dist/wt | 431 ++++++++++++++++++++--------------------------- src/main.lua | 254 ++-------------------------- src/wt/hooks.lua | 175 +++++++++++++++++++ 3 files changed, 372 insertions(+), 488 deletions(-) create mode 100644 src/wt/hooks.lua diff --git a/dist/wt b/dist/wt index 216f9597e961fda766bcf610f2fc6ea98da590ff..82b73336a969a8c4e398e576f5a0bf55a3b6e839 100755 --- a/dist/wt +++ b/dist/wt @@ -529,6 +529,183 @@ end return M ]] +_EMBEDDED_MODULES["wt.hooks"] = [[-- 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 + +---@class wt.hooks +local M = {} + +---Load hook permissions from ~/.local/share/wt/hook-dirs.lua +---@param home_override? string override HOME for testing +---@return table +function M.load_hook_permissions(home_override) + local home = home_override or os.getenv("HOME") + if not home then + return {} + end + local path = home .. "/.local/share/wt/hook-dirs.lua" + local f = io.open(path, "r") + if not f then + return {} + end + local content = f:read("*a") + f:close() + local chunk = load("return " .. content, path, "t", {}) + if not chunk then + return {} + end + local ok, result = pcall(chunk) + if ok and type(result) == "table" then + return result + end + return {} +end + +---Save hook permissions to ~/.local/share/wt/hook-dirs.lua +---@param perms table +---@param home_override? string override HOME for testing +function M.save_hook_permissions(perms, home_override) + local home = home_override or os.getenv("HOME") + if not home then + return + end + local dir = home .. "/.local/share/wt" + run_cmd_silent("mkdir -p " .. dir) + local path = dir .. "/hook-dirs.lua" + local f = io.open(path, "w") + if not f then + return + end + f:write("{\n") + for k, v in pairs(perms) do + f:write('\t["' .. k .. '"] = ' .. tostring(v) .. ",\n") + end + f:write("}\n") + f:close() +end + +---Summarize hooks for confirmation prompt +---@param hooks {copy?: string[], symlink?: string[], run?: string[]} +---@return string +function M.summarize_hooks(hooks) + local parts = {} + if hooks.copy and #hooks.copy > 0 then + local items = {} + for i = 1, math.min(3, #hooks.copy) do + table.insert(items, hooks.copy[i]) + end + local suffix = #hooks.copy > 3 and " (+" .. (#hooks.copy - 3) .. " more)" or "" + table.insert(parts, "copy: " .. table.concat(items, ", ") .. suffix) + end + if hooks.symlink and #hooks.symlink > 0 then + local items = {} + for i = 1, math.min(3, #hooks.symlink) do + table.insert(items, hooks.symlink[i]) + end + local suffix = #hooks.symlink > 3 and " (+" .. (#hooks.symlink - 3) .. " more)" or "" + table.insert(parts, "symlink: " .. table.concat(items, ", ") .. suffix) + end + if hooks.run and #hooks.run > 0 then + local items = {} + for i = 1, math.min(3, #hooks.run) do + table.insert(items, hooks.run[i]) + end + local suffix = #hooks.run > 3 and " (+" .. (#hooks.run - 3) .. " more)" or "" + table.insert(parts, "run: " .. table.concat(items, ", ") .. suffix) + end + return table.concat(parts, "; ") +end + +---Check if hooks are allowed for a project, prompting if unknown +---@param root string project root path +---@param hooks {copy?: string[], symlink?: string[], run?: string[]} +---@return boolean allowed +function M.check_hook_permission(root, hooks) + local perms = M.load_hook_permissions() + if perms[root] ~= nil then + return perms[root] + end + + -- Prompt user + local summary = M.summarize_hooks(hooks) + local prompt = "Allow hooks for " .. root .. "?\\n" .. summary + local allowed = run_cmd_silent("gum confirm " .. "'" .. prompt:gsub("'", "'\\''") .. "'") + + perms[root] = allowed + M.save_hook_permissions(perms) + return allowed +end + +---Run hooks from .wt.lua config +---@param source string source worktree path +---@param target string target worktree path +---@param hooks {copy?: string[], symlink?: string[], run?: string[]} +---@param root string project root path +---@param home_override? string override HOME for testing +function M.run_hooks(source, target, hooks, root, home_override) + -- Check permission before running any hooks + -- For testing: check permissions file directly if home_override given + if home_override then + local perms = M.load_hook_permissions(home_override) + if perms[root] == false then + io.stderr:write("hooks skipped (not allowed for this project)\n") + return + end + else + if not M.check_hook_permission(root, hooks) then + io.stderr:write("hooks skipped (not allowed for this project)\n") + return + end + end + + if hooks.copy then + for _, item in ipairs(hooks.copy) do + local src = source .. "/" .. item + local dst = target .. "/" .. item + -- Create parent directory if needed + local parent = dst:match("(.+)/[^/]+$") + if parent then + run_cmd_silent("mkdir -p " .. parent) + end + local _, code = run_cmd("cp -r " .. src .. " " .. dst) + if code ~= 0 then + io.stderr:write("warning: failed to copy " .. item .. "\n") + end + end + end + if hooks.symlink then + for _, item in ipairs(hooks.symlink) do + local src = source .. "/" .. item + local dst = target .. "/" .. item + -- Create parent directory if needed + local parent = dst:match("(.+)/[^/]+$") + if parent then + run_cmd_silent("mkdir -p " .. parent) + end + local _, code = run_cmd("ln -s " .. src .. " " .. dst) + if code ~= 0 then + io.stderr:write("warning: failed to symlink " .. item .. "\n") + end + end + end + if hooks.run then + for _, cmd in ipairs(hooks.run) do + local _, code = run_cmd("cd " .. target .. " && " .. cmd) + if code ~= 0 then + io.stderr:write("warning: hook command failed: " .. cmd .. "\n") + end + end + end +end + +return M +]] + if _VERSION < "Lua 5.2" then io.stderr:write("error: wt requires Lua 5.2 or later\n") @@ -570,6 +747,12 @@ 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 +local hooks_mod = require("wt.hooks") +local load_hook_permissions = hooks_mod.load_hook_permissions +local save_hook_permissions = hooks_mod.save_hook_permissions +local summarize_hooks = hooks_mod.summarize_hooks +local run_hooks = hooks_mod.run_hooks + ---Print usage information local function print_usage() print("wt - git worktree manager") @@ -712,157 +895,6 @@ local function show_command_help(cmd) os.exit(EXIT_SUCCESS) end ----Load hook permissions from ~/.local/share/wt/hook-dirs.lua ----@return table -local function load_hook_permissions() - local home = os.getenv("HOME") - if not home then - return {} - end - local path = home .. "/.local/share/wt/hook-dirs.lua" - local f = io.open(path, "r") - if not f then - return {} - end - local content = f:read("*a") - f:close() - local chunk = load("return " .. content, path, "t", {}) - if not chunk then - return {} - end - local ok, result = pcall(chunk) - if ok and type(result) == "table" then - return result - end - return {} -end - ----Save hook permissions to ~/.local/share/wt/hook-dirs.lua ----@param perms table -local function save_hook_permissions(perms) - local home = os.getenv("HOME") - if not home then - return - end - local dir = home .. "/.local/share/wt" - run_cmd_silent("mkdir -p " .. dir) - local path = dir .. "/hook-dirs.lua" - local f = io.open(path, "w") - if not f then - return - end - f:write("{\n") - for k, v in pairs(perms) do - f:write('\t["' .. k .. '"] = ' .. tostring(v) .. ",\n") - end - f:write("}\n") - f:close() -end - ----Summarize hooks for confirmation prompt ----@param hooks {copy?: string[], symlink?: string[], run?: string[]} ----@return string -local function summarize_hooks(hooks) - local parts = {} - if hooks.copy and #hooks.copy > 0 then - local items = {} - for i = 1, math.min(3, #hooks.copy) do - table.insert(items, hooks.copy[i]) - end - local suffix = #hooks.copy > 3 and " (+" .. (#hooks.copy - 3) .. " more)" or "" - table.insert(parts, "copy: " .. table.concat(items, ", ") .. suffix) - end - if hooks.symlink and #hooks.symlink > 0 then - local items = {} - for i = 1, math.min(3, #hooks.symlink) do - table.insert(items, hooks.symlink[i]) - end - local suffix = #hooks.symlink > 3 and " (+" .. (#hooks.symlink - 3) .. " more)" or "" - table.insert(parts, "symlink: " .. table.concat(items, ", ") .. suffix) - end - if hooks.run and #hooks.run > 0 then - local items = {} - for i = 1, math.min(3, #hooks.run) do - table.insert(items, hooks.run[i]) - end - local suffix = #hooks.run > 3 and " (+" .. (#hooks.run - 3) .. " more)" or "" - table.insert(parts, "run: " .. table.concat(items, ", ") .. suffix) - end - return table.concat(parts, "; ") -end - ----Check if hooks are allowed for a project, prompting if unknown ----@param root string project root path ----@param hooks {copy?: string[], symlink?: string[], run?: string[]} ----@return boolean allowed -local function check_hook_permission(root, hooks) - local perms = load_hook_permissions() - if perms[root] ~= nil then - return perms[root] - end - - -- Prompt user - local summary = summarize_hooks(hooks) - local prompt = "Allow hooks for " .. root .. "?\\n" .. summary - local allowed = run_cmd_silent("gum confirm " .. "'" .. prompt:gsub("'", "'\\''") .. "'") - - perms[root] = allowed - save_hook_permissions(perms) - return allowed -end - ----Run hooks from .wt.lua config ----@param source string source worktree path ----@param target string target worktree path ----@param hooks {copy?: string[], symlink?: string[], run?: string[]} ----@param root string project root path -local function run_hooks(source, target, hooks, root) - -- Check permission before running any hooks - if not check_hook_permission(root, hooks) then - io.stderr:write("hooks skipped (not allowed for this project)\n") - return - end - - if hooks.copy then - for _, item in ipairs(hooks.copy) do - local src = source .. "/" .. item - local dst = target .. "/" .. item - -- Create parent directory if needed - local parent = dst:match("(.+)/[^/]+$") - if parent then - run_cmd_silent("mkdir -p " .. parent) - end - local _, code = run_cmd("cp -r " .. src .. " " .. dst) - if code ~= 0 then - io.stderr:write("warning: failed to copy " .. item .. "\n") - end - end - end - if hooks.symlink then - for _, item in ipairs(hooks.symlink) do - local src = source .. "/" .. item - local dst = target .. "/" .. item - -- Create parent directory if needed - local parent = dst:match("(.+)/[^/]+$") - if parent then - run_cmd_silent("mkdir -p " .. parent) - end - local _, code = run_cmd("ln -s " .. src .. " " .. dst) - if code ~= 0 then - io.stderr:write("warning: failed to symlink " .. item .. "\n") - end - end - end - if hooks.run then - for _, cmd in ipairs(hooks.run) do - local _, code = run_cmd("cd " .. target .. " && " .. cmd) - if code ~= 0 then - io.stderr:write("warning: hook command failed: " .. cmd .. "\n") - end - end - end -end - ---@param args string[] local function cmd_clone(args) -- Parse arguments: [--remote name]... [--own] @@ -1909,100 +1941,11 @@ if pcall(debug.getlocal, 4, 1) then parse_branch_remotes = parse_branch_remotes, parse_worktree_list = parse_worktree_list, escape_pattern = escape_pattern, - -- Hook helpers + -- Hook helpers (re-exported from wt.hooks) summarize_hooks = summarize_hooks, - load_hook_permissions = function(home_override) - local home = home_override or os.getenv("HOME") - if not home then - return {} - end - local path = home .. "/.local/share/wt/hook-dirs.lua" - local f = io.open(path, "r") - if not f then - return {} - end - local content = f:read("*a") - f:close() - local chunk = load("return " .. content, path, "t", {}) - if not chunk then - return {} - end - local ok, result = pcall(chunk) - if ok and type(result) == "table" then - return result - end - return {} - end, - save_hook_permissions = function(perms, home_override) - local home = home_override or os.getenv("HOME") - if not home then - return - end - local dir = home .. "/.local/share/wt" - run_cmd_silent("mkdir -p " .. dir) - local path = dir .. "/hook-dirs.lua" - local f = io.open(path, "w") - if not f then - return - end - f:write("{\n") - for k, v in pairs(perms) do - f:write('\t["' .. k .. '"] = ' .. tostring(v) .. ",\n") - end - f:write("}\n") - f:close() - end, - run_hooks = function(source, target, hooks, root, home_override) - local home = home_override or os.getenv("HOME") - if not home then - return - end - local perm_path = home .. "/.local/share/wt/hook-dirs.lua" - local perms = {} - local pf = io.open(perm_path, "r") - if pf then - local content = pf:read("*a") - pf:close() - local chunk = load("return " .. content, perm_path, "t", {}) - if chunk then - local ok, result = pcall(chunk) - if ok and type(result) == "table" then - perms = result - end - end - end - if perms[root] == false then - io.stderr:write("hooks skipped (not allowed for this project)\n") - return - end - if hooks.copy then - for _, item in ipairs(hooks.copy) do - local src = source .. "/" .. item - local dst = target .. "/" .. item - local parent = dst:match("(.+)/[^/]+$") - if parent then - run_cmd_silent("mkdir -p " .. parent) - end - run_cmd("cp -r " .. src .. " " .. dst) - end - end - if hooks.symlink then - for _, item in ipairs(hooks.symlink) do - local src = source .. "/" .. item - local dst = target .. "/" .. item - local parent = dst:match("(.+)/[^/]+$") - if parent then - run_cmd_silent("mkdir -p " .. parent) - end - run_cmd("ln -s " .. src .. " " .. dst) - end - end - if hooks.run then - for _, cmd in ipairs(hooks.run) do - run_cmd("cd " .. target .. " && " .. cmd) - end - end - end, + load_hook_permissions = load_hook_permissions, + save_hook_permissions = save_hook_permissions, + run_hooks = run_hooks, -- Project root detection (re-exported from wt.git) find_project_root = find_project_root, detect_source_worktree = detect_source_worktree, diff --git a/src/main.lua b/src/main.lua index 34f9fea55cf54b45dce033497025798251b29bce..e842874fa5c307e5d644ea150fd24ef7f2f904ed 100644 --- a/src/main.lua +++ b/src/main.lua @@ -44,6 +44,12 @@ 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 +local hooks_mod = require("wt.hooks") +local load_hook_permissions = hooks_mod.load_hook_permissions +local save_hook_permissions = hooks_mod.save_hook_permissions +local summarize_hooks = hooks_mod.summarize_hooks +local run_hooks = hooks_mod.run_hooks + ---Print usage information local function print_usage() print("wt - git worktree manager") @@ -186,157 +192,6 @@ local function show_command_help(cmd) os.exit(EXIT_SUCCESS) end ----Load hook permissions from ~/.local/share/wt/hook-dirs.lua ----@return table -local function load_hook_permissions() - local home = os.getenv("HOME") - if not home then - return {} - end - local path = home .. "/.local/share/wt/hook-dirs.lua" - local f = io.open(path, "r") - if not f then - return {} - end - local content = f:read("*a") - f:close() - local chunk = load("return " .. content, path, "t", {}) - if not chunk then - return {} - end - local ok, result = pcall(chunk) - if ok and type(result) == "table" then - return result - end - return {} -end - ----Save hook permissions to ~/.local/share/wt/hook-dirs.lua ----@param perms table -local function save_hook_permissions(perms) - local home = os.getenv("HOME") - if not home then - return - end - local dir = home .. "/.local/share/wt" - run_cmd_silent("mkdir -p " .. dir) - local path = dir .. "/hook-dirs.lua" - local f = io.open(path, "w") - if not f then - return - end - f:write("{\n") - for k, v in pairs(perms) do - f:write('\t["' .. k .. '"] = ' .. tostring(v) .. ",\n") - end - f:write("}\n") - f:close() -end - ----Summarize hooks for confirmation prompt ----@param hooks {copy?: string[], symlink?: string[], run?: string[]} ----@return string -local function summarize_hooks(hooks) - local parts = {} - if hooks.copy and #hooks.copy > 0 then - local items = {} - for i = 1, math.min(3, #hooks.copy) do - table.insert(items, hooks.copy[i]) - end - local suffix = #hooks.copy > 3 and " (+" .. (#hooks.copy - 3) .. " more)" or "" - table.insert(parts, "copy: " .. table.concat(items, ", ") .. suffix) - end - if hooks.symlink and #hooks.symlink > 0 then - local items = {} - for i = 1, math.min(3, #hooks.symlink) do - table.insert(items, hooks.symlink[i]) - end - local suffix = #hooks.symlink > 3 and " (+" .. (#hooks.symlink - 3) .. " more)" or "" - table.insert(parts, "symlink: " .. table.concat(items, ", ") .. suffix) - end - if hooks.run and #hooks.run > 0 then - local items = {} - for i = 1, math.min(3, #hooks.run) do - table.insert(items, hooks.run[i]) - end - local suffix = #hooks.run > 3 and " (+" .. (#hooks.run - 3) .. " more)" or "" - table.insert(parts, "run: " .. table.concat(items, ", ") .. suffix) - end - return table.concat(parts, "; ") -end - ----Check if hooks are allowed for a project, prompting if unknown ----@param root string project root path ----@param hooks {copy?: string[], symlink?: string[], run?: string[]} ----@return boolean allowed -local function check_hook_permission(root, hooks) - local perms = load_hook_permissions() - if perms[root] ~= nil then - return perms[root] - end - - -- Prompt user - local summary = summarize_hooks(hooks) - local prompt = "Allow hooks for " .. root .. "?\\n" .. summary - local allowed = run_cmd_silent("gum confirm " .. "'" .. prompt:gsub("'", "'\\''") .. "'") - - perms[root] = allowed - save_hook_permissions(perms) - return allowed -end - ----Run hooks from .wt.lua config ----@param source string source worktree path ----@param target string target worktree path ----@param hooks {copy?: string[], symlink?: string[], run?: string[]} ----@param root string project root path -local function run_hooks(source, target, hooks, root) - -- Check permission before running any hooks - if not check_hook_permission(root, hooks) then - io.stderr:write("hooks skipped (not allowed for this project)\n") - return - end - - if hooks.copy then - for _, item in ipairs(hooks.copy) do - local src = source .. "/" .. item - local dst = target .. "/" .. item - -- Create parent directory if needed - local parent = dst:match("(.+)/[^/]+$") - if parent then - run_cmd_silent("mkdir -p " .. parent) - end - local _, code = run_cmd("cp -r " .. src .. " " .. dst) - if code ~= 0 then - io.stderr:write("warning: failed to copy " .. item .. "\n") - end - end - end - if hooks.symlink then - for _, item in ipairs(hooks.symlink) do - local src = source .. "/" .. item - local dst = target .. "/" .. item - -- Create parent directory if needed - local parent = dst:match("(.+)/[^/]+$") - if parent then - run_cmd_silent("mkdir -p " .. parent) - end - local _, code = run_cmd("ln -s " .. src .. " " .. dst) - if code ~= 0 then - io.stderr:write("warning: failed to symlink " .. item .. "\n") - end - end - end - if hooks.run then - for _, cmd in ipairs(hooks.run) do - local _, code = run_cmd("cd " .. target .. " && " .. cmd) - if code ~= 0 then - io.stderr:write("warning: hook command failed: " .. cmd .. "\n") - end - end - end -end - ---@param args string[] local function cmd_clone(args) -- Parse arguments: [--remote name]... [--own] @@ -1383,100 +1238,11 @@ if pcall(debug.getlocal, 4, 1) then parse_branch_remotes = parse_branch_remotes, parse_worktree_list = parse_worktree_list, escape_pattern = escape_pattern, - -- Hook helpers + -- Hook helpers (re-exported from wt.hooks) summarize_hooks = summarize_hooks, - load_hook_permissions = function(home_override) - local home = home_override or os.getenv("HOME") - if not home then - return {} - end - local path = home .. "/.local/share/wt/hook-dirs.lua" - local f = io.open(path, "r") - if not f then - return {} - end - local content = f:read("*a") - f:close() - local chunk = load("return " .. content, path, "t", {}) - if not chunk then - return {} - end - local ok, result = pcall(chunk) - if ok and type(result) == "table" then - return result - end - return {} - end, - save_hook_permissions = function(perms, home_override) - local home = home_override or os.getenv("HOME") - if not home then - return - end - local dir = home .. "/.local/share/wt" - run_cmd_silent("mkdir -p " .. dir) - local path = dir .. "/hook-dirs.lua" - local f = io.open(path, "w") - if not f then - return - end - f:write("{\n") - for k, v in pairs(perms) do - f:write('\t["' .. k .. '"] = ' .. tostring(v) .. ",\n") - end - f:write("}\n") - f:close() - end, - run_hooks = function(source, target, hooks, root, home_override) - local home = home_override or os.getenv("HOME") - if not home then - return - end - local perm_path = home .. "/.local/share/wt/hook-dirs.lua" - local perms = {} - local pf = io.open(perm_path, "r") - if pf then - local content = pf:read("*a") - pf:close() - local chunk = load("return " .. content, perm_path, "t", {}) - if chunk then - local ok, result = pcall(chunk) - if ok and type(result) == "table" then - perms = result - end - end - end - if perms[root] == false then - io.stderr:write("hooks skipped (not allowed for this project)\n") - return - end - if hooks.copy then - for _, item in ipairs(hooks.copy) do - local src = source .. "/" .. item - local dst = target .. "/" .. item - local parent = dst:match("(.+)/[^/]+$") - if parent then - run_cmd_silent("mkdir -p " .. parent) - end - run_cmd("cp -r " .. src .. " " .. dst) - end - end - if hooks.symlink then - for _, item in ipairs(hooks.symlink) do - local src = source .. "/" .. item - local dst = target .. "/" .. item - local parent = dst:match("(.+)/[^/]+$") - if parent then - run_cmd_silent("mkdir -p " .. parent) - end - run_cmd("ln -s " .. src .. " " .. dst) - end - end - if hooks.run then - for _, cmd in ipairs(hooks.run) do - run_cmd("cd " .. target .. " && " .. cmd) - end - end - end, + load_hook_permissions = load_hook_permissions, + save_hook_permissions = save_hook_permissions, + run_hooks = run_hooks, -- Project root detection (re-exported from wt.git) find_project_root = find_project_root, detect_source_worktree = detect_source_worktree, diff --git a/src/wt/hooks.lua b/src/wt/hooks.lua new file mode 100644 index 0000000000000000000000000000000000000000..5ed86d57bbd66c59a7806c9fdb5f8b3336a20fc8 --- /dev/null +++ b/src/wt/hooks.lua @@ -0,0 +1,175 @@ +-- 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 + +---@class wt.hooks +local M = {} + +---Load hook permissions from ~/.local/share/wt/hook-dirs.lua +---@param home_override? string override HOME for testing +---@return table +function M.load_hook_permissions(home_override) + local home = home_override or os.getenv("HOME") + if not home then + return {} + end + local path = home .. "/.local/share/wt/hook-dirs.lua" + local f = io.open(path, "r") + if not f then + return {} + end + local content = f:read("*a") + f:close() + local chunk = load("return " .. content, path, "t", {}) + if not chunk then + return {} + end + local ok, result = pcall(chunk) + if ok and type(result) == "table" then + return result + end + return {} +end + +---Save hook permissions to ~/.local/share/wt/hook-dirs.lua +---@param perms table +---@param home_override? string override HOME for testing +function M.save_hook_permissions(perms, home_override) + local home = home_override or os.getenv("HOME") + if not home then + return + end + local dir = home .. "/.local/share/wt" + run_cmd_silent("mkdir -p " .. dir) + local path = dir .. "/hook-dirs.lua" + local f = io.open(path, "w") + if not f then + return + end + f:write("{\n") + for k, v in pairs(perms) do + f:write('\t["' .. k .. '"] = ' .. tostring(v) .. ",\n") + end + f:write("}\n") + f:close() +end + +---Summarize hooks for confirmation prompt +---@param hooks {copy?: string[], symlink?: string[], run?: string[]} +---@return string +function M.summarize_hooks(hooks) + local parts = {} + if hooks.copy and #hooks.copy > 0 then + local items = {} + for i = 1, math.min(3, #hooks.copy) do + table.insert(items, hooks.copy[i]) + end + local suffix = #hooks.copy > 3 and " (+" .. (#hooks.copy - 3) .. " more)" or "" + table.insert(parts, "copy: " .. table.concat(items, ", ") .. suffix) + end + if hooks.symlink and #hooks.symlink > 0 then + local items = {} + for i = 1, math.min(3, #hooks.symlink) do + table.insert(items, hooks.symlink[i]) + end + local suffix = #hooks.symlink > 3 and " (+" .. (#hooks.symlink - 3) .. " more)" or "" + table.insert(parts, "symlink: " .. table.concat(items, ", ") .. suffix) + end + if hooks.run and #hooks.run > 0 then + local items = {} + for i = 1, math.min(3, #hooks.run) do + table.insert(items, hooks.run[i]) + end + local suffix = #hooks.run > 3 and " (+" .. (#hooks.run - 3) .. " more)" or "" + table.insert(parts, "run: " .. table.concat(items, ", ") .. suffix) + end + return table.concat(parts, "; ") +end + +---Check if hooks are allowed for a project, prompting if unknown +---@param root string project root path +---@param hooks {copy?: string[], symlink?: string[], run?: string[]} +---@return boolean allowed +function M.check_hook_permission(root, hooks) + local perms = M.load_hook_permissions() + if perms[root] ~= nil then + return perms[root] + end + + -- Prompt user + local summary = M.summarize_hooks(hooks) + local prompt = "Allow hooks for " .. root .. "?\\n" .. summary + local allowed = run_cmd_silent("gum confirm " .. "'" .. prompt:gsub("'", "'\\''") .. "'") + + perms[root] = allowed + M.save_hook_permissions(perms) + return allowed +end + +---Run hooks from .wt.lua config +---@param source string source worktree path +---@param target string target worktree path +---@param hooks {copy?: string[], symlink?: string[], run?: string[]} +---@param root string project root path +---@param home_override? string override HOME for testing +function M.run_hooks(source, target, hooks, root, home_override) + -- Check permission before running any hooks + -- For testing: check permissions file directly if home_override given + if home_override then + local perms = M.load_hook_permissions(home_override) + if perms[root] == false then + io.stderr:write("hooks skipped (not allowed for this project)\n") + return + end + else + if not M.check_hook_permission(root, hooks) then + io.stderr:write("hooks skipped (not allowed for this project)\n") + return + end + end + + if hooks.copy then + for _, item in ipairs(hooks.copy) do + local src = source .. "/" .. item + local dst = target .. "/" .. item + -- Create parent directory if needed + local parent = dst:match("(.+)/[^/]+$") + if parent then + run_cmd_silent("mkdir -p " .. parent) + end + local _, code = run_cmd("cp -r " .. src .. " " .. dst) + if code ~= 0 then + io.stderr:write("warning: failed to copy " .. item .. "\n") + end + end + end + if hooks.symlink then + for _, item in ipairs(hooks.symlink) do + local src = source .. "/" .. item + local dst = target .. "/" .. item + -- Create parent directory if needed + local parent = dst:match("(.+)/[^/]+$") + if parent then + run_cmd_silent("mkdir -p " .. parent) + end + local _, code = run_cmd("ln -s " .. src .. " " .. dst) + if code ~= 0 then + io.stderr:write("warning: failed to symlink " .. item .. "\n") + end + end + end + if hooks.run then + for _, cmd in ipairs(hooks.run) do + local _, code = run_cmd("cd " .. target .. " && " .. cmd) + if code ~= 0 then + io.stderr:write("warning: hook command failed: " .. cmd .. "\n") + end + end + end +end + +return M