From a85e8d7721947f54972404d7c3ff8e894b7eddbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20K=C4=B1l=C4=B1=C3=A7?= Date: Sat, 7 Feb 2026 20:36:25 +0100 Subject: [PATCH] feat: add fixed point comparison for history --- README.md | 7 +++++ doc/codediff.txt | 24 ++++++++++++++++ docs/git-integration.md | 34 ++++++++++++++++++++++ lua/codediff/commands.lua | 2 ++ lua/codediff/ui/history/render.lua | 12 ++++++-- lua/codediff/ui/view/init.lua | 1 + plugin/codediff.lua | 2 +- tests/history_flags_spec.lua | 45 ++++++++++++++++++++++++++++++ 8 files changed, 123 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3580749f..f8088729 100644 --- a/README.md +++ b/README.md @@ -359,12 +359,19 @@ Review commits on a per-commit basis: :CodeDiff history HEAD~10 --reverse :CodeDiff history origin/main..HEAD -r :CodeDiff history HEAD~20 % --reverse + +" Compare each commit against the current working tree +:CodeDiff history --base WORKING + +" Compare each commit against HEAD +:CodeDiff history --base HEAD ``` The history panel shows a list of commits. Each commit can be expanded to show its changed files. Select a file to view the diff between the commit and its parent (`commit^` vs `commit`). **Options:** - `--reverse` or `-r`: Show commits in chronological order (oldest first) instead of reverse chronological. Useful for following development story from beginning to end, or reviewing PR changes in the order they were made. +- `--base` or `-b`: Compare each commit against a fixed revision instead of its parent. Accepts any git revision (`HEAD`, branch name, commit hash) or `WORKING` for the current working tree. **History Keymaps:** - `i` - Toggle between list and tree view for files under commits diff --git a/doc/codediff.txt b/doc/codediff.txt index 7fbb2661..eb561731 100644 --- a/doc/codediff.txt +++ b/doc/codediff.txt @@ -71,6 +71,30 @@ Directory comparison mode: Shows files as Added (A), Deleted (D), or Modified (M) based on file size and modification time. Select a file to view its diff. +File history mode: *codediff-history* +> + :CodeDiff history + :CodeDiff history HEAD~10 + :CodeDiff history origin/main..HEAD + :CodeDiff history HEAD~20 % + :CodeDiff history --reverse +< + +Each commit is compared against its parent by default. Use --base to compare +every commit against a fixed reference instead: +> + :CodeDiff history --base WORKING + :CodeDiff history --base HEAD + :CodeDiff history --base main + :CodeDiff history HEAD~10 -b WORKING % +< + +Options: + --reverse, -r Show commits oldest first + --base, -b Compare each commit against a fixed revision instead of + its parent. Accepts any git revision (HEAD, branch name, + commit hash) or WORKING for the current working tree. + Git merge tool: > git config --global merge.tool codediff diff --git a/docs/git-integration.md b/docs/git-integration.md index 1b17d6f0..9a5385a7 100644 --- a/docs/git-integration.md +++ b/docs/git-integration.md @@ -170,6 +170,40 @@ The plugin uses standard git commands: - File content fetching is done asynchronously (non-blocking) - The diff computation happens after file content is retrieved +## File History with Fixed Base (`--base`) + +By default, `:CodeDiff history` compares each commit against its parent. +The `--base` flag changes this to compare every commit against a fixed reference: + +```vim +" Compare each commit against the current working tree +:CodeDiff history --base WORKING + +" Compare each commit against HEAD +:CodeDiff history --base HEAD + +" Compare each commit against a branch +:CodeDiff history --base main + +" Short form +:CodeDiff history -b WORKING + +" Combine with range, file path, and other flags +:CodeDiff history origin/main..HEAD --base WORKING % +:CodeDiff history HEAD~10 -b HEAD --reverse +``` + +### Comparison Modes + +**Sequential (default):** Each commit compared to its parent. +Useful for reviewing what changed in each individual commit. + +**Fixed base (`--base WORKING`):** Each commit compared to working tree. +Useful for seeing how far each historical commit is from your current state. + +**Fixed base (`--base `):** Each commit compared to a specific ref. +Useful for seeing how each commit differs from a branch tip or tag. + ## Future Enhancements Potential improvements: diff --git a/lua/codediff/commands.lua b/lua/codediff/commands.lua index 3ef6cdca..daf8aaff 100644 --- a/lua/codediff/commands.lua +++ b/lua/codediff/commands.lua @@ -232,6 +232,7 @@ local function handle_history(range, file_path, flags) commits = commits, range = range, file_path = history_opts.path, + base_revision = flags.base, }, } @@ -650,6 +651,7 @@ function M.vscode_diff(opts) -- Define flag spec for history command local flag_spec = { ["--reverse"] = { short = "-r", type = "boolean" }, + ["--base"] = { short = "-b", type = "string" }, } -- Parse args: separate positional from flags diff --git a/lua/codediff/ui/history/render.lua b/lua/codediff/ui/history/render.lua index 1a5178fe..dc0ffa1d 100644 --- a/lua/codediff/ui/history/render.lua +++ b/lua/codediff/ui/history/render.lua @@ -16,6 +16,7 @@ local keymaps_module = require("codediff.ui.history.keymaps") -- opts: { range, path, ... } original options function M.create(commits, git_root, tabpage, width, opts) opts = opts or {} + local base_revision = opts.base_revision -- Get history panel position and size from config (separate from explorer) local history_config = config.options.history or {} @@ -94,6 +95,10 @@ function M.create(commits, git_root, tabpage, width, opts) title_text = "Commit History (" .. #commits .. ")" end + if base_revision then + title_text = title_text:gsub("%)$", ", base=" .. base_revision .. ")") + end + -- Add title node tree_nodes[#tree_nodes + 1] = Tree.Node({ id = "title", @@ -267,8 +272,9 @@ function M.create(commits, git_root, tabpage, width, opts) end -- Check if already displaying same file + local target_hash = base_revision or (commit_hash .. "^") local session = lifecycle.get_session(tabpage) - if session and session.original_revision == commit_hash .. "^" and session.modified_revision == commit_hash then + if session and session.original_revision == target_hash and session.modified_revision == commit_hash then if session.modified_path == file_path or session.original_path == file_path then return end @@ -279,9 +285,9 @@ function M.create(commits, git_root, tabpage, width, opts) local session_config = { mode = "history", git_root = git_root, - original_path = old_path or file_path, + original_path = base_revision and file_path or (old_path or file_path), modified_path = file_path, - original_revision = commit_hash .. "^", + original_revision = target_hash, modified_revision = commit_hash, } view.update(tabpage, session_config, true) diff --git a/lua/codediff/ui/view/init.lua b/lua/codediff/ui/view/init.lua index 9b6d3341..24e67b38 100644 --- a/lua/codediff/ui/view/init.lua +++ b/lua/codediff/ui/view/init.lua @@ -422,6 +422,7 @@ function M.create(session_config, filetype, on_ready) local history_obj = history.create(commits, session_config.git_root, tabpage, nil, { range = session_config.history_data.range, file_path = session_config.history_data.file_path, + base_revision = session_config.history_data.base_revision, }) -- Store history panel reference in lifecycle (reuse explorer slot) diff --git a/plugin/codediff.lua b/plugin/codediff.lua index d4bad304..b53e0af8 100644 --- a/plugin/codediff.lua +++ b/plugin/codediff.lua @@ -72,7 +72,7 @@ local function complete_codediff(arg_lead, cmd_line, _) if first_arg == "history" then -- If arg_lead starts with -, complete flags if arg_lead:match("^%-") then - local flag_candidates = { "--reverse", "-r" } + local flag_candidates = { "--reverse", "-r", "--base", "-b" } local filtered = {} for _, flag in ipairs(flag_candidates) do if flag:find(arg_lead, 1, true) == 1 then diff --git a/tests/history_flags_spec.lua b/tests/history_flags_spec.lua index 36877b87..eb395a26 100644 --- a/tests/history_flags_spec.lua +++ b/tests/history_flags_spec.lua @@ -65,3 +65,48 @@ describe("History Flag Parsing", function() assert.matches("requires a value", err) end) end) + +describe("History --base Flag Parsing", function() + local args_parser = require("codediff.core.args") + local flag_spec = { + ["--reverse"] = { short = "-r", type = "boolean" }, + ["--base"] = { short = "-b", type = "string" }, + } + + it("parses --base with space syntax", function() + local positional, flags = args_parser.parse_args({ "--base", "WORKING" }, flag_spec) + assert.are.same({}, positional) + assert.are.same({ base = "WORKING" }, flags) + end) + + it("parses -b short form", function() + local positional, flags = args_parser.parse_args({ "-b", "HEAD" }, flag_spec) + assert.are.same({}, positional) + assert.are.same({ base = "HEAD" }, flags) + end) + + it("parses --base combined with --reverse", function() + local positional, flags = args_parser.parse_args({ "--base", "HEAD", "--reverse" }, flag_spec) + assert.are.same({}, positional) + assert.are.same({ base = "HEAD", reverse = true }, flags) + end) + + it("parses --base with positional args", function() + local positional, flags = args_parser.parse_args({ "HEAD~10", "--base", "WORKING", "%" }, flag_spec) + assert.are.same({ "HEAD~10", "%" }, positional) + assert.are.same({ base = "WORKING" }, flags) + end) + + it("returns error for --base without value", function() + local positional, flags, err = args_parser.parse_args({ "--base" }, flag_spec) + assert.is_nil(positional) + assert.is_nil(flags) + assert.matches("requires a value", err) + end) + + it("parses --base with branch name", function() + local positional, flags = args_parser.parse_args({ "--base", "main" }, flag_spec) + assert.are.same({}, positional) + assert.are.same({ base = "main" }, flags) + end) +end)