generated from chrisgrieser/nvim-pseudometa-plugin-template
-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
08b9422
commit 38584d7
Showing
7 changed files
with
532 additions
and
452 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
local M = {} | ||
local fn = vim.fn | ||
local u = require("tinygit.utils") | ||
local config = require("tinygit.config").config | ||
local push = require("tinygit.push").push | ||
-------------------------------------------------------------------------------- | ||
|
||
-- if there are no staged changes, will add all changes (`git add -A`) | ||
-- if not, indicates the already staged changes | ||
---@return string|nil stageInfo, nil if staging was unsuccessful | ||
local function stageAllIfNoChanges() | ||
fn.system { "git", "diff", "--staged", "--quiet" } | ||
local hasStagedChanges = vim.v.shell_error ~= 0 | ||
|
||
if hasStagedChanges then | ||
local stagedChanges = (fn.system { "git", "diff", "--staged", "--stat" }):gsub("\n.-$", "") | ||
if u.nonZeroExit(stagedChanges) then return end | ||
return stagedChanges | ||
else | ||
local stderr = fn.system { "git", "add", "-A" } | ||
if u.nonZeroExit(stderr) then return end | ||
return "Staged all changes." | ||
end | ||
end | ||
|
||
---process a commit message: length, not empty, adheres to conventional commits | ||
---@param commitMsg string | ||
---@nodiscard | ||
---@return boolean is the commit message valid? | ||
---@return string the (modified) commit message | ||
local function processCommitMsg(commitMsg) | ||
commitMsg = vim.trim(commitMsg) | ||
local conf = config.commitMsg | ||
|
||
if #commitMsg > conf.maxLen then | ||
u.notify("Commit Message too long.", "warn") | ||
local shortenedMsg = commitMsg:sub(1, conf.maxLen) | ||
return false, shortenedMsg | ||
elseif commitMsg == "" then | ||
if not conf.emptyFillIn then | ||
u.notify("Commit Message empty.", "warn") | ||
return false, "" | ||
else | ||
---@diagnostic disable-next-line: return-type-mismatch -- checked above | ||
return true, conf.emptyFillIn | ||
end | ||
end | ||
|
||
if conf.enforceConvCommits.enabled then | ||
-- stylua: ignore | ||
local firstWord = commitMsg:match("^%w+") | ||
if not vim.tbl_contains(conf.enforceConvCommits.keywords, firstWord) then | ||
u.notify("Not using a Conventional Commits keyword.", "warn") | ||
return false, commitMsg | ||
end | ||
end | ||
|
||
-- message ok | ||
return true, commitMsg | ||
end | ||
|
||
-- Uses ColorColumn to indicate max length of commit messages, and | ||
-- additionally colors commit messages that are too long in red. | ||
local function setGitCommitAppearance() | ||
vim.api.nvim_create_autocmd("FileType", { | ||
pattern = "DressingInput", | ||
once = true, -- do not affect other DressingInputs | ||
callback = function() | ||
local conf = config.commitMsg | ||
local winNs = 1 | ||
|
||
vim.api.nvim_win_set_hl_ns(0, winNs) | ||
|
||
-- custom highlighting | ||
fn.matchadd("overLength", ([[.\{%s}\zs.*\ze]]):format(conf.maxLen - 1)) | ||
fn.matchadd( | ||
"closeToOverlength", | ||
-- \ze = end of match, \zs = start of match | ||
([[.\{%s}\zs.\{1,%s}\ze]]):format(conf.mediumLen - 1, conf.maxLen - conf.mediumLen) | ||
) | ||
fn.matchadd("issueNumber", [[#\d\+]]) | ||
vim.api.nvim_set_hl(winNs, "overLength", { link = "ErrorMsg" }) | ||
vim.api.nvim_set_hl(winNs, "closeToOverlength", { link = "WarningMsg" }) | ||
vim.api.nvim_set_hl(winNs, "issueNumber", { link = "Number" }) | ||
|
||
-- colorcolumn as extra indicators of overLength | ||
vim.opt_local.colorcolumn = { conf.mediumLen, conf.maxLen } | ||
|
||
-- treesitter highlighting | ||
vim.bo.filetype = "gitcommit" | ||
vim.api.nvim_set_hl(winNs, "Title", { link = "Normal" }) | ||
|
||
-- activate styling of statusline plugins | ||
vim.api.nvim_buf_set_name(0, "COMMIT_EDITMSG") | ||
end, | ||
}) | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
|
||
---@param opts? { forcePush?: boolean } | ||
function M.amendNoEdit(opts) | ||
if not opts then opts = {} end | ||
vim.cmd("silent update") | ||
if u.notInGitRepo() then return end | ||
|
||
local stageInfo = stageAllIfNoChanges() | ||
if not stageInfo then return end | ||
|
||
local stderr = fn.system { "git", "commit", "--amend", "--no-edit" } | ||
if u.nonZeroExit(stderr) then return end | ||
|
||
local lastCommitMsg = vim.trim(fn.system("git log -1 --pretty=%B")) | ||
local body = { stageInfo, ('"%s"'):format(lastCommitMsg) } | ||
if opts.forcePush then table.insert(body, "Force Pushing…") end | ||
local notifyText = table.concat(body, "\n \n") -- need space since empty lines are removed by nvim-notify | ||
u.notify(notifyText, "info", "Amend-No-edit") | ||
|
||
if opts.forcePush then push { force = true } end | ||
end | ||
|
||
---@param opts? { forcePush?: boolean } | ||
---@param prefillMsg? string | ||
function M.amendOnlyMsg(opts, prefillMsg) | ||
if not opts then opts = {} end | ||
vim.cmd("silent update") | ||
if u.notInGitRepo() then return end | ||
|
||
if not prefillMsg then | ||
local lastCommitMsg = vim.trim(fn.system("git log -1 --pretty=%B")) | ||
prefillMsg = lastCommitMsg | ||
end | ||
setGitCommitAppearance() | ||
|
||
vim.ui.input({ prompt = " Amend Message", default = prefillMsg }, function(commitMsg) | ||
if not commitMsg then return end -- aborted input modal | ||
local validMsg, cMsg = processCommitMsg(commitMsg) | ||
if not validMsg then -- if msg invalid, run again to fix the msg | ||
M.amendOnlyMsg(opts, cMsg) | ||
return | ||
end | ||
|
||
local stderr = fn.system { "git", "commit", "--amend", "-m", cMsg } | ||
if u.nonZeroExit(stderr) then return end | ||
|
||
local body = ('"%s"'):format(cMsg) | ||
if opts.forcePush then body = body .. "\n\n➤ Force Pushing…" end | ||
u.notify(body, "info", "Amend-Only-Msg") | ||
|
||
if opts.forcePush then push { force = true } end | ||
end) | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
|
||
---If there are staged changes, commit them. | ||
---If there aren't, add all changes (`git add -A`) and then commit. | ||
---@param prefillMsg? string | ||
---@param opts? { push?: boolean, openReferencedIssue?: boolean } | ||
function M.smartCommit(opts, prefillMsg) | ||
if u.notInGitRepo() then return end | ||
|
||
vim.cmd("silent update") | ||
if not opts then opts = {} end | ||
if not prefillMsg then prefillMsg = "" end | ||
|
||
setGitCommitAppearance() | ||
vim.ui.input({ prompt = " Commit Message", default = prefillMsg }, function(commitMsg) | ||
if not commitMsg then return end -- aborted input modal | ||
local validMsg, processedMsg = processCommitMsg(commitMsg) | ||
if not validMsg then -- if msg invalid, run again to fix the msg | ||
M.smartCommit(opts, processedMsg) | ||
return | ||
end | ||
|
||
local stageInfo = stageAllIfNoChanges() | ||
if not stageInfo then return end | ||
|
||
local stderr = fn.system { "git", "commit", "-m", processedMsg } | ||
if u.nonZeroExit(stderr) then return end | ||
|
||
local body = { stageInfo, ('"%s"'):format(processedMsg) } | ||
if opts.push then table.insert(body, "Pushing…") end | ||
local notifyText = table.concat(body, "\n \n") -- need space since empty lines are removed by nvim-notify | ||
u.notify(notifyText, "info", "Smart-Commit") | ||
|
||
local issueReferenced = processedMsg:match("#(%d+)") | ||
if opts.openReferencedIssue and issueReferenced then | ||
local url = ("https://github.com/%s/issues/%s"):format(u.getRepo(), issueReferenced) | ||
u.openUrl(url) | ||
end | ||
|
||
if opts.push then push { pullBefore = true } end | ||
end) | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
return M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
local M = {} | ||
-------------------------------------------------------------------------------- | ||
|
||
---@class pluginConfig | ||
---@field commitMsg commitConfig | ||
---@field asyncOpConfirmationSound boolean | ||
---@field issueIcons issueIconConfig | ||
|
||
---@class issueIconConfig | ||
---@field closedIssue string | ||
---@field openIssue string | ||
---@field openPR string | ||
---@field mergedPR string | ||
---@field closedPR string | ||
|
||
---@class commitConfig | ||
---@field maxLen number | ||
---@field mediumLen number | ||
---@field emptyFillIn string | ||
---@field enforceConvCommits enforceConvCommitsConfig | ||
|
||
---@class enforceConvCommitsConfig | ||
---@field enabled boolean | ||
---@field keywords string[] | ||
|
||
-------------------------------------------------------------------------------- | ||
|
||
---@type pluginConfig | ||
local defaultConfig = { | ||
commitMsg = { | ||
-- Why 50/72 is recommended: https://stackoverflow.com/q/2290016/22114136 | ||
maxLen = 72, | ||
mediumLen = 50, | ||
|
||
-- When conforming the commit message popup with an empty message, fill in | ||
-- this message. Set to `false` to disallow empty commit messages. | ||
emptyFillIn = "chore", ---@type string|false | ||
|
||
-- disallow commit messages without a conventinal commit keyword | ||
enforceConvCommits = { | ||
enabled = true, | ||
-- stylua: ignore | ||
keywords = { | ||
"chore", "build", "test", "fix", "feat", "refactor", "perf", | ||
"style", "revert", "ci", "docs", "break", "improv", | ||
}, | ||
}, | ||
}, | ||
asyncOpConfirmationSound = true, -- currently macOS only | ||
issueIcons = { | ||
closedIssue = "🟣", | ||
openIssue = "🟢", | ||
openPR = "🟦", | ||
mergedPR = "🟨", | ||
closedPR = "🟥", | ||
}, | ||
} | ||
|
||
|
||
-------------------------------------------------------------------------------- | ||
|
||
-- in case user does not call `setup` | ||
M.config = defaultConfig | ||
|
||
---@param userConfig pluginConfig | ||
function M.setupPlugin(userConfig) | ||
M.config = vim.tbl_deep_extend("force", defaultConfig, userConfig) | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
return M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
local M = {} | ||
local fn = vim.fn | ||
local u = require("tinygit.utils") | ||
local config = require("tinygit.config").config | ||
-------------------------------------------------------------------------------- | ||
|
||
---opens current buffer in the browser & copies the link to the clipboard | ||
---normal mode: link to file | ||
---visual mode: link to selected lines | ||
---@param justRepo any -- don't link to file with a specific commit, just link to repo | ||
function M.githubUrl(justRepo) | ||
if u.notInGitRepo() then return end | ||
|
||
local filepath = vim.fn.expand("%:p") | ||
local gitroot = vim.fn.system("git --no-optional-locks rev-parse --show-toplevel") | ||
local pathInRepo = filepath:sub(#gitroot + 1) | ||
|
||
local pathInRepoEncoded = pathInRepo:gsub("%s+", "%%20") | ||
local remote = fn.system("git --no-optional-locks remote -v"):gsub(".*:(.-)%.git.*", "%1") | ||
local hash = vim.trim(fn.system("git --no-optional-locks rev-parse HEAD")) | ||
local branch = vim.trim(fn.system("git --no-optional-locks branch --show-current")) | ||
|
||
local selStart = fn.line("v") | ||
local selEnd = fn.line(".") | ||
local isVisualMode = fn.mode():find("[Vv]") | ||
local isNormalMode = fn.mode() == "n" | ||
local url = "https://github.com/" .. remote | ||
|
||
if not justRepo and isNormalMode then | ||
url = url .. ("/blob/%s/%s"):format(branch, pathInRepoEncoded) | ||
elseif not justRepo and isVisualMode then | ||
local location | ||
if selStart == selEnd then -- one-line-selection | ||
location = "#L" .. tostring(selStart) | ||
elseif selStart < selEnd then | ||
location = "#L" .. tostring(selStart) .. "-L" .. tostring(selEnd) | ||
else | ||
location = "#L" .. tostring(selEnd) .. "-L" .. tostring(selStart) | ||
end | ||
url = url .. ("/blob/%s/%s%s"):format(hash, pathInRepoEncoded, location) | ||
end | ||
|
||
u.openUrl(url) | ||
fn.setreg("+", url) -- copy to clipboard | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
---formats the list of issues/PRs for vim.ui.select | ||
---@param issue table | ||
---@return table | ||
local function issueListFormatter(issue) | ||
local isPR = issue.pull_request ~= nil | ||
local merged = isPR and issue.pull_request.merged_at ~= nil | ||
|
||
local icon | ||
if issue.state == "open" and isPR then | ||
icon = config.issueIcons.openPR | ||
elseif issue.state == "closed" and isPR and merged then | ||
icon = config.issueIcons.mergedPR | ||
elseif issue.state == "closed" and isPR and not merged then | ||
icon = config.issueIcons.closedPR | ||
elseif issue.state == "closed" and not isPR then | ||
icon = config.issueIcons.closedIssue | ||
elseif issue.state == "open" and not isPR then | ||
icon = config.issueIcons.openIssue | ||
end | ||
|
||
return icon .. " #" .. issue.number .. " " .. issue.title | ||
end | ||
|
||
---Choose a GitHub issue/PR from the current repo to open in the browser. | ||
---CAVEAT Due to GitHub API liminations, only the last 100 issues are shown. | ||
---@param userOpts { state?: string, type?: string } | ||
function M.issuesAndPrs(userOpts) | ||
if u.notInGitRepo() then return end | ||
local defaultOpts = { state = "all", type = "all" } | ||
local opts = vim.tbl_deep_extend("force", defaultOpts, userOpts) | ||
|
||
local repo = u.getRepo() | ||
|
||
-- DOCS https://docs.github.com/en/free-pro-team@latest/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues | ||
local rawJsonUrl = ("https://api.github.com/repos/%s/issues?per_page=100&state=%s&sort=updated"):format( | ||
repo, | ||
opts.state | ||
) | ||
local rawJSON = fn.system { "curl", "-sL", rawJsonUrl } | ||
local issues = vim.json.decode(rawJSON) | ||
if not issues then | ||
u.notify("Failed to fetch issues.", "warn") | ||
return | ||
end | ||
if issues and opts.type ~= "all" then | ||
issues = vim.tbl_filter(function(issue) | ||
local isPR = issue.pull_request ~= nil | ||
local isRightKind = (isPR and opts.type == "pr") or (not isPR and opts.type == "issue") | ||
return isRightKind | ||
end, issues) | ||
end | ||
|
||
if #issues == 0 then | ||
local state = opts.state == "all" and "" or opts.state .. " " | ||
local type = opts.type == "all" and "issues or PRs " or opts.type .. "s " | ||
u.notify(("There are no %s%sfor this repo."):format(state, type), "warn") | ||
return | ||
end | ||
|
||
local title = opts.type == "all" and "Issue/PR" or opts.type | ||
vim.ui.select(issues, { | ||
prompt = " Select " .. title, | ||
kind = "github_issue", | ||
format_item = function(issue) return issueListFormatter(issue) end, | ||
}, function(choice) | ||
if not choice then return end | ||
u.openUrl(choice.html_url) | ||
end) | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
return M |
Oops, something went wrong.