From eb3c2df6302170a5c25de160e3ab37e37d0ca5d2 Mon Sep 17 00:00:00 2001 From: Rick Guo Date: Fri, 21 Aug 2026 10:57:55 +0800 Subject: [PATCH 1/3] fix(modules): fall back to minimum formula version without tags --- internal/modules/load.go | 13 +++++++++-- internal/modules/load_coverage_test.go | 23 ++++++++++++++++++++ internal/modules/source.go | 30 ++++++++++++++++++++++++++ internal/modules/source_test.go | 17 +++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/internal/modules/load.go b/internal/modules/load.go index 185ba01f..8bcdfa53 100644 --- a/internal/modules/load.go +++ b/internal/modules/load.go @@ -2,6 +2,7 @@ package modules import ( "context" + "errors" "fmt" "io/fs" "os" @@ -19,6 +20,8 @@ import ( classfile "github.com/goplus/llar/formula" ) +var errNoTags = errors.New("no tags found") + func validateModulePath(modPath string) error { if modPath == "" { return fmt.Errorf("invalid module path %q: path is empty", modPath) @@ -69,7 +72,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 "", fmt.Errorf("failed to retrieve the latest version: %w", errNoTags) } max := slices.MaxFunc(tags, func(a, b string) int { return comparator(module.Version{modPath, a}, module.Version{modPath, b}) @@ -184,7 +187,13 @@ func Load(ctx context.Context, main module.Version, opts Options) ([]*Module, er } latest, err := latestVersion(ctx, main.Path, latestRepo, cmp) if err != nil { - return nil, err + if !errors.Is(err, errNoTags) { + return nil, err + } + latest, err = mainMod.findMinFromVer(cmp) + if err != nil { + return nil, err + } } main.Version = latest } diff --git a/internal/modules/load_coverage_test.go b/internal/modules/load_coverage_test.go index ac780072..0908d929 100644 --- a/internal/modules/load_coverage_test.go +++ b/internal/modules/load_coverage_test.go @@ -102,6 +102,9 @@ func TestLatestVersion_NoTags(t *testing.T) { if !strings.Contains(err.Error(), "no tags found") { t.Fatalf("error = %v, want contains %q", err, "no tags found") } + if !errors.Is(err, errNoTags) { + t.Fatalf("error = %v, want errNoTags", err) + } } func TestLatestVersion_TagsError(t *testing.T) { @@ -265,6 +268,26 @@ func TestLoad_EmptyVersion_LatestVersionTagsError(t *testing.T) { } } +func TestLoad_EmptyVersion_NoTagsFallsBackToMinimumFormulaVersion(t *testing.T) { + fakeGitDir := t.TempDir() + if err := os.WriteFile(filepath.Join(fakeGitDir, "git"), []byte("#!/bin/sh\nexit 0\n"), 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 != "1.0.0" { + t.Fatalf("main version = %q, want %q", modules[0].Version, "1.0.0") + } +} + 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/source.go b/internal/modules/source.go index 461bde20..432cafef 100644 --- a/internal/modules/source.go +++ b/internal/modules/source.go @@ -130,6 +130,36 @@ func (m *formulaModule) findMaxFromVer(mod module.Version, compare func(v1, v2 m return maxFromVer, formulaPath, nil } +// findMinFromVer finds the lowest fromVer among the module's formula files. +func (m *formulaModule) findMinFromVer(compare func(v1, v2 module.Version) int) (minFromVer string, err error) { + err = fs.WalkDir(m.fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !strings.HasSuffix(path, defaultFormulaSuffix) { + return nil + } + + fromVer, err := fromVerOf(m.fsys.(fs.ReadFileFS), path) + if err != nil { + return err + } + fromVerMod := module.Version{Path: m.modPath, Version: fromVer} + if minFromVer == "" || compare(fromVerMod, module.Version{Path: m.modPath, Version: minFromVer}) < 0 { + minFromVer = fromVer + } + return nil + }) + + if err != nil { + return "", err + } + if minFromVer == "" { + return "", fmt.Errorf("no formula found for %s", m.modPath) + } + return minFromVer, nil +} + // fromVerOf extracts the fromVer value from a formula file by parsing its AST. func fromVerOf(fsys fs.ReadFileFS, formulaPath string) (string, error) { content, err := fsys.ReadFile(formulaPath) diff --git a/internal/modules/source_test.go b/internal/modules/source_test.go index ca1da05d..2cea18e2 100644 --- a/internal/modules/source_test.go +++ b/internal/modules/source_test.go @@ -6,6 +6,7 @@ import ( "go/token" "io/fs" "os" + "strings" "sync" "testing" @@ -244,6 +245,22 @@ func TestFormulaModule_FindMaxFromVer(t *testing.T) { } } +func TestFormulaModule_FindMinFromVer(t *testing.T) { + fsys := os.DirFS("testdata/DaveGamble/cJSON") + mod := newFormulaModule(fsys, "DaveGamble/cJSON") + + cmp := func(v1, v2 module.Version) int { + return strings.Compare(v2.Version, v1.Version) + } + fromVer, err := mod.findMinFromVer(cmp) + if err != nil { + t.Fatalf("findMinFromVer() failed: %v", err) + } + if fromVer != "v2.0.0" { + t.Errorf("fromVer = %q, want %q", fromVer, "v2.0.0") + } +} + func TestFromVerOf(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON").(fs.ReadFileFS) From 84c7b2a511d2b1ac0f21de0c717bb2fb58631801 Mon Sep 17 00:00:00 2001 From: Rick Guo Date: Fri, 21 Aug 2026 11:52:36 +0800 Subject: [PATCH 2/3] fix(modules): use HEAD ref for tagless repositories --- internal/modules/load.go | 13 ++----- internal/modules/load_coverage_test.go | 49 +++++++++++++++++--------- internal/modules/source.go | 30 ---------------- internal/modules/source_test.go | 17 --------- 4 files changed, 34 insertions(+), 75 deletions(-) diff --git a/internal/modules/load.go b/internal/modules/load.go index 8bcdfa53..4278abab 100644 --- a/internal/modules/load.go +++ b/internal/modules/load.go @@ -2,7 +2,6 @@ package modules import ( "context" - "errors" "fmt" "io/fs" "os" @@ -20,8 +19,6 @@ import ( classfile "github.com/goplus/llar/formula" ) -var errNoTags = errors.New("no tags found") - func validateModulePath(modPath string) error { if modPath == "" { return fmt.Errorf("invalid module path %q: path is empty", modPath) @@ -72,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: %w", errNoTags) + return repo.Latest(ctx) } max := slices.MaxFunc(tags, func(a, b string) int { return comparator(module.Version{modPath, a}, module.Version{modPath, b}) @@ -187,13 +184,7 @@ func Load(ctx context.Context, main module.Version, opts Options) ([]*Module, er } latest, err := latestVersion(ctx, main.Path, latestRepo, cmp) if err != nil { - if !errors.Is(err, errNoTags) { - return nil, err - } - latest, err = mainMod.findMinFromVer(cmp) - if err != nil { - return nil, err - } + return nil, err } main.Version = latest } diff --git a/internal/modules/load_coverage_test.go b/internal/modules/load_coverage_test.go index 0908d929..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,20 +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) - if err == nil { - t.Fatal("expected error for no tags") + got, err := latestVersion(context.Background(), "towner/leafmod", repo, cmp) + if err != nil { + t.Fatalf("latestVersion failed: %v", err) } - if !strings.Contains(err.Error(), "no tags found") { - t.Fatalf("error = %v, want contains %q", err, "no tags found") + 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 latest error") } - if !errors.Is(err, errNoTags) { - t.Fatalf("error = %v, want errNoTags", err) + if !strings.Contains(err.Error(), "forced latest error") { + t.Fatalf("error = %v, want contains %q", err, "forced latest error") } } @@ -268,9 +282,10 @@ func TestLoad_EmptyVersion_LatestVersionTagsError(t *testing.T) { } } -func TestLoad_EmptyVersion_NoTagsFallsBackToMinimumFormulaVersion(t *testing.T) { +func TestLoad_EmptyVersion_NoTagsUsesHeadRef(t *testing.T) { fakeGitDir := t.TempDir() - if err := os.WriteFile(filepath.Join(fakeGitDir, "git"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + 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")) @@ -283,8 +298,8 @@ func TestLoad_EmptyVersion_NoTagsFallsBackToMinimumFormulaVersion(t *testing.T) if len(modules) != 1 { t.Fatalf("loaded modules = %d, want 1", len(modules)) } - if modules[0].Version != "1.0.0" { - t.Fatalf("main version = %q, want %q", modules[0].Version, "1.0.0") + if modules[0].Version != "deadbeef" { + t.Fatalf("main version = %q, want %q", modules[0].Version, "deadbeef") } } diff --git a/internal/modules/source.go b/internal/modules/source.go index 432cafef..461bde20 100644 --- a/internal/modules/source.go +++ b/internal/modules/source.go @@ -130,36 +130,6 @@ func (m *formulaModule) findMaxFromVer(mod module.Version, compare func(v1, v2 m return maxFromVer, formulaPath, nil } -// findMinFromVer finds the lowest fromVer among the module's formula files. -func (m *formulaModule) findMinFromVer(compare func(v1, v2 module.Version) int) (minFromVer string, err error) { - err = fs.WalkDir(m.fsys, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if !strings.HasSuffix(path, defaultFormulaSuffix) { - return nil - } - - fromVer, err := fromVerOf(m.fsys.(fs.ReadFileFS), path) - if err != nil { - return err - } - fromVerMod := module.Version{Path: m.modPath, Version: fromVer} - if minFromVer == "" || compare(fromVerMod, module.Version{Path: m.modPath, Version: minFromVer}) < 0 { - minFromVer = fromVer - } - return nil - }) - - if err != nil { - return "", err - } - if minFromVer == "" { - return "", fmt.Errorf("no formula found for %s", m.modPath) - } - return minFromVer, nil -} - // fromVerOf extracts the fromVer value from a formula file by parsing its AST. func fromVerOf(fsys fs.ReadFileFS, formulaPath string) (string, error) { content, err := fsys.ReadFile(formulaPath) diff --git a/internal/modules/source_test.go b/internal/modules/source_test.go index 2cea18e2..ca1da05d 100644 --- a/internal/modules/source_test.go +++ b/internal/modules/source_test.go @@ -6,7 +6,6 @@ import ( "go/token" "io/fs" "os" - "strings" "sync" "testing" @@ -245,22 +244,6 @@ func TestFormulaModule_FindMaxFromVer(t *testing.T) { } } -func TestFormulaModule_FindMinFromVer(t *testing.T) { - fsys := os.DirFS("testdata/DaveGamble/cJSON") - mod := newFormulaModule(fsys, "DaveGamble/cJSON") - - cmp := func(v1, v2 module.Version) int { - return strings.Compare(v2.Version, v1.Version) - } - fromVer, err := mod.findMinFromVer(cmp) - if err != nil { - t.Fatalf("findMinFromVer() failed: %v", err) - } - if fromVer != "v2.0.0" { - t.Errorf("fromVer = %q, want %q", fromVer, "v2.0.0") - } -} - func TestFromVerOf(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON").(fs.ReadFileFS) From 0f7f476c804100e222854eaf741c87a93012a4af Mon Sep 17 00:00:00 2001 From: Rick Guo Date: Fri, 21 Aug 2026 11:59:16 +0800 Subject: [PATCH 3/3] test(modules): cover tagless repository HEAD resolution --- internal/modules/load_test.go | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) 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) + } +}