Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@
- `lua/git-wip/init.lua` is the plugin for Neovim written in Lua
- `vim/plugin/git-wip.vim` is the legacy plugin for Vim written in VimL -- maintained, but not actively improved

## Lua Plugin Configuration

The Neovim Lua plugin supports the following configuration options (set via `opts` in lazy.nvim or passed to `setup()`):

- `git_wip_path`: Path to the git-wip binary (default: "git-wip")
- `gpg_sign`: nil (default), true (--gpg-sign), false (--no-gpg-sign)
- `untracked`: nil (default), true (--untracked), false (--no-untracked)
- `ignored`: nil (default), true (--ignored), false (--no-ignored)
- `background`: false (default, sync execution), true (async if Neovim 0.10+, else sync with warning)
- `filetypes`: Array of filetypes to enable (default: { "*" } for all)

Async execution uses Neovim's `vim.system` with `on_exit` callback for non-blocking saves.

## Test Infrastructure

### test/cli/lib.sh
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,11 @@ $ git wip -h
{
"bartman/git-wip",
opts = {
gpg_sign = false,
untracked = true,
ignored = false,
filetypes = { "*" },
gpg_sign = false, -- true enables GPG signing of commits
untracked = true, -- true to include untracked files
ignored = false, -- true to include files ignored by .gitignore
background = false, -- true for async execution if supported (Neovim 0.10+), false for sync
filetypes = { "*" }, -- list of vim file types to call git-wip on
},
},
```
Expand Down Expand Up @@ -349,7 +350,7 @@ $ git diff --cached

Adjust with `git reset` / `git add -p` as needed, then commit.

# Appendix
# Appendix

## related projects

Expand Down
133 changes: 87 additions & 46 deletions lua/git-wip/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,31 @@ local M = {}
-- Detect Neovim version for API compatibility
-- vim.system is available in Neovim 0.10+
local has_vim_system = vim.system ~= nil
local has_loop_spawn = vim.loop.spawn ~= nil
local has_loop_hrtime = vim.loop and vim.loop.hrtime ~= nil

-- Configuration
M.defaults = {
git_wip_path = "git-wip", -- path to git-wip binary (can be absolute)
gpg_sign = nil, -- true for --gpg-sign, false for --no-gpg-sign
untracked = nil, -- true for --untracked, false for --no-untracked
ignored = nil, -- true for --ignored, false for --no-ignored
background = nil, -- true for async execution if supported, false for sync
filetypes = { "*" },
}

---@type table
M.config = M.defaults

---Wrapper for vim.loop.hrtime(), returns 0 if not available
local function get_hrtime()
if has_loop_hrtime then
return vim.loop.hrtime()
else
return 0
end
end

---Helper for tri-state flags
local function add_tri_flag(cmd, value, positive, negative)
if value == true then
Expand All @@ -28,6 +40,76 @@ local function add_tri_flag(cmd, value, positive, negative)
-- nil = do nothing (git-wip default)
end

---Helper: Build command array
local function build_command(display_name, filename)
local cmd = { M.config.git_wip_path, "save", string.format("WIP from neovim for %s", display_name) }
add_tri_flag(cmd, M.config.gpg_sign, "--gpg-sign", "--no-gpg-sign")
add_tri_flag(cmd, M.config.untracked, "--untracked", "--no-untracked")
add_tri_flag(cmd, M.config.ignored, "--ignored", "--no-ignored")
table.insert(cmd, "--editor")
if filename ~= nil then
table.insert(cmd, "--")
table.insert(cmd, filename)
end
return cmd
end

---Helper: Notify result
local function notify_result(display_name, code, elapsed)
if code == 0 then
local msg = "[git-wip] saved " .. display_name
if elapsed and elapsed > 0 then
msg = msg .. string.format(" in %.3f sec", elapsed)
end
vim.notify(msg, vim.log.levels.INFO)
else
local msg = "[git-wip] failed for " .. display_name .. " (exit " .. code .. ")"
if M.config.background and not has_loop_spawn then
msg = msg .. " (async not supported, ran sync)"
end
vim.notify(msg, vim.log.levels.WARN)
end
end

---Helper: Run sync
local function run_sync(cmd, dir, display_name)
local start = get_hrtime()
local code = 0
if has_vim_system then
local job = vim.system(cmd, { cwd = dir, text = true })
local result = job:wait()
code = result.code
else
local shell_cmd = "cd " .. vim.fn.shellescape(dir) .. " && " .. cmd[1]
for i = 2, #cmd do
shell_cmd = shell_cmd .. " " .. vim.fn.shellescape(cmd[i])
end
vim.fn.system(shell_cmd)
code = vim.v.shell_error
end
local elapsed = (get_hrtime() - start) / 1e9
notify_result(display_name, code, elapsed)
end

---Helper: Run async
local function run_async(cmd, dir, display_name)
local unpack = table.unpack or unpack -- unpack is deprecated
local start = get_hrtime()
local handle
handle = vim.loop.spawn(cmd[1], {
args = {unpack(cmd, 2)},
cwd = dir,
stdio = {nil, nil, nil},
}, function(code, signal)
local elapsed = has_loop_hrtime and (get_hrtime() - start) / 1e9 or 0
notify_result(display_name, code, elapsed)
handle:close()
end)
if not handle then
vim.notify("Failed to spawn git-wip process", vim.log.levels.ERROR)
end
end

---Setup function (automatically called by Lazy.nvim)
---@param opts? table
function M.setup(opts)
Expand Down Expand Up @@ -65,53 +147,12 @@ function M.setup(opts)
end

function M.RunGitWip(dir, filename)
local display_name = '*'
if filename ~= nil then
display_name = filename
end

-- Build the command using config options
local cmd = { M.config.git_wip_path, "save", string.format("WIP from neovim for %s", display_name) }

-- Tri-state flags
add_tri_flag(cmd, M.config.gpg_sign, "--gpg-sign", "--no-gpg-sign")
add_tri_flag(cmd, M.config.untracked, "--untracked", "--no-untracked")
add_tri_flag(cmd, M.config.ignored, "--ignored", "--no-ignored")

table.insert(cmd, "--editor")
if filename ~= nil then
table.insert(cmd, "--")
table.insert(cmd, filename) -- the actual file (full path)
end

-- Run git-wip from the correct directory
if has_vim_system then
-- Neovim 0.10+: use async vim.system with job:wait()
local start = vim.loop.hrtime()
local job = vim.system(cmd, { cwd = dir, text = true })
local result = job:wait()
local elapsed = (vim.loop.hrtime() - start) / 1e9

vim.schedule(function()
if result.code == 0 then
vim.notify(string.format("[git-wip] saved %s in %.3f sec", display_name, elapsed), vim.log.levels.INFO)
else
vim.notify(string.format("[git-wip] failed for %s (exit %d)", display_name, result.code), vim.log.levels.WARN)
end
end)
local display_name = filename or '*'
local cmd = build_command(display_name, filename)
if M.config.background and has_loop_spawn then
run_async(cmd, dir, display_name)
else
-- Neovim < 0.10: use vim.fn.system() with shell cd
local shell_cmd = "cd " .. vim.fn.shellescape(dir) .. " && " .. cmd[1]
for i = 2, #cmd do
shell_cmd = shell_cmd .. " " .. vim.fn.shellescape(cmd[i])
end
vim.fn.system(shell_cmd)

if vim.v.shell_error == 0 then
vim.notify(string.format("[git-wip] saved %s", display_name), vim.log.levels.INFO)
else
vim.notify(string.format("[git-wip] failed for %s", display_name), vim.log.levels.WARN)
end
run_sync(cmd, dir, display_name)
end
end

Expand Down
2 changes: 1 addition & 1 deletion test/nvim/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ find_program(NVIM_EXECUTABLE nvim)
if(NVIM_EXECUTABLE)
message(STATUS "Found nvim: ${NVIM_EXECUTABLE}")

foreach(TEST_NAME IN ITEMS test_nvim_single test_nvim_buffers test_nvim_windows)
foreach(TEST_NAME IN ITEMS test_nvim_single test_nvim_buffers test_nvim_windows test_nvim_background)
add_test(
NAME "nvim/${TEST_NAME}"
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/${TEST_NAME}.sh"
Expand Down
2 changes: 2 additions & 0 deletions test/nvim/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ require("git-wip").setup({
git_wip_path = git_wip_path,
untracked = false,
ignored = false,
background = os.getenv("GIT_WIP_TEST_BACKGROUND") == "true",
filetypes = { "*" },
})
INITLUA
Expand All @@ -118,6 +119,7 @@ INITLUA

# Run nvim with a watchdog timeout (10 seconds max)
# This prevents tests from hanging indefinitely
GIT_WIP_TEST_BACKGROUND="${GIT_WIP_TEST_BACKGROUND:-false}" \
GIT_WIP="$GIT_WIP" \
REPO_ROOT="$REPO_ROOT" \
timeout 10 "$NVIM" --headless -u "$REPO/init.lua" "${nvim_args[@]}" -c "quit!" >"$NVIM_OUT" 2>&1
Expand Down
60 changes: 60 additions & 0 deletions test/nvim/test_nvim_background.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# test_nvim_background.sh -- Test git-wip with background=true (async execution)
source "$(dirname "$0")/lib.sh"

# -------------------------------------------------------------------------
# Setup: create a test repo with one file

create_test_repo

# Create initial file and commit
echo "initial content" > file.txt
git add file.txt
git commit -m "initial commit"

note "Repo created at $REPO"

# Set background=true for async execution
export GIT_WIP_TEST_BACKGROUND=true

# -------------------------------------------------------------------------
# Test 1: Single save (one edit + :w)

note "Test 1: Single save with background=true"

# Make a change and save using nvim
run_nvim "edit file.txt" "normal osecond line" "write"

# Verify WIP commit was created
run git for-each-ref | grep -q "refs/wip/master"
note "WIP ref exists after single save"

# Verify the change is in the WIP tree
run git show wip/master:file.txt | grep -q "second line"
note "Change captured in WIP tree"

# Verify exactly 1 WIP commit
"$GIT_WIP" status | grep -q "branch master has 1 wip commit"
note "Exactly 1 WIP commit"

# -------------------------------------------------------------------------
# Test 2: Two saves (two edits)

note "Test 2: Two saves with background=true"

# Make another change and save
run_nvim "edit file.txt" "normal othird line" "write"

# Verify exactly 2 WIP commits
"$GIT_WIP" status | grep -q "branch master has 2 wip commit"
note "Exactly 2 WIP commits"

# Verify latest WIP has third line
run git show wip/master:file.txt | grep -q "third line"
note "Third line in WIP tree"

# Verify first WIP still has second line but not third
run git show wip/master~1:file.txt | grep -q "second line"
note "First WIP still has second line"

echo "OK: $TEST_NAME"
Loading