package proposal import ( "context" "os" "os/exec" "path/filepath" "strings" "testing" "time" "github.com/barerepo/server/internal/gitx" "github.com/barerepo/server/internal/thread" ) // seeded is a bare repository with one commit on master, which a proposal ref can point at. func seeded(t *testing.T) string { t.Helper() dir := bare(t) work := t.TempDir() mustGit(t, "", "init", "-q", "-b", "master", work) if err := os.WriteFile(filepath.Join(work, "f"), []byte("hi\n"), 0o644); err != nil { t.Fatal(err) } mustGit(t, work, "add", "-A") mustGit(t, work, "-c", "user.email=t@x", "-c", "user.name=t", "commit", "-qm", "first") mustGit(t, work, "push", "-q", dir, "master") return dir } func mustGit(t *testing.T, dir string, args ...string) string { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@x", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@x") out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } return strings.TrimSpace(string(out)) } // Chapter 26: a proposal ref pins commits, so it expires, and the thread that is small stays. func TestExpireDropsTheRefAndKeepsTheThread(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() dir := seeded(t) tip := mustGit(t, dir, "rev-parse", "master") // Two proposals, one touched long ago and one touched now. for _, n := range []int{1, 2} { mustGit(t, dir, "update-ref", Ref(n), tip) meta := thread.Meta{Title: "proposal", State: thread.Open, Ref: Ref(n), Author: "mark", Opened: time.Now()} if err := thread.Write(ctx, dir, n, "open", func(tr *thread.Tree) { tr.Meta = meta }); err != nil { t.Fatal(err) } } // Nothing is stale yet, so a sweep with a long window must take nothing. gone, err := Expire(ctx, dir, 180*24*time.Hour, time.Now()) if err != nil { t.Fatal(err) } if len(gone) != 0 { t.Fatalf("a fresh proposal was expired: %v", gone) } // A year on, both are past the window. gone, err = Expire(ctx, dir, 180*24*time.Hour, time.Now().Add(365*24*time.Hour)) if err != nil { t.Fatal(err) } if len(gone) != 2 { t.Fatalf("expired %v, wanted both", gone) } refs, err := gitx.ListRefs(dir, "refs/proposals") if err != nil { t.Fatal(err) } if len(refs) != 0 { t.Errorf("the refs are still here: %v", refs) } // The thread is the discussion and the whole point of keeping it. list, err := thread.List(ctx, dir) if err != nil { t.Fatal(err) } if len(list) != 2 { t.Errorf("expiring a ref took its thread with it: %d threads left", len(list)) } if _, err := gitx.Run(ctx, dir, "cat-file", "-e", tip); err != nil { t.Error("the commit is gone before a gc has run, which update-ref alone cannot do") } } // A window of zero is expiry switched off, not expiry of everything. func TestExpireOfZeroTakesNothing(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() dir := seeded(t) mustGit(t, dir, "update-ref", Ref(1), mustGit(t, dir, "rev-parse", "master")) gone, err := Expire(ctx, dir, 0, time.Now().Add(365*24*time.Hour)) if err != nil { t.Fatal(err) } if len(gone) != 0 { t.Errorf("a zero window expired %v", gone) } } // Chapter 26: keep the most recent five retained tips, and the ones a comment is anchored to. func TestPruneRevisionsKeepsFiveAndTheAnchoredOne(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() dir := seeded(t) tip := mustGit(t, dir, "rev-parse", "master") meta := thread.Meta{Title: "a proposal", State: thread.Open, Ref: Ref(1), Author: "mark", Opened: time.Now()} if err := thread.Write(ctx, dir, 1, "open", func(tr *thread.Tree) { tr.Meta = meta }); err != nil { t.Fatal(err) } for k := 1; k <= 8; k++ { mustGit(t, dir, "update-ref", RevisionRef(1, k), tip) } if got := CurrentRevision(dir, 1); got != 9 { t.Fatalf("CurrentRevision = %d, wanted 9", got) } // One comment is anchored to revision 2, which is outside the newest five. anchored := thread.Comment{Author: "john", Time: time.Now(), Body: "this line", Anchor: "f:1", Blob: strings.Repeat("a", 40), Revision: 2} if err := thread.Reply(ctx, dir, 1, tip, anchored); err != nil { t.Fatal(err) } gone, err := PruneRevisions(ctx, dir, 1, RevisionsKept) if err != nil { t.Fatal(err) } // Eight revisions, five newest kept, revision 2 held by its comment, so 1 and 3 go. if len(gone) != 2 || gone[0] != 1 || gone[1] != 3 { t.Fatalf("pruned %v, wanted [1 3]", gone) } left, err := gitx.ListRefs(dir, RevisionPrefix(1)) if err != nil { t.Fatal(err) } if len(left) != 6 { t.Errorf("%d revisions left, wanted 6", len(left)) } if _, ok := left[RevisionRef(1, 2)]; !ok { t.Error("the revision a comment is anchored to was deleted") } } // A comment that does not say which revision it belongs to pins all of them, per 43.4. func TestAnAnchoredCommentWithNoRevisionHoldsEverything(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() dir := seeded(t) tip := mustGit(t, dir, "rev-parse", "master") meta := thread.Meta{Title: "a proposal", State: thread.Open, Ref: Ref(1), Author: "mark", Opened: time.Now()} if err := thread.Write(ctx, dir, 1, "open", func(tr *thread.Tree) { tr.Meta = meta }); err != nil { t.Fatal(err) } for k := 1; k <= 8; k++ { mustGit(t, dir, "update-ref", RevisionRef(1, k), tip) } old := thread.Comment{Author: "john", Time: time.Now(), Body: "written before revisions were recorded", Anchor: "f:1", Blob: strings.Repeat("b", 40)} if err := thread.Reply(ctx, dir, 1, tip, old); err != nil { t.Fatal(err) } gone, err := PruneRevisions(ctx, dir, 1, RevisionsKept) if err != nil { t.Fatal(err) } if len(gone) != 0 { t.Errorf("pruned %v while a comment could not say what it needs", gone) } }