diff --git a/internal/modules/load.go b/internal/modules/load.go index 185ba01f..4278abab 100644 --- a/internal/modules/load.go +++ b/internal/modules/load.go @@ -69,7 +69,7 @@ func latestVersion(ctx context.Context, modPath string, repo vcs.Repo, comparato return "", err } if len(tags) == 0 { - return "", fmt.Errorf("failed to retrieve the latest version: no tags found") + return repo.Latest(ctx) } max := slices.MaxFunc(tags, func(a, b string) int { return comparator(module.Version{modPath, a}, module.Version{modPath, b}) diff --git a/internal/modules/load_coverage_test.go b/internal/modules/load_coverage_test.go index ac780072..217bccca 100644 --- a/internal/modules/load_coverage_test.go +++ b/internal/modules/load_coverage_test.go @@ -51,14 +51,16 @@ func (f fakeFile) Read(_ []byte) (int, error) { return 0, io.EOF } func (f fakeFile) Close() error { return nil } type mockLatestRepo struct { - tags []string - tagsErr error + tags []string + tagsErr error + latest string + latestErr error } var _ vcs.Repo = (*mockLatestRepo)(nil) func (m *mockLatestRepo) Tags(context.Context) ([]string, error) { return m.tags, m.tagsErr } -func (m *mockLatestRepo) Latest(context.Context) (string, error) { return "", nil } +func (m *mockLatestRepo) Latest(context.Context) (string, error) { return m.latest, m.latestErr } func (m *mockLatestRepo) At(ref, localDir string) fs.FS { return os.DirFS(localDir) } func (m *mockLatestRepo) Sync(ctx context.Context, ref, path, localDir string) error { return nil @@ -90,17 +92,32 @@ func TestLatestVersion_SelectsMaxByComparator(t *testing.T) { } } -func TestLatestVersion_NoTags(t *testing.T) { - repo := &mockLatestRepo{tags: []string{}} +func TestLatestVersion_NoTagsUsesLatestRef(t *testing.T) { + repo := &mockLatestRepo{latest: "deadbeef"} - cmp := func(v1, v2 module.Version) int { return strings.Compare(v1.Version, v2.Version) } + cmp := func(v1, v2 module.Version) int { + t.Fatalf("comparator called for no-tag repository: %v vs %v", v1, v2) + return 0 + } - _, err := latestVersion(context.Background(), "towner/leafmod", repo, cmp) + got, err := latestVersion(context.Background(), "towner/leafmod", repo, cmp) + if err != nil { + t.Fatalf("latestVersion failed: %v", err) + } + if got != "deadbeef" { + t.Fatalf("latestVersion = %q, want %q", got, "deadbeef") + } +} + +func TestLatestVersion_LatestError(t *testing.T) { + repo := &mockLatestRepo{latestErr: errors.New("forced latest error")} + + _, err := latestVersion(context.Background(), "towner/leafmod", repo, func(module.Version, module.Version) int { return 0 }) if err == nil { - t.Fatal("expected error for no tags") + t.Fatal("expected latest error") } - if !strings.Contains(err.Error(), "no tags found") { - t.Fatalf("error = %v, want contains %q", err, "no tags found") + if !strings.Contains(err.Error(), "forced latest error") { + t.Fatalf("error = %v, want contains %q", err, "forced latest error") } } @@ -265,6 +282,27 @@ func TestLoad_EmptyVersion_LatestVersionTagsError(t *testing.T) { } } +func TestLoad_EmptyVersion_NoTagsUsesHeadRef(t *testing.T) { + fakeGitDir := t.TempDir() + fakeGit := "#!/bin/sh\nif [ \"$3\" = \"HEAD\" ]; then\n printf 'deadbeef\\tHEAD\\n'\nfi\n" + if err := os.WriteFile(filepath.Join(fakeGitDir, "git"), []byte(fakeGit), 0o755); err != nil { + t.Fatalf("write fake git: %v", err) + } + t.Setenv("PATH", fakeGitDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + store := setupTestStore(t, "testdata/load") + modules, err := Load(context.Background(), module.Version{Path: "towner/standalone"}, Options{FormulaStore: store}) + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if len(modules) != 1 { + t.Fatalf("loaded modules = %d, want 1", len(modules)) + } + if modules[0].Version != "deadbeef" { + t.Fatalf("main version = %q, want %q", modules[0].Version, "deadbeef") + } +} + func TestResolveDeps_OnRequireMkdirTempError(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "tmp-file") if err := os.WriteFile(tmpFile, []byte("not-a-dir"), 0644); err != nil { diff --git a/internal/modules/load_test.go b/internal/modules/load_test.go index 49a77272..97b552b0 100644 --- a/internal/modules/load_test.go +++ b/internal/modules/load_test.go @@ -949,3 +949,44 @@ func TestIntegration_LoadWithEmptyVersion(t *testing.T) { } t.Logf("resolved version: %s", modules[0].Version) } + +func TestIntegration_LoadTaglessRepositoryAtHead(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + const formulaRef = "57f92f7d1ac9673cd4c2e81d6d9777bd9340f7c9" + ctx := context.Background() + + sourceRepo, err := vcs.NewRepo("github.com/rxi/log.c") + if err != nil { + t.Fatalf("create source repo failed: %v", err) + } + tags, err := sourceRepo.Tags(ctx) + if err != nil { + t.Fatalf("list source tags failed: %v", err) + } + if len(tags) != 0 { + t.Fatalf("source repository has tags %v, want no tags", tags) + } + head, err := sourceRepo.Latest(ctx) + if err != nil { + t.Fatalf("resolve source HEAD failed: %v", err) + } + + formulaRepo, err := vcs.NewRepo("github.com/MeteorsLiu/llarhub") + if err != nil { + t.Fatalf("create formula repo failed: %v", err) + } + store := repo.New(t.TempDir(), &pinnedRepo{Repo: formulaRepo, ref: formulaRef}) + modules, err := Load(ctx, module.Version{Path: "rxi/log.c"}, Options{FormulaStore: store}) + if err != nil { + t.Fatalf("Load tagless repository failed: %v", err) + } + if len(modules) != 1 { + t.Fatalf("loaded modules = %d, want 1", len(modules)) + } + if modules[0].Version != head { + t.Fatalf("resolved version = %q, want HEAD %q", modules[0].Version, head) + } +}