From 73cba30662e2a552303e085a4edad1c437405fdd Mon Sep 17 00:00:00 2001 From: Icy Avocado Date: Thu, 22 Jan 2026 11:17:53 -0700 Subject: [PATCH] [INIT] --- README.md | 50 +++++++++++ lua/merged-repl-plugin.lua | 3 + lua/repl_plugin/git.lua | 137 ++++++++++++++++++++++++++++++ lua/repl_plugin/init.lua | 87 +++++++++++++++++++ lua/repl_plugin/runner.lua | 137 ++++++++++++++++++++++++++++++ lua/repl_plugin/sync.lua | 84 +++++++++++++++++++ package-lock.json | 24 ++++++ package.json | 5 ++ test_main_repo | 1 + test_repl_repo | 1 + tests/_headless_single.lua | 35 ++++++++ tests/_headless_test.lua | 41 +++++++++ tests/repl_output_captured.log | 1 + tests/run_single.sh | 147 +++++++++++++++++++++++++++++++++ tests/run_tests.sh | 130 +++++++++++++++++++++++++++++ tests/sync_result.txt | 5 ++ 16 files changed, 888 insertions(+) create mode 100644 README.md create mode 100644 lua/merged-repl-plugin.lua create mode 100644 lua/repl_plugin/git.lua create mode 100644 lua/repl_plugin/init.lua create mode 100644 lua/repl_plugin/runner.lua create mode 100644 lua/repl_plugin/sync.lua create mode 100644 package-lock.json create mode 100644 package.json create mode 160000 test_main_repo create mode 160000 test_repl_repo create mode 100644 tests/_headless_single.lua create mode 100644 tests/_headless_test.lua create mode 100644 tests/repl_output_captured.log create mode 100755 tests/run_single.sh create mode 100755 tests/run_tests.sh create mode 100644 tests/sync_result.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..9177749 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# merged-repl-plugin + +Neovim plugin that synchronizes a REPL repository with a main repository and provides a Perl REPL workflow inside Neovim. + +Features +- Automatic synchronization of REPL repo branches with main repo (polling-based) +- `:REPL` command to open the REPL session file +- `:RUN` command to execute the REPL script and show output in a readonly buffer +- Auto-commit of REPL repo changes by default + +Defaults +- `auto_commit = true` (auto-commit changes in REPL repo if dirty) +- `output_target = 'buffer'` (REPL output appears in an in-editor readonly buffer) +- `auto_start = false` (sync timer not started automatically) + +Installation +1. Place the `lua/` directory inside your Neovim config or as part of a plugin path. +2. In your `init.lua` add: + +```lua +require('merged-repl-plugin').setup{ + main_repo_path = '/path/to/main/repo', + repl_repo_path = '/path/to/repl/repo', + repl_file = '/path/to/repl/repo/repl.pl', + auto_commit = true, -- default + output_target = 'buffer', -- default + auto_start = false, -- default +} +``` + +Usage +- `:REPL` — open the configured `repl.pl` file for editing +- `:RUN` — save and run `repl.pl`; output streams to a buffer called `REPL Output: ` +- `require('merged-repl-plugin').start_auto_sync()` — start automatic polling for branch changes +- `require('merged-repl-plugin').stop_auto_sync()` — stop automatic polling + +Testing +- A basic integration test script is provided at `tests/run_tests.sh`. +- Run it from the project root: `./tests/run_tests.sh` + +Notes & Caveats +- The plugin performs Git operations (add/commit/checkout). `auto_commit` will create commits automatically using the repo's configured Git author. +- The plugin runs Git and Perl via asynchronous jobs to avoid blocking the editor, but operations may still fail due to hooks or merge conflicts — the plugin will notify and leave repositories unchanged in those cases. +- `auto_start` is `false` by default to avoid surprising behavior. Call `start_auto_sync()` when you're ready to enable automatic syncing. + +Contributing +- Feel free to open PRs for improvements: add `auto_stash` behavior, enable file-based output for headless environments, or enhance logging. + +License +- MIT diff --git a/lua/merged-repl-plugin.lua b/lua/merged-repl-plugin.lua new file mode 100644 index 0000000..a4208a0 --- /dev/null +++ b/lua/merged-repl-plugin.lua @@ -0,0 +1,3 @@ +-- shim to new repl_plugin module +return require('repl_plugin') + diff --git a/lua/repl_plugin/git.lua b/lua/repl_plugin/git.lua new file mode 100644 index 0000000..6900789 --- /dev/null +++ b/lua/repl_plugin/git.lua @@ -0,0 +1,137 @@ +local M = {} + +local fn = vim.fn +local api = vim.api + +local function job_async(argv, opts) + opts = opts or {} + local on_stdout = opts.on_stdout + local on_stderr = opts.on_stderr + local on_exit = opts.on_exit + + local id = fn.jobstart(argv, { + stdout_buffered = true, + stderr_buffered = true, + on_stdout = vim.schedule_wrap(function(_, data, _) + if on_stdout then on_stdout(data) end + end), + on_stderr = vim.schedule_wrap(function(_, data, _) + if on_stderr then on_stderr(data) end + end), + on_exit = vim.schedule_wrap(function(_, code, _) + if on_exit then on_exit(code) end + end), + }) + return id +end + +-- get current branch name of repo_path; cb(err, branch) +function M.get_branch(repo_path, cb) + cb = cb or function() end + if not repo_path or repo_path == '' then + return cb('no_repo') + end + job_async({'git','-C',repo_path,'rev-parse','--abbrev-ref','HEAD'}, { + on_stdout = function(data) + local out = table.concat(data, '\n'):gsub('\n+$','') + cb(nil, out) + end, + on_stderr = function(data) + -- ignore stderr unless needed + end, + on_exit = function(code) + if code ~= 0 then cb('git_error') end + end, + }) +end + +-- status porcelain; cb(err, lines_table) +function M.status_porcelain(repo_path, cb) + cb = cb or function() end + job_async({'git','-C',repo_path,'status','--porcelain'}, { + on_stdout = function(data) + -- data is table of lines + cb(nil, data) + end, + on_exit = function(code) + if code ~= 0 then cb('git_error') end + end, + }) +end + +-- commit_if_dirty: if dirty and auto_commit true, run add+commit +-- opts = { auto_commit = true, message = 'msg' } +-- cb(err, result) +function M.commit_if_dirty(repo_path, opts, cb) + cb = cb or function() end + opts = opts or {} + local auto_commit = opts.auto_commit + local message = opts.message or ("Auto REPL state: " .. os.date("%Y-%m-%d %H:%M:%S")) + + M.status_porcelain(repo_path, function(err, lines) + if err then return cb('status_error') end + -- lines may contain empty string elements + local dirty = false + for _, l in ipairs(lines) do + if l and l:match('%S') then dirty = true; break end + end + if not dirty then + return cb(nil, {committed = false, reason = 'no_changes'}) + end + if not auto_commit then + return cb(nil, {committed = false, reason = 'auto_commit_disabled'}) + end + + -- run git add -A && git commit -m message + -- We'll run as a shell command via sh -c to combine commands easily + local cmd = {'sh','-c', "git -C " .. fn.shellescape(repo_path) .. " add -A && git -C " .. fn.shellescape(repo_path) .. " commit -m " .. fn.shellescape(message)} + local stdout_acc = {} + local stderr_acc = {} + job_async(cmd, { + on_stdout = function(data) for _, ln in ipairs(data) do table.insert(stdout_acc, ln) end end, + on_stderr = function(data) for _, ln in ipairs(data) do table.insert(stderr_acc, ln) end end, + on_exit = function(code) + if code == 0 then + cb(nil, {committed = true, success = true, stdout = stdout_acc, stderr = stderr_acc}) + else + cb('commit_failed', {committed = true, success = false, stdout = stdout_acc, stderr = stderr_acc, code = code}) + end + end, + }) + end) +end + +function M.branch_exists(repo_path, branch, cb) + cb = cb or function() end + job_async({'git','-C',repo_path,'rev-parse','--verify','--quiet','refs/heads/' .. branch}, { + on_exit = function(code) + if code == 0 then cb(nil, true) else cb(nil, false) end + end, + }) +end + +function M.checkout_branch(repo_path, branch, cb) + cb = cb or function() end + job_async({'git','-C',repo_path,'checkout',branch}, { + on_stdout = function(_) end, + on_stderr = function(_) end, + on_exit = function(code) + if code == 0 then cb(nil, true) else cb('checkout_failed', false) end + end, + }) +end + +-- create branch from 'from' (string), prefer using checkout then checkout -b +function M.create_branch(repo_path, branch, from, cb) + cb = cb or function() end + from = from or 'HEAD' + -- do two commands in shell: git -C repo checkout && git -C repo checkout -b + local cmd = {'sh','-c', "git -C " .. fn.shellescape(repo_path) .. " checkout " .. fn.shellescape(from) .. " && git -C " .. fn.shellescape(repo_path) .. " checkout -b " .. fn.shellescape(branch)} + job_async(cmd, { + on_exit = function(code) + if code == 0 then cb(nil, true) else cb('create_failed', false) end + end, + }) +end + +return M diff --git a/lua/repl_plugin/init.lua b/lua/repl_plugin/init.lua new file mode 100644 index 0000000..318a54e --- /dev/null +++ b/lua/repl_plugin/init.lua @@ -0,0 +1,87 @@ +local git = require('repl_plugin.git') +local runner = require('repl_plugin.runner') +local sync = require('repl_plugin.sync') + +local M = {} + +local default_config = { + main_repo_path = vim.fn.getcwd(), + repl_repo_path = nil, + repl_file = nil, + poll_interval = 5000, + auto_commit = true, + output_target = 'buffer', -- buffer or file + auto_start = false, + log_file = nil, +} + +local config = {} + +local function merge_opts(opts) + opts = opts or {} + for k, v in pairs(default_config) do + if opts[k] == nil then opts[k] = v end + end + return opts +end + +function M.setup(opts) + config = merge_opts(opts) + if not config.repl_repo_path or config.repl_repo_path == '' then + config.repl_repo_path = config.main_repo_path + end + if not config.repl_file or config.repl_file == '' then + config.repl_file = config.repl_repo_path .. '/repl.pl' + end + + -- pass config to sync module + sync.setup({ + main_repo_path = config.main_repo_path, + repl_repo_path = config.repl_repo_path, + auto_commit = config.auto_commit, + poll_interval = config.poll_interval, + }) + + -- register commands + vim.api.nvim_create_user_command('REPL', function() M.open_repl_file() end, {}) + vim.api.nvim_create_user_command('RUN', function() M.run_repl() end, {}) + + if config.auto_start then M.start_auto_sync() end +end + +function M.open_repl_file() + local file = config.repl_file + if not file or file == '' then + vim.notify('No repl_file configured', vim.log.levels.ERROR) + return + end + vim.cmd('edit ' .. vim.fn.fnameescape(file)) +end + +function M.run_repl() + local file = config.repl_file + if not file or file == '' then + vim.notify('No repl_file configured', vim.log.levels.ERROR) + return + end + runner.run_perl(file, { + repo_base = vim.fn.fnamemodify(config.repl_repo_path, ':t'), + open_split = true, + output_target = config.output_target, + repl_repo_path = config.repl_repo_path, + }) +end + +function M.start_auto_sync() + sync.start(config.poll_interval) +end + +function M.stop_auto_sync() + sync.stop() +end + +function M.sync_once(cb) + sync.sync_once(cb) +end + +return M diff --git a/lua/repl_plugin/runner.lua b/lua/repl_plugin/runner.lua new file mode 100644 index 0000000..fadd35d --- /dev/null +++ b/lua/repl_plugin/runner.lua @@ -0,0 +1,137 @@ +local M = {} +local fn = vim.fn +local api = vim.api + +local function get_buf_name(repo_base) + return "REPL Output: " .. (repo_base or "repl") +end + +local function find_or_create_buffer(name) + -- search for existing buffer with this name in the buffer list + for _, bufnr in ipairs(api.nvim_list_bufs()) do + if api.nvim_buf_get_name(bufnr) == name then + return bufnr + end + end + -- create new buffer + local bufnr = api.nvim_create_buf(false, true) -- listed=false, scratch + api.nvim_buf_set_name(bufnr, name) + api.nvim_buf_set_option(bufnr, 'buftype', 'nofile') + api.nvim_buf_set_option(bufnr, 'bufhidden', 'hide') + api.nvim_buf_set_option(bufnr, 'swapfile', false) + return bufnr +end + +local function append_lines_to_buf(bufnr, lines) + if not bufnr or not api.nvim_buf_is_valid(bufnr) then return end + api.nvim_buf_set_option(bufnr, 'modifiable', true) + local current_lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) + local start = #current_lines + api.nvim_buf_set_lines(bufnr, start, start, false, lines) + api.nvim_buf_set_option(bufnr, 'modifiable', false) +end + +-- run perl file asynchronously and stream to output buffer +-- opts: { repo_base = 'name', open_split = true } +function M.run_perl(repl_file, opts) + opts = opts or {} + local target = opts.output_target or 'buffer' + local repo_base = opts.repo_base or fn.fnamemodify(repl_file, ':h:t') + + if target == 'file' then + -- write output to file under repl_repo_path if provided, else next to repl_file + local out_path = opts.out_file or (opts.repl_repo_path and (opts.repl_repo_path .. '/repl_output.log')) or (fn.tempname() .. '_repl_output.log') + -- clear/initialize file + local f = io.open(out_path, 'w') + if f then f:write('-- REPL Run: ' .. os.date('%Y-%m-%d %H:%M:%S') .. '\n') f:close() end + + local function append_lines_to_file(path, lines) + local fh, err = io.open(path, 'a') + if not fh then return end + for _, l in ipairs(lines) do fh:write(l .. '\n') end + fh:close() + end + + local function on_stdout(data) + if data then append_lines_to_file(out_path, data) end + end + local function on_stderr(data) + if data then append_lines_to_file(out_path, data) end + end + local function on_exit(code) + append_lines_to_file(out_path, {'-- REPL exited with code: ' .. tostring(code)}) + end + + local abs_repl = fn.fnamemodify(repl_file, ':p') + local cmd = {'perl', abs_repl} + fn.jobstart(cmd, { + stdout_buffered = true, + stderr_buffered = true, + on_stdout = vim.schedule_wrap(function(_, data, _) + on_stdout(data) + end), + on_stderr = vim.schedule_wrap(function(_, data, _) + on_stderr(data) + end), + on_exit = vim.schedule_wrap(function(_, code, _) + on_exit(code) + end), + }) + + return out_path + end + + -- default buffer target + local bufname = get_buf_name(repo_base) + local bufnr = find_or_create_buffer(bufname) + + -- clear buffer + api.nvim_buf_set_option(bufnr, 'modifiable', true) + api.nvim_buf_set_lines(bufnr, 0, -1, false, {"-- REPL Run: " .. os.date('%Y-%m-%d %H:%M:%S')}) + api.nvim_buf_set_option(bufnr, 'modifiable', false) + + -- open split to show buffer + if opts.open_split ~= false then + api.nvim_command('belowright split ' .. fn.shellescape(bufname)) + local win = api.nvim_get_current_win() + api.nvim_win_set_buf(win, bufnr) + end + + local stdout_acc = {} + local stderr_acc = {} + + local function on_stdout(data) + if data then + append_lines_to_buf(bufnr, data) + for _, l in ipairs(data) do table.insert(stdout_acc, l) end + end + end + local function on_stderr(data) + if data then + append_lines_to_buf(bufnr, data) + for _, l in ipairs(data) do table.insert(stderr_acc, l) end + end + end + local function on_exit(code) + append_lines_to_buf(bufnr, {"-- REPL exited with code: " .. tostring(code)}) + api.nvim_buf_set_option(bufnr, 'modifiable', false) + end + + -- spawn via jobstart + local cmd = {'perl', repl_file} + fn.jobstart(cmd, { + stdout_buffered = true, + stderr_buffered = true, + on_stdout = vim.schedule_wrap(function(_, data, _) + on_stdout(data) + end), + on_stderr = vim.schedule_wrap(function(_, data, _) + on_stderr(data) + end), + on_exit = vim.schedule_wrap(function(_, code, _) + on_exit(code) + end), + }) +end + +return M diff --git a/lua/repl_plugin/sync.lua b/lua/repl_plugin/sync.lua new file mode 100644 index 0000000..363e67d --- /dev/null +++ b/lua/repl_plugin/sync.lua @@ -0,0 +1,84 @@ +local git = require('repl_plugin.git') +local M = {} + +local api = vim.api +local fn = vim.fn + +local config = {} +local last_branch = nil +local timer_id = nil + +function M.setup(cfg) + config = cfg or {} +end + +function M.sync_once(cb) + cb = cb or function() end + git.get_branch(config.main_repo_path, function(err, branch) + if err or not branch or branch == '' then + vim.notify('Could not determine main repo branch', vim.log.levels.WARN) + return cb('no_branch') + end + if branch == last_branch then + return cb(nil, {changed=false}) + end + + -- attempt to commit repl repo if dirty + git.commit_if_dirty(config.repl_repo_path, {auto_commit = config.auto_commit}, function(cerr, cres) + if cerr then + vim.notify('Failed to commit REPL repo: ' .. tostring(cerr), vim.log.levels.ERROR) + return cb('commit_failed') + end + -- proceed to ensure branch exists and checkout + git.branch_exists(config.repl_repo_path, branch, function(_, exists) + if exists then + git.checkout_branch(config.repl_repo_path, branch, function(co_err, ok) + if co_err then + vim.notify('Failed to checkout repl branch: ' .. tostring(co_err), vim.log.levels.ERROR) + return cb('checkout_failed') + end + last_branch = branch + vim.notify('Repl switched to branch: ' .. branch, vim.log.levels.INFO) + return cb(nil, {changed=true, branch=branch}) + end) + else + -- prefer template + git.branch_exists(config.repl_repo_path, 'template', function(_, tmpl) + local from = tmpl and 'template' or 'HEAD' + git.create_branch(config.repl_repo_path, branch, from, function(cr_err, ok) + if cr_err then + vim.notify('Failed to create repl branch: ' .. tostring(cr_err), vim.log.levels.ERROR) + return cb('create_failed') + end + last_branch = branch + vim.notify('Repl branch created: ' .. branch, vim.log.levels.INFO) + return cb(nil, {changed=true, branch=branch}) + end) + end) + end + end) + end) + end) +end + +function M.start(interval) + if timer_id then + vim.notify('REPL sync already running', vim.log.levels.INFO) + return + end + interval = interval or config.poll_interval or 5000 + timer_id = fn.timer_start(interval, function() + vim.schedule(function() M.sync_once() end) + end, {['repeat'] = -1}) + vim.notify('Started REPL sync (interval ' .. tostring(interval) .. ' ms)', vim.log.levels.INFO) +end + +function M.stop() + if timer_id then + pcall(fn.timer_stop, timer_id) + timer_id = nil + vim.notify('Stopped REPL sync', vim.log.levels.INFO) + end +end + +return M diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3741b8a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "reperl.nvim", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "opencode-box": "^1.4.1" + } + }, + "node_modules/opencode-box": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/opencode-box/-/opencode-box-1.4.1.tgz", + "integrity": "sha512-jYVne0PpzOEYRLqa8LOBsFjL4sNVAIdcZ8sKtIKjQ/QPyR6M0tU+0WJZpAs3qglk5m6iji8H3doQa+T09ws7xA==", + "license": "MIT", + "bin": { + "opencodebox": "bin/agentbox.js" + }, + "engines": { + "node": ">=16.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e6dc213 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "opencode-box": "^1.4.1" + } +} diff --git a/test_main_repo b/test_main_repo new file mode 160000 index 0000000..76f0a53 --- /dev/null +++ b/test_main_repo @@ -0,0 +1 @@ +Subproject commit 76f0a53b6a1b1880975ded6c6ba5c5c51756bb0f diff --git a/test_repl_repo b/test_repl_repo new file mode 160000 index 0000000..76f0a53 --- /dev/null +++ b/test_repl_repo @@ -0,0 +1 @@ +Subproject commit 76f0a53b6a1b1880975ded6c6ba5c5c51756bb0f diff --git a/tests/_headless_single.lua b/tests/_headless_single.lua new file mode 100644 index 0000000..4c62215 --- /dev/null +++ b/tests/_headless_single.lua @@ -0,0 +1,35 @@ +local M = require('merged-repl-plugin') +local main = [[/home/dai/projects/reperl.nvim/test_main_repo]] +local repl = [[/home/dai/projects/reperl.nvim/test_repl_repo]] +local rf = repl .. '/repl.pl' +M.setup{ main_repo_path = main, repl_repo_path = repl, repl_file = rf, auto_start = false, output_target = 'file' } +-- run one sync and capture result +M.sync_once(function(err, res) + local f = io.open([[/home/dai/projects/reperl.nvim/tests/sync_result.txt]], 'w') + f:write(tostring(err) .. "\n") + if res then f:write(vim.inspect(res) .. "\n") end + f:close() +end) +-- ensure repl file exists (writing fresh, since sync may change working tree) +local fh = io.open(rf, 'w') +if fh then fh:write('print "Hello from REPL\n";') fh:close() end + +-- run the repl; runner will write repl_output.log in the repl repo +M.run_repl() +-- wait up to 2s for the output file to appear +local out_path = repl .. '/repl_output.log' +local ok = vim.wait(2000, function() + return vim.loop.fs_stat(out_path) ~= nil +end, 50) +if ok then + -- copy to tests location for inspection + local in_f = io.open(out_path, 'r') + if in_f then + local content = in_f:read('*a') + in_f:close() + local out_f = io.open([[/home/dai/projects/reperl.nvim/tests/repl_output_captured.log]], 'w') + if out_f then out_f:write(content); out_f:close() end + end +end + +vim.cmd('qa!') diff --git a/tests/_headless_test.lua b/tests/_headless_test.lua new file mode 100644 index 0000000..c636ae9 --- /dev/null +++ b/tests/_headless_test.lua @@ -0,0 +1,41 @@ +-- headless test for merged-repl-plugin +local M = require('merged-repl-plugin') + +local main = [[/home/dai/projects/reperl.nvim/test_main_repo]] +local repl = [[/home/dai/projects/reperl.nvim/test_repl_repo]] +local repl_file = repl .. '/repl.pl' +local sync_out = [[/home/dai/projects/reperl.nvim/tests/sync_result.txt]] +local repl_out = [[/home/dai/projects/reperl.nvim/tests/repl_output_captured.log]] + +M.setup{ main_repo_path = main, repl_repo_path = repl, repl_file = repl_file, auto_start = false } + +-- run sync_once and write result to file +M.sync_once(function(err, res) + local f = io.open(sync_out, 'w') + f:write(tostring(err) .. '\n') + if res then f:write(vim.inspect(res) .. '\n') end + f:close() +end) + +-- run the repl using plugin runner +M.run_repl() + +-- wait for buffer to appear and capture its contents to repl_out +local bufname = 'REPL Output: ' .. vim.fn.fnamemodify(repl, ':t') +local ok = vim.wait(2000, function() + local bufnr = vim.fn.bufnr(bufname) + if bufnr <= 0 then return false end + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + return #lines > 0 +end, 50) + +local bufnr = vim.fn.bufnr(bufname) +if bufnr > 0 then + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local f = io.open(repl_out, 'w') + f:write(table.concat(lines, '\n')) + f:close() +end + +-- exit +vim.cmd('qa!') diff --git a/tests/repl_output_captured.log b/tests/repl_output_captured.log new file mode 100644 index 0000000..63230df --- /dev/null +++ b/tests/repl_output_captured.log @@ -0,0 +1 @@ +-- REPL Run: 2026-01-22 11:16:46 diff --git a/tests/run_single.sh b/tests/run_single.sh new file mode 100755 index 0000000..33fd773 --- /dev/null +++ b/tests/run_single.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Single-run integration test for merged-repl-plugin +# Usage: ./tests/run_single.sh +# Requirements: bash, git, nvim (headless) available in PATH + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +MAIN="$ROOT/test_main_repo" +REPL="$ROOT/test_repl_repo" +LUA_TEST="$ROOT/tests/_headless_single.lua" +SYNC_OUT="$ROOT/tests/sync_result.txt" +REPL_OUT="$ROOT/tests/repl_output_captured.log" +TMP_CLEANUP=() + +fail() { echo "ERROR: $*" >&2; exit 1; } + +# ensure nvim not running (we'll be conservative) +if pgrep -a nvim > /dev/null; then + echo "Warning: nvim process found. Please close running nvim instances to avoid interference." +fi + +echo "Preparing test repos in: $ROOT" +rm -rf "$MAIN" "$REPL" "$SYNC_OUT" "$REPL_OUT" "$LUA_TEST" +mkdir -p "$MAIN" "$REPL" "$ROOT/tests" + +# init main +git -C "$MAIN" init >/dev/null 2>&1 || true +if [ -z "$(git -C "$MAIN" rev-parse --verify HEAD 2>/dev/null || true)" ]; then + touch "$MAIN/README" + git -C "$MAIN" add README + git -C "$MAIN" -c user.name='CI' -c user.email='ci@example.com' commit -m "init" >/dev/null +fi + +# init repl +git -C "$REPL" init >/dev/null 2>&1 || true +if [ -z "$(git -C "$REPL" rev-parse --verify HEAD 2>/dev/null || true)" ]; then + touch "$REPL/README" + git -C "$REPL" add README + git -C "$REPL" -c user.name='CI' -c user.email='ci@example.com' commit -m "init" >/dev/null +fi + +# ensure template branch exists in repl +git -C "$REPL" checkout -B template >/dev/null 2>&1 || true + +# create a simple repl.pl and commit +cat > "$REPL/repl.pl" <<'EOF' +print "Hello from REPL\n"; +EOF +git -C "$REPL" add repl.pl >/dev/null 2>&1 || true +git -C "$REPL" -c user.name='CI' -c user.email='ci@example.com' commit -m "add repl" >/dev/null 2>&1 || true + +# headless lua script that triggers sync_once and run_repl and dumps file output to a file +cat > "$LUA_TEST" <&2 + exit 2 +fi +if ! grep -q "Hello from REPL" "$REPL/repl_output.log"; then + echo "ERROR: expected REPL output missing" >&2 + cat "$REPL/repl_output.log" || true + exit 3 +fi + +# verify branch mirroring (create branch in main and run sync_once again) +git -C "$MAIN" checkout -B feature-single || fail "failed to create test branch in main" +nvim --headless -u NONE -c "lua package.path = package.path .. ';./lua/?.lua;./lua/?/init.lua'" -c "lua require('merged-repl-plugin').setup{ main_repo_path='${MAIN}', repl_repo_path='${REPL}', repl_file='${REPL}/repl.pl', auto_start=false } require('merged-repl-plugin').sync_once()" -c "qa!" + +echo "=== repl branches ===" +git -C "$REPL" branch --list + +# test auto-commit: append to repl.pl and run sync_once; then show latest commit message +echo 'print "Modified for single test\\n";' >> "$REPL/repl.pl" +nvim --headless -u NONE -c "lua package.path = package.path .. ';./lua/?.lua;./lua/?/init.lua'" -c "lua require('merged-repl-plugin').setup{ main_repo_path='${MAIN}', repl_repo_path='${REPL}', repl_file='${REPL}/repl.pl', auto_start=false } require('merged-repl-plugin').sync_once()" -c "qa!" + +echo "=== latest repl commit message ===" +git -C "$REPL" log -1 --pretty=%B || true + +echo "Single run complete." diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..9e0ca5a --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Run integration tests for merged-repl-plugin +# Usage: ./tests/run_tests.sh +# This script creates two test git repositories under the project directory, +# runs headless Neovim to exercise the plugin (sync and run), and verifies +# expected results (branch creation, repl output capture, auto-commit). + +REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) +MAIN="$REPO_ROOT/test_main_repo" +REPL="$REPO_ROOT/test_repl_repo" +LUA_TMP="$REPO_ROOT/tests/_headless_test.lua" +SYNC_OUT="$REPO_ROOT/tests/sync_result.txt" +REPL_OUT="$REPO_ROOT/tests/repl_output_captured.log" + +echo "Running tests in: $REPO_ROOT" + +# Prepare test repos +rm -rf "$MAIN" "$REPL" "$REPL_OUT" "$SYNC_OUT" +mkdir -p "$MAIN" "$REPL" "$REPO_ROOT/tests" + +git -C "$MAIN" init || true +# create initial commit in main +if [ -z "$(git -C "$MAIN" rev-parse --verify HEAD 2>/dev/null || true)" ]; then + touch "$MAIN/README" + git -C "$MAIN" add README + git -C "$MAIN" -c user.name='CI' -c user.email='ci@example.com' commit -m "init" +fi + +git -C "$REPL" init || true +if [ -z "$(git -C "$REPL" rev-parse --verify HEAD 2>/dev/null || true)" ]; then + touch "$REPL/README" + git -C "$REPL" add README + git -C "$REPL" -c user.name='CI' -c user.email='ci@example.com' commit -m "init" +fi + +# Ensure template branch exists in repl repo +git -C "$REPL" checkout -B template || true + +# Write a simple repl.pl +cat > "$REPL/repl.pl" <<'EOF' +print "Hello from REPL\n"; +EOF +# commit repl.pl +git -C "$REPL" add repl.pl || true +git -C "$REPL" -c user.name='CI' -c user.email='ci@example.com' commit -m "add repl" || true + +# Compose headless lua test file +cat > "$LUA_TMP" < 0 +end, 50) + +local bufnr = vim.fn.bufnr(bufname) +if bufnr > 0 then + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local f = io.open(repl_out, 'w') + f:write(table.concat(lines, '\n')) + f:close() +end + +-- exit +vim.cmd('qa!') +LUALIB + +# Run headless Neovim with the test lua file +nvim --headless -u NONE -c "lua package.path = package.path .. ';./lua/?.lua;./lua/?/init.lua'" -c "luafile ${LUA_TMP}" + +# Check results +echo "--- Sync result ---" +if [ -f "$SYNC_OUT" ]; then + cat "$SYNC_OUT" +else + echo "No sync output file found" +fi + +echo "--- Repl output ---" +if [ -f "$REPL_OUT" ]; then + cat "$REPL_OUT" +else + echo "No repl output captured" +fi + +# Verify repl repo got branch created if main branch changed +# create a new branch in main to test branch mirroring +git -C "$MAIN" checkout -B feature-test + +# run sync_once again +nvim --headless -u NONE -c "lua package.path = package.path .. ';./lua/?.lua;./lua/?/init.lua'" -c "lua require('merged-repl-plugin').setup{ main_repo_path = '${MAIN}', repl_repo_path = '${REPL}', repl_file = '${REPL}/repl.pl', auto_start = false } require('merged-repl-plugin').sync_once()" -c "qa!" + +echo "--- Branches in repl repo ---" +git -C "$REPL" branch --list + +# Test auto-commit: modify repl.pl and run sync_once to ensure commit +echo 'print "Modified\n";' >> "$REPL/repl.pl" + +nvim --headless -u NONE -c "lua package.path = package.path .. ';./lua/?.lua;./lua/?/init.lua'" -c "lua require('merged-repl-plugin').setup{ main_repo_path = '${MAIN}', repl_repo_path = '${REPL}', repl_file = '${REPL}/repl.pl', auto_start = false } require('merged-repl-plugin').sync_once()" -c "qa!" + +echo "--- Latest commit message in repl repo ---" +git -C "$REPL" log -1 --pretty=%B + +echo "Tests finished." diff --git a/tests/sync_result.txt b/tests/sync_result.txt new file mode 100644 index 0000000..64ba5c1 --- /dev/null +++ b/tests/sync_result.txt @@ -0,0 +1,5 @@ +nil +{ + branch = "master", + changed = true +}