Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions doc/codediff.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions docs/git-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <revision>`):** Each commit compared to a specific ref.
Useful for seeing how each commit differs from a branch tip or tag.

## Future Enhancements

Potential improvements:
Expand Down
2 changes: 2 additions & 0 deletions lua/codediff/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}

Expand Down Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions lua/codediff/ui/history/render.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions lua/codediff/ui/view/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion plugin/codediff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/history_flags_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)