refactor(wt): extract cmd.add module

Amolith created

Assisted-by: Claude Opus 4.5 via Amp

Change summary

dist/wt            | 508 +++++++++++++++++++++++++----------------------
src/main.lua       | 110 ----------
src/wt/cmd/add.lua | 122 +++++++++++
3 files changed, 394 insertions(+), 346 deletions(-)

Detailed changes

dist/wt 🔗

@@ -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[]

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: <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[]

src/wt/cmd/add.lua 🔗

@@ -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