From ee76d76dc605c7958d6a6fe64a085f90d85bf94b Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Wed, 3 Jun 2026 09:11:08 +0800 Subject: [PATCH] test(cli): cover BuildSourceArchive dirty-tree stash-create path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dirty-tree branch (where git stash create captures uncommitted tracked changes) was untested. Add two cases: one asserting the archive contains the dirty working-copy contents (not HEAD) and the working tree is not mutated; another asserting untracked files are excluded — the contract the --allow-dirty publish warning relies on. --- cmd/ninja/internal/archive/archive_test.go | 119 +++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/cmd/ninja/internal/archive/archive_test.go b/cmd/ninja/internal/archive/archive_test.go index 08dbdd5..4454e78 100644 --- a/cmd/ninja/internal/archive/archive_test.go +++ b/cmd/ninja/internal/archive/archive_test.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "testing" "github.com/klauspost/compress/zstd" @@ -87,3 +88,121 @@ 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 +}