package changelog import ( "os" "os/exec" "path/filepath" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/docsite" ) // scratchRepo builds a small history with every shape main has had: // // v0.1.0: a root commit adding docs/site/cli.md // a direct commit // an Amadan squash of branch "feat/x", whose own two commits // survive at refs/amadan/closed/feat/x // a GitHub merge of branch "gh-branch" // v0.2.0 type scratch struct { t *testing.T dir string n int } func (s *scratch) git(env []string, args ...string) string { s.t.Helper() cmd := exec.Command("git", append([]string{"-C", s.dir}, args...)...) cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_AUTHOR_NAME=Paul Campbell", "GIT_AUTHOR_EMAIL=paul@tito.io", "GIT_COMMITTER_NAME=Paul Campbell", "GIT_COMMITTER_EMAIL=paul@tito.io") cmd.Env = append(cmd.Env, env...) out, err := cmd.CombinedOutput() if err != nil { s.t.Fatalf("git %v: %v\n%s", args, err, out) } return strings.TrimSpace(string(out)) } func (s *scratch) commit(author []string, msg, file, body string) string { s.t.Helper() s.n++ date := []string{ "GIT_AUTHOR_DATE=2026-09-0" + string(rune('0'+s.n)) + "T12:00:00Z", "GIT_COMMITTER_DATE=2026-09-0" + string(rune('0'+s.n)) + "T12:00:00Z", } if err := os.MkdirAll(filepath.Dir(filepath.Join(s.dir, file)), 0o755); err != nil { s.t.Fatal(err) } if err := os.WriteFile(filepath.Join(s.dir, file), []byte(body), 0o644); err != nil { s.t.Fatal(err) } s.git(nil, "add", file) s.git(append(author, date...), "commit", "-q", "-m", msg) return s.git(nil, "rev-parse", "HEAD") } var asClaude = []string{"GIT_AUTHOR_NAME=claude", "GIT_AUTHOR_EMAIL=claude~paul@keymail.dev"} func newScratch(t *testing.T) *scratch { s := &scratch{t: t, dir: t.TempDir()} s.git(nil, "init", "-q", "-b", "main") s.commit(nil, "root", "docs/site/cli.md", "# The CLI\n\n### carlos db\n\ntext\n") s.git(nil, "tag", "-a", "v0.1.0", "-m", "carlos v0.1.0") s.commit(nil, "a direct commit\n\nCo-Authored-By: Claude ", "a.txt", "a") s.git(nil, "checkout", "-q", "-b", "feat/x") s.commit(asClaude, "x one", "x.txt", "1") s.commit(asClaude, "x two (#7)", "docs/site/cli.md", "# The CLI\n\n### carlos db\n\ntext\nmore\n") s.git(nil, "update-ref", "refs/amadan/closed/feat/x", "HEAD") s.git(nil, "checkout", "-q", "main") s.git(nil, "merge", "-q", "--squash", "feat/x") s.git(append(asClaude, "GIT_COMMITTER_DATE=2026-09-08T12:00:00Z", "GIT_AUTHOR_DATE=2026-09-08T12:00:00Z"), "commit", "-q", "-m", "x: the feature\n\nSquashed from feat/x into main.") s.git(nil, "checkout", "-q", "-b", "gh-branch") s.commit([]string{"GIT_AUTHOR_NAME=Bill Horsman", "GIT_AUTHOR_EMAIL=bill@keymail.dev"}, "gh work", "g.txt", "g") s.git(nil, "checkout", "-q", "main") s.git([]string{"GIT_AUTHOR_NAME=GitHub", "GIT_AUTHOR_EMAIL=noreply@github.com", "GIT_COMMITTER_DATE=2026-09-09T12:00:00Z"}, "merge", "-q", "--no-ff", "gh-branch", "-m", "Merge pull request #3 from carlosframework/gh-branch") s.git(nil, "tag", "v0.2.0") return s } func TestExecGitEndToEnd(t *testing.T) { s := newScratch(t) g, err := NewExecGit(s.dir) if err != nil { t.Fatal(err) } tags, err := g.Tags() if err != nil || len(tags) != 2 { t.Fatalf("tags = %+v, %v", tags, err) } if _, _, ok := g.Tombstone("feat/x"); !ok { t.Fatal("tombstone feat/x not found") } rel := fakeReleases{ "v0.1.0": "First release.\n", "v0.2.0": "carlos v0.2.0 (platform x, 2026-09-09)\n\n- **The feature.** `carlos db pull`.\n", } disc := &fakeDiscussions{byID: map[int]Discussion{7: {ID: 7, Title: "seven", State: "open", Created: 1}}} site := testSite(t) site.BySlug["cli"] = docsite.Parse("cli", "# The CLI\n\n### carlos db\n\ntext\nmore\n") c, err := Build(Sources{g, rel, disc}, testPeople, Links{"v0.2.0": {"The feature.": {"feat/x"}}}, site) if err != nil { t.Fatal(err) } if len(c.Releases) != 2 || c.Releases[0].Tag != "v0.2.0" { t.Fatalf("releases = %+v", c.Releases) } ch := c.Releases[0].Changes if len(ch) != 3 { t.Fatalf("v0.2.0 changes = %+v", ch) } merge, squash, direct := ch[0], ch[1], ch[2] if merge.Branch != "gh-branch" || merge.BranchURL != "" || merge.Commits != 1 || merge.Subject != "gh work" || !reflect.DeepEqual(merge.Credit, []Credit{{Person: "Bill Horsman"}}) { t.Errorf("merge = %+v", merge) } if squash.Branch != "feat/x" || squash.BranchURL != RepoURL+"/-/branch/feat%2Fx" || squash.Commits != 2 { t.Errorf("squash = %+v", squash) } if !reflect.DeepEqual(squash.Credit, []Credit{{Person: "Paul Campbell", Agents: []string{"claude"}}}) { t.Errorf("squash credit = %+v", squash.Credit) } if len(squash.Discussions) != 1 || squash.Discussions[0].ID != 7 { t.Errorf("squash discussions = %+v", squash.Discussions) } if !reflect.DeepEqual(squash.Docs, []string{"/docs/cli#carlos-db"}) { t.Errorf("squash docs = %v", squash.Docs) } if direct.Branch != "" || !reflect.DeepEqual(direct.Credit, []Credit{{Person: "Paul Campbell", Agents: []string{"claude"}}}) { t.Errorf("direct = %+v", direct) } first := c.Releases[1] if first.Tag != "v0.1.0" || !first.Published || len(first.Changes) != 1 || first.Changes[0].Subject != "root" { t.Fatalf("v0.1.0 = %+v", first) } if !reflect.DeepEqual(first.Changes[0].Docs, []string{"/docs/cli#carlos-db"}) { t.Errorf("root docs = %v", first.Changes[0].Docs) } }