diff --git a/lua/git-worktree/git.lua b/lua/git-worktree/git.lua index c161fad..c176936 100644 --- a/lua/git-worktree/git.lua +++ b/lua/git-worktree/git.lua @@ -8,41 +8,44 @@ local M = {} -- A lot of this could be cleaned up if there was better job -> job -> function -- communication. That should be doable here in the near future --- ----@param path_str string path to the worktree to check. if relative, then path from the git root dir +---@param path_str string path to the worktree to check +---@param branch string? branch the worktree is associated with ---@param cb any -function M.has_worktree(path_str, cb) +function M.has_worktree(path_str, branch, cb) local found = false - local path = Path:new(path_str) + local path if path_str == '.' then path_str = vim.loop.cwd() - path = Path:new(path_str) end - local job = Job:new { - command = 'git', - args = { 'worktree', 'list' }, - on_stdout = function(_, data) - local list_data = {} - for section in data:gmatch('%S+') do - table.insert(list_data, section) - end + path = Path:new(path_str) + if not path:is_absolute() then + path = Path:new(string.format('%s' .. Path.path.sep .. '%s', vim.loop.cwd(), path_str)) + end + path = path:absolute() - data = list_data[1] + Log.debug('has_worktree: %s %s', path, branch) - local start - if path:is_absolute() then - start = data == path_str - else - local worktree_path = Path:new(string.format('%s' .. Path.path.sep .. '%s', vim.loop.cwd(), path_str)) - worktree_path = worktree_path:absolute() - start = data == worktree_path + local job = Job:new { + command = 'git', + args = { 'worktree', 'list', '--porcelain' }, + on_stdout = function(_, line) + if line:match('^worktree ') then + local current_worktree = Path:new(line:match('^worktree (.+)$')):absolute() + Log.debug('current_worktree: "%s"', current_worktree) + if path == current_worktree then + found = true + return + end + elseif branch ~= nil and line:match('^branch ') then + local worktree_branch = line:match('^branch (.+)$') + Log.debug('worktree_branch: %s', worktree_branch) + if worktree_branch == 'refs/heads/' .. branch then + found = true + return + end end - - -- TODO: This is clearly a hack (do not think we need this anymore?) - --local start_with_head = string.find(data, string.format('[heads/%s]', path), 1, true) - found = found or start - Log.debug('found: %s', found) end, cwd = vim.loop.cwd(), } @@ -108,15 +111,18 @@ function M.toplevel_dir() return table.concat(stdout, '') end -function M.has_branch(branch, cb) +function M.has_branch(branch, opts, cb) local found = false + local args = { 'branch', '--format=%(refname:short)' } + opts = opts or {} + for _, opt in ipairs(opts) do + args[#args + 1] = opt + end + local job = Job:new { command = 'git', - args = { 'branch' }, + args = args, on_stdout = function(_, data) - -- remove markere on current branch - data = data:gsub('*', '') - data = vim.trim(data) found = found or data == branch end, cwd = vim.loop.cwd(), @@ -129,20 +135,32 @@ function M.has_branch(branch, cb) end --- @param path string ---- @param branch string +--- @param branch string? --- @param found_branch boolean +--- @param upstream string +--- @param found_upstream boolean --- @return Job -function M.create_worktree_job(path, branch, found_branch) +function M.create_worktree_job(path, branch, found_branch, upstream, found_upstream) local worktree_add_cmd = 'git' local worktree_add_args = { 'worktree', 'add' } - if not found_branch then - table.insert(worktree_add_args, '-b') - table.insert(worktree_add_args, branch) + if branch == nil then + table.insert(worktree_add_args, '-d') table.insert(worktree_add_args, path) else - table.insert(worktree_add_args, path) - table.insert(worktree_add_args, branch) + if not found_branch then + table.insert(worktree_add_args, '-b') + table.insert(worktree_add_args, branch) + table.insert(worktree_add_args, path) + + if found_upstream and branch ~= upstream then + table.insert(worktree_add_args, '--track') + table.insert(worktree_add_args, upstream) + end + else + table.insert(worktree_add_args, path) + table.insert(worktree_add_args, branch) + end end return Job:new { @@ -195,7 +213,7 @@ end --- @return Job function M.setbranch_job(path, branch, upstream) local set_branch_cmd = 'git' - local set_branch_args = { 'branch', string.format('--set-upstream-to=%s/%s', upstream, branch) } + local set_branch_args = { 'branch', branch, string.format('--set-upstream-to=%s', upstream) } return Job:new { command = set_branch_cmd, args = set_branch_args, @@ -238,4 +256,56 @@ function M.rebase_job(path) } end + +--- @param path string +--- @return string|nil +function M.parse_head(path) + local job = Job:new { + command = 'git', + args = { 'rev-parse', '--abbrev-ref', 'HEAD' }, + cwd = path, + on_start = function() + Log.debug('git rev-parse --abbrev-ref HEAD') + end, + } + + local stdout, code = job:sync() + if code ~= 0 then + Log.error( + 'Error in parsing the HEAD: code:' + .. tostring(code) + .. ' out: ' + .. table.concat(stdout, '') + .. '.' + ) + return nil + end + + return table.concat(stdout, '') +end + +--- @param branch string +--- @return Job|nil +function M.delete_branch_job(branch) + local root = M.gitroot_dir() + if root == nil then + return nil + end + + local default = M.parse_head(root) + if default == branch then + print('Refusing to delete default branch') + return nil + end + + return Job:new { + command = 'git', + args = { 'branch', '-D', branch }, + cwd = M.gitroot_dir(), + on_start = function() + Log.debug('git branch -D') + end, + } +end + return M diff --git a/lua/git-worktree/init.lua b/lua/git-worktree/init.lua index 522c889..4ebd56e 100644 --- a/lua/git-worktree/init.lua +++ b/lua/git-worktree/init.lua @@ -23,7 +23,7 @@ local M = {} local Worktree = require('git-worktree.worktree') --Switch the current worktree ----@param path string +---@param path string? function M.switch_worktree(path) Worktree.switch(path) end diff --git a/lua/git-worktree/worktree.lua b/lua/git-worktree/worktree.lua index 4e85288..d5f1228 100644 --- a/lua/git-worktree/worktree.lua +++ b/lua/git-worktree/worktree.lua @@ -14,6 +14,15 @@ local function get_absolute_path(path) end local function change_dirs(path) + if path == nil then + local out = vim.fn.systemlist('git rev-parse --git-common-dir') + if vim.v.shell_error ~= 0 then + Log.error('Could not parse common dir') + return + end + path = out[1] + end + Log.info('changing dirs: %s ', path) local worktree_path = get_absolute_path(path) local previous_worktree = vim.loop.cwd() @@ -25,7 +34,7 @@ local function change_dirs(path) Log.debug('Changing to directory %s', worktree_path) vim.cmd(cmd) else - Log.error('Could not chang to directory: %s', worktree_path) + Log.error('Could not change to directory: %s', worktree_path) end if Config.clearjumps_on_change then @@ -33,6 +42,7 @@ local function change_dirs(path) vim.cmd('clearjumps') end + print(string.format('Switched to %s', path)) return previous_worktree end @@ -60,25 +70,31 @@ local M = {} --- SWITCH --- --Switch the current worktree ----@param path string +---@param path string? function M.switch(path) - Git.has_worktree(path, function(found) - Log.debug('test') - if not found then - Log.error('worktree does not exists, please create it first %s ', path) + if path == nil then + change_dirs(path) + else + if path == vim.loop.cwd() then + return end - Log.debug('has worktree') + Git.has_worktree(path, nil, function(found) + if not found then + Log.error('Worktree does not exists, please create it first %s ', path) + return + end - vim.schedule(function() - local prev_path = change_dirs(path) - Hooks.emit(Hooks.type.SWITCH, path, prev_path) + vim.schedule(function() + local prev_path = change_dirs(path) + Hooks.emit(Hooks.type.SWITCH, path, prev_path) + end) end) - end) + end end --- CREATE --- ---crerate a worktree +--create a worktree ---@param path string ---@param branch string ---@param upstream? string @@ -91,67 +107,53 @@ function M.create(path, branch, upstream) -- M.setup_git_info() - Git.has_worktree(path, function(found) + Git.has_worktree(path, branch, function(found) if found then - Log.error('worktree already exists') + Log.error('Path "%s" or branch "%s" already in use.', path, branch) return end - Git.has_branch(branch, function(found_branch) - Config = require('git-worktree.config') - local worktree_path - if Path:new(path):is_absolute() then - worktree_path = path - else - worktree_path = Path:new(vim.loop.cwd(), path):absolute() - end - - -- create_worktree(path, branch, upstream, found_branch) - local create_wt_job = Git.create_worktree_job(path, branch, found_branch) - - if upstream ~= nil then - local fetch = Git.fetchall_job(path, branch, upstream) - local set_branch = Git.setbranch_job(path, branch, upstream) - local set_push = Git.setpush_job(path, branch, upstream) - local rebase = Git.rebase_job(path) - - create_wt_job:and_then_on_success(fetch) - fetch:and_then_on_success(set_branch) - - if Config.autopush then - -- These are "optional" operations. - -- We have to figure out how we want to handle these... - set_branch:and_then(set_push) - set_push:and_then(rebase) - set_push:after_failure(failure('create_worktree', set_branch.args, worktree_path, true)) - else - set_branch:and_then(rebase) - end + if branch == '' then + -- detached head + local create_wt_job = Git.create_worktree_job(path, nil, false, nil, false) + create_wt_job:after(function() + vim.schedule(function() + Hooks.emit(Hooks.type.CREATE, path, branch, upstream) + M.switch(path) + end) + end) + create_wt_job:start() + return + end - create_wt_job:after_failure(failure('create_worktree', create_wt_job.args, vim.loop.cwd())) - fetch:after_failure(failure('create_worktree', fetch.args, worktree_path)) + Git.has_branch(branch, { '--remotes' }, function(found_remote_branch) + Log.debug('Found remote branch %s? %s', branch, found_remote_branch) + if found_remote_branch then + upstream = branch + branch = 'local/' .. branch + end + Git.has_branch(branch, nil, function(found_branch) + Log.debug('Found branch %s? %s', branch, found_branch) + Git.has_branch(upstream, { '--all' }, function(found_upstream) + Log.debug('Found upstream %s? %s', upstream, found_upstream) - set_branch:after_failure(failure('create_worktree', set_branch.args, worktree_path, true)) + local create_wt_job = Git.create_worktree_job(path, branch, found_branch, upstream, found_upstream) - rebase:after(function() - if rebase.code ~= 0 then - Log.devel("Rebase failed, but that's ok.") + if found_branch and found_upstream and branch ~= upstream then + local set_remote = Git.setbranch_job(path, branch, upstream) + create_wt_job:and_then_on_success(set_remote) end - vim.schedule(function() - Hooks.emit(Hooks.type.CREATE, path, branch, upstream) - M.switch(path) - end) - end) - else - create_wt_job:after(function() - vim.schedule(function() - Hooks.emit(Hooks.type.CREATE, path, branch, upstream) - M.switch(path) + create_wt_job:after(function() + vim.schedule(function() + Hooks.emit(Hooks.type.CREATE, path, branch, upstream) + M.switch(path) + end) end) + + create_wt_job:start() end) - end - create_wt_job:start() + end) end) end) end @@ -167,12 +169,12 @@ function M.delete(path, force, opts) opts = {} end - Git.has_worktree(path, function(found) - Log.info('OMG here') + local branch = Git.parse_head(path) + + Git.has_worktree(path, nil, function(found) if not found then Log.error('Worktree %s does not exist', path) - else - Log.info('Worktree %s does exist', path) + return end local delete = Git.delete_worktree_job(path, force) @@ -180,7 +182,7 @@ function M.delete(path, force, opts) Log.info('delete after success') Hooks.emit(Hooks.type.DELETE, path) if opts.on_success then - opts.on_success() + opts.on_success({ branch = branch }) end end)) diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index dc5fee0..73a926a 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -8,6 +8,8 @@ local action_state = require('telescope.actions.state') local conf = require('telescope.config').values local git_worktree = require('git-worktree') local Config = require('git-worktree.config') +local Git = require('git-worktree.git') +local Log = require('git-worktree.logger') local force_next_deletion = false @@ -16,6 +18,9 @@ local force_next_deletion = false -- @return string: the path of the selected worktree local get_worktree_path = function(prompt_bufnr) local selection = action_state.get_selected_entry(prompt_bufnr) + if selection == nil then + return + end return selection.path end @@ -24,10 +29,12 @@ end -- @return nil local switch_worktree = function(prompt_bufnr) local worktree_path = get_worktree_path(prompt_bufnr) - actions.close(prompt_bufnr) - if worktree_path ~= nil then - git_worktree.switch_worktree(worktree_path) + if worktree_path == nil then + vim.print('No worktree selected') + return end + actions.close(prompt_bufnr) + git_worktree.switch_worktree(worktree_path) end -- Toggle the forced deletion of the next worktree @@ -44,55 +51,75 @@ local toggle_forced_deletion = function() end end --- Handler for successful deletion --- @return nil -local delete_success_handler = function() - force_next_deletion = false -end - --- Handler for failed deletion --- @return nil -local delete_failure_handler = function() - print('Deletion failed, use to force the next deletion') -end - --- Ask the user to confirm the deletion of a worktree +-- Confirm the deletion of a worktree -- @param forcing boolean: whether the deletion is forced -- @return boolean: whether the deletion is confirmed -local ask_to_confirm_deletion = function(forcing) +local confirm_worktree_deletion = function(forcing) + if not Config.confirm_telescope_deletions then + return true + end + + local confirmed = nil if forcing then - return vim.fn.input('Force deletion of worktree? [y/n]: ') + confirmed = vim.fn.input('Force deletion of worktree? [y/n]: ') + else + confirmed = vim.fn.input('Delete worktree? [y/n]: ') + end + + if string.sub(string.lower(confirmed), 0, 1) == 'y' then + return true end - return vim.fn.input('Delete worktree? [y/n]: ') + print("Didn't delete worktree") + return false end -- Confirm the deletion of a worktree --- @param forcing boolean: whether the deletion is forced -- @return boolean: whether the deletion is confirmed -local confirm_deletion = function(forcing) - if not Config.confirm_telescope_deletions then - return true - end - - local confirmed = ask_to_confirm_deletion(forcing) +local confirm_branch_deletion = function() + local confirmed = vim.fn.input('Worktree deleted, now force deletion of branch? [y/n]: ') if string.sub(string.lower(confirmed), 0, 1) == 'y' then return true end - print("Didn't delete worktree") + print("Didn't delete branch") return false end +-- Handler for successful deletion +-- @return nil +local delete_success_handler = function(opts) + opts = opts or {} + force_next_deletion = false + if opts.branch ~= nil and opts.branch ~= 'HEAD' and confirm_branch_deletion() then + local delete_branch_job = Git.delete_branch_job(opts.branch) + if delete_branch_job ~= nil then + delete_branch_job:after_success(vim.schedule_wrap(function() + print('Branch deleted') + end)) + delete_branch_job:start() + end + end +end + +-- Handler for failed deletion +-- @return nil +local delete_failure_handler = function() + print('Deletion failed, use to force the next deletion') +end + -- Delete the selected worktree -- @param prompt_bufnr number: the prompt buffer number -- @return nil local delete_worktree = function(prompt_bufnr) - if not confirm_deletion() then + -- TODO: confirm_deletion(forcing) + if not confirm_worktree_deletion() then return end + git_worktree.switch_worktree(nil) + local worktree_path = get_worktree_path(prompt_bufnr) actions.close(prompt_bufnr) if worktree_path ~= nil then @@ -106,39 +133,96 @@ end -- Create a prompt to get the path of the new worktree -- @param cb function: the callback to call with the path -- @return nil -local create_input_prompt = function(cb) - local subtree = vim.fn.input('Path to subtree > ') - cb(subtree) +local create_input_prompt = function(opts, cb) + opts = opts or {} + opts.pattern = nil -- show all branches that can be tracked + + local path = vim.fn.input('Path to subtree > ', opts.branch) + if path == '' then + Log.error("No worktree path provided") + return + end + + if opts.branch == '' then + cb(path, nil) + return + end + + local branches = vim.fn.systemlist('git branch --all') + if #branches == 0 then + cb(path, nil) + return + end + + local re = string.format('git branch --remotes --list %s', opts.branch) + local remote_branch = vim.fn.systemlist(re) + if #remote_branch == 1 then + cb(path, nil) + return + end + + local confirmed = vim.fn.input('Track an upstream? [y/n]: ') + if string.sub(string.lower(confirmed), 0, 1) == 'y' then + opts.attach_mappings = function() + actions.select_default:replace(function(prompt_bufnr, _) + local selected_entry = action_state.get_selected_entry() + local current_line = action_state.get_current_line() + actions.close(prompt_bufnr) + local upstream = selected_entry ~= nil and selected_entry.value or current_line + cb(path, upstream) + end) + return true + end + require('telescope.builtin').git_branches(opts) + else + cb(path, nil) + end end -- Create a worktree -- @param opts table: the options for the telescope picker (optional) -- @return nil -local create_worktree = function(opts) +local telescope_create_worktree = function(opts) + git_worktree.switch_worktree(nil) opts = opts or {} - opts.attach_mappings = function() - actions.select_default:replace(function(prompt_bufnr, _) - local selected_entry = action_state.get_selected_entry() - local current_line = action_state.get_current_line() - - actions.close(prompt_bufnr) - - local branch = selected_entry ~= nil and selected_entry.value or current_line - if branch == nil then - return - end + local create_branch = function(prompt_bufnr, _) + -- if current_line is still not enough to filter everything but user + -- still wants to use it as the new branch name, without selecting anything + local branch = action_state.get_current_line() + actions.close(prompt_bufnr) + opts.branch = branch + create_input_prompt(opts, function(path, upstream) + git_worktree.create_worktree(path, branch, upstream) + end) + end - create_input_prompt(function(name) - if name == '' then - name = branch - end - git_worktree.create_worktree(name, branch) - end) + local select_or_create_branch = function(prompt_bufnr, _) + local selected_entry = action_state.get_selected_entry() + local current_line = action_state.get_current_line() + actions.close(prompt_bufnr) + -- selected_entry can be null if current_line filters everything + -- and there's no branch shown + local branch = selected_entry ~= nil and selected_entry.value or current_line + if branch == nil or branch == '' then + Log.error("No branch selected") + return + end + opts.branch = branch + create_input_prompt(opts, function(path, upstream) + git_worktree.create_worktree(path, branch, upstream) end) + end + opts.attach_mappings = function(_, map) + map({ 'i', 'n' }, '', create_branch) + actions.select_default:replace(select_or_create_branch) return true end + + -- TODO: A corner case here is that of a new bare repo which has no branch nor tree, + -- but user may want to create one using this picker when creating the first worktree. + -- Perhaps telescope git_branches should only be used for selecting the upstream to track. require('telescope.builtin').git_branches(opts) end @@ -182,9 +266,9 @@ local telescope_git_worktree = function(opts) parse_line(line) end - if #results == 0 then - return - end + -- if #results == 0 then + -- return + -- end local displayer = require('telescope.pickers.entry_display').create { separator = ' ', @@ -220,8 +304,14 @@ local telescope_git_worktree = function(opts) attach_mappings = function(_, map) action_set.select:replace(switch_worktree) - map('i', '', delete_worktree) - map('n', '', delete_worktree) + map('i', '', function() + telescope_create_worktree {} + end) + map('n', '', function() + telescope_create_worktree {} + end) + map('i', '', delete_worktree) + map('n', '', delete_worktree) map('i', '', toggle_forced_deletion) map('n', '', toggle_forced_deletion) @@ -236,6 +326,6 @@ end return require('telescope').register_extension { exports = { git_worktree = telescope_git_worktree, - create_git_worktree = create_worktree, + create_git_worktree = telescope_create_worktree, }, }