updated how convert works; added few commands to make worktrees a bit easier to use #12
+278
-104
@@ -1,9 +1,17 @@
|
||||
-- Worktree-aware git project switching.
|
||||
--
|
||||
-- Turns a plain single-worktree checkout into a worktree-style layout where
|
||||
-- every local branch is checked out in its own linked worktree, and provides a
|
||||
-- Telescope picker to switch between branches (creating / fetching worktrees
|
||||
-- on demand).
|
||||
-- Two layouts are supported, detected automatically from `git worktree list`:
|
||||
--
|
||||
-- • Bare-home layout (created by `convert`): the repo's main directory IS a
|
||||
-- bare repo (no working tree) and every local branch is checked out in a
|
||||
-- linked worktree under `<home>/.worktrees/<branch>`. nvim can be opened
|
||||
-- in the bare home itself to switch branches / create worktrees.
|
||||
-- • Plain layout: the usual single-checkout repo, where switching between
|
||||
-- branches materialises linked worktrees in `<parent>/.worktrees/…`.
|
||||
--
|
||||
-- The Telescope picker switches between branches (creating / fetching
|
||||
-- worktrees on demand) and offers creating a new branch from the worktree
|
||||
-- that is currently open.
|
||||
--
|
||||
-- Loaded lazily: the module is only required when a keymap, command or the
|
||||
-- explorer title calls into it (see lua/plugins/worktree.lua and
|
||||
@@ -17,9 +25,12 @@ local M = {}
|
||||
|
||||
---@class git-worktree.Opts
|
||||
M.config = {
|
||||
-- Where linked worktrees are created. Defaults to `<parent>/.worktrees/<project>/<branch>`
|
||||
-- (e.g. `~/src/.worktrees/desi_explorer/feat-api_connection`). Branch names
|
||||
-- are sanitised so `/` maps to `-`: any valid branch becomes a valid path.
|
||||
-- Where linked worktrees are created for a non-bare (plain) checkout.
|
||||
-- Defaults to `<parent>/.worktrees/<project>/<branch>` (e.g.
|
||||
-- `~/src/.worktrees/desi_explorer/feat-api_connection`). For the bare-home
|
||||
-- layout the worktrees always live under `<home>/.worktrees/<branch>` instead
|
||||
-- (see git-worktree.worktree_dir). Branch names are sanitised so `/` maps to
|
||||
-- `-`: any valid branch becomes a valid path.
|
||||
worktree_dir = function(branch, repo)
|
||||
local parent = vim.fs.dirname(repo)
|
||||
local project = vim.fs.basename(repo)
|
||||
@@ -76,20 +87,56 @@ local function git_async(args, cb)
|
||||
end)
|
||||
end
|
||||
|
||||
---Absolute path to the git root of the cwd, or nil when not in a repo.
|
||||
---Normalise a path: make it absolute and drop trailing slashes.
|
||||
---@param p string
|
||||
---@return string
|
||||
local function norm(p)
|
||||
return vim.fn.fnamemodify(p, ":p"):gsub("/+$", "")
|
||||
end
|
||||
|
||||
---Absolute path to the git root of `cwd`: the top-level of the worktree being
|
||||
---worked in. Returns nil when `cwd` is not inside a working tree (which is
|
||||
---normally the case when nvim sits in a bare repo home).
|
||||
---@param cwd? string
|
||||
---@return string?
|
||||
function M.git_root()
|
||||
local res = git({ "rev-parse", "--show-toplevel" })
|
||||
function M.git_root(cwd)
|
||||
local res = git({ "rev-parse", "--show-toplevel" }, { cwd = cwd })
|
||||
if res.code ~= 0 or res.stdout == "" then
|
||||
return nil
|
||||
end
|
||||
return res.stdout -- already absolute; drop any trailing slash
|
||||
end
|
||||
|
||||
---Name of the branch checked out in the current worktree (nil when detached).
|
||||
---Root of the whole worktree layout for `cwd`: the bare home directory when the
|
||||
---repo uses the bare-home layout, or the main (plain) checkout otherwise.
|
||||
---Both are the first `worktree` entry of `git worktree list --porcelain`.
|
||||
---@param cwd? string
|
||||
---@return string?
|
||||
function M.current_branch()
|
||||
local res = git({ "symbolic-ref", "-q", "--short", "HEAD" })
|
||||
function M.home(cwd)
|
||||
local res = git({ "worktree", "list", "--porcelain" }, { cwd = cwd })
|
||||
if res.code ~= 0 then
|
||||
return nil
|
||||
end
|
||||
local path = res.stdout:match("^worktree ([^\n]+)")
|
||||
if not path then
|
||||
return nil
|
||||
end
|
||||
return norm(path)
|
||||
end
|
||||
|
||||
---Whether the repo at `cwd` is bare (i.e. it has no working tree).
|
||||
---@param cwd? string
|
||||
---@return boolean
|
||||
function M.is_bare(cwd)
|
||||
local res = git({ "rev-parse", "--is-bare-repository" }, { cwd = cwd })
|
||||
return res.code == 0 and res.stdout == "true"
|
||||
end
|
||||
|
||||
---Name of the branch checked out in the current worktree (nil when detached).
|
||||
---@param cwd? string
|
||||
---@return string?
|
||||
function M.current_branch(cwd)
|
||||
local res = git({ "symbolic-ref", "-q", "--short", "HEAD" }, { cwd = cwd })
|
||||
if res.code ~= 0 then
|
||||
return nil
|
||||
end
|
||||
@@ -104,9 +151,10 @@ end
|
||||
---@field locked? boolean
|
||||
|
||||
---Parse `git worktree list --porcelain` into records.
|
||||
---@param cwd? string
|
||||
---@return git-worktree.Worktree[]
|
||||
function M.worktrees()
|
||||
local res = git({ "worktree", "list", "--porcelain" })
|
||||
function M.worktrees(cwd)
|
||||
local res = git({ "worktree", "list", "--porcelain" }, { cwd = cwd })
|
||||
if res.code ~= 0 then
|
||||
return {}
|
||||
end
|
||||
@@ -116,7 +164,7 @@ function M.worktrees()
|
||||
if line == "" then
|
||||
cur = nil
|
||||
elseif line:sub(1, 8) == "worktree" then
|
||||
cur = { path = vim.fn.fnamemodify(line:sub(10), ":p"):gsub("/+$", "") }
|
||||
cur = { path = norm(line:sub(10)) }
|
||||
table.insert(wts, cur)
|
||||
elseif line:sub(1, 6) == "branch" and cur then
|
||||
cur.branch = line:sub(8):gsub("^refs/heads/", "")
|
||||
@@ -132,9 +180,10 @@ function M.worktrees()
|
||||
end
|
||||
|
||||
---Names of all local branches.
|
||||
---@param cwd? string
|
||||
---@return string[]
|
||||
function M.local_branches()
|
||||
local res = git({ "for-each-ref", "--format=%(refname:short)", "refs/heads" })
|
||||
function M.local_branches(cwd)
|
||||
local res = git({ "for-each-ref", "--format=%(refname:short)", "refs/heads" }, { cwd = cwd })
|
||||
if res.code ~= 0 then
|
||||
return {}
|
||||
end
|
||||
@@ -183,7 +232,10 @@ function M.branches()
|
||||
end
|
||||
end
|
||||
|
||||
local current = M.current_branch()
|
||||
-- Current branch = the one checked out in the worktree we are sitting in. A
|
||||
-- bare home has no working tree, so nothing is "current" there (symbolic-ref
|
||||
-- would just report the bare repo's default HEAD).
|
||||
local current = M.git_root() and M.current_branch() or nil
|
||||
|
||||
-- Preserve for-each-ref's date ordering (newest first) across both namespaces
|
||||
local ordered = {} ---@type {name: string, info: string[], remote: boolean}[]
|
||||
@@ -247,16 +299,23 @@ end
|
||||
-- Worktree operations
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
---Target directory for a branch's linked worktree.
|
||||
---Target directory for a branch's linked worktree, rooted at the layout home.
|
||||
---For a bare home the linked worktrees live inside it, under `.worktrees/`; for
|
||||
---a plain checkout they stay next to it (`.worktrees/` beside the repo).
|
||||
---@param branch string
|
||||
---@param repo string git root
|
||||
---@param home string worktree-layout home directory (see M.home())
|
||||
---@return string
|
||||
function M.worktree_dir(branch, repo)
|
||||
return (M.config.worktree_dir or function(b, r)
|
||||
local parent = vim.fs.dirname(r)
|
||||
local project = vim.fs.basename(r)
|
||||
return parent .. "/.worktrees/" .. project .. "/" .. b:gsub("/", "-")
|
||||
end)(branch, repo)
|
||||
function M.worktree_dir(branch, home)
|
||||
if M.is_bare(home) then
|
||||
return vim.fs.joinpath(home, ".worktrees", (branch:gsub("/", "-")))
|
||||
end
|
||||
local fn = M.config.worktree_dir
|
||||
or function(b, r)
|
||||
local parent = vim.fs.dirname(r)
|
||||
local project = vim.fs.basename(r)
|
||||
return parent .. "/.worktrees/" .. project .. "/" .. b:gsub("/", "-")
|
||||
end
|
||||
return fn(branch, home)
|
||||
end
|
||||
|
||||
---Materialise `entry` as a linked worktree (creating a tracking local branch
|
||||
@@ -267,11 +326,11 @@ function M.ensure_worktree(entry)
|
||||
if entry.worktree then
|
||||
return { ok = true, path = entry.worktree, err = nil }
|
||||
end
|
||||
local repo = M.git_root()
|
||||
if not repo then
|
||||
local home = M.home()
|
||||
if not home then
|
||||
return { ok = false, path = nil, err = "Not inside a git repo" }
|
||||
end
|
||||
local dir = M.worktree_dir(entry.name, repo)
|
||||
local dir = M.worktree_dir(entry.name, home)
|
||||
if vim.fn.isdirectory(dir) == 1 then
|
||||
return { ok = false, path = nil, err = "Target exists and is not a registered worktree: " .. dir }
|
||||
end
|
||||
@@ -301,8 +360,8 @@ function M.remove_entry(entry)
|
||||
return
|
||||
end
|
||||
if entry.worktree then
|
||||
if entry.worktree == M.git_root() then
|
||||
vim.notify("Cannot remove the main worktree (" .. entry.worktree .. ")", vim.log.levels.WARN)
|
||||
if entry.worktree == M.home() then
|
||||
vim.notify("Cannot remove the home worktree (" .. entry.worktree .. ")", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local choice = vim.fn.confirm(("Remove worktree for %s?\n%s"):format(entry.name, entry.worktree), "&Yes\n&No", 2)
|
||||
@@ -369,11 +428,11 @@ function M.new_worktree(entry)
|
||||
if base_name ~= nil and (base_name == "" or base_name:sub(1, 1) == ":") then
|
||||
base_name = nil -- empty input → default
|
||||
end
|
||||
local repo = M.git_root()
|
||||
if not repo then
|
||||
local home = M.home()
|
||||
if not home then
|
||||
return
|
||||
end
|
||||
local dir = M.worktree_dir(name, repo)
|
||||
local dir = M.worktree_dir(name, home)
|
||||
if vim.fn.isdirectory(dir) == 1 then
|
||||
vim.notify("Target already exists: " .. dir, vim.log.levels.ERROR)
|
||||
return
|
||||
@@ -458,84 +517,152 @@ function M.switch(entry)
|
||||
M.open_worktree(res.path, entry.name)
|
||||
end
|
||||
|
||||
---Convert the current repo from a single-worktree checkout into a worktree
|
||||
---layout: every local branch gets its own linked worktree, so no branch that
|
||||
---was on the machine is lost. The branch in `keep` (default: the current one)
|
||||
---stays checked out in the main worktree.
|
||||
---@param opts? {keep?: string}
|
||||
---Convert the current repo into a bare-home worktree layout:
|
||||
---
|
||||
--- 1. The whole directory (working tree + `.git`) is copied to a temp backup.
|
||||
--- 2. The current directory (which must hold the repo's `.git`) is turned into
|
||||
--- a bare repo — it becomes the layout's home with no working tree.
|
||||
--- 3. Every local branch is materialised as a linked worktree under
|
||||
--- `<home>/.worktrees/<branch>`, so nvim can be opened in the bare home and
|
||||
--- switch branches / create worktrees from there.
|
||||
---
|
||||
---Any linked worktrees that already exist (e.g. from an earlier, legacy
|
||||
---conversion in a sibling `.worktrees/` dir) are re-anchored and kept.
|
||||
---
|
||||
---Must be run from the repo's main checkout (the directory holding `.git`),
|
||||
---on a repo that is not already bare.
|
||||
---@param opts? {keep?: string} `keep` (default: the previously checked-out
|
||||
---branch) is re-opened after conversion
|
||||
function M.convert(opts)
|
||||
opts = opts or {}
|
||||
local repo = M.git_root()
|
||||
if not repo then
|
||||
local cwd = norm(vim.uv.cwd())
|
||||
if M.is_bare(cwd) then
|
||||
vim.notify("Already a bare repo — nothing to convert", vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
local root = M.git_root()
|
||||
if not root then
|
||||
vim.notify("Not inside a git repo", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local project = vim.fs.basename(repo)
|
||||
local cur = M.current_branch()
|
||||
local keep = opts.keep or cur
|
||||
if not keep then
|
||||
if norm(root) ~= cwd then
|
||||
vim.notify(
|
||||
("Detached HEAD — pass --keep=<branch> to choose which branch stays at %s"):format(repo),
|
||||
("Convert must run from the repo's main directory (where `.git` lives).\nCurrent dir: %s\nRepo root: %s"):format(
|
||||
cwd,
|
||||
root
|
||||
),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
if keep ~= cur then
|
||||
local res = git({ "switch", "--", keep })
|
||||
if res.code ~= 0 then
|
||||
vim.notify(("Could not switch to --keep branch %s:\n%s"):format(keep, res.stderr), vim.log.levels.ERROR)
|
||||
return
|
||||
local git_entry = cwd .. "/.git"
|
||||
if vim.fn.isdirectory(git_entry) ~= 1 then
|
||||
if vim.fn.filereadable(git_entry) == 1 then
|
||||
vim.notify(
|
||||
"You are inside a linked worktree of an already-converted repo.\n"
|
||||
.. "Open nvim in its bare home ("
|
||||
.. (M.home() or "?")
|
||||
.. ") instead",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
else
|
||||
vim.notify("Expected a `.git` directory in " .. cwd .. " — nothing to convert", vim.log.levels.ERROR)
|
||||
end
|
||||
cur = keep
|
||||
end
|
||||
|
||||
local branches = M.local_branches()
|
||||
local todo = {}
|
||||
for _, b in ipairs(branches) do
|
||||
if b ~= keep then
|
||||
todo[#todo + 1] = b
|
||||
end
|
||||
end
|
||||
if #todo == 0 then
|
||||
vim.notify(
|
||||
("%s only has local branch %s — nothing else to materialise"):format(project, keep),
|
||||
vim.log.levels.INFO
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
-- Branches already checked out somewhere (idempotent re-runs)
|
||||
local project = vim.fs.basename(cwd)
|
||||
local branches = M.local_branches()
|
||||
if #branches == 0 then
|
||||
vim.notify(("%s has no local branches to materialise"):format(project), vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local cur = M.current_branch()
|
||||
|
||||
local dirty = git({ "status", "--porcelain" })
|
||||
local dirty_msg = dirty.code == 0
|
||||
and dirty.stdout ~= ""
|
||||
and "\nUncommitted / untracked files are only preserved in the backup.\n"
|
||||
or ""
|
||||
local choice = vim.fn.confirm(
|
||||
("Turn %s into a bare home repo?\n"):format(project)
|
||||
.. "\n"
|
||||
.. (" %s becomes a bare repo (no working tree)\n"):format(cwd)
|
||||
.. (" %s/.worktrees/<branch> keeps one worktree per local branch\n"):format(cwd)
|
||||
.. " the whole repo is copied to a temp backup first\n"
|
||||
.. dirty_msg
|
||||
.. "\nProceed?",
|
||||
"&Yes\n&No",
|
||||
2
|
||||
)
|
||||
if choice ~= 1 then
|
||||
vim.notify("Convert aborted", vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- 1. Full backup (working tree + .git). nvim's own temp dir is wiped on exit,
|
||||
-- so use the OS tmp dir instead so the backup survives the session.
|
||||
local tmp = vim.env.TMPDIR ~= nil and vim.env.TMPDIR ~= "" and vim.env.TMPDIR or "/tmp"
|
||||
local backup = vim.fs.joinpath(tmp, project .. "-worktree-backup-" .. os.time())
|
||||
local bump = 1
|
||||
while vim.fn.isdirectory(backup) == 1 or vim.fn.filereadable(backup) == 1 do
|
||||
bump = bump + 1
|
||||
backup = vim.fs.joinpath(tmp, project .. "-worktree-backup-" .. os.time() .. "-" .. bump)
|
||||
end
|
||||
vim.fn.mkdir(backup, "p")
|
||||
vim.notify(("Backing up %s to %s …"):format(project, backup), vim.log.levels.INFO)
|
||||
local cp = vim.system({ "cp", "-a", cwd .. "/.", backup .. "/" }, { text = true, timeout = 300000 }):wait()
|
||||
if cp.code ~= 0 then
|
||||
vim.notify(("Backup failed:\n%s\nConversion aborted"):format(cp.stderr), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- 2. Mark the repo bare while `.git` is still discoverable. Drop any stale
|
||||
-- `core.worktree` so git-dir discovery keys off the directory itself.
|
||||
local bare = git({ "config", "core.bare", "true" })
|
||||
if bare.code ~= 0 then
|
||||
vim.notify("Could not mark the repo as bare:\n" .. bare.stderr, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
git({ "config", "--unset-all", "core.worktree" })
|
||||
|
||||
-- 3. Drop the old working tree, then hoist `.git/*` so the directory itself
|
||||
-- becomes the bare git dir (backed up above, so nothing is lost).
|
||||
local entries = {}
|
||||
for e in vim.fs.dir(cwd) do
|
||||
entries[#entries + 1] = e
|
||||
end
|
||||
for _, e in ipairs(entries) do
|
||||
if e ~= ".git" then
|
||||
vim.fn.delete(cwd .. "/" .. e, "rf")
|
||||
end
|
||||
end
|
||||
for e in vim.fs.dir(cwd .. "/.git") do
|
||||
vim.fn.rename(cwd .. "/.git/" .. e, cwd .. "/" .. e)
|
||||
end
|
||||
vim.fn.delete(cwd .. "/.git", "rf")
|
||||
|
||||
-- 4. Re-anchor any linked worktrees (from a previous layout) whose paths may
|
||||
-- no longer point at the moved admin dirs.
|
||||
git({ "worktree", "repair" })
|
||||
|
||||
-- 5. Materialise every local branch as a linked worktree under the bare home.
|
||||
local existing = {} ---@type table<string, string>
|
||||
for _, wt in ipairs(M.worktrees()) do
|
||||
if wt.branch then
|
||||
existing[wt.branch] = wt.path
|
||||
end
|
||||
end
|
||||
|
||||
-- Confirm if the main worktree is dirty (only affects `--keep` switching)
|
||||
local dirty = git({ "status", "--porcelain", "--untracked-files=no" })
|
||||
if dirty.code == 0 and dirty.stdout ~= "" then
|
||||
local choice = vim.fn.confirm(
|
||||
("Uncommitted changes in %s.\nConvert anyway? (they stay in the %s worktree)"):format(project, keep),
|
||||
"&Yes\n&No",
|
||||
2
|
||||
)
|
||||
if choice ~= 1 then
|
||||
vim.notify("Convert aborted", vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local created, skipped, errors = {}, {}, {}
|
||||
for _, b in ipairs(todo) do
|
||||
vim.fn.mkdir(cwd .. "/.worktrees", "p")
|
||||
local created, kept, errors = {}, {}, {}
|
||||
for _, b in ipairs(branches) do
|
||||
if existing[b] then
|
||||
skipped[#skipped + 1] = { b, existing[b] }
|
||||
kept[#kept + 1] = { b, existing[b] }
|
||||
else
|
||||
local dir = M.worktree_dir(b, repo)
|
||||
local dir = M.worktree_dir(b, cwd)
|
||||
if vim.fn.isdirectory(dir) == 1 then
|
||||
errors[#errors + 1] = { b, "target exists and is not a worktree: " .. dir }
|
||||
else
|
||||
vim.fn.mkdir(vim.fn.fnamemodify(dir, ":h"), "p")
|
||||
local res = git({ "worktree", "add", dir, "--", b })
|
||||
if res.code == 0 then
|
||||
created[#created + 1] = { b, dir }
|
||||
@@ -546,19 +673,21 @@ function M.convert(opts)
|
||||
end
|
||||
end
|
||||
|
||||
local lines = { ("%s now uses a worktree layout (%s stays at the main worktree)"):format(project, keep) }
|
||||
-- 6. Report; then move into the branch that was checked out before the
|
||||
-- conversion.
|
||||
local lines = { ("%s is now a bare home at %s (%d branch worktrees)"):format(project, cwd, #branches) }
|
||||
if #created > 0 then
|
||||
lines[#lines + 1] = "Created " .. #created .. " worktree(s):"
|
||||
for _, c in ipairs(created) do
|
||||
lines[#lines + 1] = " " .. c[1] .. " → " .. c[2]
|
||||
end
|
||||
end
|
||||
if #skipped > 0 then
|
||||
lines[#lines + 1] = "Already materialised: "
|
||||
if #kept > 0 then
|
||||
lines[#lines + 1] = "Already checked out: "
|
||||
.. table.concat(
|
||||
vim.tbl_map(function(e)
|
||||
return e[1]
|
||||
end, skipped),
|
||||
end, kept),
|
||||
", "
|
||||
)
|
||||
end
|
||||
@@ -570,7 +699,25 @@ function M.convert(opts)
|
||||
"\n"
|
||||
), vim.log.levels.WARN)
|
||||
end
|
||||
vim.notify(table.concat(lines, "\n") .. "\nSwitch branches anytime with <leader>gw", vim.log.levels.INFO)
|
||||
lines[#lines + 1] = "Backup: " .. backup
|
||||
lines[#lines + 1] = "Open nvim in " .. cwd .. " and switch branches with <leader>gw"
|
||||
vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO)
|
||||
|
||||
local open = opts.keep or cur
|
||||
if open then
|
||||
local dir = existing[open]
|
||||
if not dir then
|
||||
for _, c in ipairs(created) do
|
||||
if c[1] == open then
|
||||
dir = c[2]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if dir then
|
||||
M.open_worktree(dir, open)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Parse `GitWorktreeConvert` command args (--keep=<branch>).
|
||||
@@ -586,18 +733,25 @@ function M.convert_prompt(fargs)
|
||||
M.convert({ keep = keep })
|
||||
end
|
||||
|
||||
---Title for the explorer window: current project + checked-out branch.
|
||||
---Title for the explorer window: project + layout home + current branch.
|
||||
---@param path? string dir to label (defaults to cwd)
|
||||
---@return string?
|
||||
function M.explorer_title(path)
|
||||
local cwd = path or vim.uv.cwd()
|
||||
local repo = git({ "rev-parse", "--show-toplevel" }, { cwd = cwd })
|
||||
if repo.code ~= 0 or repo.stdout == "" then
|
||||
return nil -- not a git dir; let the explorer use its default title
|
||||
local home = M.home(cwd)
|
||||
if not home then
|
||||
return nil -- not a git repo; let the explorer use its default title
|
||||
end
|
||||
local project = vim.fs.basename(home)
|
||||
local branch_label
|
||||
if M.git_root(cwd) then
|
||||
local branch = git({ "symbolic-ref", "-q", "--short", "HEAD" }, { cwd = cwd })
|
||||
branch_label = branch.code == 0 and branch.stdout or "detached"
|
||||
elseif M.is_bare(cwd) then
|
||||
branch_label = "bare home"
|
||||
else
|
||||
return nil
|
||||
end
|
||||
local project = vim.fs.basename(repo.stdout:gsub("/+$", ""))
|
||||
local branch = git({ "symbolic-ref", "-q", "--short", "HEAD" }, { cwd = cwd })
|
||||
local branch_label = branch.code == 0 and branch.stdout or "detached"
|
||||
local wts = git({ "worktree", "list", "--porcelain" }, { cwd = cwd })
|
||||
local count = 0
|
||||
if wts.code == 0 then
|
||||
@@ -620,7 +774,7 @@ end
|
||||
---@param opts? table telescope opts
|
||||
function M.pick(kind, opts)
|
||||
opts = opts or {}
|
||||
local repo = M.git_root()
|
||||
local repo = M.home()
|
||||
if not repo then
|
||||
vim.notify("Not inside a git repo — open nvim from a project to switch branches", vim.log.levels.ERROR)
|
||||
return
|
||||
@@ -668,14 +822,22 @@ function M.pick(kind, opts)
|
||||
|
||||
if kind == "worktree" then
|
||||
local wts = M.worktrees()
|
||||
local cur_wt = M.git_root()
|
||||
for _, wt in ipairs(wts) do
|
||||
local label = wt.branch or ("(detached " .. (wt.head or "") .. ")")
|
||||
local label
|
||||
if wt.branch then
|
||||
label = wt.branch
|
||||
elseif wt.path == repo then
|
||||
label = project .. " (bare home)"
|
||||
else
|
||||
label = "(bare " .. (wt.head or "") .. ")"
|
||||
end
|
||||
items[#items + 1] = {
|
||||
name = label,
|
||||
worktree = wt.path,
|
||||
remote = nil,
|
||||
remote_only = false,
|
||||
current = wt.path == repo,
|
||||
current = wt.path == repo or wt.path == cur_wt,
|
||||
}
|
||||
end
|
||||
local d = displayer_for(items)
|
||||
@@ -810,7 +972,11 @@ function M.pick(kind, opts)
|
||||
remap({ "i", "n" }, "<c-n>", function()
|
||||
actions.close(prompt_bufnr)
|
||||
M.new_worktree(selected())
|
||||
end, "New branch worktree")
|
||||
end, "New branch worktree (from selected)")
|
||||
remap({ "i", "n" }, "<c-b>", function()
|
||||
actions.close(prompt_bufnr)
|
||||
M.pick("worktree")
|
||||
end, "Switch to worktree picker")
|
||||
else
|
||||
remap({ "i", "n" }, "<c-d>", function()
|
||||
if selected() then
|
||||
@@ -819,6 +985,14 @@ function M.pick(kind, opts)
|
||||
reopen()
|
||||
end
|
||||
end, "Remove worktree")
|
||||
remap({ "i", "n" }, "<c-n>", function()
|
||||
actions.close(prompt_bufnr)
|
||||
M.new_worktree()
|
||||
end, "New branch worktree (from current)")
|
||||
remap({ "i", "n" }, "<c-b>", function()
|
||||
actions.close(prompt_bufnr)
|
||||
M.pick("branch")
|
||||
end, "Switch to branch picker")
|
||||
end
|
||||
return true
|
||||
end,
|
||||
|
||||
@@ -24,6 +24,13 @@ return {
|
||||
end,
|
||||
desc = "Git Worktrees",
|
||||
},
|
||||
{
|
||||
"<leader>gN",
|
||||
function()
|
||||
require("config.git-worktree").new_worktree()
|
||||
end,
|
||||
desc = "New branch worktree (from current)",
|
||||
},
|
||||
{
|
||||
"<leader>gC",
|
||||
function()
|
||||
|
||||
Reference in New Issue
Block a user