From 46cd191628d8ba681ee22245bf136e402a9e069a Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Sun, 21 Jul 2024 08:47:22 -0300 Subject: [PATCH 01/12] feat: Set --track when creating worktree, if there is an upstream Currently, the user experience when creating worktrees that track remote upstreams are not straight forward. This patch improves it by allowing the user to pass any ref to the --track option of git. --- lua/git-worktree/git.lua | 77 +++++++++++-------- lua/git-worktree/worktree.lua | 75 ++++++------------ lua/telescope/_extensions/git_worktree.lua | 88 ++++++++++++++++------ 3 files changed, 135 insertions(+), 105 deletions(-) diff --git a/lua/git-worktree/git.lua b/lua/git-worktree/git.lua index c161fad..908e46e 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,14 +111,21 @@ 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' } + opts = opts or {} + for i = 1, #opts do + args[i+1] = opts[i] + end + local job = Job:new { command = 'git', - args = { 'branch' }, + args = args, on_stdout = function(_, data) - -- remove markere on current branch + -- remove marker on current branch data = data:gsub('*', '') + data = data:gsub('remotes/', '') data = vim.trim(data) found = found or data == branch end, @@ -131,8 +141,10 @@ end --- @param path 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' } @@ -140,6 +152,11 @@ function M.create_worktree_job(path, branch, found_branch) 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) @@ -195,7 +212,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, diff --git a/lua/git-worktree/worktree.lua b/lua/git-worktree/worktree.lua index 4e85288..f6ea30f 100644 --- a/lua/git-worktree/worktree.lua +++ b/lua/git-worktree/worktree.lua @@ -67,7 +67,11 @@ function M.switch(path) if not found then Log.error('worktree does not exists, please create it first %s ', path) 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) @@ -78,7 +82,7 @@ end --- CREATE --- ---crerate a worktree +--create a worktree ---@param path string ---@param branch string ---@param upstream? string @@ -91,67 +95,32 @@ 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 + Git.has_branch(branch, nil, function(found_branch) + Git.has_branch(upstream, { '--all' }, function(found_upstream) + local create_wt_job = Git.create_worktree_job(path, branch, found_branch, upstream, found_upstream) - -- 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) + Log.debug('Found branch %s? %s', branch, found_branch) + Log.debug('Found upstream %s? %s', upstream, found_upstream) + 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 - 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)) - - set_branch:after_failure(failure('create_worktree', set_branch.args, worktree_path, true)) - - rebase:after(function() - if rebase.code ~= 0 then - Log.devel("Rebase failed, but that's ok.") - 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) end) end) - end - create_wt_job:start() + + create_wt_job:start() + end) end) end) end @@ -167,12 +136,10 @@ function M.delete(path, force, opts) opts = {} end - Git.has_worktree(path, function(found) - Log.info('OMG here') + 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) diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index dc5fee0..5c55f35 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -106,9 +106,33 @@ 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) + + local branches = vim.fn.systemlist('git branch --all') + if #branches == 0 then + cb(path, nil) + 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 @@ -116,29 +140,51 @@ end -- @return nil local create_worktree = function(opts) 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 + -- TODO: Parse this as an user option. + -- opts.pattern = 'refs/heads' -- only show local branches + + -- TODO: Enable detached HEAD worktree creation, but for this the telescope + -- picker git_branches must show refs/tags. + + 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) + if branch == nil then + return + end + 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 then + 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 From 73e64c969d83f6e636d8aaa6cf9e6288f7eaaf89 Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Sat, 27 Jul 2024 16:15:48 -0300 Subject: [PATCH 02/12] feat: Enable user to get "out" of a worktree This is especially useful when creating or deleting a worktree, to avoid deleting errors if the user is within the to-be deleted worktree or unintended nested paths when creating. --- lua/git-worktree/init.lua | 2 +- lua/git-worktree/worktree.lua | 36 ++++++++++++++++------ lua/telescope/_extensions/git_worktree.lua | 13 ++++++-- 3 files changed, 38 insertions(+), 13 deletions(-) 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 f6ea30f..f4b2ff2 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 @@ -60,12 +69,18 @@ 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) + -- TODO: do we need to send an event when getting out of a tree? + -- vim.schedule(function() + -- local prev_path = change_dirs(path) + -- Hooks.emit(Hooks.type.SWITCH, path, prev_path) + -- end) + else + if path == vim.loop.cwd() then + return end Git.has_worktree(path, nil, function(found) if not found then @@ -73,11 +88,12 @@ function M.switch(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 --- diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index 5c55f35..0916349 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -16,6 +16,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 @@ -25,9 +28,11 @@ end 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 + git_worktree.switch_worktree(worktree_path) end -- Toggle the forced deletion of the next worktree @@ -93,6 +98,8 @@ local delete_worktree = function(prompt_bufnr) return end + git_worktree.switch_worktree(nil) + local worktree_path = get_worktree_path(prompt_bufnr) actions.close(prompt_bufnr) if worktree_path ~= nil then @@ -139,6 +146,8 @@ end -- @param opts table: the options for the telescope picker (optional) -- @return nil local create_worktree = function(opts) + git_worktree.switch_worktree(nil) + opts = opts or {} -- TODO: Parse this as an user option. -- opts.pattern = 'refs/heads' -- only show local branches From 4b2854b5d4140ae58d124ed5a04863270c4ef8c6 Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Sun, 21 Jul 2024 08:53:46 -0300 Subject: [PATCH 03/12] fix: Rename create_worktree to telescope_create_worktree Follow the same naming convention of the main picker. --- lua/telescope/_extensions/git_worktree.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index 0916349..13d5a06 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -145,7 +145,7 @@ 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 {} @@ -291,6 +291,6 @@ end return require('telescope').register_extension { exports = { git_worktree = telescope_git_worktree, - create_git_worktree = create_worktree, + create_git_worktree = telescope_create_worktree, }, } From 0092aa50ab84a8294b9354acf1469a5c604b0dee Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Sun, 21 Jul 2024 14:52:44 -0300 Subject: [PATCH 04/12] fix: Add telescope_create_worktree mappings from the worktree picker This means that a user can create a worktree when there is none, so allow for an empty worktree list by not returning early if #results == 0. --- lua/telescope/_extensions/git_worktree.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index 13d5a06..dbcc162 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -237,9 +237,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 = ' ', @@ -275,6 +275,12 @@ local telescope_git_worktree = function(opts) attach_mappings = function(_, map) action_set.select:replace(switch_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) From a5b27a23b791d30d0e41add27db953b4a08be1c5 Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Mon, 22 Jul 2024 16:21:11 -0300 Subject: [PATCH 05/12] fix: Use non conflicting keymap for delete_worktree Other pickers already use for scrolling preview and telescope-file-browser.nvim use for deleting files, so use that for consistency. --- lua/telescope/_extensions/git_worktree.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index dbcc162..13b8965 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -281,8 +281,8 @@ local telescope_git_worktree = function(opts) map('n', '', function() telescope_create_worktree {} end) - map('i', '', delete_worktree) - map('n', '', delete_worktree) + map('i', '', delete_worktree) + map('n', '', delete_worktree) map('i', '', toggle_forced_deletion) map('n', '', toggle_forced_deletion) From 94b6b093dff9f6255ad274b03b753a3e5c53e288 Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Thu, 19 Sep 2024 11:02:57 -0300 Subject: [PATCH 06/12] fix: Use refname:short as format in Git.has_branch This corrects the output and make unnecessary to make use of :gsub and :trim. --- lua/git-worktree/git.lua | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lua/git-worktree/git.lua b/lua/git-worktree/git.lua index 908e46e..b29192d 100644 --- a/lua/git-worktree/git.lua +++ b/lua/git-worktree/git.lua @@ -113,20 +113,16 @@ end function M.has_branch(branch, opts, cb) local found = false - local args = { 'branch' } + local args = { 'branch', '--format=%(refname:short)' } opts = opts or {} - for i = 1, #opts do - args[i+1] = opts[i] + for _, opt in ipairs(opts) do + args[#args + 1] = opt end local job = Job:new { command = 'git', args = args, on_stdout = function(_, data) - -- remove marker on current branch - data = data:gsub('*', '') - data = data:gsub('remotes/', '') - data = vim.trim(data) found = found or data == branch end, cwd = vim.loop.cwd(), From 409a885875ef8535c02eb92472822edaf6d667df Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Thu, 19 Sep 2024 11:04:51 -0300 Subject: [PATCH 07/12] fix: Only asks to track an upstream if the selected branch is not remote --- lua/telescope/_extensions/git_worktree.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index 13b8965..560d2fb 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -122,6 +122,14 @@ local create_input_prompt = function(opts, cb) 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]: ') @@ -147,10 +155,7 @@ end -- @return nil local telescope_create_worktree = function(opts) git_worktree.switch_worktree(nil) - opts = opts or {} - -- TODO: Parse this as an user option. - -- opts.pattern = 'refs/heads' -- only show local branches -- TODO: Enable detached HEAD worktree creation, but for this the telescope -- picker git_branches must show refs/tags. From 707f98e621545c0d8a032ec4f4e724a9be26545c Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Thu, 19 Sep 2024 11:06:05 -0300 Subject: [PATCH 08/12] fix: Disambiguate remote branches Currently, if the user chooses a remote branch to checkout, it will be created locally with the same name causing it to produce an ambiguous ref which git won't be able to use as upstream to track. This defaults the new local branch to be prefixed with "local/". --- lua/git-worktree/worktree.lua | 40 +++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/lua/git-worktree/worktree.lua b/lua/git-worktree/worktree.lua index f4b2ff2..04c2eee 100644 --- a/lua/git-worktree/worktree.lua +++ b/lua/git-worktree/worktree.lua @@ -117,25 +117,33 @@ function M.create(path, branch, upstream) return end - Git.has_branch(branch, nil, function(found_branch) - Git.has_branch(upstream, { '--all' }, function(found_upstream) - local create_wt_job = Git.create_worktree_job(path, branch, found_branch, upstream, found_upstream) - + 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) - Log.debug('Found upstream %s? %s', upstream, found_upstream) - 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 - - create_wt_job:after(function() - vim.schedule(function() - Hooks.emit(Hooks.type.CREATE, path, branch, upstream) - M.switch(path) + Git.has_branch(upstream, { '--all' }, function(found_upstream) + Log.debug('Found upstream %s? %s', upstream, found_upstream) + + local create_wt_job = Git.create_worktree_job(path, branch, found_branch, upstream, found_upstream) + + 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 + + create_wt_job:after(function() + vim.schedule(function() + Hooks.emit(Hooks.type.CREATE, path, branch, upstream) + M.switch(path) + end) end) - end) - create_wt_job:start() + create_wt_job:start() + end) end) end) end) From a45a865d71cdacb36f18ea12169bc24af5dbcfb6 Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Thu, 19 Sep 2024 13:02:42 -0300 Subject: [PATCH 09/12] fix: Add completion messages --- lua/git-worktree/worktree.lua | 6 +----- lua/telescope/_extensions/git_worktree.lua | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lua/git-worktree/worktree.lua b/lua/git-worktree/worktree.lua index 04c2eee..34a5d8a 100644 --- a/lua/git-worktree/worktree.lua +++ b/lua/git-worktree/worktree.lua @@ -42,6 +42,7 @@ local function change_dirs(path) vim.cmd('clearjumps') end + print(string.format('Switched to %s', path)) return previous_worktree end @@ -73,11 +74,6 @@ local M = {} function M.switch(path) if path == nil then change_dirs(path) - -- TODO: do we need to send an event when getting out of a tree? - -- vim.schedule(function() - -- local prev_path = change_dirs(path) - -- Hooks.emit(Hooks.type.SWITCH, path, prev_path) - -- end) else if path == vim.loop.cwd() then return diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index 560d2fb..d25aa93 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -29,7 +29,7 @@ local switch_worktree = function(prompt_bufnr) local worktree_path = get_worktree_path(prompt_bufnr) actions.close(prompt_bufnr) if worktree_path == nil then - vim.print("No worktree selected") + vim.print('No worktree selected') return end git_worktree.switch_worktree(worktree_path) @@ -52,6 +52,7 @@ end -- Handler for successful deletion -- @return nil local delete_success_handler = function() + print('Deleted worktree') force_next_deletion = false end From fe4c077ba5076b0816c3212a180f922dd740863f Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Thu, 19 Sep 2024 13:03:02 -0300 Subject: [PATCH 10/12] feat: Enable user to delete branch after deleting a worktree --- lua/git-worktree/git.lua | 52 +++++++++++++++++ lua/git-worktree/worktree.lua | 4 +- lua/telescope/_extensions/git_worktree.lua | 67 +++++++++++++--------- 3 files changed, 96 insertions(+), 27 deletions(-) diff --git a/lua/git-worktree/git.lua b/lua/git-worktree/git.lua index b29192d..021f313 100644 --- a/lua/git-worktree/git.lua +++ b/lua/git-worktree/git.lua @@ -251,4 +251,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/worktree.lua b/lua/git-worktree/worktree.lua index 34a5d8a..082f1be 100644 --- a/lua/git-worktree/worktree.lua +++ b/lua/git-worktree/worktree.lua @@ -156,6 +156,8 @@ function M.delete(path, force, opts) opts = {} end + 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) @@ -167,7 +169,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 d25aa93..375aa23 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -8,6 +8,7 @@ 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 force_next_deletion = false @@ -49,53 +50,67 @@ local toggle_forced_deletion = function() end end --- Handler for successful deletion --- @return nil -local delete_success_handler = function() - print('Deleted worktree') - 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 - return vim.fn.input('Delete worktree? [y/n]: ') + if string.sub(string.lower(confirmed), 0, 1) == 'y' then + return true + end + + 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 confirm_branch_deletion() and opts.branch ~= nil then + local delete_branch_job = Git.delete_branch_job(opts.branch) + if delete_branch_job ~= nil then + 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 From 314e334e9875650e14163c407a1793a30af1c5ff Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Thu, 19 Sep 2024 15:43:42 -0300 Subject: [PATCH 11/12] fix: Close prompt only if worktree_path is not nil in switch_worktree --- lua/telescope/_extensions/git_worktree.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index 375aa23..b773a3b 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -28,11 +28,11 @@ 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 vim.print('No worktree selected') return end + actions.close(prompt_bufnr) git_worktree.switch_worktree(worktree_path) end From 0f6da2603450a252933a2dc7c724e1955e0cf13e Mon Sep 17 00:00:00 2001 From: "Ricardo B. Marliere" Date: Sat, 21 Sep 2024 10:06:32 -0300 Subject: [PATCH 12/12] feat: Enable user to create detached worktree --- lua/git-worktree/git.lua | 27 +++++++++++++--------- lua/git-worktree/worktree.lua | 13 +++++++++++ lua/telescope/_extensions/git_worktree.lua | 24 ++++++++++++------- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/lua/git-worktree/git.lua b/lua/git-worktree/git.lua index 021f313..c176936 100644 --- a/lua/git-worktree/git.lua +++ b/lua/git-worktree/git.lua @@ -135,7 +135,7 @@ function M.has_branch(branch, opts, cb) end --- @param path string ---- @param branch string +--- @param branch string? --- @param found_branch boolean --- @param upstream string --- @param found_upstream boolean @@ -144,18 +144,23 @@ function M.create_worktree_job(path, branch, found_branch, upstream, found_upstr 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) - - 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) + 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 { diff --git a/lua/git-worktree/worktree.lua b/lua/git-worktree/worktree.lua index 082f1be..d5f1228 100644 --- a/lua/git-worktree/worktree.lua +++ b/lua/git-worktree/worktree.lua @@ -113,6 +113,19 @@ function M.create(path, branch, upstream) return 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 + 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 diff --git a/lua/telescope/_extensions/git_worktree.lua b/lua/telescope/_extensions/git_worktree.lua index b773a3b..73a926a 100644 --- a/lua/telescope/_extensions/git_worktree.lua +++ b/lua/telescope/_extensions/git_worktree.lua @@ -9,6 +9,7 @@ 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 @@ -91,9 +92,12 @@ end local delete_success_handler = function(opts) opts = opts or {} force_next_deletion = false - if confirm_branch_deletion() and opts.branch ~= nil then + 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 @@ -134,6 +138,15 @@ local create_input_prompt = function(opts, cb) 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 @@ -173,17 +186,11 @@ local telescope_create_worktree = function(opts) git_worktree.switch_worktree(nil) opts = opts or {} - -- TODO: Enable detached HEAD worktree creation, but for this the telescope - -- picker git_branches must show refs/tags. - 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) - if branch == nil then - return - end opts.branch = branch create_input_prompt(opts, function(path, upstream) git_worktree.create_worktree(path, branch, upstream) @@ -197,7 +204,8 @@ local telescope_create_worktree = function(opts) -- 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 then + if branch == nil or branch == '' then + Log.error("No branch selected") return end opts.branch = branch