From 97f8909001e6d43f6079b6bce3736a2240bd0308 Mon Sep 17 00:00:00 2001 From: a Date: Mon, 30 Jun 2025 20:23:13 -0500 Subject: [PATCH] noot --- Makefile | 4 +- main.lua | 81 ++++++++----- shell/zsh/repotool.plugin.zsh | 2 +- src/cmd.lua | 36 ++++++ src/fs.lua | 9 +- src/get_command.lua | 10 +- src/git.lua | 20 ++-- src/open_command.lua | 33 +++--- src/worktree.lua | 207 ++++++++++++++++++++++++++++++++++ src/worktree_command.lua | 139 ----------------------- 10 files changed, 327 insertions(+), 214 deletions(-) create mode 100644 src/cmd.lua create mode 100644 src/worktree.lua delete mode 100644 src/worktree_command.lua diff --git a/Makefile b/Makefile index 0fa8876..8b50ea7 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,9 @@ all: dist/repotool install: dist/repotool shell/zsh/repotool.zsh shell/zsh/repotool.plugin.zsh mkdir -p ${REPOTOOL_PATH}/.bin/ - install dist/repotool shell/zsh/repotool.zsh shell/zsh/repotool.plugin.zsh ${REPOTOOL_PATH}/.bin/ + mkdir -p ${REPOTOOL_PATH}/.shell/ + install dist/repotool ${REPOTOOL_PATH}/.bin/ + install shell/zsh/repotool.zsh shell/zsh/repotool.plugin.zsh ${REPOTOOL_PATH}/.shell/ dist/repotool: $(SOURCES_SRC) $(LIBS_SRC) main.lua @mkdir -p dist diff --git a/main.lua b/main.lua index f294be4..233cd1a 100755 --- a/main.lua +++ b/main.lua @@ -11,7 +11,7 @@ local json = require("json") -- Load command modules local get_command = require("get_command") -local worktree_command = require("worktree_command") +local worktree = require("worktree") local open_command = require("open_command") -- Create the main command with subcommands @@ -62,41 +62,60 @@ local app = cli.cmd { return 0 end), }, - -- Worktree command + -- Worktree command with subcommands cli.cmd { 'worktree', 'w', 'wt', - desc = 'goes to or creates a worktree with the name for the repo you are in', - term = cli.all { - name = cli.arg { - 'name', - desc = 'Name of the worktree', - required = false, + desc = 'manage git worktrees', + subs = { + -- List subcommand + cli.cmd { + 'list', 'ls', + desc = 'list existing worktrees for this repo', + term = cli.val():and_then(function() + worktree.handle_list() + return 0 + end), }, - list = cli.opt { - '--list', '-l', - desc = 'List existing worktrees for this repo', - flag = true, + -- Get subcommand + cli.cmd { + 'get',"new","create","n","c", + desc = 'create or go to a worktree', + term = cli.all { + name = cli.arg { + 'name', + desc = 'Name of the worktree', + required = true, + }, + }:and_then(function(args) + worktree.handle_get(args.name) + return 0 + end), }, - root = cli.opt { - '--root', '-r', - desc = 'Return the root directory of the original repo', - flag = true, + -- Root subcommand + cli.cmd { + 'root', 'back', 'r', 'return', + desc = 'return to the root directory of the original repo', + term = cli.val():and_then(function() + worktree.handle_root() + return 0 + end), }, - }:and_then(function(args) - -- Handle special cases: "list" and "ls" as commands - local name = args.name - if name == "list" or name == "ls" then - args.list = true - args.name = nil - end - - worktree_command({ - name = args.name, - list = args.list, - root = args.root, - }) - return 0 - end), + -- Remove subcommand + cli.cmd { + 'remove', 'rm', 'delete', 'del', + desc = 'remove a worktree', + term = cli.all { + name = cli.arg { + 'name', + desc = 'Name of the worktree to remove', + required = true, + }, + }:and_then(function(args) + worktree.handle_remove(args.name) + return 0 + end), + }, + }, }, -- Open command cli.cmd { diff --git a/shell/zsh/repotool.plugin.zsh b/shell/zsh/repotool.plugin.zsh index 54c2764..c5ea9e0 100755 --- a/shell/zsh/repotool.plugin.zsh +++ b/shell/zsh/repotool.plugin.zsh @@ -1,4 +1,4 @@ #!/usr/bin/env zsh [[ -z "$REPOTOOL_PATH" ]] && export REPOTOOL_PATH="$HOME/repo" -alias repo=". $REPOTOOL_PATH/.bin/repotool.zsh" +alias repo=". $REPOTOOL_PATH/.shell/repotool.zsh" diff --git a/src/cmd.lua b/src/cmd.lua new file mode 100644 index 0000000..92f9faa --- /dev/null +++ b/src/cmd.lua @@ -0,0 +1,36 @@ +local cmd = {} + +-- Execute a command, redirecting stdout to stderr to avoid polluting our JSON output +function cmd.execute(command) + -- Redirect stdout to stderr so only our JSON goes to stdout + local redirected_cmd = command .. " 1>&2" + return os.execute(redirected_cmd) +end + +-- Execute a command and capture its output +function cmd.read(command) + local handle = io.popen(command .. " 2>&1") + local result = handle:read("*a") + local success = handle:close() + return result, success +end + +-- Execute a command and capture stdout separately from stderr +function cmd.read_stdout(command) + local handle = io.popen(command .. " 2>/dev/null") + local result = handle:read("*a") + local success = handle:close() + return result, success +end + +-- Execute a command and return only the exit status +function cmd.run(command) + return os.execute(command .. " >/dev/null 2>&1") +end + +-- Check if a command exists +function cmd.exists(command_name) + return cmd.run("command -v " .. command_name) == 0 +end + +return cmd \ No newline at end of file diff --git a/src/fs.lua b/src/fs.lua index 75fd6ff..8f75356 100644 --- a/src/fs.lua +++ b/src/fs.lua @@ -1,3 +1,5 @@ +local cmd = require("cmd") + local fs = {} -- Check if a file exists @@ -13,17 +15,14 @@ function fs.dir_exists(path) if f then io.close(f) -- Check if it's actually a directory by trying to list it - local handle = io.popen('test -d "' .. path .. '" && echo "yes" || echo "no"') - local result = handle:read("*a"):gsub("\n", "") - handle:close() - return result == "yes" + return cmd.run('test -d "' .. path .. '"') == 0 end return false end -- Create directory with parents function fs.mkdir_p(path) - os.execute('mkdir -p "' .. path .. '"') + cmd.execute('mkdir -p "' .. path .. '"') end -- Get the parent directory of a path diff --git a/src/get_command.lua b/src/get_command.lua index dfe81ba..c4fea4e 100644 --- a/src/get_command.lua +++ b/src/get_command.lua @@ -1,6 +1,7 @@ local git = require("git") local json = require("json") local fs = require("fs") +local cmd = require("cmd") local function get_command(args) -- Validate URL @@ -53,8 +54,7 @@ http_pass: %s fs.mkdir_p(target_dir) end - -- Change to target directory - os.execute("cd '" .. target_dir .. "'") + -- Note: cd command doesn't work in os.execute as it runs in a subshell -- Construct repo URL based on method local clone_url @@ -73,9 +73,9 @@ http_pass: %s local git_dir = fs.join(target_dir, ".git") if not fs.dir_exists(git_dir) then -- Check if remote exists - local check_cmd = "cd '" .. target_dir .. "' && git ls-remote '" .. clone_url .. "' >/dev/null 2>&1" - if os.execute(check_cmd) == 0 then - os.execute("git clone '" .. clone_url .. "' '" .. target_dir .. "' >&2") + local check_cmd = "cd '" .. target_dir .. "' && git ls-remote '" .. clone_url .. "'" + if cmd.run(check_cmd) == 0 then + cmd.execute("git clone '" .. clone_url .. "' '" .. target_dir .. "'") cloned = "true" else io.stderr:write("Could not find repo: " .. clone_url .. "\n") diff --git a/src/git.lua b/src/git.lua index 32829df..463c547 100644 --- a/src/git.lua +++ b/src/git.lua @@ -1,3 +1,5 @@ +local cmd = require("cmd") + local git = {} -- Parse a git URL into domain and path components @@ -34,9 +36,8 @@ end -- Get domain and path from current git repository's origin function git.parse_origin() - local handle = io.popen("git config --get remote.origin.url 2>/dev/null") - local origin_url = handle:read("*a"):gsub("\n", "") - handle:close() + local origin_url = cmd.read_stdout("git config --get remote.origin.url") + origin_url = origin_url:gsub("\n", "") if origin_url == "" then return nil, nil @@ -59,11 +60,8 @@ function git.valid_url(url) end -- Execute command and return output -function git.execute(cmd) - local handle = io.popen(cmd .. " 2>&1") - local result = handle:read("*a") - local success = handle:close() - return result, success +function git.execute(command) + return cmd.read(command) end -- Check if we're in a git repository @@ -93,10 +91,10 @@ end -- Get list of all worktrees with their properties function git.worktree_list() local worktrees = {} - local handle = io.popen("git worktree list --porcelain") + local output = cmd.read_stdout("git worktree list --porcelain") local current_worktree = nil - for line in handle:lines() do + for line in output:gmatch("[^\n]+") do local worktree_path = line:match("^worktree (.+)$") if worktree_path then -- Save previous worktree if any @@ -137,8 +135,6 @@ function git.worktree_list() table.insert(worktrees, current_worktree) end - handle:close() - return worktrees end diff --git a/src/open_command.lua b/src/open_command.lua index 05f6d91..ad1550e 100644 --- a/src/open_command.lua +++ b/src/open_command.lua @@ -1,5 +1,6 @@ local git = require("git") local json = require("json") +local cmd = require("cmd") local function open_command(args) -- Check if we're in a git repository @@ -9,9 +10,8 @@ local function open_command(args) end -- Get the remote URL - local handle = io.popen("git ls-remote --get-url") - local raw_url = handle:read("*a"):gsub("\n", "") - handle:close() + local raw_url = cmd.read_stdout("git ls-remote --get-url") + raw_url = raw_url:gsub("\n", "") if raw_url == "" then io.stderr:write("Error: No remote URL found\n") @@ -30,29 +30,22 @@ local function open_command(args) -- Detect platform and open URL local open_cmd - local result = os.execute("command -v xdg-open >/dev/null 2>&1") - if result == true or result == 0 then + if cmd.exists("xdg-open") then -- Linux open_cmd = "xdg-open" + elseif cmd.exists("open") then + -- macOS + open_cmd = "open" + elseif cmd.exists("start") then + -- Windows + open_cmd = "start" else - result = os.execute("command -v open >/dev/null 2>&1") - if result == true or result == 0 then - -- macOS - open_cmd = "open" - else - result = os.execute("command -v start >/dev/null 2>&1") - if result == true or result == 0 then - -- Windows - open_cmd = "start" - else - io.stderr:write("Error: Unable to detect platform open command\n") - os.exit(1) - end - end + io.stderr:write("Error: Unable to detect platform open command\n") + os.exit(1) end -- Open the URL - os.execute(open_cmd .. " '" .. https_url .. "' 2>/dev/null") + cmd.execute(open_cmd .. " '" .. https_url .. "'") -- Output JSON print(json.encode({ diff --git a/src/worktree.lua b/src/worktree.lua new file mode 100644 index 0000000..67e95c3 --- /dev/null +++ b/src/worktree.lua @@ -0,0 +1,207 @@ +local git = require("git") +local json = require("json") +local fs = require("fs") +local cmd = require("cmd") + +local worktree = {} + +-- Handle the root/return subcommand +function worktree.handle_root() + -- Check if we're in a git repository + if not git.in_repo() then + io.stderr:write("Error: Not in a git repository\n") + os.exit(1) + end + + -- Get repository root + local repo_root = git.get_repo_root() + + -- Check if we're in a worktree + local git_common_dir = git.get_common_dir() + local cd_path = repo_root + + if git_common_dir ~= ".git" and git_common_dir ~= "" then + -- We're in a worktree, get the main repo path + cd_path = git_common_dir:match("(.+)/.git") + end + + print(json.encode({cd = cd_path, hook = "worktree"})) +end + +-- Handle the list subcommand +function worktree.handle_list() + -- Check if we're in a git repository + if not git.in_repo() then + io.stderr:write("Error: Not in a git repository\n") + os.exit(1) + end + + -- Parse origin URL + local domain, path = git.parse_origin() + if not domain or not path then + io.stderr:write("Error: Unable to parse repository origin URL\n") + os.exit(1) + end + + -- Get repository root + local repo_root = git.get_repo_root() + + -- Calculate worktree base directory + local repotool_path = os.getenv("REPOTOOL_PATH") or os.getenv("HOME") .. "/repo" + local worktree_base = repotool_path .. "/.worktree/" .. domain .. "/" .. path + + -- Get all worktrees using the git library + local worktrees = git.worktree_list() + + -- Filter and display worktrees under our worktree base + io.stderr:write("Worktrees for " .. domain .. "/" .. path .. ":\n") + local found_any = false + + local any_prunable = false + for _, wt in ipairs(worktrees) do + if wt.path == repo_root then + -- Skip the main repository + else + found_any = true + local wt_name = wt.path:match(".*/([^/]+)$") + local exists = fs.dir_exists(wt.path) + + local status = "" + if not exists then + if wt.prunable then + any_prunable = true + status = " (prunable)" + else + status = " (missing)" + end + elseif wt.locked then + status = " (locked)" + elseif wt.detached then + status = " (detached)" + end + io.stderr:write(" - " .. wt_name .. status .. "\n") + end + end + + if any_prunable then + io.stderr:write("Run 'git worktree prune' to remove prunable worktrees\n") + end + + if not found_any then + io.stderr:write(" No worktrees found\n") + end + + print(json.encode({hook = "worktree.list"})) +end + +-- Handle the get subcommand +function worktree.handle_get(worktree_name) + -- Check if we're in a git repository + if not git.in_repo() then + io.stderr:write("Error: Not in a git repository\n") + os.exit(1) + end + + -- Parse origin URL + local domain, path = git.parse_origin() + if not domain or not path then + io.stderr:write("Error: Unable to parse repository origin URL\n") + os.exit(1) + end + + -- Calculate worktree base directory + local repotool_path = os.getenv("REPOTOOL_PATH") or os.getenv("HOME") .. "/repo" + local worktree_base = repotool_path .. "/.worktree/" .. domain .. "/" .. path + + -- Construct worktree path + local worktree_path = worktree_base .. "/" .. worktree_name + + -- Check if a prunable worktree exists at this path + local prunable_output = cmd.read_stdout("git worktree list | grep '" .. worktree_path .. ".*prunable'") + if prunable_output ~= "" then + io.stderr:write("Found prunable worktree at " .. worktree_path .. ", cleaning up...\n") + cmd.execute("git worktree prune") + end + + -- Check if worktree already exists + local created = "false" + local exists_output = cmd.read_stdout("git worktree list | grep '" .. worktree_path .. "'") + if exists_output == "" then + -- Create parent directories if they don't exist + fs.mkdir_p(fs.dirname(worktree_path)) + + -- Create the worktree (try different methods) + local success = cmd.run("git worktree add '" .. worktree_path .. "' -b '" .. worktree_name .. "'") == 0 + if not success then + success = cmd.run("git worktree add '" .. worktree_path .. "' '" .. worktree_name .. "'") == 0 + end + if not success then + cmd.execute("git worktree add '" .. worktree_path .. "'") + end + created = "true" + end + + -- Output JSON + print(json.encode({ + cd = worktree_path, + domain = domain, + path = path, + worktree_name = worktree_name, + created = created, + hook = "worktree" + })) +end + +-- Handle the remove/delete subcommand +function worktree.handle_remove(worktree_name) + -- Check if we're in a git repository + if not git.in_repo() then + io.stderr:write("Error: Not in a git repository\n") + os.exit(1) + end + + -- Parse origin URL + local domain, path = git.parse_origin() + if not domain or not path then + io.stderr:write("Error: Unable to parse repository origin URL\n") + os.exit(1) + end + + -- Calculate worktree base directory + local repotool_path = os.getenv("REPOTOOL_PATH") or os.getenv("HOME") .. "/repo" + local worktree_base = repotool_path .. "/.worktree/" .. domain .. "/" .. path + + -- Construct worktree path + local worktree_path = worktree_base .. "/" .. worktree_name + + -- Check if worktree exists + local exists_output = cmd.read_stdout("git worktree list | grep '" .. worktree_path .. "'") + if exists_output == "" then + io.stderr:write("Error: Worktree '" .. worktree_name .. "' not found\n") + os.exit(1) + end + + -- Remove the worktree + io.stderr:write("Removing worktree '" .. worktree_name .. "'...\n") + local success = cmd.run("git worktree remove '" .. worktree_path .. "'") == 0 + + if not success then + -- Try with --force if normal remove fails + io.stderr:write("Normal remove failed, trying with --force...\n") + success = cmd.run("git worktree remove --force '" .. worktree_path .. "'") == 0 + end + + if success then + io.stderr:write("Successfully removed worktree '" .. worktree_name .. "'\n") + print(json.encode({ + removed = worktree_name, + path = worktree_path, + hook = "worktree.remove" + })) + else + io.stderr:write("Error: Failed to remove worktree '" .. worktree_name .. "'\n") + os.exit(1) + end +end + +return worktree \ No newline at end of file diff --git a/src/worktree_command.lua b/src/worktree_command.lua deleted file mode 100644 index f7b03fd..0000000 --- a/src/worktree_command.lua +++ /dev/null @@ -1,139 +0,0 @@ -local git = require("git") -local json = require("json") -local fs = require("fs") - -local function worktree_command(args) - -- Check if we're in a git repository - if not git.in_repo() then - io.stderr:write("Error: Not in a git repository\n") - os.exit(1) - end - - -- Get repository root - local repo_root = git.get_repo_root() - - -- Handle --root flag - if args.root then - -- Check if we're in a worktree - local git_common_dir = git.get_common_dir() - local cd_path = repo_root - - if git_common_dir ~= ".git" and git_common_dir ~= "" then - -- We're in a worktree, get the main repo path - cd_path = git_common_dir:match("(.+)/.git") - end - - print(json.encode({cd = cd_path, hook = "worktree"})) - return - end - - -- Parse origin URL - local domain, path = git.parse_origin() - if not domain or not path then - io.stderr:write("Error: Unable to parse repository origin URL\n") - os.exit(1) - end - - -- Calculate worktree base directory - local repotool_path = os.getenv("REPOTOOL_PATH") or os.getenv("HOME") .. "/repo" - local worktree_base = repotool_path .. "/worktree/" .. domain .. "/" .. path - - -- Handle --list flag - if args.list then - -- Get all worktrees using the git library - local worktrees = git.worktree_list() - - -- Filter and display worktrees under our worktree base - io.stderr:write("Worktrees for " .. domain .. "/" .. path .. ":\n") - local found_any = false - - local any_prunable = false - for _, wt in ipairs(worktrees) do - if wt.path == repo_root then - else - found_any = true - local wt_name = wt.path:match(".*/([^/]+)$") - local exists = fs.dir_exists(wt.path) - - local status = "" - if not exists then - if wt.prunable then - any_prunable = true - status = " (prunable)" - else - status = " (missing)" - end - elseif wt.locked then - status = " (locked)" - elseif wt.detached then - status = " (detached)" - end - io.stderr:write(" - " .. wt_name .. status .. "\n") - end - end - - if any_prunable then - io.stderr:write("Run 'git worktree prune' to remove prunable worktrees\n") - end - - if not found_any then - io.stderr:write(" No worktrees found\n") - end - - print(json.encode({hook = "worktree.list"})) - return - end - - -- Get worktree name from arguments - local worktree_name = args.name - - -- Check if name is provided - if not worktree_name or worktree_name == "" then - io.stderr:write([[Error: Missing required argument: name -Run 'repo worktree --help' for usage information -]]) - os.exit(1) - end - - -- Construct worktree path - local worktree_path = worktree_base .. "/" .. worktree_name - - -- Check if a prunable worktree exists at this path - local check_prunable = io.popen("git worktree list | grep '" .. worktree_path .. ".*prunable'") - if check_prunable:read("*a") ~= "" then - io.stderr:write("Found prunable worktree at " .. worktree_path .. ", cleaning up...\n") - os.execute("git worktree prune") - end - check_prunable:close() - - -- Check if worktree already exists - local created = "false" - local check_exists = io.popen("git worktree list | grep '" .. worktree_path .. "'") - if check_exists:read("*a") == "" then - -- Create parent directories if they don't exist - fs.mkdir_p(fs.dirname(worktree_path)) - - -- Create the worktree (try different methods) - local success = os.execute("git worktree add '" .. worktree_path .. "' -b '" .. worktree_name .. "' 2>/dev/null") == 0 - if not success then - success = os.execute("git worktree add '" .. worktree_path .. "' '" .. worktree_name .. "' 2>/dev/null") == 0 - end - if not success then - os.execute("git worktree add '" .. worktree_path .. "'") - end - created = "true" - end - check_exists:close() - - -- Output JSON - print(json.encode({ - cd = worktree_path, - domain = domain, - path = path, - worktree_name = worktree_name, - created = created, - hook = "worktree" - })) -end - -return worktree_command