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:
johnybx
2023-12-04 23:03:32 +01:00
committed by GitHub
parent 63cbf41221
commit 02db3e4b0e
22 changed files with 1458 additions and 232 deletions

View 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)

View 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)

View 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)

219
tests/spec/util_spec.lua Normal file
View File

@@ -0,0 +1,219 @@
describe("utils/init.lua", function()
it("Loads package", function()
local utils_ok, _ = pcall(require, "gitlab.utils")
assert._is_true(utils_ok)
end)
local _, u = pcall(require, "gitlab.utils")
describe("extract", function()
it("Extracts a single value", function()
local t = { { one = 1, two = 2 }, { three = 3, four = 4 } }
local got = u.extract(t, "one")
local want = { 1 }
assert.are.same(want, got)
end)
it("Returns nothing with empty table", function()
local t = {}
local got = u.extract(t, "one")
local want = {}
assert.are.same(want, got)
end)
end)
describe("get_last_word", function()
it("Returns the last word in a sentence", function()
local sentence = "Hello world!"
local got = u.get_last_word(sentence)
local want = "world!"
assert.are.same(want, got)
end)
it("Returns an empty string without text", function()
local sentence = ""
local got = u.get_last_word(sentence)
local want = ""
assert.are.same(want, got)
end)
it("Returns whole string w/out divider", function()
local sentence = "Thisdoesnothavebreaks"
local got = u.get_last_word(sentence)
assert.are.same(sentence, got)
end)
it("Returns correct word w/ different divider", function()
local sentence = "this|uses|a|different|divider"
local got = u.get_last_word(sentence, "|")
local want = "divider"
assert.are.same(want, got)
end)
end)
describe("format_date", function()
local current_date = {
day = 19,
hour = 22,
isdst = false,
min = 0,
month = 11,
sec = 44,
wday = 1,
yday = 323,
year = 2023,
}
it("Returns days since a valid UTC timestamp", function()
local stamp = "2023-11-16T19:52:36.946Z"
local got = u.time_since(stamp, current_date)
local want = "3 days ago"
assert.are.same(want, got)
end)
it("Returns hours since a valid UTC timestamp", function()
local stamp = "2023-11-19T19:52:36.946Z"
local got = u.time_since(stamp, current_date)
local want = "2 hours ago"
assert.are.same(want, got)
end)
it("Returns readable time if > 1 year", function()
local stamp = "2011-11-19T19:52:36.946Z"
local got = u.time_since(stamp, current_date)
local want = "November 19, 2011"
assert.are.same(want, got)
end)
end)
describe("remove_first_value", function()
it("Removes the first value correctly", function()
local got = u.remove_first_value({ 1, 2 })
local want = { 2 }
assert.are.same(want, got)
end)
it("Handles a one-length list", function()
local got = u.remove_first_value({ 1 })
local want = {}
assert.are.same(want, got)
end)
it("Handles a zero-length list", function()
local got = u.remove_first_value({})
local want = {}
assert.are.same(want, got)
end)
end)
describe("table_size", function()
it("Works for associative tables", function()
local got = u.remove_first_value({ 1, 2 })
local want = { 2 }
assert.are.same(want, got)
end)
it("Handles a one-length list", function()
local got = u.remove_first_value({ 1 })
local want = {}
assert.are.same(want, got)
end)
it("Handles a zero-length list", function()
local got = u.remove_first_value({})
local want = {}
assert.are.same(want, got)
end)
end)
describe("contains", function()
it("Finds a value in a list", function()
local got = u.contains({ 1, 2 }, 1)
assert._is_true(got)
end)
it("Handles missing values", function()
local got = u.contains({ 1, 3, 4 }, 2)
assert._is_false(got)
end)
it("Handles empty lists", function()
local got = u.contains({}, 1)
assert._is_false(got)
end)
end)
describe("reverse", function()
it("Reverses the values in a list", function()
local got = u.reverse({ 1, 2, 3, 4 })
local want = { 4, 3, 2, 1 }
assert.are.same(got, want)
end)
it("Handles single value", function()
local got = u.reverse({ 1 })
local want = { 1 }
assert.are.same(got, want)
end)
it("Handles empty list", function()
local got = u.reverse({})
local want = {}
assert.are.same(got, want)
end)
end)
describe("spread", function()
it("Spreads the values", function()
local t1 = { 1, 2, 3 }
local t2 = { 4, 5, 6 }
local got = u.spread(t1, t2)
local want = { 1, 2, 3, 4, 5, 6 }
assert.are.same(got, want)
end)
it("Handles an empty t1 table", function()
local t1 = {}
local t2 = { 4, 5, 6 }
local got = u.spread(t1, t2)
local want = { 4, 5, 6 }
assert.are.same(got, want)
end)
it("Handles an empty t2 table", function()
local t1 = { 1, 2, 3 }
local t2 = {}
local got = u.spread(t1, t2)
local want = { 1, 2, 3 }
assert.are.same(got, want)
end)
it("Handles both empty tables", function()
local t1 = {}
local t2 = {}
local got = u.spread(t1, t2)
local want = {}
assert.are.same(got, want)
end)
end)
describe("offset_to_seconds", function()
local tests = {
est = { "-0500", -18000 },
pst = { "-0800", -28800 },
gmt = { "+0000", 0 },
cet = { "+0100", 360 },
jst = { "+0900", 32400 },
ist = { "+0530", 19800 },
art = { "-0300", -10800 },
aest = { "+1100", 39600 },
mmt = { "+0630", 23400 },
}
for _, val in ipairs(tests) do
local got = u.offset_to_seconds(val[1])
local want = val[2]
assert.are.same(got, want)
end
end)
describe("format_to_local", function()
local tests = {
{ "2023-10-28T16:25:09.482Z", "-0500", "10/28/2023 at 11:25" },
{ "2016-11-22T1:25:09.482Z", "-0500", "11/21/2016 at 20:25" },
{ "2016-11-22T1:25:09.482Z", "-0000", "11/22/2016 at 01:25" },
{ "2017-3-22T13:25:09.482Z", "+0700", "03/22/2017 at 20:25" },
{ "2023-10-28T11:25:09.482-05:00", "-0500", "10/28/2023 at 11:25" },
{ "2016-11-21T20:25:09.482-05:00", "-0500", "11/21/2016 at 20:25" },
{ "2016-11-22T1:25:09.482-00:00", "-0000", "11/22/2016 at 01:25" },
{ "2017-3-22T20:25:09.482+07:00", "+0700", "03/22/2017 at 20:25" },
}
for _, val in ipairs(tests) do
local got = u.format_to_local(val[1], val[2])
local want = val[3]
assert.are.same(got, want)
end
end)
end)