diff --git a/lua/neogit/lib/ansi.lua b/lua/neogit/lib/ansi.lua index ee7f1fc45..83125168e 100644 --- a/lua/neogit/lib/ansi.lua +++ b/lua/neogit/lib/ansi.lua @@ -38,6 +38,7 @@ function M.parse(str, opts) local out = {} for g in parsed:gmatch(".") do if g == mark then + assert(not vim.tbl_isempty(colored), "ANSI Parser didn't construct all graph parts: " .. str) table.insert(out, table.remove(colored, 1)) else table.insert(out, { text = g, color = "Gray", oid = oid }) diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 246b9d408..264e7a37b 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -278,7 +278,7 @@ end --- @return table, string|nil local function determine_order(options, graph) if - (graph or {})[1] + graph and not vim.tbl_contains(options, "--date-order") and not vim.tbl_contains(options, "--author-date-order") and not vim.tbl_contains(options, "--topo-order") @@ -332,7 +332,7 @@ local function format(show_signature) local signature_format = { [["signer":"%GS"]], [["signer_key":"%GK"]], - [["verification_flag":"%G?"]] + [["verification_flag":"%G?"]], } table.insert(template, table.concat(signature_format, ",")) @@ -395,7 +395,16 @@ function M.list(options, graph, files) .call() :trim().stdout - return parse_log(parse_json(output), graph or {}) + local commits = parse_json(output) + + local graph_output + if graph then + graph_output = require("neogit.lib.graph").build(commits) + else + graph_output = {} + end + + return parse_log(commits, graph_output) end ---Determines if commit a is an ancestor of commit b diff --git a/lua/neogit/lib/graph.lua b/lua/neogit/lib/graph.lua new file mode 100644 index 000000000..b16905d8e --- /dev/null +++ b/lua/neogit/lib/graph.lua @@ -0,0 +1,523 @@ +local M = {} + +local graph_error = "flog: internal error drawing graph" + +-- Init graph strings +local current_commit_str = "* " +local commit_branch_str = "│ " +local commit_empty_str = " " +local complex_merge_str_1 = "┬┊" +local complex_merge_str_2 = "╰┤" +local merge_all_str = "┼" +local merge_jump_str = "┊" +local merge_up_down_left_str = "┤" +local merge_up_down_right_str = "├" +local merge_up_down_str = "│" +local merge_up_left_right_str = "┴" +local merge_up_left_str = "╯" +local merge_up_right_str = "╰" +local merge_up_str = " " +local merge_down_left_right_str = "┬" +local merge_down_left_str = "╮" +local merge_down_right_str = "╭" +local merge_left_right_str = "─" +local merge_empty_str = " " +local missing_parent_str = "┊ " +local missing_parent_branch_str = "│ " +local missing_parent_empty_str = " " + +function M.build(commits) + commits = require("neogit.lib.util").filter_map(commits, function(item) + if item.oid then + return item + end + end) + + -- Init commit parsing data + local commit_hashes = {} + + for _, commit in ipairs(commits) do + commit_hashes[commit.oid] = 1 + end + + local vim_out = {} + local vim_out_index = 1 + + -- Init graph data + local branch_hashes = {} + local branch_indexes = {} + local nbranches = 0 + + -- Draw graph + + for _, commit in ipairs(commits) do + -- Get commit data + + local commit_hash = commit.oid + local parents = vim.split(commit.parent, " ") + local parent_hashes = {} + local nparents = #parents + local ncommit_lines = #commit.body + + for _, parent in ipairs(parents) do + parent_hashes[parent] = 1 + end + + -- Init commit output + + -- The prefix that goes before the first commit line + local commit_prefix = {} + -- The number of strings in commit lines + local ncommit_strings = 0 + -- The merge line that goes after the commit + local merge_line = {} + -- The complex merge line that goes after the merge + local complex_merge_line = {} + -- The number of strings in merge lines + local nmerge_strings = 0 + -- The two lines indicating missing parents after the complex line + local missing_parents_line_1 = {} + local missing_parents_line_2 = {} + -- The number of strings in missing parent lines + local nmissing_parents_strings = 0 + + -- Init visual data + + -- The number of columns in the commit output + local ncommit_cols = 0 + -- The number of visual parents + local nvisual_parents = 0 + -- The number of complex merges (octopus) + local ncomplex_merges = 0 + -- The number of missing parents + local nmissing_parents = 0 + + -- Init graph data + + -- The number of passed merges + local nmerges_left = 0 + -- The number of upcoming merges (parents + commit) + local nmerges_right = nparents + 1 + -- The index of the commit branch + local commit_branch_index = branch_indexes[commit_hash] + -- The index of the moved parent branch (there is only one) + local moved_parent_branch_index = nil + -- The number of branches on the commit line + local ncommit_branches = nbranches + (commit_branch_index and 0 or 1) + + -- Init indexes + + -- The current branch + local branch_index = 1 + -- The current parent + local parent_index = 1 + + -- Find the first empty parent + while parent_index <= nparents and branch_indexes[parents[parent_index]] do + parent_index = parent_index + 1 + end + + -- Traverse old and new branches + + while branch_index <= nbranches or nmerges_right > 0 do + -- Get branch data + + local branch_hash = branch_hashes[branch_index] + local is_commit = branch_index == commit_branch_index + + -- Set merge info before updates + + local merge_up = branch_hash or moved_parent_branch_index == branch_index + local merge_left = nmerges_left > 0 and nmerges_right > 0 + local is_complex = false + local is_missing_parent = false + + -- Handle commit + + if not branch_hash and not commit_branch_index then + -- Found empty branch and commit does not have a branch + -- Add the commit in the empty spot + + commit_branch_index = branch_index + is_commit = true + end + + if is_commit then + -- Count commit merge + nmerges_right = nmerges_right - 1 + nmerges_left = nmerges_left + 1 + + if branch_hash then + -- End of branch + + -- Remove branch + branch_hashes[commit_branch_index] = nil + branch_indexes[commit_hash] = nil + + -- Trim trailing empty branches + while nbranches > 0 and not branch_hashes[nbranches] do + nbranches = nbranches - 1 + end + + -- Clear branch hash + branch_hash = nil + end + + if parent_index > nparents and nmerges_right == 1 then + -- There is only one remaining parent, to the right + -- Move it under the commit + + -- Find parent to right + parent_index = nparents + while (branch_indexes[parents[parent_index]] or -1) < branch_index do + parent_index = parent_index - 1 + end + + -- Get parent data + local parent_hash = parents[parent_index] + local parent_branch_index = branch_indexes[parent_hash] + + -- Remove old parent branch + branch_hashes[parent_branch_index] = nil + branch_indexes[parent_hash] = nil + + -- Trim trailing empty branches + while nbranches > 0 and not branch_hashes[nbranches] do + nbranches = nbranches - 1 + end + + -- Record the old index + moved_parent_branch_index = parent_branch_index + + -- Count upcoming moved parent as another merge + nmerges_right = nmerges_right + 1 + end + end + + -- Handle parents + + if not branch_hash and parent_index <= nparents then + -- New parent + + -- Get parent data + local parent_hash = parents[parent_index] + + -- Set branch to parent + branch_indexes[parent_hash] = branch_index + branch_hashes[branch_index] = parent_hash + + -- Update branch has + branch_hash = parent_hash + + -- Update the number of branches + if branch_index > nbranches then + nbranches = branch_index + end + + -- Jump to next available parent + parent_index = parent_index + 1 + while parent_index <= nparents and branch_indexes[parents[parent_index]] do + parent_index = parent_index + 1 + end + + -- Count new parent merge + nmerges_right = nmerges_right - 1 + nmerges_left = nmerges_left + 1 + + -- Determine if parent is missing + if branch_hash and not commit_hashes[parent_hash] then + is_missing_parent = true + nmissing_parents = nmissing_parents + 1 + end + + -- Record the visual parent + nvisual_parents = nvisual_parents + 1 + elseif + branch_index == moved_parent_branch_index or (nmerges_right > 0 and parent_hashes[branch_hash]) + then + -- Existing parents + + -- Count existing parent merge + nmerges_right = nmerges_right - 1 + nmerges_left = nmerges_left + 1 + + -- Determine if parent has a complex merge + is_complex = merge_left and nmerges_right > 0 + if is_complex then + ncomplex_merges = ncomplex_merges + 1 + end + + -- Determine if parent is missing + if branch_hash and not commit_hashes[branch_hash] then + is_missing_parent = true + nmissing_parents = nmissing_parents + 1 + end + + if branch_index ~= moved_parent_branch_index then + -- Record the visual parent + nvisual_parents = nvisual_parents + 1 + end + end + + -- Draw commit lines + + if branch_index <= ncommit_branches then + -- Update commit visual info + + ncommit_cols = ncommit_cols + 2 + ncommit_strings = ncommit_strings + 1 + + if is_commit then + -- Draw current commit + + commit_prefix[ncommit_strings] = current_commit_str + elseif merge_up then + -- Draw unrelated branch + + commit_prefix[ncommit_strings] = commit_branch_str + else + -- Draw empty branch + + commit_prefix[ncommit_strings] = commit_empty_str + end + end + + -- Update merge visual info + + nmerge_strings = nmerge_strings + 1 + + -- Draw merge lines + + if is_complex then + -- Draw merge lines for complex merge + + merge_line[nmerge_strings] = complex_merge_str_1 + complex_merge_line[nmerge_strings] = complex_merge_str_2 + else + -- Draw non-complex merge lines + + -- Update merge info after drawing commit + + merge_up = merge_up or is_commit or branch_index == moved_parent_branch_index + local merge_right = nmerges_left > 0 and nmerges_right > 0 + + -- Draw left character + + if branch_index > 1 then + if merge_left then + -- Draw left merge line + merge_line[nmerge_strings] = merge_left_right_str + else + -- No merge to left + -- Draw empty space + merge_line[nmerge_strings] = merge_empty_str + end + -- Complex merge line always has empty space here + complex_merge_line[nmerge_strings] = merge_empty_str + + -- Update visual merge info + + nmerge_strings = nmerge_strings + 1 + end + + -- Draw right character + + if merge_up then + if branch_hash then + if merge_left then + if merge_right then + if is_commit then + -- Merge up, down, left, right + merge_line[nmerge_strings] = merge_all_str + else + -- Jump over + merge_line[nmerge_strings] = merge_jump_str + end + else + -- Merge up, down, left + merge_line[nmerge_strings] = merge_up_down_left_str + end + else + if merge_right then + -- Merge up, down, right + merge_line[nmerge_strings] = merge_up_down_right_str + else + -- Merge up, down + merge_line[nmerge_strings] = merge_up_down_str + end + end + else + if merge_left then + if merge_right then + -- Merge up, left, right + merge_line[nmerge_strings] = merge_up_left_right_str + else + -- Merge up, left + merge_line[nmerge_strings] = merge_up_left_str + end + else + if merge_right then + -- Merge up, right + merge_line[nmerge_strings] = merge_up_right_str + else + -- Merge up + merge_line[nmerge_strings] = merge_up_str + end + end + end + else + if branch_hash then + if merge_left then + if merge_right then + -- Merge down, left, right + merge_line[nmerge_strings] = merge_down_left_right_str + else + -- Merge down, left + merge_line[nmerge_strings] = merge_down_left_str + end + else + if merge_right then + -- Merge down, right + merge_line[nmerge_strings] = merge_down_right_str + else + -- Merge down + -- Not possible to merge down only + error(graph_error) + end + end + else + if merge_left then + if merge_right then + -- Merge left, right + merge_line[nmerge_strings] = merge_left_right_str + else + -- Merge left + -- Not possible to merge left only + error(graph_error) + end + else + if merge_right then + -- Merge right + -- Not possible to merge right only + error(graph_error) + else + -- No merges + merge_line[nmerge_strings] = merge_empty_str + end + end + end + end + + -- Draw complex right char + + if branch_hash then + complex_merge_line[nmerge_strings] = merge_up_down_str + else + complex_merge_line[nmerge_strings] = merge_empty_str + end + end + + -- Update visual missing parents info + + nmissing_parents_strings = nmissing_parents_strings + 1 + + -- Draw missing parents lines + + if is_missing_parent then + missing_parents_line_1[nmissing_parents_strings] = missing_parent_str + missing_parents_line_2[nmissing_parents_strings] = missing_parent_empty_str + elseif branch_hash then + missing_parents_line_1[nmissing_parents_strings] = missing_parent_branch_str + missing_parents_line_2[nmissing_parents_strings] = missing_parent_branch_str + else + missing_parents_line_1[nmissing_parents_strings] = missing_parent_empty_str + missing_parents_line_2[nmissing_parents_strings] = missing_parent_empty_str + end + + -- Remove missing parent + + if is_missing_parent and branch_index ~= moved_parent_branch_index then + -- Remove branch + branch_hashes[branch_index] = nil + branch_indexes[branch_hash] = nil + + -- Trim trailing empty branches + while nbranches > 0 and not branch_hashes[nbranches] do + nbranches = nbranches - 1 + end + end + + -- Increment + + branch_index = branch_index + 1 + end + + -- Output + + -- Calculate whether certain lines should be outputted + + local should_out_merge = ( + nparents > 1 + or moved_parent_branch_index + or (nparents == 0 and nbranches == 0) + or (nparents == 1 and branch_indexes[parents[1]] ~= commit_branch_index) + ) + + local should_out_complex = should_out_merge and ncomplex_merges > 0 + local should_out_missing_parents = nmissing_parents > 0 + + -- Initialize commit objects + -- local vim_commit_body = {} + local vim_commit_suffix = {} + local vim_commit_suffix_index = 1 + + vim_out[vim_out_index] = { text = table.concat(commit_prefix, ""), color = "Purple", oid = commit_hash } + vim_out_index = vim_out_index + 1 + + -- Add merge lines + if should_out_merge then + vim_commit_suffix[vim_commit_suffix_index] = table.concat(merge_line, "") + vim_out[vim_out_index] = { text = vim_commit_suffix[vim_commit_suffix_index], color = "Purple" } + + vim_out_index = vim_out_index + 1 + vim_commit_suffix_index = vim_commit_suffix_index + 1 + + if should_out_complex then + vim_commit_suffix[vim_commit_suffix_index] = table.concat(complex_merge_line, "") + vim_out[vim_out_index] = { text = vim_commit_suffix[vim_commit_suffix_index], color = "Purple" } + + vim_out_index = vim_out_index + 1 + vim_commit_suffix_index = vim_commit_suffix_index + 1 + end + end + + -- Add missing parents lines + if should_out_missing_parents then + vim_commit_suffix[vim_commit_suffix_index] = table.concat(missing_parents_line_1, "") + vim_out[vim_out_index] = { text = vim_commit_suffix[vim_commit_suffix_index], color = "Purple" } + + vim_out_index = vim_out_index + 1 + vim_commit_suffix_index = vim_commit_suffix_index + 1 + + vim_commit_suffix[vim_commit_suffix_index] = table.concat(missing_parents_line_2, "") + vim_out[vim_out_index] = { text = vim_commit_suffix[vim_commit_suffix_index], color = "Purple" } + + vim_out_index = vim_out_index + 1 + vim_commit_suffix_index = vim_commit_suffix_index + 1 + end + end + + local graph = {} + for _, line in ipairs(vim_out) do + local g = {} + for _, c in ipairs(vim.split(line.text, "")) do + table.insert(g, { text = c, color = line.color, oid = line.oid }) + end + + table.insert(graph, g) + end + + return graph +end + +return M diff --git a/lua/neogit/lib/ui/component.lua b/lua/neogit/lib/ui/component.lua index d32fe9e9f..e23b0d3d3 100644 --- a/lua/neogit/lib/ui/component.lua +++ b/lua/neogit/lib/ui/component.lua @@ -61,7 +61,8 @@ end function Component:get_width() if self.tag == "text" then - local width = vim.fn.strdisplaywidth(self.value) + local width = string.len(self.value) + -- local width = vim.fn.strdisplaywidth(self.value) if self.options.align_right then return width + (self.options.align_right - width) else diff --git a/lua/neogit/popups/log/actions.lua b/lua/neogit/popups/log/actions.lua index 0fa3a9006..ab4a9d17e 100644 --- a/lua/neogit/popups/log/actions.lua +++ b/lua/neogit/popups/log/actions.lua @@ -30,7 +30,7 @@ end local function commits(popup, flags) return git.log.list( util.merge(popup:get_arguments(), flags), - maybe_graph(popup, flags), + popup:get_internal_arguments().graph, popup.state.env.files ) end