You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
4.9 KiB
Lua
186 lines
4.9 KiB
Lua
-- Git operations module for nvim-claude
|
|
local M = {}
|
|
local utils = require('nvim-claude.utils')
|
|
|
|
M.config = {}
|
|
|
|
function M.setup(config)
|
|
M.config = config or {}
|
|
end
|
|
|
|
-- Check if git worktrees are supported
|
|
function M.supports_worktrees()
|
|
local result = utils.exec('git worktree list 2>/dev/null')
|
|
return result ~= nil and not result:match('error')
|
|
end
|
|
|
|
-- Get list of existing worktrees
|
|
function M.list_worktrees()
|
|
local result = utils.exec('git worktree list --porcelain')
|
|
if not result then return {} end
|
|
|
|
local worktrees = {}
|
|
local current = {}
|
|
|
|
for line in result:gmatch('[^\n]+') do
|
|
if line:match('^worktree ') then
|
|
if current.path then
|
|
table.insert(worktrees, current)
|
|
end
|
|
current = { path = line:match('^worktree (.+)') }
|
|
elseif line:match('^HEAD ') then
|
|
current.head = line:match('^HEAD (.+)')
|
|
elseif line:match('^branch ') then
|
|
current.branch = line:match('^branch (.+)')
|
|
end
|
|
end
|
|
|
|
if current.path then
|
|
table.insert(worktrees, current)
|
|
end
|
|
|
|
return worktrees
|
|
end
|
|
|
|
-- Create a new worktree
|
|
function M.create_worktree(path, branch)
|
|
branch = branch or M.default_branch()
|
|
|
|
-- Check if worktree already exists
|
|
local worktrees = M.list_worktrees()
|
|
for _, wt in ipairs(worktrees) do
|
|
if wt.path == path then
|
|
return true, wt
|
|
end
|
|
end
|
|
|
|
-- Generate unique branch name for the worktree
|
|
local worktree_branch = 'agent-' .. utils.timestamp()
|
|
|
|
-- Create worktree with new branch based on specified branch
|
|
local cmd = string.format('git worktree add -b "%s" "%s" "%s" 2>&1', worktree_branch, path, branch)
|
|
local result, err = utils.exec(cmd)
|
|
|
|
if err then
|
|
return false, result
|
|
end
|
|
|
|
-- For background agents, ensure no hooks by creating empty .claude directory
|
|
-- This prevents inline diffs from triggering
|
|
local claude_dir = path .. '/.claude'
|
|
if vim.fn.isdirectory(claude_dir) == 0 then
|
|
vim.fn.mkdir(claude_dir, 'p')
|
|
end
|
|
|
|
-- Create empty settings.json to disable hooks
|
|
local empty_settings = '{"hooks": {}}'
|
|
utils.write_file(claude_dir .. '/settings.json', empty_settings)
|
|
|
|
return true, { path = path, branch = worktree_branch, base_branch = branch }
|
|
end
|
|
|
|
-- Remove a worktree
|
|
function M.remove_worktree(path)
|
|
-- First try to remove as git worktree
|
|
local cmd = string.format('git worktree remove "%s" --force 2>&1', path)
|
|
local result, err = utils.exec(cmd)
|
|
|
|
-- If it's not a worktree or already removed, just delete the directory
|
|
if err or result:match('not a working tree') then
|
|
local rm_cmd = string.format('rm -rf "%s"', path)
|
|
utils.exec(rm_cmd)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Add entry to .gitignore
|
|
function M.add_to_gitignore(pattern)
|
|
local gitignore_path = utils.get_project_root() .. '/.gitignore'
|
|
|
|
-- Read existing content
|
|
local content = utils.read_file(gitignore_path) or ''
|
|
|
|
-- Check if pattern already exists
|
|
if content:match('\n' .. pattern:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1') .. '\n') or
|
|
content:match('^' .. pattern:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1') .. '\n') then
|
|
return true
|
|
end
|
|
|
|
-- Append pattern
|
|
if not content:match('\n$') and content ~= '' then
|
|
content = content .. '\n'
|
|
end
|
|
content = content .. pattern .. '\n'
|
|
|
|
return utils.write_file(gitignore_path, content)
|
|
end
|
|
|
|
-- Get current branch
|
|
function M.current_branch()
|
|
local result = utils.exec('git branch --show-current 2>/dev/null')
|
|
if result then
|
|
return result:gsub('\n', '')
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Get default branch (usually main or master)
|
|
function M.default_branch()
|
|
-- Try to get the default branch from remote
|
|
local result = utils.exec('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null')
|
|
if result and result ~= '' then
|
|
local branch = result:match('refs/remotes/origin/(.+)')
|
|
if branch then
|
|
return branch:gsub('\n', '')
|
|
end
|
|
end
|
|
|
|
-- Fallback: check if main or master exists
|
|
local main_exists = utils.exec('git show-ref --verify --quiet refs/heads/main')
|
|
if main_exists and main_exists == '' then
|
|
return 'main'
|
|
end
|
|
|
|
local master_exists = utils.exec('git show-ref --verify --quiet refs/heads/master')
|
|
if master_exists and master_exists == '' then
|
|
return 'master'
|
|
end
|
|
|
|
-- Final fallback
|
|
return 'main'
|
|
end
|
|
|
|
-- Get git status
|
|
function M.status(path)
|
|
local cmd = 'git status --porcelain'
|
|
if path then
|
|
cmd = string.format('cd "%s" && %s', path, cmd)
|
|
end
|
|
|
|
local result = utils.exec(cmd)
|
|
if not result then return {} end
|
|
|
|
local files = {}
|
|
for line in result:gmatch('[^\n]+') do
|
|
local status, file = line:match('^(..) (.+)$')
|
|
if status and file then
|
|
table.insert(files, { status = status, file = file })
|
|
end
|
|
end
|
|
|
|
return files
|
|
end
|
|
|
|
-- Get diff between two paths
|
|
function M.diff(path1, path2)
|
|
local cmd = string.format(
|
|
'git diff --no-index --name-status "%s" "%s" 2>/dev/null',
|
|
path1,
|
|
path2
|
|
)
|
|
local result = utils.exec(cmd)
|
|
return result or ''
|
|
end
|
|
|
|
return M |