8000 fix(utils): only pass git URLs through [1]; flag local paths in validate by zuqini · Pull Request #44 · zuqini/zpack.nvim · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified Diff View
Unified
Diff view
37 changes: 22 additions & 15 deletions doc/zpack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -595,9 +595,9 @@ parent's lazy trigger fires.
>lua
{
-- Plugin source (provide at least one; src > url > dir > [1])
[1] = "user/repo", -- Short name (github.com/...)
src = "https://...", -- Custom git URL or local path
dir = "/path/to/plugin", -- Local path (lazy.nvim compat, ~ expanded)
[1] = "user/repo", -- GitHub short name, or a full git URL
src = "https://...", -- Git URL, passed to git verbatim
dir = "/path/to/plugin", -- Local checkout (~ expanded; lazy.nvim compat)
url = "https://...", -- Git URL (lazy.nvim compat)

-- Dependencies
Expand Down Expand Up @@ -667,25 +667,31 @@ parent's lazy trigger fires.

*zpack-Spec.[1]*
[1] (string, optional)
Plugin short name (e.g., "user/repo"). Expands to
`https://github.com/{[1]}` when no explicit src, url, or
dir is given (lazy.nvim parity). Provide at least one of:
[1], src, dir, or url. Priority: src > url > dir > [1].
Plugin short name (e.g., "user/repo") or a full git URL.
`owner/repo` shorthand expands to `https://github.com/{[1]}`
when no explicit src, url, or dir is given; a full git URL
(any `scheme://` URL or scp-style `user@host:`) is used
as-is (lazy.nvim parity). Use |zpack-Spec.dir| for local
paths.
Provide at least one of: [1], src, dir, or url.
Priority: src > url > dir > [1].
The plugin NAME still derives from [1] even when an
explicit source wins; specs deriving the same name merge
into one plugin — see |zpack-Spec.name| for precedence.

*zpack-Spec.src*
src (string, optional)
Custom git URL or local path for the plugin source.
Custom git URL for the plugin source. Passed to git
verbatim: an absolute local path works, but `~` is not
expanded — use |zpack-Spec.dir| for local checkouts.
Overrides [1] when both are set.

*zpack-Spec.dir*
dir (string, optional)
Local plugin directory path. For lazy.nvim compatibility.
Paths are automatically expanded (e.g., `~` expands to home
directory). Mapped to `src` internally. Overrides [1] when
both are set.
Local plugin directory. This is the field for local
checkouts: the path is |expand()|ed, so `~` and environment
variables work. lazy.nvim compatibility; mapped to `src`
internally. Overrides [1] when both are set.

*zpack-Spec.url*
url (string, optional)
Expand All @@ -697,9 +703,10 @@ url (string, optional)
dependencies (string|string[]|zpack.Spec|zpack.Spec[], optional)
Plugin dependencies. Dependencies are automatically loaded
before the parent plugin when the parent's lazy trigger fires.
Can be a string (plugin short name), array of strings, inline
spec, or array of specs. Dependency specs can have their own
configuration (opts, config, etc.) and nested dependencies.
Can be a string (a `[1]` value, see |zpack-Spec|), array
of strings, inline spec, or array of specs. Dependency
specs can have their own configuration (opts, config, etc.)
and nested dependencies.

*zpack-Spec.enabled*
enabled (boolean|function, optional)
Expand Down
6 changes: 3 additions & 3 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
```lua
{
-- Plugin source (provide at least one; src > url > dir > [1])
[1] = "user/repo", -- Plugin short name. Expands to https://github.com/{user/repo} when no explicit src/url/dir is given; the plugin name always derives from [1]
src = "https://...", -- Custom git URL or local path
dir = "/path/to/plugin", -- Local plugin directory (lazy.nvim compat, ~ expanded, mapped to src)
[1] = "user/repo", -- Plugin short name or full git URL. owner/repo expands to https://github.com/{owner/repo} when no explicit src/url/dir is given; a full git URL (any scheme:// URL or scp-style user@host:) is used as-is; use dir for local paths. The plugin name always derives from [1]
src = "https://...", -- Custom git URL, passed to git verbatim (~ not expanded — use dir for local checkouts)
dir = "/path/to/plugin", -- Local plugin directory, the field for local checkouts (~ expanded; lazy.nvim compat, mapped to src)
url = "https://...", -- Custom git URL (lazy.nvim compat, mapped to src, overrides [1] — e.g. to swap in a fork)

-- Dependencies
Expand Down
7 changes: 4 additions & 3 deletions lua/zpack/import.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ end
---Normalize plugin source using priority: dev > src > url > dir > [1]
---lazy.nvim parity: `[1]` is only a fallback for deriving the URL, so an
---explicit src/url/dir wins — `{ 'user/repo', url = fork }` installs the fork.
---fold_rank in merge.lua mirrors this precedence and its explicit-field set;
---update both together.
---fold_rank in merge.lua mirrors this precedence and its explicit-field set,
---and validate_spec's local-path advisory in validate.lua mirrors the
---"`[1]` is the source" condition; update all three together.
---@param spec zpack.Spec
---@return string|nil source URL/path, or nil if invalid
---@return string|nil error message if validation fails
Expand All @@ -72,7 +73,7 @@ local normalize_source = function(spec)
return dev_path
end
-- Each source field must be a string; a non-string (over-nested spec or
-- typo) would crash the `[1]` concat or `dir` expand. Skip rather than
-- typo) would crash the `[1]` or `dir` expansion. Skip rather than
-- abort setup().
if type(spec.src) == 'string' then
return spec.src
Expand Down
2 changes: 1 addition & 1 deletion lua/zpack/merge.lua
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ end

---Rank an entry for the coalesce fold as a (tier, import order) pair; compare
---tier first, then order. A `dev = true` fragment that did not fall back to
---its own `[1]` shorthand wins outright — dev outranks the whole explicit
---its own `[1]` wins outright — dev outranks the whole explicit
---chain in normalize_source, independent of import order. Below that, an
---explicit-source fragment beats a bare one, and among bare entries a
---top-level fragment beats a dependency-only one — the same
Expand Down
6 changes: 3 additions & 3 deletions lua/zpack/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
---@alias zpack.FtValue string|string[]

---@class zpack.Spec
---@field [1]? string Plugin short name (e.g., "user/repo"). Fallback source when src/dir/url not provided
---@field src? string Custom git URL or local path. Highest-priority source, overrides [1]
---@field dir? string Local plugin directory path (lazy.nvim compat). Mapped to src, overrides [1]
---@field [1]? string Plugin short name (e.g., "user/repo") or full git URL. Fallback source when src/dir/url not provided; use dir for local paths
---@field src? string Custom git URL, passed to git verbatim (`~` not expanded — use dir for local checkouts). Highest-priority source, overrides [1]
---@field dir? string Local plugin directory, the field for local checkouts (`~` expanded; lazy.nvim compat). Mapped to src, overrides [1]
---@field url? string Custom git URL (lazy.nvim compat). Mapped to src, overrides [1]
---@field name? string Custom plugin name. Overrides the name derived from [1] (or the source URL)
---@field init? fun(plugin: zpack.Plugin?)
Expand Down
14 changes: 8 additions & 6 deletions
Original file line number Diff line number Diff line change
Expand Up @@ -290,14 +290,16 @@ M.derive_name_from_src = function(src)
return (basename:gsub('%.git$', ''))
end

---Resolve a `[1]` value to a source. `user/repo` shorthand expands to its GitHub URL; full URLs and
---paths are returned unchanged. Shared by
---`normalize_source` and merge.lua's coalesce fold so the two sites cannot
---drift on what a shorthand resolves to.
---@param src string
---Resolve a `[1]` value to a source. `owner/repo` shorthand expands to its
---GitHub URL; a full git URL (`scheme://`, `user@host:`) is returned
---unchanged (lazy.nvim parity). Filesystem paths are not recognized: vim.pack
---hands src to git verbatim, so local checkouts belong in `dir`, which
---normalize_source expands. Shared by `normalize_source` and merge.lua's
---coalesce fold so the two sites cannot drift on what a shorthand resolves to.
---@param src string `[1]` value: `owner/repo` or a full git URL
---@return string
M.expand_src = function(src)
if src:match('^%a[%w+.-]*://') or src:match('^[%w_.-]+@[^/]+:') or src:match('^[/~.]') then
if src:match('^%a[%w+.-]*://') or src:match('^[%w_.-]+@[^/]+:') then
return src
end
return 'https://github.com/' .. src
Expand Down
11 changes: 11 additions & 0 deletions lua/zpack/validate.lua
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ function M.validate_spec(spec)

local errors = {}
check(errors, '[1]', spec[1], 'string')
-- `[1]` is the source only when no src/url/dir is set and the spec is not
-- `dev` (a dev spec loads from dev.path; `[1]` reaches git only under
-- `dev.fallback = true`, which validate cannot see). `utils.expand_src` has
-- no path branch, so a path here becomes a github.com URL that does not
-- exist; `dir` is the expanded local-path field.
if type(spec[1]) == 'string' and spec[1]:match('^[~./]')
and spec.src == nil and spec.url == nil and spec.dir == nil
and spec.dev ~= true then
errors[#errors + 1] = ('[1] "%s" looks like a local path — use dir for local checkouts')
:format(spec[1])
end
for _, field in ipairs(SORTED_SPEC_FIELDS) do
check(errors, field, spec[field], SPEC_FIELD_TYPES[field])
end
Expand Down
2 changes: 1 addition & 1 deletion tests/import_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ describe("Spec Import", function()
it("spec with a non-string source is skipped, not crashed on", function()
local state = require('zpack.state')
-- An over-nested spec — `[1]` is a table, not a "user/plugin" string.
-- normalize_source must not crash concatenating it; the spec is
-- normalize_source must not crash expanding it; the spec is
-- skipped and setup() completes rather than aborting partway through.
require('zpack').setup({ spec = { { { 'user/plugin' } } } })
helpers.flush_pending()
Expand Down
17 changes: 17 additions & 0 deletions tests/setup_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,23 @@ describe("Setup and Initialization", function()
"[1] shorthand must not be used when url is set")
end)

it("[1] with a full git URL is used as-is, not prefixed with github.com", function()
local state = require('zpack.state')
local url = 'https://forge.example.com/owner/repo'

require('zpack').setup({
spec = { { url } },
defaults = { confirm = false },
})

assert.is_not_nil(state.spec_registry[url],
"a full URL in [1] must register under that URL")
assert.is_nil(state.spec_registry['https://github.com/' .. url],
"a full URL in [1] must not be prefixed with github.com")
assert.are.equal(url, state.name_to_src['repo'],
"plugin name derives from the URL basename")
end)

it("source precedence is src > url > dir > [1]", function()
local state = require('zpack.state')

Expand Down
18 changes: 11 additions & 7 deletions tests/utils_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,15 @@ describe('latch_first_call', function()
end)

describe("expand_src", function()
local utils = require('zpack.utils')

it("expands a short name to a GitHub URL", function()
local utils = require('zpack.utils')
assert.are.equal('https://github.com/user/repo', utils.expand_src('user/repo'))
end)

it("returns full URLs unchanged", function()
local utils = require('zpack.utils')
local urls = {
'https://forge.barrettruth.com/barrettruth/canola.nvim',
'https://forge.example.com/owner/repo',
'http://example.com/user/repo.git',
'ssh://git@example.com/user/repo.git',
'git@example.com:user/repo.git',
Expand All @@ -213,9 +213,13 @@ describe("expand_src", function()
end
end)

it("returns local paths unchanged", function()
assert.are.equal('/home/user/repo', utils.expand_src('/home/user/repo'))
assert.are.equal('~/repo', utils.expand_src('~/repo'))
assert.are.equal('./repo', utils.expand_src('./repo'))
it("treats paths as shorthand (local checkouts use dir)", function()
local utils = require('zpack.utils')
-- vim.pack gives src to git verbatim, so a path in [1] would clone
-- literally; `dir` is the expanded local-path field. lazy.nvim parity:
-- anything that is not a git URL is owner/repo shorthand.
assert.are.equal('https://github.com/~/repo', utils.expand_src('~/repo'))
assert.are.equal('https://github.com/./repo', utils.expand_src('./repo'))
assert.are.equal('https://github.com//home/user/repo', utils.expand_src('/home/user/repo'))
end)
end)
24 changes: 24 additions & 0 deletions tests/validate_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,30 @@ describe("Spec Validation", function()
assert.is_truthy(errors[1]:find('[1]', 1, true) ~= nil, "error should name the [1] field")
end)

it("validate_spec flags a [1] that looks like a local path", function()
local validate = require('zpack.validate')
for _, path in ipairs({ '~/repo', './repo', '/home/user/repo' }) do
local errors = validate.validate_spec({ path })
assert.are.equal(1, #errors, path)
assert.is_truthy(errors[1]:find('use dir', 1, true) ~= nil,
path .. " should point at dir")
end
assert.are.equal(0, #validate.validate_spec({ 'https://forge.example.com/owner/repo' }))
assert.are.equal(0, #validate.validate_spec({ 'git@example.com:owner/repo.git' }))
end)

it("validate_spec does not flag a path-like [1] when an explicit source is set", function()
local validate = require('zpack.validate')
-- With src/url/dir present, [1] only feeds the name and never reaches git.
assert.are.equal(0, #validate.validate_spec({ '~/repo', dir = '~/repo' }))
assert.are.equal(0, #validate.validate_spec({ './repo', src = 'https://forge.example.com/owner/repo' }))
assert.are.equal(0, #validate.validate_spec({ '/home/user/repo', url = 'https://forge.example.com/owner/repo' }))
-- A dev spec loads from dev.path; [1] reaches git only under
-- dev.fallback = true with a missing checkout, which validate cannot
-- see, so it is not flagged.
assert.are.equal(0, #validate.validate_spec({ '~/repo', dev = true }))
end)

it("validate_spec rejects a non-table spec", function()
local validate = require('zpack.validate')
local errors = validate.validate_spec('user/plugin')
Expand Down
Loading
0