fix(pack): only use tags that strictly comply with semver spec #39342

Problem: Using `version=vim.version.range(...)` in plugin specification
  is meant to use semver-like tags. Whether a tag is semver-like was
  decided by a plain `vim.version.parse` which is not strict by default.
  This allowed treating tags like `nvim-0.6` (which is usually reserved
  for the latest revision compatible with Nvim<=0.6 version) like semver
  tags and resulted in confusing behavior (preferring `nvim-0.6` tag
  over `v0.2.2`, for example).

Solution: Use `vim.version.range(x, { strict = true })` to decide if the
  tag name is semver-like or not. This allows tags like both `v1.2.3`
  and `1.2.3` while being consistent in what Nvim thinks is a semver
  string.

  This is technically not a breaking change since it was documented that
  only tags like `v<major>.<minor>.<patch>` will be recognized as
  semver.
This commit is contained in:
Evgeni Chasnovski
2026-04-23 18:14:06 +03:00
committed by GitHub
parent 2124ffb27b
commit f8c94bb8cf
3 changed files with 25 additions and 21 deletions

View File

@@ -220,7 +220,8 @@ is assumed that all plugins in the directory are managed exclusively by
Uses Git to manage plugins and requires present `git` executable. Target
plugins should be Git repositories with versions as named tags following
semver convention `v<major>.<minor>.<patch>`.
semver convention `v<major>.<minor>.<patch>` (with or without `v` prefix).
Like `v1.2.0` or `1.2.0`, but not `1.2` or `v1`.
The latest state of all managed plugins is stored inside a *vim.pack-lockfile*
located at `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`. It is a JSON file that

View File

