Feat: View + Manage Pipeline (#53)
This MR adds the `.pipeline()` command which opens up the pipeline popup. This popup shows information about the current pipeline and it's jobs, and gives users the ability to re-trigger failed jobs.
This commit is contained in:
committed by
GitHub
parent
4c6dcacfcd
commit
e6e0bf4093
18
README.md
18
README.md
@@ -119,6 +119,17 @@ require("gitlab").setup({
|
|||||||
close = { "<Esc>", "<C-c>" },
|
close = { "<Esc>", "<C-c>" },
|
||||||
submit = { "<CR>", "<Space>" },
|
submit = { "<CR>", "<Space>" },
|
||||||
},
|
},
|
||||||
|
pipeline = {
|
||||||
|
created = "",
|
||||||
|
pending = "",
|
||||||
|
preparing = "",
|
||||||
|
scheduled = "",
|
||||||
|
running = "ﰌ",
|
||||||
|
canceled = "ﰸ",
|
||||||
|
skipped = "ﰸ",
|
||||||
|
success = "✓",
|
||||||
|
failed = "",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -174,6 +185,12 @@ require("gitlab").approve()
|
|||||||
require("gitlab").revoke()
|
require("gitlab").revoke()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can view the status of the pipeline for the current MR. To re-trigger failed jobs in the pipeline manually, use your `settings.popup.perform_action` keybinding:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
require("gitlab").pipeline()
|
||||||
|
```
|
||||||
|
|
||||||
The `add_reviewer` and `delete_reviewer` commands, as well as the `add_assignee` and `delete_assignee` functions, will let you choose from a list of users who are availble in the current project:
|
The `add_reviewer` and `delete_reviewer` commands, as well as the `add_assignee` and `delete_assignee` functions, will let you choose from a list of users who are availble in the current project:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
@@ -209,6 +226,7 @@ vim.keymap.set("n", "<leader>glaa", gitlab.add_assignee)
|
|||||||
vim.keymap.set("n", "<leader>glad", gitlab.delete_assignee)
|
vim.keymap.set("n", "<leader>glad", gitlab.delete_assignee)
|
||||||
vim.keymap.set("n", "<leader>glra", gitlab.add_reviewer)
|
vim.keymap.set("n", "<leader>glra", gitlab.add_reviewer)
|
||||||
vim.keymap.set("n", "<leader>glrd", gitlab.delete_reviewer)
|
vim.keymap.set("n", "<leader>glrd", gitlab.delete_reviewer)
|
||||||
|
vim.keymap.set("n", "<leader>glp", gitlab.pipeline)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ func ApproveHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
c := r.Context().Value("client").(Client)
|
c := r.Context().Value("client").(Client)
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", http.MethodPost)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func DescriptionHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
c := r.Context().Value("client").(Client)
|
c := r.Context().Value("client").(Client)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if r.Method != http.MethodPut {
|
if r.Method != http.MethodPut {
|
||||||
|
w.Header().Set("Allow", http.MethodPut)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ func InfoHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
c := r.Context().Value("client").(Client)
|
c := r.Context().Value("client").(Client)
|
||||||
|
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
|
w.Header().Set("Allow", http.MethodGet)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ func ListDiscussionsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
c := r.Context().Value("client").(Client)
|
c := r.Context().Value("client").(Client)
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", http.MethodPost)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func main() {
|
|||||||
m.Handle("/comment", withGitlabContext(http.HandlerFunc(CommentHandler), c))
|
m.Handle("/comment", withGitlabContext(http.HandlerFunc(CommentHandler), c))
|
||||||
m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c))
|
m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c))
|
||||||
m.Handle("/members", withGitlabContext(http.HandlerFunc(ProjectMembersHandler), c))
|
m.Handle("/members", withGitlabContext(http.HandlerFunc(ProjectMembersHandler), c))
|
||||||
|
m.Handle("/pipeline", withGitlabContext(http.HandlerFunc(PipelineHandler), c))
|
||||||
|
|
||||||
port := fmt.Sprintf(":%s", os.Args[3])
|
port := fmt.Sprintf(":%s", os.Args[3])
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
|
|||||||
109
cmd/pipeline.go
Normal file
109
cmd/pipeline.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PipelineRequest struct {
|
||||||
|
PipelineId int `json:"pipeline_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RetriggerPipelineResponse struct {
|
||||||
|
SuccessResponse
|
||||||
|
Pipeline *gitlab.Pipeline
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetJobsResponse struct {
|
||||||
|
SuccessResponse
|
||||||
|
Jobs []*gitlab.Job
|
||||||
|
}
|
||||||
|
|
||||||
|
func PipelineHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
GetJobs(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
RetriggerPipeline(w, r)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetJobs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
c := r.Context().Value("client").(Client)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var pipelineRequest PipelineRequest
|
||||||
|
err = json.Unmarshal(body, &pipelineRequest)
|
||||||
|
if err != nil {
|
||||||
|
c.handleError(w, err, "Could not read JSON", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs, res, err := c.git.Jobs.ListPipelineJobs(c.projectId, pipelineRequest.PipelineId, &gitlab.ListJobsOptions{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.handleError(w, err, "Could not get pipeline jobs", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
response := GetJobsResponse{
|
||||||
|
SuccessResponse: SuccessResponse{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Message: "Jobs fetched successfully",
|
||||||
|
},
|
||||||
|
Jobs: jobs,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetriggerPipeline(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
c := r.Context().Value("client").(Client)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var pipelineRequest PipelineRequest
|
||||||
|
err = json.Unmarshal(body, &pipelineRequest)
|
||||||
|
if err != nil {
|
||||||
|
c.handleError(w, err, "Could not read JSON", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline, res, err := c.git.Pipelines.RetryPipelineBuild(c.projectId, pipelineRequest.PipelineId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.handleError(w, err, "Could not retrigger pipeline", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := RetriggerPipelineResponse{
|
||||||
|
SuccessResponse: SuccessResponse{
|
||||||
|
Message: "Pipeline retriggered",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
},
|
||||||
|
Pipeline: pipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ func ReplyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", http.MethodPost)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func RevisionsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
|
w.Header().Set("Allow", http.MethodGet)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ func RevokeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", http.MethodPost)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -290,10 +290,8 @@ M.rebuild_unlinked_discussion_tree = function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
M.switch_can_edit_bufs = function(bool)
|
M.switch_can_edit_bufs = function(bool)
|
||||||
vim.api.nvim_buf_set_option(M.unlinked_section_bufnr, 'modifiable', bool)
|
u.switch_can_edit_buf(M.unlinked_section_bufnr, bool)
|
||||||
vim.api.nvim_buf_set_option(M.unlinked_section_bufnr, "readonly", not bool)
|
u.switch_can_edit_buf(M.linked_section_bufnr, bool)
|
||||||
vim.api.nvim_buf_set_option(M.linked_section_bufnr, 'modifiable', bool)
|
|
||||||
vim.api.nvim_buf_set_option(M.linked_section_bufnr, "readonly", not bool)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
M.add_discussion = function(arg)
|
M.add_discussion = function(arg)
|
||||||
|
|||||||
96
lua/gitlab/actions/pipeline.lua
Normal file
96
lua/gitlab/actions/pipeline.lua
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
-- This module is responsible for the MR pipline
|
||||||
|
-- This lets the user see the current status of the pipeline
|
||||||
|
-- and retrigger the pipeline from within the editor
|
||||||
|
local Popup = require("nui.popup")
|
||||||
|
local state = require("gitlab.state")
|
||||||
|
local job = require("gitlab.job")
|
||||||
|
local u = require("gitlab.utils")
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- The function will render the Pipeline state in a popup
|
||||||
|
M.open = function()
|
||||||
|
local pipeline = state.INFO.pipeline
|
||||||
|
|
||||||
|
if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then
|
||||||
|
vim.notify("Pipeline not found", vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local body = { pipeline_id = state.INFO.pipeline.id }
|
||||||
|
job.run_job("/pipeline", "GET", body, function(data)
|
||||||
|
local pipeline_jobs = u.reverse(type(data.Jobs) == "table" and data.Jobs or {})
|
||||||
|
|
||||||
|
local width = string.len(pipeline.web_url) + 10
|
||||||
|
local height = 6 + #pipeline_jobs + 3
|
||||||
|
|
||||||
|
local pipeline_popup = Popup(u.create_popup_state("Loading Pipeline...", width, height))
|
||||||
|
pipeline_popup:mount()
|
||||||
|
|
||||||
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
vim.opt_local.wrap = false
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
|
||||||
|
u.switch_can_edit_buf(bufnr, true)
|
||||||
|
table.insert(lines, string.format("Status: %s (%s)", state.settings.pipeline[pipeline.status], pipeline.status))
|
||||||
|
table.insert(lines, "")
|
||||||
|
table.insert(lines, string.format("Last Run: %s", u.format_date(pipeline.created_at)))
|
||||||
|
table.insert(lines, string.format("Url: %s", pipeline.web_url))
|
||||||
|
table.insert(lines, string.format("Triggered By: %s", pipeline.source))
|
||||||
|
|
||||||
|
table.insert(lines, "")
|
||||||
|
table.insert(lines, "Jobs:")
|
||||||
|
for _, pipeline_job in ipairs(pipeline_jobs) do
|
||||||
|
table.insert(lines,
|
||||||
|
string.format("%s (%s) %s", state.settings.pipeline[pipeline_job.status], pipeline_job.status, pipeline_job.name))
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
M.color_status(pipeline.status, bufnr, lines[1], 1)
|
||||||
|
|
||||||
|
for i, pipeline_job in ipairs(pipeline_jobs) do
|
||||||
|
M.color_status(pipeline_job.status, bufnr, lines[7 + i], 7 + i)
|
||||||
|
end
|
||||||
|
|
||||||
|
pipeline_popup.border:set_text("top", "Pipeline Status", "center")
|
||||||
|
state.set_popup_keymaps(pipeline_popup, M.retrigger)
|
||||||
|
u.switch_can_edit_buf(bufnr, false)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.retrigger = function()
|
||||||
|
local body = { pipeline_id = state.INFO.pipeline.id }
|
||||||
|
if state.INFO.pipeline.status ~= 'failed' then
|
||||||
|
vim.notify("Pipeline is not in a failed state!", vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
job.run_job("/pipeline", "POST", body, function(data)
|
||||||
|
vim.notify("Pipeline re-triggered!", vim.log.levels.INFO)
|
||||||
|
state.INFO.pipeline = data.Pipeline
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.color_status = function(status, bufnr, status_line, linnr)
|
||||||
|
local ns_id = vim.api.nvim_create_namespace("GitlabNamespace")
|
||||||
|
vim.cmd(string.format("highlight default StatusHighlight guifg=%s", state.settings.pipeline[status]))
|
||||||
|
|
||||||
|
local status_to_color_map = {
|
||||||
|
created = 'DiagnosticWarn',
|
||||||
|
pending = 'DiagnosticWarn',
|
||||||
|
preparing = 'DiagnosticWarn',
|
||||||
|
scheduled = 'DiagnosticWarn',
|
||||||
|
running = 'DiagnosticWarn',
|
||||||
|
canceled = 'DiagnosticWarn',
|
||||||
|
skipped = 'DiagnosticWarn',
|
||||||
|
failed = 'DiagnosticError',
|
||||||
|
success = 'DiagnosticOK',
|
||||||
|
}
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_extmark(bufnr, ns_id, linnr - 1, 0,
|
||||||
|
{ end_row = linnr - 1, end_col = string.len(status_line), hl_group = status_to_color_map[status] })
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -7,6 +7,7 @@ local discussions = require("gitlab.actions.discussions")
|
|||||||
local summary = require("gitlab.actions.summary")
|
local summary = require("gitlab.actions.summary")
|
||||||
local assignees_and_reviewers = require("gitlab.actions.assignees_and_reviewers")
|
local assignees_and_reviewers = require("gitlab.actions.assignees_and_reviewers")
|
||||||
local comment = require("gitlab.actions.comment")
|
local comment = require("gitlab.actions.comment")
|
||||||
|
local pipeline = require("gitlab.actions.pipeline")
|
||||||
local approvals = require("gitlab.actions.approvals")
|
local approvals = require("gitlab.actions.approvals")
|
||||||
|
|
||||||
local info = state.dependencies.info
|
local info = state.dependencies.info
|
||||||
@@ -31,6 +32,7 @@ return {
|
|||||||
create_comment = async.sequence({ info, revisions }, comment.create_comment),
|
create_comment = async.sequence({ info, revisions }, comment.create_comment),
|
||||||
create_note = async.sequence({ info }, comment.create_note),
|
create_note = async.sequence({ info }, comment.create_note),
|
||||||
review = async.sequence({ u.merge(info, { refresh = true }) }, function() reviewer.open() end),
|
review = async.sequence({ u.merge(info, { refresh = true }) }, function() reviewer.open() end),
|
||||||
|
pipeline = async.sequence({ info }, pipeline.open),
|
||||||
-- Discussion Tree Actions 🌴
|
-- Discussion Tree Actions 🌴
|
||||||
toggle_discussions = async.sequence({ info }, discussions.toggle),
|
toggle_discussions = async.sequence({ info }, discussions.toggle),
|
||||||
edit_comment = async.sequence({ info }, discussions.edit_comment),
|
edit_comment = async.sequence({ info }, discussions.edit_comment),
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ M.settings = {
|
|||||||
removed_file = "",
|
removed_file = "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
pipeline = {
|
||||||
|
created = "",
|
||||||
|
pending = "",
|
||||||
|
preparing = "",
|
||||||
|
scheduled = "",
|
||||||
|
running = "ﰌ",
|
||||||
|
canceled = "ﰸ",
|
||||||
|
skipped = "ﰸ",
|
||||||
|
success = "✓",
|
||||||
|
failed = "",
|
||||||
|
},
|
||||||
dialogue = {
|
dialogue = {
|
||||||
focus_next = { "j", "<Down>", "<Tab>" },
|
focus_next = { "j", "<Down>", "<Tab>" },
|
||||||
focus_prev = { "k", "<Up>", "<S-Tab>" },
|
focus_prev = { "k", "<Up>", "<S-Tab>" },
|
||||||
|
|||||||
@@ -49,14 +49,18 @@ M.format_date = function(date_string)
|
|||||||
|
|
||||||
local time_diff = current_date - date
|
local time_diff = current_date - date
|
||||||
|
|
||||||
|
local function pluralize(num, word)
|
||||||
|
return num .. string.format(" %s", word) .. (num > 1 and "s" or '') .. " ago"
|
||||||
|
end
|
||||||
|
|
||||||
if time_diff < 60 then
|
if time_diff < 60 then
|
||||||
return time_diff .. " seconds ago"
|
return pluralize(time_diff, "second")
|
||||||
elseif time_diff < 3600 then
|
elseif time_diff < 3600 then
|
||||||
return math.floor(time_diff / 60) .. " minutes ago"
|
return pluralize(math.floor(time_diff / 60), "minute")
|
||||||
elseif time_diff < 86400 then
|
elseif time_diff < 86400 then
|
||||||
return math.floor(time_diff / 3600) .. " hours ago"
|
return pluralize(math.floor(time_diff / 3600), "hour")
|
||||||
elseif time_diff < 2592000 then
|
elseif time_diff < 2592000 then
|
||||||
return math.floor(time_diff / 86400) .. " days ago"
|
return pluralize(math.floor(time_diff / 86400), "day")
|
||||||
else
|
else
|
||||||
local formatted_date = os.date("%A, %B %e", date)
|
local formatted_date = os.date("%A, %B %e", date)
|
||||||
return formatted_date
|
return formatted_date
|
||||||
@@ -254,4 +258,17 @@ M.get_win_from_buf = function(bufnr)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M.switch_can_edit_buf = function(buf, bool)
|
||||||
|
vim.api.nvim_buf_set_option(buf, 'modifiable', bool)
|
||||||
|
vim.api.nvim_buf_set_option(buf, "readonly", not bool)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.reverse = function(list)
|
||||||
|
local rev = {}
|
||||||
|
for i = #list, 1, -1 do
|
||||||
|
rev[#rev + 1] = list[i]
|
||||||
|
end
|
||||||
|
return rev
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
Reference in New Issue
Block a user