Skip to content

fix(import): fold sources differing only in case into one plugin - #35

Closed
zuqini wants to merge 1 commit into
mainfrom
fix/name-collisions
Closed

fix(import): fold sources differing only in case into one plugin#35
zuqini wants to merge 1 commit into
mainfrom
fix/name-collisions

Conversation

@zuqini

@zuqini zuqini commented Aug 2, 2026

Copy link
Copy Markdown
Owner

A plugin installs into a directory named after it, so two specs resolving to the same name compete for it. vim.pack.add finds out only when git refuses the second clone with destination path '.../ZSnip.nvim' already exists and is not an empty directory — a message that names neither spec and aborts setup() for the whole config.

Hit while migrating a config: one spec file declared zuqini/ZSnip.nvim, another listed zuqini/zsnip.nvim in its dependencies.

Same repository, different casing. Git hosts match owner and repo case-insensitively, so these are one plugin. They now fold onto the casing imported first and their specs merge, exactly as if both had been written alike. Folding happens in normalize_source — the single funnel both import_one_spec and register_dependencies go through, so dependency_graph is keyed by the surviving source too. Local sources are deliberately not folded: on a case-sensitive filesystem two paths differing in case are two directories.

Genuinely different plugins claiming one name (alice/shared.nvim vs bob/shared.nvim). Reported against both sources, keeping the one imported first:

zpack: skipping https://github.com/bob/shared.nvim — it resolves to the same
plugin directory (shared.nvim) as https://github.com/alice/shared.nvim.
Set `name = "..."` on one of them to install both.

Names differing only in case (alice/shared.nvim vs bob/Shared.nvim) are warned about but both kept — they are two directories on ext4, and dropping one there would remove a plugin that installs perfectly well. The warning is what explains the vim.pack error on macOS or Windows.

Collision resolution runs after prune_disabled, so a disabled spec cannot win a directory it was never going to occupy, and walks the registry in import order so the winner does not vary between starts. Drops go through the same worklist as prune_disabled, so a skipped plugin takes its dep-only dependencies with it instead of leaving them installed and orphaned.

Prior art: lazy.nvim keys its plugin registry by derived name, which is what makes the same plugin referenced two ways collapse into one. zpack keys by source, so the fold happens at import instead.

Tests

14 cases in tests/name_collision_test.lua covering folding, spec merging across casings, dependency-graph keying, lazy-parent dependency loading, first-casing-wins, local dirs staying separate, the collision report, case-variants coexisting, name breaking the tie, disabled specs not winning, and orphan cleanup. Full suite 497 passing; luacheck and lua-language-server clean.

Docs: doc/zpack.txt (zpack-gotcha-name-collision, cross-referenced from zpack-Spec.name), docs/tips.md, docs/spec.md.

A plugin installs into a directory named after it, so two specs resolving to
the same name compete for it. vim.pack.add finds out only when git refuses the
second clone with "destination path already exists and is not an empty
directory" -- a message naming neither spec, which aborts setup() for the whole
config.

Same repository, different casing (zuqini/ZSnip.nvim vs zuqini/zsnip.nvim) is
one plugin: git hosts match owner and repo case-insensitively. Those now fold
onto the casing imported first and their specs merge. The fold lives in
normalize_source, the funnel both import_one_spec and register_dependencies go
through, so the dependency graph is keyed by the surviving source too --
folding at the registration site alone left edges naming a source no registry
entry had, and every consumer guards on the entry existing, so a lazy parent
silently never loaded its dependency.

Genuinely different plugins claiming one name are reported against both
sources, keeping the one imported first. Names differing only in case are
warned about but both kept: they are two directories on a case-sensitive
filesystem, and dropping one there would remove a plugin that installs fine.
Collision resolution runs after prune_disabled so a disabled spec cannot win a
directory it was never going to occupy, and drops through the same worklist as
prune_disabled so a skipped plugin takes its dep-only dependencies with it
rather than leaving them installed and orphaned.
@zuqini
zuqini force-pushed the fix/name-collisions branch from 97e8564 to 41c0501 Compare August 2, 2026 05:44
@zuqini

zuqini commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Closing — not worth the complexity.

vim.pack already catches the platform-independent case: two specs deriving the
same name raise Conflicting src naming both sources (pack.lua:483), before
anything is installed. What's left is same-repo-different-casing, which only
misbehaves on case-insensitive filesystems and would require zpack to model
filesystem case semantics. lazy.nvim doesn't handle it either — it clones the
repo twice and gives the dependency an unconfigured copy.

@zuqini zuqini closed this Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant