Home

Awesome

<p align="center"> <a href="https://github.com/pwntester/octo.nvim"><img src="https://img.shields.io/github/repo-size/pwntester/octo.nvim" alt="GitHub repository size"/></a> <a href="https://github.com/pwntester/octo.nvim/issues"><img src="https://img.shields.io/github/issues/pwntester/octo.nvim" alt="Issues"/></a> <a href="https://github.com/pwntester/octo.nvim/blob/master/LICENSE"><img src="https://img.shields.io/github/license/pwntester/octo.nvim" alt="License"/></a> <a href="https://saythanks.io/to/alvaro%40pwntester.com"><img src="https://img.shields.io/badge/say-thanks-modal.svg" alt="Say thanks"/></a> <a href="https://github.com/pwntester/octo.nvim/commits/main"><img src="https://img.shields.io/github/last-commit/pwntester/octo.nvim" alt="Latest commit"/></a> <a href="https://github.com/pwntester/octo.nvim/stargazers"><img src="https://img.shields.io/github/stars/pwntester/octo.nvim" alt="Repository's starts"/></a> </p>

:octopus: Octo.nvim

Edit and review GitHub issues and pull requests from the comfort of your favorite editor.

<img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="BuyMeACoffee" width="140">

🌲 Table of Contents

<!--toc:start-->

💫 Features

🎯 Requirements

📦 Installation

Use your favourite plugin manager to install it, e.g.:

use {
  'pwntester/octo.nvim',
  requires = {
    'nvim-lua/plenary.nvim',
    'nvim-telescope/telescope.nvim',
    -- OR 'ibhagwan/fzf-lua',
    'nvim-tree/nvim-web-devicons',
  },
  config = function ()
    require"octo".setup()
  end
}

🔧 Configuration

