From ab44bb7eb3174d963b0ae2224a685f322638c8e6 Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Mon, 8 Jan 2024 18:00:32 +0800 Subject: [PATCH] fix(parsing): fix git url parsing (#195) --- .github/PULL_REQUEST_TEMPLATE.md | 14 - .github/workflows/ci.yml | 8 + .luacheckrc | 7 +- .luacov | 1 + .styluaignore | 2 + README.md | 153 ++++++---- codecov.yml | 1 + lua/gitlinker.lua | 137 ++++----- lua/gitlinker/giturlparser.lua | 484 +++++++++++++++++++++++++++++++ lua/gitlinker/linker.lua | 147 ++-------- lua/gitlinker/routers.lua | 18 +- test/gitlinker_spec.lua | 173 +++++------ test/linker_spec.lua | 54 ---- 13 files changed, 755 insertions(+), 444 deletions(-) create mode 100644 .styluaignore create mode 100644 lua/gitlinker/giturlparser.lua diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d5e088d..71a8948 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,17 +1,3 @@ -# For New Use Cases - -1. What's the output of `git remote get-url origin`? - - It's ... - -2. what's the expect git host url you want to generate? - - It's ... - -3. how do you configure this plugin? - - It's ... - # Regression Test ## Platforms diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3145688..48f7cb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,14 @@ jobs: cp ~/.commons.nvim/version.txt ./lua/gitlinker/commons/version.txt cd ./lua/gitlinker/commons find . -type f -name '*.lua' -exec sed -i 's/require("commons/require("gitlinker.commons/g' {} \; + - name: Install giturlparser.lua + if: ${{ github.ref != 'refs/heads/master' }} + shell: bash + run: | + echo "pwd" + echo $PWD + git clone --depth=1 https://github.com/linrongbin16/giturlparser.lua.git ~/.giturlparser.lua + cp ~/.giturlparser.lua/src/giturlparser.lua ./lua/gitlinker/giturlparser.lua - uses: stefanzweifel/git-auto-commit-action@v4 if: ${{ github.ref != 'refs/heads/master' }} with: diff --git a/.luacheckrc b/.luacheckrc index 5c2db4e..1388eaa 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -2,5 +2,8 @@ globals = { "vim", "describe", "before_each", "it", "assert" } max_line_length = 500 unused = false unused_args = false -exclude_files = - { "lua/gitlinker/commons/_system.lua", "lua/gitlinker/commons/_json.lua" } +exclude_files = { + "lua/gitlinker/commons/_system.lua", + "lua/gitlinker/commons/_json.lua", + "lua/gitlinker/giturlparser.lua", +} diff --git a/.luacov b/.luacov index 6b665e2..e60afe4 100644 --- a/.luacov +++ b/.luacov @@ -5,4 +5,5 @@ modules = { exclude = { "lua/gitlinker/commons/*.lua", + "lua/gitlinker/giturlparser.lua", } diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 0000000..aee8c82 --- /dev/null +++ b/.styluaignore @@ -0,0 +1,2 @@ +lua/gitlinker/giturlparser.lua +lua/gitlinker/commons/*.lua diff --git a/README.md b/README.md index 58d23b4..55cb038 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ PRs are welcomed for other git host websites! - [Highlighting](#highlighting) - [Self-host Git Hosts](#self-host-git-hosts) - [Fully Customize Urls](#fully-customize-urls) - - [GitWeb](#gitweb) - [Create Your Own Router](#create-your-own-router) - [Highlight Group](#highlight-group) - [Development](#development) @@ -185,7 +184,7 @@ require('gitlinker').setup({ browse = { -- example: https://github.com/linrongbin16/gitlinker.nvim/blob/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^github%.com"] = "https://github.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.REV}/" .. "{_A.FILE}?plain=1" -- '?plain=1' @@ -193,15 +192,15 @@ require('gitlinker').setup({ .. "{(_A.LEND > _A.LSTART and ('-L' .. _A.LEND) or '')}", -- example: https://gitlab.com/linrongbin16/gitlinker.nvim/blob/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^gitlab%.com"] = "https://gitlab.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.REV}/" .. "{_A.FILE}" .. "#L{_A.LSTART}" .. "{(_A.LEND > _A.LSTART and ('-L' .. _A.LEND) or '')}", - -- example: https://bitbucket.org/linrongbin16/gitlinker.nvim/src/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#lines-3:4 + -- example: https://bitbucket.org/linrongbin16/gitlinker.nvim/src/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^bitbucket%.org"] = "https://bitbucket.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/src/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -209,7 +208,7 @@ require('gitlinker').setup({ .. "{(_A.LEND > _A.LSTART and (':' .. _A.LEND) or '')}", -- example: https://codeberg.org/linrongbin16/gitlinker.nvim/src/commit/a570f22ff833447ee0c58268b3bae4f7197a8ad8/LICENSE#L5-L6 ["^codeberg%.org"] = "https://codeberg.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/src/commit/" .. "{_A.REV}/" .. "{_A.FILE}?display=source" -- '?display=source' @@ -219,7 +218,7 @@ require('gitlinker').setup({ -- main repo: https://git.samba.org/?p=samba.git;a=blob;f=wscript;hb=83e8971c0f1c1db8c3574f83107190ac1ac23db0#l6 -- dev repo: https://git.samba.org/?p=bbaumbach/samba.git;a=blob;f=wscript;hb=8de348e9d025d336a7985a9025fe08b7096c0394#l7 ["^git%.samba%.org"] = "https://git.samba.org/?p=" - .. "{string.len(_A.USER) > 0 and (_A.USER .. '/') or ''}" -- 'p=samba.git;' or 'p=bbaumbach/samba.git;' + .. "{string.len(_A.ORG) > 0 and (_A.ORG .. '/') or ''}" -- 'p=samba.git;' or 'p=bbaumbach/samba.git;' .. "{_A.REPO .. '.git'};a=blob;" .. "f={_A.FILE};" .. "hb={_A.REV}" @@ -228,7 +227,7 @@ require('gitlinker').setup({ blame = { -- example: https://github.com/linrongbin16/gitlinker.nvim/blame/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^github%.com"] = "https://github.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blame/" .. "{_A.REV}/" .. "{_A.FILE}?plain=1" -- '?plain=1' @@ -236,7 +235,7 @@ require('gitlinker').setup({ .. "{(_A.LEND > _A.LSTART and ('-L' .. _A.LEND) or '')}", -- example: https://gitlab.com/linrongbin16/gitlinker.nvim/blame/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^gitlab%.com"] = "https://gitlab.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blame/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -244,7 +243,7 @@ require('gitlinker').setup({ .. "{(_A.LEND > _A.LSTART and ('-L' .. _A.LEND) or '')}", -- example: https://bitbucket.org/linrongbin16/gitlinker.nvim/annotate/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#lines-3:4 ["^bitbucket%.org"] = "https://bitbucket.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/annotate/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -252,7 +251,7 @@ require('gitlinker').setup({ .. "{(_A.LEND > _A.LSTART and (':' .. _A.LEND) or '')}", -- example: https://codeberg.org/linrongbin16/gitlinker.nvim/blame/commit/a570f22ff833447ee0c58268b3bae4f7197a8ad8/LICENSE#L5-L6 ["^codeberg%.org"] = "https://codeberg.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blame/commit/" .. "{_A.REV}/" .. "{_A.FILE}?display=source" -- '?display=source' @@ -323,17 +322,59 @@ You can directly use below builtin APIs: ### Fully Customize Urls +> [!NOTE] +> +> Please refer to [Git Protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols) and [giturlparser](https://github.com/linrongbin16/giturlparser.lua?tab=readme-ov-file#features) for better understanding git url. + To fully customize url generation, please refer to the implementation of [routers.lua](https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua), a router is simply construct the url string from below components: -- `protocol`: `git@`, `ssh://git@`, `https`, etc. -- `host`: `github.com`, `gitlab.com`, `bitbucket.org`, etc. -- `user`: `linrongbin16` (for this plugin), `neovim` (for [neovim](https://github.com/neovim/neovim)), etc. -- `repo`: `gitlinker.nvim.git`, `neovim.git`, etc. -- `rev`: git commit, e.g. `dbf3922382576391fbe50b36c55066c1768b08b6`. -- `default_branch`: git default branch, `master`, `main`, etc, retrieved from `git rev-parse --abbrev-ref origin/HEAD`. -- `current_branch`: git current branch, `feat-router-types`, etc, retrieved from `git rev-parse --abbrev-ref HEAD`. -- `file`: file name, e.g. `lua/gitlinker/routers.lua`. -- `lstart`/`lend`: start/end line numbers, e.g. `#L37-L156`. +- `protocol`: The component before `://` delimiter. For example: + - The `https` in `https://github.com`. + - The `ssh` in `ssh://github.com`. +- `username`: Optional component after `protocol`, before host name separated by `@`. For example: + - The `git` in `ssh://git@github.com:linrongbin16/gitlinker.nvim.git`. + - The `myname` in `myname@github.com:linrongbin16/gitlinker.nvim.git` (**Note:** the `ssh://` in ssh protocol can be omitted). +- `password`: Optional component after `username` separated by `:`, before `host` name separated by `@`. For example: + - The `mypass` in `ssh://myname:mypass@github.com:linrongbin16/gitlinker.nvim.git`. + - The `mypass` in `https://myname:mypass@github.com/linrongbin16/gitlinker.nvim.git`. +- `host`: The first component after `protocol` (and optional `username`, `password`). For example: + - The `github.com` in `https://github.com/linrongbin16/gitlinker.nvim` (**Note:** for http/https, `host` ends with `/`). + - The `127.0.0.1` in `ssh://127.0.0.1:linrongbin16/gitlinker.nvim` (**Note:** for ssh, `host` ends with `:`, and cannot have the following `port` component). +- `port`: Optional component after `host` separated by `:` (**Note:** ssh protocol cannot have `port` component). For example: + - The `22` in `https://github.com:22/linrongbin16/gitlinker.nvim`. + - The `123456` in `https://127.0.0.1:123456/linrongbin16/gitlinker.nvim`. +- `path`: All the left parts after `host` (and optional `port`). 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`. +- `rev`: Git commit. For example: + - The `a009dacda96756a8c418ff5fa689999b148639f6` in `https://github.com/linrongbin16/gitlinker.nvim/blob/a009dacda96756a8c418ff5fa689999b148639f6/lua/gitlinker/git.lua?plain=1#L3`. +- `file`: Relative file path. For example: + - `lua/gitlinker/routers.lua` in `https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua`. +- `lstart`/`lend`: Start/end line numbers. For example: + - `3`/`13` in `https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua#L3-L13`. + +There're also 2 sugar components derived from `path`: + +- `repo`: The last part after the last slash (`/`) in `path`, with around slashes been removed. For example: + - `gitlinker.nvim.git` in `https://github.com/linrongbin16/gitlinker.nvim`. + - `neovim.git` in `https://github.com/neovim/neovim.git`. +- `org`: (Optional) all the other parts before `repo` in `path`, 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`. + +> [!NOTE] +> +> The `org` component can be empty when the `path` only contains 1 slash (`/`), for example: +> +> - `ssh://git@host.xyz/repo.git`. + +There're also 2 branch components: + +- `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`. +- `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: @@ -347,21 +388,21 @@ end --- @param lk gitlinker.Linker local function your_router(lk) local builder = "https://" - -- host: 'github.com', 'gitlab.com', 'bitbucket.org' + -- host builder = builder .. lk.host .. "/" - -- user: 'linrongbin16', 'neovim' - builder = builder .. lk.user .. "/" - -- repo: 'gitlinker.nvim.git', 'neovim' + -- 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: git commit, e.g. 'e605210941057849491cca4d7f44c0e09f363a69' + -- rev builder = lk.rev .. "/" - -- file: 'lua/gitlinker/logger.lua' + -- file builder = builder .. lk.file .. (string_endswith(lk.file, ".md") and "?plain=1" or "") - -- line range: start line number, end line number + -- line range builder = builder .. string.format("&lines=%d", lk.lstart) if lk.lend > lk.lstart then builder = builder @@ -388,7 +429,7 @@ require("gitlinker").setup({ router = { browse = { ["^github%.your%.host"] = "https://github.your.host/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -403,39 +444,27 @@ The template string use curly braces `{}` to contains lua scripts, and evaluate The available variables are the same with the `lk` parameter passing to hook functions, but in upper case, and with the `_A.` prefix: -- `_A.PROTOCOL`: `git@`, `ssh://git@`, `https`, etc. -- `_A.HOST`: `github.com`, `gitlab.com`, `bitbucket.org`, etc. -- `_A.USER`: `linrongbin16` (for this plugin), `neovim` (for [neovim](https://github.com/neovim/neovim)), etc. -- `_A.REPO`: `gitlinker.nvim`, `neovim`, etc. - - **Note:** for easier writing, the `.git` suffix is been removed. -- `_A.REV`: git commit, e.g. `dbf3922382576391fbe50b36c55066c1768b08b6`. -- `_A.DEFAULT_BRANCH`: git default branch, `master`, `main`, etc, retrieved from `git rev-parse --abbrev-ref origin/HEAD`. -- `_A.CURRENT_BRANCH`: git current branch, `feat-router-types`, etc, retrieved from `git rev-parse --abbrev-ref HEAD`. +- `_A.PROTOCOL` +- `_A.USERNAME` +- `_A.PASSWORD` +- `_A.HOST` +- `_A.PORT` +- `_A.PATH` +- `_A.REV` +- `_A.DEFAULT_BRANCH` +- `_A.CURRENT_BRANCH` - `_A.FILE`: file name, e.g. `lua/gitlinker/routers.lua`. - `_A.LSTART`/`_A.LEND`: start/end line numbers, e.g. `#L37-L156`. -### GitWeb +The 2 sugar components derived from `path` are: -For [GitWeb](https://git-scm.com/book/en/v2/Git-on-the-Server-GitWeb), there're two types of urls: the main repository and the user's dev repository. For example on [git.samba.org](https://git.samba.org/): - -```bash -# main repo -https://git.samba.org/samba.git (`git remote get-url origin`) -https://git.samba.org/?p=samba.git;a=blob;f=wscript;hb=83e8971c0f1c1db8c3574f83107190ac1ac23db0#l7 -| | | | | | -protocol host repo file rev line number - -# user's dev repo -https://git.samba.org/bbaumbach/samba.git (`git remote get-url origin`) -https://git.samba.org/?p=bbaumbach/samba.git;a=blob;f=wscript;hb=8de348e9d025d336a7985a9025fe08b7096c0394#l7 -| | | | | | | -protocol host user repo file rev line number -``` +- `_A.ORG` +- `_A.REPO` - **Note:** for easier writing, the `.git` suffix is been removed. -The difference is: the main repo doesn't have the `user` component, it's just `https://git.samba.org/?p=samba.git`. To support such case, `user` and `repo` components have a little bit different when facing the main repo: +The 2 branch components are: -- `lk.user` (`_A.USER`): the value is `` (empty string). -- `lk.repo`: the value is `samba.git`, for `_A.REPO` the value is `samba` (the `.git` suffix is been removed for easier writing url template). +- `_A.DEFAULT_BRANCH` +- `_A.CURRENT_BRANCH` ### Create Your Own Router @@ -446,7 +475,7 @@ require("gitlinker").setup({ router = { default_branch = { ["^github%.com"] = "https://github.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.DEFAULT_BRANCH}/" -- always 'master'/'main' branch .. "{_A.FILE}?plain=1" -- '?plain=1' @@ -455,7 +484,7 @@ require("gitlinker").setup({ }, current_branch = { ["^github%.com"] = "https://github.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.CURRENT_BRANCH}/" -- always current branch .. "{_A.FILE}?plain=1" -- '?plain=1' @@ -466,13 +495,13 @@ require("gitlinker").setup({ }) ``` -Then use it just like `blame`: +Then use it just like `browse`: ```vim -GitLink default_branch " copy default branch to clipboard -GitLink! default_branch " open default branch in browser -GitLink current_branch " copy current branch to clipboard -GitLink! current_branch " open current branch in browser +GitLink default_branch +GitLink! default_branch +GitLink current_branch +GitLink! current_branch ``` ## Highlight Group diff --git a/codecov.yml b/codecov.yml index c18c3db..aedd6b1 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,3 +8,4 @@ coverage: threshold: 90% ignore: - "lua/gitlinker/commons/*.lua" + - "lua/gitlinker/giturlparser.lua" diff --git a/lua/gitlinker.lua b/lua/gitlinker.lua index b757f6c..0d1fbf3 100644 --- a/lua/gitlinker.lua +++ b/lua/gitlinker.lua @@ -30,7 +30,7 @@ local Defaults = { browse = { -- example: https://github.com/linrongbin16/gitlinker.nvim/blob/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^github%.com"] = "https://github.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.REV}/" .. "{_A.FILE}?plain=1" -- '?plain=1' @@ -38,7 +38,7 @@ local Defaults = { .. "{(_A.LEND > _A.LSTART and ('-L' .. _A.LEND) or '')}", -- example: https://gitlab.com/linrongbin16/gitlinker.nvim/blob/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^gitlab%.com"] = "https://gitlab.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -46,7 +46,7 @@ local Defaults = { .. "{(_A.LEND > _A.LSTART and ('-L' .. _A.LEND) or '')}", -- example: https://bitbucket.org/linrongbin16/gitlinker.nvim/src/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^bitbucket%.org"] = "https://bitbucket.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/src/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -54,7 +54,7 @@ local Defaults = { .. "{(_A.LEND > _A.LSTART and (':' .. _A.LEND) or '')}", -- example: https://codeberg.org/linrongbin16/gitlinker.nvim/src/commit/a570f22ff833447ee0c58268b3bae4f7197a8ad8/LICENSE#L5-L6 ["^codeberg%.org"] = "https://codeberg.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/src/commit/" .. "{_A.REV}/" .. "{_A.FILE}?display=source" -- '?display=source' @@ -64,7 +64,7 @@ local Defaults = { -- main repo: https://git.samba.org/?p=samba.git;a=blob;f=wscript;hb=83e8971c0f1c1db8c3574f83107190ac1ac23db0#l6 -- dev repo: https://git.samba.org/?p=bbaumbach/samba.git;a=blob;f=wscript;hb=8de348e9d025d336a7985a9025fe08b7096c0394#l7 ["^git%.samba%.org"] = "https://git.samba.org/?p=" - .. "{string.len(_A.USER) > 0 and (_A.USER .. '/') or ''}" -- 'p=samba.git;' or 'p=bbaumbach/samba.git;' + .. "{string.len(_A.ORG) > 0 and (_A.ORG .. '/') or ''}" -- 'p=samba.git;' or 'p=bbaumbach/samba.git;' .. "{_A.REPO .. '.git'};a=blob;" .. "f={_A.FILE};" .. "hb={_A.REV}" @@ -73,7 +73,7 @@ local Defaults = { blame = { -- example: https://github.com/linrongbin16/gitlinker.nvim/blame/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^github%.com"] = "https://github.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blame/" .. "{_A.REV}/" .. "{_A.FILE}?plain=1" -- '?plain=1' @@ -81,7 +81,7 @@ local Defaults = { .. "{(_A.LEND > _A.LSTART and ('-L' .. _A.LEND) or '')}", -- example: https://gitlab.com/linrongbin16/gitlinker.nvim/blame/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#L3-L4 ["^gitlab%.com"] = "https://gitlab.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blame/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -89,7 +89,7 @@ local Defaults = { .. "{(_A.LEND > _A.LSTART and ('-L' .. _A.LEND) or '')}", -- example: https://bitbucket.org/linrongbin16/gitlinker.nvim/annotate/9679445c7a24783d27063cd65f525f02def5f128/lua/gitlinker.lua#lines-3:4 ["^bitbucket%.org"] = "https://bitbucket.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/annotate/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -97,7 +97,7 @@ local Defaults = { .. "{(_A.LEND > _A.LSTART and (':' .. _A.LEND) or '')}", -- example: https://codeberg.org/linrongbin16/gitlinker.nvim/blame/commit/a570f22ff833447ee0c58268b3bae4f7197a8ad8/LICENSE#L5-L6 ["^codeberg%.org"] = "https://codeberg.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blame/commit/" .. "{_A.REV}/" .. "{_A.FILE}?display=source" -- '?display=source' @@ -129,6 +129,8 @@ local function _url_template_engine(lk, template) return template end + local logger = logging.get("gitlinker") --[[@as commons.logging.Logger]] + --- @alias gitlinker.UrlTemplateExpr {plain:boolean,body:string} --- @type gitlinker.UrlTemplateExpr[] local exprs = {} @@ -182,9 +184,13 @@ local function _url_template_engine(lk, template) table.insert(results, exp.body) else local evaluated = vim.fn.luaeval(exp.body, { - PROTOCOL = lk.protocol, - HOST = lk.host, - USER = lk.user, + PROTOCOL = lk.protocol or "", + USERNAME = lk.username or "", + PASSWORD = lk.password or "", + HOST = lk.host or "", + PORT = lk.port or "", + USER = lk.user or "", + ORG = lk.org or "", REPO = strings.endswith(lk.repo, ".git") and lk.repo:sub(1, #lk.repo - 4) or lk.repo, @@ -200,12 +206,12 @@ local function _url_template_engine(lk, template) lk.current_branch ) > 0) and lk.current_branch or "", }) - -- logger.debug( - -- "|_url_template_engine| exp:%s, lk:%s, evaluated:%s", - -- vim.inspect(exp.body), - -- vim.inspect(lk), - -- vim.inspect(evaluated) - -- ) + logger:debug( + "|_url_template_engine| exp:%s, lk:%s, evaluated:%s", + vim.inspect(exp.body), + vim.inspect(lk), + vim.inspect(evaluated) + ) table.insert(results, evaluated) end end @@ -213,17 +219,6 @@ local function _url_template_engine(lk, template) return table.concat(results, "") end ---- @param lk gitlinker.Linker ---- @return string -local function _make_resolved_remote_url(lk) - local resolved_remote_url = - string.format("%s%s%s%s", lk.protocol, lk.host, lk.host_delimiter, lk.user) - if type(lk.repo) == "string" and string.len(lk.repo) > 0 then - resolved_remote_url = string.format("%s/%s", resolved_remote_url, lk.repo) - end - return resolved_remote_url -end - --- @param lk gitlinker.Linker --- @param p string --- @param r string|function(lk:gitlinker.Linker):string? @@ -276,28 +271,24 @@ local function _router(router_type, lk) if type(i) == "number" and type(tuple) == "table" and #tuple == 2 then local pattern = tuple[1] local route = tuple[2] - local resolved_remote_url = _make_resolved_remote_url(lk) - logger:debug( - "|_router| list i:%d, pattern_route_tuple:%s, match host:%s(%s), remote_url:%s(%s), resolved_remote_url:%s(%s)", - vim.inspect(i), - vim.inspect(tuple), - vim.inspect(string.match(lk.host, pattern)), - vim.inspect(lk.host), - vim.inspect(string.match(lk.remote_url, pattern)), - vim.inspect(lk.remote_url), - vim.inspect(string.match(resolved_remote_url, pattern)), - vim.inspect(resolved_remote_url) - ) + -- logger:debug( + -- "|_router| list i:%d, pattern_route_tuple:%s, match host:%s(%s), remote_url:%s(%s)", + -- vim.inspect(i), + -- vim.inspect(tuple), + -- vim.inspect(string.match(lk.host, pattern)), + -- vim.inspect(lk.host), + -- vim.inspect(string.match(lk.remote_url, pattern)), + -- vim.inspect(lk.remote_url) + -- ) if string.match(lk.host, pattern) or string.match(lk.remote_url, pattern) - or string.match(resolved_remote_url, pattern) then - logger:debug( - "|_router| match-1 router:%s with pattern:%s", - vim.inspect(route), - vim.inspect(pattern) - ) + -- logger:debug( + -- "|_router| match-1 router:%s with pattern:%s", + -- vim.inspect(route), + -- vim.inspect(pattern) + -- ) return _worker(lk, pattern, route) end end @@ -308,24 +299,21 @@ local function _router(router_type, lk) and string.len(pattern) > 0 and (type(route) == "string" or type(route) == "function") then - local resolved_remote_url = _make_resolved_remote_url(lk) - logger:debug( - "|_router| table pattern:%s, match host:%s, remote_url:%s, resolved_remote_url:%s", - vim.inspect(pattern), - vim.inspect(lk.host), - vim.inspect(lk.remote_url), - vim.inspect(resolved_remote_url) - ) + -- logger:debug( + -- "|_router| table pattern:%s, match host:%s, remote_url:%s", + -- vim.inspect(pattern), + -- vim.inspect(lk.host), + -- vim.inspect(lk.remote_url) + -- ) if string.match(lk.host, pattern) or string.match(lk.remote_url, pattern) - or string.match(resolved_remote_url, pattern) then - logger:debug( - "|_router| match-2 router:%s with pattern:%s", - vim.inspect(route), - vim.inspect(pattern) - ) + -- logger:debug( + -- "|_router| match-2 router:%s with pattern:%s", + -- vim.inspect(route), + -- vim.inspect(pattern) + -- ) return _worker(lk, pattern, route) end end @@ -366,12 +354,12 @@ local link = function(opts) async.scheduler() local ok, url = pcall(opts.router, lk, true) - logger:debug( - "|link| ok:%s, url:%s, router:%s", - vim.inspect(ok), - vim.inspect(url), - vim.inspect(opts.router) - ) + -- logger:debug( + -- "|link| ok:%s, url:%s, router:%s", + -- vim.inspect(ok), + -- vim.inspect(url), + -- vim.inspect(opts.router) + -- ) assert( ok and type(url) == "string" and string.len(url) > 0, string.format( @@ -522,7 +510,7 @@ local function setup(opts) local logger = logging.get("gitlinker") --[[@as commons.logging.Logger]] - logger:debug("|setup| Configs:%s", vim.inspect(Configs)) + -- logger:debug("|setup| Configs:%s", vim.inspect(Configs)) -- command vim.api.nvim_create_user_command(Configs.command.name, function(command_opts) @@ -533,12 +521,12 @@ local function setup(opts) ) and vim.trim(command_opts.args) or nil - logger:debug( - "|setup| command opts:%s, parsed:%s, range:%s", - vim.inspect(command_opts), - vim.inspect(args), - vim.inspect(r) - ) + -- logger:debug( + -- "|setup| command opts:%s, parsed:%s, range:%s", + -- vim.inspect(command_opts), + -- vim.inspect(args), + -- vim.inspect(r) + -- ) local lstart = math.min(r.lstart, r.lend, command_opts.line1, command_opts.line2) local lend = @@ -583,7 +571,6 @@ end local M = { setup = setup, link = link, - _make_resolved_remote_url = _make_resolved_remote_url, _worker = _worker, _router = _router, _browse = _browse, diff --git a/lua/gitlinker/giturlparser.lua b/lua/gitlinker/giturlparser.lua new file mode 100644 index 0000000..639dd81 --- /dev/null +++ b/lua/gitlinker/giturlparser.lua @@ -0,0 +1,484 @@ +local M = {} + +-- utils { + +--- @param s string +--- @param t string +--- @param opts {ignorecase:boolean?}? +--- @return boolean +M._startswith = function(s, t, opts) + assert(type(s) == "string") + assert(type(t) == "string") + + opts = opts or { ignorecase = false } + opts.ignorecase = type(opts.ignorecase) == "boolean" and opts.ignorecase + or false + + if opts.ignorecase then + return string.len(s) >= string.len(t) and s:sub(1, #t):lower() == t:lower() + else + return string.len(s) >= string.len(t) and s:sub(1, #t) == t + end +end + +--- @param s string +--- @param t string +--- @param opts {ignorecase:boolean?}? +--- @return boolean +M._endswith = function(s, t, opts) + assert(type(s) == "string") + assert(type(t) == "string") + + opts = opts or { ignorecase = false } + opts.ignorecase = type(opts.ignorecase) == "boolean" and opts.ignorecase + or false + + if opts.ignorecase then + return string.len(s) >= string.len(t) + and s:sub(#s - #t + 1):lower() == t:lower() + else + return string.len(s) >= string.len(t) and s:sub(#s - #t + 1) == t + end +end + +--- @param s string +--- @param t string +--- @param start integer? by default start=1 +--- @return integer? +M._find = function(s, t, start) + assert(type(s) == "string") + assert(type(t) == "string") + + start = start or 1 + for i = start, #s do + local match = true + for j = 1, #t do + if i + j - 1 > #s then + match = false + break + end + local a = string.byte(s, i + j - 1) + local b = string.byte(t, j) + if a ~= b then + match = false + break + end + end + if match then + return i + end + end + return nil +end + +--- @param s string +--- @param t string +--- @param rstart integer? by default rstart=#s +--- @return integer? +M._rfind = function(s, t, rstart) + assert(type(s) == "string") + assert(type(t) == "string") + + rstart = rstart or #s + for i = rstart, 1, -1 do + local match = true + for j = 1, #t do + if i + j - 1 > #s then + match = false + break + end + local a = string.byte(s, i + j - 1) + local b = string.byte(t, j) + if a ~= b then + match = false + break + end + end + if match then + return i + end + end + return nil +end + +-- utils } + +-- 'path' is all payload after 'host', e.g. 'org/repo'. +-- +--- @alias giturlparser.GitUrlPos {start_pos:integer?,end_pos:integer?} +--- @alias giturlparser.GitUrlInfo {protocol:string?,protocol_pos:giturlparser.GitUrlPos?,user:string?,user_pos:giturlparser.GitUrlPos?,password:string?,password_pos:giturlparser.GitUrlPos?,host:string?,host_pos:giturlparser.GitUrlPos?,port:string?,port_pos:giturlparser.GitUrlPos?,org:string?,org_pos:giturlparser.GitUrlPos?,repo:string?,repo_pos:giturlparser.GitUrlPos?,path:string?,path_pos:giturlparser.GitUrlPos?} +-- +--- @param url string +--- @param start_pos integer +--- @param end_pos integer +--- @return string, giturlparser.GitUrlPos +M._make = function(url, start_pos, end_pos) + --- @type giturlparser.GitUrlPos + local pos = { + start_pos = start_pos, + end_pos = end_pos, + } + local component = string.sub(url, start_pos, end_pos) + return component, pos +end + +--- @param val string +--- @param pos giturlparser.GitUrlPos +--- @return string, giturlparser.GitUrlPos +M._trim_slash = function(val, pos) + assert(type(val) == "string") + if val and M._startswith(val, "/") then + val = string.sub(val, 2) + pos.start_pos = pos.start_pos + 1 + end + if val and M._endswith(val, "/") then + val = string.sub(val, 1, string.len(val) - 1) + pos.end_pos = pos.end_pos - 1 + end + + return val, pos +end + +--- @alias giturlparser._GitUrlPath {org:string?,org_pos:giturlparser.GitUrlPos?,repo:string?,repo_pos:giturlparser.GitUrlPos?,path:string?,path_pos:giturlparser.GitUrlPos?} +-- +--- @param p string +--- @param start integer +--- @return giturlparser._GitUrlPath +M._make_path = function(p, start) + assert(type(start) == "number") + + -- local inspect = require("inspect") + + local endswith_slash = M._endswith(p, "/") + + local org = nil + local org_pos = nil + local repo = nil + local repo_pos = nil + local path = nil + local path_pos = nil + local plen = string.len(p) + + local last_slash_pos = M._rfind(p, "/", endswith_slash and plen - 1 or plen) + if + type(last_slash_pos) == "number" + and last_slash_pos > start + and last_slash_pos < plen + then + org, org_pos = M._make(p, start, last_slash_pos - 1) + repo, repo_pos = M._make(p, last_slash_pos, plen) + else + -- no slash found, only 1 path component + repo, repo_pos = M._make(p, start, plen) + end + + -- print( + -- string.format( + -- "|_make_path| p:%s, start:%s, plen:%s\n", + -- inspect(p), + -- inspect(start), + -- inspect(plen) + -- ) + -- ) + path, path_pos = M._make(p, start, plen) + + if repo and repo_pos then + repo, repo_pos = M._trim_slash(repo, repo_pos) + end + if org and org_pos then + org, org_pos = M._trim_slash(org, org_pos) + end + + return { + org = org, + org_pos = org_pos, + repo = repo, + repo_pos = repo_pos, + path = path, + path_pos = path_pos, + } +end + +-- without omitted ssh protocol, host (and port end with ':') end with '/' +-- +--- @alias giturlparser._GitUrlHost {host:string?,host_pos:giturlparser.GitUrlPos?,port:string?,port_pos:giturlparser.GitUrlPos?,path_obj:giturlparser._GitUrlPath} +-- +--- @param p string +--- @param start integer +--- @return giturlparser._GitUrlHost +M._make_host = function(p, start) + assert(type(start) == "number") + assert(not M._startswith(p, "/")) + + local host = nil + local host_pos = nil + local port = nil + local port_pos = nil + --- @type giturlparser._GitUrlPath + local path_obj = {} + + local plen = string.len(p) + + -- find ':', the end position of host, start position of port + local first_colon_pos = M._find(p, ":", start) + if type(first_colon_pos) == "number" and first_colon_pos > start then + -- host end with ':', port start with ':' + host, host_pos = M._make(p, start, first_colon_pos - 1) + + -- find first slash '/' (after second ':'), the end position of port, start position of path + local first_slash_pos = M._find(p, "/", first_colon_pos + 1) + if + type(first_slash_pos) == "number" + and first_slash_pos > first_colon_pos + 1 + then + -- port end with '/' + port, port_pos = M._make(p, first_colon_pos + 1, first_slash_pos - 1) + path_obj = M._make_path(p, first_slash_pos) + else + -- path not found, port end until url end + port, port_pos = M._make(p, first_colon_pos + 1, plen) + end + else + -- port not found, host (highly possibly) end with '/' + + -- find first slash '/', the end position of host, start position of path + local first_slash_pos = M._find(p, "/", start) + if type(first_slash_pos) == "number" and first_slash_pos > start then + -- host end with '/' + host, host_pos = M._make(p, start, first_slash_pos - 1) + path_obj = M._make_path(p, first_slash_pos) + else + -- first slash not found, host is omitted, path end until url end + path_obj = M._make_path(p, start) + end + end + + return { + host = host, + host_pos = host_pos, + port = port, + port_pos = port_pos, + path_obj = path_obj, + } +end + +-- with omitted ssh protocol, host end with ':' +-- +--- @param p string +--- @param start integer +--- @return giturlparser._GitUrlHost +M._make_host_with_omit_ssh = function(p, start) + assert(type(start) == "number") + assert(not M._startswith(p, "/")) + + local host = nil + local host_pos = nil + local port = nil + local port_pos = nil + --- @type giturlparser._GitUrlPath + local path_obj = {} + + local plen = string.len(p) + + -- find ':', the end position of host, start position of path + local first_colon_pos = M._find(p, ":", start) + if type(first_colon_pos) == "number" and first_colon_pos > start then + -- host end with ':', path start with ':' + host, host_pos = M._make(p, start, first_colon_pos - 1) + path_obj = M._make_path(p, first_colon_pos + 1) + else + -- host not found, path start from beginning + path_obj = M._make_path(p, start) + end + + return { + host = host, + host_pos = host_pos, + port = port, + port_pos = port_pos, + path_obj = path_obj, + } +end + +--- @alias giturlparser._GitUrlUser {user:string?,user_pos:giturlparser.GitUrlPos?,password:string?,password_pos:giturlparser.GitUrlPos?,host_obj:giturlparser._GitUrlHost} +-- +--- @param p string +--- @param start integer +--- @param ssh_protocol_omitted boolean? +--- @return giturlparser._GitUrlUser +M._make_user = function(p, start, ssh_protocol_omitted) + assert(type(start) == "number") + assert(not M._startswith(p, "/")) + + -- local inspect = require("inspect") + + ssh_protocol_omitted = ssh_protocol_omitted or false + + local user = nil + local user_pos = nil + local password = nil + local password_pos = nil + --- @type giturlparser._GitUrlHost + local host_obj = {} + + local plen = string.len(p) + + local host_start_pos = start + + -- find first '@', the end position of user and password + local first_at_pos = M._find(p, "@", start) + -- print( + -- string.format( + -- "|_make_user-1| p:%s, start:%s, ssh_protocol_omitted:%s, first_at_pos:%s, host_start_pos:%s\n", + -- inspect(p), + -- inspect(start), + -- inspect(ssh_protocol_omitted), + -- inspect(first_at_pos), + -- inspect(host_start_pos) + -- ) + -- ) + if type(first_at_pos) == "number" and first_at_pos > start then + -- user (and password) end with '@' + + -- find first ':' (before '@'), the end position of password + local first_colon_pos = M._find(p, ":", start) + if + type(first_colon_pos) == "number" + and first_colon_pos > start + and first_colon_pos < first_at_pos + then + -- password end with ':' + user, user_pos = M._make(p, start, first_colon_pos - 1) + password, password_pos = M._make(p, first_colon_pos + 1, first_at_pos - 1) + else + -- password not found, user end with '@' + user, user_pos = M._make(p, start, first_at_pos - 1) + end + + -- host start from '@', user (and password) end position + host_start_pos = first_at_pos + 1 + else + -- user (and password) not found + -- host start from beginning + end + + -- print( + -- string.format( + -- "|_make_user-2| ssh_protocol_omitted:%s, host_start_pos:%s, first_at_pos:%s\n", + -- inspect(ssh_protocol_omitted), + -- inspect(host_start_pos), + -- inspect(first_at_pos) + -- ) + -- ) + host_obj = ssh_protocol_omitted + and M._make_host_with_omit_ssh(p, host_start_pos) + or M._make_host(p, host_start_pos) + + return { + user = user, + user_pos = user_pos, + password = password, + password_pos = password_pos, + host_obj = host_obj, + } +end + +--- @param url string +--- @return giturlparser.GitUrlInfo?, string? +M.parse = function(url) + if type(url) ~= "string" or string.len(url) == 0 then + return nil, "empty string" + end + + -- find first '://', the end position of protocol + local protocol_delimiter_pos = M._find(url, "://") + if + type(protocol_delimiter_pos) == "number" and protocol_delimiter_pos > 1 + then + -- protocol end with '://' + local protocol, protocol_pos = M._make(url, 1, protocol_delimiter_pos - 1) + + local user_obj = M._make_user(url, protocol_delimiter_pos + 3) + local host_obj = user_obj.host_obj + local path_obj = host_obj.path_obj + + return { + protocol = protocol, + protocol_pos = protocol_pos, + + -- user + user = user_obj.user, + user_pos = user_obj.user_pos, + password = user_obj.password, + password_pos = user_obj.password_pos, + + -- host + host = host_obj.host, + host_pos = host_obj.host_pos, + port = host_obj.port, + port_pos = host_obj.port_pos, + + -- path + org = path_obj.org, + org_pos = path_obj.org_pos, + repo = path_obj.repo, + repo_pos = path_obj.repo_pos, + path = path_obj.path, + path_pos = path_obj.path_pos, + } + else + -- protocol not found, either ssh/local file path + + -- find first ':', host end position on omitted ssh protocol + local first_colon_pos = M._find(url, ":") + if type(first_colon_pos) == "number" and first_colon_pos > 1 then + local user_obj = M._make_user(url, 1, true) + local host_obj = user_obj.host_obj + local path_obj = host_obj.path_obj + + return { + -- no protocol + + -- user + user = user_obj.user, + user_pos = user_obj.user_pos, + password = user_obj.password, + password_pos = user_obj.password_pos, + + -- host + host = host_obj.host, + host_pos = host_obj.host_pos, + port = host_obj.port, + port_pos = host_obj.port_pos, + + -- path + org = path_obj.org, + org_pos = path_obj.org_pos, + repo = path_obj.repo, + repo_pos = path_obj.repo_pos, + path = path_obj.path, + path_pos = path_obj.path_pos, + } + else + -- host not found + + -- treat as local file path, either absolute/relative + local path_obj = M._make_path(url, 1) + return { + -- no protocol + -- no user + -- no host + + -- path + org = path_obj.org, + org_pos = path_obj.org_pos, + repo = path_obj.repo, + repo_pos = path_obj.repo_pos, + path = path_obj.path, + path_pos = path_obj.path_pos, + } + end + end +end + +return M diff --git a/lua/gitlinker/linker.lua b/lua/gitlinker/linker.lua index 4122e70..e4b0c44 100644 --- a/lua/gitlinker/linker.lua +++ b/lua/gitlinker/linker.lua @@ -1,121 +1,10 @@ local logging = require("gitlinker.commons.logging") -local strings = require("gitlinker.commons.strings") local git = require("gitlinker.git") local path = require("gitlinker.path") +local giturlparser = require("gitlinker.giturlparser") local async = require("gitlinker.commons.async") --- example: --- git@github.com:linrongbin16/gitlinker.nvim.git --- https://github.com/linrongbin16/gitlinker.nvim.git --- ssh://git@git.xyz.abc/PROJECT_KEY/PROJECT.git --- https://git.samba.org/samba.git (main repo without user component) --- https://git.samba.org/ab/samba.git (dev repo with user component) --- ---- @param remote_url string ---- @return {protocol:string?,host:string?,host_delimiter:string?,user:string?,repo:string?} -local function _parse_remote_url(remote_url) - local logger = logging.get("gitlinker") --[[@as commons.logging.Logger]] - - local PROTOS = { "git@", "https://", "http://" } - local INT32_MAX = 2 ^ 31 - 1 - - local protocol = nil - local protocol_end_pos = nil - local host = nil - local host_end_pos = nil - local host_delimiter = nil - local user = nil - local repo = nil - - --- @type string - local proto = nil - --- @type integer? - local proto_pos = nil - for _, p in ipairs(PROTOS) do - proto_pos = strings.find(remote_url, p) - if type(proto_pos) == "number" and proto_pos > 0 then - proto = p - break - end - end - if not proto_pos then - error( - string.format( - "failed to parse remote url protocol:%s", - vim.inspect(remote_url) - ) - ) - end - - logger:debug( - "|_parse_remote_url| 1. remote_url:%s, proto_pos:%s (%s)", - vim.inspect(remote_url), - vim.inspect(proto_pos), - vim.inspect(proto) - ) - if type(proto_pos) == "number" and proto_pos > 0 then - protocol_end_pos = proto_pos + string.len(proto) - 1 - protocol = remote_url:sub(1, protocol_end_pos) - logger:debug( - "|_parse_remote_url| 2. remote_url:%s, proto_pos:%s (%s), protocol_end_pos:%s (%s)", - vim.inspect(remote_url), - vim.inspect(proto_pos), - vim.inspect(proto), - vim.inspect(protocol_end_pos), - vim.inspect(protocol) - ) - local first_slash_pos = strings.find(remote_url, "/", protocol_end_pos + 1) - or INT32_MAX - local first_colon_pos = strings.find(remote_url, ":", protocol_end_pos + 1) - or INT32_MAX - host_end_pos = math.min(first_slash_pos, first_colon_pos) - if not host_end_pos then - error( - string.format( - "failed to parse remote url host:%s", - vim.inspect(remote_url) - ) - ) - end - host_delimiter = remote_url:sub(host_end_pos, host_end_pos) - host = remote_url:sub(protocol_end_pos + 1, host_end_pos - 1) - logger:debug( - "|_parse_remote_url| last. remote_url:%s, proto_pos:%s (%s), protocol_end_pos:%s (%s), host_end_pos:%s (%s), host_delimiter:%s", - vim.inspect(remote_url), - vim.inspect(proto_pos), - vim.inspect(proto), - vim.inspect(protocol_end_pos), - vim.inspect(protocol), - vim.inspect(host_end_pos), - vim.inspect(host), - vim.inspect(host_delimiter) - ) - end - - local user_end_pos = strings.find(remote_url, "/", host_end_pos + 1) - if type(user_end_pos) == "number" and user_end_pos > host_end_pos + 1 then - user = remote_url:sub(host_end_pos + 1, user_end_pos - 1) - repo = remote_url:sub(user_end_pos + 1) - else - -- if no slash '/', then don't have 'user', but only 'repo' - -- example: - -- * main repo: https://git.samba.org/?p=samba.git - -- * user dev repo: https://git.samba.org/?p=bbaumbach/samba.git - repo = remote_url:sub(host_end_pos + 1) - user = "" - end - local result = { - protocol = protocol, - host = host, - host_delimiter = host_delimiter, - user = user, - repo = repo, - } - logger:debug("|_parse_remote_url| result:%s", vim.inspect(result)) - return result -end - ---- @alias gitlinker.Linker {remote_url:string,protocol:string,host:string,host_delimiter:string,user:string,repo:string?,rev:string,file:string,lstart:integer,lend:integer,file_changed:boolean,default_branch:string?,current_branch:string?} +--- @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?} --- @param remote string? --- @return gitlinker.Linker? local function make_linker(remote) @@ -138,8 +27,23 @@ local function make_linker(remote) return nil end - local parsed_remote_url = _parse_remote_url(remote_url) - local resolved_host = git.resolve_host(parsed_remote_url.host) + local parsed_url, parsed_err = giturlparser.parse(remote_url) + logger:debug( + "|make_linker| remote:%s, parsed_url:%s, parsed_err:%s", + vim.inspect(remote), + vim.inspect(parsed_url), + vim.inspect(parsed_err) + ) + assert( + parsed_url ~= nil, + string.format( + "failed to parse git remote url:%s, error:%s", + vim.inspect(remote_url), + vim.inspect(parsed_err) + ) + ) + + local resolved_host = git.resolve_host(parsed_url.host) if not resolved_host then return nil end @@ -185,11 +89,15 @@ local function make_linker(remote) local o = { remote_url = remote_url, - protocol = parsed_remote_url.protocol, + protocol = parsed_url.protocol, + username = parsed_url.user, + password = parsed_url.password, host = resolved_host, - host_delimiter = parsed_remote_url.host_delimiter, - user = parsed_remote_url.user, - repo = parsed_remote_url.repo, + port = parsed_url.port, + --- @deprecated please use 'org' + user = parsed_url.org, + org = parsed_url.org, + repo = parsed_url.repo, rev = rev, file = buf_path_on_root, ---@diagnostic disable-next-line: need-check-nil @@ -206,7 +114,6 @@ local function make_linker(remote) end local M = { - _parse_remote_url = _parse_remote_url, make_linker = make_linker, } diff --git a/lua/gitlinker/routers.lua b/lua/gitlinker/routers.lua index 599b37f..4509b35 100644 --- a/lua/gitlinker/routers.lua +++ b/lua/gitlinker/routers.lua @@ -4,7 +4,7 @@ local logging = require("gitlinker.commons.logging") --- @class gitlinker.Builder --- @field domain string? ---- @field user string? +--- @field org string? --- @field repo string? --- @field rev string? --- @field location string? @@ -78,12 +78,8 @@ end function Builder:new(lk, range_maker) local r = range_maker({ lstart = lk.lstart, lend = lk.lend }) local o = { - domain = string.format( - "%s%s", - strings.endswith(lk.protocol, "git@") and "https://" or lk.protocol, - lk.host - ), - user = lk.user, + domain = string.format("https://%s", lk.host), + org = lk.org, repo = strings.endswith(lk.repo, ".git") and lk.repo:sub(1, #lk.repo - 4) or lk.repo, rev = lk.rev, @@ -109,7 +105,7 @@ end function Builder:build(url) return table.concat({ self.domain, - self.user, + self.org, self.repo, url, self.rev, @@ -126,10 +122,10 @@ local function samba_browse(lk) local logger = logging.get("gitlinker") --[[@as commons.logging.Logger]] logger:debug("|samba_browse| lk:%s", vim.inspect(lk)) local builder = "https://git.samba.org/?p=" - -- user + -- org builder = builder - .. (string.len(lk.user) > 0 and string.format("%s/", lk.user) or "") - -- user + .. (string.len(lk.org) > 0 and string.format("%s/", lk.org) or "") + -- repo builder = builder .. string.format("%s;a=blob;", lk.repo) -- file: 'wscript' builder = builder .. string.format("f=%s;", lk.file) diff --git a/test/gitlinker_spec.lua b/test/gitlinker_spec.lua index d3594bd..fadfadf 100644 --- a/test/gitlinker_spec.lua +++ b/test/gitlinker_spec.lua @@ -34,7 +34,7 @@ describe("gitlinker", function() }, default_branch = { ["^github%.com"] = "https://github.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.DEFAULT_BRANCH}/" -- always 'master'/'main' branch .. "{_A.FILE}?plain=1" -- '?plain=1' @@ -43,7 +43,7 @@ describe("gitlinker", function() }, current_branch = { ["^github%.com"] = "https://github.com/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blob/" .. "{_A.CURRENT_BRANCH}/" -- always current branch .. "{_A.FILE}?plain=1" -- '?plain=1' @@ -60,10 +60,11 @@ describe("gitlinker", function() it("git.samba.org/samba.git with same lstart/lend", function() local lk = { remote_url = "git@git.samba.org:samba.git", - protocol = "git@", + protocol = nil, + username = "git", + password = nil, host = "git.samba.org", - host_delimiter = ":", - user = "", + org = "", repo = "samba.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "wscript", @@ -81,10 +82,11 @@ describe("gitlinker", function() it("git.samba.org/samba.git with different lstart/lend", function() local lk = { remote_url = "https://git.samba.org/samba.git", - protocol = "https://", + protocol = "https", + username = nil, + password = nil, host = "git.samba.org", - host_delimiter = "/", - user = "", + org = "", repo = "samba.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "wscript", @@ -102,10 +104,11 @@ describe("gitlinker", function() it("git.samba.org/bbaumbach/samba.git with same lstart/lend", function() local lk = { remote_url = "git@git.samba.org:bbaumbach/samba.git", - protocol = "git@", + protocol = nil, + username = "git", + password = nil, host = "git.samba.org", - host_delimiter = ":", - user = "bbaumbach", + org = "bbaumbach", repo = "samba.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "wscript", @@ -125,10 +128,11 @@ describe("gitlinker", function() function() local lk = { remote_url = "https://git.samba.org/bbaumbach/samba.git", - protocol = "https://", + protocol = "https", + username = nil, + password = nil, host = "git.samba.org", - host_delimiter = "/", - user = "bbaumbach", + org = "bbaumbach", repo = "samba.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "wscript", @@ -147,9 +151,11 @@ describe("gitlinker", function() it("github with same lstart/lend", function() local lk = { remote_url = "git@github.com:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + protocol = nil, + username = "git", + password = nil, host = "github.com", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -167,9 +173,9 @@ describe("gitlinker", function() it("github with different lstart/lend", function() local lk = { remote_url = "git@github.com:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "github.com", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -187,10 +193,11 @@ describe("gitlinker", function() it("ssh://git@git.xyz.com with same lstart/lend", function() local lk = { remote_url = "ssh://git@git.xyz.com/linrongbin16/gitlinker.nvim.git", - protocol = "ssh://git@", + protocol = "ssh", + username = "git", host = "git.xyz.com", - host_delimiter = "/", user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -208,9 +215,10 @@ describe("gitlinker", function() it("ssh://git@git.xyz.com with different lstart/lend", function() local lk = { remote_url = "ssh://git@github.com:linrongbin16/gitlinker.nvim.git", - protocol = "ssh://git@", + protocol = "ssh", + username = "git", host = "git.xyz.com", - host_delimiter = ":", + org = "linrongbin16", user = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", @@ -229,9 +237,9 @@ describe("gitlinker", function() it("gitlab with same line start and line end", function() local lk = { remote_url = "https://gitlab.com/linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "gitlab.com", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -249,9 +257,9 @@ describe("gitlinker", function() it("gitlab with different line start and line end", function() local lk = { remote_url = "git@gitlab.com:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "gitlab.com", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -269,9 +277,9 @@ describe("gitlinker", function() it("bitbucket with same line start and line end", function() local lk = { remote_url = "git@bitbucket.org:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "bitbucket.org", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -289,9 +297,9 @@ describe("gitlinker", function() it("bitbucket with different line start and line end", function() local lk = { remote_url = "https://bitbucket.org/linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "bitbucket.org", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -309,9 +317,9 @@ describe("gitlinker", function() it("codeberg with same line start and line end", function() local lk = { remote_url = "git@codeberg.org:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "codeberg.org", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -329,9 +337,9 @@ describe("gitlinker", function() it("codeberg with different line start and line end", function() local lk = { remote_url = "https://codeberg.org/linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "codeberg.org", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -351,9 +359,9 @@ describe("gitlinker", function() it("github with same lstart/lend", function() local lk = { remote_url = "git@github.com:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "github.com", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -371,9 +379,9 @@ describe("gitlinker", function() it("github with different lstart/lend", function() local lk = { remote_url = "https://github.com:linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "github.com", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -391,9 +399,9 @@ describe("gitlinker", function() it("gitlab with same lstart/lend", function() local lk = { remote_url = "git@gitlab.com:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "gitlab.com", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -411,9 +419,9 @@ describe("gitlinker", function() it("gitlab with different lstart/lend", function() local lk = { remote_url = "https://gitlab.com:linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "gitlab.com", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -431,9 +439,9 @@ describe("gitlinker", function() it("bitbucket with same lstart/lend", function() local lk = { remote_url = "git@bitbucket.org:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "bitbucket.org", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -451,9 +459,9 @@ describe("gitlinker", function() it("bitbucket with different lstart/lend", function() local lk = { remote_url = "https://bitbucket.org:linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "bitbucket.org", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -471,9 +479,9 @@ describe("gitlinker", function() it("codeberg with same lstart/lend", function() local lk = { remote_url = "git@codeberg.org:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "codeberg.org", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -491,10 +499,9 @@ describe("gitlinker", function() it("codeberg with different lstart/lend", function() local lk = { remote_url = "https://codeberg.org:linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "codeberg.org", - host_delimiter = ":", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -510,56 +517,13 @@ describe("gitlinker", function() assert_eq(actual, routers.codeberg_blame(lk)) end) end) - describe("[_make_resolved_remote_url]", function() - it("resolve /", function() - local lk = { - remote_url = "https://codeberg.org/linrongbin16/gitlinker.nvim.git", - protocol = "https://", - host = "my-personal-codeberg.org", - host_delimiter = "/", - user = "linrongbin16", - repo = "gitlinker.nvim.git", - rev = "399b1d05473c711fc5592a6ffc724e231c403486", - file = "lua/gitlinker/logger.lua", - lstart = 13, - lend = 21, - file_changed = false, - }--[[@as gitlinker.Linker]] - local actual = gitlinker._make_resolved_remote_url(lk) - assert_eq( - actual, - "https://my-personal-codeberg.org/linrongbin16/gitlinker.nvim.git" - ) - end) - it("resolve :", function() - local lk = { - remote_url = "git@codeberg.org:linrongbin16/gitlinker.nvim.git", - protocol = "git@", - host = "my-personal-codeberg.org", - host_delimiter = ":", - user = "linrongbin16", - repo = "gitlinker.nvim.git", - rev = "399b1d05473c711fc5592a6ffc724e231c403486", - file = "lua/gitlinker/logger.lua", - lstart = 13, - lend = 21, - file_changed = false, - }--[[@as gitlinker.Linker]] - local actual = gitlinker._make_resolved_remote_url(lk) - assert_eq( - actual, - "git@my-personal-codeberg.org:linrongbin16/gitlinker.nvim.git" - ) - end) - end) describe("[_worker]", function() it("is function", function() local lk = { remote_url = "git@codeberg.org:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "my-personal-codeberg.org", - host_delimiter = ":", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -576,10 +540,9 @@ describe("gitlinker", function() it("is string", function() local lk = { remote_url = "git@codeberg.org:linrongbin16/gitlinker.nvim.git", - protocol = "git@", + username = "git", host = "my-personal-codeberg.org", - host_delimiter = ":", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -588,7 +551,7 @@ describe("gitlinker", function() file_changed = false, }--[[@as gitlinker.Linker]] local string_template = "https://codeberg.org/" - .. "{_A.USER}/" + .. "{_A.ORG}/" .. "{_A.REPO}/blame/commit/" .. "{_A.REV}/" .. "{_A.FILE}" @@ -766,10 +729,9 @@ describe("gitlinker", function() it("default_branch", function() local lk = { remote_url = "https://github.com/linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "github.com", - host_delimiter = "/", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", @@ -788,10 +750,9 @@ describe("gitlinker", function() it("current_branch", function() local lk = { remote_url = "https://github.com/linrongbin16/gitlinker.nvim.git", - protocol = "https://", + protocol = "https", host = "github.com", - host_delimiter = "/", - user = "linrongbin16", + org = "linrongbin16", repo = "gitlinker.nvim.git", rev = "399b1d05473c711fc5592a6ffc724e231c403486", file = "lua/gitlinker/logger.lua", diff --git a/test/linker_spec.lua b/test/linker_spec.lua index 7878bdf..df0d358 100644 --- a/test/linker_spec.lua +++ b/test/linker_spec.lua @@ -16,60 +16,6 @@ describe("linker", function() local async = require("gitlinker.commons.async") local github_actions = os.getenv("GITHUB_ACTIONS") == "true" local linker = require("gitlinker.linker") - describe("[_parse_remote_url]", function() - it("parse git", function() - local parsed = linker._parse_remote_url( - "git@github.com:linrongbin16/gitlinker.nvim.git" - ) - assert_eq(type(parsed), "table") - assert_eq(parsed.protocol, "git@") - assert_eq(parsed.host, "github.com") - assert_eq(parsed.user, "linrongbin16") - assert_eq(parsed.repo, "gitlinker.nvim.git") - end) - it("parse ssh://git@", function() - local parsed = linker._parse_remote_url( - "ssh://git@github.com:linrongbin16/gitlinker.nvim.git" - ) - assert_eq(type(parsed), "table") - assert_eq(parsed.protocol, "ssh://git@") - assert_eq(parsed.host, "github.com") - assert_eq(parsed.host_delimiter, ":") - assert_eq(parsed.user, "linrongbin16") - assert_eq(parsed.repo, "gitlinker.nvim.git") - end) - it("parse ssh://git@ - v2", function() - local parsed = linker._parse_remote_url( - "ssh://git@github.com/linrongbin16/gitlinker.nvim.git" - ) - assert_eq(type(parsed), "table") - assert_eq(parsed.protocol, "ssh://git@") - assert_eq(parsed.host, "github.com") - assert_eq(parsed.host_delimiter, "/") - assert_eq(parsed.user, "linrongbin16") - assert_eq(parsed.repo, "gitlinker.nvim.git") - end) - it("parse http", function() - local parsed = linker._parse_remote_url( - "http://github.com/linrongbin16/gitlinker.nvim.git" - ) - assert_eq(type(parsed), "table") - assert_eq(parsed.protocol, "http://") - assert_eq(parsed.host, "github.com") - assert_eq(parsed.user, "linrongbin16") - assert_eq(parsed.repo, "gitlinker.nvim.git") - end) - it("parse https", function() - local parsed = linker._parse_remote_url( - "https://github.com/linrongbin16/gitlinker.nvim.git" - ) - assert_eq(type(parsed), "table") - assert_eq(parsed.protocol, "https://") - assert_eq(parsed.host, "github.com") - assert_eq(parsed.user, "linrongbin16") - assert_eq(parsed.repo, "gitlinker.nvim.git") - end) - end) describe("[make_linker]", function() it("make", function() async.run(function()