diff --git a/lua/jj/core/runner.lua b/lua/jj/core/runner.lua index ccf80f0..b9a644f 100644 --- a/lua/jj/core/runner.lua +++ b/lua/jj/core/runner.lua @@ -42,14 +42,15 @@ end --- @param silent boolean|nil Optional to silent the notification --- @return string|nil output Raw stdout bytes, or nil if failed --- @return boolean success Whether the command succeeded +--- @return string stderr The command's stderr (empty on success) function M.execute_command_raw(cmd, error_prefix, silent) local result = vim.system({ "sh", "-c", cmd }):wait() if result.code ~= 0 then local msg = result.stderr ~= "" and result.stderr or result.stdout or "" error_notify(msg, error_prefix, silent) - return nil, false + return nil, false, msg end - return result.stdout or "", true + return result.stdout or "", true, "" end --- Execute a system command synchronously and call success callback. diff --git a/lua/jj/diff/codediff.lua b/lua/jj/diff/codediff.lua index cb9f138..1a63789 100644 --- a/lua/jj/diff/codediff.lua +++ b/lua/jj/diff/codediff.lua @@ -172,8 +172,10 @@ diff.register_backend("codediff", { -- side is decoded in the same way as the current file. -- Assumes the encoding didn't change between revisions. local enc = file.get_buf_encoding(0) - local base_lines, base_had_eol, ok_read = file.get_file_content(revset, path, enc) - if not ok_read then + -- A file added since `revset` has no content there; show it as empty + -- so it diffs as fully added rather than aborting the diff. + local base_lines, base_had_eol, ok_read, _, absent = file.get_file_content(revset, path, enc) + if not ok_read and not absent then utils.notify(string.format("Could not read `%s` from `%s` for CodeDiff", path, revset), vim.log.levels.ERROR) return end diff --git a/lua/jj/diff/native.lua b/lua/jj/diff/native.lua index d8da695..cb54d72 100644 --- a/lua/jj/diff/native.lua +++ b/lua/jj/diff/native.lua @@ -28,8 +28,10 @@ local function open_revision(rev, path, enc) return end - local lines, had_eol, ok_read, used_enc = file.get_file_content(change_id, rel_path, enc) - if not ok_read then + -- A file added since `rev` has no content there; show it as empty so it + -- diffs as fully added rather than aborting the diff. + local lines, had_eol, ok_read, used_enc, absent = file.get_file_content(change_id, rel_path, enc) + if not ok_read and not absent then utils.notify(string.format("Could not read `%s` from `%s`", rel_path, change_id), vim.log.levels.ERROR) return end diff --git a/lua/jj/file.lua b/lua/jj/file.lua index d421306..7c7b740 100644 --- a/lua/jj/file.lua +++ b/lua/jj/file.lua @@ -219,23 +219,26 @@ M._encode = encode --- (default: auto-detected from content) --- @return string[] lines --- @return boolean had_eol Whether the content had a trailing newline ---- @return boolean ok Whether the command succeeded +--- @return boolean ok Whether the read succeeded --- @return jj.file.enc used_enc The encoding used to decode the content +--- @return boolean absent True when the path does not exist in `rev` (e.g. a +--- file added since `rev`). local function get_file_content(rev, path, enc) - local raw, ok = runner.execute_command_raw( + local raw, ok, stderr = runner.execute_command_raw( string.format("jj file show -r %s %s", vim.fn.shellescape(rev), vim.fn.shellescape(path)), nil, true ) if not ok or not raw then - return {}, false, false, enc or { fenc = "", bomb = false, ff = "unix" } + local absent = stderr ~= nil and stderr:find("No such path", 1, true) ~= nil + return {}, false, false, enc or { fenc = "", bomb = false, ff = "unix" }, absent end local lines, had_eol, used_enc = decode(raw, enc) if not lines then utils.notify(had_eol --[[@as string]], vim.log.levels.ERROR) - return {}, false, false, used_enc + return {}, false, false, used_enc, false end - return lines, had_eol --[[@as boolean]], true, used_enc + return lines, had_eol --[[@as boolean]], true, used_enc, false end M.get_file_content = get_file_content @@ -436,8 +439,9 @@ function M.register_command() local name = vim.api.nvim_buf_get_name(0) local change_id, path = utils.parse_jj_uri(name) if not change_id or not path then return end - local lines, had_eol, ok_read, used_enc = get_file_content(change_id, path) - if not ok_read then + -- Keep reloads of an added-file diff buffer empty rather than erroring. + local lines, had_eol, ok_read, used_enc, absent = get_file_content(change_id, path) + if not ok_read and not absent then utils.notify(string.format("Could not read `%s` from `%s`", path, change_id), vim.log.levels.ERROR) return end diff --git a/tests/run_tests.lua b/tests/run_tests.lua index 2ab59cf..77249fc 100755 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -793,6 +793,63 @@ run_test("decode: roundtrip reproduces the bytes exactly", function() end end) +print("\n=== Running get_file_content tests ===\n") + +run_test("get_file_content: reads existing file content", function() + local runner = require("jj.core.runner") + local original = runner.execute_command_raw + runner.execute_command_raw = function() + return "a\nb\n", true, "" + end + local ok_test, err = pcall(function() + local lines, had_eol, ok, _, absent = jj_file.get_file_content("abc123", "src/file.py") + assert_table_equals({ "a", "b" }, lines) + assert_equals(true, had_eol) + assert_equals(true, ok) + assert_equals(false, absent) + end) + runner.execute_command_raw = original + if not ok_test then + error(err) + end +end) + +run_test("get_file_content: absent path in revision reports absent (not a read error)", function() + local runner = require("jj.core.runner") + local original = runner.execute_command_raw + runner.execute_command_raw = function() + return nil, false, "Error: No such path: src/new_file.py\n" + end + local ok_test, err = pcall(function() + local lines, had_eol, ok, _, absent = jj_file.get_file_content("abc123", "src/new_file.py") + assert_table_equals({}, lines) + assert_equals(false, had_eol) + assert_equals(false, ok) + assert_equals(true, absent) + end) + runner.execute_command_raw = original + if not ok_test then + error(err) + end +end) + +run_test("get_file_content: genuine read error returns failure without absent", function() + local runner = require("jj.core.runner") + local original = runner.execute_command_raw + runner.execute_command_raw = function() + return nil, false, "Error: Revision `nope` doesn't exist\n" + end + local ok_test, err = pcall(function() + local _, _, ok, _, absent = jj_file.get_file_content("nope", "src/file.py") + assert_equals(false, ok) + assert_equals(false, absent) + end) + runner.execute_command_raw = original + if not ok_test then + error(err) + end +end) + -- Print summary print(string.format("\n=== Test Summary ===")) print(string.format("Passed: %d", tests_passed))