Review pull requests and manage issues across GitHub, GitLab, Bitbucket and Jira without leaving your editor.
Quick links
[!CAUTION] Still in early development, will have breaking changes!
---@module "atlas"
{
"emrearmagan/atlas.nvim",
dependencies = {
"nvim-tree/nvim-web-devicons", -- optional but recommended
"MeanderingProgrammer/render-markdown.nvim", -- optional but recommended
"esmuellert/codediff.nvim", -- optional (PullRequest diff)
"sindrets/diffview.nvim", -- optional; or "dlyongemallo/diffview-plus.nvim"
},
-- See Configuration below
---@type AtlasConfig
opts = {},
}
vim.pack.add({
"https://github.com/emrearmagan/atlas.nvim",
})
-- See Configuration below
require("atlas").setup({})
0.10+git and curl on $PATH*.atlassian.net) or Jira Server REST API v2api.bitbucket.org)gh) authenticated with gh auth logingitlab.com or self-hosted), Personal Access Token with api scope[!tip] It's a good idea to run
:checkhealth atlasto see if everything is set up correctly.
Run :Atlas review in a Git repository to pick a pull request, or pass a PR URL directly. Atlas opens it in your configured diff viewer.
[!NOTE] Alternative viewers: CodeDiff, Diffview, and Diffview-plus can display Atlas comment, task, and local-note overlays, but their integrations rely on plugin internals and may break after upstream changes.
Local notes let you leave something on a diff without posting it to the pull request. Each note is attached to a file and line and can be an ISSUE, SUGGESTION, NOTE, or PRAISE.
For scripts, use bin/atlas-notes. Notes added there appear in AtlasDiff, CodeDiff, Diffview, Diffview-plus, and :Atlas notes:
./bin/atlas-notes add \
--target https://github.com/owner/repository/pull/123 \
--file lua/review_queue.lua --line 19 \
--context "local item = queue[index]" \
--type suggestion --body "Should this be a bool?"
My dotfiles include a Pi extension that wraps this script so review agents can list and add notes.
Use your configured language servers for hover and go-to-definition on the new side of AtlasDiff.
pulls = {
diff = {
lsp = {
enabled = true,
-- link = { "node_modules", ".venv" }, -- optional dependencies
},
},
}
Atlas uses a temporary worktree and removes it when the diff closes. Set link to symlink dependency folders from your local checkout. See Pulls Configuration for all options.
Browse a repository's README, pull requests, issues, builds, branches, tags and releases. Use :Atlas browse . for the current repository, or pass a repository URL.
View pipelines and their jobs, inspect their status, and read job logs directly in Atlas.
Use :Atlas pipelines <target> with a branch name, PR URL or number (123, #123, or GitLab !123), or a build URL. Branch names use the local repository; :Atlas pipelines . opens builds for the current branch.
Atlas uses your provider's CI by default. Set ci.backend to use your own. For Bamboo on Bitbucket, use require("atlas.pulls.pipelines.bamboo").new(opts) with host, user, and password.
providers = {
github = {
ci = {
backend = {
fetch = function(context, opts, done)
-- Fetch pipelines with their stages and jobs.
done({}, nil)
end,
fetch_job = function(context, pipeline, job, done)
-- Fetch the updated job, including any steps.
done(job, nil)
end,
fetch_job_log = function(context, pipeline, job, done)
done({ raw = "Your log output here" }, nil)
end,
parse = function(log)
-- Return cleaned lines or your own nested groups.
return log.lines
end,
},
highlights = {
{ pattern = "^FAIL%s", level = "error" },
{ pattern = "deprecated", level = "warn", hl_group = "DiagnosticWarn" },
},
},
},
}
ci.highlights controls log highlighting using Lua patterns. Set level for a severity color or hl_group for an existing Neovim highlight group.
Add project-specific actions to pull requests and issues. Custom actions receive the current item and provider context, making it possible to call local scripts, open repositories in tmux, copy branch names, or connect Atlas to your own tooling.
pulls = {
repo_config = {
paths = {
["your-workspace/*"] = "~/code/repos/*",
},
settings = {},
},
custom_actions = {
{
id = "show_repo_status",
label = "Show repository status",
icon = "",
confirmation = true,
---@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
local output = ctx.output("Repository status")
output:write("Checking " .. ctx.repo_path)
output:run({ "git", "status", "--short" }, function(code)
if code ~= 0 then
done(false, "Failed to read repository status")
return
end
done(true, "Repository status loaded")
end, {
cwd = ctx.repo_path,
})
end,
},
},
},
issues = {
custom_actions = {
{
id = "copy_branch_name",
label = "Copy branch name",
icon = "",
---@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.title:lower():gsub("%s+", "-"))
vim.fn.setreg("+", branch)
done(true, "Copied: " .. branch)
end,
},
},
},
Use ctx.output(title) to show output from a custom action:
output:write("Loading...")
output:run(cmd, on_exit, { cwd = "/repo" })
Use :Atlas create [pr|issue] to create a pull request from the current branch or a new issue. For pull requests, Atlas can fill the description from your template or commits.
Save searches as bookmarks, or press * to star a pull request or issue. Both appear alongside your configured views.
:Atlas diff main...HEAD.{
ui = {
-- Global statusline for Atlas. See the Statusline section below.
statusline = true,
-- "auto", "default", "snacks", or "fzf-lua".
picker = "auto",
-- Make the main Atlas dashboard a listed buffer.
listed_buffer = false,
},
providers = {
---@type AtlasGitHubConfig
github = {
-- hostname = "github.company.com", -- Defaults to GH_HOST, then github.com.
cache_ttl = 300, -- Set to 0 to disable caching.
},
---@type AtlasGitLabConfig
gitlab = {
base_url = "https://gitlab.com",
-- Personal Access Token with `api` scope:
-- https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
token = vim.env.GITLAB_TOKEN,
cache_ttl = 300, -- Set to 0 to disable caching.
},
---@type AtlasBitbucketConfig
bitbucket = {
user = vim.env.BITBUCKET_USER,
token = vim.env.BITBUCKET_TOKEN,
cache_ttl = 300, -- Set to 0 to disable caching.
},
---@type AtlasJiraConfig
jira = {
base_url = "https://your-site.atlassian.net",
email = "you@example.com", -- Required for basic authentication only.
--- 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, -- Set to 0 to disable caching.
},
},
-- See Pulls Configuration below.
pulls = { },
-- See Issue Configuration below.
issues = { },
}
Atlas comes with its own statusline for key hints, loading progress, and notifications. Keeping it enabled is recommended because most interaction and feedback goes through it.
If you use lualine, disable its statusline for Atlas buffers so it does not replace the Atlas statusline:
require("lualine").setup({
options = {
disabled_filetypes = {
statusline = { "atlas" },
winbar = {},
},
},
})
At some point there will probably an extension for lualine.
:Atlas - Pick a command:Atlas pulls [provider] - Open a pull-request provider dashboard:Atlas issues [provider] - Open an issue provider dashboard:Atlas review [pull-request-url] - Review a pull request with the configured diff viewer:Atlas diff [target] - Open a Git range or pull request in native AtlasDiff:Atlas pipelines [target|.] - Open pipelines by branch name, PR URL or number, or build URL; . uses the current branch:Atlas create [pr|issue] - Create a pull request or issue:Atlas search [provider] - Search configured pull-request and issue providers:Atlas open [target|.] - Open a provider URL, Jira key, a PR/issue number in the current repository, or the current repository:Atlas browse [repository URL|.] [page] - Open the repository browser, e.g. :Atlas browse . branches:Atlas notes [target] - Inspect local review notes:Atlas clear [cache|notes|stars] - Clear all Atlas data or only cached data and cloned repositories, local review notes, or starred items:Atlas logs - Toggle Atlas logs:AtlasDiff <base>...<head> - Open a Git range in native AtlasDiff directly:AtlasDiff <pull-request-url> - Open a pull request in native AtlasDiff directlyUse :Atlas pulls [provider] to browse and manage pull requests from GitHub, Bitbucket, and GitLab.
Shared authentication and endpoints are configured in the top-level providers table.
pulls = {
delete_notes = false, -- Delete local PR notes after approval or merge.
default_merge_method = "merge", -- "merge" or "squash".
default_delete_branch = false,
git_transport = "https", -- "https" or "ssh" for Atlas-managed Git remotes.
-- Replaces the built-in Conventional Comments templates.
comment_templates = {
insert_mode = true, -- Enter Insert mode after applying a template.
items = {
{ label = "Suggestion", text = "suggestion: " },
{ label = "Issue", text = "issue: " },
{ label = "Nitpick", text = "nitpick: " },
},
},
diff = {
-- Any command that accepts explicit <base>...<head> Git revisions.
open_cmd = "AtlasDiff", -- default; for example "DiffviewOpen" or "CodeDiff".
comment_display = "virtual_lines", -- "virtual_lines" or compact "virtual_text" hints.
review_panel = {
hidden = true, -- Set false to show the review panel when a diff opens.
height = 10,
},
-- AtlasDiff options; external viewers use their own configuration.
layout = "inline", -- "inline" or "side-by-side".
compact = true, -- Start with only changed hunks and surrounding context visible.
compact_context_lines = 3, -- Context lines shown around hunks in compact mode.
lsp = {
-- Back the new side of the diff with a detached worktree at the PR head so it is made of
-- real files and your language servers attach to it (AtlasDiff only). Off by default.
enabled = false,
-- Defaults to `stdpath("cache")/atlas/worktrees/<repo>/pr-<id>` (or `<repo>/<sha>` without a PR).
-- May be an absolute path, or a function receiving
-- { repo_root, repo_full_name, pr_id, head_sha, default } that returns a path or nil.
dir = nil,
-- Directories symlinked from your checkout into the worktree so servers can resolve
-- dependencies. These are the same directories on disk, not copies.
link = {}, -- e.g. { "node_modules", ".venv" }
},
explorer = {
grouped = true, -- Group changed files by directory.
hidden = false,
show_commits = false, -- Set true to show commits below changed files initially.
width = 40,
initial_focus = "explorer", -- "explorer" or "diff".
preview = false, -- Show a file as soon as the explorer cursor moves onto it.
ignore = { ".git/**", ".jj/**" },
},
},
repo_config = {
-- Maps `workspace/repo` to local paths. Used for checkout, diffs, 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 :help atlas-custom-actions.
},
pulls = {
---@type AtlasGitHubPullsConfig
github = {
---@type AtlasGitHubViewConfig[]
views = {
{
name = "My PRs",
key = "1",
layout = "plain", -- "compact", "grouped", or "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 = "grouped",
search = "repo:your-org/your-repo",
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["Drafts"] = "is:pr is:draft author:@me",
["Recently merged"] = "is:pr is:merged author:@me sort:updated-desc",
["Review requested"] = "is:pr is:open review-requested:@me",
},
},
},
},
pulls = {
---@type AtlasBitbucketPullsConfig
bitbucket = {
---@type AtlasBitbucketViewConfig[]
views = {
{
name = "Me",
key = "M",
layout = "compact", -- "compact", "grouped", or "plain"
-- https://developer.atlassian.com/cloud/bitbucket/rest/#filter-and-sort-api-objects
search = 'repo:your-workspace/standalone-repo project:your-workspace/CORE author.nickname = "your-name"',
},
{
name = "Team",
key = "1",
layout = "grouped",
search = 'project:your-workspace/TEAM destination.branch.name = "main"',
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["Atlas"] = {
layout = "grouped",
search = 'repo:your-workspace/atlas project:your-workspace/ATLAS title ~ "atlas"',
},
},
},
},
},
pulls = {
---@type AtlasGitLabPullsConfig
gitlab = {
---@type AtlasGitLabPullsViewConfig[]
views = {
{
name = "Assigned",
key = "1",
layout = "grouped", -- "compact", "grouped", or "plain"
scope = "assigned_to_me",
},
{
name = "Reviewing",
key = "3",
scope = "reviews_for_me",
},
-- Single project
{
name = "GitLab",
key = "G",
project = "gitlab-org/gitlab",
extra_params = { target_branch = "main" },
},
-- Whole group, all projects under it
{
name = "GitLab Org",
key = "O",
group = "gitlab-org",
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["Reviewing"] = { scope = "reviews_for_me" },
["Created by me"] = { scope = "all", author_username = "me" },
},
},
},
},
Use :Atlas issues [provider] to browse and manage Jira, GitHub, and GitLab issues.
Shared authentication and endpoints are configured in the top-level providers table.
issues = {
with_relationships = true, -- Fetch parent/subissue relationships for plain issue tree views.
custom_actions = {}, -- See :help atlas-custom-actions.
}
[!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 = {
---@type AtlasJiraIssuesConfig
jira = {
---@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",
},
},
bookmarks = {
key = "J", -- default
label = "JQL", -- default
items = {
["Backlog"] = "project = KAN AND statusCategory != Done AND (sprint IS EMPTY OR sprint NOT IN openSprints()) ORDER BY Rank ASC",
["Next sprint"] = "project = KAN AND sprint in futureSprints() ORDER BY Rank ASC",
["My open"] = "assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC",
},
},
project_config = {
-- The Jira custom field ID used for story points. Defaults to "customfield_10016".
story_points_field = "customfield_10016",
issue_types = {
["Maintenance"] = { icon = "", hl_group = "AtlasTextWarning" },
["Infrastructure"] = { icon = "", hl_group = "AtlasLogInfo" },
},
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"
},
},
},
},
},
issues = {
---@type AtlasGitHubIssuesConfig
github = {
---@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",
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["Bugs"] = "is:issue is:open label:bug",
["Recently closed"] = "is:issue is:closed author:@me sort:updated-desc",
},
},
},
},
issues = {
---@type AtlasGitLabIssuesConfig
gitlab = {
---@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" },
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["No labels"] = { scope = "all", state = "opened",
extra_params = { ["not[labels]"] = "*" } },
["Closed"] = { scope = "created_by_me", state = "closed" },
},
},
},
},
Atlas emits these User events after the corresponding cleanup or setup has completed:
AtlasUIClosed for the main pulls/issues dashboard.AtlasDiffOpened and AtlasDiffClosed for the native AtlasDiff view.AtlasReviewAttached and AtlasReviewDetached for Atlas review overlays in AtlasDiff, CodeDiff, and Diffview.Set an action to false to disable it, or set it to a list to add aliases.
keymaps = {
ui = {
help = "g?", -- { "g?", "<leader>?" } would add aliases
close = "q", -- false would disable it
next_item = "j",
previous_item = "k",
first_item = "gg",
last_item = "G",
select = "<CR>",
submit = "<C-s>",
delete = "dd",
comments = {
add = { "a", "i" },
reply = "c",
edit = "e",
react = "gr",
},
toggle_panel = "p",
toggle_fold = "za",
toggle_all_folds = "zA",
previous_panel_tab = "<S-Tab>",
next_panel_tab = "<Tab>",
notifications = {
open = "N",
mark_read = "r",
mark_done = "d",
},
toggle_subscription = "gS",
toggle_star = "*",
refresh = "r",
refresh_view = "R",
next_page = "]p",
previous_page = "[p",
open_actions = "A",
open_in_browser = "gx",
open_references = "gl",
copy_id = "y",
copy_url = "Y",
show_details = "K",
search = "?",
},
picker = {
next_item = { "<Down>", "<C-n>", "<C-j>" },
previous_item = { "<Up>", "<C-p>", "<C-k>" },
select = { "<CR>", "<C-s>" },
toggle = "<Tab>",
close = { "q", "<Esc>" },
},
issues = {
transition_issue = "gs",
change_assignee = "ga",
change_reporter = "gr",
edit_issue = "ge",
edit_search = "i",
create_issue = "c",
toggle_description_mode = "m",
},
pulls = {
open_diff = "gd",
checkout = "gc",
external_help = "gA", -- Atlas help in external diff viewers
open_repository = "o",
toggle_repo_issue_state = "t",
edit_title = "T",
edit_description = "D",
edit_search = "i",
pipelines = {
next_job = { "]j", "<Tab>" },
previous_job = { "[j", "<S-Tab>" },
show_history = "gH",
toggle_raw_logs = "gL",
},
review = {
open_item = "<CR>", -- Open the selected file, review item, or inline comment/note.
show_details = "K",
approve = "<leader>ga",
request_changes = "<leader>gr",
submit_review = "<leader>gs",
add_task = "<leader>t",
find_file = "<leader>ff",
comment_templates = "gT",
explorer = {
toggle_explorer = "<leader>b",
find_file = { "f", "<leader>ff" },
next_file = { "]f", "<Tab>" },
previous_file = { "[f", "<S-Tab>" },
next_unreviewed_file = "]u",
previous_unreviewed_file = "[u",
toggle_grouping = "T",
toggle_file_reviewed = "-",
toggle_commits = "gC",
},
diff = {
toggle_layout = "t",
toggle_compact = "gc",
next_hunk = "]h",
previous_hunk = "[h",
toggle_review_panel = "gR",
toggle_detail_panel = "gD",
toggle_comments = "gH",
next_comment = "]c",
previous_comment = "[c",
next_note = "]n",
previous_note = "[n",
add_comment = "c",
submit_comment = "C",
add_suggestion = "s",
submit_suggestion = "S",
add_note = "<leader>n",
toggle_resolved = "x",
},
},
filters = {
open = "gpo",
merged = "gpm",
declined = "gpd",
},
},
},
You can add custom keymaps to run Atlas actions, commands, or your own Lua functions.
local actions = require("atlas.pulls.actions")
keymaps = {
pulls = {
custom = {
{
key = "gP",
desc = "Open pipelines",
callback = function(context, done)
actions.run("open_pipelines", context, done)
end,
},
},
},
issues = {
custom = {
{
key = "<leader>as",
desc = "Atlas search",
callback = function()
vim.cmd("Atlas search")
end,
},
},
},
}
Thank you to everyone who has contributed to Atlas! ❤️
Contributions are welcome! If you'd like to contribute, please open an issue or pull request on GitHub. See CONTRIBUTING.md.
MIT License - see LICENSE for details.