Maintained fork of ruifm's gitlinker, refactored with bug fixes, ssh host alias, blame support and other improvements.
A lua plugin for Neovim to generate sharable file permalinks (with line ranges) for git host websites. Inspired by tpope/vim-fugitive's :GBrowse
.
Here's an example of git permalink: https://github.com/neovim/neovim/blob/2e156a3b7d7e25e56b03683cc6228c531f4c91ef/src/nvim/main.c#L137-L156.
https://github.com/linrongbin16/gitlinker.nvim/assets/6496887/d3e425a5-cf08-487f-badc-d393ca9dda2f
For now supported platforms are:
PRs are welcomed for other git host websites!
GitLink
command instead of default key mappings.?plain=1
for markdown files.stderr
output as error message.uv.spawn
.[!NOTE]
This plugin always supports the latest stable and (possibly) nightly Neovim version.
require("lazy").setup({
{
"linrongbin16/gitlinker.nvim",
cmd = "GitLink",
opts = {},
keys = {
{ "<leader>gy", "<cmd>GitLink<cr>", mode = { "n", "v" }, desc = "Yank git link" },
{ "<leader>gY", "<cmd>GitLink!<cr>", mode = { "n", "v" }, desc = "Open git link" },
},
},
})
return require('pckr').add(
{
'linrongbin16/gitlinker.nvim',
config = function()
require('gitlinker').setup()
end,
};
)
You can use the user command GitLink
to generate a (perm)link to the git host website:
GitLink
: copy the /blob
url to clipboard.GitLink blame
: copy the /blame
url to clipboard.GitLink default_branch
: copy the /main
or /master
url to clipboard.GitLink current_branch
: copy the current branch url to clipboard.[!NOTE]
Add
!
after the command (GitLink!
) to directly open the url in browser.
There're several router types:
browse
: generate the /blob
url (default).blame
: generate the /blame
url.default_branch
: generate the /main
or /master
url.current_branch
: generate the current branch url.[!NOTE]
A router type is a collection of multiple implementations binding on different git host websites, it works for any git hosts. For example the bitbucket.org:
browse
generates the/src
url (default): https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/dbf3922382576391fbe50b36c55066c1768b08b6/.gitignore#lines-9:14.blame
generates the/annotate
url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/annotate/dbf3922382576391fbe50b36c55066c1768b08b6/.gitignore#lines-9:14.default_branch
generates the/main
or/master
url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/master/.gitignore#lines-9:14.current_branch
generates the current branch url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/feat-dev/.gitignore#lines-9:14.
When there are multiple git remotes, please specify the remote with remote=xxx
parameter. For example:
GitLink remote=upstream
: copy url for the upstream
remote.GitLink! blame remote=upstream
: open blame url for the upstream
remote.[!NOTE]
By default
GitLink
will use the first detected remote (usually it'sorigin
).
When the current buffer name is not the file name you want, please specify the target file path with file=xxx
parameter. For example:
GitLink file=lua/gitlinker.lua
: copy url for the lua/gitlinker.lua
file.GitLink! blame file=README.md
: open blame url for the README.md
file.[!NOTE]
By default
GitLink
will use the current buffer's name.
When the current git repository's commit ID is not that one you want, please specify the target commit ID with rev=xxx
parameter. For example:
GitLink rev=00b3f9a1
: copy url for the 00b3f9a1
commit ID.GitLink! blame rev=00b3f9a1
: open blame url for the 00b3f9a1
commit ID.[!NOTE]
By default
GitLink
will use the current git repository's commit ID.
[!NOTE]
Highly recommend reading Customize Urls before this section, which helps understanding the router design of this plugin.
You can also use the link
API to generate git permlink:
--- @alias gitlinker.Linker {remote_url:string,protocol:string?,username:string?,password:string?,host:string,port:string?,org:string?,user:string?,repo:string,rev:string,file:string,lstart:integer,lend:integer,file_changed:boolean,default_branch:string?,current_branch:string?}
--- @alias gitlinker.Router fun(lk:gitlinker.Linker):string?
--- @alias gitlinker.Action fun(url:string):any
--- @param opts {router_type:string?,router:gitlinker.Router?,action:gitlinker.Action?,lstart:integer?,lend:integer?,message:boolean?,highlight_duration:integer?,remote:string?,file:string?,rev:string?}?
require("gitlinker").link(opts)
The
GitLink
is actually just a user command wrapper on this API.
Parameters:
opts
: (Optional) lua table that contains below fields:
router_type
: Which router type should use. By default is browse
when not specified. It has below options:
browse
blame
default_branch
current_branch
router
: Which router implementation should use. By default it uses the configured implementations when this plugin is been setup (see Configuration). You can overwrite the configured behavior by passing your implementation to this field. Please see gitlinker.Router
for more details.
[!NOTE]
Once set this field, you will get full control of generating the url, and
router_type
field will no longer take effect.
action
: What action should do. By default it will copy the generated link to clipboard. It has below options, please see gitlinker.Action
for more details.
require("gitlinker.actions").clipboard
: Copy url to clipboard.require("gitlinker.actions").system
: Open url in browser.lstart
/lend
: Line range, i.e. start and end line numbers. By default it uses the current line or visual selections. You can also overwrite them to specify the line numbers.
message
: Whether print message in command line. By default it uses the configured value while this plugin is been setup (see Configuration). You can overwrite the configured behavior by passing your option to this field.
highlight_duration
: How long (in milliseconds) to highlight the line range. By default it uses the configured value while this plugin is been setup (see Configuration). You can overwrite the configured behavior by passing your option to this field.
remote
: Specify the git remote. By default it uses the first detected git remote (usually it's origin
).
file
: Specify the relative file path. By default it uses the current buffer's name.
rev
: Specify the git commit ID. By default it uses the current git repository's commit ID.
gitlinker.Router
A lua function that implements a router for a git host website. It uses below function signature:
function(lk:gitlinker.Linker):string?
Parameters:
lk
: A lua table that presents the gitlinker.Linker
data type. It contains all the information (fields) you need to generate a git link, e.g. the protocol
, host
, username
, path
, rev
, etc. Please see Customize Urls - Lua Function for more details.Returns:
string
type, if success.nil
, if failed.gitlinker.Action
A lua function that does some operations with the generated url. It uses below function signature:
function(url:string):any
Parameters:
url
: The generated url. For example: https://codeberg.org/linrongbin16/gitlinker.nvim/src/commit/a570f22ff833447ee0c58268b3bae4f7197a8ad8/LICENSE#L4-L7.For now we have below builtin actions:
require("gitlinker.actions").clipboard
: Copy url to clipboard.require("gitlinker.actions").system
: Open url in browser.If you only need to print the generated url, you can pass a callback function to consume:
require("gitlinker").link({
action = function(url)
print("generated url:" .. vim.inspect(url))
end,
})
-- with vim command:
-- browse
vim.keymap.set(
{"n", 'v'},
"<leader>gl",
"<cmd>GitLink<cr>",
{ silent = true, noremap = true, desc = "Yank git permlink" }
)
vim.keymap.set(
{"n", 'v'},
"<leader>gL",
"<cmd>GitLink!<cr>",
{ silent = true, noremap = true, desc = "Open git permlink" }
)
-- blame
vim.keymap.set(
{"n", 'v'},
"<leader>gb",
"<cmd>GitLink blame<cr>",
{ silent = true, noremap = true, desc = "Yank git blame link" }
)
vim.keymap.set(
{"n", 'v'},
"<leader>gB",
"<cmd>GitLink! blame<cr>",
{ silent = true, noremap = true, desc = "Open git blame link" }
)
-- default branch
vim.keymap.set(
{"n", 'v'},
"<leader>gd",
"<cmd>GitLink default_branch<cr>",
{ silent = true, noremap = true, desc = "Copy default branch link" }
)
vim.keymap.set(
{"n", 'v'},
"<leader>gD",
"<cmd>GitLink! default_branch<cr>",
{ silent = true, noremap = true, desc = "Open default branch link" }
)
-- default branch
vim.keymap.set(
{"n", 'v'},
"<leader>gc",
"<cmd>GitLink current_branch<cr>",
{ silent = true, noremap = true, desc = "Copy current branch link" }
)
vim.keymap.set(
{"n", 'v'},
"<leader>gD",
"<cmd>GitLink! current_branch<cr>",
{ silent = true, noremap = true, desc = "Open current branch link" }
)
-- with lua api:
-- browse
vim.keymap.set(
{"n", 'v'},
"<leader>gl",
require("gitlinker").link,
{ silent = true, noremap = true, desc = "GitLink" }
)
vim.keymap.set(
{"n", 'v'},
"<leader>gL",
function()
require("gitlinker").link({ action = require("gitlinker.actions").system })
end,
{ silent = true, noremap = true, desc = "GitLink!" }
)
-- blame
vim.keymap.set(
{"n", 'v'},
"<leader>gb",
function()
require("gitlinker").link({ router_type = "blame" })
end,
{ silent = true, noremap = true, desc = "GitLink blame" }
)
vim.keymap.set(
{"n", 'v'},
"<leader>gB",
function()
require("gitlinker").link({
router_type = "blame",
action = require("gitlinker.actions").system,
})
end,
{ silent = true, noremap = true, desc = "GitLink! blame" }
)
-- default branch
vim.keymap.set(
{"n", 'v'},
"<leader>gd",
function()
require("gitlinker").link({ router_type = "default_branch" })
end,
{ silent = true, noremap = true, desc = "GitLink default_branch" }
)
vim.keymap.set(
{"n", 'v'},
"<leader>gD",
function()
require("gitlinker").link({
router_type = "default_branch",
action = require("gitlinker.actions").system,
})
end,
{ silent = true, noremap = true, desc = "GitLink! default_branch" }
)
-- default branch
vim.keymap.set(
{"n", 'v'},
"<leader>gc",
function()
require("gitlinker").link({ router_type = "current_branch" })
end,
{ silent = true, noremap = true, desc = "GitLink current_branch" }
)
vim.keymap.set(
{"n", 'v'},
"<leader>gC",
function()
require("gitlinker").link({
router_type = "current_branch",
action = require("gitlinker.actions").system,
})
end,
{ silent = true, noremap = true, desc = "GitLink! current_branch" }
)
require('gitlinker').setup(opts)
The opts
is an optional lua table that override the default options.
For complete default options, please see Defaults
in configs.lua.
[!NOTE]
Recommend reading Git Protocols and giturlparser for better understanding git urls.
[!NOTE]
Please see
Defaults.router
in configs.lua for more examples.
To create customized urls for other git hosts, please bind the target git host name with a new implementation, which simply constructs the url string from below components (upper case with prefix _A.
):
_A.PROTOCOL
: Network protocol before ://
delimiter. For example:https
in https://github.com
.ssh
in ssh://github.com
._A.USERNAME
: Optional user name component before @
delimiter. For example:git
in ssh://git@github.com/linrongbin16/gitlinker.nvim.git
.myname
in myname@github.com:linrongbin16/gitlinker.nvim.git
(Note: the ssh protocol ssh://
is omitted in this case)._A.PASSWORD
: Optional password component after _A.USERNAME
. For example:mypass
in myname:mypass@github.com:linrongbin16/gitlinker.nvim.git
.mypass
in https://myname:mypass@github.com/linrongbin16/gitlinker.nvim.git
._A.HOST
: The host component. For example:github.com
in https://github.com/linrongbin16/gitlinker.nvim
(Note: for http/https protocol, the host ends with /
).127.0.0.1
in git@127.0.0.1:linrongbin16/gitlinker.nvim
(Note: for omitted ssh protocol, the host ends with :
, and it cannot have _A.PORT
component)._A.PORT
: Optional port component after _A.HOST
(Note: omitted ssh protocols cannot have _A.PORT
component). For example:22
in https://github.com:22/linrongbin16/gitlinker.nvim
.123456
in https://127.0.0.1:123456/linrongbin16/gitlinker.nvim
._A.PATH
: Path component, i.e. all the other parts in the output of the git remote get-url origin
. For example:/linrongbin16/gitlinker.nvim.git
in https://github.com/linrongbin16/gitlinker.nvim.git
.linrongbin16/gitlinker.nvim.git
in git@github.com:linrongbin16/gitlinker.nvim.git
(Note: for ssh protocol, the :
before the path component doesn't belong to it)._A.REV
: Git commit ID. For example:a009dacda96756a8c418ff5fa689999b148639f6
in https://github.com/linrongbin16/gitlinker.nvim/blob/a009dacda96756a8c418ff5fa689999b148639f6/lua/gitlinker/git.lua?plain=1#L3
._A.FILE
: Relative file path. For example:lua/gitlinker/routers.lua
in https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua
._A.LSTART
/_A.LEND
: Start/end line number. For example:5
/13
in https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua#L5-L13
.There're 2 more sugar components derived from _A.PATH
:
_A.REPO
: The last part after the last slash (/
) in _A.PATH
(around slashes are removed, and the .git
suffix is been removed for easier writing). For example:gitlinker.nvim
in https://github.com/linrongbin16/gitlinker.nvim.git
.neovim
in git@192.168.0.1:path/to/the/neovim.git
._A.ORG
: All the previous parts before _A.REPO
(around slashes are removed). For example:linrongbin16
in https://github.com/linrongbin16/gitlinker.nvim.git
.path/to/the
in https://github.com/path/to/the/repo.git
.[!IMPORTANT]
The
_A.ORG
component can be empty if_A.PATH
only contains 1 slash (/
). For example_A.ORG
inssh://git@host.xyz/repo.git
is empty, while_A.REPO
isrepo
.
There're 2 more sugar components for git branches:
_A.DEFAULT_BRANCH
: Default branch retrieved from git rev-parse --abbrev-ref origin/HEAD
. For example:master
in https://github.com/ruifm/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua#L37-L156
.main
in https://github.com/linrongbin16/commons.nvim/blob/main/lua/commons/uv.lua
._A.CURRENT_BRANCH
: Current branch retrieved from git rev-parse --abbrev-ref HEAD
. For example:feat-router-types
in https://github.com/ruifm/gitlinker.nvim/blob/feat-router-types/lua/gitlinker/routers.lua#L37-L156
.With above components, you can customize the line numbers (for example) in form ?&line=1&lines-count=2
like this:
require("gitlinker").setup({
router = {
browse = {
["^github%.your%.host"] = "https://github.your.host/"
.. "{_A.ORG}/"
.. "{_A.REPO}/blob/"
.. "{_A.REV}/"
.. "{_A.FILE}"
.. "?&lines={_A.LSTART}"
.. "{_A.LEND > _A.LSTART and ('&lines-count=' .. _A.LEND - _A.LSTART + 1) or ''}",
},
},
})
The template string use curly braces {}
to contain lua scripts, and evaluate via luaeval() (the error message can be confusing if there's any syntax issue).
[!NOTE]
Please see routers.lua for more examples.
You can also implement the router with a lua function. The function accepts only 1 lua table as its parameter, which contains the same fields as string template, but in lower case, without the prefix _A.
:
protocol
username
password
host
port
path
rev
file
lstart
/lend
The 2 sugar components derived from path
are:
org
repo
(Note: the .git
suffix is not omitted)The 2 git branch components are:
default_branch
current_branch
Recall to previous use case (customize the line numbers in form ?&line=1&lines-count=2
), you can implement the router with below function:
--- @param s string
--- @param t string
local function string_endswith(s, t)
return string.len(s) >= string.len(t) and string.sub(s, #s - #t + 1) == t
end
--- @param lk gitlinker.Linker
local function your_router(lk)
local builder = "https://"
-- host
builder = builder .. lk.host .. "/"
-- org
builder = builder .. lk.org .. "/"
-- repo
builder = builder
.. (string_endswith(lk.repo, ".git") and lk.repo:sub(1, #lk.repo - 4) or lk.repo)
.. "/"
-- rev
builder = lk.rev .. "/"
-- file
builder = builder
.. lk.file
.. (string_endswith(lk.file, ".md") and "?plain=1" or "")
-- line range
builder = builder .. string.format("&lines=%d", lk.lstart)
if lk.lend > lk.lstart then
builder = builder
.. string.format("&lines-count=%d", lk.lend - lk.lstart + 1)
end
return builder
end
require("gitlinker").setup({
router = {
browse = {
["^github%.your%.host"] = your_router,
},
},
})
There are some pre-defined APIs in gitlinker.routers
that you can use:
github_browse
/github_blame
: for https://github.com/.gitlab_browse
/gitlab_blame
: for https://gitlab.com/.bitbucket_browse
/bitbucket_blame
: for https://bitbucket.org/.codeberg_browse
/codeberg_blame
: for https://codeberg.org/.samba_browse
: for https://git.samba.org/ (blame not support).If you need to bind a github enterprise host, please use:
require('gitlinker').setup({
router = {
browse = {
["^github%.your%.host"] = require('gitlinker.routers').github_browse,
},
blame = {
["^github%.your%.host"] = require('gitlinker.routers').github_blame,
},
}
})
You can even create your own router (with the same engine). For example let's create the file_only
router type, it generates url without line numbers:
require("gitlinker").setup({
router = {
file_only = {
["^github%.com"] = "https://github.com/"
.. "{_A.ORG}/"
.. "{_A.REPO}/blob/"
.. "{_A.REV}/"
.. "{_A.FILE}"
},
},
})
Use it just like browse
:
GitLink file_only
GitLink! file_only
Highlight Group | Default Group | Description |
---|---|---|
NvimGitLinkerHighlightTextObject | Search | highlight line ranges when copy/open |
To develop the project and make PR, please setup with:
To run unit tests, please install below dependencies:
Then test with vusted ./spec
.
Please open issue/PR for anything about gitlinker.nvim.
Like gitlinker.nvim? Consider