Skip to content
go-libfossil Artifact [146f94f25b]
Login

Artifact [146f94f25b]

Artifact 146f94f25bbd8f11d278bc506ec3ce905fa8e8c9823d58706ecf3d0e104e5fb8:


//go:build unix

package stash_test

import (
	"os"
	"path/filepath"
	"testing"
	"time"

	libdb "github.com/danmestas/go-libfossil/db"
	"github.com/danmestas/go-libfossil/internal/checkout"
	"github.com/danmestas/go-libfossil/internal/manifest"
	"github.com/danmestas/go-libfossil/internal/repo"
	"github.com/danmestas/go-libfossil/internal/stash"
	"github.com/danmestas/go-libfossil/simio"
)

// newLinkCheckout commits plain.txt, other.txt and link, a symlink to
// plain.txt, with allow-symlinks on, and checks them out on disk.
func newLinkCheckout(t *testing.T) *simCheckout {
	t.Helper()
	r, err := repo.CreateWithEnv(filepath.Join(t.TempDir(), "r.fossil"), "test", simio.RealEnv(), "")
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { r.Close() })
	if err := r.SetConfig("allow-symlinks", "on"); err != nil {
		t.Fatal(err)
	}
	rid, _, err := manifest.Checkin(r, manifest.CheckinOpts{
		Files: []manifest.File{
			{Name: "plain.txt", Content: []byte("plain\n")},
			{Name: "other.txt", Content: []byte("other\n")},
			{Name: "link", Content: []byte("plain.txt"), Perm: "l"},
		},
		Comment: "initial", User: "test", Time: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
	})
	if err != nil {
		t.Fatal(err)
	}
	env := simio.RealEnv()
	dir := t.TempDir()
	co, err := checkout.Create(r, dir, checkout.CreateOpts{Env: env})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { co.Close() })
	if err := co.Extract(rid, checkout.ExtractOpts{Force: true}); err != nil {
		t.Fatal(err)
	}
	ckout, err := libdb.OpenSQL(filepath.Join(dir, checkout.PreferredDBName()), libdb.OpenConfig{}, nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ckout.Close() })
	return &simCheckout{co: co, ckout: ckout, repoDB: r.DB().SqlDB(), env: env, dir: dir}
}

func wantLink(t *testing.T, dir, name, target string) {
	t.Helper()
	got, err := os.Readlink(filepath.Join(dir, name))
	if err != nil || got != target {
		t.Fatalf("%s: Readlink = %q, %v; want a symlink to %s", name, got, err, target)
	}
}

func wantFile(t *testing.T, dir, name, body string) {
	t.Helper()
	p := filepath.Join(dir, name)
	info, err := os.Lstat(p)
	if err != nil || !info.Mode().IsRegular() {
		t.Fatalf("%s: %v, %v; want a regular file", name, info, err)
	}
	data, err := os.ReadFile(p)
	if err != nil || string(data) != body {
		t.Fatalf("%s = %q, %v; want %q", name, data, err, body)
	}
}

// A repointed symlink is stashed as the link it is, as fossil's stash reads
// it with RepoFILE and records file_islink; the stash's revert makes it a
// link to its committed target again, and Pop repoints it, each time
// leaving what the link points to alone.
func TestStashRepointedSymlink(t *testing.T) {
	s := newLinkCheckout(t)
	wantLink(t, s.dir, "link", "plain.txt")
	if err := os.Remove(filepath.Join(s.dir, "link")); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink("other.txt", filepath.Join(s.dir, "link")); err != nil {
		t.Fatal(err)
	}

	if err := stash.Save(s.co, s.ckout, s.repoDB, s.env, s.dir, "repoint"); err != nil {
		t.Fatal(err)
	}
	var isLink int64
	if err := s.ckout.QueryRow("SELECT isLink FROM stashfile WHERE newname='link'").Scan(&isLink); err != nil {
		t.Fatal(err)
	}
	if isLink != 1 {
		t.Errorf("stashfile.isLink of link = %d, want 1", isLink)
	}
	wantLink(t, s.dir, "link", "plain.txt")
	wantFile(t, s.dir, "plain.txt", "plain\n")
	wantFile(t, s.dir, "other.txt", "other\n")
	if got := s.changes(t); len(got) != 0 {
		t.Fatalf("changes after stash = %v, want nothing", got)
	}

	if err := stash.Pop(s.ckout, s.repoDB, s.env, s.dir); err != nil {
		t.Fatal(err)
	}
	wantLink(t, s.dir, "link", "other.txt")
	wantFile(t, s.dir, "plain.txt", "plain\n")
	wantFile(t, s.dir, "other.txt", "other\n")
	if got := s.changes(t); len(got) != 1 || got["link"] != checkout.ChangeModified {
		t.Fatalf("changes after pop = %v, want link modified", got)
	}
}