diff --git a/cmd/ninja/cmd/plugin.go b/cmd/ninja/cmd/plugin.go index c75455e..689e9df 100644 --- a/cmd/ninja/cmd/plugin.go +++ b/cmd/ninja/cmd/plugin.go @@ -109,7 +109,7 @@ func newPluginInitCmd() *cobra.Command { fmt.Fprintf(os.Stderr, "warning: could not commit plugin.mod: %v\n", err) } } else { - fmt.Println("Not in a git repo - run `git init` before `ninja plugin publish`") + fmt.Println("Not in a git repo - run `git init && git commit` before `ninja plugin publish`") } return nil }, @@ -294,6 +294,10 @@ func newPluginPublishCmd() *cobra.Command { return fmt.Errorf("plugin.mod must have scope, name, and version") } + if err := checkRepoHasHEAD("."); err != nil { + return err + } + if !allowDirty { out, _ := exec.Command("git", "status", "--porcelain").Output() if len(strings.TrimSpace(string(out))) > 0 { @@ -548,6 +552,20 @@ func runCmd(name string, args ...string) error { return c.Run() } +// checkRepoHasHEAD returns a friendlier error than the raw git failure when +// the publish flow is invoked in a freshly-initialised repository that has +// no commits yet. Without this, `git stash create` reports "You do not have +// the initial commit yet" via exit status 128, which surfaces as an internal +// failure to the user. +func checkRepoHasHEAD(repoDir string) error { + cmd := exec.Command("git", "rev-parse", "--verify", "HEAD") + cmd.Dir = repoDir + if err := cmd.Run(); err != nil { + return fmt.Errorf("no commits in repository; run `git add . && git commit` before publishing") + } + return nil +} + func upsertPluginMod(scope, name, kind string, categories []string) error { const file = "plugin.mod" existing, _ := os.ReadFile(file) diff --git a/cmd/ninja/cmd/plugin_test.go b/cmd/ninja/cmd/plugin_test.go new file mode 100644 index 0000000..48c073b --- /dev/null +++ b/cmd/ninja/cmd/plugin_test.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestCheckRepoHasHEAD_NoCommitsReturnsFriendlyError(t *testing.T) { + dir := t.TempDir() + runGit(t, dir, "init", "-q") + + err := checkRepoHasHEAD(dir) + if err == nil { + t.Fatal("expected error for repo with no commits, got nil") + } + if !strings.Contains(err.Error(), "no commits in repository") { + t.Errorf("error %q should mention 'no commits in repository'", err.Error()) + } + if !strings.Contains(err.Error(), "git commit") { + t.Errorf("error %q should suggest `git commit`", err.Error()) + } +} + +func TestCheckRepoHasHEAD_WithCommitReturnsNil(t *testing.T) { + dir := t.TempDir() + runGit(t, dir, "init", "-q") + if err := os.WriteFile(filepath.Join(dir, "f"), []byte("hi"), 0o644); err != nil { + t.Fatal(err) + } + runGit(t, dir, "add", "f") + runGit(t, dir, "commit", "-qm", "init") + + if err := checkRepoHasHEAD(dir); err != nil { + t.Errorf("expected nil for repo with a commit, got %v", err) + } +} + +func runGit(t *testing.T, dir string, args ...string) { + t.Helper() + cmd := exec.Command("git", args...) + cmd.Dir = dir + cmd.Env = append(os.Environ(), + "GIT_AUTHOR_NAME=t", + "GIT_AUTHOR_EMAIL=t@t", + "GIT_COMMITTER_NAME=t", + "GIT_COMMITTER_EMAIL=t@t", + ) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git %v: %v\n%s", args, err, out) + } +}