diff --git a/.busted b/.busted new file mode 100644 index 0000000..62db505 --- /dev/null +++ b/.busted @@ -0,0 +1,13 @@ +return { + _all = { + pattern = "_spec", + lpath = "lua/?.lua;lua/?/init.lua", + ROOT = {"lua/gitlab"}, + }, + default = { + verbose = true + }, + tests = { + verbose = true, + }, +} diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 3c1f1ac..e1c997d 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -4,7 +4,23 @@ on: branches: - main jobs: - build: + go_lint: + name: Lint Go ๐Ÿ’… + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.19' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.54 + only-new-issues: true + go_test: + name: Test Go ๐Ÿงช + needs: [go_lint] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/lua-tests.yaml b/.github/workflows/lua-tests.yaml new file mode 100644 index 0000000..1ae2601 --- /dev/null +++ b/.github/workflows/lua-tests.yaml @@ -0,0 +1,15 @@ +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 diff --git a/.github/workflows/lint-and-format.yaml b/.github/workflows/lua.yaml similarity index 63% rename from .github/workflows/lint-and-format.yaml rename to .github/workflows/lua.yaml index 1c86fef..dea054a 100644 --- a/.github/workflows/lint-and-format.yaml +++ b/.github/workflows/lua.yaml @@ -1,10 +1,11 @@ -name: Linting and Formatting +name: Lua on: pull_request: branches: - main jobs: - luacheck: + lua_lint: + name: Lint Lua ๐Ÿ’… runs-on: ubuntu-latest steps: - name: Checkout @@ -13,7 +14,8 @@ jobs: uses: lunarmodules/luacheck@v1 with: args: --globals vim --no-max-line-length -- . - stylua: + lua_format: + name: Formatting Lua ๐Ÿ’… runs-on: ubuntu-latest steps: - name: Checkout @@ -24,16 +26,12 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} version: latest args: --check . - golangci: + lua_test: + name: Test Lua ๐Ÿงช + needs: [lua_format,lua_lint] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 - with: - go-version: '1.19' - cache: false - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: v1.54 - only-new-issues: true + - name: Checkout + uses: actions/checkout@v3 + - name: Run Busted + uses: lunarmodules/busted@v2.2.0 diff --git a/.gitignore b/.gitignore index 2824744..021a0b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ bin .luarc.json +/luarocks +/lua_modules +/.luarocks diff --git a/logs b/logs deleted file mode 100644 index 77e0c06..0000000 --- a/logs +++ /dev/null @@ -1 +0,0 @@ -[DEBUG] GET https://gitlab.com/api/v4/merge_requests?source_branch=develop&state=opened diff --git a/lua/gitlab/actions/discussions.lua b/lua/gitlab/actions/discussions.lua index 1032027..bb05039 100644 --- a/lua/gitlab/actions/discussions.lua +++ b/lua/gitlab/actions/discussions.lua @@ -892,7 +892,7 @@ end ---@param note Note ---@return string M.build_note_header = function(note) - return "@" .. note.author.username .. " " .. u.format_date(note.created_at) + return "@" .. note.author.username .. " " .. u.time_since(note.created_at) end M.build_note_body = function(note, resolve_info) @@ -977,7 +977,7 @@ M.add_discussions_to_table = function(items) end -- Creates the first node in the discussion, and attaches children - local body = u.join_tables(root_text_nodes, discussion_children) + local body = u.spread(root_text_nodes, discussion_children) local root_node = NuiTree.Node({ text = root_text, is_note = true, diff --git a/lua/gitlab/actions/pipeline.lua b/lua/gitlab/actions/pipeline.lua index 86b3577..b1deb6b 100644 --- a/lua/gitlab/actions/pipeline.lua +++ b/lua/gitlab/actions/pipeline.lua @@ -46,7 +46,7 @@ M.open = function() u.switch_can_edit_buf(bufnr, true) table.insert(lines, string.format("Status: %s (%s)", state.settings.pipeline[pipeline.status], pipeline.status)) table.insert(lines, "") - table.insert(lines, string.format("Last Run: %s", u.format_date(pipeline.created_at))) + table.insert(lines, string.format("Last Run: %s", u.time_since(pipeline.created_at))) table.insert(lines, string.format("Url: %s", pipeline.web_url)) table.insert(lines, string.format("Triggered By: %s", pipeline.source)) @@ -99,7 +99,7 @@ M.see_logs = function() local bufnr = vim.api.nvim_get_current_buf() local linnr = vim.api.nvim_win_get_cursor(0)[1] local text = u.get_line_content(bufnr, linnr) - local last_word = u.get_last_chunk(text) + local last_word = u.get_last_word(text) if last_word == nil then u.notify("Cannot find job name", vim.log.levels.ERROR) return diff --git a/lua/gitlab/spec/util_spec.lua b/lua/gitlab/spec/util_spec.lua new file mode 100644 index 0000000..91f3aa3 --- /dev/null +++ b/lua/gitlab/spec/util_spec.lua @@ -0,0 +1,181 @@ +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) +end) diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index cf32594..53f62bf 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -122,7 +122,7 @@ M.merge_settings = function(args) end M.print_settings = function() - u.P(M.settings) + vim.print(M.settings) end -- First reads environment variables into the settings module, diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua index abb3f01..9713ec4 100644 --- a/lua/gitlab/utils/init.lua +++ b/lua/gitlab/utils/init.lua @@ -1,6 +1,156 @@ -local Job = require("plenary.job") local M = {} +---Pulls out a list of values matching a given key from an array of tables +---@param t table List of tables to search +---@param key string Value to search for in the list +---@return table List List of values that were extracted +M.extract = function(t, key) + local resultTable = {} + for _, value in ipairs(t) do + if value[key] then + table.insert(resultTable, value[key]) + end + end + return resultTable +end + +---Get the last word in a sentence +---@param sentence string The string to get the last word from +---@param divider string The regex to split the sentence by, defaults to whitespace +---@return string +M.get_last_word = function(sentence, divider) + local words = {} + local pattern = string.format("([^%s]+)", divider or " ") + for word in sentence:gmatch(pattern) do + table.insert(words, word) + end + return words[#words] or "" +end + +---Merges two deeply nested tables together, overriding values from the first with conflicts +---@param defaults table The first table +---@param overrides table The second table +---@return table +M.merge = function(defaults, overrides) + if type(defaults) == "table" and M.table_size(defaults) == 0 and type(overrides) == "table" then + return overrides + end + return vim.tbl_deep_extend("force", defaults, overrides) +end + +---Pluralizes the input word, e.g. "3 cows" +---@param num integer The count of the item/word +---@param word string The word to pluralize +---@return string +local function pluralize(num, word) + return num .. string.format(" %s", word) .. ((num > 1 or num <= 0) and "s" or "") +end + +--- Provides a human readable time since a given ISO date string +---@param date_string string -- The ISO time stamp to compare with the current time +---@return string +M.time_since = function(date_string, current_date_table) + local dt = current_date_table or os.date("!*t") + local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)") + local date = os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec }) + + local current_date = os.time({ + year = dt.year, + month = dt.month, + day = dt.day, + hour = dt.hour, + min = dt.min, + sec = dt.sec, + }) + + local time_diff = current_date - date + + if time_diff < 60 then + return pluralize(time_diff, "second") .. " ago" + elseif time_diff < 3600 then + return pluralize(math.floor(time_diff / 60), "minute") .. " ago" + elseif time_diff < 86400 then + return pluralize(math.floor(time_diff / 3600), "hour") .. " ago" + elseif time_diff < 2592000 then + return pluralize(math.floor(time_diff / 86400), "day") .. " ago" + else + local formatted_date = os.date("%B %e, %Y", date) + return tostring(formatted_date) + end +end + +---Removes the first value from a list and returns the new, smaller list +---@param tbl table The table +---@return table +M.remove_first_value = function(tbl) + local sliced_list = {} + if M.table_size(tbl) <= 1 then + return sliced_list + end + for i = 2, #tbl do + table.insert(sliced_list, tbl[i]) + end + + return sliced_list +end + +---Spreads all the values from t2 into t1 +---@param t1 table The first table (gets the values) +---@param t2 table The second table +---@return table +M.spread = function(t1, t2) + for _, value in ipairs(t2) do + table.insert(t1, value) + end + + return t1 +end + +---Returns the number of keys or values in a table +---@param t table The table to count +---@return integer +M.table_size = function(t) + local count = 0 + for _ in pairs(t) do + count = count + 1 + end + return count +end + +---Returns whether a given value is in a list or not +---@param list table The list to search +---@return boolean +M.contains = function(list, search_value) + for _, value in pairs(list) do + if value == search_value then + return true + end + end + return false +end + +---Trims whitespace from a string +---@param s string The string to trim +---@return string +M.trim = function(s) + local res = s:gsub("^%s+", ""):gsub("%s+$", "") + return res +end + +-- Reverses the order of elements in a list +---@param list table The list to reverse +---@return table +M.reverse = function(list) + if #list == 0 then + return list + end + local rev = {} + for i = #list, 1, -1 do + rev[#rev + 1] = list[i] + end + return rev +end + M.notify = function(msg, lvl) vim.notify("gitlab.nvim: " .. msg, lvl) end @@ -22,65 +172,12 @@ M.is_windows = function() return false 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) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local text = table.concat(lines, "\n") return text 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("", false, true, true), "n", false) -end - -M.format_date = function(date_string) - local date_table = os.date("!*t") - local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)") - local date = os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec }) - - 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 - - local function pluralize(num, word) - return num .. string.format(" %s", word) .. (num > 1 and "s" or "") .. " ago" - end - - 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) if line_number == nil then line_number = 1 @@ -123,38 +220,6 @@ M.create_popup_state = function(title, width, height) } end -M.merge = function(defaults, overrides) - if type(defaults) == "table" and M.table_size(defaults) == 0 and type(overrides) == "table" then - return overrides - end - return vim.tbl_deep_extend("force", defaults, overrides) -end - -M.join = function(tbl, separator) - separator = separator or " " - - local result = "" - for _, value in pairs(tbl) do - result = result .. tostring(value) .. separator - end - - -- Remove the trailing separator - if separator ~= "" then - result = result:sub(1, -#separator - 1) - end - - return result -end - -M.remove_first_value = function(tbl) - local sliced_table = {} - for i = 2, #tbl do - table.insert(sliced_table, tbl[i]) - end - - return sliced_table -end - M.read_file = function(file_path) local file = io.open(file_path, "r") if file == nil then @@ -180,41 +245,6 @@ M.uuid = function() end) end -M.join_tables = function(table1, table2) - for _, value in ipairs(table2) do - table.insert(table1, value) - end - - return table1 -end - -M.table_size = function(t) - local count = 0 - for _ in pairs(t) do - count = count + 1 - end - return count -end - -M.contains = function(array, search_value) - for _, value in ipairs(array) do - if value == search_value then - return true - end - end - return false -end - -M.extract = function(t, property) - local resultTable = {} - for _, value in ipairs(t) do - if value[property] then - table.insert(resultTable, value[property]) - end - end - return resultTable -end - M.remove_last_chunk = function(sentence) local words = {} for word in sentence:gmatch("%S+") do @@ -225,26 +255,6 @@ M.remove_last_chunk = function(sentence) return sentence_without_last end -M.get_first_chunk = function(sentence, divider) - local words = {} - for word in sentence:gmatch(divider or "%S+") do - table.insert(words, word) - end - return words[1] -end - -M.get_last_chunk = function(sentence, divider) - local words = {} - for word in sentence:gmatch(divider or "%S+") do - table.insert(words, word) - end - return words[#words] -end - -M.trim = function(s) - return s:gsub("^%s+", ""):gsub("%s+$", "") -end - M.get_line_content = function(bufnr, start) local current_buffer = vim.api.nvim_get_current_buf() local lines = vim.api.nvim_buf_get_lines(bufnr ~= nil and bufnr or current_buffer, start - 1, start, false) @@ -297,14 +307,6 @@ M.list_files_in_folder = function(folder_path) return result end -M.reverse = function(list) - local rev = {} - for i = #list, 1, -1 do - rev[#rev + 1] = list[i] - end - return rev -end - ---@class Hunk ---@field old_line integer ---@field old_range integer @@ -318,6 +320,8 @@ end M.parse_hunk_headers = function(file_path, base_branch) local hunks = {} + local Job = require("plenary.job") + local diff_job = Job:new({ command = "git", args = { "diff", "--minimal", "--unified=0", "--no-color", base_branch, "--", file_path }, @@ -430,7 +434,7 @@ M.get_lines_from_hunks = function(hunks, target_line, is_new) end ---Check if current mode is visual mode ----@return boolean true if current mode is visual mode +---@return boolean is_visual true if current mode is visual mode M.check_visual_mode = function() local mode = vim.api.nvim_get_mode().mode if mode ~= "v" and mode ~= "V" then @@ -442,7 +446,7 @@ end ---Return start line and end line of visual selection. ---Exists visual mode in order to access marks "<" , ">" ----@return integer,integer start line and end line +---@return integer start,integer end Start line and end line M.get_visual_selection_boundaries = function() vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", false, true, true), "nx", false) local start_line = vim.api.nvim_buf_get_mark(0, "<")[1] diff --git a/todo.md b/todo.md deleted file mode 100644 index 5c498bb..0000000 --- a/todo.md +++ /dev/null @@ -1,8 +0,0 @@ -## Todo - -- Screenshot folder in config (where the images will be kept) -- Within the Summary view, you can call the add_summary_image() command -- This command will open a UI picker to choose the file -- When you choose the file, we pass that file path to an API endpoint which uploads -the file and returns the JSON in the API here (https://docs.gitlab.com/ee/api/projects.html#upload-a-file) -- Then we write that into the Summary buffer at the current cursor