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.uv.spawn
.plenary
dependency.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 git permlink:
GitLink(!)
: copy the /blob
url to clipboard (use !
to open in browser).GitLink(!) blame
: copy the /blame
url to clipboard (use !
to open in browser).GitLink(!) default_branch
: copy the /main
or /master
url to clipboard (use !
to open in browser).GitLink(!) current_branch
: copy the current branch url to clipboard (use !
to open 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 general collection of router implementations binding on different git hosts, thus it can work for any git hosts, for example for bitbucket.org:
browse
generate the/src
url (default): https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/dbf3922382576391fbe50b36c55066c1768b08b6/.gitignore#lines-9:14.blame
generate the/annotate
url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/annotate/dbf3922382576391fbe50b36c55066c1768b08b6/.gitignore#lines-9:14.default_branch
generate the/main
or/master
url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/master/.gitignore#lines-9:14.current_branch
generate the current branch url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/feat-dev/.gitignore#lines-9:14.
To specify the remote when there're multiple git remotes, add 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.By default
GitLink
will use the first detected remote (usually it'sorigin
).
To specify the relative file path when current buffer's file path is not a normal file name, add 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.By default
GitLink
will use the current buffer's file name.
To specify the git commit ID when current repository's commit ID is not on your needs, add 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.By default
GitLink
will use the current 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)
Parameters:
opts
: (Optional) lua table that contains below fields:
router_type
: Which router type should this API use. By default is nil
, means browse
. It has below builtin options:
browse
blame
default_branch
current_branch
router
: Which router implementation should this API use. By default is nil
, it uses the configured router implementations while this plugin is been setup (see Configuration). You can dynamically overwrite the generate behavior by pass a router in this field.
Once set this field, you will get full control of generating the url, and
router_type
field will no longer take effect.Please refer to
gitlinker.Router
for more details.
action
: What action should this API behave. By default is nil
, this API will copy the generated link to clipboard. It has below builtin options:
require("gitlinker.actions").clipboard
: Copy generated link to clipboard.require("gitlinker.actions").system
: Open generated link in browser.Please refer to
gitlinker.Action
for more details.
lstart
/lend
: Visual selected line range, e.g. start & end line numbers. By default both are nil
, it will automatically try to find user selected line range. You can also overwrite these two fields to force the line numbers in generated url.
message
: Whether print message in nvim command line. By default it uses the configured value while this plugin is been setup (see Configuration). You can also overwrite this field to change the configured behavior.
highlight_duration
: How long (milliseconds) to highlight the line range. By default it uses the configured value while this plugin is been setup (see Configuration). You can also overwrite this field to change the configured behavior.
remote
: Specify the git remote. By default is nil
, it uses the first detected git remote (usually it's origin
).
file
: Specify the relative file path. By default is nil
, it uses the current buffer's file name.
rev
: Specify the git commit ID. By default is nil
, it uses the current repository's git commit ID.
gitlinker.Router
gitlinker.Router
is a lua function that implements a router for a git host. It use below function signature:
function(lk:gitlinker.Linker):string?
Parameters:
lk
: 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 refer to Customize Urls - Lua Function for more details.
Returns:
string
type, if success.nil
, if failed.gitlinker.Action
gitlinker.Action
is a lua function that do some operations with a generated git link. It use below function signature:
function(url:string):any
Parameters:
url
: The generated git link. 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 get the generated url, instead of do some actions, you can pass a callback function to accept the url:
require("gitlinker").link({
action = function(url)
print("generated url:" .. vim.inspect(url))
end,
})
The
link
API is running in async mode and cannot directly returns the generated link, because it uses lua coroutine to avoid blocking IO.
-- 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]
Please refer to Git Protocols and giturlparser for better understanding git url.
[!NOTE]
Please refer to
Defaults.router
in configs.lua for more examples about string template.
To create customized urls for other git hosts, please bind the target git host name with a new router. A router 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://
can be omitted)._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, host ends with /
).127.0.0.1
in git@127.0.0.1:linrongbin16/gitlinker.nvim
(Note: for omitted ssh protocol, host ends with :
, and 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
: 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
._A.REV
: Git commit, 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
, with around slashes been 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 other parts before _A.REPO
, with around slashes been 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 when the_A.PATH
contains only 1 slash (/
), for example: the_A.ORG
inssh://git@host.xyz/repo.git
is empty.
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
.For example you can customize the line numbers 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(), while the error message can be confusing if there's any syntax issue.
[!NOTE]
Please refer to routers.lua for more examples about function-based routers.
You can also bind a lua function to the git host, 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 derived components are:
org
repo
: Note: the .git
suffix is not omitted.The 2 branch components are:
default_branch
current_branch
Recall to previous use case, e.g. 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 lua apis in gitlinker.routers
that you can use:
github_browse
/github_blame
: for github.com.gitlab_browse
/gitlab_blame
: for gitlab.com.bitbucket_browse
/bitbucket_blame
: for bitbucket.org.codeberg_browse
/codeberg_blame
: for codeberg.org.samba_browse
: for git.samba.org (blame not support).For example if you need to bind a github enterprise domain, you can 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 (e.g. use the same engine with browse
/blame
), for example create the file_only
router type (generate link without line numbers):
require("gitlinker").setup({
router = {
file_only = {
["^github%.com"] = "https://github.com/"
.. "{_A.ORG}/"
.. "{_A.REPO}/blob/"
.. "{_A.REV}/"
.. "{_A.FILE}"
},
},
})
Then 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