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 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..1d15f11 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,279 @@ +# 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 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. +- `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. +- 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. + +`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. 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/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/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/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 = { 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/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) 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/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 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")