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)