From 137a50c9321a5a660c2371633d75c4f707aedd75 Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Wed, 3 Jun 2026 08:57:46 +0800 Subject: [PATCH] fix(cli): warn when publishing a repo that contains submodules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `git archive` does not recurse into submodules, so a plugin shipping vendored code via submodule produced a tarball where the submodule path existed but was empty — silent failure. Now publish reads .gitmodules and lists submodule paths to stderr with guidance to vendor or pack them separately. The publish still proceeds, since the developer may not actually need the submodule contents in the archive. Co-Authored-By: Claude Opus 4.7 --- cmd/ninja/cmd/plugin.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cmd/ninja/cmd/plugin.go b/cmd/ninja/cmd/plugin.go index 240977c..9524cff 100644 --- a/cmd/ninja/cmd/plugin.go +++ b/cmd/ninja/cmd/plugin.go @@ -325,6 +325,18 @@ func newPluginPublishCmd() *cobra.Command { fmt.Fprintln(os.Stderr, " (run `git rm --cached ` to drop)") } + // `git archive` does not recurse into submodules, so any submodule + // paths will appear as empty directories in the tarball. Detect via + // .gitmodules so this works even for submodules that haven't been + // initialised yet. + if paths := submodulePaths("."); len(paths) > 0 { + fmt.Fprintln(os.Stderr, "warning: this repo has submodules; git archive will ship them as empty directories:") + for _, p := range paths { + fmt.Fprintln(os.Stderr, " "+p) + } + fmt.Fprintln(os.Stderr, " (vendor the contents or pack them separately if the plugin depends on them)") + } + archiveBytes, err := archive.BuildSourceArchive(".") if err != nil { return fmt.Errorf("build archive: %w", err) @@ -577,6 +589,27 @@ func checkRepoHasHEAD(repoDir string) error { return nil } +// submodulePaths returns the configured submodule paths from .gitmodules. +// Reading the file directly (rather than running `git submodule status`) means +// we detect submodules that have been declared but not yet initialised. +func submodulePaths(repoDir string) []string { + cmd := exec.Command("git", "config", "--file", ".gitmodules", "--get-regexp", `submodule\..*\.path`) + cmd.Dir = repoDir + out, err := cmd.Output() + if err != nil { + return nil + } + var paths []string + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + // each line is "submodule..path " + fields := strings.Fields(line) + if len(fields) >= 2 { + paths = append(paths, fields[len(fields)-1]) + } + } + return paths +} + func upsertPluginMod(scope, name, kind string, categories []string) error { const file = "plugin.mod" existing, _ := os.ReadFile(file)