require"octo".setup({
  use_local_fs = false,                    -- use local files on right side of reviews
  enable_builtin = false,                  -- shows a list of builtin actions when no action is provided
  default_remote = {"upstream", "origin"}; -- order to try remotes
  default_merge_method = "commit",         -- default merge method which should be used when calling `Octo pr merge`, could be `commit`, `rebase` or `squash`
  ssh_aliases = {},                        -- SSH aliases. e.g. `ssh_aliases = {["github.com-work"] = "github.com"}`. The key part will be interpreted as an anchored Lua pattern.
  picker = "telescope",                    -- or "fzf-lua"
  picker_config = {
    use_emojis = false,                    -- only used by "fzf-lua" picker for now
    mappings = {                           -- mappings for the pickers
      open_in_browser = { lhs = "<C-b>", desc = "open issue in browser" },
      copy_url = { lhs = "<C-y>", desc = "copy url to system clipboard" },
      checkout_pr = { lhs = "<C-o>", desc = "checkout pull request" },
      merge_pr = { lhs = "<C-r>", desc = "merge pull request" },
    },
  },
  comment_icon = "▎",                      -- comment marker
  outdated_icon = "󰅒 ",                    -- outdated indicator
  resolved_icon = " ",                    -- resolved indicator
  reaction_viewer_hint_icon = " ";        -- marker for user reactions
  users = "search",                        -- Users for assignees or reviewers. Values: "search" | "mentionable" | "assignable"
  user_icon = " ";                        -- user icon
  timeline_marker = " ";                  -- timeline marker
  timeline_indent = "2";                   -- timeline indentation
  right_bubble_delimiter = "";            -- bubble delimiter
  left_bubble_delimiter = "";             -- bubble delimiter
  github_hostname = "";                    -- GitHub Enterprise host
  snippet_context_lines = 4;               -- number or lines around commented lines
  gh_cmd = "gh",                           -- Command to use when calling Github CLI
  gh_env = {},                             -- extra environment variables to pass on to GitHub CLI, can be a table or function returning a table
  timeout = 5000,                          -- timeout for requests between the remote server
  default_to_projects_v2 = false,          -- use projects v2 for the `Octo card ...` command by default. Both legacy and v2 commands are available under `Octo cardlegacy ...` and `Octo cardv2 ...` respectively.
  ui = {
    use_signcolumn = false,                -- show "modified" marks on the sign column
    use_signstatus = true,                 -- show "modified" marks on the status column
  },
  issues = {
    order_by = {                           -- criteria to sort results of `Octo issue list`
      field = "CREATED_AT",                -- either COMMENTS, CREATED_AT or UPDATED_AT (https://docs.github.com/en/graphql/reference/enums#issueorderfield)
      direction = "DESC"                   -- either DESC or ASC (https://docs.github.com/en/graphql/reference/enums#orderdirection)
    }
  },
  reviews = {
    auto_show_threads = true,              -- automatically show comment threads on cursor move
  },
  pull_requests = {
    order_by = {                           -- criteria to sort the results of `Octo pr list`
      field = "CREATED_AT",                -- either COMMENTS, CREATED_AT or UPDATED_AT (https://docs.github.com/en/graphql/reference/enums#issueorderfield)
      direction = "DESC"                   -- either DESC or ASC (https://docs.github.com/en/graphql/reference/enums#orderdirection)
    },
    always_select_remote_on_create = false -- always give prompt to select base remote repo when creating PRs
  },
  file_panel = {
    size = 10,                             -- changed files panel rows
    use_icons = true                       -- use web-devicons in file panel (if false, nvim-web-devicons does not need to be installed)
  },
  colors = {                               -- used for highlight groups (see Colors section below)
    white = "#ffffff",
    grey = "#2A354C",
    black = "#000000",
    red = "#fdb8c0",
    dark_red = "#da3633",
    green = "#acf2bd",
    dark_green = "#238636",
    yellow = "#d3c846",
    dark_yellow = "#735c0f",
    blue = "#58A6FF",
    dark_blue = "#0366d6",
    purple = "#6f42c1",
  },
  mappings_disable_default = false,        -- disable default mappings if true, but will still adapt user mappings
  mappings = {
    issue = {
      close_issue = { lhs = "<leader>ic", desc = "close issue" },
      reopen_issue = { lhs = "<leader>io", desc = "reopen issue" },
      list_issues = { lhs = "<leader>il", desc = "list open issues on same repo" },
      reload = { lhs = "<C-r>", desc = "reload issue" },
      open_in_browser = { lhs = "<C-b>", desc = "open issue in browser" },
      copy_url = { lhs = "<C-y>", desc = "copy url to system clipboard" },
      add_assignee = { lhs = "<leader>aa", desc = "add assignee" },
      remove_assignee = { lhs = "<leader>ad", desc = "remove assignee" },
      create_label = { lhs = "<leader>lc", desc = "create label" },
      add_label = { lhs = "<leader>la", desc = "add label" },
      remove_label = { lhs = "<leader>ld", desc = "remove label" },
      goto_issue = { lhs = "<leader>gi", desc = "navigate to a local repo issue" },
      add_comment = { lhs = "<leader>ca", desc = "add comment" },
      delete_comment = { lhs = "<leader>cd", desc = "delete comment" },
      next_comment = { lhs = "]c", desc = "go to next comment" },
      prev_comment = { lhs = "[c", desc = "go to previous comment" },
      react_hooray = { lhs = "<leader>rp", desc = "add/remove 🎉 reaction" },
      react_heart = { lhs = "<leader>rh", desc = "add/remove ❤️ reaction" },
      react_eyes = { lhs = "<leader>re", desc = "add/remove 👀 reaction" },
      react_thumbs_up = { lhs = "<leader>r+", desc = "add/remove 👍 reaction" },
      react_thumbs_down = { lhs = "<leader>r-", desc = "add/remove 👎 reaction" },
      react_rocket = { lhs = "<leader>rr", desc = "add/remove 🚀 reaction" },
      react_laugh = { lhs = "<leader>rl", desc = "add/remove 😄 reaction" },
      react_confused = { lhs = "<leader>rc", desc = "add/remove 😕 reaction" },
    },
    pull_request = {
      checkout_pr = { lhs = "<leader>po", desc = "checkout PR" },
      merge_pr = { lhs = "<leader>pm", desc = "merge commit PR" },
      squash_and_merge_pr = { lhs = "<leader>psm", desc = "squash and merge PR" },
      rebase_and_merge_pr = { lhs = "<leader>prm", desc = "rebase and merge PR" },
      list_commits = { lhs = "<leader>pc", desc = "list PR commits" },
      list_changed_files = { lhs = "<leader>pf", desc = "list PR changed files" },
      show_pr_diff = { lhs = "<leader>pd", desc = "show PR diff" },
      add_reviewer = { lhs = "<leader>va", desc = "add reviewer" },
      remove_reviewer = { lhs = "<leader>vd", desc = "remove reviewer request" },
      close_issue = { lhs = "<leader>ic", desc = "close PR" },
      reopen_issue = { lhs = "<leader>io", desc = "reopen PR" },
      list_issues = { lhs = "<leader>il", desc = "list open issues on same repo" },
      reload = { lhs = "<C-r>", desc = "reload PR" },
      open_in_browser = { lhs = "<C-b>", desc = "open PR in browser" },
      copy_url = { lhs = "<C-y>", desc = "copy url to system clipboard" },
      goto_file = { lhs = "gf", desc = "go to file" },
      add_assignee = { lhs = "<leader>aa", desc = "add assignee" },
      remove_assignee = { lhs = "<leader>ad", desc = "remove assignee" },
      create_label = { lhs = "<leader>lc", desc = "create label" },
      add_label = { lhs = "<leader>la", desc = "add label" },
      remove_label = { lhs = "<leader>ld", desc = "remove label" },
      goto_issue = { lhs = "<leader>gi", desc = "navigate to a local repo issue" },
      add_comment = { lhs = "<leader>ca", desc = "add comment" },
      delete_comment = { lhs = "<leader>cd", desc = "delete comment" },
      next_comment = { lhs = "]c", desc = "go to next comment" },
      prev_comment = { lhs = "[c", desc = "go to previous comment" },
      react_hooray = { lhs = "<leader>rp", desc = "add/remove 🎉 reaction" },
      react_heart = { lhs = "<leader>rh", desc = "add/remove ❤️ reaction" },
      react_eyes = { lhs = "<leader>re", desc = "add/remove 👀 reaction" },
      react_thumbs_up = { lhs = "<leader>r+", desc = "add/remove 👍 reaction" },
      react_thumbs_down = { lhs = "<leader>r-", desc = "add/remove 👎 reaction" },
      react_rocket = { lhs = "<leader>rr", desc = "add/remove 🚀 reaction" },
      react_laugh = { lhs = "<leader>rl", desc = "add/remove 😄 reaction" },
      react_confused = { lhs = "<leader>rc", desc = "add/remove 😕 reaction" },
      review_start = { lhs = "<leader>vs", desc = "start a review for the current PR" },
      review_resume = { lhs = "<leader>vr", desc = "resume a pending review for the current PR" },
    },
    review_thread = {
      goto_issue = { lhs = "<leader>gi", desc = "navigate to a local repo issue" },
      add_comment = { lhs = "<leader>ca", desc = "add comment" },
      add_suggestion = { lhs = "<leader>sa", desc = "add suggestion" },
      delete_comment = { lhs = "<leader>cd", desc = "delete comment" },
      next_comment = { lhs = "]c", desc = "go to next comment" },
      prev_comment = { lhs = "[c", desc = "go to previous comment" },
      select_next_entry = { lhs = "]q", desc = "move to next changed file" },
      select_prev_entry = { lhs = "[q", desc = "move to previous changed file" },
      select_first_entry = { lhs = "[Q", desc = "move to first changed file" },
      select_last_entry = { lhs = "]Q", desc = "move to last changed file" },
      close_review_tab = { lhs = "<C-c>", desc = "close review tab" },
      react_hooray = { lhs = "<leader>rp", desc = "add/remove 🎉 reaction" },
      react_heart = { lhs = "<leader>rh", desc = "add/remove ❤️ reaction" },
      react_eyes = { lhs = "<leader>re", desc = "add/remove 👀 reaction" },
      react_thumbs_up = { lhs = "<leader>r+", desc = "add/remove 👍 reaction" },
      react_thumbs_down = { lhs = "<leader>r-", desc = "add/remove 👎 reaction" },
      react_rocket = { lhs = "<leader>rr", desc = "add/remove 🚀 reaction" },
      react_laugh = { lhs = "<leader>rl", desc = "add/remove 😄 reaction" },
      react_confused = { lhs = "<leader>rc", desc = "add/remove 😕 reaction" },
    },
    submit_win = {
      approve_review = { lhs = "<C-a>", desc = "approve review" },
      comment_review = { lhs = "<C-m>", desc = "comment review" },
      request_changes = { lhs = "<C-r>", desc = "request changes review" },
      close_review_tab = { lhs = "<C-c>", desc = "close review tab" },
    },
    review_diff = {
      submit_review = { lhs = "<leader>vs", desc = "submit review" },
      discard_review = { lhs = "<leader>vd", desc = "discard review" },
      add_review_comment = { lhs = "<leader>ca", desc = "add a new review comment" },
      add_review_suggestion = { lhs = "<leader>sa", desc = "add a new review suggestion" },
      focus_files = { lhs = "<leader>e", desc = "move focus to changed file panel" },
      toggle_files = { lhs = "<leader>b", desc = "hide/show changed files panel" },
      next_thread = { lhs = "]t", desc = "move to next thread" },
      prev_thread = { lhs = "[t", desc = "move to previous thread" },
      select_next_entry = { lhs = "]q", desc = "move to next changed file" },
      select_prev_entry = { lhs = "[q", desc = "move to previous changed file" },
      select_first_entry = { lhs = "[Q", desc = "move to first changed file" },
      select_last_entry = { lhs = "]Q", desc = "move to last changed file" },
      close_review_tab = { lhs = "<C-c>", desc = "close review tab" },
      toggle_viewed = { lhs = "<leader><space>", desc = "toggle viewer viewed state" },
      goto_file = { lhs = "gf", desc = "go to file" },
    },
    file_panel = {
      submit_review = { lhs = "<leader>vs", desc = "submit review" },
      discard_review = { lhs = "<leader>vd", desc = "discard review" },
      next_entry = { lhs = "j", desc = "move to next changed file" },
      prev_entry = { lhs = "k", desc = "move to previous changed file" },
      select_entry = { lhs = "<cr>", desc = "show selected changed file diffs" },
      refresh_files = { lhs = "R", desc = "refresh changed files panel" },
      focus_files = { lhs = "<leader>e", desc = "move focus to changed file panel" },
      toggle_files = { lhs = "<leader>b", desc = "hide/show changed files panel" },
      select_next_entry = { lhs = "]q", desc = "move to next changed file" },
      select_prev_entry = { lhs = "[q", desc = "move to previous changed file" },
      select_first_entry = { lhs = "[Q", desc = "move to first changed file" },
      select_last_entry = { lhs = "]Q", desc = "move to last changed file" },
      close_review_tab = { lhs = "<C-c>", desc = "close review tab" },
      toggle_viewed = { lhs = "<leader><space>", desc = "toggle viewer viewed state" },
    },
  },
})

🚀 Usage

Just edit the issue title, body or comments as a regular buffer and use :w(rite) to sync the issue with GitHub.

🤖 Commands

There is only an Octo <object> <action> [arguments] command: If no command is passed, the argument to Octo is treated as a URL from where an issue or pr repo and number are extracted.

ObjectActionArguments
issuecloseClose the current issue
reopenReopen the current issue
create [repo]Creates a new issue in the current or specified repo
developCreate and checkout a new branch for an issue in the current repo
edit [repo] <number>Edit issue <number> in current or specified repo
list [repo] [key=value] (1)List all issues satisfying given filter
searchLive issue search
reloadReload issue. Same as doing e!
browserOpen current issue in the browser
urlCopies the URL of the current issue to the system clipboard
prlist [repo] [key=value] (2)List all PRs satisfying given filter
searchLive issue search
edit [repo] <number>Edit PR <number> in current or specified repo
reopenReopen the current PR
createCreates a new PR for the current branch
closeClose the current PR
checkoutCheckout PR
commitsList all PR commits
changesShow all PR changes (diff hunks)
diffShow PR diff
merge [commit|rebase|squash] [delete]Merge current PR using the specified method
readyMark a draft PR as ready for review
draftSend a ready PR back to draft
checksShow the status of all checks run on the PR
reloadReload PR. Same as doing e!
browserOpen current PR in the browser
urlCopies the URL of the current PR to the system clipboard
repolist (3)List repos user owns, contributes or belong to
forkFork repo
browserOpen current repo in the browser
urlCopies the URL of the current repo to the system clipboard
viewOpen a repo by path ({organization}/{name})
gistlist [repo] [key=value] (4)List user gists
commentaddAdd a new comment
deleteDelete a comment
threadresolveMark a review thread as resolved
unresolveMark a review thread as unresolved
labeladd [label]Add a label from available label menu
remove [label]Remove a label
create [label]Create a new label
assigneeadd [login]Assign a user
remove [login]Unassign a user
revieweradd [login]Assign a PR reviewer
reactionthumbs_up | +1Add 👍 reaction
thumbs_down | -1Add 👎 reaction
eyesAdd 👀 reaction
laughAdd 😄 reaction
confusedAdd 😕 reaction
rocketAdd 🚀 reaction
heartAdd ❤️ reaction
hooray | party | tadaAdd 🎉 reaction
cardaddAssign issue/PR to a project new card
removeDelete project card
moveMove project card to different project/column
reviewstartStart a new review
submitSubmit the review
resumeEdit a pending review for current PR
discardDeletes a pending review for current PR if any
commentsView pending review comments
commitPick a specific commit to review
closeClose the review window and return to the PR
actionsLists all available Octo actions
search<query>Search GitHub for issues and PRs matching the query
  1. [repo]: If repo is not provided, it will be derived from <cwd>/.git/config.

  2. In-menu mappings:

Available filter keys

  1. In-menu mappings:

Available filter keys

  1. In-menu mappings:
  1. In-menu mappings:
  1. Users in the assignee and reviewer commands:

🔥 Examples

Octo https://github.com/pwntester/octo.nvim/issues/12
Octo issue create
Octo issue create pwntester/octo.nvim
Octo comment add
Octo reaction add hooray
Octo issue edit pwntester/octo.nvim 1
Octo issue edit 1
Octo issue list createdBy=pwntester
Octo issue list neovim/neovim labels=bug,help\ wanted states=OPEN
Octo search assignee:pwntester is:pr

📋 PR reviews

🍞 Completion

Octo provides a built-in omnifunc completion for issues, PRs and users that you can trigger using <C-x><C-o>. Alternately, if you use nvim-cmp for completion, you can use the cmp-git source to provide issues, PRs, commits and users completion.

🎨 Colors

Highlight GroupDefaults to
OctoDirtyErrorMsg
OctoIssueTitlePreProc
OctoIssueIdQuestion
OctoEmptyComment
OctoFloatNormalNC
OctoDateComment
OctoSymbolComment
OctoTimelineItemHeadingComment
OctoDetailsLabelTitle
OctoMissingDetailsComment
OctoDetailsValueIdentifier
OctoDiffHunkPositionNormalFloat
OctoCommentLineTabLineSel
OctoEditableNormalFloat bg
OctoViewerGitHub color
OctoBubbleNormalFloat
OctoBubbleGreenGitHub color
OctoBubbleRedGitHub color
OctoUserOctoBubble
OctoUserViewerOctoViewer
OctoReactionOctoBubble
OctoReactionViewerOctoViewer
OctoPassingTestGitHub color
OctoFailingTestGitHub color
OctoPullAdditionsGitHub color
OctoPullDeletionsGitHub color
OctoPullModificationsGitHub color
OctoStateOpenGitHub color
OctoStateClosedGitHub color
OctoStateMergeGitHub color
OctoStatePendingGitHub color
OctoStateApprovedOctoStateOpen
OctoStateChangesRequestedOctoStateClosed
OctoStateCommentedNormal
OctoStateDismissedOctoStateClosed

The term GitHub color refers to the colors used in the WebUI. The (addition) viewer means the user of the plugin or more precisely the user authenticated via the gh CLI tool used to retrieve the data from GitHub.

📺 Demos

issues

prs

🙋 FAQ

I get a warning saying Cannot request projects v2, missing scope 'read:project'

That's expected. The new support for projects v2 support requires the read:project scope on your GitHub token.

You add the scope by using gh auth refresh -s read:project or you can suppress this warning by setting the following in your config

{
  suppress_missing_scope = {
    projects_v2 = true,
  }
}

How can I disable bubbles for XYZ?

Each text-object that makes use of a bubble (except labels) do use their own highlight group that links per default to the main bubble highlight group. To disable most bubbles at once you can simply link OctoBubble to Normal. To only disable them for a certain plain do the same for the specific sub-group (e.g. OctoUser).

Why do my issue titles or markdown syntax do not get highlighted properly?

The title, body and comments of an issue or PR are special as they get special highlighting applied and is an editable section. Due to the latter property it gets the OctoEditable highlighting via a special signs linehl setting. This takes precedence over the buffer internal highlights. To only get the background highlighted by the editable section, set OctoEditable to a highlight with a background color definition only.

Why am I getting authentication error from gh?

This means that are either using a GITHUB_TOKEN to authenticate or gh is not authenticated.

In case of the former, run:

GITHUB_TOKEN= gh auth login

... and choose a method to authorise access for gh.

gh must store the credentials so it can work in a subshell.

Can I use treesitter markdown parser with octo buffers?

Just add the following lines to your TreeSitter config:

vim.treesitter.language.register('markdown', 'octo')

How can I filter PRs by filter keys that aren't available?

You can use the search command :Octo search [query]. The search syntax and available search terms are available in GitHub documentation.

For example to search for PRs with author you can use this command:

:Octo search is:pr author:pwntester repo:github/codeql

Note: You need to provide the repo, otherwise it will search for every PR by that user.

How to enable autocompletion for issues/prs (#) and users (@)?

Add the following mappings for octo file type:

How can I disable default key mappings?

Set mappings_disable_default = true in user config.

🙌 Contributing

Contributions are always welcome!

See CONTRIBUTING for ways to get started.

Please adhere to this project's CODE_OF_CONDUCT.

🌟 Credits

The PR review panel is heavily inspired in diffview.nvim

🙏 Say Thanks

If you like this plugin and would like to buy me a coffee, you can!

<img src="https://cdn.buymeacoffee.com/buttons/v2/default-violet.png" alt="BuyMeACoffee" width="140">

GitHub Sponsors

📜 License

MIT