package furgit import ( "os" "path/filepath" "strings" "testing" ) func TestResolveRef(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() workDir, cleanupWork := setupWorkDir(t) defer cleanupWork() err := os.WriteFile(filepath.Join(workDir, "test.txt"), []byte("content"), 0o644) if err != nil { t.Fatalf("Failed to write test.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "test") commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commitHash) repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() hashObj, _ := repo.ParseHash(commitHash) resolved, err := repo.ResolveRef("refs/heads/main") if err != nil { t.Fatalf("ResolveRef failed: %v", err) } if resolved.Kind != RefKindDetached { t.Fatalf("expected detached ref, got %v", resolved.Kind) } if resolved.Hash != hashObj { t.Errorf("resolved hash: got %s, want %s", resolved.Hash, hashObj) } gitRevParse := gitCmd(t, repoPath, nil, "rev-parse", "refs/heads/main") if resolved.Hash.String() != gitRevParse { t.Errorf("furgit resolved %s, git resolved %s", resolved.Hash, gitRevParse) } _, err = repo.ResolveRef("refs/heads/nonexistent") if err == nil { t.Error("expected error for nonexistent ref") } } func TestResolveHEAD(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() workDir, cleanupWork := setupWorkDir(t) defer cleanupWork() err := os.WriteFile(filepath.Join(workDir, "test.txt"), []byte("content"), 0o644) if err != nil { t.Fatalf("failed to write test.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "test") commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commitHash) gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD", "refs/heads/main") repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() ref, err := repo.ResolveRef("HEAD") if err != nil { t.Fatalf("ResolveRef(HEAD) failed: %v", err) } if ref.Kind != RefKindSymbolic { t.Fatalf("HEAD kind: got %v, want %v", ref.Kind, RefKindSymbolic) } if ref.Ref != "refs/heads/main" { t.Errorf("HEAD symbolic ref: got %q, want %q", ref.Ref, "refs/heads/main") } gitSymRef := gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD") if ref.Ref != gitSymRef { t.Errorf("furgit resolved %v, git resolved %s", ref.Ref, gitSymRef) } } func TestPackedRefs(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() workDir, cleanupWork := setupWorkDir(t) defer cleanupWork() err := os.WriteFile(filepath.Join(workDir, "test.txt"), []byte("content1"), 0o644) if err != nil { t.Fatalf("failed to write test.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "commit1") commit1Hash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") err = os.WriteFile(filepath.Join(workDir, "test2.txt"), []byte("content2"), 0o644) if err != nil { t.Fatalf("failed to write test2.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "commit2") commit2Hash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") gitCmd(t, repoPath, nil, "update-ref", "refs/heads/branch1", commit1Hash) gitCmd(t, repoPath, nil, "update-ref", "refs/heads/branch2", commit2Hash) gitCmd(t, repoPath, nil, "update-ref", "refs/tags/v1.0", commit1Hash) gitCmd(t, repoPath, nil, "pack-refs", "--all") repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() hash1, _ := repo.ParseHash(commit1Hash) hash2, _ := repo.ParseHash(commit2Hash) resolved1, err := repo.ResolveRef("refs/heads/branch1") if err != nil { t.Fatalf("ResolveRef branch1 failed: %v", err) } if resolved1.Kind != RefKindDetached || resolved1.Hash != hash1 { t.Errorf("branch1: got %s, want %s", resolved1.Hash, hash1) } gitResolved1 := gitCmd(t, repoPath, nil, "rev-parse", "refs/heads/branch1") if resolved1.Hash.String() != gitResolved1 { t.Errorf("furgit resolved %s, git resolved %s", resolved1.Hash, gitResolved1) } resolved2, err := repo.ResolveRef("refs/heads/branch2") if err != nil { t.Fatalf("ResolveRef branch2 failed: %v", err) } if resolved2.Kind != RefKindDetached || resolved2.Hash != hash2 { t.Errorf("branch2: got %s, want %s", resolved2.Hash, hash2) } resolvedTag, err := repo.ResolveRef("refs/tags/v1.0") if err != nil { t.Fatalf("ResolveRef tag failed: %v", err) } if resolvedTag.Kind != RefKindDetached || resolvedTag.Hash != hash1 { t.Errorf("tag: got %s, want %s", resolvedTag.Hash, hash1) } } func TestResolveRefFully(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() workDir, cleanupWork := setupWorkDir(t) defer cleanupWork() // Create an initial commit err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("content"), 0o644) if err != nil { t.Fatalf("failed to write file.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "init") commit := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") // Create two layers of symbolic refs gitCmd(t, repoPath, nil, "symbolic-ref", "refs/heads/level1", "refs/heads/level2") gitCmd(t, repoPath, nil, "symbolic-ref", "refs/heads/level2", "refs/heads/main") gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit) repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() commitHash, err := repo.ParseHash(commit) if err != nil { t.Fatalf("ParseHash failed: %v", err) } resolved, err := repo.ResolveRefFully("refs/heads/level1") if err != nil { t.Fatalf("ResolveRefFully failed: %v", err) } if resolved.Hash != commitHash { t.Errorf("ResolveRefFully: got hash %s, want %s", resolved.Hash, commitHash) } } func TestResolveRefFullySymbolicCycle(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() gitCmd(t, repoPath, nil, "symbolic-ref", "refs/heads/A", "refs/heads/B") gitCmd(t, repoPath, nil, "symbolic-ref", "refs/heads/B", "refs/heads/A") _, err = repo.ResolveRefFully("refs/heads/A") if err == nil { t.Fatalf("ResolveRefFully should fail on a symbolic cycle") } if !strings.Contains(err.Error(), "cycle") { t.Fatalf("unexpected error for symbolic cycle: %v", err) } } func TestResolveRefHashInput(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() workDir, cleanupWork := setupWorkDir(t) defer cleanupWork() err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("content"), 0o644) if err != nil { t.Fatalf("failed to write file.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "init") commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() hashObj, err := repo.ParseHash(commitHash) if err != nil { t.Fatalf("ParseHash failed: %v", err) } ref, err := repo.ResolveRef(commitHash) if err != nil { t.Fatalf("ResolveRef(hash) failed: %v", err) } if ref.Kind != RefKindDetached { t.Fatalf("expected RefKindDetached, got %v", ref.Kind) } if ref.Hash != hashObj { t.Fatalf("hash mismatch: got %s, want %s", ref.Hash, hashObj) } hashRef, err := repo.ResolveRefFully(commitHash) if err != nil { t.Fatalf("ResolveRefFully(hash) failed: %v", err) } if hashRef.Hash != hashObj { t.Fatalf("hash mismatch: got %s, want %s", hashRef.Hash, hashObj) } _, err = repo.ResolveRef("this_is_not_a_hash") if err == nil { t.Fatalf("expected error for invalid hash input") } } func TestListRefsLooseOverridesPacked(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() workDir, cleanupWork := setupWorkDir(t) defer cleanupWork() gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD", "refs/heads/main") err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644) if err != nil { t.Fatalf("failed to write file.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "c1") commit1 := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit1) gitCmd(t, repoPath, nil, "update-ref", "refs/heads/feature", commit1) gitCmd(t, repoPath, nil, "pack-refs", "--all", "--prune") err = os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("two"), 0o644) if err != nil { t.Fatalf("failed to write file.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "c2") commit2 := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit2) repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() hash1, _ := repo.ParseHash(commit1) hash2, _ := repo.ParseHash(commit2) refs, err := repo.ListRefs("refs/heads/*") if err != nil { t.Fatalf("ListRefs failed: %v", err) } if len(refs) != 2 { t.Fatalf("expected 2 refs, got %d", len(refs)) } got := make(map[string]Ref, len(refs)) for _, r := range refs { if _, exists := got[r.Name]; exists { t.Fatalf("duplicate ref %q in results", r.Name) } got[r.Name] = r } mainRef, ok := got["refs/heads/main"] if !ok { t.Fatalf("missing refs/heads/main in results") } if mainRef.Kind != RefKindDetached || mainRef.Hash != hash2 { t.Fatalf("refs/heads/main hash: got %s (kind %v), want %s", mainRef.Hash, mainRef.Kind, hash2) } featureRef, ok := got["refs/heads/feature"] if !ok { t.Fatalf("missing refs/heads/feature in results") } if featureRef.Kind != RefKindDetached || featureRef.Hash != hash1 { t.Fatalf("refs/heads/feature hash: got %s (kind %v), want %s", featureRef.Hash, featureRef.Kind, hash1) } } func TestListRefsPatternFiltering(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() workDir, cleanupWork := setupWorkDir(t) defer cleanupWork() gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD", "refs/heads/main") err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644) if err != nil { t.Fatalf("failed to write file.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "c1") commit1 := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit1) gitCmd(t, repoPath, nil, "update-ref", "refs/heads/feature", commit1) gitCmd(t, repoPath, nil, "pack-refs", "--all", "--prune") repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() hash1, _ := repo.ParseHash(commit1) refs, err := repo.ListRefs("refs/heads/fea*") if err != nil { t.Fatalf("ListRefs failed: %v", err) } if len(refs) != 1 { t.Fatalf("expected 1 ref, got %d", len(refs)) } if refs[0].Name != "refs/heads/feature" { t.Fatalf("unexpected ref name: got %q, want %q", refs[0].Name, "refs/heads/feature") } if refs[0].Kind != RefKindDetached || refs[0].Hash != hash1 { t.Fatalf("refs/heads/feature hash: got %s (kind %v), want %s", refs[0].Hash, refs[0].Kind, hash1) } } func TestListRefsPackedPatterns(t *testing.T) { repoPath, cleanup := setupTestRepo(t) defer cleanup() workDir, cleanupWork := setupWorkDir(t) defer cleanupWork() gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD", "refs/heads/main") err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644) if err != nil { t.Fatalf("failed to write file.txt: %v", err) } gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".") gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "c1") commit := gitCmd(t, repoPath, nil, "rev-parse", "HEAD") gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit) gitCmd(t, repoPath, nil, "update-ref", "refs/heads/feature/one", commit) gitCmd(t, repoPath, nil, "update-ref", "refs/notes/review", commit) gitCmd(t, repoPath, nil, "update-ref", "refs/tags/v1", commit) gitCmd(t, repoPath, nil, "pack-refs", "--all", "--prune") repo, err := OpenRepository(repoPath) if err != nil { t.Fatalf("OpenRepository failed: %v", err) } defer func() { _ = repo.Close() }() tests := []struct { pattern string want []string }{ { pattern: "refs/heads/*", want: []string{"refs/heads/main"}, }, { pattern: "refs/heads/*/*", want: []string{"refs/heads/feature/one"}, }, { pattern: "refs/*/feature/one", want: []string{"refs/heads/feature/one"}, }, { pattern: "refs/heads/feat?re/one", want: []string{"refs/heads/feature/one"}, }, { pattern: "refs/tags/v[0-9]", want: []string{"refs/tags/v1"}, }, { pattern: "refs/*/*", want: []string{"refs/heads/main", "refs/notes/review", "refs/tags/v1"}, }, } for _, tt := range tests { t.Run(tt.pattern, func(t *testing.T) { refs, err := repo.ListRefs(tt.pattern) if err != nil { t.Fatalf("ListRefs(%q) failed: %v", tt.pattern, err) } got := make(map[string]struct{}, len(refs)) for _, r := range refs { got[r.Name] = struct{}{} } want := make(map[string]struct{}, len(tt.want)) for _, w := range tt.want { want[w] = struct{}{} } if len(got) != len(want) { t.Fatalf("ListRefs(%q) returned %d refs, want %d", tt.pattern, len(got), len(want)) } for name := range got { if _, ok := want[name]; !ok { t.Fatalf("ListRefs(%q) unexpected ref %q", tt.pattern, name) } } }) } } func TestRefShort(t *testing.T) { t.Run("unambiguous", func(t *testing.T) { ref := Ref{Name: "refs/heads/main"} short := ref.Short([]Ref{ref}, false) if short != "main" { t.Fatalf("expected short name %q, got %q", "main", short) } }) t.Run("ambiguous", func(t *testing.T) { ref := Ref{Name: "refs/heads/main"} tags := Ref{Name: "refs/tags/main"} short := ref.Short([]Ref{ref, tags}, false) if short != "heads/main" { t.Fatalf("expected ambiguous ref to shorten to %q, got %q", "heads/main", short) } }) t.Run("strict", func(t *testing.T) { ref := Ref{Name: "refs/heads/main"} remoteHead := Ref{Name: "refs/remotes/main/HEAD"} shortNonStrict := ref.Short([]Ref{ref, remoteHead}, false) if shortNonStrict != "main" { t.Fatalf("expected non-strict short name %q, got %q", "main", shortNonStrict) } shortStrict := ref.Short([]Ref{ref, remoteHead}, true) if shortStrict != "heads/main" { t.Fatalf("expected strict ambiguity to shorten to %q, got %q", "heads/main", shortStrict) } }) }