lewis6991 / gitsigns.nvim

Git integration for buffers
MIT License
4.91k stars 187 forks source link

Inconvenient Lua API #1030

Closed Iskustvo closed 1 month ago

Iskustvo commented 3 months ago

Description

Thank you for this plugin and sorry for the long post (I wrote it locally, before seeing the template :disappointed: )! I'm well aware that this contains a mix of bugs, feature requests and questions for help, but I honestly believe that as a whole, it should help you better understand struggles and frustration of everyday users with current API. Please bear with me on this one, and if you prefer, I'll later split it into smaller issues (whichever you consider "in scope").

Goal

I want to create <Enter> mapping which will:

Check existing support (technical breakdown)

It seems quite reassuring that this is "in scope" of the plugin and should be easy to implement, right?

Implementation

Blame the line under cursor and get commit SHA

Unfortunately, gitsigns.blame_line seems to be the only API available for the job. Issues:

Because of these, the only way to extract blamed commit SHA via Gitsigns is the following:

-- Open popup window with commit of blamed line.
gitsigns.blame_line(
    { full = false },
    function()
        -- In order to focus opened popup window, blame_line needs to be called again.
        gitsigns.blame_line(
            {},
            function()
                -- Now that popup is focused, extract commit SHA from the start of it.
                local blamed_commit = vim.fn.getline(1):match("^(%x+)")

                -- Close the focused popup.
                vim.cmd(":quit")

                -- If commit is unavailable (i.e. local changes), reopen the popup window, but without focus.
                if blamed_commit == nil then
                    gitsigns.blame_line()
                    return
                end

                -- Rest of the code that has to be executed synchronously after blamed commit is extracted.
            end)
    end)

Preferred solution: Having something like gitsigns.get_blame_line which will synchronously return blamed line for opened file snapshot.

Open snapshot of a file in blamed commit

Here, gitsigns.show does just what we want, right? Issues:

Because of this, inconvenient code is forced on us again:

-- Open popup window with commit SHA of blamed line.
gitsigns.blame_line(
    { full = false },
    function()
        -- In order to focus opened popup window, blame_line needs to be called again.
        gitsigns.blame_line(
            {},
            function()
                -- Now that popup is focused, extract commit SHA from the start of it.
                local blamed_commit = vim.fn.getline(1):match("^(%x+)")

                -- Close the focused popup.
                vim.cmd(":quit")

                -- If commit is unavailable (i.e. local changes), reopen the popup window, but without focus.
                if blamed_commit == nil then
                    gitsigns.blame_line()
                    return
                end

                -- Create new tab which is viewing current version of the file.
                vim.cmd(":tabnew %")

                -- React to opening of the gitsigns.show (below).
                vim.api.nvim_create_autocmd("User", {
                    pattern = "GitSignsUpdate",
                    callback = function()
                        -- Rest of the code that has to be executed synchronously after gitsigns.show.
                    end,
                    once = true
                })

                -- Open and focus blamed version of the file in new tab.
                gitsigns.show(blamed_commit)
            end)
    end)

Turn that snapshot into before/after diff

Again, this also looks like a thing that can't go wrong. We just have to diffthis, right?

Issues:

Only thing that fixed "recursive" behavior was doing diffthis synchronously after manually changing the base:

-- Open popup window with commit SHA of blamed line.
gitsigns.blame_line(
    { full = false },
    function()
        -- In order to focus opened popup window, blame_line needs to be called again.
        gitsigns.blame_line(
            {},
            function()
                -- Now that popup is focused, extract commit SHA from the start of it.
                local blamed_commit = vim.fn.getline(1):match("^(%x+)")

                -- Close the focused popup.
                vim.cmd(":quit")

                -- If commit is unavailable (i.e. local changes), reopen the popup window, but without focus.
                if blamed_commit == nil then
                    gitsigns.blame_line()
                    return
                end

                -- Create new tab which is viewing current version of the file.
                vim.cmd(":tabnew %")

                -- React to opening of the gitsigns.show (below).
                vim.api.nvim_create_autocmd("User", {
                    pattern = "GitSignsUpdate",
                    callback = function()
                        -- Diff blamed version of the file with its parent's version of the same file.
                        gitsigns.change_base(blamed_commit .. "^", false, gitsigns.diffthis)
                    end,
                    once = true
                })

                -- Open and focus blamed version of the file in new tab.
                gitsigns.show(blamed_commit)
            end)
    end)

