A Neovim plugin for managing GitHub/Bitbucket/GitLab PRs and Jira/GitHub/GitLab issues without leaving your editor.
[!CAUTION] Still in early development, will have breaking changes!
{
"emrearmagan/atlas.nvim",
dependencies = {
"MeanderingProgrammer/render-markdown.nvim", -- optional but recommended
"esmuellert/codediff.nvim", -- optional (PullRequest diff)
"sindrets/diffview.nvim", -- optional (PullRequest diff - alternative)
},
opts = {
pulls = {
providers = {
---@type AtlasBitbucketConfig
bitbucket = {}, -- See configuration below
---@type AtlasGitHubConfig
github = {}, -- See configuration below
---@type AtlasGitLabPullsConfig
gitlab = {}, -- See configuration below
},
},
issues = {
providers = {
---@type AtlasJiraIssuesConfig
jira = {}, -- See configuration below
---@type AtlasGitHubIssuesConfig
github = {}, -- See configuration below
---@type AtlasGitLabIssuesConfig
gitlab = {}, -- See configuration below
},
},
},
}
use {
"emrearmagan/atlas.nvim",
config = function()
require("atlas").setup({
pulls = {
providers = {
---@type AtlasBitbucketConfig
bitbucket = {}, -- See configuration below
---@type AtlasGitHubConfig
github = {}, -- See configuration below
---@type AtlasGitLabPullsConfig
gitlab = {}, -- See configuration below
},
},
issues = {
providers = {
---@type AtlasJiraIssuesConfig
jira = {}, -- See configuration below
---@type AtlasGitHubIssuesConfig
github = {}, -- See configuration below
---@type AtlasGitLabIssuesConfig
gitlab = {}, -- See configuration below
},
},
})
end
}
[!tip] It's a good idea to run
:checkhealth atlasto see if everything is set up correctly.
0.10+*.atlassian.net)api.bitbucket.org)gh) authenticated with gh auth logingitlab.com or self-hosted), Personal Access Token with api scope[!NOTE] I have only tested this with my personal and work accounts. If you encounter any issues, please feel free to open an issue. See: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
I have also not tested with self-hosted GitLab instances, but in theory it should work. If it doesn't, feel free to open an issue. If it does work, please remove this note :)
:AtlasIssues [provider] - Open Atlas issues domain:AtlasPulls [provider] - Open Atlas pulls domain:AtlasCreatePR - Create a pull request from the current branch:AtlasCreateIssue - Create an issue (GitHub / Jira):AtlasSearch [provider] - Pick a configured provider and prompt its search:AtlasClearCache - Clear Atlas disk and memory cache:AtlasLogs - Toggle Atlas logs[!NOTE] If you're only looking for Jira support, check out https://github.com/letieu/jira.nvim. This plugin was the main inspiration for this project.
Jira support is included here mainly because I wanted a single tool that works with both Atlassian products.
[!IMPORTANT] The markdown editor for issue descriptions and comments is still experimental and may not work perfectly in all cases. You can toggle between markdown and ADF view in the overview tab to see the raw ADF content and how it translates to markdown. If you encounter any issues with the markdown editor, please open an issue with details.
issues = {
max_results = 100,
with_relationships = true, -- Fetch parent/subissue relationships for plain issue tree views.
custom_actions = {}, -- See Custom Actions below.
providers = {
jira = {
base_url = "https://your-site.atlassian.net",
email = "you@example.com",
--- See: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
token = "your_jira_api_token",
auth_method = "basic", -- "basic" or "bearer", defaults to "basic". If using bearer, set `token` to your API token.
api_type = "cloud", -- either "cloud" or "server", defaults to "cloud". Cloud API is v3, server API is v2
cache_ttl = 300,
project_config = {
-- The Jira custom field ID used for story points. Defaults to "customfield_10016".
story_points_field = "customfield_10016",
KAN = {
customfield_10003 = {
name = "Approvers",
format = function(value)
if type(value) ~= "table" or #value == 0 then
return nil -- nil hides the field
end
return table.concat(value, ", ")
end,
hl_group = "AtlasChipActive",
display = "chip", -- "chip" or "table"
},
},
},
---@type AtlasJiraViewConfig[]
views = {
{
name = "My Board",
key = "M",
layout = "plain",
jql = "project = KAN AND assignee = currentUser() ORDER BY updated DESC",
},
{
name = "Team Board",
key = "T",
layout = "compact",
jql = "project = KAN ORDER BY updated DESC",
},
},
},
},
},
issues = {
providers = {
github = {
cache_ttl = 300,
---@type AtlasGitHubIssuesViewConfig[]
views = {
{
name = "Assigned",
key = "1",
layout = "plain",
search = "assignee:@me is:open",
},
{
name = "Created",
key = "2",
layout = "compact",
search = "author:@me is:open",
},
{
name = "Mentions",
key = "3",
layout = "plain",
search = "mentions:@me is:open",
},
},
},
},
},
Auth uses a Personal Access Token with the api scope. Set base_url to https://gitlab.com or your self-hosted instance.
issues = {
providers = {
gitlab = {
base_url = "https://gitlab.com",
token = os.getenv("GITLAB_TOKEN") or "",
cache_ttl = 300,
---@type AtlasGitLabIssuesViewConfig[]
views = {
{
name = "Assigned",
key = "1",
scope = "assigned_to_me",
state = "opened",
},
{
name = "Created",
key = "2",
scope = "created_by_me",
state = "opened",
},
{
name = "All open",
key = "3",
scope = "all",
state = "opened",
-- Anything not covered by the explicit fields below can be passed via `extra_params`.
extra_params = { ["not[labels]"] = "wontfix" },
},
},
},
},
},
You can add custom issue actions under issues.custom_actions.
Context type:
---@class AtlasIssuesCustomActionContext
---@field issue Issue|nil
---@field user IssueUser|nil
Example:
issues = {
custom_actions = {
{
id = "copy_branch_name",
label = "Copy branch name",
---@param issue Issue
---@param ctx AtlasIssuesCustomActionContext
---@param done fun(ok: boolean|nil, message: string|nil)
run = function(issue, ctx, done)
local branch = string.format("%s/%s", issue.key, issue.summary:lower():gsub("%s+", "-"))
vim.fn.setreg("+", branch)
done(true, "Copied: " .. branch)
end,
},
},
}
:AtlasCreatePR)pulls = {
diff = {
-- Command must support range input: origin/<destination>...origin/<source>
open_cmd = "DiffviewOpen", -- e.g. "DiffviewOpen" or "CodeDiff", defaults to nil.
},
repo_config = {
-- Maps `workspace/repo` to local paths. Used for checkout and custom actions.
paths = {
["your-workspace/*"] = "~/code/repos/*",
["your-workspace/atlas"] = "~/code/atlas",
},
settings = {
["your-workspace/atlas"] = {
readme = "README.md", -- optional, defaults to README.md
pr_template = ".github/pull_request_template.md", -- optional, defaults to .github/pull_request_template.md
},
},
},
custom_actions = {}, -- See Custom Actions below.
},
pulls = {
providers = {
github = {
cache_ttl = 300,
---@type AtlasGitHubViewConfig[]
views = {
{
name = "My PRs",
key = "1",
layout = "plain",
search = "author:@me sort:updated-desc",
},
{
name = "Team",
key = "2",
layout = "compact",
search = "org:your-org sort:updated-desc",
},
{
name = "Repo",
key = "3",
layout = "plain",
search = "repo:your-org/your-repo",
},
},
},
},
},
pulls = {
providers = {
bitbucket = {
user = os.getenv("BITBUCKET_USER") or "",
token = os.getenv("BITBUCKET_TOKEN") or "",
cache_ttl = 300,
---@type AtlasBitbucketViewConfig[]
views = {
{
name = "Me",
key = "M",
layout = "compact",
repos = {
{ workspace = "your-workspace", repo = "atlas" },
},
---@param pr PullRequest
---@param ctx { user: PullsUser|nil }
filter = function(pr, ctx)
local user = ctx.user
return pr.author and user and pr.author.id == user.id
end,
},
{
name = "Team",
key = "1",
layout = "plain", -- "compact" or "plain"
repos = {
{ workspace = "your-workspace", repo = "atlas" },
{ workspace = "your-workspace", repo = "other-repo" },
},
},
},
},
},
},
Auth uses a Personal Access Token with the api scope. Set base_url to https://gitlab.com or your self-hosted instance.
pulls = {
providers = {
gitlab = {
base_url = "https://gitlab.com",
token = os.getenv("GITLAB_TOKEN") or "",
cache_ttl = 300,
---@type AtlasGitLabPullsViewConfig[]
views = {
{
name = "Assigned",
key = "1",
scope = "assigned_to_me",
},
{
name = "Reviewing",
key = "3",
scope = "all",
extra_params = { reviewer_id = "Me" },
},
-- Single project
{
name = "GitLab",
key = "G",
project = "gitlab-org/gitlab",
},
-- Whole group, all projects under it
{
name = "GitLab Org",
key = "O",
group = "gitlab-org",
},
},
},
},
},
You can add custom PR actions under pulls.custom_actions.
Context type:
---@class AtlasPullsCustomActionContext
---@field repo_path string|nil
---@field pr PullRequest
Example:
pulls = {
repo_config = {
paths = {
["your-workspace/*"] = "~/code/repos/*",
},
settings = {},
},
custom_actions = {
{
id = "open_tmux_window",
label = "Open repo in tmux window",
confirmation = true, -- present a confirmation prompt before running the action
---@param pr PullRequest
---@param ctx AtlasPullsCustomActionContext
---@param done fun(ok: boolean|nil, message: string|nil)
run = function(_, ctx, done)
if not ctx.repo_path then
done(false, "No repo path")
return
end
vim.system({ "tmux", "new-window", "-c", ctx.repo_path }, { text = true }, function(res)
vim.schedule(function()
if res.code ~= 0 then
done(false, "Failed to open tmux window")
return
end
done(true, "Opened tmux window")
end)
end)
end,
},
},
providers = {
...,
},
}
Use :AtlasSearch [provider] to search configured providers.
:AtlasSearch jira opens a JQL prompt with full completion (fields, operators, values).
project = KAN AND assignee = currentUser() ORDER BY updated DESC
summary ~ "login bug"
:AtlasSearch github opens a GitHub search prompt with completion (is:, repo:, author:, label: etc.).
is:pr is:open author:@me
is:issue label:bug
:AtlasSearch bitbucket asks for a workspace and a repository and opens the UI scoped to that repo.
Set an action to false to disable it, or set it to a list to add aliases.
keymaps = {
ui = {
help = "g?",
close = "q", -- false would disable it
toggle_panel = "p", -- { "p", "k" } would add aliases
toggle_fold = "za",
toggle_all_folds = "zA",
previous_panel_tab = "<S-Tab>",
next_panel_tab = "<Tab>",
open_notifications = "N",
notifications_mark_read = "r",
notifications_mark_done = "d",
notifications_refresh = "R",
toggle_subscription = "gS",
refresh = "r",
refresh_view = "R",
open_actions = "A",
open_in_browser = "gx",
copy_url = "Y",
show_details = "K",
search = "?",
},
issues = {
copy_key = "y",
transition_issue = "gs",
change_assignee = "ga",
change_reporter = "gr",
edit_issue = "ge",
create_issue = "c",
},
pulls = {
copy_id = "y",
open_diff = "gd",
checkout = "gc",
next_hunk = "]h",
previous_hunk = "[h",
filter_status_open = "gpo",
filter_status_merged = "gpm",
filter_status_declined = "gpd",
},
},
Thanks go to these wonderful people (emoji key):
MIT License - see LICENSE for details.