Skip to content
go-libfossil Artifact [afb9c25623]
Login

Artifact [afb9c25623]

Artifact afb9c25623d2f1c51c186fce34a389ef3e02240baf2f55cd6f3512eb45126b76:


package libfossil_test

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

	"github.com/danmestas/go-libfossil"
)

// updateFixture creates a repo, commits two revs via Repo.Commit, then
// creates a checkout (initialized to tip = rid2). Returns the second RID
// so callers can pass it as a deliberate Update target.
func updateFixture(t *testing.T) (*libfossil.Repo, *libfossil.Checkout, int64) {
	t.Helper()
	dir := t.TempDir()
	repoPath := filepath.Join(dir, "u.fossil")
	repo, err := libfossil.Create(repoPath, libfossil.CreateOpts{})
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	// First commit (no parent — genesis).
	rid1, _, err := repo.Commit(libfossil.CommitOpts{
		Comment: "first", User: "test",
		Files: []libfossil.FileToCommit{{Name: "a.txt", Content: []byte("v1")}},
	})
	if err != nil {
		t.Fatalf("first Commit: %v", err)
	}
	// Second commit (parent = rid1). This becomes the tip.
	rid2, _, err := repo.Commit(libfossil.CommitOpts{
		Comment: "second", User: "test", ParentID: rid1,
		Files: []libfossil.FileToCommit{{Name: "a.txt", Content: []byte("v2")}},
	})
	if err != nil {
		t.Fatalf("second Commit: %v", err)
	}

	// CreateCheckout (NOT OpenCheckout — the dir is fresh) initializes the
	// working tree to the tip checkin (rid2).
	checkoutDir := filepath.Join(dir, "wt")
	checkout, err := repo.CreateCheckout(checkoutDir, libfossil.CheckoutCreateOpts{})
	if err != nil {
		t.Fatalf("CreateCheckout: %v", err)
	}
	t.Cleanup(func() { _ = checkout.Close() })
	return repo, checkout, rid2
}

func TestCheckoutUpdate_TargetRID(t *testing.T) {
	_, checkout, rid := updateFixture(t)
	result, err := checkout.Update(libfossil.UpdateOpts{TargetRID: rid})
	if err != nil {
		t.Fatalf("Update: %v", err)
	}
	if len(result.Conflicted) != 0 {
		t.Fatalf("expected clean update, got Conflicted=%v", result.Conflicted)
	}
}

func TestCheckoutUpdate_ZeroTargetRIDIsTipUpdate(t *testing.T) {
	// TargetRID=0 means the newest open descendant leaf on the checkout's branch.
	_, checkout, _ := updateFixture(t)
	result, err := checkout.Update(libfossil.UpdateOpts{TargetRID: 0})
	if err != nil {
		t.Fatalf("Update(0): %v", err)
	}
	if len(result.Conflicted) != 0 {
		t.Fatalf("expected clean update, got Conflicted=%v", result.Conflicted)
	}
}

func TestCheckoutUpdate_NonexistentRIDErrors(t *testing.T) {
	_, checkout, _ := updateFixture(t)
	result, err := checkout.Update(libfossil.UpdateOpts{TargetRID: 999999})
	if err == nil {
		t.Fatal("expected error for missing RID, got nil")
	}
	// A genuine failure must be distinguishable from a conflicted success:
	// no marker paths are reported for a failed update.
	if len(result.Conflicted) != 0 {
		t.Fatalf("expected empty Conflicted on failure, got %v", result.Conflicted)
	}
}

