From d26a11aa185160465f67fb1465574f5fd9692192 Mon Sep 17 00:00:00 2001 From: Peter Tri Ho Date: Thu, 22 Oct 2026 19:52:52 +1100 Subject: [PATCH 1/6] docs: move the README notes to docs/configuration.md The README now has only the features, the requirements, the installation, the setup, and the default config. The notes on trigger actions, trailers, filtering, sorting, and hosted instances move to docs/configuration.md, and the README links to each section. docs/configuration.md also documents behavior that no doc described: the format functions and item fields, sort_by, filetypes, remotes and ssh_aliases, gh repo set-default, Octo buffers, the differences between the CLIs and curl, the debug log, and the cache for each buffer. These docs were wrong: - GitHub mentions are not curl only. They use gh api since 73f6b7f. - The GitHub issue filter has no subscribed or repos value since 40725f6. - The default config did not show ssh_aliases, added in c1f76bf. - The source functions take no config table since b414e48. - The hosted instances example had no closing parenthesis. --- README.md | 147 +++++----------------- docs/configuration.md | 276 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+), 115 deletions(-) create mode 100644 docs/configuration.md diff --git a/README.md b/README.md index 8913a2a..0e7ce26 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Git source for [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp) and [Sagh | Trailer identities | `Name ` after an identity key, such as `Co-authored-by: ` | | Trailer values | An earlier value of the key after any other key, such as `Assisted-by: ` | -| GitHub | Trigger | -| ---------------------- | ------- | -| Issues | # | -| Mentions (`curl` only) | @ | -| Pull Requests | # | +| GitHub | Trigger | +| ------------- | ------- | +| Issues | # | +| Mentions | @ | +| Pull Requests | # | | GitLab | Trigger | | -------------- | ------- | @@ -28,20 +28,20 @@ Git source for [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp) and [Sagh - Neovim >= 0.12 - git (>= 2.31 to find trailer keys in history) - curl -- [GitHub CLI](https://cli.github.com/) (optional, will use curl instead if not avaliable) -- [GitLab CLI](https://gitlab.com/gitlab-org/cli) (optional, will use curl instead if not avaliable) +- [GitHub CLI](https://cli.github.com/) (optional, cmp-git uses curl if it is not available) +- [GitLab CLI](https://gitlab.com/gitlab-org/cli) (optional, cmp-git uses curl if it is not available) -### GitHub Private Repositories +### GitHub private repositories -- `curl`: Generate [token](https://github.com/settings/tokens) - with `repo` scope. Set `GITHUB_API_TOKEN` environment variable. -- `GitHub CLI`: Run [gh auth login](https://cli.github.com/manual/gh_auth_login) +- `curl`: Generate a [token](https://github.com/settings/tokens) with the `repo` scope. Set the `GITHUB_API_TOKEN` + environment variable to the token. +- `GitHub CLI`: Run [gh auth login](https://cli.github.com/manual/gh_auth_login). -### GitLab Private Repositories +### GitLab private repositories -- `curl` Generate [token](https://gitlab.com/-/profile/personal_access_tokens) - with `api` scope. Set `GITLAB_TOKEN` environment variable. -- `GitLab CLI`: Run [glab auth login](https://glab.readthedocs.io/en/latest/auth/login.html) +- `curl`: Generate a [token](https://gitlab.com/-/profile/personal_access_tokens) with the `api` scope. Set the + `GITLAB_TOKEN` environment variable to the token. +- `GitLab CLI`: Run [glab auth login](https://glab.readthedocs.io/en/latest/auth/login.html). ## Installation @@ -99,7 +99,8 @@ require("cmp_git").setup() ### blink.cmp -`blink.cmp` support is optional and configured from your `blink.cmp` setup. Do not call `require("cmp_git").setup()` for blink; that function registers the `nvim-cmp` source. +If you use `blink.cmp`, configure cmp-git in your `blink.cmp` setup. Do not call `require("cmp_git").setup()` for +`blink.cmp`. That function registers the `nvim-cmp` source. ```lua require("blink.cmp").setup({ @@ -118,11 +119,8 @@ require("blink.cmp").setup({ }) ``` -To enable `cmp-git` only for commit-related filetypes while keeping your normal blink sources elsewhere, add it through blink's `sources.per_filetype` configuration for filetypes such as `gitcommit`, `octo`, or `NeogitCommitMessage`. - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, the tests, and the CI checks. +To use cmp-git only in commit-related filetypes, add it to `sources.per_filetype` for these filetypes, for example +`gitcommit`, `octo`, or `NeogitCommitMessage`. Your normal `blink.cmp` sources then stay in the other filetypes. ## Config @@ -135,6 +133,7 @@ require("cmp_git").setup({ filetypes = { "gitcommit", "octo", "NeogitCommitMessage" }, remotes = { "upstream", "origin" }, -- in order of most to least prioritized enableRemoteUrlRewrites = false, -- enable git url rewrites, see https://git-scm.com/docs/git-config#Documentation/git-config.txt-urlltbasegtinsteadOf + ssh_aliases = {}, -- SSH host aliases and their real hosts, such as { ["github-work"] = "github.com" } git = { commits = { limit = 100, @@ -176,7 +175,7 @@ require("cmp_git").setup({ hosts = {}, -- list of private instances of github issues = { fields = { "title", "number", "body", "updatedAt", "state" }, - filter = "all", -- assigned, created, mentioned, subscribed, all, repos + filter = "all", -- all, assigned, created, mentioned limit = 100, state = "open", -- open, closed, all sort_by = sort.github.issues, @@ -245,102 +244,20 @@ require("cmp_git").setup({ ) ``` -Capability-level `format.filterText` is the preferred way to customize completion filtering. Provider-level -`filter_fn` is supported as a compatibility alias and is applied to every capability for that provider, -except `git.filter_fn`, which applies only to `git.commits`. - ---- - -**NOTE** - -If you want specific behaviour for a trigger, add an entry in the `trigger_actions` table of the config. -The preferred fields are `trigger_character` and `actions`. `trigger_character` has to be a single -character, and `actions` is an ordered list of named behaviours. Multiple actions can be used for the same -character; they run in order until one handles the request. - -An entry can use `pattern` instead of `trigger_character`. `pattern` is a vim regex in the default `magic` -mode. It applies when it matches the text before the cursor, up to the cursor. The match start is where the -completed text starts. The patterns are also added to the `nvim-cmp` keyword pattern. Entries run in -config order, and the first action that handles the request wins. This is why the trailer entries come -before the `:` entry: `Co-authored-by:` completes identities, not commits. - -Built-in actions are `git_commits`, `git_trailer_keys`, `git_trailer_identities`, `git_trailer_values`, -`gitlab_issues`, `gitlab_mentions`, `gitlab_change_requests`, `github_issues`, `github_change_requests`, -`github_issues_and_change_requests`, and `github_mentions`. The `git_trailer_*` actions need a `pattern` -entry. -Compatibility aliases are also available: `gitlab_mrs` routes to `gitlab_change_requests`, and -`github_issues_and_prs` routes to `github_issues_and_change_requests`. +[docs/configuration.md](docs/configuration.md) describes these topics: -Legacy callback-style trigger actions are still supported for compatibility. These entries use -`trigger_character` and `action`, where `action` receives the different sources (currently `git`, `gitlab` -and `github`), the trigger character, the completion callback, the parameters passed to `complete` from -`nvim-cmp`, and the current git info. New configuration should prefer named `actions` instead. +- [Trigger actions](docs/configuration.md#trigger-actions) +- [Trailer completion](docs/configuration.md#trailer-completion) +- [Format and sort items](docs/configuration.md#format-and-sort-items) +- [Filetypes](docs/configuration.md#filetypes) +- [Remotes and SSH aliases](docs/configuration.md#remotes-and-ssh-aliases) +- [GitHub CLI, GitLab CLI, and curl](docs/configuration.md#github-cli-gitlab-cli-and-curl) +- [Hosted GitHub and GitLab instances](docs/configuration.md#hosted-github-and-gitlab-instances) +- [Troubleshooting](docs/configuration.md#troubleshooting) -All source functions take an optional config table as last argument, with which the configuration set -in `setup` can be overwritten for a specific call. - -**NOTE on trailers** - -| You type | Completion | From | -| -------------------- | -------------------------------------------------- | ---------------------------------------------- | -| `Co` | `Co-authored-by: ` | A trailer key | -| `Co-authored-by: Gr` | `Co-authored-by: Grace Hopper ` | A commit author or identity trailer in history | -| `Signed-off-by:` | `Signed-off-by: Ada Lovelace ` | Your git identity | -| `Assisted-by: Cl` | `Assisted-by: Claude Code` | An earlier `Assisted-by` value | - -Trailer completion works only in the `git.trailers.filetypes` buffers, from line `git.trailers.min_line` -(default 3), and on a line after a blank line or after another `Key: value` line. Line 1 counts as after a -blank line. Keys come from `git.trailers.keys.builtin` and from the last -`git.trailers.keys.limit` commits. - -The value of a `git.trailers.identities.keys` trailer completes `Name ` from the authors and those -trailers of the last `git.trailers.identities.limit` commits, and from your git identity. The value of any -other trailer completes the values that the same key had in the last `git.trailers.values.limit` commits. -`Key:` with no space after the colon still completes commits for other keys, so `Fixes:` works as before. -Value completion stops when the typed value contains `:`, `#`, `@`, or `!` outside `<...>`, so that these -triggers keep working (for example, `Fixes: #12` completes issues). - -To disable trailer completion, set `trigger_actions` without the two `pattern` entries. If you already set -your own `trigger_actions`, add the two `pattern` entries from the defaults above to get trailer completion. - -**NOTE on sorting** - -The default sorting order is last updated (for PRs, MRs and issues) and latest (for commits). -To make `nvim-cmp` sort in this order, move `cmp.config.compare.sort_text` closer to the top of (lower index) in `sorting.comparators`. E.g. - -```lua -require("cmp").setup({ - -- As above - sorting = { - comparators = { - cmp.config.compare.offset, - cmp.config.compare.exact, - cmp.config.compare.sort_text, - cmp.config.compare.score, - cmp.config.compare.recently_used, - cmp.config.compare.kind, - cmp.config.compare.length, - cmp.config.compare.order, - }, - }, -}) -``` - -### Working with hosted instances of GitHub or GitLab - -You can add hosted instances of Github Enterprise or GitLab to the corresponding `hosts` list as such: -```lua -require("cmp_git").setup({ - github = { - hosts = { "github.mycompany.com", }, - }, - gitlab = { - hosts = { "gitlab.mycompany.com", } - } -} -``` +## Contributing ---- +See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, the tests, and the CI checks. ## Acknowledgements diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..90b44c9 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,276 @@ +# Configuration + +The [README](../README.md#config) shows the default configuration. This page describes the options that need more +explanation. + +## Trigger actions + +To change the behavior of a trigger, add an entry to `trigger_actions`. The preferred fields of an entry are +`trigger_character` and `actions`: + +- `trigger_character` must be a single character. +- `actions` is an ordered list of named actions. + +An entry can have more than one action. cmp-git runs the actions of the matching entries in config order. It stops at +the first action that handles the request. + +### Pattern entries + +An entry can use `pattern` in place of `trigger_character`. `pattern` is a Vim regex in the default `magic` mode. The +entry applies when the pattern matches the text before the cursor, up to the cursor. The completed text starts at the +start of the match. cmp-git also adds the patterns to the `nvim-cmp` keyword pattern. + +The default trailer entries come before the `:` entry. As a result, `Co-authored-by:` completes identities, not +commits. + +### Built-in actions + +The built-in actions are `git_commits`, `git_trailer_keys`, `git_trailer_identities`, `git_trailer_values`, +`gitlab_issues`, `gitlab_mentions`, `gitlab_change_requests`, `github_issues`, `github_change_requests`, +`github_issues_and_change_requests`, and `github_mentions`. The `git_trailer_*` actions need a `pattern` entry. + +Two compatibility aliases are also available: + +- `gitlab_mrs` runs `gitlab_change_requests`. +- `github_issues_and_prs` runs `github_issues_and_change_requests`. + +### Callback actions + +For compatibility, cmp-git also supports legacy callback actions. A callback entry has `trigger_character` and +`action`. The `action` function receives these arguments, in this order: + +1. The sources (currently `git`, `gitlab`, and `github`). +2. The trigger character. +3. The completion callback. +4. The parameters that `nvim-cmp` passes to `complete`. +5. The current git info. + +We recommend named `actions` for new configuration. + +## Trailer completion + +| You type | Completion | From | +| -------------------- | -------------------------------------------------- | ---------------------------------------------- | +| `Co` | `Co-authored-by: ` | A trailer key | +| `Co-authored-by: Gr` | `Co-authored-by: Grace Hopper ` | A commit author or identity trailer in history | +| `Signed-off-by:` | `Signed-off-by: Ada Lovelace ` | Your git identity | +| `Assisted-by: Cl` | `Assisted-by: Claude Code` | An earlier `Assisted-by` value | + +Trailer completion works only when all of these conditions are true: + +- The filetype of the buffer is in `git.trailers.filetypes`. +- The cursor is on line `git.trailers.min_line` (default 3) or a later line. +- The previous line is blank or is a `Key: value` line. Line 1 counts as after a blank line. + +Keys come from `git.trailers.keys.builtin` and from the last `git.trailers.keys.limit` commits. + +The value of a trailer completes in one of two ways: + +- For a key in `git.trailers.identities.keys`, the value completes to `Name `. The identities come from your git + identity, and from the authors and the trailers with these keys in the last `git.trailers.identities.limit` commits. +- For any other key, the value completes to the values that the same key had in the last `git.trailers.values.limit` + commits. `Key:` with no space after the colon completes commits. For example, `Fixes:` completes commits. + +Value completion stops when the typed value contains `:`, `#`, `@`, or `!` outside `<...>`. As a result, these +triggers also work in a trailer value. For example, `Fixes: #12` completes issues. + +To turn off trailer completion, set `trigger_actions` without the two `pattern` entries. + +If you set your own `trigger_actions`, add the two `pattern` entries from the [defaults](../README.md#config) to get +trailer completion. + +## Format and sort items + +Each capability, such as `git.commits` or `github.issues`, has a `format` table and a `sort_by` value. + +### Format + +`format` has four functions. Each function receives `trigger_char` and the item, and returns one part of the +completion item: + +| Function | Returns | +| --------------- | ----------------------------------------------------------------------- | +| `label` | The text in the completion menu | +| `filterText` | The text that the completion engine filters on | +| `insertText` | The text that cmp-git inserts | +| `documentation` | The text of the documentation window, as `{ kind = "markdown", value }` | + +`trigger_char` is the trigger character, such as `#`. It can be `nil` for a pattern entry. + +You can replace one function. The other functions keep their defaults. For example, this setup inserts the full SHA +of a commit and shows it in the label: + +```lua +require("cmp_git").setup({ + git = { + commits = { + sha_length = 40, + format = { + label = function(_trigger_char, commit) + return string.format("%s: %s", commit.sha, commit.title) + end, + }, + }, + }, +}) +``` + +For a trigger character, `filterText` must contain that character, or the completion engine shows no items. The +default functions start `filterText` with the trigger character. + +For compatibility, cmp-git also supports `filter_fn` on a provider, as an alias of `format.filterText`. A provider +`filter_fn` applies to every capability of that provider. The exception is `git.filter_fn`, which applies only to +`git.commits`. + +### Item fields + +| Capability | Fields | +| ---------------------------------------- | --------------------------------------------------------------------------------------- | +| `git.commits` | `sha`, `title`, `description`, `author_name`, `author_mail`, `commit_timestamp`, `diff` | +| `git.trailers.keys` | `key`, `count`, `builtin_index` | +| `git.trailers.identities` | `name`, `email`, `source`, `is_self`, `timestamp`, `diff` | +| `git.trailers.values` | `key`, `value`, `count`, `timestamp`, `diff` | +| `github.issues`, `github.pull_requests` | The `fields` with the GitHub CLI, or the REST API fields with curl | +| `github.mentions` | `login` | +| `gitlab.issues`, `gitlab.merge_requests` | The GitLab API fields, such as `iid`, `title`, `description`, and `updated_at` | +| `gitlab.mentions` | `username`, `name` | + +- `sha` has `git.commits.sha_length` characters (default 7). The default `insertText` inserts `sha`. The default + `label` shows the first 7 characters. +- `author_name` and `author_mail` are the name and the email of the committer. +- GitHub issues and pull requests always have `body` and `updatedAt`. +- `diff` is the number of seconds since the commit, or since the most recent use of the identity or the value. +- `count` is the number of uses in the scanned commits. +- `builtin_index` is the position of the key in `git.trailers.keys.builtin`. It is `nil` for a key from history. +- `source` is `"author"`, the trailer key, or `"self"`. `is_self` is `true` for your git identity. + +### Sort + +`sort_by` sets the `sortText` of each item. It can be a function that receives the item and returns a string, or a +field name, such as `"title"`. For a field name, cmp-git uses the value of that field. + +| Capability | Default order | +| ----------------------------------------- | -------------------------------------------------------------------------- | +| `git.commits` | Newest first | +| `git.trailers.keys` | Most used first, then the order of `keys.builtin`, then alphabetical order | +| `git.trailers.identities` | Most recent first. Your git identity comes first if it is not in history. | +| `git.trailers.values` | Most used first, then most recent | +| Issues, pull requests, and merge requests | Last updated first | +| `github.mentions`, `gitlab.mentions` | No `sort_by`, so the items have no `sortText` | + +### Sorting in nvim-cmp + +To make `nvim-cmp` use the order of cmp-git, move `cmp.config.compare.sort_text` nearer the start of +`sorting.comparators` (a lower index). For example: + +```lua +require("cmp").setup({ + -- sources and your other options + sorting = { + comparators = { + cmp.config.compare.offset, + cmp.config.compare.exact, + cmp.config.compare.sort_text, + cmp.config.compare.score, + cmp.config.compare.recently_used, + cmp.config.compare.kind, + cmp.config.compare.length, + cmp.config.compare.order, + }, + }, +}) +``` + +## Filetypes + +`filetypes` lists the filetypes where cmp-git completes. To complete in all filetypes, set `filetypes = { "*" }`. + +`git.trailers.filetypes` lists the filetypes where trailer completion works. It does not support `"*"`. Trailer +completion also needs the filetype in `filetypes`. + +A compound filetype, such as `gitcommit.markdown`, matches when one of its parts is in the list. + +## Remotes and SSH aliases + +cmp-git gets the host, the owner, and the repository from the URL of a git remote. It reads the remotes in the order +of `remotes`, and uses the first URL that it can parse. `remotes` can also be a single string. + +cmp-git runs git in the directory of the current file. If it finds no remote there, it uses the working directory of +Neovim. + +If `enableRemoteUrlRewrites` is `true`, cmp-git reads the URL with `git remote get-url`. This command applies the +`url..insteadOf` rewrites. If it is `false`, cmp-git reads `remote..url` without rewrites. + +If a remote uses a host alias from your SSH config, add the alias and the real host to `ssh_aliases`: + +```lua +require("cmp_git").setup({ + ssh_aliases = { + ["github-work"] = "github.com", + }, +}) +``` + +With this setup, cmp-git uses `github.com` for the remote `git@github-work:owner/repo.git`. The alias must match the +full host. + +If you set a default repository with `gh repo set-default`, GitHub issues and pull requests come from that repository. +GitHub mentions come from the repository of the remote. + +In an `octo` buffer, cmp-git gets the owner and the repository from the buffer name. It gets the host from +`github_hostname` in the Octo config. The default host is `github.com`. + +## GitHub CLI, GitLab CLI, and curl + +For GitHub, cmp-git uses the GitHub CLI (`gh`). For GitLab, it uses the GitLab CLI (`glab`). If the CLI is not +installed, or if the CLI command fails, cmp-git uses curl. + +The results are different in these cases: + +- `github.issues.fields` and `github.pull_requests.fields` apply only to the GitHub CLI. +- `github.pull_requests.state = "merged"` works only with the GitHub CLI. The REST API returns no pull requests for + this state. +- With curl, `github.issues` and `github.pull_requests` get at most 100 items. cmp-git requests one page, and the + GitHub API gives at most 100 items for each page. +- GitLab issues, merge requests, and mentions get at most 100 items, for the same reason. This limit applies to `glab` + and to curl. + +`github.issues.filter` can be `all`, `assigned`, `created`, or `mentioned`. The last three values show only the issues +that are assigned to you, that you created, or that mention you. + +GitHub mentions come from the collaborators of the repository when your token can read them. Otherwise, they come from +the contributors. cmp-git gets more pages until it has `github.mentions.limit` mentions. To get all mentions, set +`github.mentions.limit = math.huge`. + +## Hosted GitHub and GitLab instances + +To use a hosted instance of GitHub Enterprise or GitLab, add its host to the `hosts` list of the provider: + +```lua +require("cmp_git").setup({ + github = { + hosts = { "github.mycompany.com", }, + }, + gitlab = { + hosts = { "gitlab.mycompany.com", } + } +}) +``` + +The host must be the same as the host in the URL of the remote. cmp-git always adds `github.com` and `gitlab.com`. +With curl, cmp-git uses `https:///api/v3` for GitHub Enterprise and `https:///api/v4` for GitLab. + +## Troubleshooting + +If no items show, make sure that these conditions are true: + +- The filetype of the buffer is in `filetypes`. +- The file is in a git repository. +- For GitHub and GitLab items, the host of the remote is in `github.hosts` or `gitlab.hosts`. +- For GitHub and GitLab items, `gh`, `glab`, or `curl` is installed. + +To log missing executables, and the exit code and stderr of failed commands, set `vim.g.cmp_git_debug = true`. cmp-git +sends the messages with `vim.notify`. + +cmp-git caches the items for each buffer and does not refresh them. To get new items, wipe the buffer with +`:bwipeout` and open the file again, or restart Neovim. From 9d91a720bdc43ce09bcd215a43f868f674db8b49 Mon Sep 17 00:00:00 2001 From: Peter Tri Ho Date: Thu, 22 Oct 2026 19:52:52 +1100 Subject: [PATCH 2/6] docs: describe how to test the newest cached nightly The Neovim nightly job tests the newest overlay commit that is in the cache, not the locked one. CONTRIBUTING.md now gives the commands that run the same test locally with scripts/nightly-cache.sh latest. --- CONTRIBUTING.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f530d9..de671ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Use one of these setups: CI is authoritative. Other versions of these tools can give different results. -The first `make test` or `make typecheck` clones mini.nvim, so it needs network access. +The first `make test` or `make typecheck` clones mini.nvim, so this first run needs network access. ## Development shells @@ -91,6 +91,16 @@ nix develop .#nvim-0_12_0 -c make test nix develop .#nightly -c make test ``` +The `Neovim nightly` job tests the newest nightly that is in the cache, not the locked nightly. To do the same test on +your computer, run these commands. `scripts/nightly-cache.sh latest` needs an authenticated `gh`, and it checks the last +10 overlay commits. To check more commits, give a count, for example `latest 20`. + +```sh +rev=$(scripts/nightly-cache.sh latest) +nix develop .#nightly --no-write-lock-file \ + --override-input neovim-nightly-overlay "github:nix-community/neovim-nightly-overlay/$rev" -c make test +``` + ## CI The workflow in `.github/workflows/ci.yml` runs on pushes to `main`, on pull requests, and on manual dispatch. It From b0b302daddc3495e4723c69c9fe17ba6e4761d55 Mon Sep 17 00:00:00 2001 From: Peter Tri Ho Date: Thu, 22 Oct 2026 19:52:52 +1100 Subject: [PATCH 3/6] docs: name the author_mail field in the commits sort comment The comment listed author_email, but the commit field is author_mail, so sort_by = "author_email" gives no sortText. --- lua/cmp_git/sort.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/cmp_git/sort.lua b/lua/cmp_git/sort.lua index 9ffd692..3da7077 100644 --- a/lua/cmp_git/sort.lua +++ b/lua/cmp_git/sort.lua @@ -3,7 +3,7 @@ local utils = require("cmp_git.utils") local M = { git = { ---@param commit cmp_git.Commit - commits = function(commit) -- nil, "sha", "title", "description", "author_name", "author_email", "commit_timestamp", or custom function + commits = function(commit) -- nil, "sha", "title", "description", "author_name", "author_mail", "commit_timestamp", or custom function return string.format("%010d", commit.diff) end, trailers = { From a503fea08446d5a159e25ce527ff510b3c349475 Mon Sep 17 00:00:00 2001 From: Peter Tri Ho Date: Thu, 22 Oct 2026 19:56:14 +1100 Subject: [PATCH 4/6] fix: keep the GitHub issue and pull request caches separate The issues and pull requests action put the cached issue list in its result, and then added the pull requests to that list. As a result, the issue cache also got the pull requests, and github_issues then returned them. A cache hit returned only the issue cache, which gave the correct result only because of this mixed cache. The pull request job was also made before the issues were known. On a pull request cache hit, the callback ran at once without the issues. The action now builds a new list from the two results, and requests the pull requests only after the issues are known. --- lua/cmp_git/sources/github.lua | 59 +++++++++++-------------- tests/sources/test_github.lua | 80 ++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 34 deletions(-) diff --git a/lua/cmp_git/sources/github.lua b/lua/cmp_git/sources/github.lua index 1c42d65..7145942 100644 --- a/lua/cmp_git/sources/github.lua +++ b/lua/cmp_git/sources/github.lua @@ -140,17 +140,7 @@ function GitHub:complete_issues_and_change_requests(callback, git_info, trigger_ git_info = self:_normalize_git_info(git_info) - local bufnr = vim.api.nvim_get_current_buf() - if self.cache.issues[bufnr] and self.cache.change_requests[bufnr] then - local items = {} - items = vim.list_extend(items, self.cache.issues[bufnr]) - items = vim.list_extend(items, self.cache.change_requests[bufnr]) - log.fmt_debug("Got %d issues and prs from cache", #items) - callback({ items = self.cache.issues[bufnr], isIncomplete = false }) - return true, nil - end - - local items = {} + local issue_items = {} local state = { cancelled = false, active = nil } local controller = { command = "github_issues_and_change_requests", @@ -162,40 +152,41 @@ function GitHub:complete_issues_and_change_requests(callback, git_info, trigger_ end, } - local issues_job = self:_issues_job(function(args) + -- The capability jobs fill the caches. On a cache hit, a capability calls the callback at once and gives no job, + -- so the pull requests are requested only after the issues are known. + local function complete_change_requests() if state.cancelled then return end - items = args.items - self.cache.issues[bufnr] = args.items - end, git_info, trigger_char) - local change_requests_job = self:_change_requests_job(function(args) + local change_requests_job = self:_change_requests_job(function(args) + if state.cancelled then + return + end + -- A new list, so that the issue cache does not get the pull requests + local items = vim.list_extend(vim.list_extend({}, issue_items), args.items) + log.fmt_debug("Got %d issues and prs", #items) + callback({ items = items, isIncomplete = false }) + end, git_info, trigger_char) + + if change_requests_job then + state.active = change_requests_job + change_requests_job:start() + end + end + + local issues_job = self:_issues_job(function(args) if state.cancelled then return end - local prs = args.items - self.cache.change_requests[bufnr] = args.items - items = vim.list_extend(items, prs) - - log.fmt_debug("Got %d issues and prs from GitHub", #items) - callback({ items = items, isIncomplete = false }) + issue_items = args.items end, git_info, trigger_char) if issues_job then state.active = issues_job - issues_job:start(function() - if state.cancelled then - return - end - if change_requests_job then - state.active = change_requests_job - change_requests_job:start() - end - end) - elseif change_requests_job then - state.active = change_requests_job - change_requests_job:start() + issues_job:start(complete_change_requests) + else + complete_change_requests() end return true, controller diff --git a/tests/sources/test_github.lua b/tests/sources/test_github.lua index f0edc64..855d2c7 100644 --- a/tests/sources/test_github.lua +++ b/tests/sources/test_github.lua @@ -1,4 +1,5 @@ local helpers = require("tests.helpers") +local command = require("cmp_git.command") local config = require("cmp_git.config") local GitHub = require("cmp_git.sources.github") @@ -34,4 +35,83 @@ T["github_instance_state_isolation"] = function() ok(first.cache.pull_requests ~= second.cache.pull_requests, "GitHub PR cache table is instance-local") end +local git_info = { host = "github.com", owner = "owner", repo = "repo" } + +---A GitHub source whose `gh` commands give one issue and one pull request. `requests` counts the commands that ran. +---@return cmp_git.Source.GitHub source +---@return { issue: integer, pr: integer } requests +local function fake_github() + local source = GitHub.new(config.normalize({}).github) + local requests = { issue = 0, pr = 0 } + local records = { + issue = { number = 1, title = "an issue", body = "", updatedAt = "2024-01-01T00:00:00Z" }, + pr = { number = 2, title = "a pull request", body = "", updatedAt = "2024-01-01T00:00:00Z" }, + } + + -- Do not run `gh repo set-default` + helpers.stub(source, "_normalize_git_info", function(_, info) + return info + end) + helpers.stub(command, "build_fallback_list", function(commands, callback, handle_item) + local kind = commands[1].args[1] + return { + command = "fake", + start = function(_, on_complete) + requests[kind] = requests[kind] + 1 + callback({ items = { handle_item(vim.deepcopy(records[kind])) }, isIncomplete = false }) + if on_complete then + on_complete(true) + end + end, + } + end) + + return source, requests +end + +---@param items cmp_git.CompletionItem[] +---@return string[] +local function labels(items) + return vim.tbl_map(function(item) + return item.label + end, items) +end + +T["issues_and_change_requests_keep_the_caches_separate"] = function() + local source, requests = fake_github() + local callback, lists = helpers.recorder() + local bufnr = vim.api.nvim_get_current_buf() + + source:complete_issues_and_change_requests(callback, git_info, "#") + eq(labels(lists[1].items), { "#1: an issue", "#2: a pull request" }, "issues, then pull requests") + eq(labels(source.cache.issues[bufnr]), { "#1: an issue" }, "the issue cache has no pull requests") + + source:complete_issues_and_change_requests(callback, git_info, "#") + eq( + labels(lists[2].items), + { "#1: an issue", "#2: a pull request" }, + "a cache hit gives the issues and pull requests" + ) + + source:complete_issues(callback, git_info, "#") + eq(labels(lists[3].items), { "#1: an issue" }, "the issues action gives only issues") + eq(requests, { issue = 1, pr = 1 }, "each command runs once") +end + +T["issues_and_change_requests_after_a_cached_change_request"] = function() + local source, requests = fake_github() + local callback, lists = helpers.recorder() + + source:complete_change_requests(callback, git_info, "#") + source:complete_issues_and_change_requests(callback, git_info, "#") + + eq(#lists, 2, "one list for each request") + eq( + labels(lists[2].items), + { "#1: an issue", "#2: a pull request" }, + "the cached pull requests come after the issues" + ) + eq(requests, { issue = 1, pr = 1 }, "each command runs once") +end + return T From b7a3f505555125d94fdfe8b3c3d939d3879a7684 Mon Sep 17 00:00:00 2001 From: Peter Tri Ho Date: Thu, 22 Oct 2026 19:56:14 +1100 Subject: [PATCH 5/6] fix: remove pull requests from the GitHub issues that curl gets The REST issues endpoint also returns pull requests, with a pull_request field. gh issue list does not. With curl, # showed each pull request two times: as an issue and as a pull request. The issues request now removes the items that have a pull_request field. Because the pull requests use part of the page, curl can give fewer issues than limit. docs/configuration.md tells this. --- docs/configuration.md | 2 ++ lua/cmp_git/provider/adapters/github.lua | 14 ++++++++++++++ tests/provider/adapters/test_github.lua | 12 ++++++++++++ 3 files changed, 28 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 90b44c9..9af2481 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -232,6 +232,8 @@ The results are different in these cases: this state. - With curl, `github.issues` and `github.pull_requests` get at most 100 items. cmp-git requests one page, and the GitHub API gives at most 100 items for each page. +- With curl, the page of `github.issues` also contains pull requests. cmp-git removes them, so it can show fewer issues + than `limit`. - GitLab issues, merge requests, and mentions get at most 100 items, for the same reason. This limit applies to `glab` and to curl. diff --git a/lua/cmp_git/provider/adapters/github.lua b/lua/cmp_git/provider/adapters/github.lua index e2d1466..b2b79cf 100644 --- a/lua/cmp_git/provider/adapters/github.lua +++ b/lua/cmp_git/provider/adapters/github.lua @@ -106,6 +106,20 @@ function M.issues_request(git_info, trigger_char, config) normalize_body(issue) return format.item(config, trigger_char, issue) end, + -- The REST API also returns pull requests, with a `pull_request` field. `gh issue list` does not. + handle_parsed = function(parsed) + local result = {} + if type(parsed) ~= "table" then + return result + end + + for _, issue in ipairs(parsed) do + if issue.pull_request == nil or issue.pull_request == vim.NIL then + table.insert(result, issue) + end + end + return result + end, } end diff --git a/tests/provider/adapters/test_github.lua b/tests/provider/adapters/test_github.lua index f05dbee..f43a988 100644 --- a/tests/provider/adapters/test_github.lua +++ b/tests/provider/adapters/test_github.lua @@ -243,6 +243,18 @@ T["mentions_request"] = function() eq(request.handle_item({ login = "octocat" }).label, "@octocat", "mention item") end +T["issues_request_drops_pull_requests"] = function() + local request = adapter.issues_request(github, "#", github_config().issues) + + eq( + request.handle_parsed({ { number = 1 }, { number = 2, pull_request = { url = "u" } }, { number = 3 } }), + { { number = 1 }, { number = 3 } }, + "the REST issues endpoint also returns pull requests" + ) + eq(request.handle_parsed({ message = "Not Found" }), {}, "an error response gives no issues") + eq(request.handle_parsed(vim.NIL), {}, "a null response gives no issues") +end + T["handle_item_normalizes_body"] = function() local request = adapter.issues_request(github, "#", github_config().issues) From fc8f7dfb74942ca64f999ec3aedf952f5beb4585 Mon Sep 17 00:00:00 2001 From: Peter Tri Ho Date: Thu, 22 Oct 2026 19:56:21 +1100 Subject: [PATCH 6/6] fix: show the commit author, not the committer The author_name and author_mail fields came from %cn and %ce, which are the committer. After a rebase or a cherry-pick by another person, the commit documentation showed that person as the author. The fields now come from %aN and %aE, so they respect the mailmap, as the trailer identities do. The documentation now says "Authored by (), committed on " in place of "Commited by". The date is still the commit date. --- docs/configuration.md | 3 ++- lua/cmp_git/format.lua | 2 +- lua/cmp_git/provider/capabilities/commits.lua | 2 +- tests/provider/capabilities/test_commits.lua | 2 +- tests/test_format.lua | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9af2481..1d15f11 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -137,7 +137,8 @@ For compatibility, cmp-git also supports `filter_fn` on a provider, as an alias - `sha` has `git.commits.sha_length` characters (default 7). The default `insertText` inserts `sha`. The default `label` shows the first 7 characters. -- `author_name` and `author_mail` are the name and the email of the committer. +- `author_name` and `author_mail` are the name and the email of the author, with the `.mailmap` applied. + `commit_timestamp` is the commit date. - GitHub issues and pull requests always have `body` and `updatedAt`. - `diff` is the number of seconds since the commit, or since the most recent use of the identity or the value. - `count` is the number of uses in the scanned commits. diff --git a/lua/cmp_git/format.lua b/lua/cmp_git/format.lua index 227fbb8..07ba26f 100644 --- a/lua/cmp_git/format.lua +++ b/lua/cmp_git/format.lua @@ -26,7 +26,7 @@ local M = { return { kind = "markdown", value = string.format( - "# %s\n\n%s\n\nCommited by %s (%s) on %s", + "# %s\n\n%s\n\nAuthored by %s (%s), committed on %s", commit.title, commit.description, commit.author_name, diff --git a/lua/cmp_git/provider/capabilities/commits.lua b/lua/cmp_git/provider/capabilities/commits.lua index 65734e7..f774f76 100644 --- a/lua/cmp_git/provider/capabilities/commits.lua +++ b/lua/cmp_git/provider/capabilities/commits.lua @@ -17,7 +17,7 @@ function M.request(config) "-n", tostring(config.limit), "--date=unix", - "--pretty=format:%H%x00%s%x00%b%x00%cn%x00%ce%x00%cd%x00", + "--pretty=format:%H%x00%s%x00%b%x00%aN%x00%aE%x00%cd%x00", }, } end diff --git a/tests/provider/capabilities/test_commits.lua b/tests/provider/capabilities/test_commits.lua index f837e8c..1fb9777 100644 --- a/tests/provider/capabilities/test_commits.lua +++ b/tests/provider/capabilities/test_commits.lua @@ -107,7 +107,7 @@ T["commit_capability_uses_parsed_cache_and_runner"] = function() eq(spec.args[3], "100", "commit capability command limit") eq( spec.args[5], - "--pretty=format:%H%x00%s%x00%b%x00%cn%x00%ce%x00%cd%x00", + "--pretty=format:%H%x00%s%x00%b%x00%aN%x00%aE%x00%cd%x00", "commit capability command format" ) return helpers.fake_job(function() diff --git a/tests/test_format.lua b/tests/test_format.lua index 4d14b17..927589f 100644 --- a/tests/test_format.lua +++ b/tests/test_format.lua @@ -35,7 +35,7 @@ T["git_commits"] = function() label = "abcdef1: feat: add a thing", insertText = "abcdef1234567890", filterText = ": abcdef1 feat: add a thing", - documentation = "# feat: add a thing\n\nBody text\n\nCommited by Ada Lovelace (ada@example.com) on " + documentation = "# feat: add a thing\n\nBody text\n\nAuthored by Ada Lovelace (ada@example.com), committed on " .. os.date("%c", 1234567890), }) eq(item.documentation.kind, "markdown", "documentation is markdown")