Feat: Sort Discussions by File Name (#102)
This MR adds the ability to sort discussions by file name, rather than just by date. This is an optional configuration that can be passed in on startup. The MR also introduces a test suite for the Lua code that runs through Neovim, so that the plugin can be fully tested with required dependencies and APIs. Major props to @johnybx for the hard work on this change!
This commit is contained in:
4
.busted
4
.busted
@@ -2,10 +2,10 @@ return {
|
|||||||
_all = {
|
_all = {
|
||||||
pattern = "_spec",
|
pattern = "_spec",
|
||||||
lpath = "lua/?.lua;lua/?/init.lua",
|
lpath = "lua/?.lua;lua/?/init.lua",
|
||||||
ROOT = {"lua/gitlab"},
|
ROOT = { "tests/spec" },
|
||||||
},
|
},
|
||||||
default = {
|
default = {
|
||||||
verbose = true
|
verbose = true,
|
||||||
},
|
},
|
||||||
tests = {
|
tests = {
|
||||||
verbose = true,
|
verbose = true,
|
||||||
|
|||||||
15
.github/workflows/lua-tests.yaml
vendored
15
.github/workflows/lua-tests.yaml
vendored
@@ -1,15 +0,0 @@
|
|||||||
name: Lua Tests
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Run Busted
|
|
||||||
uses: lunarmodules/busted@v2.2.0
|
|
||||||
# with:
|
|
||||||
# args: --run=api
|
|
||||||
32
.github/workflows/lua.yaml
vendored
32
.github/workflows/lua.yaml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
lua_lint:
|
lua_lint:
|
||||||
name: Lint Lua 💅
|
name: Lint Lua 💅
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
args: --globals vim --no-max-line-length -- .
|
args: --globals vim --no-max-line-length -- .
|
||||||
lua_format:
|
lua_format:
|
||||||
name: Formatting Lua 💅
|
name: Formatting Lua 💅
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -27,11 +27,29 @@ jobs:
|
|||||||
version: latest
|
version: latest
|
||||||
args: --check .
|
args: --check .
|
||||||
lua_test:
|
lua_test:
|
||||||
name: Test Lua 🧪
|
name: Run tests 🧪
|
||||||
needs: [lua_format,lua_lint]
|
strategy:
|
||||||
|
matrix:
|
||||||
|
nvim_version: [stable, nightly]
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Run Busted
|
- name: Install neovim
|
||||||
uses: lunarmodules/busted@v2.2.0
|
uses: rhysd/action-setup-vim@v1
|
||||||
|
id: vim
|
||||||
|
with:
|
||||||
|
neovim: true
|
||||||
|
version: ${{ matrix.nvim_version }}
|
||||||
|
- name: Install luajit
|
||||||
|
uses: leafo/gh-actions-lua@v10
|
||||||
|
with:
|
||||||
|
luaVersion: "luajit-2.1.0-beta3"
|
||||||
|
- name: Install luarocks
|
||||||
|
uses: leafo/gh-actions-luarocks@v4
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
chmod +x lua-test.sh
|
||||||
|
./lua-test.sh
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,3 +3,7 @@ bin
|
|||||||
/luarocks
|
/luarocks
|
||||||
/lua_modules
|
/lua_modules
|
||||||
/.luarocks
|
/.luarocks
|
||||||
|
*.rockspec
|
||||||
|
tests/plugins
|
||||||
|
!tests/plugins/.placeholder
|
||||||
|
luacov.*
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -57,6 +57,7 @@ return {
|
|||||||
"nvim-lua/plenary.nvim",
|
"nvim-lua/plenary.nvim",
|
||||||
"sindrets/diffview.nvim",
|
"sindrets/diffview.nvim",
|
||||||
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
||||||
|
"nvim-tree/nvim-web-devicons" -- Recommended but not required. Icons in discussion tree.
|
||||||
enabled = true,
|
enabled = true,
|
||||||
},
|
},
|
||||||
build = function () require("gitlab.server").build(true) end, -- Builds the Go binary
|
build = function () require("gitlab.server").build(true) end, -- Builds the Go binary
|
||||||
@@ -104,7 +105,6 @@ gitlab_url=https://my-personal-gitlab-instance.com/
|
|||||||
|
|
||||||
The plugin will look for the `.gitlab.nvim` file in the root of the current project by default. However, you may provide a custom path to the configuration file via the `config_path` option. This must be an absolute path to the directory that holds your `.gitlab.nvim` file.
|
The plugin will look for the `.gitlab.nvim` file in the root of the current project by default. However, you may provide a custom path to the configuration file via the `config_path` option. This must be an absolute path to the directory that holds your `.gitlab.nvim` file.
|
||||||
|
|
||||||
|
|
||||||
## Configuring the Plugin
|
## Configuring the Plugin
|
||||||
|
|
||||||
Here is the default setup function. All of these values are optional, and if you call this function with no values the defaults will be used:
|
Here is the default setup function. All of these values are optional, and if you call this function with no values the defaults will be used:
|
||||||
@@ -135,6 +135,7 @@ require("gitlab").setup({
|
|||||||
relative = "editor", -- Position of tree split relative to "editor" or "window"
|
relative = "editor", -- Position of tree split relative to "editor" or "window"
|
||||||
resolved = '✓', -- Symbol to show next to resolved discussions
|
resolved = '✓', -- Symbol to show next to resolved discussions
|
||||||
unresolved = '✖', -- Symbol to show next to unresolved discussions
|
unresolved = '✖', -- Symbol to show next to unresolved discussions
|
||||||
|
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
|
||||||
},
|
},
|
||||||
info = { -- Show additional fields in the summary pane
|
info = { -- Show additional fields in the summary pane
|
||||||
enabled = true,
|
enabled = true,
|
||||||
@@ -195,10 +196,13 @@ require("gitlab").setup({
|
|||||||
},
|
},
|
||||||
colors = {
|
colors = {
|
||||||
discussion_tree = {
|
discussion_tree = {
|
||||||
username = 'Keyword', -- The highlight group used, for instance 'DiagnosticSignWarn'
|
username = "Keyword",
|
||||||
date = 'Comment',
|
date = "Comment",
|
||||||
chevron = 'Comment',
|
chevron = "DiffviewNonText",
|
||||||
}
|
directory = "Directory",
|
||||||
|
directory_icon = "DiffviewFolderSign",
|
||||||
|
file_name = "Normal",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
13
lua-coverage.sh
Executable file
13
lua-coverage.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Process generated luacov stats file into coverage report for gitlab.nvim.
|
||||||
|
#
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if ! [[ -f luacov.stats.out ]]; then
|
||||||
|
echo "You need to first run \`./lua-test.sh --coverage\`"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
eval "$(luarocks path)"
|
||||||
|
luacov "$@"
|
||||||
59
lua-test.sh
Executable file
59
lua-test.sh
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Setup and run tests for lua part of gitlab.nvim.
|
||||||
|
#
|
||||||
|
# In order to run tests you need to have `luarocks` and `git` installed. This script will check if
|
||||||
|
# environment is already setup, if not it will initialize current directory with `luarocks`,
|
||||||
|
# install `busted` framework and download plugin dependencies.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LUA_VERSION="5.1"
|
||||||
|
PLUGINS_FOLDER="tests/plugins"
|
||||||
|
PLUGINS=(
|
||||||
|
"https://github.com/MunifTanjim/nui.nvim"
|
||||||
|
"https://github.com/nvim-lua/plenary.nvim"
|
||||||
|
"https://github.com/sindrets/diffview.nvim"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ! command -v luarocks > /dev/null 2>&1; then
|
||||||
|
echo "You need to have luarocks installed in order to run tests."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v git > /dev/null 2>&1; then
|
||||||
|
echo "You need to have git installed in order to run tests."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! luarocks --lua-version=$LUA_VERSION which busted > /dev/null 2>&1; then
|
||||||
|
echo "Installing busted."
|
||||||
|
luarocks init
|
||||||
|
luarocks config --scope project lua_version "$LUA_VERSION"
|
||||||
|
luarocks install --lua-version="$LUA_VERSION" busted
|
||||||
|
fi
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ $arg =~ "--coverage" ]] && ! luarocks --lua-version=$LUA_VERSION which luacov > /dev/null 2>&1; then
|
||||||
|
luarocks install --lua-version="$LUA_VERSION" luacov
|
||||||
|
# lcov reporter for luacov - lcov format is supported by `nvim-coverage`
|
||||||
|
luarocks install --lua-version="$LUA_VERSION" luacov-reporter-lcov
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for plugin in "${PLUGINS[@]}"; do
|
||||||
|
plugin_name=${plugin##*/}
|
||||||
|
plugin_folder="$PLUGINS_FOLDER/$plugin_name"
|
||||||
|
|
||||||
|
# Check if plugin was already downloaded
|
||||||
|
if [[ -d "$plugin_folder/.git" ]]; then
|
||||||
|
# We could also try to pull here but I am not sure if that wouldn't slow down tests too much.
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone --depth 1 "$plugin" "$plugin_folder"
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
nvim -u NONE -U NONE -N -i NONE -l tests/init.lua "$@"
|
||||||
66
lua/gitlab/actions/discussions/annotations.lua
Normal file
66
lua/gitlab/actions/discussions/annotations.lua
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
---@meta diagnostics
|
||||||
|
|
||||||
|
---@class Author
|
||||||
|
---@field id integer
|
||||||
|
---@field username string
|
||||||
|
---@field email string
|
||||||
|
---@field name string
|
||||||
|
---@field state string
|
||||||
|
---@field avatar_url string
|
||||||
|
---@field web_url string
|
||||||
|
|
||||||
|
---@class LinePosition
|
||||||
|
---@field line_code string
|
||||||
|
---@field type string
|
||||||
|
|
||||||
|
---@class GitlabLineRange
|
||||||
|
---@field start LinePosition
|
||||||
|
---@field end LinePosition
|
||||||
|
|
||||||
|
---@class NotePosition
|
||||||
|
---@field base_sha string
|
||||||
|
---@field start_sha string
|
||||||
|
---@field head_sha string
|
||||||
|
---@field position_type string
|
||||||
|
---@field new_path string?
|
||||||
|
---@field new_line integer?
|
||||||
|
---@field old_path string?
|
||||||
|
---@field old_line integer?
|
||||||
|
---@field line_range GitlabLineRange?
|
||||||
|
|
||||||
|
---@class Note
|
||||||
|
---@field id integer
|
||||||
|
---@field type string
|
||||||
|
---@field body string
|
||||||
|
---@field attachment string
|
||||||
|
---@field title string
|
||||||
|
---@field file_name string
|
||||||
|
---@field author Author
|
||||||
|
---@field system boolean
|
||||||
|
---@field expires_at string?
|
||||||
|
---@field updated_at string?
|
||||||
|
---@field created_at string?
|
||||||
|
---@field noteable_id integer
|
||||||
|
---@field noteable_type string
|
||||||
|
---@field commit_id string
|
||||||
|
---@field position NotePosition
|
||||||
|
---@field resolvable boolean
|
||||||
|
---@field resolved boolean
|
||||||
|
---@field resolved_by Author
|
||||||
|
---@field resolved_at string?
|
||||||
|
---@field noteable_iid integer
|
||||||
|
|
||||||
|
---@class UnlinkedNote: Note
|
||||||
|
---@field position nil
|
||||||
|
|
||||||
|
---@class Discussion
|
||||||
|
---@field id string
|
||||||
|
---@field individual_note boolean
|
||||||
|
---@field notes Note[]
|
||||||
|
|
||||||
|
---@class UnlinkedDiscussion: Discussion
|
||||||
|
---@field notes UnlinkedNote[]
|
||||||
|
|
||||||
|
---@class DiscussionData
|
||||||
|
---@field discussions Discussion[]
|
||||||
|
---@field unlinked_discussions UnlinkedDiscussion[]
|
||||||
@@ -4,12 +4,14 @@
|
|||||||
local Split = require("nui.split")
|
local Split = require("nui.split")
|
||||||
local Popup = require("nui.popup")
|
local Popup = require("nui.popup")
|
||||||
local NuiTree = require("nui.tree")
|
local NuiTree = require("nui.tree")
|
||||||
|
local NuiLine = require("nui.line")
|
||||||
local Layout = require("nui.layout")
|
local Layout = require("nui.layout")
|
||||||
local job = require("gitlab.job")
|
local job = require("gitlab.job")
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local reviewer = require("gitlab.reviewer")
|
local reviewer = require("gitlab.reviewer")
|
||||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||||
|
local discussions_tree = require("gitlab.actions.discussions.tree")
|
||||||
|
|
||||||
local edit_popup = Popup(u.create_popup_state("Edit Comment", "80%", "80%"))
|
local edit_popup = Popup(u.create_popup_state("Edit Comment", "80%", "80%"))
|
||||||
local reply_popup = Popup(u.create_popup_state("Reply", "80%", "80%"))
|
local reply_popup = Popup(u.create_popup_state("Reply", "80%", "80%"))
|
||||||
@@ -32,79 +34,16 @@ local M = {
|
|||||||
discussion_tree = nil,
|
discussion_tree = nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
---@class Author
|
|
||||||
---@field id integer
|
|
||||||
---@field username string
|
|
||||||
---@field email string
|
|
||||||
---@field name string
|
|
||||||
---@field state string
|
|
||||||
---@field avatar_url string
|
|
||||||
---@field web_url string
|
|
||||||
|
|
||||||
---@class LinePosition
|
|
||||||
---@field line_code string
|
|
||||||
---@field type string
|
|
||||||
|
|
||||||
---@class GitlabLineRange
|
|
||||||
---@field start LinePosition
|
|
||||||
---@field end LinePosition
|
|
||||||
|
|
||||||
---@class NotePosition
|
|
||||||
---@field base_sha string
|
|
||||||
---@field start_sha string
|
|
||||||
---@field head_sha string
|
|
||||||
---@field position_type string
|
|
||||||
---@field new_path string?
|
|
||||||
---@field new_line integer?
|
|
||||||
---@field old_path string?
|
|
||||||
---@field old_line integer?
|
|
||||||
---@field line_range GitlabLineRange?
|
|
||||||
|
|
||||||
---@class Note
|
|
||||||
---@field id integer
|
|
||||||
---@field type string
|
|
||||||
---@field body string
|
|
||||||
---@field attachment string
|
|
||||||
---@field title string
|
|
||||||
---@field file_name string
|
|
||||||
---@field author Author
|
|
||||||
---@field system boolean
|
|
||||||
---@field expires_at string?
|
|
||||||
---@field updated_at string?
|
|
||||||
---@field created_at string?
|
|
||||||
---@field noteable_id integer
|
|
||||||
---@field noteable_type string
|
|
||||||
---@field commit_id string
|
|
||||||
---@field position NotePosition
|
|
||||||
---@field resolvable boolean
|
|
||||||
---@field resolved boolean
|
|
||||||
---@field resolved_by Author
|
|
||||||
---@field resolved_at string?
|
|
||||||
---@field noteable_iid integer
|
|
||||||
|
|
||||||
---@class UnlinkedNote: Note
|
|
||||||
---@field position nil
|
|
||||||
|
|
||||||
---@class Discussion
|
|
||||||
---@field id string
|
|
||||||
---@field individual_note boolean
|
|
||||||
---@field notes Note[]
|
|
||||||
|
|
||||||
---@class UnlinkedDiscussion: Discussion
|
|
||||||
---@field notes UnlinkedNote[]
|
|
||||||
|
|
||||||
---@class DiscussionData
|
|
||||||
---@field discussions Discussion[]
|
|
||||||
---@field unlinked_discussions UnlinkedDiscussion[]
|
|
||||||
|
|
||||||
---Load the discussion data, storage them in M.discussions and M.unlinked_discussions and call
|
---Load the discussion data, storage them in M.discussions and M.unlinked_discussions and call
|
||||||
---callback with data
|
---callback with data
|
||||||
---@param callback fun(data: DiscussionData): nil
|
---@param callback (fun(data: DiscussionData): nil)?
|
||||||
M.load_discussions = function(callback)
|
M.load_discussions = function(callback)
|
||||||
job.run_job("/discussions/list", "POST", { blacklist = state.settings.discussion_tree.blacklist }, function(data)
|
job.run_job("/discussions/list", "POST", { blacklist = state.settings.discussion_tree.blacklist }, function(data)
|
||||||
M.discussions = data.discussions
|
M.discussions = data.discussions
|
||||||
M.unlinked_discussions = data.unlinked_discussions
|
M.unlinked_discussions = data.unlinked_discussions
|
||||||
callback(data)
|
if type(callback) == "function" then
|
||||||
|
callback(data)
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -145,7 +84,8 @@ M.filter_discussions_for_signs_and_diagnostics = function()
|
|||||||
--Skip discussions from old revisions
|
--Skip discussions from old revisions
|
||||||
and not (
|
and not (
|
||||||
state.settings.discussion_sign_and_diagnostic.skip_old_revision_discussion
|
state.settings.discussion_sign_and_diagnostic.skip_old_revision_discussion
|
||||||
and first_note.position.base_sha ~= state.MR_REVISIONS[1].base_sha
|
and u.from_iso_format_date_to_timestamp(first_note.created_at)
|
||||||
|
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
|
||||||
)
|
)
|
||||||
then
|
then
|
||||||
table.insert(discussions, discussion)
|
table.insert(discussions, discussion)
|
||||||
@@ -253,6 +193,13 @@ M.refresh_signs = function()
|
|||||||
reviewer.place_sign(new_signs, "new")
|
reviewer.place_sign(new_signs, "new")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Build note header from note.
|
||||||
|
---@param note Note
|
||||||
|
---@return string
|
||||||
|
M.build_note_header = function(note)
|
||||||
|
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
||||||
|
end
|
||||||
|
|
||||||
---Refresh the diagnostics for the currently reviewed file
|
---Refresh the diagnostics for the currently reviewed file
|
||||||
M.refresh_diagnostics = function()
|
M.refresh_diagnostics = function()
|
||||||
-- Keep in mind that diagnostic line numbers use 0-based indexing while line numbers use
|
-- Keep in mind that diagnostic line numbers use 0-based indexing while line numbers use
|
||||||
@@ -537,6 +484,7 @@ M.send_reply = function(tree, discussion_id)
|
|||||||
job.run_job("/reply", "POST", body, function(data)
|
job.run_job("/reply", "POST", body, function(data)
|
||||||
u.notify("Sent reply!", vim.log.levels.INFO)
|
u.notify("Sent reply!", vim.log.levels.INFO)
|
||||||
M.add_reply_to_tree(tree, data.note, discussion_id)
|
M.add_reply_to_tree(tree, data.note, discussion_id)
|
||||||
|
M.load_discussions()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -561,7 +509,7 @@ M.send_deletion = function(tree, unlinked)
|
|||||||
local root_node = M.get_root_node(tree, current_node)
|
local root_node = M.get_root_node(tree, current_node)
|
||||||
local note_id = note_node.is_root and root_node.root_note_id or note_node.id
|
local note_id = note_node.is_root and root_node.root_note_id or note_node.id
|
||||||
|
|
||||||
local body = { discussion_id = root_node.id, note_id = note_id }
|
local body = { discussion_id = root_node.id, note_id = tonumber(note_id) }
|
||||||
|
|
||||||
job.run_job("/comment", "DELETE", body, function(data)
|
job.run_job("/comment", "DELETE", body, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
@@ -608,11 +556,14 @@ M.edit_comment = function(tree, unlinked)
|
|||||||
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
|
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
|
||||||
state.set_popup_keymaps(
|
state.set_popup_keymaps(
|
||||||
edit_popup,
|
edit_popup,
|
||||||
M.send_edits(tostring(root_node.id), note_node.root_note_id or note_node.id, unlinked)
|
M.send_edits(tostring(root_node.id), tonumber(note_node.root_note_id or note_node.id), unlinked)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- This function sends the edited comment to the Go server
|
---This function sends the edited comment to the Go server
|
||||||
|
---@param discussion_id string
|
||||||
|
---@param note_id integer
|
||||||
|
---@param unlinked boolean
|
||||||
M.send_edits = function(discussion_id, note_id, unlinked)
|
M.send_edits = function(discussion_id, note_id, unlinked)
|
||||||
return function(text)
|
return function(text)
|
||||||
local body = {
|
local body = {
|
||||||
@@ -623,10 +574,10 @@ M.send_edits = function(discussion_id, note_id, unlinked)
|
|||||||
job.run_job("/comment", "PATCH", body, function(data)
|
job.run_job("/comment", "PATCH", body, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
if unlinked then
|
if unlinked then
|
||||||
M.unlinked_discussions = M.replace_text(M.unlinked_discussions, discussion_id, note_id, text)
|
M.replace_text(M.unlinked_discussions, discussion_id, note_id, text)
|
||||||
M.rebuild_unlinked_discussion_tree()
|
M.rebuild_unlinked_discussion_tree()
|
||||||
else
|
else
|
||||||
M.discussions = M.replace_text(M.discussions, discussion_id, note_id, text)
|
M.replace_text(M.discussions, discussion_id, note_id, text)
|
||||||
M.rebuild_discussion_tree()
|
M.rebuild_discussion_tree()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -684,12 +635,16 @@ M.toggle_node = function(tree)
|
|||||||
end
|
end
|
||||||
if node:is_expanded() then
|
if node:is_expanded() then
|
||||||
node:collapse()
|
node:collapse()
|
||||||
for _, child in ipairs(children) do
|
if M.is_node_note(node) then
|
||||||
tree:get_node(child):collapse()
|
for _, child in ipairs(children) do
|
||||||
|
tree:get_node(child):collapse()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
for _, child in ipairs(children) do
|
if M.is_node_note(node) then
|
||||||
tree:get_node(child):expand()
|
for _, child in ipairs(children) do
|
||||||
|
tree:get_node(child):expand()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
node:expand()
|
node:expand()
|
||||||
end
|
end
|
||||||
@@ -700,12 +655,48 @@ end
|
|||||||
--
|
--
|
||||||
-- 🌲 Helper Functions
|
-- 🌲 Helper Functions
|
||||||
--
|
--
|
||||||
|
---Inspired by default func https://github.com/MunifTanjim/nui.nvim/blob/main/lua/nui/tree/util.lua#L38
|
||||||
|
local function nui_tree_prepare_node(node)
|
||||||
|
if not node.text then
|
||||||
|
error("missing node.text")
|
||||||
|
end
|
||||||
|
|
||||||
|
local texts = node.text
|
||||||
|
|
||||||
|
if type(node.text) ~= "table" or node.text.content then
|
||||||
|
texts = { node.text }
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
|
||||||
|
for i, text in ipairs(texts) do
|
||||||
|
local line = NuiLine()
|
||||||
|
|
||||||
|
line:append(string.rep(" ", node._depth - 1))
|
||||||
|
|
||||||
|
if i == 1 and node:has_children() then
|
||||||
|
line:append(node:is_expanded() and " " or " ")
|
||||||
|
if node.icon then
|
||||||
|
line:append(node.icon .. " ", node.icon_hl)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
line:append(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
line:append(text, node.text_hl)
|
||||||
|
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
M.rebuild_discussion_tree = function()
|
M.rebuild_discussion_tree = function()
|
||||||
M.switch_can_edit_bufs(true)
|
M.switch_can_edit_bufs(true)
|
||||||
vim.api.nvim_buf_set_lines(M.linked_section.bufnr, 0, -1, false, {})
|
vim.api.nvim_buf_set_lines(M.linked_section.bufnr, 0, -1, false, {})
|
||||||
local discussion_tree_nodes = M.add_discussions_to_table(M.discussions)
|
local discussion_tree_nodes = discussions_tree.add_discussions_to_table(M.discussions, false)
|
||||||
local discussion_tree = NuiTree({ nodes = discussion_tree_nodes, bufnr = M.linked_section.bufnr })
|
local discussion_tree =
|
||||||
|
NuiTree({ nodes = discussion_tree_nodes, bufnr = M.linked_section.bufnr, prepare_node = nui_tree_prepare_node })
|
||||||
discussion_tree:render()
|
discussion_tree:render()
|
||||||
M.set_tree_keymaps(discussion_tree, M.linked_section.bufnr, false)
|
M.set_tree_keymaps(discussion_tree, M.linked_section.bufnr, false)
|
||||||
M.discussion_tree = discussion_tree
|
M.discussion_tree = discussion_tree
|
||||||
@@ -716,8 +707,12 @@ end
|
|||||||
M.rebuild_unlinked_discussion_tree = function()
|
M.rebuild_unlinked_discussion_tree = function()
|
||||||
M.switch_can_edit_bufs(true)
|
M.switch_can_edit_bufs(true)
|
||||||
vim.api.nvim_buf_set_lines(M.unlinked_section.bufnr, 0, -1, false, {})
|
vim.api.nvim_buf_set_lines(M.unlinked_section.bufnr, 0, -1, false, {})
|
||||||
local unlinked_discussion_tree_nodes = M.add_discussions_to_table(M.unlinked_discussions)
|
local unlinked_discussion_tree_nodes = discussions_tree.add_discussions_to_table(M.unlinked_discussions, true)
|
||||||
local unlinked_discussion_tree = NuiTree({ nodes = unlinked_discussion_tree_nodes, bufnr = M.unlinked_section.bufnr })
|
local unlinked_discussion_tree = NuiTree({
|
||||||
|
nodes = unlinked_discussion_tree_nodes,
|
||||||
|
bufnr = M.unlinked_section.bufnr,
|
||||||
|
prepare_node = nui_tree_prepare_node,
|
||||||
|
})
|
||||||
unlinked_discussion_tree:render()
|
unlinked_discussion_tree:render()
|
||||||
M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_section.bufnr, true)
|
M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_section.bufnr, true)
|
||||||
M.unlinked_discussion_tree = unlinked_discussion_tree
|
M.unlinked_discussion_tree = unlinked_discussion_tree
|
||||||
@@ -793,29 +788,59 @@ M.add_empty_titles = function(args)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Check if type of node is note or note body
|
||||||
|
---@param node NuiTree.Node?
|
||||||
|
---@return boolean
|
||||||
|
M.is_node_note = function(node)
|
||||||
|
if node and (node.type == "note_body" or node.type == "note") then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Check if type of current node is note or note body
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@return boolean
|
||||||
|
M.is_current_node_note = function(tree)
|
||||||
|
return M.is_node_note(tree:get_node())
|
||||||
|
end
|
||||||
|
|
||||||
M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.edit_comment, function()
|
vim.keymap.set("n", state.settings.discussion_tree.edit_comment, function()
|
||||||
M.edit_comment(tree, unlinked)
|
if M.is_current_node_note(tree) then
|
||||||
|
M.edit_comment(tree, unlinked)
|
||||||
|
end
|
||||||
end, { buffer = bufnr })
|
end, { buffer = bufnr })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.delete_comment, function()
|
vim.keymap.set("n", state.settings.discussion_tree.delete_comment, function()
|
||||||
M.delete_comment(tree, unlinked)
|
if M.is_current_node_note(tree) then
|
||||||
|
M.delete_comment(tree, unlinked)
|
||||||
|
end
|
||||||
end, { buffer = bufnr })
|
end, { buffer = bufnr })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.toggle_resolved, function()
|
vim.keymap.set("n", state.settings.discussion_tree.toggle_resolved, function()
|
||||||
M.toggle_discussion_resolved(tree)
|
if M.is_current_node_note(tree) then
|
||||||
|
M.toggle_discussion_resolved(tree)
|
||||||
|
end
|
||||||
end, { buffer = bufnr })
|
end, { buffer = bufnr })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.toggle_node, function()
|
vim.keymap.set("n", state.settings.discussion_tree.toggle_node, function()
|
||||||
M.toggle_node(tree)
|
M.toggle_node(tree)
|
||||||
end, { buffer = bufnr })
|
end, { buffer = bufnr })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.reply, function()
|
vim.keymap.set("n", state.settings.discussion_tree.reply, function()
|
||||||
M.reply(tree)
|
if M.is_current_node_note(tree) then
|
||||||
|
M.reply(tree)
|
||||||
|
end
|
||||||
end, { buffer = bufnr })
|
end, { buffer = bufnr })
|
||||||
|
|
||||||
if not unlinked then
|
if not unlinked then
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.jump_to_file, function()
|
vim.keymap.set("n", state.settings.discussion_tree.jump_to_file, function()
|
||||||
M.jump_to_file(tree)
|
if M.is_current_node_note(tree) then
|
||||||
|
M.jump_to_file(tree)
|
||||||
|
end
|
||||||
end, { buffer = bufnr })
|
end, { buffer = bufnr })
|
||||||
vim.keymap.set("n", state.settings.discussion_tree.jump_to_reviewer, function()
|
vim.keymap.set("n", state.settings.discussion_tree.jump_to_reviewer, function()
|
||||||
M.jump_to_reviewer(tree)
|
if M.is_current_node_note(tree) then
|
||||||
|
M.jump_to_reviewer(tree)
|
||||||
|
end
|
||||||
end, { buffer = bufnr })
|
end, { buffer = bufnr })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -850,153 +875,68 @@ M.redraw_resolved_status = function(tree, note, mark_resolved)
|
|||||||
tree:render()
|
tree:render()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Replace text in discussion after note update.
|
||||||
|
---@param data Discussion[]|UnlinkedDiscussion[]
|
||||||
|
---@param discussion_id string
|
||||||
|
---@param note_id integer
|
||||||
|
---@param text string
|
||||||
M.replace_text = function(data, discussion_id, note_id, text)
|
M.replace_text = function(data, discussion_id, note_id, text)
|
||||||
for i, discussion in ipairs(data) do
|
for i, discussion in ipairs(data) do
|
||||||
if discussion.id == discussion_id then
|
if discussion.id == discussion_id then
|
||||||
for j, note in ipairs(discussion.notes) do
|
for j, note in ipairs(discussion.notes) do
|
||||||
if note.id == note_id then
|
if note.id == note_id then
|
||||||
data[i].notes[j].body = text
|
data[i].notes[j].body = text
|
||||||
return data
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Get root node
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@param node NuiTree.Node?
|
||||||
|
---@return NuiTree.Node?
|
||||||
M.get_root_node = function(tree, node)
|
M.get_root_node = function(tree, node)
|
||||||
if not node.is_root then
|
if not node then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if node.type == "note_body" or node.type == "note" and not node.is_root then
|
||||||
local parent_id = node:get_parent_id()
|
local parent_id = node:get_parent_id()
|
||||||
return M.get_root_node(tree, tree:get_node(parent_id))
|
return M.get_root_node(tree, tree:get_node(parent_id))
|
||||||
else
|
elseif node.is_root then
|
||||||
return node
|
return node
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Get note node
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@param node NuiTree.Node?
|
||||||
|
---@return NuiTree.Node?
|
||||||
M.get_note_node = function(tree, node)
|
M.get_note_node = function(tree, node)
|
||||||
if not node.is_note then
|
if not node then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if node.type == "note_body" then
|
||||||
local parent_id = node:get_parent_id()
|
local parent_id = node:get_parent_id()
|
||||||
if parent_id == nil then
|
if parent_id == nil then
|
||||||
return node
|
return node
|
||||||
end
|
end
|
||||||
return M.get_note_node(tree, tree:get_node(parent_id))
|
return M.get_note_node(tree, tree:get_node(parent_id))
|
||||||
else
|
elseif node.type == "note" then
|
||||||
return node
|
return node
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local attach_uuid = function(str)
|
|
||||||
return { text = str, id = u.uuid() }
|
|
||||||
end
|
|
||||||
|
|
||||||
---Build note header from note.
|
|
||||||
---@param note Note
|
|
||||||
---@return string
|
|
||||||
M.build_note_header = function(note)
|
|
||||||
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
|
||||||
end
|
|
||||||
|
|
||||||
M.build_note_body = function(note, resolve_info)
|
|
||||||
local text_nodes = {}
|
|
||||||
for bodyLine in note.body:gmatch("[^\n]+") do
|
|
||||||
local line = attach_uuid(bodyLine)
|
|
||||||
table.insert(
|
|
||||||
text_nodes,
|
|
||||||
NuiTree.Node({
|
|
||||||
new_line = (type(note.position) == "table" and note.position.new_line),
|
|
||||||
old_line = (type(note.position) == "table" and note.position.old_line),
|
|
||||||
text = line.text,
|
|
||||||
id = line.id,
|
|
||||||
is_body = true,
|
|
||||||
}, {})
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
local resolve_symbol = ""
|
|
||||||
if resolve_info ~= nil and resolve_info.resolvable then
|
|
||||||
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
|
||||||
or state.settings.discussion_tree.unresolved
|
|
||||||
end
|
|
||||||
|
|
||||||
local noteHeader = M.build_note_header(note) .. " " .. resolve_symbol
|
|
||||||
|
|
||||||
return noteHeader, text_nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
M.build_note = function(note, resolve_info)
|
|
||||||
local text, text_nodes = M.build_note_body(note, resolve_info)
|
|
||||||
local note_node = NuiTree.Node({
|
|
||||||
text = text,
|
|
||||||
id = note.id,
|
|
||||||
file_name = (type(note.position) == "table" and note.position.new_path),
|
|
||||||
new_line = (type(note.position) == "table" and note.position.new_line),
|
|
||||||
old_line = (type(note.position) == "table" and note.position.old_line),
|
|
||||||
is_note = true,
|
|
||||||
}, text_nodes)
|
|
||||||
|
|
||||||
return note_node, text, text_nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
M.add_reply_to_tree = function(tree, note, discussion_id)
|
M.add_reply_to_tree = function(tree, note, discussion_id)
|
||||||
local note_node = M.build_note(note)
|
local note_node = discussions_tree.build_note(note)
|
||||||
note_node:expand()
|
note_node:expand()
|
||||||
tree:add_node(note_node, discussion_id and ("-" .. discussion_id) or nil)
|
tree:add_node(note_node, discussion_id and ("-" .. discussion_id) or nil)
|
||||||
tree:render()
|
tree:render()
|
||||||
end
|
end
|
||||||
|
|
||||||
M.add_discussions_to_table = function(items)
|
---Get note location
|
||||||
local t = {}
|
---@param tree NuiTree
|
||||||
for _, discussion in ipairs(items) do
|
|
||||||
local discussion_children = {}
|
|
||||||
|
|
||||||
-- These properties are filled in by the first note
|
|
||||||
local root_text = ""
|
|
||||||
local root_note_id = ""
|
|
||||||
local root_file_name = ""
|
|
||||||
local root_id = 0
|
|
||||||
local root_text_nodes = {}
|
|
||||||
local resolvable = false
|
|
||||||
local resolved = false
|
|
||||||
local root_new_line = nil
|
|
||||||
local root_old_line = nil
|
|
||||||
|
|
||||||
for j, note in ipairs(discussion.notes) do
|
|
||||||
if j == 1 then
|
|
||||||
_, root_text, root_text_nodes = M.build_note(note, { resolved = note.resolved, resolvable = note.resolvable })
|
|
||||||
|
|
||||||
root_file_name = (type(note.position) == "table" and note.position.new_path)
|
|
||||||
root_new_line = (type(note.position) == "table" and note.position.new_line)
|
|
||||||
root_old_line = (type(note.position) == "table" and note.position.old_line)
|
|
||||||
root_id = discussion.id
|
|
||||||
root_note_id = note.id
|
|
||||||
resolvable = note.resolvable
|
|
||||||
resolved = note.resolved
|
|
||||||
else -- Otherwise insert it as a child node...
|
|
||||||
local note_node = M.build_note(note)
|
|
||||||
table.insert(discussion_children, note_node)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Creates the first node in the discussion, and attaches children
|
|
||||||
local body = u.spread(root_text_nodes, discussion_children)
|
|
||||||
local root_node = NuiTree.Node({
|
|
||||||
text = root_text,
|
|
||||||
is_note = true,
|
|
||||||
is_root = true,
|
|
||||||
id = root_id,
|
|
||||||
root_note_id = root_note_id,
|
|
||||||
file_name = root_file_name,
|
|
||||||
new_line = root_new_line,
|
|
||||||
old_line = root_old_line,
|
|
||||||
resolvable = resolvable,
|
|
||||||
resolved = resolved,
|
|
||||||
}, body)
|
|
||||||
|
|
||||||
table.insert(t, root_node)
|
|
||||||
end
|
|
||||||
|
|
||||||
return t
|
|
||||||
end
|
|
||||||
|
|
||||||
M.get_note_location = function(tree)
|
M.get_note_location = function(tree)
|
||||||
local node = tree:get_node()
|
local node = tree:get_node()
|
||||||
if node == nil then
|
if node == nil then
|
||||||
279
lua/gitlab/actions/discussions/tree.lua
Normal file
279
lua/gitlab/actions/discussions/tree.lua
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
local state = require("gitlab.state")
|
||||||
|
local u = require("gitlab.utils")
|
||||||
|
local NuiTree = require("nui.tree")
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local attach_uuid = function(str)
|
||||||
|
return { text = str, id = u.uuid() }
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create path node
|
||||||
|
---@param relative_path string
|
||||||
|
---@param full_path string
|
||||||
|
---@param child_nodes NuiTree.Node[]?
|
||||||
|
---@return NuiTree.Node
|
||||||
|
local function create_path_node(relative_path, full_path, child_nodes)
|
||||||
|
return NuiTree.Node({
|
||||||
|
text = relative_path,
|
||||||
|
path = full_path,
|
||||||
|
id = full_path,
|
||||||
|
type = "path",
|
||||||
|
icon = " ",
|
||||||
|
icon_hl = "GitlabDirectoryIcon",
|
||||||
|
text_hl = "GitlabDirectory",
|
||||||
|
}, child_nodes or {})
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create file name node
|
||||||
|
---@param file_name string
|
||||||
|
---@param full_file_path string
|
||||||
|
---@param child_nodes NuiTree.Node[]?
|
||||||
|
---@return NuiTree.Node
|
||||||
|
local function create_file_name_node(file_name, full_file_path, child_nodes)
|
||||||
|
local icon, icon_hl = u.get_icon(file_name)
|
||||||
|
return NuiTree.Node({
|
||||||
|
text = file_name,
|
||||||
|
file_name = full_file_path,
|
||||||
|
id = full_file_path,
|
||||||
|
type = "file_name",
|
||||||
|
icon = icon,
|
||||||
|
icon_hl = icon_hl,
|
||||||
|
text_hl = "GitlabFileName",
|
||||||
|
}, child_nodes or {})
|
||||||
|
end
|
||||||
|
|
||||||
|
---Sort list of nodes (in place) of type "path" or "file_name"
|
||||||
|
---@param nodes NuiTree.Node[]
|
||||||
|
local function sort_nodes(nodes)
|
||||||
|
table.sort(nodes, function(node1, node2)
|
||||||
|
if node1.type == "path" and node2.type == "path" then
|
||||||
|
return node1.path < node2.path
|
||||||
|
elseif node1.type == "file_name" and node2.type == "file_name" then
|
||||||
|
return node1.file_name < node2.file_name
|
||||||
|
elseif node1.type == "path" and node2.type == "file_name" then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Merge path nodes which have only single path child
|
||||||
|
---@param node NuiTree.Node
|
||||||
|
local function flatten_nodes(node)
|
||||||
|
if node.type ~= "path" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _, child in ipairs(node.__children) do
|
||||||
|
flatten_nodes(child)
|
||||||
|
end
|
||||||
|
if #node.__children == 1 and node.__children[1].type == "path" then
|
||||||
|
local child = node.__children[1]
|
||||||
|
node.__children = child.__children
|
||||||
|
node.id = child.id
|
||||||
|
node.path = child.path
|
||||||
|
node.text = node.text .. u.path_separator .. child.text
|
||||||
|
end
|
||||||
|
sort_nodes(node.__children)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Build note header from note.
|
||||||
|
---@param note Note
|
||||||
|
---@return string
|
||||||
|
local function build_note_header(note)
|
||||||
|
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Build note node body
|
||||||
|
---@param note Note
|
||||||
|
---@param resolve_info table?
|
||||||
|
---@return string
|
||||||
|
---@return NuiTree.Node[]
|
||||||
|
local function build_note_body(note, resolve_info)
|
||||||
|
local text_nodes = {}
|
||||||
|
for bodyLine in note.body:gmatch("[^\n]+") do
|
||||||
|
local line = attach_uuid(bodyLine)
|
||||||
|
table.insert(
|
||||||
|
text_nodes,
|
||||||
|
NuiTree.Node({
|
||||||
|
new_line = (type(note.position) == "table" and note.position.new_line),
|
||||||
|
old_line = (type(note.position) == "table" and note.position.old_line),
|
||||||
|
text = line.text,
|
||||||
|
id = line.id,
|
||||||
|
type = "note_body",
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local resolve_symbol = ""
|
||||||
|
if resolve_info ~= nil and resolve_info.resolvable then
|
||||||
|
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
||||||
|
or state.settings.discussion_tree.unresolved
|
||||||
|
end
|
||||||
|
|
||||||
|
local noteHeader = build_note_header(note) .. " " .. resolve_symbol
|
||||||
|
|
||||||
|
return noteHeader, text_nodes
|
||||||
|
end
|
||||||
|
|
||||||
|
---Build note node
|
||||||
|
---@param note Note
|
||||||
|
---@param resolve_info table?
|
||||||
|
---@return NuiTree.Node
|
||||||
|
---@return string
|
||||||
|
---@return NuiTree.Node[]
|
||||||
|
M.build_note = function(note, resolve_info)
|
||||||
|
local text, text_nodes = build_note_body(note, resolve_info)
|
||||||
|
local note_node = NuiTree.Node({
|
||||||
|
text = text,
|
||||||
|
id = note.id,
|
||||||
|
file_name = (type(note.position) == "table" and note.position.new_path),
|
||||||
|
new_line = (type(note.position) == "table" and note.position.new_line),
|
||||||
|
old_line = (type(note.position) == "table" and note.position.old_line),
|
||||||
|
type = "note",
|
||||||
|
}, text_nodes)
|
||||||
|
|
||||||
|
return note_node, text, text_nodes
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create nodes for NuiTree from discussions
|
||||||
|
---@param items Discussion[]
|
||||||
|
---@param unlinked boolean? False or nil means that discussions are linked to code lines
|
||||||
|
---@return NuiTree.Node[]
|
||||||
|
M.add_discussions_to_table = function(items, unlinked)
|
||||||
|
local t = {}
|
||||||
|
for _, discussion in ipairs(items) do
|
||||||
|
local discussion_children = {}
|
||||||
|
|
||||||
|
-- These properties are filled in by the first note
|
||||||
|
---@type string?
|
||||||
|
local root_text = ""
|
||||||
|
---@type string?
|
||||||
|
local root_note_id = ""
|
||||||
|
---@type string?
|
||||||
|
local root_file_name = ""
|
||||||
|
---@type string
|
||||||
|
local root_id
|
||||||
|
local root_text_nodes = {}
|
||||||
|
local resolvable = false
|
||||||
|
local resolved = false
|
||||||
|
local root_new_line = nil
|
||||||
|
local root_old_line = nil
|
||||||
|
|
||||||
|
for j, note in ipairs(discussion.notes) do
|
||||||
|
if j == 1 then
|
||||||
|
_, root_text, root_text_nodes = M.build_note(note, { resolved = note.resolved, resolvable = note.resolvable })
|
||||||
|
|
||||||
|
root_file_name = (type(note.position) == "table" and note.position.new_path or nil)
|
||||||
|
root_new_line = (type(note.position) == "table" and note.position.new_line or nil)
|
||||||
|
root_old_line = (type(note.position) == "table" and note.position.old_line or nil)
|
||||||
|
root_id = discussion.id
|
||||||
|
root_note_id = tostring(note.id)
|
||||||
|
resolvable = note.resolvable
|
||||||
|
resolved = note.resolved
|
||||||
|
else -- Otherwise insert it as a child node...
|
||||||
|
local note_node = M.build_note(note)
|
||||||
|
table.insert(discussion_children, note_node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Creates the first node in the discussion, and attaches children
|
||||||
|
local body = u.spread(root_text_nodes, discussion_children)
|
||||||
|
local root_node = NuiTree.Node({
|
||||||
|
text = root_text,
|
||||||
|
type = "note",
|
||||||
|
is_root = true,
|
||||||
|
id = root_id,
|
||||||
|
root_note_id = root_note_id,
|
||||||
|
file_name = root_file_name,
|
||||||
|
new_line = root_new_line,
|
||||||
|
old_line = root_old_line,
|
||||||
|
resolvable = resolvable,
|
||||||
|
resolved = resolved,
|
||||||
|
}, body)
|
||||||
|
|
||||||
|
table.insert(t, root_node)
|
||||||
|
end
|
||||||
|
if state.settings.discussion_tree.tree_type == "simple" or unlinked == true then
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create all the folder and file name nodes.
|
||||||
|
local discussion_by_file_name = {}
|
||||||
|
local top_level_path_to_node = {}
|
||||||
|
for _, node in ipairs(t) do
|
||||||
|
local path = ""
|
||||||
|
local parent_node = nil
|
||||||
|
local path_parts = u.split_path(node.file_name)
|
||||||
|
local file_name = table.remove(path_parts, #path_parts)
|
||||||
|
-- Create folders
|
||||||
|
for i, path_part in ipairs(path_parts) do
|
||||||
|
path = path ~= nil and path .. u.path_separator .. path_part or path_part
|
||||||
|
if i == 1 then
|
||||||
|
if top_level_path_to_node[path] == nil then
|
||||||
|
parent_node = create_path_node(path_part, path)
|
||||||
|
top_level_path_to_node[path] = parent_node
|
||||||
|
table.insert(discussion_by_file_name, parent_node)
|
||||||
|
end
|
||||||
|
parent_node = top_level_path_to_node[path]
|
||||||
|
elseif parent_node then
|
||||||
|
local child_node = nil
|
||||||
|
for _, child in ipairs(parent_node.__children) do
|
||||||
|
if child.path == path then
|
||||||
|
child_node = child
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if child_node == nil then
|
||||||
|
child_node = create_path_node(path_part, path)
|
||||||
|
table.insert(parent_node.__children, child_node)
|
||||||
|
parent_node:expand()
|
||||||
|
parent_node = child_node
|
||||||
|
else
|
||||||
|
parent_node = child_node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create file name nodes
|
||||||
|
if parent_node == nil then
|
||||||
|
---Top level file name
|
||||||
|
if top_level_path_to_node[node.file_name] ~= nil then
|
||||||
|
table.insert(top_level_path_to_node[node.file_name].__children, node)
|
||||||
|
else
|
||||||
|
local file_node = create_file_name_node(file_name, node.file_name, { node })
|
||||||
|
file_node:expand()
|
||||||
|
top_level_path_to_node[node.file_name] = file_node
|
||||||
|
table.insert(discussion_by_file_name, file_node)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local child_node = nil
|
||||||
|
for _, child in ipairs(parent_node.__children) do
|
||||||
|
if child.file_name == node.file_name then
|
||||||
|
child_node = child
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if child_node == nil then
|
||||||
|
child_node = create_file_name_node(file_name, node.file_name, { node })
|
||||||
|
table.insert(parent_node.__children, child_node)
|
||||||
|
parent_node:expand()
|
||||||
|
child_node:expand()
|
||||||
|
else
|
||||||
|
table.insert(child_node.__children, node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Flatten empty folders
|
||||||
|
for _, node in ipairs(discussion_by_file_name) do
|
||||||
|
flatten_nodes(node)
|
||||||
|
end
|
||||||
|
sort_nodes(discussion_by_file_name)
|
||||||
|
|
||||||
|
return discussion_by_file_name
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -38,7 +38,7 @@ M.attach_file = function()
|
|||||||
if not choice then
|
if not choice then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local full_path = attachment_dir .. (u.is_windows() and "\\" or "/") .. choice
|
local full_path = attachment_dir .. u.path_separator .. choice
|
||||||
local body = { file_path = full_path, file_name = choice }
|
local body = { file_path = full_path, file_name = choice }
|
||||||
job.run_job("/mr/attachment", "POST", body, function(data)
|
job.run_job("/mr/attachment", "POST", body, function(data)
|
||||||
local markdown = data.markdown
|
local markdown = data.markdown
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ M.create_layout = function(info_lines)
|
|||||||
details_popup = Popup(details_popup_settings)
|
details_popup = Popup(details_popup_settings)
|
||||||
if state.settings.info.horizontal then
|
if state.settings.info.horizontal then
|
||||||
local longest_line = u.get_longest_string(info_lines)
|
local longest_line = u.get_longest_string(info_lines)
|
||||||
print(longest_line)
|
|
||||||
internal_layout = Layout.Box({
|
internal_layout = Layout.Box({
|
||||||
Layout.Box(title_popup, { size = 3 }),
|
Layout.Box(title_popup, { size = 3 }),
|
||||||
Layout.Box({
|
Layout.Box({
|
||||||
|
|||||||
@@ -7,3 +7,6 @@ local discussion = colors.discussion_tree
|
|||||||
vim.api.nvim_set_hl(0, "GitlabUsername", u.get_colors_for_group(discussion.username))
|
vim.api.nvim_set_hl(0, "GitlabUsername", u.get_colors_for_group(discussion.username))
|
||||||
vim.api.nvim_set_hl(0, "GitlabDate", u.get_colors_for_group(discussion.date))
|
vim.api.nvim_set_hl(0, "GitlabDate", u.get_colors_for_group(discussion.date))
|
||||||
vim.api.nvim_set_hl(0, "GitlabChevron", u.get_colors_for_group(discussion.chevron))
|
vim.api.nvim_set_hl(0, "GitlabChevron", u.get_colors_for_group(discussion.chevron))
|
||||||
|
vim.api.nvim_set_hl(0, "GitlabDirectory", u.get_colors_for_group(discussion.directory))
|
||||||
|
vim.api.nvim_set_hl(0, "GitlabDirectoryIcon", u.get_colors_for_group(discussion.directory_icon))
|
||||||
|
vim.api.nvim_set_hl(0, "GitlabFileName", u.get_colors_for_group(discussion.file_name))
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ M.settings = {
|
|||||||
size = "20%",
|
size = "20%",
|
||||||
resolved = "✓",
|
resolved = "✓",
|
||||||
unresolved = "",
|
unresolved = "",
|
||||||
|
tree_type = "simple",
|
||||||
},
|
},
|
||||||
info = {
|
info = {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
@@ -97,7 +98,10 @@ M.settings = {
|
|||||||
discussion_tree = {
|
discussion_tree = {
|
||||||
username = "Keyword",
|
username = "Keyword",
|
||||||
date = "Comment",
|
date = "Comment",
|
||||||
chevron = "Comment",
|
chevron = "DiffviewNonText",
|
||||||
|
directory = "Directory",
|
||||||
|
directory_icon = "DiffviewFolderSign",
|
||||||
|
file_name = "Normal",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
---Pulls out a list of values matching a given key from an array of tables
|
---Pulls out a list of values matching a given key from an array of tables
|
||||||
@@ -246,12 +247,83 @@ M.is_windows = function()
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Path separator based on current OS.
|
||||||
|
---@type string
|
||||||
|
M.path_separator = M.is_windows() and "\\" or "/"
|
||||||
|
|
||||||
|
---Split path by OS path separator.
|
||||||
|
---@param path string
|
||||||
|
---@return string[]
|
||||||
|
M.split_path = function(path)
|
||||||
|
local path_parts = {}
|
||||||
|
for part in string.gmatch(path, "([^" .. M.path_separator .. "]+)") do
|
||||||
|
table.insert(path_parts, part)
|
||||||
|
end
|
||||||
|
return path_parts
|
||||||
|
end
|
||||||
|
|
||||||
|
M.P = function(...)
|
||||||
|
local objects = {}
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local v = select(i, ...)
|
||||||
|
table.insert(objects, vim.inspect(v))
|
||||||
|
end
|
||||||
|
|
||||||
|
print(table.concat(objects, "\n"))
|
||||||
|
return ...
|
||||||
|
end
|
||||||
|
|
||||||
M.get_buffer_text = function(bufnr)
|
M.get_buffer_text = function(bufnr)
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
local text = table.concat(lines, "\n")
|
local text = table.concat(lines, "\n")
|
||||||
return text
|
return text
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M.string_starts = function(str, start)
|
||||||
|
return str:sub(1, #start) == start
|
||||||
|
end
|
||||||
|
|
||||||
|
M.press_enter = function()
|
||||||
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>", false, true, true), "n", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Return timestamp from ISO 8601 formatted date string.
|
||||||
|
---@param date_string string ISO 8601 formatted date string
|
||||||
|
---@return integer timestamp
|
||||||
|
M.from_iso_format_date_to_timestamp = function(date_string)
|
||||||
|
local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
|
||||||
|
return os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
|
||||||
|
end
|
||||||
|
|
||||||
|
M.format_date = function(date_string)
|
||||||
|
local date_table = os.date("!*t")
|
||||||
|
local date = M.from_iso_format_date_to_timestamp(date_string)
|
||||||
|
|
||||||
|
local current_date = os.time({
|
||||||
|
year = date_table.year,
|
||||||
|
month = date_table.month,
|
||||||
|
day = date_table.day,
|
||||||
|
hour = date_table.hour,
|
||||||
|
min = date_table.min,
|
||||||
|
sec = date_table.sec,
|
||||||
|
})
|
||||||
|
|
||||||
|
local time_diff = current_date - date
|
||||||
|
|
||||||
|
if time_diff < 60 then
|
||||||
|
return pluralize(time_diff, "second")
|
||||||
|
elseif time_diff < 3600 then
|
||||||
|
return pluralize(math.floor(time_diff / 60), "minute")
|
||||||
|
elseif time_diff < 86400 then
|
||||||
|
return pluralize(math.floor(time_diff / 3600), "hour")
|
||||||
|
elseif time_diff < 2592000 then
|
||||||
|
return pluralize(math.floor(time_diff / 86400), "day")
|
||||||
|
else
|
||||||
|
local formatted_date = os.date("%A, %B %e", date)
|
||||||
|
return formatted_date
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
M.jump_to_file = function(filename, line_number)
|
M.jump_to_file = function(filename, line_number)
|
||||||
if line_number == nil then
|
if line_number == nil then
|
||||||
line_number = 1
|
line_number = 1
|
||||||
@@ -362,7 +434,7 @@ M.list_files_in_folder = function(folder_path)
|
|||||||
local files = {}
|
local files = {}
|
||||||
if folder ~= nil then
|
if folder ~= nil then
|
||||||
for _, file in ipairs(folder) do
|
for _, file in ipairs(folder) do
|
||||||
local file_path = folder_path .. (M.is_windows() and "\\" or "/") .. file
|
local file_path = folder_path .. M.path_separator .. file
|
||||||
local timestamp = vim.fn.getftime(file_path)
|
local timestamp = vim.fn.getftime(file_path)
|
||||||
table.insert(files, { name = file, timestamp = timestamp })
|
table.insert(files, { name = file, timestamp = timestamp })
|
||||||
end
|
end
|
||||||
@@ -528,4 +600,21 @@ M.get_visual_selection_boundaries = function()
|
|||||||
return start_line, end_line
|
return start_line, end_line
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Get icon for filename if nvim-web-devicons plugin is available otherwise return empty string
|
||||||
|
---@return string?
|
||||||
|
---@return string?
|
||||||
|
M.get_icon = function(filename)
|
||||||
|
if has_devicons then
|
||||||
|
local extension = vim.fn.fnamemodify(filename, ":e")
|
||||||
|
local icon, icon_hl = devicons.get_icon(filename, extension, { default = true })
|
||||||
|
if icon ~= nil then
|
||||||
|
return icon .. " ", icon_hl
|
||||||
|
else
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
40
tests/init.lua
Normal file
40
tests/init.lua
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---Initialize neovim to use lua modules from luarocks and prepare correct search paths for
|
||||||
|
---modules, neovim plugins and load busted frameworks.
|
||||||
|
|
||||||
|
local function build_path(modules, extensions)
|
||||||
|
local path = ""
|
||||||
|
for _, module_path in ipairs(modules) do
|
||||||
|
for _, lua_path_extension in ipairs(extensions) do
|
||||||
|
path = path .. module_path .. lua_path_extension .. ";"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
local plugins_folder = "tests/plugins/*/lua"
|
||||||
|
local luarocks_cmd = "luarocks config --scope project"
|
||||||
|
|
||||||
|
-- Project path
|
||||||
|
local modules = { "lua" }
|
||||||
|
-- External plugins - dependencies
|
||||||
|
for plugin_path in vim.fn.glob(plugins_folder):gmatch("[^\r\n]+") do
|
||||||
|
table.insert(modules, plugin_path)
|
||||||
|
end
|
||||||
|
-- Lua modules path
|
||||||
|
table.insert(modules, vim.fn.trim(vim.fn.system(luarocks_cmd .. " deploy_lua_dir")))
|
||||||
|
|
||||||
|
local lua_path_extensions = { "/?.lua", "/?/init.lua" }
|
||||||
|
package.path = build_path(modules, lua_path_extensions) .. package.path
|
||||||
|
|
||||||
|
local cmodules = {
|
||||||
|
vim.fn.trim(vim.fn.system(luarocks_cmd .. " deploy_lib_dir")),
|
||||||
|
}
|
||||||
|
local lua_lib_extensions = { "/?.so", "/?/init.so" }
|
||||||
|
package.cpath = build_path(cmodules, lua_lib_extensions) .. package.cpath
|
||||||
|
|
||||||
|
-- Initialize required plugins which needs it
|
||||||
|
require("diffview").setup()
|
||||||
|
|
||||||
|
-- Run busted -
|
||||||
|
require("busted.runner")({ standalone = false })
|
||||||
|
os.exit(0)
|
||||||
0
tests/plugins/.placeholder
Normal file
0
tests/plugins/.placeholder
Normal file
6
tests/spec/discussions_spec.lua
Normal file
6
tests/spec/discussions_spec.lua
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
describe("gitlab/actions/discussions/init.lua", function()
|
||||||
|
it("Loads package", function()
|
||||||
|
local utils_ok, _ = pcall(require, "gitlab.actions.discussions")
|
||||||
|
assert._is_true(utils_ok)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
707
tests/spec/discussions_tree_spec.lua
Normal file
707
tests/spec/discussions_tree_spec.lua
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
---@class ResultNodeTree
|
||||||
|
---@field type string
|
||||||
|
---@field text string
|
||||||
|
---@field children ResultNodeTree[]?
|
||||||
|
|
||||||
|
---Transform nui nodes to table for easier comparison in tests We could compare directly
|
||||||
|
---NuiTree.Node but that have a lot of parameters which we don't care about
|
||||||
|
---@param nodes NuiTree.Node[]
|
||||||
|
---@param allowed_node_types table<string, boolean>
|
||||||
|
---@return ResultNodeTree
|
||||||
|
local function tree_nodes_to_table(nodes, allowed_node_types)
|
||||||
|
local result = {}
|
||||||
|
for _, node in ipairs(nodes) do
|
||||||
|
assert._is_true(allowed_node_types[node.type])
|
||||||
|
local current = {
|
||||||
|
type = node.type,
|
||||||
|
text = node.text,
|
||||||
|
children = tree_nodes_to_table(node.__children, allowed_node_types),
|
||||||
|
}
|
||||||
|
table.insert(result, current)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
math.randomseed(os.time())
|
||||||
|
---Create new discussion node, change ids and path
|
||||||
|
---@param discussion Discussion
|
||||||
|
---@param path string
|
||||||
|
local function copy_discussion_with_new_path(discussion, path)
|
||||||
|
local new_discussion = vim.fn.deepcopy(discussion)
|
||||||
|
new_discussion.id = tostring(math.random(1000, 10000000))
|
||||||
|
new_discussion.notes[1].id = math.random(1000, 10000000)
|
||||||
|
new_discussion.notes[1].position.new_path = path
|
||||||
|
new_discussion.notes[1].position.old_path = path
|
||||||
|
return new_discussion
|
||||||
|
end
|
||||||
|
|
||||||
|
describe("gitlab/actions/discussions/tree.lua", function()
|
||||||
|
it("Loads package", function()
|
||||||
|
local utils_ok, _ = pcall(require, "gitlab.actions.discussions.tree")
|
||||||
|
assert._is_true(utils_ok)
|
||||||
|
end)
|
||||||
|
describe("add_discussions_to_table", function()
|
||||||
|
local tree = require("gitlab.actions.discussions.tree")
|
||||||
|
local state = require("gitlab.state")
|
||||||
|
local utils = require("gitlab.utils")
|
||||||
|
local original_time_since = utils.time_since
|
||||||
|
local discussions
|
||||||
|
local unlinked_discussions
|
||||||
|
local spy_time_since
|
||||||
|
local all_node_types = { note = true, note_body = true, path = true, file_name = true }
|
||||||
|
|
||||||
|
it("Returns empty list with no discussions", function()
|
||||||
|
assert.are.same(tree.add_discussions_to_table({}), {})
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
utils.time_since = original_time_since
|
||||||
|
end)
|
||||||
|
before_each(function()
|
||||||
|
spy_time_since = spy.new(function()
|
||||||
|
return "5 days ago"
|
||||||
|
end)
|
||||||
|
utils.time_since = spy_time_since
|
||||||
|
local author = {
|
||||||
|
avatar_url = "https://secure.gravatar.com/avatar/a857c8a11e80d5c9116ad6ac4c0fb98a?s=80&d=identicon",
|
||||||
|
email = "",
|
||||||
|
id = 12345,
|
||||||
|
name = "Gitlab Name",
|
||||||
|
state = "active",
|
||||||
|
username = "gitlab.username",
|
||||||
|
web_url = "https://gitlab.com/gitlab.username",
|
||||||
|
}
|
||||||
|
local empty_resolved_by = {
|
||||||
|
avatar_url = "",
|
||||||
|
email = "",
|
||||||
|
id = 0,
|
||||||
|
name = "",
|
||||||
|
state = "",
|
||||||
|
username = "",
|
||||||
|
web_url = "",
|
||||||
|
}
|
||||||
|
|
||||||
|
discussions = {
|
||||||
|
{
|
||||||
|
id = "17c7b7558925d0caa7f73684482x9055977bf454",
|
||||||
|
individual_note = false,
|
||||||
|
notes = {
|
||||||
|
{
|
||||||
|
attachment = "",
|
||||||
|
author = author,
|
||||||
|
body = "Multiline comment",
|
||||||
|
commit_id = "",
|
||||||
|
created_at = "2023-10-28T18:27:34.082Z",
|
||||||
|
expires_at = vim.NIL,
|
||||||
|
file_name = "",
|
||||||
|
id = 1624411,
|
||||||
|
noteable_id = 240727,
|
||||||
|
noteable_iid = 1,
|
||||||
|
noteable_type = "MergeRequest",
|
||||||
|
position = {
|
||||||
|
base_sha = "d687b5ad4ad5ccd5ae9517efcd103629af1750d6",
|
||||||
|
head_sha = "18f76ebeb6e8fcd76a80dce5b592a4f133d2ad05",
|
||||||
|
line_range = {
|
||||||
|
["end"] = {
|
||||||
|
line_code = "8ec9a01bfd10b3191ac6b22252dba2aa95a0579d_18_17",
|
||||||
|
new_line = 0,
|
||||||
|
old_line = 0,
|
||||||
|
type = "new",
|
||||||
|
},
|
||||||
|
start = {
|
||||||
|
line_code = "8ec9a01bfd10b3191ac6b22252dba2aa95a0579d_18_19",
|
||||||
|
new_line = 0,
|
||||||
|
old_line = 0,
|
||||||
|
type = "new",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new_line = 17,
|
||||||
|
new_path = "README.md",
|
||||||
|
old_path = "README.md",
|
||||||
|
position_type = "text",
|
||||||
|
start_sha = "d687b5ad4ad5ccd5ae9517efcd103629af1750d6",
|
||||||
|
},
|
||||||
|
resolvable = true,
|
||||||
|
resolved = false,
|
||||||
|
resolved_at = vim.NIL,
|
||||||
|
resolved_by = empty_resolved_by,
|
||||||
|
system = false,
|
||||||
|
title = "",
|
||||||
|
type = "DiffNote",
|
||||||
|
updated_at = "2023-10-28T18:27:34.082Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = "c418928237e9e542b676d25c4211160agcs11733",
|
||||||
|
individual_note = false,
|
||||||
|
notes = {
|
||||||
|
{
|
||||||
|
attachment = "",
|
||||||
|
author = author,
|
||||||
|
body = "test single line comment!",
|
||||||
|
commit_id = "",
|
||||||
|
created_at = "2023-10-28T18:26:22.336Z",
|
||||||
|
expires_at = vim.NIL,
|
||||||
|
file_name = "",
|
||||||
|
id = 1624415,
|
||||||
|
noteable_id = 240727,
|
||||||
|
noteable_iid = 1,
|
||||||
|
noteable_type = "MergeRequest",
|
||||||
|
position = {
|
||||||
|
base_sha = "d687b5ad4ad5ccd5ae9517efcd103629af1750d6",
|
||||||
|
head_sha = "18f76ebeb6e8fcd76a80dce5b592a4f133d2ad05",
|
||||||
|
new_line = 11,
|
||||||
|
new_path = "folder_1/folder_2/folder_3/file.lua",
|
||||||
|
old_path = "folder_1/folder_2/folder_3/file.lua",
|
||||||
|
position_type = "text",
|
||||||
|
start_sha = "d687b5ad4ad5ccd5ae9517efcd103629af1750d6",
|
||||||
|
},
|
||||||
|
resolvable = true,
|
||||||
|
resolved = false,
|
||||||
|
resolved_at = vim.NIL,
|
||||||
|
resolved_by = empty_resolved_by,
|
||||||
|
system = false,
|
||||||
|
title = "",
|
||||||
|
type = "DiffNote",
|
||||||
|
updated_at = "2023-10-28T18:26:22.336Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
unlinked_discussions = {
|
||||||
|
{
|
||||||
|
id = "16c5b7558923d0caa7f73684481c9055976bf454",
|
||||||
|
individual_note = false,
|
||||||
|
notes = {
|
||||||
|
{
|
||||||
|
attachment = "",
|
||||||
|
author = author,
|
||||||
|
body = "Test just unlinked note",
|
||||||
|
commit_id = "",
|
||||||
|
created_at = "2021-05-20T10:10:00.648Z",
|
||||||
|
expires_at = vim.NIL,
|
||||||
|
file_name = "",
|
||||||
|
id = 165260,
|
||||||
|
noteable_id = 25024,
|
||||||
|
noteable_iid = 1,
|
||||||
|
noteable_type = "MergeRequest",
|
||||||
|
position = vim.NIL,
|
||||||
|
resolvable = true,
|
||||||
|
resolved = false,
|
||||||
|
resolved_at = vim.NIL,
|
||||||
|
resolved_by = empty_resolved_by,
|
||||||
|
system = false,
|
||||||
|
title = "",
|
||||||
|
type = "DiscussionNote",
|
||||||
|
updated_at = "2023-11-16T24:15:49.648Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = "38bbe42a1bb8f2a014c4fd87d87760772f090a3c",
|
||||||
|
individual_note = false,
|
||||||
|
notes = {
|
||||||
|
{
|
||||||
|
attachment = "",
|
||||||
|
author = author,
|
||||||
|
body = "Other unlinked note",
|
||||||
|
commit_id = "",
|
||||||
|
created_at = "2022-10-25T12:20:30.648Z",
|
||||||
|
expires_at = vim.NIL,
|
||||||
|
file_name = "",
|
||||||
|
id = 165260,
|
||||||
|
noteable_id = 25024,
|
||||||
|
noteable_iid = 1,
|
||||||
|
noteable_type = "MergeRequest",
|
||||||
|
position = vim.NIL,
|
||||||
|
resolvable = true,
|
||||||
|
resolved = false,
|
||||||
|
resolved_at = vim.NIL,
|
||||||
|
resolved_by = empty_resolved_by,
|
||||||
|
system = false,
|
||||||
|
title = "",
|
||||||
|
type = "DiscussionNote",
|
||||||
|
updated_at = "2023-11-16T20:15:49.648Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attachment = "",
|
||||||
|
author = author,
|
||||||
|
body = "Response to the unlinked note",
|
||||||
|
commit_id = "",
|
||||||
|
created_at = "2023-11-18T20:15:49.648Z",
|
||||||
|
expires_at = vim.NIL,
|
||||||
|
file_name = "",
|
||||||
|
id = 165260,
|
||||||
|
noteable_id = 25024,
|
||||||
|
noteable_iid = 1,
|
||||||
|
noteable_type = "MergeRequest",
|
||||||
|
position = vim.NIL,
|
||||||
|
resolvable = true,
|
||||||
|
resolved = false,
|
||||||
|
resolved_at = vim.NIL,
|
||||||
|
resolved_by = empty_resolved_by,
|
||||||
|
system = false,
|
||||||
|
title = "",
|
||||||
|
type = "DiscussionNote",
|
||||||
|
updated_at = "2023-11-16T20:15:49.648Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Returns list of note nodes if `tree_type` is `simple`", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "simple"
|
||||||
|
local nodes = tree.add_discussions_to_table(discussions)
|
||||||
|
assert.are.same(tree_nodes_to_table(nodes, { note = true, note_body = true }), {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
children = {},
|
||||||
|
text = "Multiline comment",
|
||||||
|
type = "note_body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "test single line comment!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Returns path tree of note nodes if tree_type is `by_file_name`", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "by_file_name"
|
||||||
|
local nodes = tree.add_discussions_to_table(discussions)
|
||||||
|
assert.are.same(tree_nodes_to_table(nodes, all_node_types), {
|
||||||
|
{
|
||||||
|
text = "folder_1/folder_2/folder_3",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "file.lua",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "test single line comment!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "README.md",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "Multiline comment",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
it("Merges the paths in path tree if there is no file in folder", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "by_file_name"
|
||||||
|
local nodes = tree.add_discussions_to_table({ discussions[2] })
|
||||||
|
assert.are.same(tree_nodes_to_table(nodes, all_node_types), {
|
||||||
|
{
|
||||||
|
text = "folder_1/folder_2/folder_3",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "file.lua",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "test single line comment!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
it("Correctly places files in folders in file tree", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "by_file_name"
|
||||||
|
local discussion1 = copy_discussion_with_new_path(discussions[2], "folder_1/first_level.txt")
|
||||||
|
local discussion2 = copy_discussion_with_new_path(discussions[2], "folder_1/folder_2/second_level.txt")
|
||||||
|
local expected_result = {
|
||||||
|
{
|
||||||
|
text = "folder_1",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "folder_2",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "folder_3",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "file.lua",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "test single line comment!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "second_level.txt",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "test single line comment!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "first_level.txt",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "test single line comment!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
-- Make sure that order of nodes does not change result!
|
||||||
|
assert.are.same(
|
||||||
|
tree_nodes_to_table(tree.add_discussions_to_table({ discussions[2], discussion2, discussion1 }), all_node_types),
|
||||||
|
expected_result
|
||||||
|
)
|
||||||
|
assert.are.same(
|
||||||
|
tree_nodes_to_table(tree.add_discussions_to_table({ discussion2, discussions[2], discussion1 }), all_node_types),
|
||||||
|
expected_result
|
||||||
|
)
|
||||||
|
assert.are.same(
|
||||||
|
tree_nodes_to_table(tree.add_discussions_to_table({ discussion2, discussion1, discussions[2] }), all_node_types),
|
||||||
|
expected_result
|
||||||
|
)
|
||||||
|
assert.are.same(
|
||||||
|
tree_nodes_to_table(tree.add_discussions_to_table({ discussion1, discussion2, discussions[2] }), all_node_types),
|
||||||
|
expected_result
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
it("Correctly places files with same filenames and different paths", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "by_file_name"
|
||||||
|
local discussion1 = copy_discussion_with_new_path(discussions[2], "folder_1/diffent_folder/folder_3/file.lua")
|
||||||
|
discussion1.notes[1].body = "path: folder_1/diffent_folder/folder_3/file.lua"
|
||||||
|
local discussion2 = copy_discussion_with_new_path(discussions[2], "another/folder_2/folder_3/file.lua")
|
||||||
|
discussion2.notes[1].body = "path: another/folder_2/folder_3/file.lua"
|
||||||
|
local expected_result = {
|
||||||
|
{
|
||||||
|
text = "another/folder_2/folder_3",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "file.lua",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "path: another/folder_2/folder_3/file.lua",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "folder_1",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "diffent_folder/folder_3",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "file.lua",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "path: folder_1/diffent_folder/folder_3/file.lua",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "folder_2/folder_3",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "file.lua",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "test single line comment!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.are.same(
|
||||||
|
tree_nodes_to_table(tree.add_discussions_to_table({ discussions[2], discussion2, discussion1 }), all_node_types),
|
||||||
|
expected_result
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
it("Correctly places multiple notes in same file", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "by_file_name"
|
||||||
|
local discussion1 = copy_discussion_with_new_path(discussions[2], "folder_1/folder_2/folder_3/file.lua")
|
||||||
|
discussion1.notes[1].body = "This is different note!"
|
||||||
|
local expected_result = {
|
||||||
|
{
|
||||||
|
text = "folder_1/folder_2/folder_3",
|
||||||
|
type = "path",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "file.lua",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "test single line comment!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "This is different note!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.are.same(
|
||||||
|
tree_nodes_to_table(tree.add_discussions_to_table({ discussions[2], discussion1 }), all_node_types),
|
||||||
|
expected_result
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
it("Correctly places multiple notes in same top level file", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "by_file_name"
|
||||||
|
local discussion1 = copy_discussion_with_new_path(discussions[1], "README.md")
|
||||||
|
discussion1.notes[1].body = "This is different note!"
|
||||||
|
local expected_result = {
|
||||||
|
{
|
||||||
|
text = "README.md",
|
||||||
|
type = "file_name",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "Multiline comment",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "This is different note!",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.are.same(
|
||||||
|
tree_nodes_to_table(tree.add_discussions_to_table({ discussions[1], discussion1 }), all_node_types),
|
||||||
|
expected_result
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Returns list of note nodes for unlinked discussions", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "simple"
|
||||||
|
local nodes = tree.add_discussions_to_table(unlinked_discussions, true)
|
||||||
|
assert.are.same(tree_nodes_to_table(nodes, { note = true, note_body = true }), {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
children = {},
|
||||||
|
text = "Test just unlinked note",
|
||||||
|
type = "note_body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "Other unlinked note",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
children = {},
|
||||||
|
text = "Response to the unlinked note",
|
||||||
|
type = "note_body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.spy(spy_time_since).was.called_with("2021-05-20T10:10:00.648Z")
|
||||||
|
assert.spy(spy_time_since).was.called_with("2022-10-25T12:20:30.648Z")
|
||||||
|
assert.spy(spy_time_since).was.called_with("2023-11-18T20:15:49.648Z")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Returns list of note nodes for unlinked discussions even if tree_type is not `simple`", function()
|
||||||
|
state.settings.discussion_tree.tree_type = "by_file_name"
|
||||||
|
local nodes = tree.add_discussions_to_table(unlinked_discussions, true)
|
||||||
|
assert.are.same(tree_nodes_to_table(nodes, { note = true, note_body = true }), {
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
children = {},
|
||||||
|
text = "Test just unlinked note",
|
||||||
|
type = "note_body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
text = "Other unlinked note",
|
||||||
|
type = "note_body",
|
||||||
|
children = {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "@gitlab.username 5 days ago ",
|
||||||
|
type = "note",
|
||||||
|
children = {
|
||||||
|
{
|
||||||
|
children = {},
|
||||||
|
text = "Response to the unlinked note",
|
||||||
|
type = "note_body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
7
tests/spec/gitlab_spec.lua
Normal file
7
tests/spec/gitlab_spec.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
describe("gitlab", function()
|
||||||
|
it("Loads module", function()
|
||||||
|
require("gitlab")
|
||||||
|
local utils_ok, _ = pcall(require, "gitlab")
|
||||||
|
assert._is_true(utils_ok)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Reference in New Issue
Block a user