From 14bae6571e28b6d5e46e773e34a2d43559bd9730 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 7 Nov 2023 13:39:13 +0100 Subject: [PATCH 01/21] Parse log output as JSON --- lua/neogit/buffers/common.lua | 2 +- lua/neogit/lib/git/log.lua | 112 ++++++++++++---------------------- 2 files changed, 41 insertions(+), 73 deletions(-) diff --git a/lua/neogit/buffers/common.lua b/lua/neogit/buffers/common.lua index d714ee6cf..2da20d348 100644 --- a/lua/neogit/buffers/common.lua +++ b/lua/neogit/buffers/common.lua @@ -206,7 +206,7 @@ M.CommitEntry = Component.new(function(commit, args) row( util.merge({ text(commit.oid:sub(1, 7), { - highlight = commit.signature_code and highlight_for_signature[commit.signature_code] or "Comment", + highlight = commit.verification_flag and highlight_for_signature[commit.verification_flag] or "Comment", }), text(" "), }, graph, { text(" ") }, ref, { text(commit.description[1]) }), diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index ac7e114b1..2bfca6723 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -167,31 +167,14 @@ function M.parse(raw) end local function make_commit(entry, graph) - local hash, subject, author_name, rel_date, ref_name, author_date, committer_name, committer_date, committer_email, author_email, body, signature_code = - unpack(entry) + entry.graph = graph + entry.description = { entry.subject, entry.body } - if rel_date then - rel_date, _ = rel_date:gsub(" ago$", "") + if entry.rel_date then + entry.rel_date, _ = entry.rel_date:gsub(" ago$", "") end - return { - graph = graph, - oid = hash, - description = { subject, body }, - author_name = author_name, - author_email = author_email, - author_date = author_date, - rel_date = rel_date, - ref_name = ref_name, - committer_date = committer_date, - committer_name = committer_name, - committer_email = committer_email, - body = body, - signature_code = signature_code, - -- TODO: Remove below here - hash = hash, - message = subject, - } + return entry end ---@param output table @@ -210,7 +193,7 @@ local function parse_log(output, graph) local commit_lookup = {} for i = 1, #output do - commit_lookup[output[i][1]] = output[i] + commit_lookup[output[i]["oid"]] = output[i] end for i = 1, #graph do @@ -231,36 +214,6 @@ local function parse_log(output, graph) return commits end ----Parses log output to a table ----@param output table ----@return string[][] -local function split_output(output) - output = table.concat(output, "\n") - output = vim.split(output, "\31", { trimempty = true }) - output = util.map(output, function(line) - return vim.split(vim.trim(line:gsub("\n", " ")), "\30") - end) - - return output -end - -local format_args = { - "%H", -- Full Hash - "%s", -- Subject - "%aN", -- Author Name - "%cr", -- Commit Date (Relative) - "%D", -- Ref Name - "%ad", -- Author Date - "%cN", -- Committer Name - "%cd", -- Committer Date - "%ce", -- Committer Email - "%ae", -- Author Email - "%b", -- Body - "%G?", -- Signature status - "%x1F", -- Entry delimiter to split on (dec \31) -} -local format_delimiter = "%x1E" -- Field delimiter to split on (dec \30) - --- Ensure a max is passed to the list function to prevent accidentally getting thousands of results. ---@param options table ---@return table @@ -336,21 +289,6 @@ local function determine_order(options, graph) return options end ---- Parses the arguments needed for the format output of git log ----@param show_signature boolean Should '%G?' be omitted from the arguments ----@return string Concatenated format arguments -local function parse_log_format(show_signature) - if not show_signature then - return table.concat( - vim.tbl_filter(function(value) - return value ~= "%G?" - end, format_args), - format_delimiter - ) - end - return table.concat(format_args, format_delimiter) -end - ---@param options table|nil ---@param files? table ---@param color boolean @@ -367,27 +305,57 @@ function M.graph(options, files, color) end) end + +local format = '{"oid":"%H","abbreviated_commit":"%h","tree":"%T","abbreviated_tree":"%t","parent":"%P","abbreviated_parent":"%p","ref_name":"%D","encoding":"%e","subject":"%s","sanitized_subject_line":"%f","body":"%b","commit_notes":"%N","verification_flag":"%G?","signer":"%GS","signer_key":"%GK","author_name":"%aN","author_email":"%aE","author_date":"%aD","committer_name":"%cN","committer_email":"%cE","committer_date":"%cD","rel_date":"%cr"},' + +local function parse_json(output) + -- Wrap list of commits in an Array + local commits = "[" .. table.concat(output, "\\n") .. "]" + + -- Remove trailing comma from last object in array + commits, _ = commits:gsub(",]", "]") + + -- Remove escaped newlines from in-between objects + commits, _ = commits:gsub("},\\n{", "},{") + + -- Escape any double-quote characters in the body + commits, _ = commits:gsub([[(,"body":")(.-)(","commit_notes":")]], function(before, body, after) + body, _ = body:gsub([["]], [[\"]]) + return table.concat({ before, body, after }, "") + end) + + -- Escape any double-quote characters in the subject + commits, _ = commits:gsub([[(,"subject":")(.-)(","sanitized_subject_line":")]], function(before, body, after) + body, _ = body:gsub([["]], [[\"]]) + return table.concat({ before, body, after }, "") + end) + + local ok, result = pcall(vim.json.decode, commits, { luanil = { object = true, array = true } }) + assert(ok, "Failed to parse log json!") + + return result +end + ---@param options? string[] ---@param graph? table ---@param files? table ---@return CommitLogEntry[] function M.list(options, graph, files) files = files or {} - local signature = false options = ensure_max(options or {}) options = determine_order(options, graph) - options, signature = show_signature(options) local output = cli.log - .format(parse_log_format(signature)) + .format(format) .arg_list(options) .files(unpack(files)) .show_popup(false) .call() :trim().stdout - return parse_log(split_output(output), graph or {}) + + return parse_log(parse_json(output), graph or {}) end ---Determines if commit a is an ancestor of commit b From 158d8b05da72b52e17f51f2347e90da641495b75 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 7 Nov 2023 13:45:28 +0100 Subject: [PATCH 02/21] Annotations --- lua/neogit/lib/git/log.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 2bfca6723..de66df583 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -308,6 +308,8 @@ end local format = '{"oid":"%H","abbreviated_commit":"%h","tree":"%T","abbreviated_tree":"%t","parent":"%P","abbreviated_parent":"%p","ref_name":"%D","encoding":"%e","subject":"%s","sanitized_subject_line":"%f","body":"%b","commit_notes":"%N","verification_flag":"%G?","signer":"%GS","signer_key":"%GK","author_name":"%aN","author_email":"%aE","author_date":"%aD","committer_name":"%cN","committer_email":"%cE","committer_date":"%cD","rel_date":"%cr"},' +---@param output table +---@return table local function parse_json(output) -- Wrap list of commits in an Array local commits = "[" .. table.concat(output, "\\n") .. "]" From 975a0d46ae008d36d1dc0be501c355dba39d638a Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 7 Nov 2023 14:02:34 +0100 Subject: [PATCH 03/21] Cleanup --- lua/neogit/lib/git/log.lua | 42 +++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index de66df583..f825dec30 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -305,8 +305,41 @@ function M.graph(options, files, color) end) end +local function format(show_signature) + local template = { + [["oid":"%H"]], + [["abbreviated_commit":"%h"]], + [["tree":"%T"]], + [["abbreviated_tree":"%t"]], + [["parent":"%P"]], + [["abbreviated_parent":"%p"]], + [["ref_name":"%D"]], + [["encoding":"%e"]], + [["subject":"%s"]], + [["sanitized_subject_line":"%f"]], + [["body":"%b"]], + [["commit_notes":"%N"]], + [["author_name":"%aN"]], + [["author_email":"%aE"]], + [["author_date":"%aD"]], + [["committer_name":"%cN"]], + [["committer_email":"%cE"]], + [["committer_date":"%cD"]], + [["rel_date":"%cr"]], + } + + if show_signature then + local signature_format = { + [["signer":"%GS"]], + [["signer_key":"%GK"]], + [["verification_flag":"%G?"]] + } -local format = '{"oid":"%H","abbreviated_commit":"%h","tree":"%T","abbreviated_tree":"%t","parent":"%P","abbreviated_parent":"%p","ref_name":"%D","encoding":"%e","subject":"%s","sanitized_subject_line":"%f","body":"%b","commit_notes":"%N","verification_flag":"%G?","signer":"%GS","signer_key":"%GK","author_name":"%aN","author_email":"%aE","author_date":"%aD","committer_name":"%cN","committer_email":"%cE","committer_date":"%cD","rel_date":"%cr"},' + table.insert(template, table.concat(signature_format, ",")) + end + + return string.format("{%s},", table.concat(template, ",")) +end ---@param output table ---@return table @@ -333,7 +366,7 @@ local function parse_json(output) end) local ok, result = pcall(vim.json.decode, commits, { luanil = { object = true, array = true } }) - assert(ok, "Failed to parse log json!") + assert(ok, "Failed to parse log json!: " .. result) return result end @@ -345,11 +378,14 @@ end function M.list(options, graph, files) files = files or {} + local signature = false + options = ensure_max(options or {}) options = determine_order(options, graph) + options, signature = show_signature(options) local output = cli.log - .format(format) + .format(format(signature)) .arg_list(options) .files(unpack(files)) .show_popup(false) From d56e1ca5f66f5aeed19bbcf2cf24138330c288d0 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 7 Nov 2023 14:03:23 +0100 Subject: [PATCH 04/21] Need to wrap this so we can concatenate the result --- lua/neogit/lib/git/log.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index f825dec30..2257b0cf8 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -366,7 +366,9 @@ local function parse_json(output) end) local ok, result = pcall(vim.json.decode, commits, { luanil = { object = true, array = true } }) - assert(ok, "Failed to parse log json!: " .. result) + if not ok then + assert(ok, "Failed to parse log json!: " .. result) + end return result end From b23bb99df850c6418e46afeba581fce22b49cebd Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 7 Nov 2023 23:54:39 +0100 Subject: [PATCH 05/21] Escape escape characters --- lua/neogit/lib/git/log.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 2257b0cf8..f5bf5b199 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -353,14 +353,16 @@ local function parse_json(output) -- Remove escaped newlines from in-between objects commits, _ = commits:gsub("},\\n{", "},{") - -- Escape any double-quote characters in the body + -- Escape any double-quote characters, or escape codes, in the body commits, _ = commits:gsub([[(,"body":")(.-)(","commit_notes":")]], function(before, body, after) + body, _ = body:gsub([[\]], [[\\]]) body, _ = body:gsub([["]], [[\"]]) return table.concat({ before, body, after }, "") end) - -- Escape any double-quote characters in the subject + -- Escape any double-quote characters, or escape codes, in the subject commits, _ = commits:gsub([[(,"subject":")(.-)(","sanitized_subject_line":")]], function(before, body, after) + body, _ = body:gsub([[\]], [[\\]]) body, _ = body:gsub([["]], [[\"]]) return table.concat({ before, body, after }, "") end) @@ -394,7 +396,6 @@ function M.list(options, graph, files) .call() :trim().stdout - return parse_log(parse_json(output), graph or {}) end From b7b9a831d81d66e20d15902a6f60789c8f0721d2 Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 8 Nov 2023 11:13:16 +0100 Subject: [PATCH 06/21] Use builtin function to escape string --- lua/neogit/lib/git/log.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index f5bf5b199..246b9d408 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -355,17 +355,16 @@ local function parse_json(output) -- Escape any double-quote characters, or escape codes, in the body commits, _ = commits:gsub([[(,"body":")(.-)(","commit_notes":")]], function(before, body, after) - body, _ = body:gsub([[\]], [[\\]]) - body, _ = body:gsub([["]], [[\"]]) - return table.concat({ before, body, after }, "") + return table.concat({ before, vim.fn.escape(body, [[\"]]), after }, "") end) -- Escape any double-quote characters, or escape codes, in the subject - commits, _ = commits:gsub([[(,"subject":")(.-)(","sanitized_subject_line":")]], function(before, body, after) - body, _ = body:gsub([[\]], [[\\]]) - body, _ = body:gsub([["]], [[\"]]) - return table.concat({ before, body, after }, "") - end) + commits, _ = commits:gsub( + [[(,"subject":")(.-)(","sanitized_subject_line":")]], + function(before, subject, after) + return table.concat({ before, vim.fn.escape(subject, [[\"]]), after }, "") + end + ) local ok, result = pcall(vim.json.decode, commits, { luanil = { object = true, array = true } }) if not ok then From f22f0747f34eb763177d3b76110d868c47d2c7ff Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 8 Nov 2023 11:14:16 +0100 Subject: [PATCH 07/21] WIP flog graph --- lua/neogit/lib/ansi.lua | 1 + lua/neogit/lib/git/log.lua | 15 +- lua/neogit/lib/graph.lua | 523 ++++++++++++++++++++++++++++++ lua/neogit/lib/ui/component.lua | 3 +- lua/neogit/popups/log/actions.lua | 2 +- 5 files changed, 539 insertions(+), 5 deletions(-) create mode 100644 lua/neogit/lib/graph.lua 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 From 7a3dea2c9acdeb546e2568499d2c299193aad136 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sat, 18 Nov 2023 23:09:17 +0100 Subject: [PATCH 08/21] Use new field --- lua/neogit/lib/git/status.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/neogit/lib/git/status.lua b/lua/neogit/lib/git/status.lua index 87904490c..c6395fec4 100644 --- a/lua/neogit/lib/git/status.lua +++ b/lua/neogit/lib/git/status.lua @@ -184,7 +184,7 @@ local function update_branch_information(state) local commit = git.log.list({ state.upstream.ref, "--max-count=1" })[1] -- May be done earlier by `update_status`, but this function can be called separately if commit then - state.upstream.commit_message = commit.message + state.upstream.commit_message = commit.subject state.upstream.abbrev = git.rev_parse.abbreviate_commit(commit.oid) end end) @@ -195,7 +195,7 @@ local function update_branch_information(state) table.insert(tasks, function() local commit = git.log.list({ pushRemote, "--max-count=1" })[1] if commit then - state.pushRemote.commit_message = commit.message + state.pushRemote.commit_message = commit.subject state.pushRemote.abbrev = git.rev_parse.abbreviate_commit(commit.oid) end end) From 1b63c04ff832628d61a034e4708a3c60dac067ea Mon Sep 17 00:00:00 2001 From: Cameron Date: Sat, 18 Nov 2023 23:17:32 +0100 Subject: [PATCH 09/21] Use git lib entrypoint --- lua/neogit/buffers/commit_view/init.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lua/neogit/buffers/commit_view/init.lua b/lua/neogit/buffers/commit_view/init.lua index 945af594f..2fb4a48e0 100644 --- a/lua/neogit/buffers/commit_view/init.lua +++ b/lua/neogit/buffers/commit_view/init.lua @@ -1,8 +1,7 @@ local Buffer = require("neogit.lib.buffer") -local cli = require("neogit.lib.git.cli") local parser = require("neogit.buffers.commit_view.parsing") local ui = require("neogit.buffers.commit_view.ui") -local log = require("neogit.lib.git.log") +local git = require("neogit.lib.git") local config = require("neogit.config") local popups = require("neogit.popups") local notification = require("neogit.lib.notification") @@ -47,15 +46,15 @@ function M.new(commit_id, notify) notification.info("Parsing commit...") end - local commit_info = log.parse(cli.show.format("fuller").args(commit_id).call_sync().stdout)[1] + local commit_info = git.log.parse(git.cli.show.format("fuller").args(commit_id).call_sync().stdout)[1] commit_info.commit_arg = commit_id local instance = { is_open = false, commit_info = commit_info, commit_overview = parser.parse_commit_overview( - cli.show.stat.oneline.args(commit_id).call_sync():trim().stdout + git.cli.show.stat.oneline.args(commit_id).call_sync():trim().stdout ), - commit_signature = config.values.commit_view.verify_commit and log.verify_commit(commit_id) or {}, + commit_signature = config.values.commit_view.verify_commit and git.log.verify_commit(commit_id) or {}, buffer = nil, } From c9cad0a08596ffeff6c647a1fe2ebd5d1f180d4f Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 19 Nov 2023 20:51:19 +0100 Subject: [PATCH 10/21] Replace commit.description in favor of subject/body --- lua/neogit/buffers/common.lua | 7 ++++--- lua/neogit/lib/git/log.lua | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lua/neogit/buffers/common.lua b/lua/neogit/buffers/common.lua index 2da20d348..ec50b5ea2 100644 --- a/lua/neogit/buffers/common.lua +++ b/lua/neogit/buffers/common.lua @@ -184,7 +184,7 @@ M.CommitEntry = Component.new(function(commit, args) })), row(graph), col( - flat_map(commit.description, function(line) + flat_map({ commit.subject, commit.body }, function(line) local lines = map(util.str_wrap(line, vim.o.columns * 0.6), function(l) return row(util.merge(graph, { text(" "), text(l) })) end) @@ -206,10 +206,11 @@ M.CommitEntry = Component.new(function(commit, args) row( util.merge({ text(commit.oid:sub(1, 7), { - highlight = commit.verification_flag and highlight_for_signature[commit.verification_flag] or "Comment", + highlight = commit.verification_flag and highlight_for_signature[commit.verification_flag] + or "Comment", }), text(" "), - }, graph, { text(" ") }, ref, { text(commit.description[1]) }), + }, graph, { text(" ") }, ref, { text(commit.subject) }), { virtual_text = { { " ", "Constant" }, diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 264e7a37b..677e9ada2 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -442,7 +442,7 @@ function M.present_commit(commit) end return { - name = string.format("%s %s", commit.oid:sub(1, 7), commit.description[1] or ""), + name = string.format("%s %s", commit.oid:sub(1, 7), commit.subject or ""), oid = commit.oid, commit = commit, } From 8a1f7ccedba58ae7303265cb9ee2f0e95eea74bd Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 19 Nov 2023 20:51:38 +0100 Subject: [PATCH 11/21] Formatting --- lua/neogit/lib/graph.lua | 49 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/lua/neogit/lib/graph.lua b/lua/neogit/lib/graph.lua index b16905d8e..603fb436a 100644 --- a/lua/neogit/lib/graph.lua +++ b/lua/neogit/lib/graph.lua @@ -1,30 +1,32 @@ +-- Modified version of graphing algorithm from https://github.com/rbong/vim-flog + 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 = " " +-- styleua: ignore +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 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 = " " +local missing_parent_empty_str = " " function M.build(commits) commits = require("neogit.lib.util").filter_map(commits, function(item) @@ -35,7 +37,6 @@ function M.build(commits) -- Init commit parsing data local commit_hashes = {} - for _, commit in ipairs(commits) do commit_hashes[commit.oid] = 1 end @@ -49,15 +50,13 @@ function M.build(commits) local nbranches = 0 -- Draw graph - for _, commit in ipairs(commits) do - -- Get commit data + -- 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 From d3dbebb968db617465577869b8cc75d7ae1d4484 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 19 Nov 2023 20:52:02 +0100 Subject: [PATCH 12/21] Change the author color highlight --- lua/neogit/buffers/common.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/neogit/buffers/common.lua b/lua/neogit/buffers/common.lua index ec50b5ea2..72de62edb 100644 --- a/lua/neogit/buffers/common.lua +++ b/lua/neogit/buffers/common.lua @@ -158,8 +158,8 @@ M.CommitEntry = Component.new(function(commit, args) details = col.hidden(true).padding_left(8) { row(util.merge(graph, { text(" "), - text("Author: ", { highlight = "NeogitGraphAuthor" }), - text(commit.author_name), + text("Author: ", { highlight = "Comment" }), + text(commit.author_name, { highlight = "NeogitGraphAuthor" }), text(" <"), text(commit.author_email), text(">"), From 7eccfc49e3f5ff34bf9149d523414e17066d2170 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 20 Nov 2023 20:44:53 +0100 Subject: [PATCH 13/21] Formatting, gsub --- lua/neogit/lib/graph.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/neogit/lib/graph.lua b/lua/neogit/lib/graph.lua index 603fb436a..81a934048 100644 --- a/lua/neogit/lib/graph.lua +++ b/lua/neogit/lib/graph.lua @@ -4,7 +4,7 @@ local M = {} local graph_error = "flog: internal error drawing graph" --- styleua: ignore +-- stylua: ignore start local current_commit_str = "• " local commit_branch_str = "│ " local commit_empty_str = " " @@ -27,6 +27,7 @@ local merge_empty_str = " " local missing_parent_str = "┊ " local missing_parent_branch_str = "│ " local missing_parent_empty_str = " " +-- stylua: ignore end function M.build(commits) commits = require("neogit.lib.util").filter_map(commits, function(item) @@ -51,7 +52,6 @@ function M.build(commits) -- Draw graph for _, commit in ipairs(commits) do - -- Get commit data local commit_hash = commit.oid local parents = vim.split(commit.parent, " ") @@ -509,9 +509,9 @@ function M.build(commits) local graph = {} for _, line in ipairs(vim_out) do local g = {} - for _, c in ipairs(vim.split(line.text, "")) do + line.text:gsub(".", function(c) table.insert(g, { text = c, color = line.color, oid = line.oid }) - end + end) table.insert(graph, g) end From b04bf8b5aeb97996648922113689015bd894d5f5 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Mon, 27 Nov 2023 11:03:55 +0100 Subject: [PATCH 14/21] CHANGE: Allow to open fetch popup from commit, log, reflog views --- lua/neogit/buffers/commit_view/init.lua | 1 + lua/neogit/buffers/log_view/init.lua | 2 ++ lua/neogit/buffers/reflog_view/init.lua | 1 + 3 files changed, 4 insertions(+) diff --git a/lua/neogit/buffers/commit_view/init.lua b/lua/neogit/buffers/commit_view/init.lua index 706aea21a..b8a3fadbb 100644 --- a/lua/neogit/buffers/commit_view/init.lua +++ b/lua/neogit/buffers/commit_view/init.lua @@ -154,6 +154,7 @@ function M:open() [popups.mapping_for("CommitPopup")] = popups.open("commit", function(p) p { commit = self.commit_info.oid } end), + [popups.mapping_for("FetchPopup")] = popups.open("fetch"), [popups.mapping_for("PushPopup")] = popups.open("push", function(p) p { commit = self.commit_info.oid } end), diff --git a/lua/neogit/buffers/log_view/init.lua b/lua/neogit/buffers/log_view/init.lua index b6a860d46..fe2c940da 100644 --- a/lua/neogit/buffers/log_view/init.lua +++ b/lua/neogit/buffers/log_view/init.lua @@ -51,6 +51,7 @@ function M:open() [popups.mapping_for("CommitPopup")] = popups.open("commit", function(p) p { commit = self.buffer.ui:get_commit_under_cursor() } end), + [popups.mapping_for("FetchPopup")] = popups.open("fetch"), [popups.mapping_for("PushPopup")] = popups.open("push", function(p) p { commit = self.buffer.ui:get_commit_under_cursor() } end), @@ -87,6 +88,7 @@ function M:open() [popups.mapping_for("CommitPopup")] = popups.open("commit", function(p) p { commit = self.buffer.ui:get_commit_under_cursor() } end), + [popups.mapping_for("FetchPopup")] = popups.open("fetch"), [popups.mapping_for("PushPopup")] = popups.open("push", function(p) p { commit = self.buffer.ui:get_commit_under_cursor() } end), diff --git a/lua/neogit/buffers/reflog_view/init.lua b/lua/neogit/buffers/reflog_view/init.lua index 41936b91f..b429a5498 100644 --- a/lua/neogit/buffers/reflog_view/init.lua +++ b/lua/neogit/buffers/reflog_view/init.lua @@ -73,6 +73,7 @@ function M:open(_) [popups.mapping_for("CommitPopup")] = popups.open("commit", function(p) p { commit = self.buffer.ui:get_commit_under_cursor() } end), + [popups.mapping_for("FetchPopup")] = popups.open("fetch"), [popups.mapping_for("PushPopup")] = popups.open("push", function(p) p { commit = self.buffer.ui:get_commit_under_cursor() } end), From b464b8ad9b852951f70857b11eac35ebda1ab726 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 28 Nov 2023 12:05:15 +0100 Subject: [PATCH 15/21] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index e4889155d..4b7e199b8 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,14 @@ local neogit = require('neogit') neogit.setup {} ``` +## Compatibility + +The `master` branch will always be compatible with the latest **stable** release of Neovim, and with the latest **nightly** build as well. + +Some features may only be available using unreleased (neovim nightly) API's - to use them, set your plugin manager to track the `nightly` branch instead. + +The `nightly` branch has the same stability guarantees as the `master` branch. + ## Configuration You can configure neogit by running the `neogit.setup()` function, passing a table as the argument. From 6d92f47e100f902a88968e683e136348374dd114 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Mon, 27 Nov 2023 11:36:14 +0100 Subject: [PATCH 16/21] FIX: Properly render HEAD with `NeogitBranchHead` highlight group Previoulsy this highlighting failed for branches which didn't have a remote. Instead of the local branch rendering with the `NeogitBranchHead` highlight group, the next tracked branch would be highlighted as HEAD instead. --- lua/neogit/buffers/common.lua | 16 +++++++++------- lua/neogit/lib/git/log.lua | 8 ++++++++ tests/specs/neogit/lib/git/log_spec.lua | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lua/neogit/buffers/common.lua b/lua/neogit/buffers/common.lua index 268e8e2f9..1ea5e5e73 100644 --- a/lua/neogit/buffers/common.lua +++ b/lua/neogit/buffers/common.lua @@ -113,13 +113,12 @@ M.CommitEntry = Component.new(function(commit, args) -- Parse out ref names if args.decorate and commit.ref_name ~= "" then - local ref_name, _ = commit.ref_name:gsub("HEAD %-> ", "") - local is_head = string.match(commit.ref_name, "HEAD %->") ~= nil - local branch_highlight = is_head and "NeogitBranchHead" or "NeogitBranch" - - local info = git.log.branch_info(ref_name, git.remote.list()) + local info = git.log.branch_info(commit.ref_name, git.remote.list()) for _, branch in ipairs(info.untracked) do - table.insert(ref, text(branch, { highlight = "NeogitBranch" })) + table.insert( + ref, + text(branch, { highlight = info.head == branch and "NeogitBranchHead" or "NeogitBranch" }) + ) table.insert(ref, text(" ")) end for branch, remotes in pairs(info.tracked) do @@ -129,7 +128,10 @@ M.CommitEntry = Component.new(function(commit, args) if #remotes > 1 then table.insert(ref, text("{" .. table.concat(remotes, ",") .. "}/", { highlight = "NeogitRemote" })) end - table.insert(ref, text(branch, { highlight = branch_highlight })) + table.insert( + ref, + text(branch, { highlight = info.head == branch and "NeogitBranchHead" or "NeogitBranch" }) + ) table.insert(ref, text(" ")) end for _, tag in pairs(info.tags) do diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 62557d110..f2bcc8de9 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -439,6 +439,7 @@ function M.verify_commit(commit) end ---@class CommitBranchInfo +---@field head string? The name of the local branch, which is currently checked out (if any) ---@field untracked string[] List of local branches on without any remote counterparts ---@field tracked table Mapping from (local) branch names to list of remotes where this branch is present ---@field tags string[] List of tags placed on this commit @@ -446,11 +447,13 @@ end --- Parse information of branches, tags and remotes from a given commit's ref output --- @param ref string comma separated list of branches, tags and remotes, e.g.: --- * "origin/main, main, origin/HEAD, tag: 1.2.3, fork/develop" +--- * "HEAD -> main, origin/main, origin/HEAD, tag: 1.2.3, fork/develop" --- @param remotes string[] list of remote names, e.g. by calling `require("neogit.lib.git.remote").list()` --- @return CommitBranchInfo function M.branch_info(ref, remotes) local parts = vim.split(ref, ", ") local result = { + head = nil, untracked = {}, tracked = {}, tags = {}, @@ -464,6 +467,11 @@ function M.branch_info(ref, remotes) -- No need to annotate tags with remotes, probably too cluttered skip = true end + if part:match("HEAD %-> ") then + part = part:gsub("HEAD %-> ", "") + result.head = part + end + local has_remote = false for _, remote in ipairs(remotes) do if not skip then diff --git a/tests/specs/neogit/lib/git/log_spec.lua b/tests/specs/neogit/lib/git/log_spec.lua index 948fd3c74..2f1fd35dc 100644 --- a/tests/specs/neogit/lib/git/log_spec.lua +++ b/tests/specs/neogit/lib/git/log_spec.lua @@ -331,6 +331,23 @@ describe("lib.git.log.parse", function() subject.branch_info("main, develop", remotes) ) end) + it("lib.git.log.branch_info extracts head", function() + local remotes = remote.list() + assert.are.same( + { head = "main", untracked = { "main" }, tracked = {}, tags = {} }, + subject.branch_info("HEAD -> main", remotes) + ) + assert.are.same( + { head = "develop", untracked = { "main", "develop" }, tracked = {}, tags = {} }, + subject.branch_info("main, HEAD -> develop", remotes) + ) + assert.are.same({ + head = "foo", + untracked = { "foo" }, + tracked = { develop = { "origin" }, main = { "origin" } }, + tags = {}, + }, subject.branch_info("HEAD -> foo, origin/HEAD, main, origin/main, origin/develop", { "origin" })) + end) it("lib.git.log.branch_info extracts local & remote branch names", function() local remotes = { "origin" } From b3d7a01129303b0b52cb2a3b5bff54bb52ce2a92 Mon Sep 17 00:00:00 2001 From: Thore Goll Date: Tue, 28 Nov 2023 08:24:15 +0100 Subject: [PATCH 17/21] FIX: Properly highlight remote only branches --- lua/neogit/buffers/common.lua | 25 ++--- lua/neogit/lib/git/log.lua | 55 +++++------ tests/specs/neogit/lib/git/log_spec.lua | 121 +++++++++++++++--------- 3 files changed, 118 insertions(+), 83 deletions(-) diff --git a/lua/neogit/buffers/common.lua b/lua/neogit/buffers/common.lua index 1ea5e5e73..6137e3b85 100644 --- a/lua/neogit/buffers/common.lua +++ b/lua/neogit/buffers/common.lua @@ -114,24 +114,27 @@ M.CommitEntry = Component.new(function(commit, args) -- Parse out ref names if args.decorate and commit.ref_name ~= "" then local info = git.log.branch_info(commit.ref_name, git.remote.list()) - for _, branch in ipairs(info.untracked) do - table.insert( - ref, - text(branch, { highlight = info.head == branch and "NeogitBranchHead" or "NeogitBranch" }) - ) - table.insert(ref, text(" ")) + + -- Render local only branches first + for name, _ in pairs(info.locals) do + if info.remotes[name] == nil then + local branch_highlight = info.head == name and "NeogitBranchHead" or "NeogitBranch" + table.insert(ref, text(name, { highlight = branch_highlight })) + table.insert(ref, text(" ")) + end end - for branch, remotes in pairs(info.tracked) do + + -- Render tracked (local+remote) branches next + for name, remotes in pairs(info.remotes) do if #remotes == 1 then table.insert(ref, text(remotes[1] .. "/", { highlight = "NeogitRemote" })) end if #remotes > 1 then table.insert(ref, text("{" .. table.concat(remotes, ",") .. "}/", { highlight = "NeogitRemote" })) end - table.insert( - ref, - text(branch, { highlight = info.head == branch and "NeogitBranchHead" or "NeogitBranch" }) - ) + local branch_highlight = info.head == name and "NeogitBranchHead" or "NeogitBranch" + local locally = info.locals[name] ~= nil + table.insert(ref, text(name, { highlight = locally and branch_highlight or "NeogitRemote" })) table.insert(ref, text(" ")) end for _, tag in pairs(info.tags) do diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index f2bcc8de9..0a23a0d41 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -440,8 +440,8 @@ end ---@class CommitBranchInfo ---@field head string? The name of the local branch, which is currently checked out (if any) ----@field untracked string[] List of local branches on without any remote counterparts ----@field tracked table Mapping from (local) branch names to list of remotes where this branch is present +---@field locals table Set of local branch names +---@field remotes table table Mapping from (local) branch names to list of remotes where this branch is present ---@field tags string[] List of tags placed on this commit --- Parse information of branches, tags and remotes from a given commit's ref output @@ -454,49 +454,46 @@ function M.branch_info(ref, remotes) local parts = vim.split(ref, ", ") local result = { head = nil, - untracked = {}, - tracked = {}, + locals = {}, + remotes = {}, tags = {}, } - local untracked = {} - for _, part in ipairs(parts) do + + for _, name in pairs(parts) do local skip = false - if part:match("^tag: .*") ~= nil then - local tag = part:gsub("tag: ", "") + if name:match("^tag: .*") ~= nil then + local tag = name:gsub("tag: ", "") table.insert(result.tags, tag) - -- No need to annotate tags with remotes, probably too cluttered skip = true end - if part:match("HEAD %-> ") then - part = part:gsub("HEAD %-> ", "") - result.head = part + + if name:match("HEAD %-> ") then + name = name:gsub("HEAD %-> ", "") + result.head = name end - local has_remote = false - for _, remote in ipairs(remotes) do + local remote = nil + for _, r in ipairs(remotes) do if not skip then - if part:match("^" .. remote .. "/") then - has_remote = true - local name = part:gsub("^" .. remote .. "/", "") + if name:match("^" .. r .. "/") then + name = name:gsub("^" .. r .. "/", "") if name == "HEAD" then skip = true else - if result.tracked[name] == nil then - result.tracked[name] = {} - end - table.insert(result.tracked[name], remote) + remote = r end end end end - -- if not skip and not has_remote and result.tracked[part] == nil then - if not skip and not has_remote then - table.insert(untracked, part) - end - end - for _, branch in ipairs(untracked) do - if result.tracked[branch] == nil then - table.insert(result.untracked, branch) + if not skip then + if remote ~= nil then + if result.remotes[name] == nil then + result.remotes[name] = {} + end + table.insert(result.remotes[name], remote) + else + result.locals[name] = true + end end end return result diff --git a/tests/specs/neogit/lib/git/log_spec.lua b/tests/specs/neogit/lib/git/log_spec.lua index 2f1fd35dc..08892cf92 100644 --- a/tests/specs/neogit/lib/git/log_spec.lua +++ b/tests/specs/neogit/lib/git/log_spec.lua @@ -325,88 +325,123 @@ describe("lib.git.log.parse", function() it("lib.git.log.branch_info extracts local branch name", function() local remotes = remote.list() - assert.are.same({ untracked = { "main" }, tracked = {}, tags = {} }, subject.branch_info("main", remotes)) assert.are.same( - { untracked = { "main", "develop" }, tracked = {}, tags = {} }, - subject.branch_info("main, develop", remotes) + { tags = {}, locals = { main = true }, remotes = {} }, + subject.branch_info("main", remotes) ) + assert.are.same({ + locals = { main = true, develop = true }, + remotes = {}, + tags = {}, + }, subject.branch_info("main, develop", remotes)) end) + it("lib.git.log.branch_info extracts head", function() local remotes = remote.list() assert.are.same( - { head = "main", untracked = { "main" }, tracked = {}, tags = {} }, + { head = "main", locals = { main = true }, remotes = {}, tags = {} }, subject.branch_info("HEAD -> main", remotes) ) - assert.are.same( - { head = "develop", untracked = { "main", "develop" }, tracked = {}, tags = {} }, - subject.branch_info("main, HEAD -> develop", remotes) - ) + assert.are.same({ + head = "develop", + locals = { main = true, develop = true }, + remotes = {}, + tags = {}, + }, subject.branch_info("main, HEAD -> develop", remotes)) assert.are.same({ head = "foo", - untracked = { "foo" }, - tracked = { develop = { "origin" }, main = { "origin" } }, + locals = { foo = true, develop = true }, + remotes = { + main = { "origin" }, + foo = { "origin" }, + }, tags = {}, - }, subject.branch_info("HEAD -> foo, origin/HEAD, main, origin/main, origin/develop", { "origin" })) + }, subject.branch_info( + "HEAD -> foo, origin/HEAD, origin/main, foo, origin/foo, develop", + { "origin" } + )) end) - it("lib.git.log.branch_info extracts local & remote branch names", function() + it("lib.git.log.branch_info extracts local & remote branch names (tracked)", function() local remotes = { "origin" } assert.are.same( - { tracked = { main = { "origin" } }, tags = {}, untracked = {} }, - subject.branch_info("origin/main", remotes) - ) - assert.are.same( - { tracked = { main = { "origin" }, develop = { "origin" } }, tags = {}, untracked = {} }, - subject.branch_info("origin/main, origin/develop", remotes) - ) - assert.are.same( - { tracked = { main = { "origin" }, develop = { "origin" } }, tags = {}, untracked = {} }, - subject.branch_info("origin/main, main, origin/develop", remotes) - ) - assert.are.same( - { tracked = { main = { "origin" }, develop = { "origin" } }, tags = {}, untracked = { "foo" } }, - subject.branch_info("main, origin/main, origin/develop, develop, foo", remotes) + { tags = {}, locals = { main = true }, remotes = { main = remotes } }, + subject.branch_info("main, origin/main", remotes) ) + assert.are.same({ + locals = { main = true, develop = true }, + remotes = { + main = remotes, + develop = remotes, + }, + tags = {}, + }, subject.branch_info("main, develop, origin/main, origin/develop", remotes)) + assert.are.same({ + locals = { main = true }, + remotes = { + main = remotes, + develop = remotes, + }, + tags = {}, + }, subject.branch_info("origin/main, main, origin/develop", remotes)) + assert.are.same({ + tags = {}, + locals = { main = true, develop = true, foo = true }, + remotes = { + main = remotes, + develop = remotes, + }, + }, subject.branch_info("main, origin/main, origin/develop, develop, foo", remotes)) end) it("lib.git.log.branch_info can deal with multiple remotes", function() local remotes = { "origin", "fork" } - assert.are.same( - { tracked = { main = { "origin", "fork" } }, tags = {}, untracked = {} }, - subject.branch_info("origin/main, main, fork/main", remotes) - ) assert.are.same({ - tracked = { main = { "origin" }, develop = { "origin", "fork" } }, + locals = { main = true }, + remotes = { + main = { "origin", "fork" }, + }, + tags = {}, + }, subject.branch_info("origin/main, main, fork/main", remotes)) + assert.are.same({ + locals = { develop = true, foo = true }, + remotes = { + main = { "origin" }, + develop = { "origin", "fork" }, + }, tags = {}, - untracked = { "foo" }, }, subject.branch_info("origin/main, develop, origin/develop, fork/develop, foo", remotes)) end) it("lib.git.log.branch_info can deal with slashes in branch names", function() local remotes = { "origin" } - assert.are.same( - { tracked = { ["feature/xyz"] = { "origin" } }, untracked = { "foo/bar/baz" }, tags = {} }, - subject.branch_info("feature/xyz, foo/bar/baz, origin/feature/xyz", remotes) - ) + assert.are.same({ + locals = { ["feature/xyz"] = true, ["foo/bar/baz"] = true }, + remotes = { + ["feature/xyz"] = { "origin" }, + }, + tags = {}, + }, subject.branch_info("feature/xyz, foo/bar/baz, origin/feature/xyz", remotes)) end) it("lib.git.log.branch_info ignores HEAD references", function() local remotes = { "origin", "fork" } - assert.are.same( - { tracked = { main = { "origin", "fork" } }, untracked = { "develop" }, tags = {} }, - subject.branch_info("origin/main, fork/main, develop, origin/HEAD, fork/HEAD", remotes) - ) + assert.are.same({ + remotes = { main = { "origin", "fork" } }, + locals = { develop = true }, + tags = {}, + }, subject.branch_info("origin/main, fork/main, develop, origin/HEAD, fork/HEAD", remotes)) end) it("lib.git.log.branch_info parses tags", function() local remotes = { "origin" } assert.are.same( - { tracked = {}, untracked = {}, tags = { "0.1.0" } }, + { locals = {}, remotes = {}, tags = { "0.1.0" } }, subject.branch_info("tag: 0.1.0", remotes) ) assert.are.same({ - tracked = {}, - untracked = {}, + locals = {}, + remotes = {}, tags = { "0.5.7", "foo-bar" }, }, subject.branch_info("tag: 0.5.7, tag: foo-bar", remotes)) end) From e1a0a9db307a76f66b2bcb8e7124c73bf0b9336e Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 28 Nov 2023 22:41:11 +0100 Subject: [PATCH 18/21] Allow configuring graph style --- lua/neogit/config.lua | 4 ++++ lua/neogit/lib/git/log.lua | 9 +++++++-- lua/neogit/popups/log/actions.lua | 17 ++--------------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/lua/neogit/config.lua b/lua/neogit/config.lua index 5d6674156..3abefaa78 100644 --- a/lua/neogit/config.lua +++ b/lua/neogit/config.lua @@ -90,6 +90,8 @@ end ---@alias NeogitConfigMappingsPopup "HelpPopup" | "DiffPopup" | "PullPopup" | "RebasePopup" | "MergePopup" | "PushPopup" | "CommitPopup" | "LogPopup" | "RevertPopup" | "StashPopup" | "IgnorePopup" | "CherryPickPopup" | "BranchPopup" | "FetchPopup" | "ResetPopup" | "RemotePopup" | "TagPopup" | false +---@alias NeogitGraphStyle "ascii" | "flog" + ---@class NeogitConfigMappings Consult the config file or documentation for values ---@field finder? { [string]: NeogitConfigMappingsFinder } A dictionary that uses finder commands to set multiple keybinds ---@field status? { [string]: NeogitConfigMappingsStatus } A dictionary that uses status commands to set a single keybind @@ -97,6 +99,7 @@ end ---@class NeogitConfig Neogit configuration settings ---@field filewatcher? NeogitFilewatcherConfig Values for filewatcher +---@field graph_style? NeogitGraphStyle Style for graph ---@field disable_hint? boolean Remove the top hint in the Status buffer ---@field disable_context_highlighting? boolean Disable context highlights based on cursor position ---@field disable_signs? boolean Special signs to draw for sections etc. in Neogit @@ -141,6 +144,7 @@ function M.get_default_values() disable_context_highlighting = false, disable_signs = false, disable_commit_confirmation = false, + graph_style = "ascii", filewatcher = { interval = 1000, enabled = false, diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 677e9ada2..502af249e 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -378,7 +378,7 @@ end ---@param graph? table ---@param files? table ---@return CommitLogEntry[] -function M.list(options, graph, files) +function M.list(options, graph, files, graph_color) files = files or {} local signature = false @@ -399,7 +399,12 @@ function M.list(options, graph, files) local graph_output if graph then - graph_output = require("neogit.lib.graph").build(commits) + if config.values.graph_style == "flog" then + graph_output = require("neogit.lib.graph").build(commits) + elseif config.values.graph_style == "ascii" then + util.remove_item_from_table(options, "--show-signature") + graph_output = M.graph(options, files, graph_color) + end else graph_output = {} end diff --git a/lua/neogit/popups/log/actions.lua b/lua/neogit/popups/log/actions.lua index ab4a9d17e..61345290c 100644 --- a/lua/neogit/popups/log/actions.lua +++ b/lua/neogit/popups/log/actions.lua @@ -9,20 +9,6 @@ local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") local operation = require("neogit.operations") ----Builds a graph for the popup if required ----@param popup table Contains the argument list ----@param flags table extra CLI flags like --branches or --remotes ----@return table|nil -local function maybe_graph(popup, flags) - local args = popup:get_internal_arguments() - if args.graph then - local external_args = popup:get_arguments() - util.remove_item_from_table(external_args, "--show-signature") - - return git.log.graph(util.merge(external_args, flags), popup.state.env.files, args.color) - end -end - --- Runs `git log` and parses the commits ---@param popup table Contains the argument list ---@param flags table extra CLI flags like --branches or --remotes @@ -31,7 +17,8 @@ local function commits(popup, flags) return git.log.list( util.merge(popup:get_arguments(), flags), popup:get_internal_arguments().graph, - popup.state.env.files + popup.state.env.files, + popup:get_internal_arguments().color ) end From ce4747646859f57754fd5a60d379f77ff0081fb6 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 28 Nov 2023 22:43:05 +0100 Subject: [PATCH 19/21] Flog -> Unicode --- lua/neogit/config.lua | 2 +- lua/neogit/lib/git/log.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/neogit/config.lua b/lua/neogit/config.lua index 3abefaa78..725aa921b 100644 --- a/lua/neogit/config.lua +++ b/lua/neogit/config.lua @@ -90,7 +90,7 @@ end ---@alias NeogitConfigMappingsPopup "HelpPopup" | "DiffPopup" | "PullPopup" | "RebasePopup" | "MergePopup" | "PushPopup" | "CommitPopup" | "LogPopup" | "RevertPopup" | "StashPopup" | "IgnorePopup" | "CherryPickPopup" | "BranchPopup" | "FetchPopup" | "ResetPopup" | "RemotePopup" | "TagPopup" | false ----@alias NeogitGraphStyle "ascii" | "flog" +---@alias NeogitGraphStyle "ascii" | "unicode" ---@class NeogitConfigMappings Consult the config file or documentation for values ---@field finder? { [string]: NeogitConfigMappingsFinder } A dictionary that uses finder commands to set multiple keybinds diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index 502af249e..9211ade90 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -399,7 +399,7 @@ function M.list(options, graph, files, graph_color) local graph_output if graph then - if config.values.graph_style == "flog" then + if config.values.graph_style == "unicode" then graph_output = require("neogit.lib.graph").build(commits) elseif config.values.graph_style == "ascii" then util.remove_item_from_table(options, "--show-signature") From 273027a8b534b02aa61237e757c695fbd6f50f87 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 28 Nov 2023 22:43:47 +0100 Subject: [PATCH 20/21] Only show "--color" switch when rendering ASCII graph --- lua/neogit/popups/log/init.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/neogit/popups/log/init.lua b/lua/neogit/popups/log/init.lua index e60823209..3ad4a40df 100644 --- a/lua/neogit/popups/log/init.lua +++ b/lua/neogit/popups/log/init.lua @@ -1,4 +1,5 @@ local popup = require("neogit.lib.popup") +local config = require("neogit.config") local actions = require("neogit.popups.log.actions") local M = {} @@ -50,7 +51,13 @@ function M.create() incompatible = { "reverse" }, dependant = { "color" }, }) - :switch("c", "color", "Show graph in color", { internal = true, incompatible = { "reverse" } }) + :switch_if( + config.values.graph_style == "ascii", + "c", + "color", + "Show graph in color", + { internal = true, incompatible = { "reverse" } } + ) :switch("d", "decorate", "Show refnames", { enabled = true, internal = true }) :switch("S", "show-signature", "Show signatures", { key_prefix = "=" }) :group_heading("Log") From 69ca013c5d68436b49f8b9c13dab3ffb4516846d Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 28 Nov 2023 22:46:48 +0100 Subject: [PATCH 21/21] Update readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e4889155d..d19d1c5ec 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,9 @@ neogit.setup { interval = 1000, enabled = true, }, + -- "ascii" is the graph the git CLI generates + -- "unicode" is the graph like https://github.com/rbong/vim-flog + graph_style = "ascii", -- Used to generate URL's for branch popup action "pull request". git_services = { ["github.com"] = "https://github.com/${owner}/${repository}/compare/${branch_name}?expand=1",