diff --git a/README.md b/README.md index 41a64f2..71c0581 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Please follow along with the [initial release discussion](https://github.com/kid ## ⚡️ Requirements -- Install [http.nvim](https://github.com/jcdickinson/http.nvim) +- Instead of using the Jira API directly in all cases, this plugin leverages the [jira-cli tool](https://github.com/ankitpokhrel/jira-cli) to interact with Jira. You'll need to install it and configure it with your Jira credentials in order to use some of the features of this plugin. ## 📦 Installation @@ -82,6 +82,7 @@ There is only an Jira [arguments] command. |---|---|---| | issue | view [issue_id] | View the given issue, if none provided it will attempt to extract one out of the current git branch (disabled via `use_git_branch_issue_id`), else falls back to a prompt | | | transition [issue_id] [transition_name] | Transition the ticket to a given status. Will attempt to extract issue ID from git branch, and will prompt if no options given +| | create | Create a new issue. Will prompt for all required fields. It will also prompt you to create a branch with the created issue ID in the name. Additionally, the transition command has a Telescope picker. @@ -95,4 +96,5 @@ There are no default mappings, but you can create your own. Here's an example: local t = require 'telescope' vim.keymap.set('n', 'jv', 'Jira issue view', {}) vim.keymap.set('n', 'jt', t.extensions.jira.transitions, {}) +vim.keymap.set('n', 'jc', 'Jira issue create', {}) ``` diff --git a/lua/jira/api_client.lua b/lua/jira/api_client.lua index 9b64674..37d6079 100644 --- a/lua/jira/api_client.lua +++ b/lua/jira/api_client.lua @@ -1,6 +1,7 @@ local utils = require 'jira.utils' local config = require 'jira.config' local curl = require 'plenary.curl' +local _, Job = pcall(require, 'plenary.job') local M = {} local transition_cache = {} @@ -81,4 +82,45 @@ M.transition_issue = function(transition_id, issue_id) }) end +-- Creates an issue with the given type, summary, and optional description file path +-- @param issue table - the issue to create +-- @param issue.type string - the type of the issue +-- @param issue.summary string - the summary of the issue +-- @param issue.descriptionFile string - the path to the file containing the description +-- @return table - the issue id and link +-- e.g. { issue_id = 'ABC-1234', link = 'https://blah.atlassian.net/ABC-1234' } +M.create_issue = function(issue) + local type = issue.type + local summary = issue.summary + local descriptionFilePath = issue.descriptionFile + assert(type, 'Missing issue type') + assert(summary, 'Missing issue summary') + + local args = { 'issue', 'create', '-t', type, '-s', summary } + if descriptionFilePath then + table.insert(args, '-T') + table.insert(args, descriptionFilePath) + end + + local job = Job:new { + enable_recording = true, + interactive = false, + command = 'jira', + args = args, + on_exit = function(_, code) + if code ~= 0 then + vim.nofity('Error creating issue', vim.log.levels.ERROR) + end + end, + } + job:sync() + local lines = job:result() + -- last line is the issue id + -- e.g. 'Issue created: ABC-1234' + -- extract the issue id: + -- 'ABC-1234' + local issue_id = lines[#lines]:match '([A-Z]+%-[0-9]+)' + return { issue_id = issue_id, link = lines[#lines] } +end + return M diff --git a/lua/jira/commands.lua b/lua/jira/commands.lua index 0783f74..f631740 100644 --- a/lua/jira/commands.lua +++ b/lua/jira/commands.lua @@ -1,6 +1,7 @@ local api_client = require 'jira.api_client' local utils = require 'jira.utils' local M = {} +local config = require 'jira.config' M.setup = function() vim.api.nvim_create_user_command('Jira', function(opts) @@ -16,6 +17,9 @@ M.setup = function() transition = function(transition_name, issue_id) M.transition_issue_name(transition_name, issue_id) end, + create = function(args) + M.create_issue(args) + end, }, } end @@ -106,4 +110,108 @@ function M.transition_issue_name(transition_name, issue_id) end end +local function get_char_input() + local char = vim.fn.getchar() + -- if char is escape + if char == 27 then + return nil + end + return vim.fn.nr2char(char) +end + +local function clear_prompt() + vim.api.nvim_command 'normal! :' +end + +-- create a git branch with the issue id +-- @param issue_id string - the issue id to use for the branch +-- @param branch_suffix string - the suffix to append to the branch name +local create_git_branch = function(issue_id, branch_suffix) + local branch_from = config.get_config().git_trunk_branch + vim.ui.input({ + prompt = 'Enter branch to create branch from [esc to cancel]: ', + default = branch_from or 'main', + }, function(value) + branch_from = value + end) + + if not branch_from or branch_from == '' then + return + end + + utils.create_git_branch(issue_id, branch_suffix, branch_from) +end + +function M.create_issue(issueType) + if not issueType then + vim.ui.input({ + prompt = 'Issue type: ', + default = 'Task', + }, function(value) + issueType = value + end) + end + if not issueType or issueType == '' then + return + end + + local summary + vim.ui.input({ + prompt = 'Summary: ', + }, function(value) + summary = value + end) + + if not summary or summary == '' then + return + end + + clear_prompt() + print 'Edit description? (y/N)' + local res = get_char_input() + clear_prompt() + -- if user cancels + if res == nil then + return + end + local edit_description = false + if res:match '\r' or res:match '\n' or res:match 'n' or res:match 'N' then + edit_description = false + end + if res:match 'y' or res:match 'Y' then + edit_description = true + end + + if not edit_description then + local r = api_client.create_issue { + type = issueType, + summary = summary, + } + vim.notify(r.link) + create_git_branch(r.issue_id, summary) + return + end + + local tempfile = vim.fn.tempname() + local bufnr = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(bufnr, tempfile) + vim.api.nvim_buf_set_option(bufnr, 'filetype', 'markdown') + vim.cmd 'split' + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win, bufnr) + vim.api.nvim_create_autocmd('BufWritePost', { + buffer = bufnr, + callback = function() + local r = api_client.create_issue { + type = issueType, + summary = summary, + descriptionFile = tempfile, + } + vim.notify(r.link) + create_git_branch(r.issue_id, summary) + vim.api.nvim_buf_delete(bufnr, { force = true }) + end, + }) +end + return M diff --git a/lua/jira/config.lua b/lua/jira/config.lua index c02caa7..72dd2ec 100644 --- a/lua/jira/config.lua +++ b/lua/jira/config.lua @@ -7,6 +7,7 @@ M.defaults = { token = vim.env.JIRA_API_TOKEN, }, use_git_branch_issue_id = true, + git_branch_prefix = 'feature/', } local config = {} @@ -15,6 +16,10 @@ function M.get_config() return config or M.defaults end +function M.set_current_issue(issue_id) + config.current_issue = issue_id +end + function M.setup(user_config) user_config = user_config or {} config = vim.tbl_deep_extend('force', M.defaults, user_config) diff --git a/lua/jira/utils.lua b/lua/jira/utils.lua index ce05ac8..80ab582 100644 --- a/lua/jira/utils.lua +++ b/lua/jira/utils.lua @@ -1,4 +1,6 @@ local M = {} +local _, Job = pcall(require, 'plenary.job') +local config = require 'jira.config' function Set(list) local set = {} @@ -324,4 +326,23 @@ M.get_issue_id = function(issue_id) return issue_id end +M.create_git_branch = function(issue_id, branch_suffix, branch_from) + -- replace whitespace with hyphens + branch_suffix = branch_suffix:gsub('%s', '-') + -- lowercase and strip all non-alphanumeric characters + branch_suffix = branch_suffix:gsub('[^%w-]+', ''):lower() + + local branch_prefix = config.get_config().git_branch_prefix + local branch = branch_prefix .. issue_id .. '/' .. branch_suffix + Job:new({ + command = 'git', + args = { 'branch', branch, branch_from or 'main' }, + on_exit = function(_, code) + if code ~= 0 then + vim.notify('Error creating branch', vim.log.levels.ERROR) + end + end, + }):sync() +end + return M