refactor(wt): extract path module

Amolith created

Assisted-by: Claude Opus 4.5 via Amp

Change summary

dist/wt         | 187 +++++++++++++++++++++++++++-----------------------
src/main.lua    |  92 +-----------------------
src/wt/path.lua |  93 +++++++++++++++++++++++++
3 files changed, 202 insertions(+), 170 deletions(-)

Detailed changes

dist/wt 🔗

@@ -102,6 +102,101 @@ end
 return M
 ]]
 
+_EMBEDDED_MODULES["wt.path"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+---@class wt.path
+local M = {}
+
+---Split path into components
+---@param path string
+---@return string[]
+function M.split_path(path)
+	local parts = {}
+	for part in path:gmatch("[^/]+") do
+		table.insert(parts, part)
+	end
+	return parts
+end
+
+---Calculate relative path from one absolute path to another
+---@param from string absolute path of starting directory
+---@param to string absolute path of target
+---@return string relative path
+function M.relative_path(from, to)
+	if from == to then
+		return "./"
+	end
+
+	local from_parts = M.split_path(from)
+	local to_parts = M.split_path(to)
+
+	local common = 0
+	for i = 1, math.min(#from_parts, #to_parts) do
+		if from_parts[i] == to_parts[i] then
+			common = i
+		else
+			break
+		end
+	end
+
+	local up_count = #from_parts - common
+	local result = {}
+
+	for _ = 1, up_count do
+		table.insert(result, "..")
+	end
+
+	for i = common + 1, #to_parts do
+		table.insert(result, to_parts[i])
+	end
+
+	if #result == 0 then
+		return "./"
+	end
+
+	return table.concat(result, "/")
+end
+
+---Convert branch name to worktree path based on style
+---@param root string project root path
+---@param branch string branch name
+---@param style string "nested" or "flat"
+---@param separator? string separator for flat style (default "_")
+---@return string worktree path
+function M.branch_to_path(root, branch, style, separator)
+	if style == "flat" then
+		local sep = separator or "_"
+		local escaped_sep = sep:gsub("%%", "%%%%")
+		local flat_name = branch:gsub("/", escaped_sep)
+		return root .. "/" .. flat_name
+	end
+	-- nested style (default): preserve slashes
+	return root .. "/" .. branch
+end
+
+---Check if path_a is inside (or equal to) path_b
+---@param path_a string the path to check
+---@param path_b string the container path
+---@return boolean
+function M.path_inside(path_a, path_b)
+	-- Normalize: ensure no trailing slash for comparison
+	path_b = path_b:gsub("/$", "")
+	path_a = path_a:gsub("/$", "")
+	return path_a == path_b or path_a:sub(1, #path_b + 1) == path_b .. "/"
+end
+
+---Escape special Lua pattern characters in a string
+---@param str string
+---@return string
+function M.escape_pattern(str)
+	return (str:gsub("([%%%-%+%[%]%(%)%.%^%$%*%?])", "%%%1"))
+end
+
+return M
+]]
+
 
 if _VERSION < "Lua 5.2" then
 	io.stderr:write("error: wt requires Lua 5.2 or later\n")
@@ -119,6 +214,13 @@ local run_cmd_silent = shell.run_cmd_silent
 local get_cwd = shell.get_cwd
 local die = shell.die
 
+local path_mod = require("wt.path")
+local branch_to_path = path_mod.branch_to_path
+local split_path = path_mod.split_path
+local relative_path = path_mod.relative_path
+local path_inside = path_mod.path_inside
+local escape_pattern = path_mod.escape_pattern
+
 ---Walk up from cwd looking for .git file pointing to .bare, or .bare/ directory
 ---@return string|nil root
 ---@return string|nil error
@@ -398,23 +500,6 @@ local function get_default_branch()
 	return "main"
 end
 
----Convert branch name to worktree path
----@param root string
----@param branch string
----@param style string "nested" or "flat"
----@param separator? string separator for flat style
----@return string
-local function branch_to_path(root, branch, style, separator)
-	if style == "flat" then
-		local sep = separator or "_"
-		local escaped_sep = sep:gsub("%%", "%%%%")
-		local flat_name = branch:gsub("/", escaped_sep)
-		return root .. "/" .. flat_name
-	end
-	-- nested style (default): preserve slashes
-	return root .. "/" .. branch
-end
-
 ---Load global config from ~/.config/wt/config.lua
 ---@return {branch_path_style?: string, flat_separator?: string, remotes?: table<string, string>, default_remotes?: string[]|string}
 local function load_global_config()
@@ -481,56 +566,6 @@ local function load_project_config(root)
 	return result
 end
 
----Split path into components
----@param path string
----@return string[]
-local function split_path(path)
-	local parts = {}
-	for part in path:gmatch("[^/]+") do
-		table.insert(parts, part)
-	end
-	return parts
-end
-
----Calculate relative path from one absolute path to another
----@param from string absolute path of starting directory
----@param to string absolute path of target
----@return string relative path
-local function relative_path(from, to)
-	if from == to then
-		return "./"
-	end
-
-	local from_parts = split_path(from)
-	local to_parts = split_path(to)
-
-	local common = 0
-	for i = 1, math.min(#from_parts, #to_parts) do
-		if from_parts[i] == to_parts[i] then
-			common = i
-		else
-			break
-		end
-	end
-
-	local up_count = #from_parts - common
-	local result = {}
-
-	for _ = 1, up_count do
-		table.insert(result, "..")
-	end
-
-	for i = common + 1, #to_parts do
-		table.insert(result, to_parts[i])
-	end
-
-	if #result == 0 then
-		return "./"
-	end
-
-	return table.concat(result, "/")
-end
-
 ---Check if cwd is inside a worktree (has .git file, not at project root)
 ---@param root string
 ---@return string|nil source_worktree path if inside worktree, nil if at project root
@@ -571,13 +606,6 @@ local function branch_exists_local(git_dir, branch)
 	return run_cmd_silent("GIT_DIR=" .. git_dir .. " git show-ref --verify --quiet refs/heads/" .. branch)
 end
 
----Escape special Lua pattern characters in a string
----@param str string
----@return string
-local function escape_pattern(str)
-	return (str:gsub("([%%%-%+%[%]%(%)%.%^%$%*%?])", "%%%1"))
-end
-
 ---Parse git branch -r output to extract remotes containing a branch
 ---@param output string git branch -r output
 ---@param branch string branch name to find
@@ -1291,17 +1319,6 @@ local function cmd_add(args)
 	print(target_path)
 end
 
----Check if path_a is inside (or equal to) path_b
----@param path_a string the path to check
----@param path_b string the container path
----@return boolean
-local function path_inside(path_a, path_b)
-	-- Normalize: ensure no trailing slash for comparison
-	path_b = path_b:gsub("/$", "")
-	path_a = path_a:gsub("/$", "")
-	return path_a == path_b or path_a:sub(1, #path_b + 1) == path_b .. "/"
-end
-
 ---Check if cwd is inside (or equal to) a given path
 ---@param target string
 ---@return boolean

src/main.lua 🔗

@@ -20,6 +20,13 @@ local run_cmd_silent = shell.run_cmd_silent
 local get_cwd = shell.get_cwd
 local die = shell.die
 
+local path_mod = require("wt.path")
+local branch_to_path = path_mod.branch_to_path
+local split_path = path_mod.split_path
+local relative_path = path_mod.relative_path
+local path_inside = path_mod.path_inside
+local escape_pattern = path_mod.escape_pattern
+
 ---Walk up from cwd looking for .git file pointing to .bare, or .bare/ directory
 ---@return string|nil root
 ---@return string|nil error
@@ -299,23 +306,6 @@ local function get_default_branch()
 	return "main"
 end
 
----Convert branch name to worktree path
----@param root string
----@param branch string
----@param style string "nested" or "flat"
----@param separator? string separator for flat style
----@return string
-local function branch_to_path(root, branch, style, separator)
-	if style == "flat" then
-		local sep = separator or "_"
-		local escaped_sep = sep:gsub("%%", "%%%%")
-		local flat_name = branch:gsub("/", escaped_sep)
-		return root .. "/" .. flat_name
-	end
-	-- nested style (default): preserve slashes
-	return root .. "/" .. branch
-end
-
 ---Load global config from ~/.config/wt/config.lua
 ---@return {branch_path_style?: string, flat_separator?: string, remotes?: table<string, string>, default_remotes?: string[]|string}
 local function load_global_config()
@@ -382,56 +372,6 @@ local function load_project_config(root)
 	return result
 end
 
----Split path into components
----@param path string
----@return string[]
-local function split_path(path)
-	local parts = {}
-	for part in path:gmatch("[^/]+") do
-		table.insert(parts, part)
-	end
-	return parts
-end
-
----Calculate relative path from one absolute path to another
----@param from string absolute path of starting directory
----@param to string absolute path of target
----@return string relative path
-local function relative_path(from, to)
-	if from == to then
-		return "./"
-	end
-
-	local from_parts = split_path(from)
-	local to_parts = split_path(to)
-
-	local common = 0
-	for i = 1, math.min(#from_parts, #to_parts) do
-		if from_parts[i] == to_parts[i] then
-			common = i
-		else
-			break
-		end
-	end
-
-	local up_count = #from_parts - common
-	local result = {}
-
-	for _ = 1, up_count do
-		table.insert(result, "..")
-	end
-
-	for i = common + 1, #to_parts do
-		table.insert(result, to_parts[i])
-	end
-
-	if #result == 0 then
-		return "./"
-	end
-
-	return table.concat(result, "/")
-end
-
 ---Check if cwd is inside a worktree (has .git file, not at project root)
 ---@param root string
 ---@return string|nil source_worktree path if inside worktree, nil if at project root
@@ -472,13 +412,6 @@ local function branch_exists_local(git_dir, branch)
 	return run_cmd_silent("GIT_DIR=" .. git_dir .. " git show-ref --verify --quiet refs/heads/" .. branch)
 end
 
----Escape special Lua pattern characters in a string
----@param str string
----@return string
-local function escape_pattern(str)
-	return (str:gsub("([%%%-%+%[%]%(%)%.%^%$%*%?])", "%%%1"))
-end
-
 ---Parse git branch -r output to extract remotes containing a branch
 ---@param output string git branch -r output
 ---@param branch string branch name to find
@@ -1192,17 +1125,6 @@ local function cmd_add(args)
 	print(target_path)
 end
 
----Check if path_a is inside (or equal to) path_b
----@param path_a string the path to check
----@param path_b string the container path
----@return boolean
-local function path_inside(path_a, path_b)
-	-- Normalize: ensure no trailing slash for comparison
-	path_b = path_b:gsub("/$", "")
-	path_a = path_a:gsub("/$", "")
-	return path_a == path_b or path_a:sub(1, #path_b + 1) == path_b .. "/"
-end
-
 ---Check if cwd is inside (or equal to) a given path
 ---@param target string
 ---@return boolean

src/wt/path.lua 🔗

@@ -0,0 +1,93 @@
+-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+---@class wt.path
+local M = {}
+
+---Split path into components
+---@param path string
+---@return string[]
+function M.split_path(path)
+	local parts = {}
+	for part in path:gmatch("[^/]+") do
+		table.insert(parts, part)
+	end
+	return parts
+end
+
+---Calculate relative path from one absolute path to another
+---@param from string absolute path of starting directory
+---@param to string absolute path of target
+---@return string relative path
+function M.relative_path(from, to)
+	if from == to then
+		return "./"
+	end
+
+	local from_parts = M.split_path(from)
+	local to_parts = M.split_path(to)
+
+	local common = 0
+	for i = 1, math.min(#from_parts, #to_parts) do
+		if from_parts[i] == to_parts[i] then
+			common = i
+		else
+			break
+		end
+	end
+
+	local up_count = #from_parts - common
+	local result = {}
+
+	for _ = 1, up_count do
+		table.insert(result, "..")
+	end
+
+	for i = common + 1, #to_parts do
+		table.insert(result, to_parts[i])
+	end
+
+	if #result == 0 then
+		return "./"
+	end
+
+	return table.concat(result, "/")
+end
+
+---Convert branch name to worktree path based on style
+---@param root string project root path
+---@param branch string branch name
+---@param style string "nested" or "flat"
+---@param separator? string separator for flat style (default "_")
+---@return string worktree path
+function M.branch_to_path(root, branch, style, separator)
+	if style == "flat" then
+		local sep = separator or "_"
+		local escaped_sep = sep:gsub("%%", "%%%%")
+		local flat_name = branch:gsub("/", escaped_sep)
+		return root .. "/" .. flat_name
+	end
+	-- nested style (default): preserve slashes
+	return root .. "/" .. branch
+end
+
+---Check if path_a is inside (or equal to) path_b
+---@param path_a string the path to check
+---@param path_b string the container path
+---@return boolean
+function M.path_inside(path_a, path_b)
+	-- Normalize: ensure no trailing slash for comparison
+	path_b = path_b:gsub("/$", "")
+	path_a = path_a:gsub("/$", "")
+	return path_a == path_b or path_a:sub(1, #path_b + 1) == path_b .. "/"
+end
+
+---Escape special Lua pattern characters in a string
+---@param str string
+---@return string
+function M.escape_pattern(str)
+	return (str:gsub("([%%%-%+%[%]%(%)%.%^%$%*%?])", "%%%1"))
+end
+
+return M