@@ -861,7 +861,7 @@ end
return M
]]
-_EMBEDDED_MODULES["wt.cmd.add"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+_EMBEDDED_MODULES["wt.cmd.clone"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
--
-- SPDX-License-Identifier: GPL-3.0-or-later
@@ -870,261 +870,273 @@ 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
+---@class wt.cmd.clone
local M = {}
----Add a worktree for an existing or new branch
+---Clone a repository with bare repo structure
---@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
+function M.cmd_clone(args)
+ -- Parse arguments: <url> [--remote name]... [--own]
+ local url = nil
+ ---@type string[]
+ local remote_flags = {}
+ local own = false
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
+ if a == "--remote" then
+ if not args[i + 1] then
+ shell.die("--remote requires a name")
end
- elseif not branch then
- branch = a
+ table.insert(remote_flags, args[i + 1])
+ i = i + 1
+ elseif a == "--own" then
+ own = true
+ elseif not url then
+ url = 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>]]")
+ if not url then
+ shell.die("usage: wt c <url> [--remote name]... [--own]")
return
end
- local root, err = git.find_project_root()
- if not root then
- shell.die(err --[[@as string]])
+ -- Extract project name from URL
+ local project_name = config.extract_project_name(url)
+ if not project_name then
+ shell.die("could not extract project name from URL: " .. url)
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)
+ -- Check if project directory already exists
local cwd = shell.get_cwd()
if not cwd then
- return false
+ shell.die("failed to get current directory", exit.EXIT_SYSTEM_ERROR)
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
+ ---@cast cwd string
+ local project_path = cwd .. "/" .. project_name
+ local check = io.open(project_path, "r")
+ if check then
+ check:close()
+ shell.die("directory already exists: " .. project_path)
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
+ -- Clone bare repo
+ local bare_path = project_path .. "/.bare"
+ local output, code = shell.run_cmd("git clone --bare " .. url .. " " .. bare_path)
+ if code ~= 0 then
+ shell.die("failed to clone: " .. output, exit.EXIT_SYSTEM_ERROR)
end
- if not branch then
- shell.die("usage: wt r <branch> [-b] [-f]")
+ -- Write .git file pointing to .bare
+ local git_file_handle = io.open(project_path .. "/.git", "w")
+ if not git_file_handle then
+ shell.die("failed to create .git file", exit.EXIT_SYSTEM_ERROR)
return
end
+ git_file_handle:write("gitdir: ./.bare\n")
+ git_file_handle:close()
- local root, err = git.find_project_root()
- if not root then
- shell.die(err --[[@as string]])
- return
- end
+ -- Detect default branch
+ local git_dir = bare_path
+ local default_branch = git.detect_cloned_default_branch(git_dir)
- local git_dir = root .. "/.bare"
+ -- Load global config
+ local global_config = config.load_global_config()
- 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
+ -- Determine which remotes to use
+ ---@type string[]
+ local selected_remotes = {}
- 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
+ if #remote_flags > 0 then
+ selected_remotes = remote_flags
+ elseif global_config.default_remotes then
+ if type(global_config.default_remotes) == "table" then
+ selected_remotes = global_config.default_remotes --[[@as string[] ]]
+ elseif global_config.default_remotes == "prompt" then
+ if global_config.remotes then
+ local keys = {}
+ for k in pairs(global_config.remotes) do
+ table.insert(keys, k)
+ end
+ table.sort(keys)
+ if #keys > 0 then
+ local input = table.concat(keys, "\n")
+ local choose_type = own and "" or " --no-limit"
+ local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
+ output, code = shell.run_cmd(cmd)
+ if code == 0 and output ~= "" then
+ for line in output:gmatch("[^\n]+") do
+ table.insert(selected_remotes, line)
+ end
+ end
+ end
+ end
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)")
+ elseif global_config.remotes then
+ local keys = {}
+ for k in pairs(global_config.remotes) do
+ table.insert(keys, k)
+ end
+ table.sort(keys)
+ if #keys > 0 then
+ local input = table.concat(keys, "\n")
+ local choose_type = own and "" or " --no-limit"
+ local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
+ output, code = shell.run_cmd(cmd)
+ if code == 0 and output ~= "" then
+ for line in output:gmatch("[^\n]+") do
+ table.insert(selected_remotes, line)
+ end
+ end
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
+ -- Track configured remotes for summary
+ ---@type string[]
+ local configured_remotes = {}
- 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 own then
+ -- User's own project: origin is their canonical remote
+ if #selected_remotes > 0 then
+ local first_remote = selected_remotes[1]
+ -- Rename origin to first remote
+ output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin " .. first_remote)
+ if code ~= 0 then
+ io.stderr:write("warning: failed to rename origin to " .. first_remote .. ": " .. output .. "\n")
+ else
+ -- Configure fetch refspec
+ shell.run_cmd(
+ "GIT_DIR="
+ .. git_dir
+ .. " git config remote."
+ .. first_remote
+ .. ".fetch '+refs/heads/*:refs/remotes/"
+ .. first_remote
+ .. "/*'"
+ )
+ table.insert(configured_remotes, first_remote)
+ 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
+ -- Add additional remotes and push to them
+ for j = 2, #selected_remotes do
+ local remote_name = selected_remotes[j]
+ local template = global_config.remotes and global_config.remotes[remote_name]
+ if template then
+ local remote_url = config.resolve_url_template(template, project_name)
+ output, code =
+ shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
+ if code ~= 0 then
+ io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
+ else
+ shell.run_cmd(
+ "GIT_DIR="
+ .. git_dir
+ .. " git config remote."
+ .. remote_name
+ .. ".fetch '+refs/heads/*:refs/remotes/"
+ .. remote_name
+ .. "/*'"
+ )
+ -- Push to additional remotes
+ output, code =
+ shell.run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
+ if code ~= 0 then
+ io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
+ end
+ table.insert(configured_remotes, remote_name)
+ end
+ else
+ io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+ end
+ end
+ else
+ -- No remotes selected, keep origin as-is
+ shell.run_cmd(
+ "GIT_DIR=" .. git_dir .. " git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'"
+ )
+ table.insert(configured_remotes, "origin")
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)
+ else
+ -- Contributing to someone else's project
+ -- Rename origin to upstream
+ output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin upstream")
+ if code ~= 0 then
+ io.stderr:write("warning: failed to rename origin to upstream: " .. output .. "\n")
+ else
+ shell.run_cmd(
+ "GIT_DIR=" .. git_dir .. " git config remote.upstream.fetch '+refs/heads/*:refs/remotes/upstream/*'"
+ )
+ table.insert(configured_remotes, "upstream")
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
+ -- Add user's remotes and push to each
+ for _, remote_name in ipairs(selected_remotes) do
+ local template = global_config.remotes and global_config.remotes[remote_name]
+ if template then
+ local remote_url = config.resolve_url_template(template, project_name)
+ output, code =
+ shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
+ if code ~= 0 then
+ io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
+ else
+ shell.run_cmd(
+ "GIT_DIR="
+ .. git_dir
+ .. " git config remote."
+ .. remote_name
+ .. ".fetch '+refs/heads/*:refs/remotes/"
+ .. remote_name
+ .. "/*'"
+ )
+ -- Push to this remote
+ output, code =
+ shell.run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
+ if code ~= 0 then
+ io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
+ end
+ table.insert(configured_remotes, remote_name)
+ end
+ else
+ io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+ end
end
+ end
- print("Worktree and branch '" .. branch .. "' removed")
- else
- print("Worktree removed")
+ -- Fetch all remotes
+ shell.run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all")
+
+ -- Load config for path style
+ local style = global_config.branch_path_style or "nested"
+ local separator = global_config.flat_separator
+ local worktree_path = path_mod.branch_to_path(project_path, default_branch, style, separator)
+
+ -- Create initial worktree
+ output, code =
+ shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
+ if code ~= 0 then
+ shell.die("failed to create worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
+ end
+
+ -- Print summary
+ print("Created project: " .. project_path)
+ print("Default branch: " .. default_branch)
+ print("Worktree: " .. worktree_path)
+ if #configured_remotes > 0 then
+ print("Remotes: " .. table.concat(configured_remotes, ", "))
end
end
return M
]]
-_EMBEDDED_MODULES["wt.cmd.list"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+_EMBEDDED_MODULES["wt.cmd.new"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
--
-- SPDX-License-Identifier: GPL-3.0-or-later
@@ -1132,588 +1144,622 @@ 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")
----@class wt.cmd.list
+---@class wt.cmd.new
local M = {}
----List all worktrees with status
-function M.cmd_list()
- local root, err = git.find_project_root()
- if not root then
- shell.die(err --[[@as string]])
+---Create a new project with bare repo structure
+---@param args string[]
+function M.cmd_new(args)
+ -- Parse arguments: <project-name> [--remote name]...
+ local project_name = nil
+ ---@type string[]
+ local remote_flags = {}
+
+ local i = 1
+ while i <= #args do
+ local a = args[i]
+ if a == "--remote" then
+ if not args[i + 1] then
+ shell.die("--remote requires a name")
+ end
+ table.insert(remote_flags, args[i + 1])
+ i = i + 1
+ elseif not project_name then
+ project_name = a
+ else
+ shell.die("unexpected argument: " .. a)
+ end
+ i = i + 1
+ end
+
+ if not project_name then
+ shell.die("usage: wt n <project-name> [--remote name]...")
return
end
- local git_dir = root .. "/.bare"
- local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain")
- if code ~= 0 then
- shell.die("failed to list worktrees: " .. output, exit.EXIT_SYSTEM_ERROR)
+ -- Check if project directory already exists
+ local cwd = shell.get_cwd()
+ if not cwd then
+ shell.die("failed to get current directory", exit.EXIT_SYSTEM_ERROR)
+ end
+ ---@cast cwd string
+ local project_path = cwd .. "/" .. project_name
+ local check = io.open(project_path, "r")
+ if check then
+ check:close()
+ shell.die("directory already exists: " .. project_path)
end
- ---@type {path: string, head: string, branch: string}[]
- local worktrees = {}
- local current = {}
+ -- Load global config
+ local global_config = config.load_global_config()
- for line in output:gmatch("[^\n]+") do
- local key, value = line:match("^(%S+)%s*(.*)$")
- if key == "worktree" and value then
- if current.path then
- table.insert(worktrees, current)
+ -- Determine which remotes to use
+ ---@type string[]
+ local selected_remotes = {}
+
+ if #remote_flags > 0 then
+ -- Use explicitly provided remotes
+ selected_remotes = remote_flags
+ elseif global_config.default_remotes then
+ if type(global_config.default_remotes) == "table" then
+ selected_remotes = global_config.default_remotes --[[@as string[] ]]
+ elseif global_config.default_remotes == "prompt" then
+ -- Prompt with gum choose
+ if global_config.remotes then
+ local keys = {}
+ for k in pairs(global_config.remotes) do
+ table.insert(keys, k)
+ end
+ table.sort(keys)
+ if #keys > 0 then
+ local input = table.concat(keys, "\n")
+ local cmd = "echo '" .. input .. "' | gum choose --no-limit"
+ local output, code = shell.run_cmd(cmd)
+ if code == 0 and output ~= "" then
+ for line in output:gmatch("[^\n]+") do
+ table.insert(selected_remotes, line)
+ end
+ end
+ end
end
- if value:match("/%.bare$") then
- current = {}
- else
- current = { path = value, head = "", branch = "(detached)" }
+ end
+ elseif global_config.remotes then
+ -- No default_remotes configured, prompt if remotes exist
+ local keys = {}
+ for k in pairs(global_config.remotes) do
+ table.insert(keys, k)
+ end
+ table.sort(keys)
+ if #keys > 0 then
+ local input = table.concat(keys, "\n")
+ local cmd = "echo '" .. input .. "' | gum choose --no-limit"
+ local output, code = shell.run_cmd(cmd)
+ if code == 0 and output ~= "" then
+ for line in output:gmatch("[^\n]+") do
+ table.insert(selected_remotes, line)
+ end
end
- elseif key == "HEAD" and value then
- current.head = value:sub(1, 7)
- elseif key == "branch" and value then
- current.branch = value:gsub("^refs/heads/", "")
- elseif key == "bare" then
- current = {}
end
end
- if current.path then
- table.insert(worktrees, current)
- end
- if #worktrees == 0 then
- print("No worktrees found")
- return
+ -- Create project structure
+ local bare_path = project_path .. "/.bare"
+ local output, code = shell.run_cmd("mkdir -p " .. bare_path)
+ if code ~= 0 then
+ shell.die("failed to create directory: " .. output, exit.EXIT_SYSTEM_ERROR)
end
- local cwd = shell.get_cwd() or ""
-
- local rows = {}
- for _, wt in ipairs(worktrees) do
- local rel_path = path_mod.relative_path(cwd, wt.path)
-
- local status_out = shell.run_cmd("git -C " .. wt.path .. " status --porcelain")
- local status = status_out == "" and "clean" or "dirty"
-
- table.insert(rows, rel_path .. "," .. wt.branch .. "," .. wt.head .. "," .. status)
+ output, code = shell.run_cmd("git init --bare " .. bare_path)
+ if code ~= 0 then
+ shell.die("failed to init bare repo: " .. output, exit.EXIT_SYSTEM_ERROR)
end
- local table_input = "Path,Branch,HEAD,Status\n" .. table.concat(rows, "\n")
- table_input = table_input:gsub("EOF", "eof")
- local table_cmd = "gum table --print <<'EOF'\n" .. table_input .. "\nEOF"
- local table_handle = io.popen(table_cmd, "r")
- if not table_handle then
+ -- Write .git file pointing to .bare
+ local git_file_handle = io.open(project_path .. "/.git", "w")
+ if not git_file_handle then
+ shell.die("failed to create .git file", exit.EXIT_SYSTEM_ERROR)
return
end
- io.write(table_handle:read("*a") or "")
- table_handle:close()
-end
-
-return M
-]]
+ git_file_handle:write("gitdir: ./.bare\n")
+ git_file_handle:close()
-_EMBEDDED_MODULES["wt.cmd.fetch"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
---
--- SPDX-License-Identifier: GPL-3.0-or-later
+ -- Add remotes
+ local git_dir = bare_path
+ for _, remote_name in ipairs(selected_remotes) do
+ local template = global_config.remotes and global_config.remotes[remote_name]
+ if template then
+ local url = config.resolve_url_template(template, project_name)
+ output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. url)
+ if code ~= 0 then
+ io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
+ else
+ -- Configure fetch refspec for the remote
+ shell.run_cmd(
+ "GIT_DIR="
+ .. git_dir
+ .. " git config remote."
+ .. remote_name
+ .. ".fetch '+refs/heads/*:refs/remotes/"
+ .. remote_name
+ .. "/*'"
+ )
+ end
+ else
+ io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+ end
+ end
-local exit = require("wt.exit")
-local shell = require("wt.shell")
-local git = require("wt.git")
+ -- Detect default branch
+ local default_branch = git.get_default_branch()
----@class wt.cmd.fetch
-local M = {}
+ -- Load config for path style
+ local style = global_config.branch_path_style or "nested"
+ local separator = global_config.flat_separator
+ local worktree_path = path_mod.branch_to_path(project_path, default_branch, style, separator)
----Fetch all remotes with pruning
-function M.cmd_fetch()
- local root, err = git.find_project_root()
- if not root then
- shell.die(err --[[@as string]])
- return
+ -- Create orphan worktree
+ output, code = shell.run_cmd(
+ "GIT_DIR=" .. git_dir .. " git worktree add --orphan -b " .. default_branch .. " -- " .. worktree_path
+ )
+ if code ~= 0 then
+ shell.die("failed to create worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
end
- local git_dir = root .. "/.bare"
- local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all --prune")
- io.write(output)
- if code ~= 0 then
- os.exit(exit.EXIT_SYSTEM_ERROR)
+ -- Print summary
+ print("Created project: " .. project_path)
+ print("Default branch: " .. default_branch)
+ print("Worktree: " .. worktree_path)
+ if #selected_remotes > 0 then
+ print("Remotes: " .. table.concat(selected_remotes, ", "))
end
end
return M
]]
-
-if _VERSION < "Lua 5.2" then
- io.stderr:write("error: wt requires Lua 5.2 or later\n")
- os.exit(1)
-end
+_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 EXIT_SUCCESS = exit.EXIT_SUCCESS
-local EXIT_USER_ERROR = exit.EXIT_USER_ERROR
-local EXIT_SYSTEM_ERROR = exit.EXIT_SYSTEM_ERROR
-
local shell = require("wt.shell")
-local run_cmd = shell.run_cmd
-local run_cmd_silent = shell.run_cmd_silent
-local get_cwd = shell.get_cwd
-local die = shell.die
-
+local git = require("wt.git")
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
-
-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 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 config_mod = require("wt.config")
-local resolve_url_template = config_mod.resolve_url_template
-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
-
-local help_mod = require("wt.help")
-local print_usage = help_mod.print_usage
-local show_command_help = help_mod.show_command_help
-
-local fetch_mod = require("wt.cmd.fetch")
-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 config = require("wt.config")
+local hooks = require("wt.hooks")
-local add_mod = require("wt.cmd.add")
-local cmd_add = add_mod.cmd_add
+---@class wt.cmd.add
+local M = {}
+---Add a worktree for an existing or new branch
---@param args string[]
-local function cmd_clone(args)
- -- Parse arguments: <url> [--remote name]... [--own]
- local url = nil
- ---@type string[]
- local remote_flags = {}
- local own = false
+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 == "--remote" then
- if not args[i + 1] then
- die("--remote requires a name")
+ 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
- table.insert(remote_flags, args[i + 1])
- i = i + 1
- elseif a == "--own" then
- own = true
- elseif not url then
- url = a
+ elseif not branch then
+ branch = a
else
- die("unexpected argument: " .. a)
+ shell.die("unexpected argument: " .. a)
end
i = i + 1
end
- if not url then
- die("usage: wt c <url> [--remote name]... [--own]")
+ if not branch then
+ shell.die("usage: wt a <branch> [-b [<start-point>]]")
return
end
- -- Extract project name from URL
- local project_name = extract_project_name(url)
- if not project_name then
- die("could not extract project name from URL: " .. url)
+ local root, err = git.find_project_root()
+ if not root then
+ shell.die(err --[[@as string]])
return
end
- -- Check if project directory already exists
- local cwd = get_cwd()
- if not cwd then
- die("failed to get current directory", EXIT_SYSTEM_ERROR)
- end
- local project_path = cwd .. "/" .. project_name
- local check = io.open(project_path, "r")
+ 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()
- die("directory already exists: " .. project_path)
+ 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
- -- Clone bare repo
- local bare_path = project_path .. "/.bare"
- local output, code = run_cmd("git clone --bare " .. url .. " " .. bare_path)
if code ~= 0 then
- die("failed to clone: " .. output, EXIT_SYSTEM_ERROR)
+ shell.die("failed to add worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
end
- -- Write .git file pointing to .bare
- local git_file_handle = io.open(project_path .. "/.git", "w")
- if not git_file_handle then
- die("failed to create .git file", EXIT_SYSTEM_ERROR)
- return
+ -- 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
- git_file_handle:write("gitdir: ./.bare\n")
- git_file_handle:close()
- -- Detect default branch
- local git_dir = bare_path
- local default_branch = detect_cloned_default_branch(git_dir)
+ print(target_path)
+end
- -- Load global config
- local global_config = load_global_config()
+return M
+]]
- -- Determine which remotes to use
- ---@type string[]
- local selected_remotes = {}
+_EMBEDDED_MODULES["wt.cmd.remove"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
- if #remote_flags > 0 then
- selected_remotes = remote_flags
- elseif global_config.default_remotes then
- if type(global_config.default_remotes) == "table" then
- selected_remotes = global_config.default_remotes
- elseif global_config.default_remotes == "prompt" then
- if global_config.remotes then
- local keys = {}
- for k in pairs(global_config.remotes) do
- table.insert(keys, k)
- end
- table.sort(keys)
- if #keys > 0 then
- local input = table.concat(keys, "\n")
- local choose_type = own and "" or " --no-limit"
- local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
- output, code = run_cmd(cmd)
- if code == 0 and output ~= "" then
- for line in output:gmatch("[^\n]+") do
- table.insert(selected_remotes, line)
- end
- end
- end
- end
+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
- elseif global_config.remotes then
- local keys = {}
- for k in pairs(global_config.remotes) do
- table.insert(keys, k)
+ 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
- table.sort(keys)
- if #keys > 0 then
- local input = table.concat(keys, "\n")
- local choose_type = own and "" or " --no-limit"
- local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
- output, code = run_cmd(cmd)
- if code == 0 and output ~= "" then
- for line in output:gmatch("[^\n]+") do
- table.insert(selected_remotes, line)
- end
- 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
- -- Track configured remotes for summary
- ---@type string[]
- local configured_remotes = {}
+ 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
- if own then
- -- User's own project: origin is their canonical remote
- if #selected_remotes > 0 then
- local first_remote = selected_remotes[1]
- -- Rename origin to first remote
- output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin " .. first_remote)
- if code ~= 0 then
- io.stderr:write("warning: failed to rename origin to " .. first_remote .. ": " .. output .. "\n")
- else
- -- Configure fetch refspec
- run_cmd(
- "GIT_DIR="
- .. git_dir
- .. " git config remote."
- .. first_remote
- .. ".fetch '+refs/heads/*:refs/remotes/"
- .. first_remote
- .. "/*'"
- )
- table.insert(configured_remotes, first_remote)
- end
+ local output, code = shell.run_cmd(remove_cmd)
+ if code ~= 0 then
+ shell.die("failed to remove worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
+ end
- -- Add additional remotes and push to them
- for j = 2, #selected_remotes do
- local remote_name = selected_remotes[j]
- local template = global_config.remotes and global_config.remotes[remote_name]
- if template then
- local remote_url = resolve_url_template(template, project_name)
- output, code =
- run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
- if code ~= 0 then
- io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
- else
- run_cmd(
- "GIT_DIR="
- .. git_dir
- .. " git config remote."
- .. remote_name
- .. ".fetch '+refs/heads/*:refs/remotes/"
- .. remote_name
- .. "/*'"
- )
- -- Push to additional remotes
- output, code =
- run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
- if code ~= 0 then
- io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
- end
- table.insert(configured_remotes, remote_name)
- end
- else
- io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
- end
- end
- else
- -- No remotes selected, keep origin as-is
- run_cmd("GIT_DIR=" .. git_dir .. " git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'")
- table.insert(configured_remotes, "origin")
+ 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
- else
- -- Contributing to someone else's project
- -- Rename origin to upstream
- output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin upstream")
- if code ~= 0 then
- io.stderr:write("warning: failed to rename origin to upstream: " .. output .. "\n")
- else
- run_cmd(
- "GIT_DIR=" .. git_dir .. " git config remote.upstream.fetch '+refs/heads/*:refs/remotes/upstream/*'"
- )
- table.insert(configured_remotes, "upstream")
+
+ 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
- -- Add user's remotes and push to each
- for _, remote_name in ipairs(selected_remotes) do
- local template = global_config.remotes and global_config.remotes[remote_name]
- if template then
- local remote_url = resolve_url_template(template, project_name)
- output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
- if code ~= 0 then
- io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
- else
- run_cmd(
- "GIT_DIR="
- .. git_dir
- .. " git config remote."
- .. remote_name
- .. ".fetch '+refs/heads/*:refs/remotes/"
- .. remote_name
- .. "/*'"
- )
- -- Push to this remote
- output, code =
- run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
- if code ~= 0 then
- io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
- end
- table.insert(configured_remotes, remote_name)
- 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
+
+local exit = require("wt.exit")
+local shell = require("wt.shell")
+local git = require("wt.git")
+local path_mod = require("wt.path")
+
+---@class wt.cmd.list
+local M = {}
+
+---List all worktrees with status
+function M.cmd_list()
+ local root, err = git.find_project_root()
+ if not root then
+ shell.die(err --[[@as string]])
+ return
+ end
+
+ local git_dir = root .. "/.bare"
+ local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain")
+ if code ~= 0 then
+ shell.die("failed to list worktrees: " .. output, exit.EXIT_SYSTEM_ERROR)
+ end
+
+ ---@type {path: string, head: string, branch: string}[]
+ local worktrees = {}
+ local current = {}
+
+ for line in output:gmatch("[^\n]+") do
+ local key, value = line:match("^(%S+)%s*(.*)$")
+ if key == "worktree" and value then
+ if current.path then
+ table.insert(worktrees, current)
+ end
+ if value:match("/%.bare$") then
+ current = {}
else
- io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
+ current = { path = value, head = "", branch = "(detached)" }
end
+ elseif key == "HEAD" and value then
+ current.head = value:sub(1, 7)
+ elseif key == "branch" and value then
+ current.branch = value:gsub("^refs/heads/", "")
+ elseif key == "bare" then
+ current = {}
end
end
+ if current.path then
+ table.insert(worktrees, current)
+ end
- -- Fetch all remotes
- run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all")
+ if #worktrees == 0 then
+ print("No worktrees found")
+ return
+ end
- -- Load config for path style
- local style = global_config.branch_path_style or "nested"
- local separator = global_config.flat_separator
- local worktree_path = branch_to_path(project_path, default_branch, style, separator)
+ local cwd = shell.get_cwd() or ""
- -- Create initial worktree
- output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
- if code ~= 0 then
- die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
+ local rows = {}
+ for _, wt in ipairs(worktrees) do
+ local rel_path = path_mod.relative_path(cwd, wt.path)
+
+ local status_out = shell.run_cmd("git -C " .. wt.path .. " status --porcelain")
+ local status = status_out == "" and "clean" or "dirty"
+
+ table.insert(rows, rel_path .. "," .. wt.branch .. "," .. wt.head .. "," .. status)
end
- -- Print summary
- print("Created project: " .. project_path)
- print("Default branch: " .. default_branch)
- print("Worktree: " .. worktree_path)
- if #configured_remotes > 0 then
- print("Remotes: " .. table.concat(configured_remotes, ", "))
+ local table_input = "Path,Branch,HEAD,Status\n" .. table.concat(rows, "\n")
+ table_input = table_input:gsub("EOF", "eof")
+ local table_cmd = "gum table --print <<'EOF'\n" .. table_input .. "\nEOF"
+ local table_handle = io.popen(table_cmd, "r")
+ if not table_handle then
+ return
end
+ io.write(table_handle:read("*a") or "")
+ table_handle:close()
end
----@param args string[]
-local function cmd_new(args)
- -- Parse arguments: <project-name> [--remote name]...
- local project_name = nil
- ---@type string[]
- local remote_flags = {}
+return M
+]]
- local i = 1
- while i <= #args do
- local a = args[i]
- if a == "--remote" then
- if not args[i + 1] then
- die("--remote requires a name")
- end
- table.insert(remote_flags, args[i + 1])
- i = i + 1
- elseif not project_name then
- project_name = a
- else
- die("unexpected argument: " .. a)
- end
- i = i + 1
- end
+_EMBEDDED_MODULES["wt.cmd.fetch"] = [[-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+--
+-- SPDX-License-Identifier: GPL-3.0-or-later
- if not project_name then
- die("usage: wt n <project-name> [--remote name]...")
+local exit = require("wt.exit")
+local shell = require("wt.shell")
+local git = require("wt.git")
+
+---@class wt.cmd.fetch
+local M = {}
+
+---Fetch all remotes with pruning
+function M.cmd_fetch()
+ local root, err = git.find_project_root()
+ if not root then
+ shell.die(err --[[@as string]])
return
end
- -- Check if project directory already exists
- local cwd = get_cwd()
- if not cwd then
- die("failed to get current directory", EXIT_SYSTEM_ERROR)
- end
- local project_path = cwd .. "/" .. project_name
- local check = io.open(project_path, "r")
- if check then
- check:close()
- die("directory already exists: " .. project_path)
+ local git_dir = root .. "/.bare"
+ local output, code = shell.run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all --prune")
+ io.write(output)
+ if code ~= 0 then
+ os.exit(exit.EXIT_SYSTEM_ERROR)
end
+end
- -- Load global config
- local global_config = load_global_config()
+return M
+]]
- -- Determine which remotes to use
- ---@type string[]
- local selected_remotes = {}
- if #remote_flags > 0 then
- -- Use explicitly provided remotes
- selected_remotes = remote_flags
- elseif global_config.default_remotes then
- if type(global_config.default_remotes) == "table" then
- selected_remotes = global_config.default_remotes
- elseif global_config.default_remotes == "prompt" then
- -- Prompt with gum choose
- if global_config.remotes then
- local keys = {}
- for k in pairs(global_config.remotes) do
- table.insert(keys, k)
- end
- table.sort(keys)
- if #keys > 0 then
- local input = table.concat(keys, "\n")
- local cmd = "echo '" .. input .. "' | gum choose --no-limit"
- local output, code = run_cmd(cmd)
- if code == 0 and output ~= "" then
- for line in output:gmatch("[^\n]+") do
- table.insert(selected_remotes, line)
- end
- end
- end
- end
- end
- elseif global_config.remotes then
- -- No default_remotes configured, prompt if remotes exist
- local keys = {}
- for k in pairs(global_config.remotes) do
- table.insert(keys, k)
- end
- table.sort(keys)
- if #keys > 0 then
- local input = table.concat(keys, "\n")
- local cmd = "echo '" .. input .. "' | gum choose --no-limit"
- local output, code = run_cmd(cmd)
- if code == 0 and output ~= "" then
- for line in output:gmatch("[^\n]+") do
- table.insert(selected_remotes, line)
- end
- end
- end
- end
+if _VERSION < "Lua 5.2" then
+ io.stderr:write("error: wt requires Lua 5.2 or later\n")
+ os.exit(1)
+end
- -- Create project structure
- local bare_path = project_path .. "/.bare"
- local output, code = run_cmd("mkdir -p " .. bare_path)
- if code ~= 0 then
- die("failed to create directory: " .. output, EXIT_SYSTEM_ERROR)
- end
+local exit = require("wt.exit")
+local EXIT_SUCCESS = exit.EXIT_SUCCESS
+local EXIT_USER_ERROR = exit.EXIT_USER_ERROR
+local EXIT_SYSTEM_ERROR = exit.EXIT_SYSTEM_ERROR
- output, code = run_cmd("git init --bare " .. bare_path)
- if code ~= 0 then
- die("failed to init bare repo: " .. output, EXIT_SYSTEM_ERROR)
- end
+local shell = require("wt.shell")
+local run_cmd = shell.run_cmd
+local run_cmd_silent = shell.run_cmd_silent
+local get_cwd = shell.get_cwd
+local die = shell.die
- -- Write .git file pointing to .bare
- local git_file_handle = io.open(project_path .. "/.git", "w")
- if not git_file_handle then
- die("failed to create .git file", EXIT_SYSTEM_ERROR)
- return
- end
- git_file_handle:write("gitdir: ./.bare\n")
- git_file_handle:close()
+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
- -- Add remotes
- local git_dir = bare_path
- for _, remote_name in ipairs(selected_remotes) do
- local template = global_config.remotes and global_config.remotes[remote_name]
- if template then
- local url = resolve_url_template(template, project_name)
- output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. url)
- if code ~= 0 then
- io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
- else
- -- Configure fetch refspec for the remote
- run_cmd(
- "GIT_DIR="
- .. git_dir
- .. " git config remote."
- .. remote_name
- .. ".fetch '+refs/heads/*:refs/remotes/"
- .. remote_name
- .. "/*'"
- )
- end
- else
- io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
- end
- end
+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 detect_cloned_default_branch = git_mod.detect_cloned_default_branch
+local parse_branch_remotes = git_mod.parse_branch_remotes
+local parse_worktree_list = git_mod.parse_worktree_list
- -- Detect default branch
- local default_branch = get_default_branch()
+local config_mod = require("wt.config")
+local resolve_url_template = config_mod.resolve_url_template
+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
- -- Load config for path style
- local style = global_config.branch_path_style or "nested"
- local separator = global_config.flat_separator
- local worktree_path = branch_to_path(project_path, default_branch, style, separator)
+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
- -- Create orphan worktree
- output, code =
- run_cmd("GIT_DIR=" .. git_dir .. " git worktree add --orphan -b " .. default_branch .. " -- " .. worktree_path)
- if code ~= 0 then
- die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
- end
+local help_mod = require("wt.help")
+local print_usage = help_mod.print_usage
+local show_command_help = help_mod.show_command_help
- -- Print summary
- print("Created project: " .. project_path)
- print("Default branch: " .. default_branch)
- print("Worktree: " .. worktree_path)
- if #selected_remotes > 0 then
- print("Remotes: " .. table.concat(selected_remotes, ", "))
- end
-end
+local fetch_mod = require("wt.cmd.fetch")
+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
+
+local new_mod = require("wt.cmd.new")
+local cmd_new = new_mod.cmd_new
+
+local clone_mod = require("wt.cmd.clone")
+local cmd_clone = clone_mod.cmd_clone
---List directory entries (excluding . and ..)
---@param path string