@@ -12,7 +12,8 @@
---
---Uses Git to manage plugins and requires present `git` executable.
---Target plugins should be Git repositories with versions as named tags
---following semver convention `v<major>.<minor>.<patch>`.
---following semver convention `v<major>.<minor>.<patch>` (with or without `v` prefix).
---Like `v1.2.0` or `1.2.0`, but not `1.2` or `v1`.
---
---The latest state of all managed plugins is stored inside a [vim.pack-lockfile]()
---located at `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`. It is a JSON file that
@@ -255,6 +256,10 @@ local function git_cmd(cmd, cwd)
return (stdout:gsub('\n+$', ''))
end
local function parse_semver(x)
return vim.version.parse(x, { strict = true })
end
--- @type vim.Version
local git_version
@@ -274,7 +279,7 @@ local function git_clone(url, path)
if vim.startswith(url, 'file://') then
cmd[#cmd + 1] = '--no-hardlinks'
elseif git_version >= vim.version.parse('2.27') then
elseif git_version >= parse_semver('2.27.0') then
cmd[#cmd + 1] = '--filter=blob:none'
end
@@ -343,7 +348,7 @@ end
--- @param x string
--- @return boolean
local function is_semver(x)
return vim.version.parse(x) ~= nil
return parse_semver(x) ~= nil
end
local function is_nonempty_string(x)
@@ -585,7 +590,7 @@ end
local function get_last_semver_tag(tags, version_range)
local last_tag, last_ver_tag --- @type string, vim.Version
for _, tag in ipairs(tags) do
local ver_tag = vim.version.parse(tag)
local ver_tag = parse_semver(tag)
if ver_tag then
if version_range:has(ver_tag) and (not last_ver_tag or ver_tag > last_ver_tag) then
last_tag, last_ver_tag = tag, ver_tag
@@ -667,7 +672,7 @@ local function checkout(p, timestamp, skip_stash)
if not skip_stash then
local stash_cmd = { 'stash' }
if git_version > vim.version.parse('2.13') then
if git_version > parse_semver('2.13.0') then
-- Use 'push' to avoid a 'stash -m' bug in versions prior to git v2.26
stash_cmd[#stash_cmd + 1] = 'push'
stash_cmd[#stash_cmd + 1] = '--message'
@@ -680,7 +685,7 @@ local function checkout(p, timestamp, skip_stash)
git_cmd({ 'checkout', '--quiet', p.info.sha_target }, p.path)
local submodule_cmd = { 'submodule', 'update', '--init', '--recursive' }
if git_version >= vim.version.parse('2.36') then
if git_version >= parse_semver('2.36.0') then
submodule_cmd[#submodule_cmd + 1] = '--filter=blob:none'
end
git_cmd(submodule_cmd, p.path)
@@ -760,7 +765,7 @@ local function infer_update_details(p)
end
local older_tags = ''
if git_version >= vim.version.parse('2.13') then
if git_version >= parse_semver('2.13.0') then
older_tags = git_cmd({ 'tag', '--list', '--no-contains', sha_head }, p.path)
end
local cur_tags = git_cmd({ 'tag', '--list', '--points-at', sha_head }, p.path)

View File

@@ -225,8 +225,8 @@ function repos_setup.semver()
add_tag('v0.3.0')
repo_write_file('semver', 'lua/semver.lua', 'return "semver middle-commit')
git_add_commit('Add middle commit', 'semver')
add_tag('0.3.1')
add_tag('v0.4')
add_tag('0.3.1') -- Semver even without `v` prefix
add_tag('v0.4') -- Not semver since it requires all three version numbers
add_tag('non-semver')
add_tag('v0.2.1') -- Intentionally add version not in order
add_tag('v1.0.0')
@@ -946,7 +946,7 @@ describe('vim.pack', function()
eq('basic some-tag', exec_lua('return require("basic")'))
eq('defbranch main', exec_lua('return require("defbranch")'))
eq('semver v0.4', exec_lua('return require("semver")'))
eq('semver 0.3.1', exec_lua('return require("semver")'))
end)
it('respects plugin/ and after/plugin/ scripts', function()
@@ -1063,7 +1063,7 @@ describe('vim.pack', function()
'Available:\nTags: some%-tag\nBranches: main, feat%-branch',
-- Should report available branches and versions if no constraint match
'`semver`',
'Available:\nVersions: v1%.0%.0, v0%.4, 0%.3%.1, v0%.3%.0.*\nBranches: main\n',
'Available:\nVersions: v1%.0%.0, 0%.3%.1, v0%.3%.0.*\nBranches: main\n',
'`pluginerr`:\n',
'Wow, an error',
}
@@ -1260,7 +1260,7 @@ describe('vim.pack', function()
-- This requires computing target hashes on each test run because they
-- change due to source repos being cleanly created on each file test.
local screen
screen = Screen.new(85, 35)
screen = Screen.new(85, 34)
hashes.fetch_new = git_get_hash('main', 'fetch')
short_hashes.fetch_new = git_get_short_hash('main', 'fetch')
@@ -1301,7 +1301,6 @@ describe('vim.pack', function()
|
Available newer versions: |
• {102:v1.0.0} |
• {102:v0.4} |
• {102:0.3.1} |
{1:~ }|
|
@@ -1453,8 +1452,8 @@ describe('vim.pack', function()
{ lnum = 3, col = 1, end_lnum = 9, end_col = 1, text = '[Module] defbranch' },
{ lnum = 9, col = 1, end_lnum = 22, end_col = 1, text = '[Namespace] Update' },
{ lnum = 11, col = 1, end_lnum = 22, end_col = 1, text = '[Module] fetch' },
{ lnum = 22, col = 1, end_lnum = 32, end_col = 1, text = '[Namespace] Same' },
{ lnum = 24, col = 1, end_lnum = 32, end_col = 1, text = '[Module] semver (not active)' },
{ lnum = 22, col = 1, end_lnum = 31, end_col = 1, text = '[Namespace] Same' },
{ lnum = 24, col = 1, end_lnum = 31, end_col = 1, text = '[Module] semver (not active)' },
}
eq(ref_loclist, loclist)
@@ -1497,8 +1496,7 @@ describe('vim.pack', function()
assert_hover({ 20, 0 }, 'Commit to be added 1')
assert_hover({ 27, 0 }, 'Add version v0.3.0')
assert_hover({ 30, 0 }, 'Add version v1.0.0')
assert_hover({ 31, 0 }, 'Add version v0.4')
assert_hover({ 32, 0 }, 'Add version 0.3.1')
assert_hover({ 31, 0 }, 'Add version 0.3.1')
-- textDocument/codeAction
n.exec_lua(function()
@@ -1560,7 +1558,7 @@ describe('vim.pack', function()
-- - Only deletion should be available for not active plugins
assert_action({ 24, 0 }, { 'Delete `semver`' }, 0)
assert_action({ 28, 0 }, { 'Delete `semver`' }, 0)
assert_action({ 32, 0 }, { 'Delete `semver`' }, 0)
assert_action({ 31, 0 }, { 'Delete `semver`' }, 0)
-- - Should correctly perform action and remove plugin's lines
local function line_match(lnum, pattern)
@@ -1633,7 +1631,7 @@ describe('vim.pack', function()
-- - Should not wrap around the edge
assert(']]', { 24, 0 })
api.nvim_win_set_cursor(0, { 32, 1 })
api.nvim_win_set_cursor(0, { 31, 1 })
assert('[[', { 24, 0 })
assert('[[', { 11, 0 })
assert('[[', { 3, 0 })
@@ -1652,7 +1650,7 @@ describe('vim.pack', function()
-- Should correctly infer that 0.3.0 is the latest version and suggest
-- versions greater than that
local confirm_text = table.concat(api.nvim_buf_get_lines(0, 0, -1, false), '\n')
matches('Available newer versions:\n• v1%.0%.0\n v0%.4\n 0%.3%.1$', confirm_text)
matches('Available newer versions:\n• v1%.0%.0\n• 0%.3%.1$', confirm_text)
end)
it('updates lockfile', function()