// TestCheckoutUpdate_CleanReportsWrittenPaths verifies the clean-success
// outcome: err == nil, Conflicted empty, and the modified file's path shows
// up in FilesWritten (not just a count). updateFixture's checkout starts at
// tip (rid2), so this pins the checkout back to rid1 first via Extract to
// force Update(TargetRID: rid2) to do real work.
func TestCheckoutUpdate_CleanReportsWrittenPaths(t *testing.T) {
	dir := t.TempDir()
	repoPath := filepath.Join(dir, "clean.fossil")
	repo, err := libfossil.Create(repoPath, libfossil.CreateOpts{})
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	rid1, _, err := repo.Commit(libfossil.CommitOpts{
		Comment: "first", User: "test",
		Files: []libfossil.FileToCommit{{Name: "a.txt", Content: []byte("v1")}},
	})
	if err != nil {
		t.Fatalf("first Commit: %v", err)
	}
	rid2, _, err := repo.Commit(libfossil.CommitOpts{
		Comment: "second", User: "test", ParentID: rid1,
		Files: []libfossil.FileToCommit{{Name: "a.txt", Content: []byte("v2")}},
	})
	if err != nil {
		t.Fatalf("second Commit: %v", err)
	}

	checkoutDir := filepath.Join(dir, "wt")
	checkout, err := repo.CreateCheckout(checkoutDir, libfossil.CheckoutCreateOpts{})
	if err != nil {
		t.Fatalf("CreateCheckout: %v", err)
	}
	t.Cleanup(func() { _ = checkout.Close() })

	// Pin the checkout to rid1 so Update(TargetRID: rid2) has real work to do.
	if err := checkout.Extract(rid1, libfossil.ExtractOpts{Force: true}); err != nil {
		t.Fatalf("Extract rid1: %v", err)
	}

	result, err := checkout.Update(libfossil.UpdateOpts{TargetRID: rid2})
	if err != nil {
		t.Fatalf("Update: %v", err)
	}
	if len(result.Conflicted) != 0 {
		t.Fatalf("expected clean update, got Conflicted=%v", result.Conflicted)
	}
	found := false
	for _, p := range result.FilesWritten {
		if p == "a.txt" {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected FilesWritten to contain a.txt, got %v", result.FilesWritten)
	}
}

// TestCheckoutUpdate_ConflictedSurfacesPaths builds a genuine fork at the
// public API level — two commits from the same parent, both editing the
// same line — and updates across it. The merge cannot resolve the
// overlapping edit, so it writes conflict markers into the working file.
// This is a SUCCESSFUL update (err == nil): the caller must be able to see
// which paths now contain markers via Conflicted.
func TestCheckoutUpdate_ConflictedSurfacesPaths(t *testing.T) {
	dir := t.TempDir()
	repoPath := filepath.Join(dir, "c.fossil")
	repo, err := libfossil.Create(repoPath, libfossil.CreateOpts{})
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	ridBase, _, err := repo.Commit(libfossil.CommitOpts{
		Comment: "base", User: "test",
		Files: []libfossil.FileToCommit{{Name: "conflict.txt", Content: []byte("original\n")}},
	})
	if err != nil {
		t.Fatalf("base Commit: %v", err)
	}
	ridA, _, err := repo.Commit(libfossil.CommitOpts{
		Comment: "branch A", User: "test", ParentID: ridBase,
		Files: []libfossil.FileToCommit{{Name: "conflict.txt", Content: []byte("local version\n")}},
	})
	if err != nil {
		t.Fatalf("branch A Commit: %v", err)
	}
	ridB, _, err := repo.Commit(libfossil.CommitOpts{
		Comment: "branch B", User: "test", ParentID: ridBase,
		Files: []libfossil.FileToCommit{{Name: "conflict.txt", Content: []byte("remote version\n")}},
	})
	if err != nil {
		t.Fatalf("branch B Commit: %v", err)
	}

	// CreateCheckout initializes to tip, which (last write wins in the
	// leaf table) is branch A here since it was committed first among the
	// two leaves and branch B follows — extract explicitly to branch A to
	// pin the starting point regardless of tip resolution.
	checkoutDir := filepath.Join(dir, "wt")
	checkout, err := repo.CreateCheckout(checkoutDir, libfossil.CheckoutCreateOpts{})
	if err != nil {
		t.Fatalf("CreateCheckout: %v", err)
	}
	t.Cleanup(func() { _ = checkout.Close() })
	if err := checkout.Extract(ridA, libfossil.ExtractOpts{Force: true}); err != nil {
		t.Fatalf("Extract ridA: %v", err)
	}

	result, err := checkout.Update(libfossil.UpdateOpts{TargetRID: ridB})
	if err != nil {
		t.Fatalf("conflicted Update must still succeed (err==nil): %v", err)
	}
	if len(result.Conflicted) != 1 || result.Conflicted[0] != "conflict.txt" {
		t.Fatalf("expected Conflicted=[conflict.txt], got %v", result.Conflicted)
	}

	data, err := os.ReadFile(filepath.Join(checkoutDir, "conflict.txt"))
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(data), "<<<<<<<") {
		t.Fatalf("expected conflict markers written to disk, got %q", data)
	}
}