From c3deefe18ceb4f9fcf9124f39b9b3d2ad64b6579 Mon Sep 17 00:00:00 2001 From: Alessio Date: Sun, 16 Jul 2023 18:45:59 +0100 Subject: [PATCH 01/11] feat(github): add gh api service --- lua/gitsigns/gh.lua | 67 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 lua/gitsigns/gh.lua diff --git a/lua/gitsigns/gh.lua b/lua/gitsigns/gh.lua new file mode 100644 index 000000000..db29ed957 --- /dev/null +++ b/lua/gitsigns/gh.lua @@ -0,0 +1,67 @@ +local async = require('gitsigns.async') +local subprocess = require('gitsigns.subprocess') +local log = require('gitsigns.debug.log') + + +--- @class GitHub.PrInfo +--- @field url string +--- @field author {login: string, name: string} +--- @field mergedAt string +--- @field number string +--- @field title string + +local M = {} + +--- Requests a list of GitHub PRs associated with the given commit SHA +--- +--- @param toplevel string The URL to the repository +--- @param sha string The commit SHA +--- +--- @return GitHub.PrInfo[]? : Array of PR object +M.associated_prs = function(toplevel, sha) + local _, _, stdout, stderr = async.wait(2, subprocess.run_job, { + command = 'gh', + cwd = toplevel, + args = { + 'pr', 'list', + '--search', sha, + '--state', 'merged', + '--json', 'url,author,title,number,mergedAt', + }, + }) + + if stderr then + log.eprintf("Received stderr when running 'gh pr list' command:\n%s", stderr) + + return {}; + end + + return vim.json.decode(stdout); +end + +--- Returns the last PR associated with the commit +--- +--- @param toplevel string The URL to the repository +--- @param sha string The commit SHA +--- +--- @return GitHub.PrInfo? : The latest PR associated with the commit or nil +M.get_last_associated_pr = function(toplevel, sha) + local prs = M.associated_prs(toplevel, sha); + --- @type GitHub.PrInfo? + local last_pr = nil; + + if prs then + for _, pr in ipairs(prs) do + local pr_number = tonumber(pr.number); + local last_pr_number = last_pr and tonumber(last_pr.number) or 0; + + if pr_number > last_pr_number then + last_pr = pr + end + end + end + + return last_pr; +end + +return M From ac03a80050e30eb9b9b877dff1db0968e85f8c97 Mon Sep 17 00:00:00 2001 From: Alessio Date: Sun, 16 Jul 2023 18:46:39 +0100 Subject: [PATCH 02/11] feat(github): show last PR number in popup win --- lua/gitsigns/actions.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/gitsigns/actions.lua b/lua/gitsigns/actions.lua index 3e351bb0d..627ef75e3 100644 --- a/lua/gitsigns/actions.lua +++ b/lua/gitsigns/actions.lua @@ -7,6 +7,7 @@ local popup = require('gitsigns.popup') local util = require('gitsigns.util') local manager = require('gitsigns.manager') local git = require('gitsigns.git') +local gh = require('gitsigns.gh') local run_diff = require('gitsigns.diff') local gs_cache = require('gitsigns.cache') @@ -805,6 +806,7 @@ local function create_blame_fmt(is_committed, full) return { header, { { '', 'NormalFloat' } }, + { { 'PR #', 'Label' } }, { { 'Hunk of ', 'Title' }, { ' ', 'LineNr' } }, { { '', 'NormalFloat' } }, } @@ -866,6 +868,8 @@ M.blame_line = void(function(opts) local hunk hunk, result.hunk_no, result.num_hunks = get_blame_hunk(bcache.git_obj.repo, result) + local last_pr = gh.get_last_associated_pr(bcache.git_obj.repo.toplevel, result.sha); + result.pr_info = last_pr and last_pr.number or 'No PR found'; result.hunk = Hunks.patch_lines(hunk, fileformat) result.hunk_head = hunk.head From b20d61616101bcdac870df0a86bdb0ba62cfa4b7 Mon Sep 17 00:00:00 2001 From: Alessio Date: Sun, 16 Jul 2023 18:48:23 +0100 Subject: [PATCH 03/11] feat(github): add blame and formatter config --- lua/gitsigns/config.lua | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lua/gitsigns/config.lua b/lua/gitsigns/config.lua index 00d9413cd..d93907831 100644 --- a/lua/gitsigns/config.lua +++ b/lua/gitsigns/config.lua @@ -46,6 +46,7 @@ end --- @field delay integer --- @field ignore_whitespace boolean --- @field virt_text_priority integer +--- @field github_blame boolean --- @class Gitsigns.Config --- @field debug_mode boolean @@ -70,6 +71,7 @@ end --- @field current_line_blame_formatter_opts { relative_time: boolean } --- @field current_line_blame_formatter string|Gitsigns.CurrentLineBlameFmtFun --- @field current_line_blame_formatter_nc string|Gitsigns.CurrentLineBlameFmtFun +--- @field current_line_blame_formatter_gh string|Gitsigns.CurrentLineBlameFmtFun --- @field current_line_blame_opts Gitsigns.CurrentLineBlameOpts --- @field preview_config table --- @field attach_to_untracked boolean @@ -669,6 +671,27 @@ M.schema = { ]], }, + current_line_blame_formatter_gh = { + type = { 'string', 'function' }, + default = ' , , PR: #', + --- TODO: confirm <author_time> in github + description = [[ + String or function used to format the virtual text of + |gitsigns-config-current_line_blame| when github is used. + + Note: |gitsigns-config-current_line_blame_opts-github_blame| must be active. + + When a string, accepts the following format specifiers in addition to the defaults: + + • `<number>` + • `<author>` + • `<mergedAt>` or `<mergedAt:FORMAT>` + • `<title>` + + See |gitsigns-config-current_line_blame_formatter| for more information. + ]], + }, + trouble = { type = 'boolean', default = function() From 8f4dbab64de984b168eed1f1d7c1cbc841877a67 Mon Sep 17 00:00:00 2001 From: Alessio <ales.marucci@gmail.com> Date: Sun, 16 Jul 2023 18:51:53 +0100 Subject: [PATCH 04/11] feat(github): show github in virtual text line blame --- lua/gitsigns/current_line_blame.lua | 31 ++++++++++++++++++----- lua/gitsigns/util.lua | 38 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lua/gitsigns/current_line_blame.lua b/lua/gitsigns/current_line_blame.lua index 7325bd06b..f7c8aea29 100644 --- a/lua/gitsigns/current_line_blame.lua +++ b/lua/gitsigns/current_line_blame.lua @@ -5,6 +5,7 @@ local scheduler = a.scheduler local cache = require('gitsigns.cache').cache local config = require('gitsigns.config').config +local gh = require('gitsigns.gh') local util = require('gitsigns.util') local uv = vim.loop @@ -63,7 +64,7 @@ BlameCache.contents = {} --- @param bufnr integer --- @param lnum integer ---- @param x? Gitsigns.BlameInfo +--- @param x? Gitsigns.BlameInfo|GitHub.PrInfo function BlameCache:add(bufnr, lnum, x) if not x then return @@ -80,7 +81,7 @@ end --- @param bufnr integer --- @param lnum integer ---- @return Gitsigns.BlameInfo? +--- @return Gitsigns.BlameInfo|GitHub.PrInfo|nil function BlameCache:get(bufnr, lnum) if not config._blame_cache then return @@ -121,7 +122,7 @@ local running = false --- @param bufnr integer --- @param lnum integer --- @param opts Gitsigns.CurrentLineBlameOpts ---- @return Gitsigns.BlameInfo? +--- @return Gitsigns.BlameInfo|GitHub.PrInfo|nil local function run_blame(bufnr, lnum, opts) local result = BlameCache:get(bufnr, lnum) if result then @@ -136,6 +137,17 @@ local function run_blame(bufnr, lnum, opts) local buftext = util.buf_lines(bufnr) local bcache = cache[bufnr] result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) + + if result and opts.github_blame then + local last_pr = gh.get_last_associated_pr(bcache.git_obj.repo.toplevel, result.sha); + + if last_pr then + result = last_pr; + -- the parser does not support accessing keys like <author.name> + result.author = last_pr.author.name + end + end + BlameCache:add(bufnr, lnum, result) running = false @@ -149,9 +161,16 @@ end local function handle_blame_info(bufnr, lnum, blame_info, opts) local bcache = cache[bufnr] local virt_text ---@type {[1]: string, [2]: string}[] - local clb_formatter = blame_info.author == 'Not Committed Yet' - and config.current_line_blame_formatter_nc - or config.current_line_blame_formatter + local code_committed = blame_info.author ~= 'Not Committed Yet' + + local clb_formatter = code_committed + and config.current_line_blame_formatter + or config.current_line_blame_formatter_nc + + if opts.github_blame and code_committed then + clb_formatter = config.current_line_blame_formatter_gh + end + if type(clb_formatter) == 'string' then virt_text = { { diff --git a/lua/gitsigns/util.lua b/lua/gitsigns/util.lua index c52311f01..5c8bcf026 100644 --- a/lua/gitsigns/util.lua +++ b/lua/gitsigns/util.lua @@ -1,3 +1,5 @@ +local log = require('gitsigns.debug.log') + local M = {} function M.path_exists(path) @@ -201,10 +203,11 @@ function M.expand_format(fmt, info, reltime) if type(v) == 'table' then v = table.concat(v, '\n') end - if vim.endswith(key, '_time') then + if vim.endswith(key, '_time') or vim.endswith(key, 'At') then if time_fmt == '' then time_fmt = reltime and '%R' or '%Y-%m-%d' end + v = get_timestamp_from_datetime(v) or v; v = expand_date(time_fmt, v) end match = tostring(v) @@ -223,4 +226,37 @@ function M.bufexists(buf) return vim.fn.bufexists(buf) == 1 end +--- Converts a DateTime string into a timestamp +--- +--- @param dateTime string +--- @return number? The timestamp +function get_timestamp_from_datetime(dateTime) + local inYear, inMonth, inDay, inHour, inMinute, inSecond, inZone = + string.match(dateTime, '^(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)(.-)$') + + if not inYear then + log.eprintf("Could not parse DateTime '%s'. Pattern did not match.", dateTime); + + return nil; + end + + local zHours, zMinutes = string.match(inZone, '^(.-):(%d%d)$') + + local returnTime = os.time({ + year = inYear, + month = inMonth, + day = inDay, + hour = inHour, + min = inMinute, + sec = inSecond, + isdst = false + }) + + if zHours then + returnTime = returnTime - ((tonumber(zHours) * 3600) + (tonumber(zMinutes) * 60)) + end + + return returnTime +end + return M From 5fb7d799fe1dedaefe62aebd88c99e30716e0d08 Mon Sep 17 00:00:00 2001 From: Alessio <ales.marucci@gmail.com> Date: Sun, 16 Jul 2023 19:07:44 +0100 Subject: [PATCH 05/11] feat(github): if no PR is found, fallback to std git blame --- lua/gitsigns/current_line_blame.lua | 4 +++- lua/gitsigns/gh.lua | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/gitsigns/current_line_blame.lua b/lua/gitsigns/current_line_blame.lua index f7c8aea29..b11b51812 100644 --- a/lua/gitsigns/current_line_blame.lua +++ b/lua/gitsigns/current_line_blame.lua @@ -145,6 +145,7 @@ local function run_blame(bufnr, lnum, opts) result = last_pr; -- the parser does not support accessing keys like <author.name> result.author = last_pr.author.name + result.is_github = true; end end @@ -162,12 +163,13 @@ local function handle_blame_info(bufnr, lnum, blame_info, opts) local bcache = cache[bufnr] local virt_text ---@type {[1]: string, [2]: string}[] local code_committed = blame_info.author ~= 'Not Committed Yet' + local use_github = opts.github_blame and blame_info.is_github; local clb_formatter = code_committed and config.current_line_blame_formatter or config.current_line_blame_formatter_nc - if opts.github_blame and code_committed then + if code_committed and use_github then clb_formatter = config.current_line_blame_formatter_gh end diff --git a/lua/gitsigns/gh.lua b/lua/gitsigns/gh.lua index db29ed957..eb83655c8 100644 --- a/lua/gitsigns/gh.lua +++ b/lua/gitsigns/gh.lua @@ -9,6 +9,7 @@ local log = require('gitsigns.debug.log') --- @field mergedAt string --- @field number string --- @field title string +--- @field is_github? boolean local M = {} From ea8ccb1bc753aa84a3e59f3e701f15b51ebdd16f Mon Sep 17 00:00:00 2001 From: Alessio <ales.marucci@gmail.com> Date: Sun, 16 Jul 2023 19:30:42 +0100 Subject: [PATCH 06/11] feat(github): update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 973043a78..fc881be7c 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,11 @@ require('gitsigns').setup { virt_text = true, virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' delay = 1000, + github_blame = false, ignore_whitespace = false, }, current_line_blame_formatter = '<author>, <author_time:%Y-%m-%d> - <summary>', + current_line_blame_formatter_gh = ' <author>, <mergedAt:%Y-%m-%d>, PR: #<number> • <title>', sign_priority = 6, update_debounce = 100, status_formatter = nil, -- Use default From b15d197160338c8079fa29be7a6dbef04f8bbf49 Mon Sep 17 00:00:00 2001 From: Alessio <ales.marucci@gmail.com> Date: Sun, 16 Jul 2023 19:37:07 +0100 Subject: [PATCH 07/11] chore: remove todos --- lua/gitsigns/config.lua | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lua/gitsigns/config.lua b/lua/gitsigns/config.lua index d93907831..0e9b58f6e 100644 --- a/lua/gitsigns/config.lua +++ b/lua/gitsigns/config.lua @@ -432,15 +432,15 @@ M.schema = { count_chars = { type = 'table', default = { - [1] = '1', -- '₁', - [2] = '2', -- '₂', - [3] = '3', -- '₃', - [4] = '4', -- '₄', - [5] = '5', -- '₅', - [6] = '6', -- '₆', - [7] = '7', -- '₇', - [8] = '8', -- '₈', - [9] = '9', -- '₉', + [1] = '1', -- '₁', + [2] = '2', -- '₂', + [3] = '3', -- '₃', + [4] = '4', -- '₄', + [5] = '5', -- '₅', + [6] = '6', -- '₆', + [7] = '7', -- '₇', + [8] = '8', -- '₈', + [9] = '9', -- '₉', ['+'] = '>', -- '₊', }, description = [[ @@ -674,7 +674,6 @@ M.schema = { current_line_blame_formatter_gh = { type = { 'string', 'function' }, default = ' <author>, <mergedAt>, PR: #<number> • <title>', - --- TODO: confirm <author_time> in github description = [[ String or function used to format the virtual text of |gitsigns-config-current_line_blame| when github is used. From 3c596fe8641d13c01bfce90b95ceeae7ae0a19ee Mon Sep 17 00:00:00 2001 From: Alessio <ales.marucci@gmail.com> Date: Sun, 16 Jul 2023 19:40:14 +0100 Subject: [PATCH 08/11] chore: tidy up --- lua/gitsigns/gh.lua | 3 +-- lua/gitsigns/util.lua | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/gitsigns/gh.lua b/lua/gitsigns/gh.lua index eb83655c8..cf995cc23 100644 --- a/lua/gitsigns/gh.lua +++ b/lua/gitsigns/gh.lua @@ -1,7 +1,6 @@ local async = require('gitsigns.async') -local subprocess = require('gitsigns.subprocess') local log = require('gitsigns.debug.log') - +local subprocess = require('gitsigns.subprocess') --- @class GitHub.PrInfo --- @field url string diff --git a/lua/gitsigns/util.lua b/lua/gitsigns/util.lua index 5c8bcf026..895a1ee0a 100644 --- a/lua/gitsigns/util.lua +++ b/lua/gitsigns/util.lua @@ -186,7 +186,7 @@ end ---@param reltime? boolean Use relative time as the default date format ---@return string function M.expand_format(fmt, info, reltime) - local ret = {} --- @type string[] + local ret = {} --- @type string[] for _ = 1, 20 do -- loop protection -- Capture <name> or <name:format> @@ -226,7 +226,7 @@ function M.bufexists(buf) return vim.fn.bufexists(buf) == 1 end ---- Converts a DateTime string into a timestamp +--- Converts a DateTime string into its timestamp --- --- @param dateTime string --- @return number? The timestamp From 1998b165141884eb4e5660f070c505dc0bd27f90 Mon Sep 17 00:00:00 2001 From: Alessio <ales.marucci@gmail.com> Date: Sun, 16 Jul 2023 19:44:53 +0100 Subject: [PATCH 09/11] fix(github): filter out numbers from timestamp util --- lua/gitsigns/util.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/gitsigns/util.lua b/lua/gitsigns/util.lua index 895a1ee0a..440c8bd53 100644 --- a/lua/gitsigns/util.lua +++ b/lua/gitsigns/util.lua @@ -228,9 +228,13 @@ end --- Converts a DateTime string into its timestamp --- ---- @param dateTime string +--- @param dateTime string|number --- @return number? The timestamp function get_timestamp_from_datetime(dateTime) + if (type(dateTime) ~= 'string') then + return nil + end + local inYear, inMonth, inDay, inHour, inMinute, inSecond, inZone = string.match(dateTime, '^(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)(.-)$') From 0423002ac91334160caf6db122a58d99bc5dcccd Mon Sep 17 00:00:00 2001 From: Alessio <ales.marucci@gmail.com> Date: Sun, 16 Jul 2023 20:00:43 +0100 Subject: [PATCH 10/11] chore: remove wrong reformatting --- lua/gitsigns/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/gitsigns/util.lua b/lua/gitsigns/util.lua index 440c8bd53..77c428c7d 100644 --- a/lua/gitsigns/util.lua +++ b/lua/gitsigns/util.lua @@ -186,7 +186,7 @@ end ---@param reltime? boolean Use relative time as the default date format ---@return string function M.expand_format(fmt, info, reltime) - local ret = {} --- @type string[] + local ret = {} --- @type string[] for _ = 1, 20 do -- loop protection -- Capture <name> or <name:format> From 014ad511d0ca09c6fd1a87a93542abefc5151e5a Mon Sep 17 00:00:00 2001 From: Alessio <ales.marucci@gmail.com> Date: Wed, 19 Jul 2023 23:53:05 +0100 Subject: [PATCH 11/11] feat(github): check gh cli is installed before running --- lua/gitsigns.lua | 6 +++++ lua/gitsigns/actions.lua | 2 +- lua/gitsigns/current_line_blame.lua | 2 +- lua/gitsigns/gh.lua | 41 ++++++++++++++++++----------- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/lua/gitsigns.lua b/lua/gitsigns.lua index d2756c0d5..3fa63e62b 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -170,6 +170,12 @@ M.setup = void(function(cfg) return end + if config.current_line_blame_opts.github_blame and vim.fn.executable('gh') == 0 then + print("gitsigns: gh not in path. Ignoring 'current_line_blame_opts.github_blame' in config") + config.current_line_blame_opts.github_blame = false + end + + setup_debug() setup_cli() diff --git a/lua/gitsigns/actions.lua b/lua/gitsigns/actions.lua index 627ef75e3..c2c0692d1 100644 --- a/lua/gitsigns/actions.lua +++ b/lua/gitsigns/actions.lua @@ -868,7 +868,7 @@ M.blame_line = void(function(opts) local hunk hunk, result.hunk_no, result.num_hunks = get_blame_hunk(bcache.git_obj.repo, result) - local last_pr = gh.get_last_associated_pr(bcache.git_obj.repo.toplevel, result.sha); + local last_pr = gh.get_last_associated_pr(result.sha); result.pr_info = last_pr and last_pr.number or 'No PR found'; result.hunk = Hunks.patch_lines(hunk, fileformat) diff --git a/lua/gitsigns/current_line_blame.lua b/lua/gitsigns/current_line_blame.lua index b11b51812..6f8ccc70a 100644 --- a/lua/gitsigns/current_line_blame.lua +++ b/lua/gitsigns/current_line_blame.lua @@ -139,7 +139,7 @@ local function run_blame(bufnr, lnum, opts) result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) if result and opts.github_blame then - local last_pr = gh.get_last_associated_pr(bcache.git_obj.repo.toplevel, result.sha); + local last_pr = gh.get_last_associated_pr(result.sha); if last_pr then result = last_pr; diff --git a/lua/gitsigns/gh.lua b/lua/gitsigns/gh.lua index cf995cc23..f2d240c1b 100644 --- a/lua/gitsigns/gh.lua +++ b/lua/gitsigns/gh.lua @@ -12,41 +12,50 @@ local subprocess = require('gitsigns.subprocess') local M = {} +local GH_NOT_FOUND_ERROR = "Could not find 'gh' command. Is the gh-cli package installed?"; + + +local gh_command = function(args) + if vim.fn.executable('gh') then + return async.wait(2, subprocess.run_job, { command = 'gh', args = args }); + end + + return {}; +end + --- Requests a list of GitHub PRs associated with the given commit SHA --- ---- @param toplevel string The URL to the repository --- @param sha string The commit SHA --- --- @return GitHub.PrInfo[]? : Array of PR object -M.associated_prs = function(toplevel, sha) - local _, _, stdout, stderr = async.wait(2, subprocess.run_job, { - command = 'gh', - cwd = toplevel, - args = { - 'pr', 'list', - '--search', sha, - '--state', 'merged', - '--json', 'url,author,title,number,mergedAt', - }, +M.associated_prs = function(sha) + local _, _, stdout, stderr = gh_command({ + 'pr', 'list', + '--search', sha, + '--state', 'merged', + '--json', 'url,author,title,number,mergedAt', }) if stderr then log.eprintf("Received stderr when running 'gh pr list' command:\n%s", stderr) + end + + local empty_set_len = 2; - return {}; + if type(stdout) == string and #stdout > empty_set_len then + return vim.json.decode(stdout); end - return vim.json.decode(stdout); + return nil; end --- Returns the last PR associated with the commit --- ---- @param toplevel string The URL to the repository --- @param sha string The commit SHA --- --- @return GitHub.PrInfo? : The latest PR associated with the commit or nil -M.get_last_associated_pr = function(toplevel, sha) - local prs = M.associated_prs(toplevel, sha); +M.get_last_associated_pr = function(sha) + local prs = M.associated_prs(sha); --- @type GitHub.PrInfo? local last_pr = nil;