Detailed changes
@@ -1696,70 +1696,18 @@ 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.init"] = [[-- 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 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 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 config = require("wt.config")
-local clone_mod = require("wt.cmd.clone")
-local cmd_clone = clone_mod.cmd_clone
+---@class wt.cmd.init
+local M = {}
---List directory entries (excluding . and ..)
---@param path string
@@ -1788,7 +1736,7 @@ local function is_dir(path)
return false
end
f:close()
- return run_cmd_silent("test -d " .. path)
+ return shell.run_cmd_silent("test -d " .. path)
end
---Check if path is a file (not directory)
@@ -1800,11 +1748,12 @@ local function is_file(path)
return false
end
f:close()
- return run_cmd_silent("test -f " .. path)
+ return shell.run_cmd_silent("test -f " .. path)
end
+---Convert existing git repository to wt bare structure
---@param args string[]
-local function cmd_init(args)
+function M.cmd_init(args)
-- Parse arguments
local dry_run = false
local skip_confirm = false
@@ -1814,13 +1763,13 @@ local function cmd_init(args)
elseif a == "-y" or a == "--yes" then
skip_confirm = true
else
- die("unexpected argument: " .. a)
+ shell.die("unexpected argument: " .. a)
end
end
- local cwd = get_cwd()
+ local cwd = shell.get_cwd()
if not cwd then
- die("failed to get current directory", EXIT_SYSTEM_ERROR)
+ shell.die("failed to get current directory", exit.EXIT_SYSTEM_ERROR)
return
end
@@ -1839,14 +1788,14 @@ local function cmd_init(args)
if is_file(git_path) and content and content:match("gitdir:%s*%.?/?%.bare") then
if bare_exists then
print("Already using wt bare structure")
- os.exit(EXIT_SUCCESS)
+ os.exit(exit.EXIT_SUCCESS)
end
end
-- Check if .git is a file pointing elsewhere (inside a worktree)
if is_file(git_path) and content and content:match("^gitdir:") then
-- It's a worktree, not project root
- die("inside a worktree; run from project root or use 'wt c' to clone fresh")
+ shell.die("inside a worktree; run from project root or use 'wt c' to clone fresh")
end
end
@@ -1856,9 +1805,9 @@ local function cmd_init(args)
if not git_dir_exists then
-- Case 5: No .git at all, or bare repo without .git dir
if bare_exists then
- die("found .bare/ but no .git; manually create .git file with 'gitdir: ./.bare'")
+ shell.die("found .bare/ but no .git; manually create .git file with 'gitdir: ./.bare'")
end
- die("not a git repository (no .git found)")
+ shell.die("not a git repository (no .git found)")
end
-- Now we have a .git directory
@@ -1876,18 +1825,18 @@ local function cmd_init(args)
io.stderr:write(" " .. wt .. "\n")
end
end
- os.exit(EXIT_USER_ERROR)
+ os.exit(exit.EXIT_USER_ERROR)
end
-- Case 4: Normal clone (.git/ directory, no worktrees)
-- Check for uncommitted changes
- local status_out = run_cmd("git status --porcelain")
+ local status_out = shell.run_cmd("git status --porcelain")
if status_out ~= "" then
- die("uncommitted changes; commit or stash before converting")
+ shell.die("uncommitted changes; commit or stash before converting")
end
-- Detect default branch
- local default_branch = detect_cloned_default_branch(git_path)
+ local default_branch = git.detect_cloned_default_branch(git_path)
-- Warnings
local warnings = {}
@@ -1898,7 +1847,7 @@ local function cmd_init(args)
end
-- Check for nested .git directories (excluding the main one)
- local nested_git_output, _ = run_cmd("find " .. cwd .. " -mindepth 2 -name .git -type d 2>/dev/null")
+ local nested_git_output, _ = shell.run_cmd("find " .. cwd .. " -mindepth 2 -name .git -type d 2>/dev/null")
if nested_git_output ~= "" then
table.insert(warnings, "nested .git directories found; these may cause issues")
end
@@ -1913,10 +1862,10 @@ local function cmd_init(args)
end
-- Load global config for path style
- local global_config = load_global_config()
+ local global_config = config.load_global_config()
local style = global_config.branch_path_style or "nested"
local separator = global_config.flat_separator
- local worktree_path = branch_to_path(cwd, default_branch, style, separator)
+ local worktree_path = path_mod.branch_to_path(cwd, default_branch, style, separator)
if dry_run then
print("Dry run - planned actions:")
@@ -1937,7 +1886,7 @@ local function cmd_init(args)
print(" ⚠ " .. w)
end
end
- os.exit(EXIT_SUCCESS)
+ os.exit(exit.EXIT_SUCCESS)
end
-- Show warnings
@@ -1955,22 +1904,22 @@ local function cmd_init(args)
local confirm_code = os.execute("gum confirm '" .. confirm_msg .. "'")
if confirm_code ~= true then
print("Aborted")
- os.exit(EXIT_USER_ERROR)
+ os.exit(exit.EXIT_USER_ERROR)
end
end
-- Step 1: Move .git to .bare
- local output, code = run_cmd("mv " .. git_path .. " " .. bare_path)
+ local output, code = shell.run_cmd("mv " .. git_path .. " " .. bare_path)
if code ~= 0 then
- die("failed to move .git to .bare: " .. output, EXIT_SYSTEM_ERROR)
+ shell.die("failed to move .git to .bare: " .. output, exit.EXIT_SYSTEM_ERROR)
end
-- Step 2: Write .git file
local git_file_handle = io.open(git_path, "w")
if not git_file_handle then
-- Try to recover
- run_cmd("mv " .. bare_path .. " " .. git_path)
- die("failed to create .git file", EXIT_SYSTEM_ERROR)
+ shell.run_cmd("mv " .. bare_path .. " " .. git_path)
+ shell.die("failed to create .git file", exit.EXIT_SYSTEM_ERROR)
return
end
git_file_handle:write("gitdir: ./.bare\n")
@@ -1978,18 +1927,19 @@ local function cmd_init(args)
-- Step 3: Detach HEAD so branch can be checked out in worktree
-- Point bare repo's HEAD to a placeholder so main branch can be used by worktree
- run_cmd("GIT_DIR=" .. bare_path .. " git symbolic-ref HEAD refs/heads/__wt_detached_placeholder__")
+ shell.run_cmd("GIT_DIR=" .. bare_path .. " git symbolic-ref HEAD refs/heads/__wt_detached_placeholder__")
-- Step 4: Create worktree for default branch
- output, code = run_cmd("GIT_DIR=" .. bare_path .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
+ output, code =
+ shell.run_cmd("GIT_DIR=" .. bare_path .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
if code ~= 0 then
- die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
+ shell.die("failed to create worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
end
-- Step 5: Remove orphaned files from root
for _, item in ipairs(orphaned) do
local item_path = cwd .. "/" .. item
- output, code = run_cmd("rm -rf " .. item_path)
+ output, code = shell.run_cmd("rm -rf " .. item_path)
if code ~= 0 then
io.stderr:write("warning: failed to remove " .. item .. ": " .. output .. "\n")
end
@@ -2004,6 +1954,73 @@ local function cmd_init(args)
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
+
+local exit = require("wt.exit")
+local EXIT_SUCCESS = exit.EXIT_SUCCESS
+
+local shell = require("wt.shell")
+local run_cmd = shell.run_cmd
+local run_cmd_silent = shell.run_cmd_silent
+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
+
+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 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 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
+
+local init_mod = require("wt.cmd.init")
+local cmd_init = init_mod.cmd_init
+
-- Main entry point
local function main()
@@ -11,13 +11,10 @@ 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
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 path_mod = require("wt.path")
@@ -30,7 +27,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 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
@@ -68,248 +64,8 @@ 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
----@return string[]
-local function list_dir(path)
- local entries = {}
- local handle = io.popen("ls -A " .. path .. " 2>/dev/null")
- if not handle then
- return entries
- end
- for line in handle:lines() do
- if line ~= "" then
- table.insert(entries, line)
- end
- end
- handle:close()
- return entries
-end
-
----Check if path is a directory
----@param path string
----@return boolean
-local function is_dir(path)
- local f = io.open(path, "r")
- if not f then
- return false
- end
- f:close()
- return run_cmd_silent("test -d " .. path)
-end
-
----Check if path is a file (not directory)
----@param path string
----@return boolean
-local function is_file(path)
- local f = io.open(path, "r")
- if not f then
- return false
- end
- f:close()
- return run_cmd_silent("test -f " .. path)
-end
-
----@param args string[]
-local function cmd_init(args)
- -- Parse arguments
- local dry_run = false
- local skip_confirm = false
- for _, a in ipairs(args) do
- if a == "--dry-run" then
- dry_run = true
- elseif a == "-y" or a == "--yes" then
- skip_confirm = true
- else
- die("unexpected argument: " .. a)
- end
- end
-
- local cwd = get_cwd()
- if not cwd then
- die("failed to get current directory", EXIT_SYSTEM_ERROR)
- return
- end
-
- -- Case 1: Already wt-managed (.git file pointing to .bare + .bare/ exists)
- local git_path = cwd .. "/.git"
- local bare_path = cwd .. "/.bare"
-
- local bare_exists = is_dir(bare_path)
- local git_file = io.open(git_path, "r")
-
- if git_file then
- local content = git_file:read("*a")
- git_file:close()
-
- -- Check if it's a file (not directory) pointing to .bare
- if is_file(git_path) and content and content:match("gitdir:%s*%.?/?%.bare") then
- if bare_exists then
- print("Already using wt bare structure")
- os.exit(EXIT_SUCCESS)
- end
- end
-
- -- Check if .git is a file pointing elsewhere (inside a worktree)
- if is_file(git_path) and content and content:match("^gitdir:") then
- -- It's a worktree, not project root
- die("inside a worktree; run from project root or use 'wt c' to clone fresh")
- end
- end
-
- -- Check for .git directory
- local git_dir_exists = is_dir(git_path)
-
- if not git_dir_exists then
- -- Case 5: No .git at all, or bare repo without .git dir
- if bare_exists then
- die("found .bare/ but no .git; manually create .git file with 'gitdir: ./.bare'")
- end
- die("not a git repository (no .git found)")
- end
-
- -- Now we have a .git directory
- -- Case 3: Existing worktree setup (.git/worktrees/ exists)
- local worktrees_path = git_path .. "/worktrees"
- if is_dir(worktrees_path) then
- local worktrees = list_dir(worktrees_path)
- io.stderr:write("error: repository already uses git worktrees\n")
- io.stderr:write("\n")
- io.stderr:write("Converting an existing worktree setup is complex and error-prone.\n")
- io.stderr:write("Recommend: clone fresh with 'wt c <url>' and recreate worktrees.\n")
- if #worktrees > 0 then
- io.stderr:write("\nExisting worktrees:\n")
- for _, wt in ipairs(worktrees) do
- io.stderr:write(" " .. wt .. "\n")
- end
- end
- os.exit(EXIT_USER_ERROR)
- end
-
- -- Case 4: Normal clone (.git/ directory, no worktrees)
- -- Check for uncommitted changes
- local status_out = run_cmd("git status --porcelain")
- if status_out ~= "" then
- die("uncommitted changes; commit or stash before converting")
- end
-
- -- Detect default branch
- local default_branch = detect_cloned_default_branch(git_path)
-
- -- Warnings
- local warnings = {}
-
- -- Check for submodules
- if is_file(cwd .. "/.gitmodules") then
- table.insert(warnings, "submodules detected (.gitmodules exists); verify they work after conversion")
- end
-
- -- Check for nested .git directories (excluding the main one)
- local nested_git_output, _ = run_cmd("find " .. cwd .. " -mindepth 2 -name .git -type d 2>/dev/null")
- if nested_git_output ~= "" then
- table.insert(warnings, "nested .git directories found; these may cause issues")
- end
-
- -- Find orphaned files (files in root that will be deleted)
- local all_entries = list_dir(cwd)
- local orphaned = {}
- for _, entry in ipairs(all_entries) do
- if entry ~= ".git" and entry ~= ".bare" then
- table.insert(orphaned, entry)
- end
- end
-
- -- Load global 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
- local worktree_path = branch_to_path(cwd, default_branch, style, separator)
-
- if dry_run then
- print("Dry run - planned actions:")
- print("")
- print("1. Move .git/ to .bare/")
- print("2. Create .git file pointing to .bare/")
- print("3. Create worktree: " .. worktree_path .. " (" .. default_branch .. ")")
- if #orphaned > 0 then
- print("4. Remove " .. #orphaned .. " orphaned items from root:")
- for _, item in ipairs(orphaned) do
- print(" - " .. item)
- end
- end
- if #warnings > 0 then
- print("")
- print("Warnings:")
- for _, w in ipairs(warnings) do
- print(" ⚠ " .. w)
- end
- end
- os.exit(EXIT_SUCCESS)
- end
-
- -- Show warnings
- for _, w in ipairs(warnings) do
- io.stderr:write("warning: " .. w .. "\n")
- end
-
- -- Confirm with gum (unless -y/--yes)
- if not skip_confirm then
- local confirm_msg = "Convert to wt bare structure? This will move .git to .bare"
- if #orphaned > 0 then
- confirm_msg = confirm_msg .. " and remove " .. #orphaned .. " items from root"
- end
-
- local confirm_code = os.execute("gum confirm '" .. confirm_msg .. "'")
- if confirm_code ~= true then
- print("Aborted")
- os.exit(EXIT_USER_ERROR)
- end
- end
-
- -- Step 1: Move .git to .bare
- local output, code = run_cmd("mv " .. git_path .. " " .. bare_path)
- if code ~= 0 then
- die("failed to move .git to .bare: " .. output, EXIT_SYSTEM_ERROR)
- end
-
- -- Step 2: Write .git file
- local git_file_handle = io.open(git_path, "w")
- if not git_file_handle then
- -- Try to recover
- run_cmd("mv " .. bare_path .. " " .. git_path)
- die("failed to create .git file", EXIT_SYSTEM_ERROR)
- return
- end
- git_file_handle:write("gitdir: ./.bare\n")
- git_file_handle:close()
-
- -- Step 3: Detach HEAD so branch can be checked out in worktree
- -- Point bare repo's HEAD to a placeholder so main branch can be used by worktree
- run_cmd("GIT_DIR=" .. bare_path .. " git symbolic-ref HEAD refs/heads/__wt_detached_placeholder__")
-
- -- Step 4: Create worktree for default branch
- output, code = run_cmd("GIT_DIR=" .. bare_path .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
- if code ~= 0 then
- die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
- end
-
- -- Step 5: Remove orphaned files from root
- for _, item in ipairs(orphaned) do
- local item_path = cwd .. "/" .. item
- output, code = run_cmd("rm -rf " .. item_path)
- if code ~= 0 then
- io.stderr:write("warning: failed to remove " .. item .. ": " .. output .. "\n")
- end
- end
-
- -- Summary
- print("Converted to wt bare structure")
- print("Bare repo: " .. bare_path)
- print("Worktree: " .. worktree_path)
- if #orphaned > 0 then
- print("Removed: " .. #orphaned .. " items from root")
- end
-end
+local init_mod = require("wt.cmd.init")
+local cmd_init = init_mod.cmd_init
-- Main entry point
@@ -0,0 +1,259 @@
+-- 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")
+
+---@class wt.cmd.init
+local M = {}
+
+---List directory entries (excluding . and ..)
+---@param path string
+---@return string[]
+local function list_dir(path)
+ local entries = {}
+ local handle = io.popen("ls -A " .. path .. " 2>/dev/null")
+ if not handle then
+ return entries
+ end
+ for line in handle:lines() do
+ if line ~= "" then
+ table.insert(entries, line)
+ end
+ end
+ handle:close()
+ return entries
+end
+
+---Check if path is a directory
+---@param path string
+---@return boolean
+local function is_dir(path)
+ local f = io.open(path, "r")
+ if not f then
+ return false
+ end
+ f:close()
+ return shell.run_cmd_silent("test -d " .. path)
+end
+
+---Check if path is a file (not directory)
+---@param path string
+---@return boolean
+local function is_file(path)
+ local f = io.open(path, "r")
+ if not f then
+ return false
+ end
+ f:close()
+ return shell.run_cmd_silent("test -f " .. path)
+end
+
+---Convert existing git repository to wt bare structure
+---@param args string[]
+function M.cmd_init(args)
+ -- Parse arguments
+ local dry_run = false
+ local skip_confirm = false
+ for _, a in ipairs(args) do
+ if a == "--dry-run" then
+ dry_run = true
+ elseif a == "-y" or a == "--yes" then
+ skip_confirm = true
+ else
+ shell.die("unexpected argument: " .. a)
+ end
+ end
+
+ local cwd = shell.get_cwd()
+ if not cwd then
+ shell.die("failed to get current directory", exit.EXIT_SYSTEM_ERROR)
+ return
+ end
+
+ -- Case 1: Already wt-managed (.git file pointing to .bare + .bare/ exists)
+ local git_path = cwd .. "/.git"
+ local bare_path = cwd .. "/.bare"
+
+ local bare_exists = is_dir(bare_path)
+ local git_file = io.open(git_path, "r")
+
+ if git_file then
+ local content = git_file:read("*a")
+ git_file:close()
+
+ -- Check if it's a file (not directory) pointing to .bare
+ if is_file(git_path) and content and content:match("gitdir:%s*%.?/?%.bare") then
+ if bare_exists then
+ print("Already using wt bare structure")
+ os.exit(exit.EXIT_SUCCESS)
+ end
+ end
+
+ -- Check if .git is a file pointing elsewhere (inside a worktree)
+ if is_file(git_path) and content and content:match("^gitdir:") then
+ -- It's a worktree, not project root
+ shell.die("inside a worktree; run from project root or use 'wt c' to clone fresh")
+ end
+ end
+
+ -- Check for .git directory
+ local git_dir_exists = is_dir(git_path)
+
+ if not git_dir_exists then
+ -- Case 5: No .git at all, or bare repo without .git dir
+ if bare_exists then
+ shell.die("found .bare/ but no .git; manually create .git file with 'gitdir: ./.bare'")
+ end
+ shell.die("not a git repository (no .git found)")
+ end
+
+ -- Now we have a .git directory
+ -- Case 3: Existing worktree setup (.git/worktrees/ exists)
+ local worktrees_path = git_path .. "/worktrees"
+ if is_dir(worktrees_path) then
+ local worktrees = list_dir(worktrees_path)
+ io.stderr:write("error: repository already uses git worktrees\n")
+ io.stderr:write("\n")
+ io.stderr:write("Converting an existing worktree setup is complex and error-prone.\n")
+ io.stderr:write("Recommend: clone fresh with 'wt c <url>' and recreate worktrees.\n")
+ if #worktrees > 0 then
+ io.stderr:write("\nExisting worktrees:\n")
+ for _, wt in ipairs(worktrees) do
+ io.stderr:write(" " .. wt .. "\n")
+ end
+ end
+ os.exit(exit.EXIT_USER_ERROR)
+ end
+
+ -- Case 4: Normal clone (.git/ directory, no worktrees)
+ -- Check for uncommitted changes
+ local status_out = shell.run_cmd("git status --porcelain")
+ if status_out ~= "" then
+ shell.die("uncommitted changes; commit or stash before converting")
+ end
+
+ -- Detect default branch
+ local default_branch = git.detect_cloned_default_branch(git_path)
+
+ -- Warnings
+ local warnings = {}
+
+ -- Check for submodules
+ if is_file(cwd .. "/.gitmodules") then
+ table.insert(warnings, "submodules detected (.gitmodules exists); verify they work after conversion")
+ end
+
+ -- Check for nested .git directories (excluding the main one)
+ local nested_git_output, _ = shell.run_cmd("find " .. cwd .. " -mindepth 2 -name .git -type d 2>/dev/null")
+ if nested_git_output ~= "" then
+ table.insert(warnings, "nested .git directories found; these may cause issues")
+ end
+
+ -- Find orphaned files (files in root that will be deleted)
+ local all_entries = list_dir(cwd)
+ local orphaned = {}
+ for _, entry in ipairs(all_entries) do
+ if entry ~= ".git" and entry ~= ".bare" then
+ table.insert(orphaned, entry)
+ end
+ end
+
+ -- Load global 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
+ local worktree_path = path_mod.branch_to_path(cwd, default_branch, style, separator)
+
+ if dry_run then
+ print("Dry run - planned actions:")
+ print("")
+ print("1. Move .git/ to .bare/")
+ print("2. Create .git file pointing to .bare/")
+ print("3. Create worktree: " .. worktree_path .. " (" .. default_branch .. ")")
+ if #orphaned > 0 then
+ print("4. Remove " .. #orphaned .. " orphaned items from root:")
+ for _, item in ipairs(orphaned) do
+ print(" - " .. item)
+ end
+ end
+ if #warnings > 0 then
+ print("")
+ print("Warnings:")
+ for _, w in ipairs(warnings) do
+ print(" ⚠ " .. w)
+ end
+ end
+ os.exit(exit.EXIT_SUCCESS)
+ end
+
+ -- Show warnings
+ for _, w in ipairs(warnings) do
+ io.stderr:write("warning: " .. w .. "\n")
+ end
+
+ -- Confirm with gum (unless -y/--yes)
+ if not skip_confirm then
+ local confirm_msg = "Convert to wt bare structure? This will move .git to .bare"
+ if #orphaned > 0 then
+ confirm_msg = confirm_msg .. " and remove " .. #orphaned .. " items from root"
+ end
+
+ local confirm_code = os.execute("gum confirm '" .. confirm_msg .. "'")
+ if confirm_code ~= true then
+ print("Aborted")
+ os.exit(exit.EXIT_USER_ERROR)
+ end
+ end
+
+ -- Step 1: Move .git to .bare
+ local output, code = shell.run_cmd("mv " .. git_path .. " " .. bare_path)
+ if code ~= 0 then
+ shell.die("failed to move .git to .bare: " .. output, exit.EXIT_SYSTEM_ERROR)
+ end
+
+ -- Step 2: Write .git file
+ local git_file_handle = io.open(git_path, "w")
+ if not git_file_handle then
+ -- Try to recover
+ shell.run_cmd("mv " .. bare_path .. " " .. git_path)
+ shell.die("failed to create .git file", exit.EXIT_SYSTEM_ERROR)
+ return
+ end
+ git_file_handle:write("gitdir: ./.bare\n")
+ git_file_handle:close()
+
+ -- Step 3: Detach HEAD so branch can be checked out in worktree
+ -- Point bare repo's HEAD to a placeholder so main branch can be used by worktree
+ shell.run_cmd("GIT_DIR=" .. bare_path .. " git symbolic-ref HEAD refs/heads/__wt_detached_placeholder__")
+
+ -- Step 4: Create worktree for default branch
+ output, code =
+ shell.run_cmd("GIT_DIR=" .. bare_path .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
+ if code ~= 0 then
+ shell.die("failed to create worktree: " .. output, exit.EXIT_SYSTEM_ERROR)
+ end
+
+ -- Step 5: Remove orphaned files from root
+ for _, item in ipairs(orphaned) do
+ local item_path = cwd .. "/" .. item
+ output, code = shell.run_cmd("rm -rf " .. item_path)
+ if code ~= 0 then
+ io.stderr:write("warning: failed to remove " .. item .. ": " .. output .. "\n")
+ end
+ end
+
+ -- Summary
+ print("Converted to wt bare structure")
+ print("Bare repo: " .. bare_path)
+ print("Worktree: " .. worktree_path)
+ if #orphaned > 0 then
+ print("Removed: " .. #orphaned .. " items from root")
+ end
+end
+
+return M