diff --git a/cmd/app/git/git.go b/cmd/app/git/git.go index b4e36f8..1fcc708 100644 --- a/cmd/app/git/git.go +++ b/cmd/app/git/git.go @@ -36,7 +36,7 @@ Extracts information about the current repository and returns it to the client for initialization. The current directory must be a valid Gitlab project and the branch must be a feature branch */ -func NewGitData(remote string, g GitManager) (GitData, error) { +func NewGitData(remote string, gitlabUrl string, g GitManager) (GitData, error) { err := g.RefreshProjectInfo(remote) if err != nil { return GitData{}, fmt.Errorf("could not get latest information from remote: %v", err) @@ -65,7 +65,14 @@ func NewGitData(remote string, g GitManager) (GitData, error) { return GitData{}, fmt.Errorf("invalid git URL format: %s", url) } - namespace := matches[1] + // remove part of the hostname from the parsed namespace + url_re := regexp.MustCompile(`[^\/]\/([^\/].*)$`) + url_matches := url_re.FindStringSubmatch(gitlabUrl) + var namespace string = matches[1] + if len(url_matches) == 2 { + namespace = strings.TrimLeft(strings.TrimPrefix(namespace, url_matches[1]), "/") + } + projectName := matches[2] branchName, err := g.GetCurrentBranchNameFromNativeGitCmd() diff --git a/cmd/app/git/git_test.go b/cmd/app/git/git_test.go index c2f8804..9158c90 100644 --- a/cmd/app/git/git_test.go +++ b/cmd/app/git/git_test.go @@ -30,6 +30,7 @@ func (f FakeGitManager) GetProjectUrlFromNativeGitCmd(string) (url string, err e type TestCase struct { desc string + url string branch string projectName string namespace string @@ -40,6 +41,7 @@ func TestExtractGitInfo_Success(t *testing.T) { testCases := []TestCase{ { desc: "Project configured in SSH under a single folder", + url: "git@custom-gitlab.com", remote: "git@custom-gitlab.com:namespace-1/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -47,6 +49,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in SSH under a single folder without .git extension", + url: "git@custom-gitlab.com", remote: "git@custom-gitlab.com:namespace-1/project-name", branch: "feature/abc", projectName: "project-name", @@ -54,6 +57,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in SSH under one nested folder", + url: "git@custom-gitlab.com", remote: "git@custom-gitlab.com:namespace-1/namespace-2/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -61,6 +65,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in SSH under two nested folders", + url: "git@custom-gitlab.com", remote: "git@custom-gitlab.com:namespace-1/namespace-2/namespace-3/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -68,6 +73,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in SSH:// under a single folder", + url: "ssh://custom-gitlab.com", remote: "ssh://custom-gitlab.com/namespace-1/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -75,6 +81,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in SSH:// under a single folder without .git extension", + url: "ssh://custom-gitlab.com", remote: "ssh://custom-gitlab.com/namespace-1/project-name", branch: "feature/abc", projectName: "project-name", @@ -82,6 +89,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in SSH:// under two nested folders", + url: "ssh://custom-gitlab.com", remote: "ssh://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -89,13 +97,23 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in SSH:// and have a custom port", + url: "ssh://custom-gitlab.com", remote: "ssh://custom-gitlab.com:2222/namespace-1/project-name", branch: "feature/abc", projectName: "project-name", namespace: "namespace-1", }, + { + desc: "Project configured in SSH:// and have a custom port (with gitlab url namespace)", + url: "ssh://custom-gitlab.com/a", + remote: "ssh://custom-gitlab.com:2222/a/namespace-1/project-name", + branch: "feature/abc", + projectName: "project-name", + namespace: "namespace-1", + }, { desc: "Project configured in HTTP and under a single folder without .git extension", + url: "http://custom-gitlab.com", remote: "http://custom-gitlab.com/namespace-1/project-name", branch: "feature/abc", projectName: "project-name", @@ -103,6 +121,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in HTTP and under a single folder without .git extension (with embedded credentials)", + url: "http://custom-gitlab.com", remote: "http://username:password@custom-gitlab.com/namespace-1/project-name", branch: "feature/abc", projectName: "project-name", @@ -110,6 +129,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in HTTPS and under a single folder", + url: "https://custom-gitlab.com", remote: "https://custom-gitlab.com/namespace-1/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -117,6 +137,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in HTTPS and under a single folder (with embedded credentials)", + url: "https://custom-gitlab.com", remote: "https://username:password@custom-gitlab.com/namespace-1/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -124,6 +145,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in HTTPS and under a nested folder", + url: "https://custom-gitlab.com", remote: "https://custom-gitlab.com/namespace-1/namespace-2/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -131,6 +153,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in HTTPS and under a nested folder (with embedded credentials)", + url: "https://custom-gitlab.com", remote: "https://username:password@custom-gitlab.com/namespace-1/namespace-2/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -138,6 +161,7 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in HTTPS and under two nested folders", + url: "https://custom-gitlab.com", remote: "https://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git", branch: "feature/abc", projectName: "project-name", @@ -145,11 +169,28 @@ func TestExtractGitInfo_Success(t *testing.T) { }, { desc: "Project configured in HTTPS and under two nested folders (with embedded credentials)", + url: "https://custom-gitlab.com", remote: "https://username:password@custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git", branch: "feature/abc", projectName: "project-name", namespace: "namespace-1/namespace-2/namespace-3", }, + { + desc: "Project configured in HTTPS and under one nested folders (with gitlab url namespace)", + url: "https://custom-gitlab.com/gitlab", + remote: "https://username:password@custom-gitlab.com/gitlab/namespace-2/namespace-3/project-name.git", + branch: "feature/abc", + projectName: "project-name", + namespace: "namespace-2/namespace-3", + }, + { + desc: "Project configured in HTTPS and under one nested folders (with gitlab url namespace + extra slash)", + url: "https://custom-gitlab.com/gitlab/", + remote: "https://username:password@custom-gitlab.com/gitlab/namespace-2/namespace-3/project-name.git", + branch: "feature/abc", + projectName: "project-name", + namespace: "namespace-2/namespace-3", + }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { @@ -159,7 +200,7 @@ func TestExtractGitInfo_Success(t *testing.T) { BranchName: tC.branch, RemoteUrl: tC.remote, } - data, err := NewGitData(tC.remote, g) + data, err := NewGitData(tC.remote, tC.url, g) if err != nil { t.Errorf("No error was expected, got %s", err) } @@ -204,7 +245,7 @@ func TestExtractGitInfo_FailToGetProjectRemoteUrl(t *testing.T) { g := failingUrlManager{ errMsg: tC.errMsg, } - _, err := NewGitData("", g) + _, err := NewGitData("", "", g) if err == nil { t.Errorf("Expected an error, got none") } @@ -236,7 +277,7 @@ func TestExtractGitInfo_FailToGetCurrentBranchName(t *testing.T) { }, errMsg: tC.errMsg, } - _, err := NewGitData("", g) + _, err := NewGitData("", "", g) if err == nil { t.Errorf("Expected an error, got none") } diff --git a/cmd/main.go b/cmd/main.go index b46a880..0bfb652 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -26,7 +26,7 @@ func main() { } gitManager := git.Git{} - gitData, err := git.NewGitData(pluginOptions.ConnectionSettings.Remote, gitManager) + gitData, err := git.NewGitData(pluginOptions.ConnectionSettings.Remote, pluginOptions.GitlabUrl, gitManager) if err != nil { log.Fatalf("Failure initializing plugin: %v", err) diff --git a/config/emojis.json b/config/emojis.json index e349f87..158a68f 100644 --- a/config/emojis.json +++ b/config/emojis.json @@ -9661,6 +9661,25 @@ ], "moji": "😅" }, + "tada": { + "unicode": "1F389", + "unicode_alternates": [], + "name": "party popper as a 'tada' celebration", + "shortname": ":tada:", + "category": "people", + "aliases": [ + ":party_popper:" + ], + "aliases_ascii": [], + "keywords": [ + "celebrate", + "celebration", + "hooray", + "hurrah", + "hurray" + ], + "moji": "🎉" + }, "thermometer_face": { "unicode": "1F912", "unicode_alternates": [], diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua index a2f3d1f..77045b1 100644 --- a/lua/gitlab/actions/comment.lua +++ b/lua/gitlab/actions/comment.lua @@ -41,13 +41,7 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion local body = { discussion_id = discussion_id, reply = text, draft = is_draft } job.run_job("/mr/reply", "POST", body, function() u.notify("Sent reply!", vim.log.levels.INFO) - if is_draft then - draft_notes.load_draft_notes(function() - discussions.rebuild_view(unlinked) - end) - else - discussions.rebuild_view(unlinked) - end + discussions.rebuild_view(unlinked) end) return end @@ -69,8 +63,6 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion return end - vim.print("Here: ", unlinked, discussion_id) - local reviewer_data = reviewer.get_reviewer_data() if reviewer_data == nil then u.notify("Error getting reviewer data", vim.log.levels.ERROR) @@ -102,7 +94,7 @@ local confirm_create_comment = function(text, visual_range, unlinked, discussion job.run_job("/mr/draft_notes/", "POST", body, function() u.notify("Draft reply created!", vim.log.levels.INFO) draft_notes.load_draft_notes(function() - discussions.rebuild_view(false, true) + discussions.rebuild_view(unlinked) end) end) return @@ -166,7 +158,7 @@ end ---@param opts LayoutOpts ---@return NuiLayout|nil M.create_comment_layout = function(opts) - if opts.unlinked ~= true then + if opts.unlinked ~= true and opts.discussion_id == nil then -- Check that diffview is initialized if reviewer.tabnr == nil then u.notify("Reviewer must be initialized first", vim.log.levels.ERROR) diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 571fe47..40f03ac 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -139,7 +139,7 @@ M.open = function(callback) local current_window = vim.api.nvim_get_current_win() -- Save user's current window in case they switched while content was loading vim.api.nvim_set_current_win(M.split.winid) - common.switch_can_edit_bufs(true, M.linked_bufnr, M.unliked_bufnr) + common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr) M.rebuild_discussion_tree() M.rebuild_unlinked_discussion_tree() @@ -432,6 +432,9 @@ M.rebuild_discussion_tree = function() if M.linked_bufnr == nil then return end + + local current_node = discussions_tree.get_node_at_cursor(M.discussion_tree, M.last_node_at_cursor) + local expanded_node_ids = M.gather_expanded_node_ids(M.discussion_tree) common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr) @@ -447,12 +450,14 @@ M.rebuild_discussion_tree = function() bufnr = M.linked_bufnr, prepare_node = tree_utils.nui_tree_prepare_node, }) + -- Re-expand already expanded nodes for _, id in ipairs(expanded_node_ids) do tree_utils.open_node_by_id(discussion_tree, id) end - discussion_tree:render() + discussions_tree.restore_cursor_position(M.split.winid, discussion_tree, current_node) + M.set_tree_keymaps(discussion_tree, M.linked_bufnr, false) M.discussion_tree = discussion_tree common.switch_can_edit_bufs(false, M.linked_bufnr, M.unlinked_bufnr) @@ -466,6 +471,9 @@ M.rebuild_unlinked_discussion_tree = function() if M.unlinked_bufnr == nil then return end + + local current_node = discussions_tree.get_node_at_cursor(M.unlinked_discussion_tree, M.last_node_at_cursor) + local expanded_node_ids = M.gather_expanded_node_ids(M.unlinked_discussion_tree) common.switch_can_edit_bufs(true, M.linked_bufnr, M.unlinked_bufnr) vim.api.nvim_buf_set_lines(M.unlinked_bufnr, 0, -1, false, {}) @@ -487,6 +495,7 @@ M.rebuild_unlinked_discussion_tree = function() tree_utils.open_node_by_id(unlinked_discussion_tree, id) end unlinked_discussion_tree:render() + discussions_tree.restore_cursor_position(M.split.winid, unlinked_discussion_tree, current_node) M.set_tree_keymaps(unlinked_discussion_tree, M.unlinked_bufnr, true) M.unlinked_discussion_tree = unlinked_discussion_tree @@ -535,6 +544,14 @@ M.create_split_and_bufs = function() buffer = linked_bufnr, callback = function() M.last_row, M.last_column = unpack(vim.api.nvim_win_get_cursor(0)) + M.last_node_at_cursor = M.discussion_tree and M.discussion_tree:get_node() or nil + end, + }) + + vim.api.nvim_create_autocmd("WinLeave", { + buffer = unlinked_bufnr, + callback = function() + M.last_node_at_cursor = M.unlinked_discussion_tree and M.unlinked_discussion_tree:get_node() or nil end, }) diff --git a/lua/gitlab/actions/discussions/tree.lua b/lua/gitlab/actions/discussions/tree.lua index 6810c35..a34f511 100644 --- a/lua/gitlab/actions/discussions/tree.lua +++ b/lua/gitlab/actions/discussions/tree.lua @@ -422,12 +422,36 @@ M.toggle_nodes = function(winid, tree, unlinked, opts) M.restore_cursor_position(winid, tree, current_node, root_node) end +-- Get current node for restoring cursor position +---@param tree NuiTree The inline discussion tree or the unlinked discussion tree +---@param last_node NuiTree.Node|nil The last active discussion tree node in case we are not in any of the discussion trees +M.get_node_at_cursor = function(tree, last_node) + if tree == nil then + return + end + if vim.api.nvim_get_current_win() == vim.fn.win_findbuf(tree.bufnr)[1] then + return tree:get_node() + else + return last_node + end +end + ---Restore cursor position to the original node if possible +---@param winid integer Window number of the discussions split +---@param tree NuiTree The inline discussion tree or the unlinked discussion tree +---@param original_node NuiTree.Node|nil The last node with the cursor +---@param root_node NuiTree.Node|nil The root node of the last node with the cursor M.restore_cursor_position = function(winid, tree, original_node, root_node) + if original_node == nil or tree == nil then + return + end local _, line_number = tree:get_node("-" .. tostring(original_node.id)) - -- If current_node is has been collapsed, get line number of root node instead - if line_number == nil and root_node then - _, line_number = tree:get_node("-" .. tostring(root_node.id)) + -- If current_node has been collapsed, try to get line number of root node instead + if line_number == nil then + root_node = root_node and root_node or common.get_root_node(tree, original_node) + if root_node ~= nil then + _, line_number = tree:get_node("-" .. tostring(root_node.id)) + end end if line_number ~= nil then vim.api.nvim_win_set_cursor(winid, { line_number, 0 }) diff --git a/lua/gitlab/actions/summary.lua b/lua/gitlab/actions/summary.lua index dfde1d7..6d034be 100644 --- a/lua/gitlab/actions/summary.lua +++ b/lua/gitlab/actions/summary.lua @@ -93,7 +93,7 @@ M.build_info_lines = function() local options = { author = { title = "Author", content = "@" .. info.author.username .. " (" .. info.author.name .. ")" }, created_at = { title = "Created", content = u.format_to_local(info.created_at, vim.fn.strftime("%z")) }, - updated_at = { title = "Updated", content = u.format_to_local(info.updated_at, vim.fn.strftime("%z")) }, + updated_at = { title = "Updated", content = u.time_since(info.updated_at) }, detailed_merge_status = { title = "Status", content = info.detailed_merge_status }, draft = { title = "Draft", content = (info.draft and "Yes" or "No") }, conflicts = { title = "Merge Conflicts", content = (info.has_conflicts and "Yes" or "No") }, diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua index 2729da1..1404fbf 100644 --- a/lua/gitlab/utils/init.lua +++ b/lua/gitlab/utils/init.lua @@ -254,6 +254,20 @@ M.format_to_local = function(date_string, offset) local tzOffsetSign, tzOffsetHour, tzOffsetMin year, month, day, hour, min, sec, _, tzOffsetSign, tzOffsetHour, tzOffsetMin = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)([%+%-])(%d%d):(%d%d)") + + -- ISO 8601 format with just "Z" (aka no time offset) + -- 2021-01-01T00:00:00Z + if year == nil then + year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z") + tzOffsetSign = "-" + tzOffsetHour = "00" + tzOffsetMin = "00" + end + + if year == nil then + return "Date Unparseable" + end + tzOffset = tzOffsetSign .. tzOffsetHour .. tzOffsetMin end diff --git a/tests/spec/util_spec.lua b/tests/spec/util_spec.lua index 629a2b8..0a569a7 100644 --- a/tests/spec/util_spec.lua +++ b/tests/spec/util_spec.lua @@ -77,6 +77,13 @@ describe("utils/init.lua", function() local want = "November 19, 2011" assert.are.same(want, got) end) + + it("Parses TZ w/out offset (relative)", function() + local stamp = "2023-11-14T18:44:02Z" + local got = u.time_since(stamp, current_date) + local want = "5 days ago" + assert.are.same(want, got) + end) end) describe("remove_first_value", function() @@ -209,6 +216,7 @@ describe("utils/init.lua", function() { "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" }, + { "2016-11-22T1:25:09Z", "-0000", "11/22/2016 at 01:25" }, } for _, val in ipairs(tests) do local got = u.format_to_local(val[1], val[2])