package configrepo import ( "context" "errors" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" "rickub.com/turbo-editors/turbo-core/profile" ) func TestParseReadsTheURLsTheForgesShow(t *testing.T) { cases := []struct { url string want Source }{ {"https://rickub.com/turbo-editors/configs/tree/main/golang-init", Source{Host: "rickub.com", Owner: "turbo-editors", Repo: "configs", Ref: "main", Path: "golang-init"}}, {"https://github.com/acme/configs/tree/main/go/service", Source{Host: "github.com", Owner: "acme", Repo: "configs", Ref: "main", Path: "go/service"}}, {"https://gitlab.com/acme/platform/configs/-/tree/main/go-service", Source{Host: "gitlab.com", Owner: "acme/platform", Repo: "configs", Ref: "main", Path: "go-service"}}, {"https://codeberg.org/acme/configs/src/branch/main/go-service", Source{Host: "codeberg.org", Owner: "acme", Repo: "configs", Ref: "main", Path: "go-service"}}, {"https://codeberg.org/acme/configs/src/tag/v1.0.0/go-service", Source{Host: "codeberg.org", Owner: "acme", Repo: "configs", Ref: "v1.0.0", Path: "go-service"}}, {"https://github.com/acme/configs", Source{Host: "github.com", Owner: "acme", Repo: "configs"}}, {"https://github.com/acme/configs.git", Source{Host: "github.com", Owner: "acme", Repo: "configs"}}, {"https://github.com/acme/configs/tree/main", Source{Host: "github.com", Owner: "acme", Repo: "configs", Ref: "main"}}, {"https://github.com/acme/configs/tree/main/go-service/", Source{Host: "github.com", Owner: "acme", Repo: "configs", Ref: "main", Path: "go-service"}}, {"https://github.com/acme/configs/blob/main/go-service/.turbo-go/settings.toml", Source{Host: "github.com", Owner: "acme", Repo: "configs", Ref: "main", Path: "go-service/.turbo-go/settings.toml"}}, {" https://rickub.com/turbo-editors/configs/tree/main/golang-init-with-agents\n", Source{Host: "rickub.com", Owner: "turbo-editors", Repo: "configs", Ref: "main", Path: "golang-init-with-agents"}}, } for _, c := range cases { got, err := Parse(c.url) if err != nil { t.Errorf("Parse(%q) error = %v", c.url, err) continue } if got != c.want { t.Errorf("Parse(%q) = %+v, want %+v", c.url, got, c.want) } } } func TestParseRefusesWhatIsNotARepository(t *testing.T) { for _, bad := range []string{"", "configs", "https://rickub.com", "https://rickub.com/turbo-editors", "ftp://x/y/z", "not a url at all ://"} { if _, err := Parse(bad); err == nil { t.Errorf("Parse(%q) accepted something that names no repository", bad) } } } func TestCloneURLsTryTheHostThenItsGitSubdomain(t *testing.T) { // rickub's pages are on rickub.com and its repositories on git.rickub.com. src := Source{Host: "rickub.com", Owner: "turbo-editors", Repo: "configs"} want := []string{"https://rickub.com/turbo-editors/configs.git", "https://git.rickub.com/turbo-editors/configs.git"} if got := src.CloneURLs(); !reflect.DeepEqual(got, want) { t.Errorf("CloneURLs() = %v, want %v", got, want) } } func TestResolveRefSettlesABranchWithASlashInIt(t *testing.T) { refs := []string{"main", "feature/x", "feature/x/deeper", "v1.0.0"} cases := []struct { ref, path, wantRef, wantPath string }{ {"main", "golang-init", "main", "golang-init"}, {"feature", "x/golang-init", "feature/x", "golang-init"}, {"feature", "x/deeper/golang-init", "feature/x/deeper", "golang-init"}, {"feature", "x", "feature/x", ""}, {"v1.0.0", "", "v1.0.0", ""}, {"nowhere", "golang-init", "nowhere", "golang-init"}, // left alone; the clone will say {"", "", "", ""}, // the default branch } for _, c := range cases { ref, path := resolveRef(refs, c.ref, c.path) if ref != c.wantRef || path != c.wantPath { t.Errorf("resolveRef(%q, %q) = %q, %q; want %q, %q", c.ref, c.path, ref, path, c.wantRef, c.wantPath) } } } func TestParseRefsReadsBranchesAndTagsAndDropsPeeledTags(t *testing.T) { output := "abc\tHEAD\nabc\trefs/heads/main\ndef\trefs/heads/feature/x\n123\trefs/tags/v1.0.0\n456\trefs/tags/v1.0.0^{}\n" want := []string{"main", "feature/x", "v1.0.0"} if got := parseRefs(output); !reflect.DeepEqual(got, want) { t.Errorf("parseRefs() = %v, want %v", got, want) } } // testProfile is an editor whose project directory is .turbo-test. func testProfile() profile.Profile { return profile.Profile{Name: "Turbo Test", Slug: "turbo-test", Language: "Test"} } // sampleRepo makes a repository on disk shaped like turbo-editors/configs: // golang-init/.turbo-test with three files and a nested one, on a branch // called main, and the same on a branch called feature/x with a marker file. func sampleRepo(t *testing.T) string { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } repo := t.TempDir() run := func(args ...string) { t.Helper() cmd := exec.Command("git", append([]string{"-C", repo, "-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false"}, args...)...) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } write := func(name, contents string) { t.Helper() path := filepath.Join(repo, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { t.Fatal(err) } } run("init", "--quiet", "--initial-branch=main") write("README.md", "# configs\n") write("golang-init/README.md", "# golang-init\n") write("golang-init/.turbo-test/settings.toml", "theme = \"turbo-dark\"\n") write("golang-init/.turbo-test/snippets.toml", "[[snippet]]\n") write("golang-init/.turbo-test/tools.toml", "[[tool]]\n") write("golang-init/.turbo-test/agents/bob.yaml", "name: bob\n") write("empty-init/README.md", "nothing for the editor here\n") run("add", "-A") run("commit", "--quiet", "-m", "sample") run("checkout", "--quiet", "-b", "feature/x") write("golang-init/.turbo-test/feature.txt", "from the branch\n") run("add", "-A") run("commit", "--quiet", "-m", "branch") run("checkout", "--quiet", "main") return repo } func TestLoadCopiesTheEditorsDirectoryIntoTheProject(t *testing.T) { repo := sampleRepo(t) dest := t.TempDir() result, err := loadFrom(context.Background(), testProfile(), []string{repo}, Source{Ref: "main", Path: "golang-init"}, dest) if err != nil { t.Fatalf("loadFrom() error = %v", err) } if result.Dir != filepath.Join(dest, ".turbo-test") || result.Remote != repo || result.Ref != "main" { t.Errorf("Result = %+v", result) } want := []string{"agents/bob.yaml", "settings.toml", "snippets.toml", "tools.toml"} if !reflect.DeepEqual(result.Files, want) { t.Errorf("Files = %v, want %v", result.Files, want) } data, err := os.ReadFile(filepath.Join(dest, ".turbo-test", "settings.toml")) if err != nil || string(data) != "theme = \"turbo-dark\"\n" { t.Errorf("settings.toml = %q, %v", data, err) } if _, err := os.Stat(filepath.Join(dest, "README.md")); err == nil { t.Error("the directory's README came along; only the editor's directory should") } } func TestLoadTakesAURLThatAlreadyNamesTheEditorsDirectory(t *testing.T) { repo := sampleRepo(t) dest := t.TempDir() result, err := loadFrom(context.Background(), testProfile(), []string{repo}, Source{Ref: "main", Path: "golang-init/.turbo-test"}, dest) if err != nil { t.Fatalf("loadFrom() error = %v", err) } if len(result.Files) != 4 { t.Errorf("Files = %v, want the four files", result.Files) } } func TestLoadResolvesABranchWithASlashAgainstTheRepository(t *testing.T) { // The URL .../tree/feature/x/golang-init reads as ref "feature", path // "x/golang-init"; only the repository knows the branch is feature/x. repo := sampleRepo(t) dest := t.TempDir() result, err := loadFrom(context.Background(), testProfile(), []string{repo}, Source{Ref: "feature", Path: "x/golang-init"}, dest) if err != nil { t.Fatalf("loadFrom() error = %v", err) } if result.Ref != "feature/x" { t.Errorf("Ref = %q, want feature/x", result.Ref) } if _, err := os.Stat(filepath.Join(dest, ".turbo-test", "feature.txt")); err != nil { t.Error("the file only the branch has did not come: the wrong ref was cloned") } } func TestLoadUsesTheDefaultBranchWhenTheURLNamesNone(t *testing.T) { repo := sampleRepo(t) dest := t.TempDir() result, err := loadFrom(context.Background(), testProfile(), []string{repo}, Source{Path: "golang-init"}, dest) if err != nil { t.Fatalf("loadFrom() error = %v", err) } if result.Ref != "" { t.Errorf("Ref = %q, want the default branch (empty)", result.Ref) } if _, err := os.Stat(filepath.Join(dest, ".turbo-test", "feature.txt")); err == nil { t.Error("a file from the feature branch came from the default branch") } } func TestLoadRefusesToOverwriteAConfigurationTheProjectHas(t *testing.T) { repo := sampleRepo(t) dest := t.TempDir() if err := os.MkdirAll(filepath.Join(dest, ".turbo-test"), 0o755); err != nil { t.Fatal(err) } kept := filepath.Join(dest, ".turbo-test", "settings.toml") if err := os.WriteFile(kept, []byte("mine\n"), 0o644); err != nil { t.Fatal(err) } _, err := loadFrom(context.Background(), testProfile(), []string{repo}, Source{Ref: "main", Path: "golang-init"}, dest) if !errors.Is(err, ErrExists) { t.Fatalf("error = %v, want ErrExists", err) } if data, _ := os.ReadFile(kept); string(data) != "mine\n" { t.Errorf("the project's own settings were overwritten: %q", data) } } func TestLoadSaysWhenTheDirectoryHasNoConfiguration(t *testing.T) { repo := sampleRepo(t) _, err := loadFrom(context.Background(), testProfile(), []string{repo}, Source{Ref: "main", Path: "empty-init"}, t.TempDir()) if !errors.Is(err, ErrNotFound) || !strings.Contains(err.Error(), ".turbo-test at empty-init") { t.Errorf("error = %v, want ErrNotFound naming the directory and the path", err) } } func TestLoadReportsABranchThatDoesNotExistInGitsWords(t *testing.T) { repo := sampleRepo(t) _, err := loadFrom(context.Background(), testProfile(), []string{repo}, Source{Ref: "nowhere", Path: "golang-init"}, t.TempDir()) if err == nil || !strings.Contains(err.Error(), "nowhere") { t.Errorf("error = %v, want git's complaint about the branch", err) } } func TestLoadFallsThroughToTheRemoteThatAnswers(t *testing.T) { // rickub.com does not serve git; git.rickub.com does. The first candidate // failing must cost a round trip, not the load. repo := sampleRepo(t) dest := t.TempDir() result, err := loadFrom(context.Background(), testProfile(), []string{filepath.Join(t.TempDir(), "nowhere.git"), repo}, Source{Ref: "main", Path: "golang-init"}, dest) if err != nil { t.Fatalf("loadFrom() error = %v", err) } if result.Remote != repo { t.Errorf("Remote = %q, want the one that answered", result.Remote) } } func TestLoadNamesEveryRemoteWhenNoneAnswers(t *testing.T) { sampleRepo(t) // for the git skip _, err := loadFrom(context.Background(), testProfile(), []string{filepath.Join(t.TempDir(), "a.git"), filepath.Join(t.TempDir(), "b.git")}, Source{}, t.TempDir()) if err == nil || !strings.Contains(err.Error(), "a.git") || !strings.Contains(err.Error(), "b.git") { t.Errorf("error = %v, want both remotes named", err) } } func TestLoadFromRickubOverTheNetwork(t *testing.T) { // The real thing, against the repository the feature was asked for. Off by // default: the suite must pass on a machine with no network. if os.Getenv("TURBO_CORE_NETWORK") == "" { t.Skip("TURBO_CORE_NETWORK is not set") } dest := t.TempDir() p := profile.Profile{Name: "Turbo Go", Slug: "turbo-go", Language: "Go"} result, err := Load(context.Background(), p, "https://rickub.com/turbo-editors/configs/tree/main/golang-init", dest) if err != nil { t.Fatalf("Load() error = %v", err) } if result.Remote != "https://git.rickub.com/turbo-editors/configs.git" { t.Errorf("Remote = %q", result.Remote) } for _, name := range []string{"settings.toml", "snippets.toml", "tools.toml"} { if _, err := os.Stat(filepath.Join(dest, ".turbo-go", name)); err != nil { t.Errorf("%s did not arrive: %v", name, err) } } }