Compare commits

..

No commits in common. "46e33890452c4ba606ff26b3e0a84ac9d6de85fc" and "7af42c1c835b2784ac907597e163c6fef3b90632" have entirely different histories.

5 changed files with 14 additions and 688 deletions

View File

@ -4,7 +4,6 @@ import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"regexp"
@ -110,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 && git commit` before `ninja plugin publish`")
fmt.Println("Not in a git repo - run `git init` before `ninja plugin publish`")
}
return nil
},
@ -295,12 +294,20 @@ 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 {
return fmt.Errorf("working tree dirty; commit or pass --allow-dirty")
}
}
if err := emitPublishWarnings(".", allowDirty, os.Stderr); err != nil {
return err
out, _ := exec.Command("git", "ls-files", "--cached", "--ignored", "--exclude-standard").Output()
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(".")
@ -541,41 +548,6 @@ 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
}
// 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 {
const file = "plugin.mod"
existing, _ := os.ReadFile(file)
@ -621,90 +593,6 @@ func writeMod(path string, m *core.ModFile) error {
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
// what's already at HEAD. No-op when there's nothing to commit.
func autoCommitPluginMod() error {

View File

@ -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)
}
}

View File

@ -7,7 +7,6 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/klauspost/compress/zstd"
@ -88,121 +87,3 @@ func keys(m map[string]string) []string {
}
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
}

View File

@ -13,11 +13,7 @@ type ModFile struct {
}
type ModPlugin struct {
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.
Name string `toml:"name"`
Scope string `toml:"scope"`
Version string `toml:"version"`
Kind string `toml:"kind,omitempty"`
@ -41,13 +37,6 @@ func ParseModFull(b []byte) (*ModFile, error) {
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 {
if m == nil {
return ""

View File

@ -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) {
src := []byte(`
[plugin]