Compare commits
No commits in common. "46e33890452c4ba606ff26b3e0a84ac9d6de85fc" and "7af42c1c835b2784ac907597e163c6fef3b90632" have entirely different histories.
46e3389045
...
7af42c1c83
@ -4,7 +4,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -110,7 +109,7 @@ func newPluginInitCmd() *cobra.Command {
|
|||||||
fmt.Fprintf(os.Stderr, "warning: could not commit plugin.mod: %v\n", err)
|
fmt.Fprintf(os.Stderr, "warning: could not commit plugin.mod: %v\n", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Not in a git repo - run `git init && git commit` before `ninja plugin publish`")
|
fmt.Println("Not in a git repo - run `git init` before `ninja plugin publish`")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@ -295,12 +294,20 @@ func newPluginPublishCmd() *cobra.Command {
|
|||||||
return fmt.Errorf("plugin.mod must have scope, name, and version")
|
return fmt.Errorf("plugin.mod must have scope, name, and version")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := checkRepoHasHEAD("."); err != nil {
|
if !allowDirty {
|
||||||
return err
|
out, _ := exec.Command("git", "status", "--porcelain").Output()
|
||||||
|
if len(strings.TrimSpace(string(out))) > 0 {
|
||||||
|
return fmt.Errorf("working tree dirty; commit or pass --allow-dirty")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := emitPublishWarnings(".", allowDirty, os.Stderr); err != nil {
|
out, _ := exec.Command("git", "ls-files", "--cached", "--ignored", "--exclude-standard").Output()
|
||||||
return err
|
if names := strings.TrimSpace(string(out)); names != "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "warning: these tracked files match .gitignore and will still be shipped:")
|
||||||
|
for _, n := range strings.Split(names, "\n") {
|
||||||
|
fmt.Fprintln(os.Stderr, " "+n)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, " (run `git rm --cached <file>` to drop)")
|
||||||
}
|
}
|
||||||
|
|
||||||
archiveBytes, err := archive.BuildSourceArchive(".")
|
archiveBytes, err := archive.BuildSourceArchive(".")
|
||||||
@ -541,41 +548,6 @@ func runCmd(name string, args ...string) error {
|
|||||||
return c.Run()
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.<name>.path <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 {
|
func upsertPluginMod(scope, name, kind string, categories []string) error {
|
||||||
const file = "plugin.mod"
|
const file = "plugin.mod"
|
||||||
existing, _ := os.ReadFile(file)
|
existing, _ := os.ReadFile(file)
|
||||||
@ -621,90 +593,6 @@ func writeMod(path string, m *core.ModFile) error {
|
|||||||
return os.WriteFile(path, []byte(b.String()), 0o644)
|
return os.WriteFile(path, []byte(b.String()), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// gitignoredTrackedWarning writes a warning to w if any tracked files
|
|
||||||
// in repoDir match the active .gitignore — those files still ship in the
|
|
||||||
// archive, which is almost always not what the author wants.
|
|
||||||
func gitignoredTrackedWarning(repoDir string, w io.Writer) {
|
|
||||||
cmd := exec.Command("git", "ls-files", "--cached", "--ignored", "--exclude-standard")
|
|
||||||
cmd.Dir = repoDir
|
|
||||||
out, _ := cmd.Output()
|
|
||||||
names := strings.TrimSpace(string(out))
|
|
||||||
if names == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintln(w, "warning: these tracked files match .gitignore and will still be shipped:")
|
|
||||||
for _, n := range strings.Split(names, "\n") {
|
|
||||||
fmt.Fprintln(w, " "+n)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(w, " (run `git rm --cached <file>` to drop)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// untrackedFilesWarning writes a warning to w listing untracked files in
|
|
||||||
// repoDir. Used on the --allow-dirty publish path because `git stash create`
|
|
||||||
// only captures tracked content, so untracked files silently vanish from the
|
|
||||||
// archive.
|
|
||||||
func untrackedFilesWarning(repoDir string, w io.Writer) {
|
|
||||||
cmd := exec.Command("git", "ls-files", "--others", "--exclude-standard")
|
|
||||||
cmd.Dir = repoDir
|
|
||||||
out, _ := cmd.Output()
|
|
||||||
names := strings.TrimSpace(string(out))
|
|
||||||
if names == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintln(w, "warning: --allow-dirty: these untracked files will NOT be in the archive:")
|
|
||||||
for _, n := range strings.Split(names, "\n") {
|
|
||||||
fmt.Fprintln(w, " "+n)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(w, " (run `git add <file>` if they should be shipped)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// emitPublishWarnings runs the publish-time warning helpers in the order the
|
|
||||||
// CLI expects:
|
|
||||||
//
|
|
||||||
// 1. gitignoredTrackedWarning is unconditional so the developer sees it even
|
|
||||||
// when the publish is about to abort.
|
|
||||||
// 2. If allowDirty is false the working tree must be clean — a dirty tree
|
|
||||||
// returns an error and the remaining warnings (which are only useful on
|
|
||||||
// the proceeding-publish path) are skipped.
|
|
||||||
// 3. If allowDirty is true the untracked-files warning fires so the user
|
|
||||||
// knows those files won't be in the archive.
|
|
||||||
// 4. submoduleWarning is unconditional on the proceeding path because
|
|
||||||
// `git archive` does not recurse into submodules.
|
|
||||||
func emitPublishWarnings(repoDir string, allowDirty bool, w io.Writer) error {
|
|
||||||
gitignoredTrackedWarning(repoDir, w)
|
|
||||||
|
|
||||||
if !allowDirty {
|
|
||||||
cmd := exec.Command("git", "status", "--porcelain")
|
|
||||||
cmd.Dir = repoDir
|
|
||||||
out, _ := cmd.Output()
|
|
||||||
if len(strings.TrimSpace(string(out))) > 0 {
|
|
||||||
return fmt.Errorf("working tree dirty; commit or pass --allow-dirty")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// `git stash create` only captures tracked content, so untracked
|
|
||||||
// files would be silently dropped from the archive. Warn loudly.
|
|
||||||
untrackedFilesWarning(repoDir, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
submoduleWarning(repoDir, w)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// submoduleWarning writes a warning to w if repoDir contains submodules.
|
|
||||||
// `git archive` doesn't recurse into them so they ship as empty directories;
|
|
||||||
// detection is via .gitmodules so it fires even for uninitialised submodules.
|
|
||||||
func submoduleWarning(repoDir string, w io.Writer) {
|
|
||||||
paths := submodulePaths(repoDir)
|
|
||||||
if len(paths) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintln(w, "warning: this repo has submodules; git archive will ship them as empty directories:")
|
|
||||||
for _, p := range paths {
|
|
||||||
fmt.Fprintln(w, " "+p)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(w, " (vendor the contents or pack them separately if the plugin depends on them)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// autoCommitPluginMod stages and commits plugin.mod if it differs from
|
// autoCommitPluginMod stages and commits plugin.mod if it differs from
|
||||||
// what's already at HEAD. No-op when there's nothing to commit.
|
// what's already at HEAD. No-op when there's nothing to commit.
|
||||||
func autoCommitPluginMod() error {
|
func autoCommitPluginMod() error {
|
||||||
|
|||||||
@ -1,418 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"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 TestAutoCommitPluginMod_CommitsWhenDirty(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "README.md")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "plugin.mod"),
|
|
||||||
[]byte("[plugin]\nname = \"x\"\nscope = \"@s\"\nversion = \"0.1.0\"\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Chdir(dir)
|
|
||||||
if err := autoCommitPluginMod(); err != nil {
|
|
||||||
t.Fatalf("autoCommitPluginMod: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
subject := gitLogSubject(t, dir)
|
|
||||||
if subject != "Add plugin.mod" {
|
|
||||||
t.Errorf("expected latest commit subject 'Add plugin.mod', got %q", subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAutoCommitPluginMod_NoopWhenClean(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "plugin.mod"),
|
|
||||||
[]byte("[plugin]\nname = \"x\"\nscope = \"@s\"\nversion = \"0.1.0\"\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "plugin.mod")
|
|
||||||
runGit(t, dir, "commit", "-qm", "seed")
|
|
||||||
|
|
||||||
beforeSHA := gitHeadSHA(t, dir)
|
|
||||||
|
|
||||||
t.Chdir(dir)
|
|
||||||
if err := autoCommitPluginMod(); err != nil {
|
|
||||||
t.Fatalf("autoCommitPluginMod: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
afterSHA := gitHeadSHA(t, dir)
|
|
||||||
if afterSHA != beforeSHA {
|
|
||||||
t.Errorf("expected no new commit, HEAD moved %s -> %s", beforeSHA, afterSHA)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAutoCommitPluginMod_WorksOnDetachedHEAD(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "README.md")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
initialSHA := gitHeadSHA(t, dir)
|
|
||||||
runGit(t, dir, "checkout", "-q", initialSHA)
|
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "plugin.mod"),
|
|
||||||
[]byte("[plugin]\nname = \"x\"\nscope = \"@s\"\nversion = \"0.1.0\"\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Chdir(dir)
|
|
||||||
if err := autoCommitPluginMod(); err != nil {
|
|
||||||
t.Fatalf("autoCommitPluginMod on detached HEAD: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
afterSHA := gitHeadSHA(t, dir)
|
|
||||||
if afterSHA == initialSHA {
|
|
||||||
t.Fatalf("expected new commit on detached HEAD, HEAD still at %s", afterSHA)
|
|
||||||
}
|
|
||||||
|
|
||||||
subject := gitLogSubject(t, dir)
|
|
||||||
if subject != "Add plugin.mod" {
|
|
||||||
t.Errorf("expected latest commit subject 'Add plugin.mod', got %q", subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
parentCmd := exec.Command("git", "rev-parse", "HEAD^")
|
|
||||||
parentCmd.Dir = dir
|
|
||||||
parentOut, err := parentCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("git rev-parse HEAD^: %v\n%s", err, parentOut)
|
|
||||||
}
|
|
||||||
parentSHA := strings.TrimSpace(string(parentOut))
|
|
||||||
if parentSHA != initialSHA {
|
|
||||||
t.Errorf("expected new commit parent to be %s, got %s", initialSHA, parentSHA)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAutoCommitPluginMod_ErrorsWhenGitMissing(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "README.md")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "plugin.mod"),
|
|
||||||
[]byte("[plugin]\nname = \"x\"\nscope = \"@s\"\nversion = \"0.1.0\"\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Chdir(dir)
|
|
||||||
t.Setenv("PATH", "")
|
|
||||||
|
|
||||||
err := autoCommitPluginMod()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error when git is missing from PATH, got nil")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "git") {
|
|
||||||
t.Errorf("error %q should mention 'git'", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitLogSubject(t *testing.T, dir string) string {
|
|
||||||
t.Helper()
|
|
||||||
cmd := exec.Command("git", "log", "-1", "--pretty=%s")
|
|
||||||
cmd.Dir = dir
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("git log: %v\n%s", err, out)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(out))
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitHeadSHA(t *testing.T, dir string) string {
|
|
||||||
t.Helper()
|
|
||||||
cmd := exec.Command("git", "rev-parse", "HEAD")
|
|
||||||
cmd.Dir = dir
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("git rev-parse: %v\n%s", err, out)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(out))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGitignoredTrackedWarning_FiresWhenTrackedFileMatchesGitignore(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "secret.env"), []byte("token=abc"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "secret.env")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.env\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", ".gitignore")
|
|
||||||
runGit(t, dir, "commit", "-qm", "ignore")
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
gitignoredTrackedWarning(dir, &buf)
|
|
||||||
|
|
||||||
out := buf.String()
|
|
||||||
if !strings.Contains(out, "secret.env") {
|
|
||||||
t.Errorf("warning should list secret.env, got: %q", out)
|
|
||||||
}
|
|
||||||
if !strings.Contains(out, "git rm --cached") {
|
|
||||||
t.Errorf("warning should suggest `git rm --cached`, got: %q", out)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(out, "\n") {
|
|
||||||
t.Errorf("warning should end with a newline (Fprintln), got: %q", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGitignoredTrackedWarning_NoopWhenNothingMatches(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "README.md")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
gitignoredTrackedWarning(dir, &buf)
|
|
||||||
if buf.Len() != 0 {
|
|
||||||
t.Errorf("expected empty output for clean repo, got: %q", buf.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUntrackedFilesWarning_FiresWithUntrackedFile(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "README.md")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("scratch"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
untrackedFilesWarning(dir, &buf)
|
|
||||||
|
|
||||||
out := buf.String()
|
|
||||||
if !strings.Contains(out, "notes.txt") {
|
|
||||||
t.Errorf("warning should list notes.txt, got: %q", out)
|
|
||||||
}
|
|
||||||
if !strings.Contains(out, "git add") {
|
|
||||||
t.Errorf("warning should suggest `git add`, got: %q", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUntrackedFilesWarning_NoopWithNoUntracked(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "README.md")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
untrackedFilesWarning(dir, &buf)
|
|
||||||
if buf.Len() != 0 {
|
|
||||||
t.Errorf("expected empty output, got: %q", buf.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubmoduleWarning_FiresWithGitmodules(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
gitmodules := `[submodule "vendor/foo"]
|
|
||||||
path = vendor/foo
|
|
||||||
url = https://example.com/foo.git
|
|
||||||
`
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, ".gitmodules"), []byte(gitmodules), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
submoduleWarning(dir, &buf)
|
|
||||||
|
|
||||||
out := buf.String()
|
|
||||||
if !strings.Contains(out, "vendor/foo") {
|
|
||||||
t.Errorf("warning should mention vendor/foo, got: %q", out)
|
|
||||||
}
|
|
||||||
if !strings.Contains(out, "submodules") {
|
|
||||||
t.Errorf("warning should mention submodules, got: %q", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubmoduleWarning_NoopWithoutGitmodules(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
submoduleWarning(dir, &buf)
|
|
||||||
if buf.Len() != 0 {
|
|
||||||
t.Errorf("expected empty output, got: %q", buf.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmitPublishWarnings_WarnsAboutGitignoreTracked(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "secret.env"), []byte("token=abc"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "secret.env")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.env\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", ".gitignore")
|
|
||||||
runGit(t, dir, "commit", "-qm", "ignore")
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := emitPublishWarnings(dir, false, &buf); err != nil {
|
|
||||||
t.Fatalf("emitPublishWarnings: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
out := buf.String()
|
|
||||||
if !strings.Contains(out, "secret.env") {
|
|
||||||
t.Errorf("expected gitignored-tracked warning to mention secret.env, got: %q", out)
|
|
||||||
}
|
|
||||||
if !strings.Contains(out, "match .gitignore") {
|
|
||||||
t.Errorf("expected gitignored-tracked warning fragment, got: %q", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmitPublishWarnings_WarnsAboutSubmodules(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "README.md")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
gitmodules := `[submodule "vendor/foo"]
|
|
||||||
path = vendor/foo
|
|
||||||
url = https://example.com/foo.git
|
|
||||||
`
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, ".gitmodules"), []byte(gitmodules), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", ".gitmodules")
|
|
||||||
runGit(t, dir, "commit", "-qm", "add submodule decl")
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := emitPublishWarnings(dir, false, &buf); err != nil {
|
|
||||||
t.Fatalf("emitPublishWarnings: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
out := buf.String()
|
|
||||||
if !strings.Contains(out, "vendor/foo") {
|
|
||||||
t.Errorf("expected submodule warning to mention vendor/foo, got: %q", out)
|
|
||||||
}
|
|
||||||
if !strings.Contains(out, "submodules") {
|
|
||||||
t.Errorf("expected submodule warning fragment, got: %q", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmitPublishWarnings_WarnsAboutUntrackedWithAllowDirty(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGit(t, dir, "init", "-q")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGit(t, dir, "add", "README.md")
|
|
||||||
runGit(t, dir, "commit", "-qm", "init")
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "scratch.txt"), []byte("notes"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("allowDirty=true surfaces untracked warning", func(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := emitPublishWarnings(dir, true, &buf); err != nil {
|
|
||||||
t.Fatalf("emitPublishWarnings: %v", err)
|
|
||||||
}
|
|
||||||
out := buf.String()
|
|
||||||
if !strings.Contains(out, "scratch.txt") {
|
|
||||||
t.Errorf("expected untracked-files warning to mention scratch.txt, got: %q", out)
|
|
||||||
}
|
|
||||||
if !strings.Contains(out, "--allow-dirty") {
|
|
||||||
t.Errorf("expected untracked-files warning fragment, got: %q", out)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("allowDirty=false aborts before untracked warning", func(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err := emitPublishWarnings(dir, false, &buf)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected dirty-tree error, got nil")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "working tree dirty") {
|
|
||||||
t.Errorf("expected dirty-tree error, got: %v", err)
|
|
||||||
}
|
|
||||||
if strings.Contains(buf.String(), "--allow-dirty: these untracked files") {
|
|
||||||
t.Errorf("untracked-files warning should not fire on dirty-abort path, got: %q", buf.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,7 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
@ -88,121 +87,3 @@ func keys(m map[string]string) []string {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildSourceArchive_DirtyTreeShipsWorkingCopy(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGitArchive(t, dir, "init", "-q")
|
|
||||||
modPath := filepath.Join(dir, "plugin.mod")
|
|
||||||
if err := os.WriteFile(modPath,
|
|
||||||
[]byte("[plugin]\nname=\"x\"\nscope=\"@s\"\nversion=\"0.1.0\"\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGitArchive(t, dir, "add", "plugin.mod")
|
|
||||||
runGitArchive(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
dirtyContents := []byte("[plugin]\nname=\"x\"\nscope=\"@s\"\nversion=\"0.1.1\"\n")
|
|
||||||
if err := os.WriteFile(modPath, dirtyContents, 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zstdBytes, err := BuildSourceArchive(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildSourceArchive: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readArchive(t, zstdBytes)
|
|
||||||
contents, ok := got["plugin.mod"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected plugin.mod in archive, got %v", keys(got))
|
|
||||||
}
|
|
||||||
if !strings.Contains(contents, `version="0.1.1"`) {
|
|
||||||
t.Errorf("archived plugin.mod should have dirty version 0.1.1, got: %q", contents)
|
|
||||||
}
|
|
||||||
if strings.Contains(contents, `version="0.1.0"`) {
|
|
||||||
t.Errorf("archived plugin.mod should NOT have HEAD version 0.1.0, got: %q", contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Working tree should be unchanged after stash-create.
|
|
||||||
postContents, err := os.ReadFile(modPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if string(postContents) != string(dirtyContents) {
|
|
||||||
t.Errorf("working tree mutated after BuildSourceArchive\nwant: %q\ngot: %q",
|
|
||||||
string(dirtyContents), string(postContents))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildSourceArchive_DirtyTreeOmitsUntracked(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
runGitArchive(t, dir, "init", "-q")
|
|
||||||
modPath := filepath.Join(dir, "plugin.mod")
|
|
||||||
if err := os.WriteFile(modPath,
|
|
||||||
[]byte("[plugin]\nname=\"x\"\nscope=\"@s\"\nversion=\"0.1.0\"\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
runGitArchive(t, dir, "add", "plugin.mod")
|
|
||||||
runGitArchive(t, dir, "commit", "-qm", "init")
|
|
||||||
|
|
||||||
// Dirty the tracked file.
|
|
||||||
if err := os.WriteFile(modPath,
|
|
||||||
[]byte("[plugin]\nname=\"x\"\nscope=\"@s\"\nversion=\"0.1.1\"\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// Add an untracked file (no git add).
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "extra.txt"), []byte("not tracked"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zstdBytes, err := BuildSourceArchive(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BuildSourceArchive: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readArchive(t, zstdBytes)
|
|
||||||
if _, ok := got["extra.txt"]; ok {
|
|
||||||
t.Errorf("untracked extra.txt should not be in archive, got %v", keys(got))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runGitArchive(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readArchive(t *testing.T, zstdBytes []byte) map[string]string {
|
|
||||||
t.Helper()
|
|
||||||
dec, err := zstd.NewReader(bytes.NewReader(zstdBytes))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer dec.Close()
|
|
||||||
tr := tar.NewReader(dec)
|
|
||||||
got := map[string]string{}
|
|
||||||
for {
|
|
||||||
hdr, err := tr.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
buf, err := io.ReadAll(tr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
got[hdr.Name] = string(buf)
|
|
||||||
}
|
|
||||||
return got
|
|
||||||
}
|
|
||||||
|
|||||||
@ -14,10 +14,6 @@ type ModFile struct {
|
|||||||
|
|
||||||
type ModPlugin struct {
|
type ModPlugin struct {
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
// Scope is the plugin owner namespace as it appears in plugin.mod. It may
|
|
||||||
// include the leading "@" (e.g. "@themes") or omit it (e.g. "themes") —
|
|
||||||
// both forms are accepted. Consumers comparing scopes should trim the "@"
|
|
||||||
// before comparing; use ModFile.Coords() for a normalised display string.
|
|
||||||
Scope string `toml:"scope"`
|
Scope string `toml:"scope"`
|
||||||
Version string `toml:"version"`
|
Version string `toml:"version"`
|
||||||
Kind string `toml:"kind,omitempty"`
|
Kind string `toml:"kind,omitempty"`
|
||||||
@ -41,13 +37,6 @@ func ParseModFull(b []byte) (*ModFile, error) {
|
|||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coords returns the canonical display coordinate for the plugin in the form
|
|
||||||
// "@scope/name@version" (or "name@version" when no scope is set).
|
|
||||||
//
|
|
||||||
// The leading "@" on m.Plugin.Scope is intentionally trimmed before
|
|
||||||
// re-prefixing so that authors may write either "@themes" or "themes" in
|
|
||||||
// plugin.mod and get the same output. Callers that need the raw scope as
|
|
||||||
// written should read m.Plugin.Scope directly.
|
|
||||||
func (m *ModFile) Coords() string {
|
func (m *ModFile) Coords() string {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -100,20 +100,6 @@ version = "0.1.0"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoords_AcceptsScopeWithOrWithoutAt(t *testing.T) {
|
|
||||||
want := "@themes/foo@1.0.0"
|
|
||||||
|
|
||||||
withAt := &ModFile{Plugin: ModPlugin{Name: "foo", Scope: "@themes", Version: "1.0.0"}}
|
|
||||||
if got := withAt.Coords(); got != want {
|
|
||||||
t.Errorf("Coords() with leading @ = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
withoutAt := &ModFile{Plugin: ModPlugin{Name: "foo", Scope: "themes", Version: "1.0.0"}}
|
|
||||||
if got := withoutAt.Coords(); got != want {
|
|
||||||
t.Errorf("Coords() without leading @ = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseModFull_RequiresAndCompat(t *testing.T) {
|
func TestParseModFull_RequiresAndCompat(t *testing.T) {
|
||||||
src := []byte(`
|
src := []byte(`
|
||||||
[plugin]
|
[plugin]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user