diff --git a/dist/wt b/dist/wt index 62ee513599ba33c6c92a9ba1138cc8643e8637e5..9ed0b6721ac6edda16c3b320663b3fa79aedac0f 100755 --- a/dist/wt +++ b/dist/wt @@ -861,6 +861,269 @@ end return M ]] +_EMBEDDED_MODULES["wt.cmd.add"] = [[-- SPDX-FileCopyrightText: Amolith +-- +-- SPDX-License-Identifier: GPL-3.0-or-later + +local exit = require("wt.exit") +local shell = require("wt.shell") +local git = require("wt.git") +local path_mod = require("wt.path") +local config = require("wt.config") +local hooks = require("wt.hooks") + +---@class wt.cmd.add +local M = {} + +---Add a worktree for an existing or new branch +---@param args string[] +function M.cmd_add(args) + -- Parse arguments: [-b []] + ---@type string|nil + local branch = nil + local create_branch = false + ---@type string|nil + local start_point = nil + + local i = 1 + while i <= #args do + local a = args[i] + if a == "-b" then + create_branch = true + -- Check if next arg is start-point (not another flag) + if args[i + 1] and not args[i + 1]:match("^%-") then + start_point = args[i + 1] + i = i + 1 + end + elseif not branch then + branch = a + else + shell.die("unexpected argument: " .. a) + end + i = i + 1 + end + + if not branch then + shell.die("usage: wt a [-b []]") + return + end + + local root, err = git.find_project_root() + if not root then + shell.die(err --[[@as string]]) + return + end + + local git_dir = root .. "/.bare" + local source_worktree = git.detect_source_worktree(root) + + -- Load config for path style + local global_config = config.load_global_config() + local style = global_config.branch_path_style or "nested" + local separator = global_config.flat_separator or "_" + + local target_path = path_mod.branch_to_path(root, branch, style, separator) + + -- Check if target already exists + local check = io.open(target_path .. "/.git", "r") + if check then + check:close() + shell.die("worktree already exists at " .. target_path) + end + + local output, code + if create_branch then + -- Create new branch with worktree + if start_point then + output, code = shell.run_cmd( + "GIT_DIR=" + .. git_dir + .. " git worktree add -b " + .. branch + .. " -- " + .. target_path + .. " " + .. start_point + ) + else + output, code = + shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -b " .. branch .. " -- " .. target_path) + end + else + -- Check if branch exists locally or on remotes + local exists_local = git.branch_exists_local(git_dir, branch) + local remotes = git.find_branch_remotes(git_dir, branch) + + if not exists_local and #remotes == 0 then + shell.die("branch '" .. branch .. "' not found locally or on any remote (use -b to create)") + end + + if #remotes > 1 then + shell.die("branch '" .. branch .. "' exists on multiple remotes: " .. table.concat(remotes, ", ")) + end + + output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. target_path .. " " .. branch) + end + + if code ~= 0 then + shell.die("failed to add worktree: " .. output, exit.EXIT_SYSTEM_ERROR) + end + + -- Run hooks if we have a source worktree + local project_config = config.load_project_config(root) + if source_worktree then + if project_config.hooks then + hooks.run_hooks(source_worktree, target_path, project_config.hooks, root) + end + elseif project_config.hooks then + io.stderr:write("warning: hooks skipped (run from inside a worktree to apply hooks)\n") + end + + print(target_path) +end + +return M +]] + +_EMBEDDED_MODULES["wt.cmd.remove"] = [[-- SPDX-FileCopyrightText: Amolith +-- +-- SPDX-License-Identifier: GPL-3.0-or-later + +local exit = require("wt.exit") +local shell = require("wt.shell") +local git = require("wt.git") +local path_mod = require("wt.path") + +---@class wt.cmd.remove +local M = {} + +---Check if cwd is inside (or equal to) a given path +---@param target string +---@return boolean +local function cwd_inside_path(target) + local cwd = shell.get_cwd() + if not cwd then + return false + end + return path_mod.path_inside(cwd, target) +end + +---Get the bare repo's HEAD branch +---@param git_dir string +---@return string|nil branch name, nil on error +local function get_bare_head(git_dir) + local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git symbolic-ref --short HEAD") + if code ~= 0 then + return nil + end + return (output:gsub("%s+$", "")) +end + +---Remove a worktree and optionally its branch +---@param args string[] +function M.cmd_remove(args) + local branch = nil + local delete_branch = false + local force = false + + for _, a in ipairs(args) do + if a == "-b" then + delete_branch = true + elseif a == "-f" then + force = true + elseif not branch then + branch = a + else + shell.die("unexpected argument: " .. a) + end + end + + if not branch then + shell.die("usage: wt r [-b] [-f]") + return + end + + local root, err = git.find_project_root() + if not root then + shell.die(err --[[@as string]]) + return + end + + local git_dir = root .. "/.bare" + + local wt_output, wt_code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain") + if wt_code ~= 0 then + shell.die("failed to list worktrees", exit.EXIT_SYSTEM_ERROR) + return + end + + local worktrees = git.parse_worktree_list(wt_output) + local target_path = nil + for _, wt in ipairs(worktrees) do + if wt.branch == branch then + target_path = wt.path + break + end + end + + if not target_path then + shell.die("no worktree found for branch '" .. branch .. "'") + return + end + + if cwd_inside_path(target_path) then + shell.die("cannot remove worktree while inside it") + end + + if not force then + local status_out = shell.run_cmd("git -C " .. target_path .. " status --porcelain") + if status_out ~= "" then + shell.die("worktree has uncommitted changes (use -f to force)") + end + end + + local remove_cmd = "GIT_DIR=" .. git_dir .. " git worktree remove" + if force then + remove_cmd = remove_cmd .. " --force" + end + remove_cmd = remove_cmd .. " -- " .. target_path + + local output, code = shell.run_cmd(remove_cmd) + if code ~= 0 then + shell.die("failed to remove worktree: " .. output, exit.EXIT_SYSTEM_ERROR) + end + + if delete_branch then + local bare_head = get_bare_head(git_dir) + if bare_head and bare_head == branch then + io.stderr:write("warning: cannot delete branch '" .. branch .. "' (it's the bare repo's HEAD)\n") + print("Worktree removed; branch retained") + return + end + + local checked_out = git.branch_checked_out_at(git_dir, branch) + if checked_out then + shell.die("cannot delete branch '" .. branch .. "': checked out at " .. checked_out) + end + + local delete_flag = force and "-D" or "-d" + local del_output, del_code = + shell.run_cmd("GIT_DIR=" .. git_dir .. " git branch " .. delete_flag .. " " .. branch) + if del_code ~= 0 then + io.stderr:write("warning: failed to delete branch: " .. del_output .. "\n") + print("Worktree removed; branch retained") + return + end + + print("Worktree and branch '" .. branch .. "' removed") + else + print("Worktree removed") + end +end + +return M +]] + _EMBEDDED_MODULES["wt.cmd.list"] = [[-- SPDX-FileCopyrightText: Amolith -- -- SPDX-License-Identifier: GPL-3.0-or-later @@ -1002,13 +1265,10 @@ 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 @@ -1032,6 +1292,12 @@ local cmd_fetch = fetch_mod.cmd_fetch local list_mod = require("wt.cmd.list") local cmd_list = list_mod.cmd_list +local remove_mod = require("wt.cmd.remove") +local cmd_remove = remove_mod.cmd_remove + +local add_mod = require("wt.cmd.add") +local cmd_add = add_mod.cmd_add + ---@param args string[] local function cmd_clone(args) -- Parse arguments: [--remote name]... [--own] @@ -1449,242 +1715,6 @@ local function cmd_new(args) end end ----@param args string[] -local function cmd_add(args) - -- Parse arguments: [-b []] - ---@type string|nil - local branch = nil - local create_branch = false - ---@type string|nil - local start_point = nil - - local i = 1 - while i <= #args do - local a = args[i] - if a == "-b" then - create_branch = true - -- Check if next arg is start-point (not another flag) - if args[i + 1] and not args[i + 1]:match("^%-") then - start_point = args[i + 1] - i = i + 1 - end - elseif not branch then - branch = a - else - die("unexpected argument: " .. a) - end - i = i + 1 - end - - if not branch then - die("usage: wt a [-b []]") - return - end - - local root, err = find_project_root() - if not root then - die(err --[[@as string]]) - return - end - - local git_dir = root .. "/.bare" - local source_worktree = detect_source_worktree(root) - - -- Load config for path style - local global_config = load_global_config() - local style = global_config.branch_path_style or "nested" - local separator = global_config.flat_separator or "_" - - local target_path = branch_to_path(root, branch, style, separator) - - -- Check if target already exists - local check = io.open(target_path .. "/.git", "r") - if check then - check:close() - die("worktree already exists at " .. target_path) - end - - local output, code - if create_branch then - -- Create new branch with worktree - if start_point then - output, code = run_cmd( - "GIT_DIR=" - .. git_dir - .. " git worktree add -b " - .. branch - .. " -- " - .. target_path - .. " " - .. start_point - ) - else - output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -b " .. branch .. " -- " .. target_path) - end - else - -- Check if branch exists locally or on remotes - local exists_local = branch_exists_local(git_dir, branch) - local remotes = find_branch_remotes(git_dir, branch) - - if not exists_local and #remotes == 0 then - die("branch '" .. branch .. "' not found locally or on any remote (use -b to create)") - end - - if #remotes > 1 then - die("branch '" .. branch .. "' exists on multiple remotes: " .. table.concat(remotes, ", ")) - end - - output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. target_path .. " " .. branch) - end - - if code ~= 0 then - die("failed to add worktree: " .. output, EXIT_SYSTEM_ERROR) - end - - -- Run hooks if we have a source worktree - local project_config = load_project_config(root) - if source_worktree then - if project_config.hooks then - run_hooks(source_worktree, target_path, project_config.hooks, root) - end - elseif project_config.hooks then - io.stderr:write("warning: hooks skipped (run from inside a worktree to apply hooks)\n") - end - - print(target_path) -end - ----Check if cwd is inside (or equal to) a given path ----@param target string ----@return boolean -local function cwd_inside_path(target) - local cwd = get_cwd() - if not cwd then - return false - end - return path_inside(cwd, target) -end - ----Get the bare repo's HEAD branch ----@param git_dir string ----@return string|nil branch name, nil on error -local function get_bare_head(git_dir) - local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git symbolic-ref --short HEAD") - if code ~= 0 then - return nil - end - return (output:gsub("%s+$", "")) -end - ----@param args string[] -local function cmd_remove(args) - -- Parse arguments: [-b] [-f] - local branch = nil - local delete_branch = false - local force = false - - for _, a in ipairs(args) do - if a == "-b" then - delete_branch = true - elseif a == "-f" then - force = true - elseif not branch then - branch = a - else - die("unexpected argument: " .. a) - end - end - - if not branch then - die("usage: wt r [-b] [-f]") - return - end - - local root, err = find_project_root() - if not root then - die(err --[[@as string]]) - return - end - - local git_dir = root .. "/.bare" - - -- Find worktree by querying git for actual location (not computed from config) - local wt_output, wt_code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain") - if wt_code ~= 0 then - die("failed to list worktrees", EXIT_SYSTEM_ERROR) - return - end - - local worktrees = parse_worktree_list(wt_output) - local target_path = nil - for _, wt in ipairs(worktrees) do - if wt.branch == branch then - target_path = wt.path - break - end - end - - if not target_path then - die("no worktree found for branch '" .. branch .. "'") - return - end - - -- Error if cwd is inside the worktree - if cwd_inside_path(target_path) then - die("cannot remove worktree while inside it") - end - - -- Check for uncommitted changes - if not force then - local status_out = run_cmd("git -C " .. target_path .. " status --porcelain") - if status_out ~= "" then - die("worktree has uncommitted changes (use -f to force)") - end - end - - -- Remove worktree - local remove_cmd = "GIT_DIR=" .. git_dir .. " git worktree remove" - if force then - remove_cmd = remove_cmd .. " --force" - end - remove_cmd = remove_cmd .. " -- " .. target_path - - local output, code = run_cmd(remove_cmd) - if code ~= 0 then - die("failed to remove worktree: " .. output, EXIT_SYSTEM_ERROR) - end - - -- Delete branch if requested - if delete_branch then - -- Check if branch is bare repo's HEAD - local bare_head = get_bare_head(git_dir) - if bare_head and bare_head == branch then - io.stderr:write("warning: cannot delete branch '" .. branch .. "' (it's the bare repo's HEAD)\n") - print("Worktree removed; branch retained") - return - end - - -- Check if branch is checked out elsewhere - local checked_out = branch_checked_out_at(git_dir, branch) - if checked_out then - die("cannot delete branch '" .. branch .. "': checked out at " .. checked_out) - end - - -- Delete branch - local delete_flag = force and "-D" or "-d" - local del_output, del_code = run_cmd("GIT_DIR=" .. git_dir .. " git branch " .. delete_flag .. " " .. branch) - if del_code ~= 0 then - io.stderr:write("warning: failed to delete branch: " .. del_output .. "\n") - print("Worktree removed; branch retained") - return - end - - print("Worktree and branch '" .. branch .. "' removed") - else - print("Worktree removed") - end -end - ---List directory entries (excluding . and ..) ---@param path string ---@return string[] diff --git a/src/main.lua b/src/main.lua index 295ba1bf9de20c82483fd54ef057f52a24ea217d..bb867ed4bde0b7b39823b4b4df07f91d224cd68f 100644 --- a/src/main.lua +++ b/src/main.lua @@ -30,8 +30,6 @@ 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 @@ -62,6 +60,9 @@ local cmd_list = list_mod.cmd_list local remove_mod = require("wt.cmd.remove") local cmd_remove = remove_mod.cmd_remove +local add_mod = require("wt.cmd.add") +local cmd_add = add_mod.cmd_add + ---@param args string[] local function cmd_clone(args) -- Parse arguments: [--remote name]... [--own] @@ -479,111 +480,6 @@ local function cmd_new(args) end end ----@param args string[] -local function cmd_add(args) - -- Parse arguments: [-b []] - ---@type string|nil - local branch = nil - local create_branch = false - ---@type string|nil - local start_point = nil - - local i = 1 - while i <= #args do - local a = args[i] - if a == "-b" then - create_branch = true - -- Check if next arg is start-point (not another flag) - if args[i + 1] and not args[i + 1]:match("^%-") then - start_point = args[i + 1] - i = i + 1 - end - elseif not branch then - branch = a - else - die("unexpected argument: " .. a) - end - i = i + 1 - end - - if not branch then - die("usage: wt a [-b []]") - return - end - - local root, err = find_project_root() - if not root then - die(err --[[@as string]]) - return - end - - local git_dir = root .. "/.bare" - local source_worktree = detect_source_worktree(root) - - -- Load config for path style - local global_config = load_global_config() - local style = global_config.branch_path_style or "nested" - local separator = global_config.flat_separator or "_" - - local target_path = branch_to_path(root, branch, style, separator) - - -- Check if target already exists - local check = io.open(target_path .. "/.git", "r") - if check then - check:close() - die("worktree already exists at " .. target_path) - end - - local output, code - if create_branch then - -- Create new branch with worktree - if start_point then - output, code = run_cmd( - "GIT_DIR=" - .. git_dir - .. " git worktree add -b " - .. branch - .. " -- " - .. target_path - .. " " - .. start_point - ) - else - output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -b " .. branch .. " -- " .. target_path) - end - else - -- Check if branch exists locally or on remotes - local exists_local = branch_exists_local(git_dir, branch) - local remotes = find_branch_remotes(git_dir, branch) - - if not exists_local and #remotes == 0 then - die("branch '" .. branch .. "' not found locally or on any remote (use -b to create)") - end - - if #remotes > 1 then - die("branch '" .. branch .. "' exists on multiple remotes: " .. table.concat(remotes, ", ")) - end - - output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. target_path .. " " .. branch) - end - - if code ~= 0 then - die("failed to add worktree: " .. output, EXIT_SYSTEM_ERROR) - end - - -- Run hooks if we have a source worktree - local project_config = load_project_config(root) - if source_worktree then - if project_config.hooks then - run_hooks(source_worktree, target_path, project_config.hooks, root) - end - elseif project_config.hooks then - io.stderr:write("warning: hooks skipped (run from inside a worktree to apply hooks)\n") - end - - print(target_path) -end - ---List directory entries (excluding . and ..) ---@param path string ---@return string[] diff --git a/src/wt/cmd/add.lua b/src/wt/cmd/add.lua new file mode 100644 index 0000000000000000000000000000000000000000..cc5d352803d53f2998889568852149210c5fd211 --- /dev/null +++ b/src/wt/cmd/add.lua @@ -0,0 +1,122 @@ +-- SPDX-FileCopyrightText: Amolith +-- +-- SPDX-License-Identifier: GPL-3.0-or-later + +local exit = require("wt.exit") +local shell = require("wt.shell") +local git = require("wt.git") +local path_mod = require("wt.path") +local config = require("wt.config") +local hooks = require("wt.hooks") + +---@class wt.cmd.add +local M = {} + +---Add a worktree for an existing or new branch +---@param args string[] +function M.cmd_add(args) + -- Parse arguments: [-b []] + ---@type string|nil + local branch = nil + local create_branch = false + ---@type string|nil + local start_point = nil + + local i = 1 + while i <= #args do + local a = args[i] + if a == "-b" then + create_branch = true + -- Check if next arg is start-point (not another flag) + if args[i + 1] and not args[i + 1]:match("^%-") then + start_point = args[i + 1] + i = i + 1 + end + elseif not branch then + branch = a + else + shell.die("unexpected argument: " .. a) + end + i = i + 1 + end + + if not branch then + shell.die("usage: wt a [-b []]") + return + end + + local root, err = git.find_project_root() + if not root then + shell.die(err --[[@as string]]) + return + end + + local git_dir = root .. "/.bare" + local source_worktree = git.detect_source_worktree(root) + + -- Load config for path style + local global_config = config.load_global_config() + local style = global_config.branch_path_style or "nested" + local separator = global_config.flat_separator or "_" + + local target_path = path_mod.branch_to_path(root, branch, style, separator) + + -- Check if target already exists + local check = io.open(target_path .. "/.git", "r") + if check then + check:close() + shell.die("worktree already exists at " .. target_path) + end + + local output, code + if create_branch then + -- Create new branch with worktree + if start_point then + output, code = shell.run_cmd( + "GIT_DIR=" + .. git_dir + .. " git worktree add -b " + .. branch + .. " -- " + .. target_path + .. " " + .. start_point + ) + else + output, code = + shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -b " .. branch .. " -- " .. target_path) + end + else + -- Check if branch exists locally or on remotes + local exists_local = git.branch_exists_local(git_dir, branch) + local remotes = git.find_branch_remotes(git_dir, branch) + + if not exists_local and #remotes == 0 then + shell.die("branch '" .. branch .. "' not found locally or on any remote (use -b to create)") + end + + if #remotes > 1 then + shell.die("branch '" .. branch .. "' exists on multiple remotes: " .. table.concat(remotes, ", ")) + end + + output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. target_path .. " " .. branch) + end + + if code ~= 0 then + shell.die("failed to add worktree: " .. output, exit.EXIT_SYSTEM_ERROR) + end + + -- Run hooks if we have a source worktree + local project_config = config.load_project_config(root) + if source_worktree then + if project_config.hooks then + hooks.run_hooks(source_worktree, target_path, project_config.hooks, root) + end + elseif project_config.hooks then + io.stderr:write("warning: hooks skipped (run from inside a worktree to apply hooks)\n") + end + + print(target_path) +end + +return M