Test environment

Outstanding bugs

Wrong caching (I assume)

Steps to reproduce:

Initial commits are missing signs and cannot be blamed.

Steps to reproduce:

NOTE: After retesting with minimal.lua, I see the following error, which doesn't popup otherwise.

Error executing luv callback:
...fig/nvim/gitsigns_issue/gitsigns//lua/gitsigns/async.lua:85: The coroutine failed with this message: ...onfig/nvim/gitsigns_issue/gitsigns//lua/gitsigns/git.lua:516: (ERROR) ...onfig/nvim/gitsigns_issue/gitsigns//lua/gitsigns/git.lua(516
): fatal: Not a valid object name fff6a8ba^^

This makes perfect sense given that fff6a8ba^ is initial commit and fff6a8ba^^ is therefore invalid. But it again makes me stuck because the only way to avoid this is to manually invoke git and check this edge case. Gitsigns doesn't provide me any way to handle it gracefully.

Neovim version

NVIM v0.10.0

Operating system and version

Arch Linux (x86_64) Kernel: 6.9.2-arch1-1

Expected behavior

No response

Actual behavior

Everything is in description.

Minimal config

for name, url in pairs {
    gitsigns = 'https://github.com/lewis6991/gitsigns.nvim',
} do
    local install_path = vim.fn.fnamemodify('gitsigns_issue/' .. name, ':p')
    if vim.fn.isdirectory(install_path) == 0 then
        vim.fn.system { 'git', 'clone', '--depth=1', url, install_path }
    end
    vim.opt.runtimepath:append(install_path)
end

require('gitsigns').setup {
    debug_mode = true, -- You must add this to enable debug messages
    on_attach = function(buffer_number)
        local gitsigns = require('gitsigns')
        vim.keymap.set(
            'n',
            '<Enter>',
            function()
                -- Open popup window with commit SHA of blamed line.
                gitsigns.blame_line(
                    { full = false },
                    function()
                        -- In order to focus opened popup window, blame_line needs to be called again.
                        gitsigns.blame_line({},
                            function()
                                -- Now that popup is focused, extract commit SHA from the start of it.
                                local blamed_commit = vim.fn.getline(1):match("^(%x+)")

                                -- Close the focused popup.
                                vim.cmd(":quit")

                                -- If commit is unavailable (i.e. local changes), reopen the popup window, but without focus.
                                if blamed_commit == nil then
                                    gitsigns.blame_line()
                                    return
                                end

                                -- Create new tab which is viewing current version of the file.
                                vim.cmd(":tabnew %")

                                -- React to opening of the gitsigns.show (below) and diff it.
                                vim.api.nvim_create_autocmd("User", {
                                    pattern = "GitSignsUpdate",
                                    callback = function()
                                        -- Diff blamed version of the file with its parent's version of the same file.
                                        gitsigns.change_base(blamed_commit .. "^", false, gitsigns.diffthis)
                                        -- gitsigns.diffthis(blamed_commit .. "^")
                                    end,
                                    once = true
                                })

                                -- Open and focus blamed version of the file in new tab.
                                gitsigns.show(blamed_commit)
                            end)
                    end)
            end,
            { buffer = buffer_number, desc = "[Enter] the diff mode between current's line commit and its parent" })
    end
}

Steps to reproduce

Everything is in description.

Gitsigns debug messages

No response

lewis6991 commented 3 months ago

Looks like you are trying to use the functions outside their intended use.

Consider using vim.system({'git', ...}):wait() directly.

Iskustvo commented 3 months ago

Well, yeah, since Lua API is designed for convenient mapping, not programming :(

While I can use git directly, given that I still like and want to have this plugin, it feels like a bad idea:

I would like to split this into following more concrete issues:

Of course, this whole "feature" (minus the new Vim tab) can be internally implemented inside of plugin and exposed as a function, but I'm not sure how many people would use it, so it's questionable. On the other hand, I really feel like Gitsings should have building blocks to allow anyone to implement this in efficient/convenient way, given the scope that it already has.

What do you think, should I proceed and create proposed issues?

lewis6991 commented 3 months ago

You can create issues, but I'll consider each separately, in conjunction with all other issues and feature requests.

There would also be no timeline for these either since I use this plugin mostly for myself and have limited time.

Iskustvo commented 3 months ago

No problem, I fully understand the situation you're in. Regardless, thank you for taking the time for this!

I guess I'll just spawn smaller tickets aiming to achieve this mapping as I go, given that behavior is constantly changed by your work. At the very least, your last commit solved this one already:

[Bug] When Gitsigns attaches to a snapshot of a file in initial commit, "signs" don't indicate that all lines were added in that commit, as they do for all other commits (compared to their parent's snapshot)

