@@ -861,6 +861,269 @@ end
return M
]]
+_EMBEDDED_MODULES["wt.cmd.add"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- 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: <branch> [-b [<start-point>]]
+ ---@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 <branch> [-b [<start-point>]]")
+ 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 <amolith@secluded.site>
+--
+-- 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 <branch> [-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 <amolith@secluded.site>
--
-- 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: <url> [--remote name]... [--own]
@@ -1449,242 +1715,6 @@ local function cmd_new(args)
end
end
----@param args string[]
-local function cmd_add(args)
- -- Parse arguments: <branch> [-b [<start-point>]]
- ---@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 <branch> [-b [<start-point>]]")
- 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: <branch> [-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 <branch> [-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[]
@@ -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: <url> [--remote name]... [--own]
@@ -479,111 +480,6 @@ local function cmd_new(args)
end
end
----@param args string[]
-local function cmd_add(args)
- -- Parse arguments: <branch> [-b [<start-point>]]
- ---@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 <branch> [-b [<start-point>]]")
- 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[]
@@ -0,0 +1,122 @@
+-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- 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: <branch> [-b [<start-point>]]
+ ---@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 <branch> [-b [<start-point>]]")
+ 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