diff --git a/doc/neogit.txt b/doc/neogit.txt index e54458d60..f46fd49dd 100644 --- a/doc/neogit.txt +++ b/doc/neogit.txt @@ -2186,17 +2186,28 @@ Mappings (normal mode): Note: it can fail if the file no longer exists or it can take you to a wrong location in the file if the lines have been moved around. + • `` On a diff hunk, open the file contents from the commit in a temporary read-only tab, or the parent revision for deleted lines; on a filepath line, jump to that file's diff section within the buffer. + • `o` Open the commit in the configured git service (requires Neovim >= 0.10 for |vim.ui.open|). - • `{` / `}` Jump to previous/next hunk or diff header. - • `q` / `` Close the commit buffer (keys are configurable via - |neogit_setup_mappings| for the commit view). - • `Y` Yank the current commit hash to the clipboard. + + • `{` / `}` Jump to previous/next hunk or diff header. + + • `q` / `` Close the commit buffer (keys are configurable via + |neogit_setup_mappings| for the commit view). + + • `Y` Opens the Yank popup, allowing copying of different details + from the commit. + + note: To copy only a specific hunk's diff, place the + cursor within that hunk. + • `za` / `` Toggle folding for the current section. + • Popup shortcuts honour |neogit_setup_mappings|, default keys: - `A` Cherry-pick, `b` Branch, `B` Bisect, `c` Commit, `d` Diff, `f` Fetch, `i` Ignore, `l` Log, `m` Merge, `p` Pull, `P` Push, diff --git a/lua/neogit/buffers/commit_view/init.lua b/lua/neogit/buffers/commit_view/init.lua index 3233bbba7..fb3e98598 100644 --- a/lua/neogit/buffers/commit_view/init.lua +++ b/lua/neogit/buffers/commit_view/init.lua @@ -8,6 +8,7 @@ local commit_view_maps = require("neogit.config").get_reversed_commit_view_maps( local status_maps = require("neogit.config").get_reversed_status_maps() local notification = require("neogit.lib.notification") local jump = require("neogit.lib.jump") +local util = require("neogit.lib.util") local api = vim.api @@ -425,11 +426,50 @@ function M:open(kind) [""] = function() self:close() end, - [status_maps["YankSelected"]] = function() - local yank = string.format("'%s'", self.commit_info.oid) - vim.cmd.let("@+=" .. yank) - vim.cmd.echo(yank) - end, + [status_maps["YankSelected"]] = popups.open("yank", function(p) + -- If the cursor is over a specific hunk, just copy that diff. + local diff + local c = self.buffer.ui:get_component_under_cursor(function(c) + return c.options.hunk ~= nil + end) + + if c then + local hunks = util.flat_map(self.commit_info.diffs, function(diff) + return diff.hunks + end) + + for _, hunk in ipairs(hunks) do + if hunk.hash == c.options.hunk.hash then + diff = table.concat(util.merge({ hunk.line }, hunk.lines), "\n") + break + end + end + end + + -- If for some reason we don't find the specific hunk, or there isn't one, fall-back to the entire patch. + if not diff then + diff = table.concat( + vim.tbl_map(function(diff) + return table.concat(diff.lines, "\n") + end, self.commit_info.diffs), + "\n" + ) + end + + p { + hash = self.commit_info.oid, + subject = self.commit_info.description[1], + message = table.concat(self.commit_info.description, "\n"), + body = table.concat( + util.slice(self.commit_info.description, 2, #self.commit_info.description), + "\n" + ), + url = git.remote.commit_url(self.commit_info.oid), + diff = diff, + author = ("%s <%s>"):format(self.commit_info.author_name, self.commit_info.author_email), + tags = table.concat(git.tag.for_commit(self.commit_info.oid), ", "), + } + end), [status_maps["Toggle"]] = function() pcall(vim.cmd, "normal! za") end, diff --git a/lua/neogit/lib/git/cli.lua b/lua/neogit/lib/git/cli.lua index 9a54b3b66..f4eb5cf19 100644 --- a/lua/neogit/lib/git/cli.lua +++ b/lua/neogit/lib/git/cli.lua @@ -136,6 +136,7 @@ end ---@field n self ---@field list self ---@field delete self +---@field points_at fun(oid: string): self ---@class GitCommandRebase: GitCommandBuilder ---@field interactive self @@ -557,6 +558,13 @@ local configurations = { list = "--list", delete = "--delete", }, + aliases = { + points_at = function(tbl) + return function(oid) + return tbl.args("--points-at", oid) + end + end, + }, }, rebase = config { diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 55d1b3e41..dfd3677b8 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -20,7 +20,7 @@ local commit_header_pat = "([| ]*)(%*?)([| ]*)commit (%w+)" ---@field committer_name string the name of the committer ---@field committer_email string the email of the committer ---@field committer_date string when the committer committed ----@field description string a list of lines +---@field description string[] a list of lines ---@field commit_arg string the passed argument of the git command ---@field subject string ---@field parent string diff --git a/lua/neogit/lib/git/tag.lua b/lua/neogit/lib/git/tag.lua index 0bc1983e9..4ac3ba804 100644 --- a/lua/neogit/lib/git/tag.lua +++ b/lua/neogit/lib/git/tag.lua @@ -24,6 +24,13 @@ function M.list_remote(remote) return git.cli["ls-remote"].tags.args(remote).call({ hidden = true }).stdout end +---Find tags that point at an object ID +---@param oid string +---@return string[] +function M.for_commit(oid) + return git.cli.tag.points_at(oid).call({ hidden = true }).stdout +end + local tag_pattern = "(.-)%-([0-9]+)%-g%x+$" function M.register(meta) diff --git a/lua/neogit/popups/yank/actions.lua b/lua/neogit/popups/yank/actions.lua new file mode 100644 index 000000000..defef5e3c --- /dev/null +++ b/lua/neogit/popups/yank/actions.lua @@ -0,0 +1,25 @@ +local notification = require("neogit.lib.notification") +local M = {} + +---@param key string +---@return fun(popup: PopupData) +local function yank(key) + return function(popup) + local data = popup:get_env(key) + if data then + vim.cmd.let(("@+='%s'"):format(data)) + notification.info(("Copied %s to clipboard."):format(key)) + end + end +end + +M.hash = yank("hash") +M.subject = yank("subject") +M.message = yank("message") +M.body = yank("body") +M.url = yank("url") +M.diff = yank("diff") +M.author = yank("author") +M.tags = yank("tags") + +return M diff --git a/lua/neogit/popups/yank/init.lua b/lua/neogit/popups/yank/init.lua new file mode 100644 index 000000000..e23018127 --- /dev/null +++ b/lua/neogit/popups/yank/init.lua @@ -0,0 +1,27 @@ +local popup = require("neogit.lib.popup") +local actions = require("neogit.popups.yank.actions") + +local M = {} + +function M.create(env) + local p = popup + .builder() + :name("NeogitYankPopup") + :group_heading("Yank Commit info") + :action("Y", "Hash", actions.hash) + :action("s", "Subject", actions.subject) + :action("m", "Message (subject and body)", actions.message) + :action("b", "Message body", actions.body) + :action_if(env.url, "u", "URL", actions.url) + :action("d", "Diff", actions.diff) + :action("a", "Author", actions.author) + :action_if(env.tags ~= "", "t", "Tags", actions.tags) + :env(env) + :build() + + p:show() + + return p +end + +return M diff --git a/spec/buffers/commit_buffer_spec.rb b/spec/buffers/commit_buffer_spec.rb index 0ada40be2..c84d069cb 100644 --- a/spec/buffers/commit_buffer_spec.rb +++ b/spec/buffers/commit_buffer_spec.rb @@ -17,9 +17,62 @@ expect(nvim.filetype).to eq("NeogitLogView") end - it "can yank OID" do + it "can open Yank popup" do nvim.keys("Y") - expect(nvim.screen.last.strip).to match(/\A[a-f0-9]{40}\z/) + expect(nvim.filetype).to eq("NeogitPopup") + end + + if ENV["CI"].nil? # Fails in GHA :'( + it "can yank oid" do + nvim.keys("YY") + yank = nvim.cmd("echo @*").first + expect(yank).to match(/[0-9a-f]{40}/) + end + + it "can yank author" do + nvim.keys("Ya") + yank = nvim.cmd("echo @*").first + expect(yank).to eq("tester ") + end + + it "can yank subject" do + nvim.keys("Ys") + yank = nvim.cmd("echo @*").first + expect(yank).to eq("Initial commit") + end + + it "can yank message" do + nvim.keys("Ym") + yank = nvim.cmd("echo @*") + expect(yank).to contain_exactly("Initial commit\n", "commit message") + end + + it "can yank body" do + nvim.keys("Yb") + yank = nvim.cmd("echo @*").first + expect(yank).to eq("commit message") + end + + it "can yank diff" do + nvim.keys("Yd") + yank = nvim.cmd("echo @*") + expect(yank).to contain_exactly("@@ -0,0 +1 @@\n", "+hello, world") + end + + it "can yank tag" do + git.add_tag("test-tag", "HEAD") + nvim.keys("Yt") + yank = nvim.cmd("echo @*").first + expect(yank).to eq("test-tag") + end + + it "can yank tags" do + git.add_tag("test-tag-a", "HEAD") + git.add_tag("test-tag-b", "HEAD") + nvim.keys("Yt") + yank = nvim.cmd("echo @*").first + expect(yank).to eq("test-tag-a, test-tag-b") + end end it "can open the bisect popup" do diff --git a/spec/popups/commit_popup_spec.rb b/spec/popups/commit_popup_spec.rb index 2fa2294ce..90912f22a 100644 --- a/spec/popups/commit_popup_spec.rb +++ b/spec/popups/commit_popup_spec.rb @@ -107,7 +107,7 @@ nvim.keys("w") nvim.keys("cc") nvim.keys("reworded!:wq") - expect(git.log(1).entries.first.message).to eq("reworded!") + expect(git.log(1).entries.first.message).to eq("reworded!\ncommit message") end end diff --git a/spec/support/context/git.rb b/spec/support/context/git.rb index d9576b7d8..f7e6f8712 100644 --- a/spec/support/context/git.rb +++ b/spec/support/context/git.rb @@ -4,11 +4,11 @@ let(:git) { Git.open(Dir.pwd) } before do - system("touch testfile") - git.config("user.email", "test@example.com") git.config("user.name", "tester") + + create_file("testfile", "hello, world\n") git.add("testfile") - git.commit("Initial commit") + git.commit("Initial commit\ncommit message") end end