Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GitHub integration #839

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ require('gitsigns').setup {
virt_text = true,
virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align'
delay = 1000,
github_blame = false,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lewis6991 should this be called something like use_github or something like that instead?

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
Expand Down
6 changes: 6 additions & 0 deletions lua/gitsigns.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions lua/gitsigns/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -805,6 +806,7 @@ local function create_blame_fmt(is_committed, full)
return {
header,
{ { '<body>', 'NormalFloat' } },
{ { 'PR #<pr_info>', 'Label' } },
{ { 'Hunk <hunk_no> of <num_hunks>', 'Title' }, { ' <hunk_head>', 'LineNr' } },
{ { '<hunk>', 'NormalFloat' } },
}
Expand Down Expand Up @@ -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(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
Expand Down
40 changes: 31 additions & 9 deletions lua/gitsigns/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string,any>
--- @field attach_to_untracked boolean
Expand Down Expand Up @@ -430,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 = [[
Expand Down Expand Up @@ -669,6 +671,26 @@ M.schema = {
]],
},

current_line_blame_formatter_gh = {
type = { 'string', 'function' },
default = ' <author>, <mergedAt>, PR: #<number> • <title>',
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>`
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an improvement, we could expand this to the full list of fields that gh gives us instead? only nested fields are not supported yet


See |gitsigns-config-current_line_blame_formatter| for more information.
]],
},

trouble = {
type = 'boolean',
default = function()
Expand Down
33 changes: 27 additions & 6 deletions lua/gitsigns/current_line_blame.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -136,6 +137,18 @@ 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(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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<author.name> maybe that's something we could add? so we could just query the GH endpoint and pass down all the available fields

result.is_github = true;
end
end

BlameCache:add(bufnr, lnum, result)
running = false

Expand All @@ -149,9 +162,17 @@ 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 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 code_committed and use_github then
clb_formatter = config.current_line_blame_formatter_gh
end

if type(clb_formatter) == 'string' then
virt_text = {
{
Expand Down
76 changes: 76 additions & 0 deletions lua/gitsigns/gh.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
local async = require('gitsigns.async')
local log = require('gitsigns.debug.log')
local subprocess = require('gitsigns.subprocess')

--- @class GitHub.PrInfo
--- @field url string
--- @field author {login: string, name: string}
--- @field mergedAt string
--- @field number string
--- @field title string
--- @field is_github? boolean

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 sha string The commit SHA
---
--- @return GitHub.PrInfo[]? : Array of PR object
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;

if type(stdout) == string and #stdout > empty_set_len then
return vim.json.decode(stdout);
end

return nil;
end

--- Returns the last PR associated with the commit
---
--- @param sha string The commit SHA
---
--- @return GitHub.PrInfo? : The latest PR associated with the commit or nil
M.get_last_associated_pr = function(sha)
local prs = M.associated_prs(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
42 changes: 41 additions & 1 deletion lua/gitsigns/util.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
local log = require('gitsigns.debug.log')

local M = {}

function M.path_exists(path)
Expand Down Expand Up @@ -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)
Expand All @@ -223,4 +226,41 @@ function M.bufexists(buf)
return vim.fn.bufexists(buf) == 1
end

--- Converts a DateTime string into its timestamp
---
--- @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)(.-)$')

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