From b5fe23351aa048eafad2b83d3945d90a5fd249de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Steinbrink?= Date: Fri, 10 Jul 2026 12:45:57 +0200 Subject: [PATCH] fix(picker): status picker crash on renamed files (#139) --- lua/jj/core/parser.lua | 28 ++++++++----------- tests/run_tests.lua | 63 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/lua/jj/core/parser.lua b/lua/jj/core/parser.lua index 3692c5c..1cbcf72 100644 --- a/lua/jj/core/parser.lua +++ b/lua/jj/core/parser.lua @@ -75,24 +75,20 @@ function M.parse_file_info_from_status_line(line) line = vim.trim(line) - -- Handle renamed files in nested path form: - -- R dir/{old_name => new_name} - local dir_path, old_name, new_name = line:match("^R%s+(.*)/{(.*)%s=>%s([^}]+)}$") - if dir_path and old_name and new_name then - return { - old_path = dir_path .. "/" .. old_name, - new_path = dir_path .. "/" .. new_name, - is_rename = true, - } - end - - -- Handle renamed files in top-level brace form: + -- Handle renamed/copied files with a brace group anywhere in the path, + -- where either side may be empty: -- R {old_name => new_name} - local old_top, new_top = line:match("^R%s+{(.-)%s=>%s([^}]+)}$") - if old_top and new_top then + -- R dir/{old_name => new_name}/rest + -- R dir/{ => new_dir}/rest + local prefix, old_part, new_part, suffix = line:match("^[RC]%s+(.-){(.-) => (.-)}(.*)$") + if prefix then + local function join(part) + local path = (prefix .. part .. suffix):gsub("//", "/"):gsub("^/", "") + return path + end return { - old_path = old_top, - new_path = new_top, + old_path = join(old_part), + new_path = join(new_part), is_rename = true, } end diff --git a/tests/run_tests.lua b/tests/run_tests.lua index 03b4cd3..80938d6 100755 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -432,6 +432,69 @@ run_test("scan_conflict_sections: empty file yields empty list", function() assert_table_equals({}, parser.scan_conflict_sections("a.txt", "/abs/a.txt", {})) end) +print("\n=== Running parse_file_info_from_status_line tests ===\n") + +run_test("status line: parses regular modified file", function() + assert_table_equals( + { old_path = "src/main.rs", new_path = "src/main.rs", is_rename = false }, + parser.parse_file_info_from_status_line("M src/main.rs") + ) +end) + +run_test("status line: parses top-level brace rename", function() + assert_table_equals( + { old_path = "old.txt", new_path = "new.txt", is_rename = true }, + parser.parse_file_info_from_status_line("R {old.txt => new.txt}") + ) +end) + +run_test("status line: parses rename with brace group at end", function() + assert_table_equals( + { old_path = "dir/old.txt", new_path = "dir/new.txt", is_rename = true }, + parser.parse_file_info_from_status_line("R dir/{old.txt => new.txt}") + ) +end) + +run_test("status line: parses rename with brace group mid-path", function() + assert_table_equals({ + old_path = "Business/OAuth/Exceptions/OAuth2RefreshTokenExpiredException.php", + new_path = "Business/OAuth2/Exceptions/OAuth2RefreshTokenExpiredException.php", + is_rename = true, + }, parser.parse_file_info_from_status_line( + "R Business/{OAuth => OAuth2}/Exceptions/OAuth2RefreshTokenExpiredException.php" + )) +end) + +run_test("status line: parses rename with empty old side mid-path", function() + assert_table_equals({ + old_path = "Business/Exceptions/Error.php", + new_path = "Business/OAuth2/Sub/Exceptions/Error.php", + is_rename = true, + }, parser.parse_file_info_from_status_line("R Business/{ => OAuth2/Sub}/Exceptions/Error.php")) +end) + +run_test("status line: parses rename with empty new side mid-path", function() + assert_table_equals({ + old_path = "Business/OAuth/Exceptions/Error.php", + new_path = "Business/Exceptions/Error.php", + is_rename = true, + }, parser.parse_file_info_from_status_line("R Business/{OAuth => }/Exceptions/Error.php")) +end) + +run_test("status line: parses rename with empty side at top level", function() + assert_table_equals( + { old_path = "a.txt", new_path = "dir/a.txt", is_rename = true }, + parser.parse_file_info_from_status_line("R { => dir}/a.txt") + ) +end) + +run_test("status line: parses copied file with brace group", function() + assert_table_equals( + { old_path = "dir/old.txt", new_path = "dir/new.txt", is_rename = true }, + parser.parse_file_info_from_status_line("C dir/{old.txt => new.txt}") + ) +end) + print("\n=== Running parse_default_cmd tests ===\n") run_test("parse_default_cmd: parses config list array output", function()