| 1 | package changelog |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "os/exec" |
| 6 | "path/filepath" |
| 7 | "reflect" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/docsite" |
| 12 | ) |
| 13 | |
| 14 | // scratchRepo builds a small history with every shape main has had: |
| 15 | // |
| 16 | // v0.1.0: a root commit adding docs/site/cli.md |
| 17 | // a direct commit |
| 18 | // an Amadan squash of branch "feat/x", whose own two commits |
| 19 | // survive at refs/amadan/closed/feat/x |
| 20 | // a GitHub merge of branch "gh-branch" |
| 21 | // v0.2.0 |
| 22 | type scratch struct { |
| 23 | t *testing.T |
| 24 | dir string |
| 25 | n int |
| 26 | } |
| 27 | |
| 28 | func (s *scratch) git(env []string, args ...string) string { |
| 29 | s.t.Helper() |
| 30 | cmd := exec.Command("git", append([]string{"-C", s.dir}, args...)...) |
| 31 | cmd.Env = append(os.Environ(), |
| 32 | "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", |
| 33 | "GIT_AUTHOR_NAME=Paul Campbell", "GIT_AUTHOR_EMAIL=paul@tito.io", |
| 34 | "GIT_COMMITTER_NAME=Paul Campbell", "GIT_COMMITTER_EMAIL=paul@tito.io") |
| 35 | cmd.Env = append(cmd.Env, env...) |
| 36 | out, err := cmd.CombinedOutput() |
| 37 | if err != nil { |
| 38 | s.t.Fatalf("git %v: %v\n%s", args, err, out) |
| 39 | } |
| 40 | return strings.TrimSpace(string(out)) |
| 41 | } |
| 42 | |
| 43 | func (s *scratch) commit(author []string, msg, file, body string) string { |
| 44 | s.t.Helper() |
| 45 | s.n++ |
| 46 | date := []string{ |
| 47 | "GIT_AUTHOR_DATE=2026-09-0" + string(rune('0'+s.n)) + "T12:00:00Z", |
| 48 | "GIT_COMMITTER_DATE=2026-09-0" + string(rune('0'+s.n)) + "T12:00:00Z", |
| 49 | } |
| 50 | if err := os.MkdirAll(filepath.Dir(filepath.Join(s.dir, file)), 0o755); err != nil { |
| 51 | s.t.Fatal(err) |
| 52 | } |
| 53 | if err := os.WriteFile(filepath.Join(s.dir, file), []byte(body), 0o644); err != nil { |
| 54 | s.t.Fatal(err) |
| 55 | } |
| 56 | s.git(nil, "add", file) |
| 57 | s.git(append(author, date...), "commit", "-q", "-m", msg) |
| 58 | return s.git(nil, "rev-parse", "HEAD") |
| 59 | } |
| 60 | |
| 61 | var asClaude = []string{"GIT_AUTHOR_NAME=claude", "GIT_AUTHOR_EMAIL=claude~paul@keymail.dev"} |
| 62 | |
| 63 | func newScratch(t *testing.T) *scratch { |
| 64 | s := &scratch{t: t, dir: t.TempDir()} |
| 65 | s.git(nil, "init", "-q", "-b", "main") |
| 66 | s.commit(nil, "root", "docs/site/cli.md", "# The CLI\n\n### carlos db\n\ntext\n") |
| 67 | s.git(nil, "tag", "-a", "v0.1.0", "-m", "carlos v0.1.0") |
| 68 | s.commit(nil, "a direct commit\n\nCo-Authored-By: Claude <noreply@anthropic.com>", "a.txt", "a") |
| 69 | |
| 70 | s.git(nil, "checkout", "-q", "-b", "feat/x") |
| 71 | s.commit(asClaude, "x one", "x.txt", "1") |
| 72 | s.commit(asClaude, "x two (#7)", "docs/site/cli.md", "# The CLI\n\n### carlos db\n\ntext\nmore\n") |
| 73 | s.git(nil, "update-ref", "refs/amadan/closed/feat/x", "HEAD") |
| 74 | s.git(nil, "checkout", "-q", "main") |
| 75 | s.git(nil, "merge", "-q", "--squash", "feat/x") |
| 76 | s.git(append(asClaude, "GIT_COMMITTER_DATE=2026-09-08T12:00:00Z", "GIT_AUTHOR_DATE=2026-09-08T12:00:00Z"), |
| 77 | "commit", "-q", "-m", "x: the feature\n\nSquashed from feat/x into main.") |
| 78 | |
| 79 | s.git(nil, "checkout", "-q", "-b", "gh-branch") |
| 80 | s.commit([]string{"GIT_AUTHOR_NAME=Bill Horsman", "GIT_AUTHOR_EMAIL=bill@keymail.dev"}, "gh work", "g.txt", "g") |
| 81 | s.git(nil, "checkout", "-q", "main") |
| 82 | s.git([]string{"GIT_AUTHOR_NAME=GitHub", "GIT_AUTHOR_EMAIL=noreply@github.com", "GIT_COMMITTER_DATE=2026-09-09T12:00:00Z"}, |
| 83 | "merge", "-q", "--no-ff", "gh-branch", "-m", "Merge pull request #3 from carlosframework/gh-branch") |
| 84 | s.git(nil, "tag", "v0.2.0") |
| 85 | return s |
| 86 | } |
| 87 | |
| 88 | func TestExecGitEndToEnd(t *testing.T) { |
| 89 | s := newScratch(t) |
| 90 | g, err := NewExecGit(s.dir) |
| 91 | if err != nil { |
| 92 | t.Fatal(err) |
| 93 | } |
| 94 | tags, err := g.Tags() |
| 95 | if err != nil || len(tags) != 2 { |
| 96 | t.Fatalf("tags = %+v, %v", tags, err) |
| 97 | } |
| 98 | if _, _, ok := g.Tombstone("feat/x"); !ok { |
| 99 | t.Fatal("tombstone feat/x not found") |
| 100 | } |
| 101 | |
| 102 | rel := fakeReleases{ |
| 103 | "v0.1.0": "First release.\n", |
| 104 | "v0.2.0": "carlos v0.2.0 (platform x, 2026-09-09)\n\n- **The feature.** `carlos db pull`.\n", |
| 105 | } |
| 106 | disc := &fakeDiscussions{byID: map[int]Discussion{7: {ID: 7, Title: "seven", State: "open", Created: 1}}} |
| 107 | site := testSite(t) |
| 108 | site.BySlug["cli"] = docsite.Parse("cli", "# The CLI\n\n### carlos db\n\ntext\nmore\n") |
| 109 | c, err := Build(Sources{g, rel, disc}, testPeople, Links{"v0.2.0": {"The feature.": {"feat/x"}}}, site) |
| 110 | if err != nil { |
| 111 | t.Fatal(err) |
| 112 | } |
| 113 | if len(c.Releases) != 2 || c.Releases[0].Tag != "v0.2.0" { |
| 114 | t.Fatalf("releases = %+v", c.Releases) |
| 115 | } |
| 116 | ch := c.Releases[0].Changes |
| 117 | if len(ch) != 3 { |
| 118 | t.Fatalf("v0.2.0 changes = %+v", ch) |
| 119 | } |
| 120 | merge, squash, direct := ch[0], ch[1], ch[2] |
| 121 | |
| 122 | if merge.Branch != "gh-branch" || merge.BranchURL != "" || merge.Commits != 1 || merge.Subject != "gh work" || |
| 123 | !reflect.DeepEqual(merge.Credit, []Credit{{Person: "Bill Horsman"}}) { |
| 124 | t.Errorf("merge = %+v", merge) |
| 125 | } |
| 126 | if squash.Branch != "feat/x" || squash.BranchURL != RepoURL+"/-/branch/feat%2Fx" || squash.Commits != 2 { |
| 127 | t.Errorf("squash = %+v", squash) |
| 128 | } |
| 129 | if !reflect.DeepEqual(squash.Credit, []Credit{{Person: "Paul Campbell", Agents: []string{"claude"}}}) { |
| 130 | t.Errorf("squash credit = %+v", squash.Credit) |
| 131 | } |
| 132 | if len(squash.Discussions) != 1 || squash.Discussions[0].ID != 7 { |
| 133 | t.Errorf("squash discussions = %+v", squash.Discussions) |
| 134 | } |
| 135 | if !reflect.DeepEqual(squash.Docs, []string{"/docs/cli#carlos-db"}) { |
| 136 | t.Errorf("squash docs = %v", squash.Docs) |
| 137 | } |
| 138 | if direct.Branch != "" || !reflect.DeepEqual(direct.Credit, []Credit{{Person: "Paul Campbell", Agents: []string{"claude"}}}) { |
| 139 | t.Errorf("direct = %+v", direct) |
| 140 | } |
| 141 | |
| 142 | first := c.Releases[1] |
| 143 | if first.Tag != "v0.1.0" || !first.Published || len(first.Changes) != 1 || first.Changes[0].Subject != "root" { |
| 144 | t.Fatalf("v0.1.0 = %+v", first) |
| 145 | } |
| 146 | if !reflect.DeepEqual(first.Changes[0].Docs, []string{"/docs/cli#carlos-db"}) { |
| 147 | t.Errorf("root docs = %v", first.Changes[0].Docs) |
| 148 | } |
| 149 | } |
| 150 | |