1#!/usr/bin/env lua
2
3-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
4--
5-- SPDX-License-Identifier: GPL-3.0-or-later
6
7if _VERSION < "Lua 5.2" then
8 io.stderr:write("error: wt requires Lua 5.2 or later\n")
9 os.exit(1)
10end
11
12local exit = require("wt.exit")
13local EXIT_SUCCESS = exit.EXIT_SUCCESS
14local EXIT_USER_ERROR = exit.EXIT_USER_ERROR
15local EXIT_SYSTEM_ERROR = exit.EXIT_SYSTEM_ERROR
16
17local shell = require("wt.shell")
18local run_cmd = shell.run_cmd
19local run_cmd_silent = shell.run_cmd_silent
20local get_cwd = shell.get_cwd
21local die = shell.die
22
23local path_mod = require("wt.path")
24local branch_to_path = path_mod.branch_to_path
25local split_path = path_mod.split_path
26local relative_path = path_mod.relative_path
27local path_inside = path_mod.path_inside
28local escape_pattern = path_mod.escape_pattern
29
30local git_mod = require("wt.git")
31local find_project_root = git_mod.find_project_root
32local detect_source_worktree = git_mod.detect_source_worktree
33local branch_exists_local = git_mod.branch_exists_local
34local find_branch_remotes = git_mod.find_branch_remotes
35local detect_cloned_default_branch = git_mod.detect_cloned_default_branch
36local get_default_branch = git_mod.get_default_branch
37local parse_branch_remotes = git_mod.parse_branch_remotes
38local parse_worktree_list = git_mod.parse_worktree_list
39local branch_checked_out_at = git_mod.branch_checked_out_at
40
41local config_mod = require("wt.config")
42local resolve_url_template = config_mod.resolve_url_template
43local extract_project_name = config_mod.extract_project_name
44local load_global_config = config_mod.load_global_config
45local load_project_config = config_mod.load_project_config
46
47---Print usage information
48local function print_usage()
49 print("wt - git worktree manager")
50 print("")
51 print("Usage: wt <command> [options]")
52 print("")
53 print("Commands:")
54 print(" c <url> [--remote name]... [--own] Clone into bare worktree structure")
55 print(" n <project-name> [--remote name]... Initialize fresh project")
56 print(" a <branch> [-b [<start-point>]] Add worktree with optional hooks")
57 print(" r <branch> [-b] [-f] Remove worktree, optionally delete branch")
58 print(" l List worktrees with status")
59 print(" f Fetch all remotes")
60 print(" init [--dry-run] [-y] Convert existing repo to bare structure")
61 print(" help Show this help message")
62end
63
64-- Per-command help text (using table.concat for performance)
65local COMMAND_HELP = {
66 c = table.concat({
67 "wt c <url> [--remote name]... [--own]",
68 "",
69 "Clone a repository into bare worktree structure.",
70 "",
71 "Arguments:",
72 " <url> Git URL to clone",
73 "",
74 "Options:",
75 " --remote <name> Add configured remote from ~/.config/wt/config.lua",
76 " Can be specified multiple times",
77 " --own Treat as your own project: first remote becomes 'origin'",
78 " (default: 'origin' renamed to 'upstream', your remotes added)",
79 "",
80 "Examples:",
81 " wt c https://github.com/user/repo.git",
82 " wt c git@github.com:user/repo.git --remote github --own",
83 }, "\n"),
84
85 n = table.concat({
86 "wt n <project-name> [--remote name]...",
87 "",
88 "Initialize a fresh project with bare worktree structure.",
89 "",
90 "Arguments:",
91 " <project-name> Name of the new project directory",
92 "",
93 "Options:",
94 " --remote <name> Add configured remote from ~/.config/wt/config.lua",
95 " Can be specified multiple times",
96 "",
97 "Examples:",
98 " wt n my-project",
99 " wt n my-project --remote github --remote gitlab",
100 }, "\n"),
101
102 a = table.concat({
103 "wt a <branch> [-b [<start-point>]]",
104 "",
105 "Add a worktree for a branch.",
106 "",
107 "Arguments:",
108 " <branch> Branch name to checkout or create",
109 "",
110 "Options:",
111 " -b Create a new branch",
112 " <start-point> Base commit/branch for new branch (only with -b)",
113 "",
114 "If run from inside an existing worktree, hooks from .wt.lua will be applied.",
115 "",
116 "Examples:",
117 " wt a main # Checkout existing branch",
118 " wt a feature/new -b # Create new branch from HEAD",
119 " wt a feature/new -b main # Create new branch from main",
120 }, "\n"),
121
122 r = table.concat({
123 "wt r <branch> [-b] [-f]",
124 "",
125 "Remove a worktree.",
126 "",
127 "Arguments:",
128 " <branch> Branch name of worktree to remove",
129 "",
130 "Options:",
131 " -b Also delete the branch after removing worktree",
132 " -f Force removal even with uncommitted changes",
133 "",
134 "Examples:",
135 " wt r feature/old # Remove worktree, keep branch",
136 " wt r feature/old -b # Remove worktree and delete branch",
137 " wt r feature/old -f # Force remove with uncommitted changes",
138 }, "\n"),
139
140 l = table.concat({
141 "wt l",
142 "",
143 "List all worktrees with status information.",
144 "",
145 "Displays a table showing:",
146 " - Branch name",
147 " - Relative path from project root",
148 " - Commit status (ahead/behind remote)",
149 " - Working tree status (clean/dirty)",
150 }, "\n"),
151
152 f = table.concat({
153 "wt f",
154 "",
155 "Fetch from all configured remotes.",
156 "",
157 "Runs 'git fetch --all' in the bare repository.",
158 }, "\n"),
159
160 init = table.concat({
161 "wt init [--dry-run] [-y]",
162 "",
163 "Convert an existing git repository to bare worktree structure.",
164 "",
165 "Options:",
166 " --dry-run Show what would be done without making changes",
167 " -y Skip confirmation prompt",
168 "",
169 "This command:",
170 " 1. Moves .git/ to .bare/",
171 " 2. Creates .git file pointing to .bare/",
172 " 3. Creates a worktree for the current branch",
173 " 4. Removes orphaned files from project root",
174 }, "\n"),
175}
176
177---Show help for a specific command
178---@param cmd string
179local function show_command_help(cmd)
180 local help = COMMAND_HELP[cmd]
181 if help then
182 print(help)
183 else
184 print_usage()
185 end
186 os.exit(EXIT_SUCCESS)
187end
188
189---Load hook permissions from ~/.local/share/wt/hook-dirs.lua
190---@return table<string, boolean>
191local function load_hook_permissions()
192 local home = os.getenv("HOME")
193 if not home then
194 return {}
195 end
196 local path = home .. "/.local/share/wt/hook-dirs.lua"
197 local f = io.open(path, "r")
198 if not f then
199 return {}
200 end
201 local content = f:read("*a")
202 f:close()
203 local chunk = load("return " .. content, path, "t", {})
204 if not chunk then
205 return {}
206 end
207 local ok, result = pcall(chunk)
208 if ok and type(result) == "table" then
209 return result
210 end
211 return {}
212end
213
214---Save hook permissions to ~/.local/share/wt/hook-dirs.lua
215---@param perms table<string, boolean>
216local function save_hook_permissions(perms)
217 local home = os.getenv("HOME")
218 if not home then
219 return
220 end
221 local dir = home .. "/.local/share/wt"
222 run_cmd_silent("mkdir -p " .. dir)
223 local path = dir .. "/hook-dirs.lua"
224 local f = io.open(path, "w")
225 if not f then
226 return
227 end
228 f:write("{\n")
229 for k, v in pairs(perms) do
230 f:write('\t["' .. k .. '"] = ' .. tostring(v) .. ",\n")
231 end
232 f:write("}\n")
233 f:close()
234end
235
236---Summarize hooks for confirmation prompt
237---@param hooks {copy?: string[], symlink?: string[], run?: string[]}
238---@return string
239local function summarize_hooks(hooks)
240 local parts = {}
241 if hooks.copy and #hooks.copy > 0 then
242 local items = {}
243 for i = 1, math.min(3, #hooks.copy) do
244 table.insert(items, hooks.copy[i])
245 end
246 local suffix = #hooks.copy > 3 and " (+" .. (#hooks.copy - 3) .. " more)" or ""
247 table.insert(parts, "copy: " .. table.concat(items, ", ") .. suffix)
248 end
249 if hooks.symlink and #hooks.symlink > 0 then
250 local items = {}
251 for i = 1, math.min(3, #hooks.symlink) do
252 table.insert(items, hooks.symlink[i])
253 end
254 local suffix = #hooks.symlink > 3 and " (+" .. (#hooks.symlink - 3) .. " more)" or ""
255 table.insert(parts, "symlink: " .. table.concat(items, ", ") .. suffix)
256 end
257 if hooks.run and #hooks.run > 0 then
258 local items = {}
259 for i = 1, math.min(3, #hooks.run) do
260 table.insert(items, hooks.run[i])
261 end
262 local suffix = #hooks.run > 3 and " (+" .. (#hooks.run - 3) .. " more)" or ""
263 table.insert(parts, "run: " .. table.concat(items, ", ") .. suffix)
264 end
265 return table.concat(parts, "; ")
266end
267
268---Check if hooks are allowed for a project, prompting if unknown
269---@param root string project root path
270---@param hooks {copy?: string[], symlink?: string[], run?: string[]}
271---@return boolean allowed
272local function check_hook_permission(root, hooks)
273 local perms = load_hook_permissions()
274 if perms[root] ~= nil then
275 return perms[root]
276 end
277
278 -- Prompt user
279 local summary = summarize_hooks(hooks)
280 local prompt = "Allow hooks for " .. root .. "?\\n" .. summary
281 local allowed = run_cmd_silent("gum confirm " .. "'" .. prompt:gsub("'", "'\\''") .. "'")
282
283 perms[root] = allowed
284 save_hook_permissions(perms)
285 return allowed
286end
287
288---Run hooks from .wt.lua config
289---@param source string source worktree path
290---@param target string target worktree path
291---@param hooks {copy?: string[], symlink?: string[], run?: string[]}
292---@param root string project root path
293local function run_hooks(source, target, hooks, root)
294 -- Check permission before running any hooks
295 if not check_hook_permission(root, hooks) then
296 io.stderr:write("hooks skipped (not allowed for this project)\n")
297 return
298 end
299
300 if hooks.copy then
301 for _, item in ipairs(hooks.copy) do
302 local src = source .. "/" .. item
303 local dst = target .. "/" .. item
304 -- Create parent directory if needed
305 local parent = dst:match("(.+)/[^/]+$")
306 if parent then
307 run_cmd_silent("mkdir -p " .. parent)
308 end
309 local _, code = run_cmd("cp -r " .. src .. " " .. dst)
310 if code ~= 0 then
311 io.stderr:write("warning: failed to copy " .. item .. "\n")
312 end
313 end
314 end
315 if hooks.symlink then
316 for _, item in ipairs(hooks.symlink) do
317 local src = source .. "/" .. item
318 local dst = target .. "/" .. item
319 -- Create parent directory if needed
320 local parent = dst:match("(.+)/[^/]+$")
321 if parent then
322 run_cmd_silent("mkdir -p " .. parent)
323 end
324 local _, code = run_cmd("ln -s " .. src .. " " .. dst)
325 if code ~= 0 then
326 io.stderr:write("warning: failed to symlink " .. item .. "\n")
327 end
328 end
329 end
330 if hooks.run then
331 for _, cmd in ipairs(hooks.run) do
332 local _, code = run_cmd("cd " .. target .. " && " .. cmd)
333 if code ~= 0 then
334 io.stderr:write("warning: hook command failed: " .. cmd .. "\n")
335 end
336 end
337 end
338end
339
340---@param args string[]
341local function cmd_clone(args)
342 -- Parse arguments: <url> [--remote name]... [--own]
343 local url = nil
344 ---@type string[]
345 local remote_flags = {}
346 local own = false
347
348 local i = 1
349 while i <= #args do
350 local a = args[i]
351 if a == "--remote" then
352 if not args[i + 1] then
353 die("--remote requires a name")
354 end
355 table.insert(remote_flags, args[i + 1])
356 i = i + 1
357 elseif a == "--own" then
358 own = true
359 elseif not url then
360 url = a
361 else
362 die("unexpected argument: " .. a)
363 end
364 i = i + 1
365 end
366
367 if not url then
368 die("usage: wt c <url> [--remote name]... [--own]")
369 return
370 end
371
372 -- Extract project name from URL
373 local project_name = extract_project_name(url)
374 if not project_name then
375 die("could not extract project name from URL: " .. url)
376 return
377 end
378
379 -- Check if project directory already exists
380 local cwd = get_cwd()
381 if not cwd then
382 die("failed to get current directory", EXIT_SYSTEM_ERROR)
383 end
384 local project_path = cwd .. "/" .. project_name
385 local check = io.open(project_path, "r")
386 if check then
387 check:close()
388 die("directory already exists: " .. project_path)
389 end
390
391 -- Clone bare repo
392 local bare_path = project_path .. "/.bare"
393 local output, code = run_cmd("git clone --bare " .. url .. " " .. bare_path)
394 if code ~= 0 then
395 die("failed to clone: " .. output, EXIT_SYSTEM_ERROR)
396 end
397
398 -- Write .git file pointing to .bare
399 local git_file_handle = io.open(project_path .. "/.git", "w")
400 if not git_file_handle then
401 die("failed to create .git file", EXIT_SYSTEM_ERROR)
402 return
403 end
404 git_file_handle:write("gitdir: ./.bare\n")
405 git_file_handle:close()
406
407 -- Detect default branch
408 local git_dir = bare_path
409 local default_branch = detect_cloned_default_branch(git_dir)
410
411 -- Load global config
412 local global_config = load_global_config()
413
414 -- Determine which remotes to use
415 ---@type string[]
416 local selected_remotes = {}
417
418 if #remote_flags > 0 then
419 selected_remotes = remote_flags
420 elseif global_config.default_remotes then
421 if type(global_config.default_remotes) == "table" then
422 selected_remotes = global_config.default_remotes
423 elseif global_config.default_remotes == "prompt" then
424 if global_config.remotes then
425 local keys = {}
426 for k in pairs(global_config.remotes) do
427 table.insert(keys, k)
428 end
429 table.sort(keys)
430 if #keys > 0 then
431 local input = table.concat(keys, "\n")
432 local choose_type = own and "" or " --no-limit"
433 local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
434 output, code = run_cmd(cmd)
435 if code == 0 and output ~= "" then
436 for line in output:gmatch("[^\n]+") do
437 table.insert(selected_remotes, line)
438 end
439 end
440 end
441 end
442 end
443 elseif global_config.remotes then
444 local keys = {}
445 for k in pairs(global_config.remotes) do
446 table.insert(keys, k)
447 end
448 table.sort(keys)
449 if #keys > 0 then
450 local input = table.concat(keys, "\n")
451 local choose_type = own and "" or " --no-limit"
452 local cmd = "echo '" .. input .. "' | gum choose" .. choose_type
453 output, code = run_cmd(cmd)
454 if code == 0 and output ~= "" then
455 for line in output:gmatch("[^\n]+") do
456 table.insert(selected_remotes, line)
457 end
458 end
459 end
460 end
461
462 -- Track configured remotes for summary
463 ---@type string[]
464 local configured_remotes = {}
465
466 if own then
467 -- User's own project: origin is their canonical remote
468 if #selected_remotes > 0 then
469 local first_remote = selected_remotes[1]
470 -- Rename origin to first remote
471 output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin " .. first_remote)
472 if code ~= 0 then
473 io.stderr:write("warning: failed to rename origin to " .. first_remote .. ": " .. output .. "\n")
474 else
475 -- Configure fetch refspec
476 run_cmd(
477 "GIT_DIR="
478 .. git_dir
479 .. " git config remote."
480 .. first_remote
481 .. ".fetch '+refs/heads/*:refs/remotes/"
482 .. first_remote
483 .. "/*'"
484 )
485 table.insert(configured_remotes, first_remote)
486 end
487
488 -- Add additional remotes and push to them
489 for j = 2, #selected_remotes do
490 local remote_name = selected_remotes[j]
491 local template = global_config.remotes and global_config.remotes[remote_name]
492 if template then
493 local remote_url = resolve_url_template(template, project_name)
494 output, code =
495 run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
496 if code ~= 0 then
497 io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
498 else
499 run_cmd(
500 "GIT_DIR="
501 .. git_dir
502 .. " git config remote."
503 .. remote_name
504 .. ".fetch '+refs/heads/*:refs/remotes/"
505 .. remote_name
506 .. "/*'"
507 )
508 -- Push to additional remotes
509 output, code =
510 run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
511 if code ~= 0 then
512 io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
513 end
514 table.insert(configured_remotes, remote_name)
515 end
516 else
517 io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
518 end
519 end
520 else
521 -- No remotes selected, keep origin as-is
522 run_cmd("GIT_DIR=" .. git_dir .. " git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'")
523 table.insert(configured_remotes, "origin")
524 end
525 else
526 -- Contributing to someone else's project
527 -- Rename origin to upstream
528 output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote rename origin upstream")
529 if code ~= 0 then
530 io.stderr:write("warning: failed to rename origin to upstream: " .. output .. "\n")
531 else
532 run_cmd(
533 "GIT_DIR=" .. git_dir .. " git config remote.upstream.fetch '+refs/heads/*:refs/remotes/upstream/*'"
534 )
535 table.insert(configured_remotes, "upstream")
536 end
537
538 -- Add user's remotes and push to each
539 for _, remote_name in ipairs(selected_remotes) do
540 local template = global_config.remotes and global_config.remotes[remote_name]
541 if template then
542 local remote_url = resolve_url_template(template, project_name)
543 output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. remote_url)
544 if code ~= 0 then
545 io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
546 else
547 run_cmd(
548 "GIT_DIR="
549 .. git_dir
550 .. " git config remote."
551 .. remote_name
552 .. ".fetch '+refs/heads/*:refs/remotes/"
553 .. remote_name
554 .. "/*'"
555 )
556 -- Push to this remote
557 output, code =
558 run_cmd("GIT_DIR=" .. git_dir .. " git push " .. remote_name .. " " .. default_branch)
559 if code ~= 0 then
560 io.stderr:write("warning: failed to push to " .. remote_name .. ": " .. output .. "\n")
561 end
562 table.insert(configured_remotes, remote_name)
563 end
564 else
565 io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
566 end
567 end
568 end
569
570 -- Fetch all remotes
571 run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all")
572
573 -- Load config for path style
574 local style = global_config.branch_path_style or "nested"
575 local separator = global_config.flat_separator
576 local worktree_path = branch_to_path(project_path, default_branch, style, separator)
577
578 -- Create initial worktree
579 output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
580 if code ~= 0 then
581 die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
582 end
583
584 -- Print summary
585 print("Created project: " .. project_path)
586 print("Default branch: " .. default_branch)
587 print("Worktree: " .. worktree_path)
588 if #configured_remotes > 0 then
589 print("Remotes: " .. table.concat(configured_remotes, ", "))
590 end
591end
592
593---@param args string[]
594local function cmd_new(args)
595 -- Parse arguments: <project-name> [--remote name]...
596 local project_name = nil
597 ---@type string[]
598 local remote_flags = {}
599
600 local i = 1
601 while i <= #args do
602 local a = args[i]
603 if a == "--remote" then
604 if not args[i + 1] then
605 die("--remote requires a name")
606 end
607 table.insert(remote_flags, args[i + 1])
608 i = i + 1
609 elseif not project_name then
610 project_name = a
611 else
612 die("unexpected argument: " .. a)
613 end
614 i = i + 1
615 end
616
617 if not project_name then
618 die("usage: wt n <project-name> [--remote name]...")
619 return
620 end
621
622 -- Check if project directory already exists
623 local cwd = get_cwd()
624 if not cwd then
625 die("failed to get current directory", EXIT_SYSTEM_ERROR)
626 end
627 local project_path = cwd .. "/" .. project_name
628 local check = io.open(project_path, "r")
629 if check then
630 check:close()
631 die("directory already exists: " .. project_path)
632 end
633
634 -- Load global config
635 local global_config = load_global_config()
636
637 -- Determine which remotes to use
638 ---@type string[]
639 local selected_remotes = {}
640
641 if #remote_flags > 0 then
642 -- Use explicitly provided remotes
643 selected_remotes = remote_flags
644 elseif global_config.default_remotes then
645 if type(global_config.default_remotes) == "table" then
646 selected_remotes = global_config.default_remotes
647 elseif global_config.default_remotes == "prompt" then
648 -- Prompt with gum choose
649 if global_config.remotes then
650 local keys = {}
651 for k in pairs(global_config.remotes) do
652 table.insert(keys, k)
653 end
654 table.sort(keys)
655 if #keys > 0 then
656 local input = table.concat(keys, "\n")
657 local cmd = "echo '" .. input .. "' | gum choose --no-limit"
658 local output, code = run_cmd(cmd)
659 if code == 0 and output ~= "" then
660 for line in output:gmatch("[^\n]+") do
661 table.insert(selected_remotes, line)
662 end
663 end
664 end
665 end
666 end
667 elseif global_config.remotes then
668 -- No default_remotes configured, prompt if remotes exist
669 local keys = {}
670 for k in pairs(global_config.remotes) do
671 table.insert(keys, k)
672 end
673 table.sort(keys)
674 if #keys > 0 then
675 local input = table.concat(keys, "\n")
676 local cmd = "echo '" .. input .. "' | gum choose --no-limit"
677 local output, code = run_cmd(cmd)
678 if code == 0 and output ~= "" then
679 for line in output:gmatch("[^\n]+") do
680 table.insert(selected_remotes, line)
681 end
682 end
683 end
684 end
685
686 -- Create project structure
687 local bare_path = project_path .. "/.bare"
688 local output, code = run_cmd("mkdir -p " .. bare_path)
689 if code ~= 0 then
690 die("failed to create directory: " .. output, EXIT_SYSTEM_ERROR)
691 end
692
693 output, code = run_cmd("git init --bare " .. bare_path)
694 if code ~= 0 then
695 die("failed to init bare repo: " .. output, EXIT_SYSTEM_ERROR)
696 end
697
698 -- Write .git file pointing to .bare
699 local git_file_handle = io.open(project_path .. "/.git", "w")
700 if not git_file_handle then
701 die("failed to create .git file", EXIT_SYSTEM_ERROR)
702 return
703 end
704 git_file_handle:write("gitdir: ./.bare\n")
705 git_file_handle:close()
706
707 -- Add remotes
708 local git_dir = bare_path
709 for _, remote_name in ipairs(selected_remotes) do
710 local template = global_config.remotes and global_config.remotes[remote_name]
711 if template then
712 local url = resolve_url_template(template, project_name)
713 output, code = run_cmd("GIT_DIR=" .. git_dir .. " git remote add " .. remote_name .. " " .. url)
714 if code ~= 0 then
715 io.stderr:write("warning: failed to add remote '" .. remote_name .. "': " .. output .. "\n")
716 else
717 -- Configure fetch refspec for the remote
718 run_cmd(
719 "GIT_DIR="
720 .. git_dir
721 .. " git config remote."
722 .. remote_name
723 .. ".fetch '+refs/heads/*:refs/remotes/"
724 .. remote_name
725 .. "/*'"
726 )
727 end
728 else
729 io.stderr:write("warning: remote '" .. remote_name .. "' not found in config\n")
730 end
731 end
732
733 -- Detect default branch
734 local default_branch = get_default_branch()
735
736 -- Load config for path style
737 local style = global_config.branch_path_style or "nested"
738 local separator = global_config.flat_separator
739 local worktree_path = branch_to_path(project_path, default_branch, style, separator)
740
741 -- Create orphan worktree
742 output, code =
743 run_cmd("GIT_DIR=" .. git_dir .. " git worktree add --orphan -b " .. default_branch .. " -- " .. worktree_path)
744 if code ~= 0 then
745 die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
746 end
747
748 -- Print summary
749 print("Created project: " .. project_path)
750 print("Default branch: " .. default_branch)
751 print("Worktree: " .. worktree_path)
752 if #selected_remotes > 0 then
753 print("Remotes: " .. table.concat(selected_remotes, ", "))
754 end
755end
756
757---@param args string[]
758local function cmd_add(args)
759 -- Parse arguments: <branch> [-b [<start-point>]]
760 ---@type string|nil
761 local branch = nil
762 local create_branch = false
763 ---@type string|nil
764 local start_point = nil
765
766 local i = 1
767 while i <= #args do
768 local a = args[i]
769 if a == "-b" then
770 create_branch = true
771 -- Check if next arg is start-point (not another flag)
772 if args[i + 1] and not args[i + 1]:match("^%-") then
773 start_point = args[i + 1]
774 i = i + 1
775 end
776 elseif not branch then
777 branch = a
778 else
779 die("unexpected argument: " .. a)
780 end
781 i = i + 1
782 end
783
784 if not branch then
785 die("usage: wt a <branch> [-b [<start-point>]]")
786 return
787 end
788
789 local root, err = find_project_root()
790 if not root then
791 die(err --[[@as string]])
792 return
793 end
794
795 local git_dir = root .. "/.bare"
796 local source_worktree = detect_source_worktree(root)
797
798 -- Load config for path style
799 local global_config = load_global_config()
800 local style = global_config.branch_path_style or "nested"
801 local separator = global_config.flat_separator or "_"
802
803 local target_path = branch_to_path(root, branch, style, separator)
804
805 -- Check if target already exists
806 local check = io.open(target_path .. "/.git", "r")
807 if check then
808 check:close()
809 die("worktree already exists at " .. target_path)
810 end
811
812 local output, code
813 if create_branch then
814 -- Create new branch with worktree
815 if start_point then
816 output, code = run_cmd(
817 "GIT_DIR="
818 .. git_dir
819 .. " git worktree add -b "
820 .. branch
821 .. " -- "
822 .. target_path
823 .. " "
824 .. start_point
825 )
826 else
827 output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -b " .. branch .. " -- " .. target_path)
828 end
829 else
830 -- Check if branch exists locally or on remotes
831 local exists_local = branch_exists_local(git_dir, branch)
832 local remotes = find_branch_remotes(git_dir, branch)
833
834 if not exists_local and #remotes == 0 then
835 die("branch '" .. branch .. "' not found locally or on any remote (use -b to create)")
836 end
837
838 if #remotes > 1 then
839 die("branch '" .. branch .. "' exists on multiple remotes: " .. table.concat(remotes, ", "))
840 end
841
842 output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree add -- " .. target_path .. " " .. branch)
843 end
844
845 if code ~= 0 then
846 die("failed to add worktree: " .. output, EXIT_SYSTEM_ERROR)
847 end
848
849 -- Run hooks if we have a source worktree
850 local project_config = load_project_config(root)
851 if source_worktree then
852 if project_config.hooks then
853 run_hooks(source_worktree, target_path, project_config.hooks, root)
854 end
855 elseif project_config.hooks then
856 io.stderr:write("warning: hooks skipped (run from inside a worktree to apply hooks)\n")
857 end
858
859 print(target_path)
860end
861
862---Check if cwd is inside (or equal to) a given path
863---@param target string
864---@return boolean
865local function cwd_inside_path(target)
866 local cwd = get_cwd()
867 if not cwd then
868 return false
869 end
870 return path_inside(cwd, target)
871end
872
873---Get the bare repo's HEAD branch
874---@param git_dir string
875---@return string|nil branch name, nil on error
876local function get_bare_head(git_dir)
877 local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git symbolic-ref --short HEAD")
878 if code ~= 0 then
879 return nil
880 end
881 return (output:gsub("%s+$", ""))
882end
883
884---@param args string[]
885local function cmd_remove(args)
886 -- Parse arguments: <branch> [-b] [-f]
887 local branch = nil
888 local delete_branch = false
889 local force = false
890
891 for _, a in ipairs(args) do
892 if a == "-b" then
893 delete_branch = true
894 elseif a == "-f" then
895 force = true
896 elseif not branch then
897 branch = a
898 else
899 die("unexpected argument: " .. a)
900 end
901 end
902
903 if not branch then
904 die("usage: wt r <branch> [-b] [-f]")
905 return
906 end
907
908 local root, err = find_project_root()
909 if not root then
910 die(err --[[@as string]])
911 return
912 end
913
914 local git_dir = root .. "/.bare"
915
916 -- Find worktree by querying git for actual location (not computed from config)
917 local wt_output, wt_code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain")
918 if wt_code ~= 0 then
919 die("failed to list worktrees", EXIT_SYSTEM_ERROR)
920 return
921 end
922
923 local worktrees = parse_worktree_list(wt_output)
924 local target_path = nil
925 for _, wt in ipairs(worktrees) do
926 if wt.branch == branch then
927 target_path = wt.path
928 break
929 end
930 end
931
932 if not target_path then
933 die("no worktree found for branch '" .. branch .. "'")
934 return
935 end
936
937 -- Error if cwd is inside the worktree
938 if cwd_inside_path(target_path) then
939 die("cannot remove worktree while inside it")
940 end
941
942 -- Check for uncommitted changes
943 if not force then
944 local status_out = run_cmd("git -C " .. target_path .. " status --porcelain")
945 if status_out ~= "" then
946 die("worktree has uncommitted changes (use -f to force)")
947 end
948 end
949
950 -- Remove worktree
951 local remove_cmd = "GIT_DIR=" .. git_dir .. " git worktree remove"
952 if force then
953 remove_cmd = remove_cmd .. " --force"
954 end
955 remove_cmd = remove_cmd .. " -- " .. target_path
956
957 local output, code = run_cmd(remove_cmd)
958 if code ~= 0 then
959 die("failed to remove worktree: " .. output, EXIT_SYSTEM_ERROR)
960 end
961
962 -- Delete branch if requested
963 if delete_branch then
964 -- Check if branch is bare repo's HEAD
965 local bare_head = get_bare_head(git_dir)
966 if bare_head and bare_head == branch then
967 io.stderr:write("warning: cannot delete branch '" .. branch .. "' (it's the bare repo's HEAD)\n")
968 print("Worktree removed; branch retained")
969 return
970 end
971
972 -- Check if branch is checked out elsewhere
973 local checked_out = branch_checked_out_at(git_dir, branch)
974 if checked_out then
975 die("cannot delete branch '" .. branch .. "': checked out at " .. checked_out)
976 end
977
978 -- Delete branch
979 local delete_flag = force and "-D" or "-d"
980 local del_output, del_code = run_cmd("GIT_DIR=" .. git_dir .. " git branch " .. delete_flag .. " " .. branch)
981 if del_code ~= 0 then
982 io.stderr:write("warning: failed to delete branch: " .. del_output .. "\n")
983 print("Worktree removed; branch retained")
984 return
985 end
986
987 print("Worktree and branch '" .. branch .. "' removed")
988 else
989 print("Worktree removed")
990 end
991end
992
993local function cmd_list()
994 local root, err = find_project_root()
995 if not root then
996 die(err --[[@as string]])
997 return
998 end
999
1000 local git_dir = root .. "/.bare"
1001 local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git worktree list --porcelain")
1002 if code ~= 0 then
1003 die("failed to list worktrees: " .. output, EXIT_SYSTEM_ERROR)
1004 end
1005
1006 -- Parse porcelain output into worktree entries
1007 ---@type {path: string, head: string, branch: string}[]
1008 local worktrees = {}
1009 local current = {}
1010
1011 for line in output:gmatch("[^\n]+") do
1012 local key, value = line:match("^(%S+)%s*(.*)$")
1013 if key == "worktree" and value then
1014 if current.path then
1015 table.insert(worktrees, current)
1016 end
1017 -- Skip .bare directory
1018 if value:match("/%.bare$") then
1019 current = {}
1020 else
1021 current = { path = value, head = "", branch = "(detached)" }
1022 end
1023 elseif key == "HEAD" and value then
1024 current.head = value:sub(1, 7)
1025 elseif key == "branch" and value then
1026 current.branch = value:gsub("^refs/heads/", "")
1027 elseif key == "bare" then
1028 -- Skip bare repo entry
1029 current = {}
1030 end
1031 end
1032 if current.path then
1033 table.insert(worktrees, current)
1034 end
1035
1036 if #worktrees == 0 then
1037 print("No worktrees found")
1038 return
1039 end
1040
1041 -- Get current working directory
1042 local cwd = get_cwd() or ""
1043
1044 -- Build table rows with status
1045 local rows = {}
1046 for _, wt in ipairs(worktrees) do
1047 local rel_path = relative_path(cwd, wt.path)
1048
1049 -- Check dirty status
1050 local status_out = run_cmd("git -C " .. wt.path .. " status --porcelain")
1051 local status = status_out == "" and "clean" or "dirty"
1052
1053 table.insert(rows, rel_path .. "," .. wt.branch .. "," .. wt.head .. "," .. status)
1054 end
1055
1056 -- Output via gum table
1057 local table_input = "Path,Branch,HEAD,Status\n" .. table.concat(rows, "\n")
1058 table_input = table_input:gsub("EOF", "eof")
1059 local table_cmd = "gum table --print <<'EOF'\n" .. table_input .. "\nEOF"
1060 local table_handle = io.popen(table_cmd, "r")
1061 if not table_handle then
1062 return
1063 end
1064 io.write(table_handle:read("*a") or "")
1065 table_handle:close()
1066end
1067
1068local function cmd_fetch()
1069 local root, err = find_project_root()
1070 if not root then
1071 die(err --[[@as string]])
1072 return
1073 end
1074
1075 local git_dir = root .. "/.bare"
1076 local output, code = run_cmd("GIT_DIR=" .. git_dir .. " git fetch --all --prune")
1077 io.write(output)
1078 if code ~= 0 then
1079 os.exit(EXIT_SYSTEM_ERROR)
1080 end
1081end
1082
1083---List directory entries (excluding . and ..)
1084---@param path string
1085---@return string[]
1086local function list_dir(path)
1087 local entries = {}
1088 local handle = io.popen("ls -A " .. path .. " 2>/dev/null")
1089 if not handle then
1090 return entries
1091 end
1092 for line in handle:lines() do
1093 if line ~= "" then
1094 table.insert(entries, line)
1095 end
1096 end
1097 handle:close()
1098 return entries
1099end
1100
1101---Check if path is a directory
1102---@param path string
1103---@return boolean
1104local function is_dir(path)
1105 local f = io.open(path, "r")
1106 if not f then
1107 return false
1108 end
1109 f:close()
1110 return run_cmd_silent("test -d " .. path)
1111end
1112
1113---Check if path is a file (not directory)
1114---@param path string
1115---@return boolean
1116local function is_file(path)
1117 local f = io.open(path, "r")
1118 if not f then
1119 return false
1120 end
1121 f:close()
1122 return run_cmd_silent("test -f " .. path)
1123end
1124
1125---@param args string[]
1126local function cmd_init(args)
1127 -- Parse arguments
1128 local dry_run = false
1129 local skip_confirm = false
1130 for _, a in ipairs(args) do
1131 if a == "--dry-run" then
1132 dry_run = true
1133 elseif a == "-y" or a == "--yes" then
1134 skip_confirm = true
1135 else
1136 die("unexpected argument: " .. a)
1137 end
1138 end
1139
1140 local cwd = get_cwd()
1141 if not cwd then
1142 die("failed to get current directory", EXIT_SYSTEM_ERROR)
1143 return
1144 end
1145
1146 -- Case 1: Already wt-managed (.git file pointing to .bare + .bare/ exists)
1147 local git_path = cwd .. "/.git"
1148 local bare_path = cwd .. "/.bare"
1149
1150 local bare_exists = is_dir(bare_path)
1151 local git_file = io.open(git_path, "r")
1152
1153 if git_file then
1154 local content = git_file:read("*a")
1155 git_file:close()
1156
1157 -- Check if it's a file (not directory) pointing to .bare
1158 if is_file(git_path) and content and content:match("gitdir:%s*%.?/?%.bare") then
1159 if bare_exists then
1160 print("Already using wt bare structure")
1161 os.exit(EXIT_SUCCESS)
1162 end
1163 end
1164
1165 -- Check if .git is a file pointing elsewhere (inside a worktree)
1166 if is_file(git_path) and content and content:match("^gitdir:") then
1167 -- It's a worktree, not project root
1168 die("inside a worktree; run from project root or use 'wt c' to clone fresh")
1169 end
1170 end
1171
1172 -- Check for .git directory
1173 local git_dir_exists = is_dir(git_path)
1174
1175 if not git_dir_exists then
1176 -- Case 5: No .git at all, or bare repo without .git dir
1177 if bare_exists then
1178 die("found .bare/ but no .git; manually create .git file with 'gitdir: ./.bare'")
1179 end
1180 die("not a git repository (no .git found)")
1181 end
1182
1183 -- Now we have a .git directory
1184 -- Case 3: Existing worktree setup (.git/worktrees/ exists)
1185 local worktrees_path = git_path .. "/worktrees"
1186 if is_dir(worktrees_path) then
1187 local worktrees = list_dir(worktrees_path)
1188 io.stderr:write("error: repository already uses git worktrees\n")
1189 io.stderr:write("\n")
1190 io.stderr:write("Converting an existing worktree setup is complex and error-prone.\n")
1191 io.stderr:write("Recommend: clone fresh with 'wt c <url>' and recreate worktrees.\n")
1192 if #worktrees > 0 then
1193 io.stderr:write("\nExisting worktrees:\n")
1194 for _, wt in ipairs(worktrees) do
1195 io.stderr:write(" " .. wt .. "\n")
1196 end
1197 end
1198 os.exit(EXIT_USER_ERROR)
1199 end
1200
1201 -- Case 4: Normal clone (.git/ directory, no worktrees)
1202 -- Check for uncommitted changes
1203 local status_out = run_cmd("git status --porcelain")
1204 if status_out ~= "" then
1205 die("uncommitted changes; commit or stash before converting")
1206 end
1207
1208 -- Detect default branch
1209 local default_branch = detect_cloned_default_branch(git_path)
1210
1211 -- Warnings
1212 local warnings = {}
1213
1214 -- Check for submodules
1215 if is_file(cwd .. "/.gitmodules") then
1216 table.insert(warnings, "submodules detected (.gitmodules exists); verify they work after conversion")
1217 end
1218
1219 -- Check for nested .git directories (excluding the main one)
1220 local nested_git_output, _ = run_cmd("find " .. cwd .. " -mindepth 2 -name .git -type d 2>/dev/null")
1221 if nested_git_output ~= "" then
1222 table.insert(warnings, "nested .git directories found; these may cause issues")
1223 end
1224
1225 -- Find orphaned files (files in root that will be deleted)
1226 local all_entries = list_dir(cwd)
1227 local orphaned = {}
1228 for _, entry in ipairs(all_entries) do
1229 if entry ~= ".git" and entry ~= ".bare" then
1230 table.insert(orphaned, entry)
1231 end
1232 end
1233
1234 -- Load global config for path style
1235 local global_config = load_global_config()
1236 local style = global_config.branch_path_style or "nested"
1237 local separator = global_config.flat_separator
1238 local worktree_path = branch_to_path(cwd, default_branch, style, separator)
1239
1240 if dry_run then
1241 print("Dry run - planned actions:")
1242 print("")
1243 print("1. Move .git/ to .bare/")
1244 print("2. Create .git file pointing to .bare/")
1245 print("3. Create worktree: " .. worktree_path .. " (" .. default_branch .. ")")
1246 if #orphaned > 0 then
1247 print("4. Remove " .. #orphaned .. " orphaned items from root:")
1248 for _, item in ipairs(orphaned) do
1249 print(" - " .. item)
1250 end
1251 end
1252 if #warnings > 0 then
1253 print("")
1254 print("Warnings:")
1255 for _, w in ipairs(warnings) do
1256 print(" ⚠ " .. w)
1257 end
1258 end
1259 os.exit(EXIT_SUCCESS)
1260 end
1261
1262 -- Show warnings
1263 for _, w in ipairs(warnings) do
1264 io.stderr:write("warning: " .. w .. "\n")
1265 end
1266
1267 -- Confirm with gum (unless -y/--yes)
1268 if not skip_confirm then
1269 local confirm_msg = "Convert to wt bare structure? This will move .git to .bare"
1270 if #orphaned > 0 then
1271 confirm_msg = confirm_msg .. " and remove " .. #orphaned .. " items from root"
1272 end
1273
1274 local confirm_code = os.execute("gum confirm '" .. confirm_msg .. "'")
1275 if confirm_code ~= true then
1276 print("Aborted")
1277 os.exit(EXIT_USER_ERROR)
1278 end
1279 end
1280
1281 -- Step 1: Move .git to .bare
1282 local output, code = run_cmd("mv " .. git_path .. " " .. bare_path)
1283 if code ~= 0 then
1284 die("failed to move .git to .bare: " .. output, EXIT_SYSTEM_ERROR)
1285 end
1286
1287 -- Step 2: Write .git file
1288 local git_file_handle = io.open(git_path, "w")
1289 if not git_file_handle then
1290 -- Try to recover
1291 run_cmd("mv " .. bare_path .. " " .. git_path)
1292 die("failed to create .git file", EXIT_SYSTEM_ERROR)
1293 return
1294 end
1295 git_file_handle:write("gitdir: ./.bare\n")
1296 git_file_handle:close()
1297
1298 -- Step 3: Detach HEAD so branch can be checked out in worktree
1299 -- Point bare repo's HEAD to a placeholder so main branch can be used by worktree
1300 run_cmd("GIT_DIR=" .. bare_path .. " git symbolic-ref HEAD refs/heads/__wt_detached_placeholder__")
1301
1302 -- Step 4: Create worktree for default branch
1303 output, code = run_cmd("GIT_DIR=" .. bare_path .. " git worktree add -- " .. worktree_path .. " " .. default_branch)
1304 if code ~= 0 then
1305 die("failed to create worktree: " .. output, EXIT_SYSTEM_ERROR)
1306 end
1307
1308 -- Step 5: Remove orphaned files from root
1309 for _, item in ipairs(orphaned) do
1310 local item_path = cwd .. "/" .. item
1311 output, code = run_cmd("rm -rf " .. item_path)
1312 if code ~= 0 then
1313 io.stderr:write("warning: failed to remove " .. item .. ": " .. output .. "\n")
1314 end
1315 end
1316
1317 -- Summary
1318 print("Converted to wt bare structure")
1319 print("Bare repo: " .. bare_path)
1320 print("Worktree: " .. worktree_path)
1321 if #orphaned > 0 then
1322 print("Removed: " .. #orphaned .. " items from root")
1323 end
1324end
1325
1326-- Main entry point
1327
1328local function main()
1329 local command = arg[1]
1330
1331 if not command or command == "help" or command == "--help" or command == "-h" then
1332 print_usage()
1333 os.exit(EXIT_SUCCESS)
1334 end
1335
1336 ---@cast command string
1337
1338 -- Collect remaining args
1339 local subargs = {}
1340 for i = 2, #arg do
1341 table.insert(subargs, arg[i])
1342 end
1343
1344 -- Check for --help on any command
1345 if subargs[1] == "--help" or subargs[1] == "-h" then
1346 show_command_help(command)
1347 end
1348
1349 if command == "c" then
1350 cmd_clone(subargs)
1351 elseif command == "n" then
1352 cmd_new(subargs)
1353 elseif command == "a" then
1354 cmd_add(subargs)
1355 elseif command == "r" then
1356 cmd_remove(subargs)
1357 elseif command == "l" then
1358 cmd_list()
1359 elseif command == "f" then
1360 cmd_fetch()
1361 elseif command == "init" then
1362 cmd_init(subargs)
1363 else
1364 die("unknown command: " .. command)
1365 end
1366end
1367
1368-- Export for testing when required as module
1369if pcall(debug.getlocal, 4, 1) then
1370 return {
1371 -- URL/project parsing
1372 extract_project_name = extract_project_name,
1373 resolve_url_template = resolve_url_template,
1374 -- Path manipulation
1375 branch_to_path = branch_to_path,
1376 split_path = split_path,
1377 relative_path = relative_path,
1378 path_inside = path_inside,
1379 -- Config loading
1380 load_global_config = load_global_config,
1381 load_project_config = load_project_config,
1382 -- Git output parsing (testable without git)
1383 parse_branch_remotes = parse_branch_remotes,
1384 parse_worktree_list = parse_worktree_list,
1385 escape_pattern = escape_pattern,
1386 -- Hook helpers
1387 summarize_hooks = summarize_hooks,
1388 load_hook_permissions = function(home_override)
1389 local home = home_override or os.getenv("HOME")
1390 if not home then
1391 return {}
1392 end
1393 local path = home .. "/.local/share/wt/hook-dirs.lua"
1394 local f = io.open(path, "r")
1395 if not f then
1396 return {}
1397 end
1398 local content = f:read("*a")
1399 f:close()
1400 local chunk = load("return " .. content, path, "t", {})
1401 if not chunk then
1402 return {}
1403 end
1404 local ok, result = pcall(chunk)
1405 if ok and type(result) == "table" then
1406 return result
1407 end
1408 return {}
1409 end,
1410 save_hook_permissions = function(perms, home_override)
1411 local home = home_override or os.getenv("HOME")
1412 if not home then
1413 return
1414 end
1415 local dir = home .. "/.local/share/wt"
1416 run_cmd_silent("mkdir -p " .. dir)
1417 local path = dir .. "/hook-dirs.lua"
1418 local f = io.open(path, "w")
1419 if not f then
1420 return
1421 end
1422 f:write("{\n")
1423 for k, v in pairs(perms) do
1424 f:write('\t["' .. k .. '"] = ' .. tostring(v) .. ",\n")
1425 end
1426 f:write("}\n")
1427 f:close()
1428 end,
1429 run_hooks = function(source, target, hooks, root, home_override)
1430 local home = home_override or os.getenv("HOME")
1431 if not home then
1432 return
1433 end
1434 local perm_path = home .. "/.local/share/wt/hook-dirs.lua"
1435 local perms = {}
1436 local pf = io.open(perm_path, "r")
1437 if pf then
1438 local content = pf:read("*a")
1439 pf:close()
1440 local chunk = load("return " .. content, perm_path, "t", {})
1441 if chunk then
1442 local ok, result = pcall(chunk)
1443 if ok and type(result) == "table" then
1444 perms = result
1445 end
1446 end
1447 end
1448 if perms[root] == false then
1449 io.stderr:write("hooks skipped (not allowed for this project)\n")
1450 return
1451 end
1452 if hooks.copy then
1453 for _, item in ipairs(hooks.copy) do
1454 local src = source .. "/" .. item
1455 local dst = target .. "/" .. item
1456 local parent = dst:match("(.+)/[^/]+$")
1457 if parent then
1458 run_cmd_silent("mkdir -p " .. parent)
1459 end
1460 run_cmd("cp -r " .. src .. " " .. dst)
1461 end
1462 end
1463 if hooks.symlink then
1464 for _, item in ipairs(hooks.symlink) do
1465 local src = source .. "/" .. item
1466 local dst = target .. "/" .. item
1467 local parent = dst:match("(.+)/[^/]+$")
1468 if parent then
1469 run_cmd_silent("mkdir -p " .. parent)
1470 end
1471 run_cmd("ln -s " .. src .. " " .. dst)
1472 end
1473 end
1474 if hooks.run then
1475 for _, cmd in ipairs(hooks.run) do
1476 run_cmd("cd " .. target .. " && " .. cmd)
1477 end
1478 end
1479 end,
1480 -- Project root detection (re-exported from wt.git)
1481 find_project_root = find_project_root,
1482 detect_source_worktree = detect_source_worktree,
1483 -- Command execution (for integration tests)
1484 run_cmd = run_cmd,
1485 run_cmd_silent = run_cmd_silent,
1486 -- Exit codes (re-exported from wt.exit)
1487 EXIT_SUCCESS = exit.EXIT_SUCCESS,
1488 EXIT_USER_ERROR = exit.EXIT_USER_ERROR,
1489 EXIT_SYSTEM_ERROR = exit.EXIT_SYSTEM_ERROR,
1490 }
1491end
1492
1493main()