Iskustvo commented 1 month ago

Hey @lewis6991, with latest changes I'm able to get (mostly) desired behavior :tada: Unfortunately, it's now even more error-prone and uglier than before, but at least it works :smile:

    vim.keymap.set(
        'n',
        '<Enter>',
        function()
            -- Open popup window with commit SHA of blamed line.
            gitsigns.blame_line(
                { full = false },
                function()
                    -- In order to focus opened popup window, blame_line needs to be called again.
                    gitsigns.blame_line({},
                        function()
                            -- Now that popup is focused, extract commit SHA from the start of it.
                            local blamed_commit = vim.fn.getline(1):match("^(%x+)")

                            -- Close the focused popup.
                            vim.cmd(":quit")

                            -- If commit is unavailable (i.e. local changes), reopen the popup window, but without focus.
                            if blamed_commit == nil then
                                gitsigns.blame_line()
                                return
                            end

                            -- Create new tab which is viewing current version of the file.
                            vim.cmd(":tabnew %")

                            -- React to opening of the gitsigns.show (below) and diff it.
                            -- FIXME: Note that diff doesn't work until all 3 updates happened, so ignore first two.
                            local callback_count = 0
                            vim.api.nvim_create_autocmd("User", {
                                pattern = "GitSignsUpdate",
                                callback = function()
                                    callback_count = callback_count + 1
                                    if (callback_count < 3) then
                                        return false
                                    end

                                    -- Diff blamed version of the file with its parent's version of the same file.
                                    gitsigns.diffthis(blamed_commit .. "^")

                                    return true
                                end,
                            })

                            -- Open and focus blamed version of the file in new tab.
                            gitsigns.show(blamed_commit)
                        end)
                end)
        end,
        { buffer = buffer_number, desc = "[Enter] the diff mode between current's line commit and its parent" })

Further improvements:

Obviously, last two can be handled on the user side (for last one, it may even be the best solution), but I really think first one should be somehow handled in the plugin itself. Therefore, I'll open another issue for that one. (Done)

I know there are no timelines and appreciate you working on this at all, but if you have some immediate feedback/suggestion, I'd like to hear them!

Iskustvo commented 1 month ago

Hey @lewis6991, after recent fixes, I think this one is in quite good shape now. Only #1045 remains from my wish list and since we already have a ticket for it, I'll close this epic. Thank you for all the work and patience!

For the reference, I ended up with following implementation for now:

    vim.keymap.set(
        'n',
        '<Enter>',
        function()
            -- Open popup window with commit SHA of blamed line.
            gitsigns.blame_line(
                { full = false },
                function()
                    -- In order to focus opened popup window, blame_line needs to be called again.
                    gitsigns.blame_line({},
                        function()
                            -- Now that popup is focused, extract commit SHA from the start of it.
                            local blamed_commit = vim.fn.getline(1):match("^(%x+)")

                            -- Close the focused popup.
                            vim.cmd(":quit")

                            -- If commit is unavailable (i.e. local changes), reopen the popup window, but without focus.
                            if blamed_commit == nil then
                                gitsigns.blame_line()
                                return
                            end

                            -- Compute parent commit of the blamed one.
                            local blamed_commit_parent = blamed_commit .. "^"
                            local result = vim.system({ "git", "rev-parse", "--short", blamed_commit_parent }):wait()
                            if result.code ~= 0 then
                                print("Ignoring the command since commit " .. blamed_commit_parent .. " doesn't exist!")
                                return
                            end

                            -- Use concrete commit SHA rather than SHA and ^ notation.
                            blamed_commit_parent = result.stdout:match("%S+")

                            -- Create new tab which is viewing current version of the file.
                            vim.cmd(":tabnew %")

                            -- Use new tab to show changes that blamed commit introduced.
                            gitsigns.show(blamed_commit, function() gitsigns.diffthis(blamed_commit_parent) end)
                        end)
                end)
        end,
        { buffer = buffer_number, desc = "[Enter] new tab and show previous changes that modified current line" })