123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 | package render_test
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"code.bruce-hill.com/repo-renderer/internal/render"
)
func TestPubExportIsCloneable(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.render()
e.git(e.work, "tag", "v1")
e.git(e.work, "push", "-q", "origin", "v1")
if err := e.site.ExportPub("proj", "main"); err != nil {
t.Fatal(err)
}
pub := e.site.Lay.PubDir("proj")
for _, f := range []string{"HEAD", "info/refs", "objects/info/packs", "refs/heads/main", "refs/tags/v1"} {
if _, err := os.Stat(filepath.Join(pub, f)); err != nil {
t.Errorf("pub missing %s", f)
}
}
for _, f := range []string{"config", "hooks", "description"} {
if _, err := os.Stat(filepath.Join(pub, f)); err == nil {
t.Errorf("pub leaks %s", f)
}
}
refs := mustReadFile(t, filepath.Join(pub, "info", "refs"))
mustContain(t, "info/refs", refs, "refs/heads/main", "refs/tags/v1")
clone := t.TempDir()
e.git("", "clone", "-q", "file://"+pub, filepath.Join(clone, "c"))
if _, err := os.Stat(filepath.Join(clone, "c", "src", "main.c")); err != nil {
t.Error("clone from pub export is missing files")
}
// A branch that is pushed but not exported must not leak through the
// pack: the hook accepts pushes to any refs/heads/*.
e.git(e.work, "checkout", "-q", "-b", "wip")
e.write("secret.txt", "do not publish\n")
e.commit("wip")
e.git(e.work, "push", "-q", "origin", "wip")
secret := strings.TrimSpace(e.git(e.work, "rev-parse", "HEAD:secret.txt"))
e.git(e.work, "checkout", "-q", "main")
// Make it the repository's only pack, as git gc would.
e.git(e.site.Lay.GitDir("proj"), "repack", "-a", "-d", "-q")
if err := e.site.ExportPub("proj", "main"); err != nil {
t.Fatal(err)
}
packs := packIndexes(t, pub)
if len(packs) != 1 {
t.Fatalf("expected one pack in pub, got %v", packs)
}
listing := e.git("", "verify-pack", "-v", packs[0])
if strings.Contains(listing, secret) {
t.Error("pub pack contains an object only reachable from a private branch")
}
if !strings.Contains(listing, strings.TrimSpace(e.git(e.work, "rev-parse", "main:src/main.c"))) {
t.Error("pub pack is missing an exported object")
}
// Nothing exported changed: the pack is reused, not rebuilt.
before := mustStat(t, packs[0])
time.Sleep(20 * time.Millisecond)
if err := e.site.ExportPub("proj", "main"); err != nil {
t.Fatal(err)
}
after := mustStat(t, packs[0])
if !after.ModTime().Equal(before.ModTime()) {
t.Error("unchanged export rebuilt the pack")
}
// The export still clones, and only has main.
clone2 := filepath.Join(t.TempDir(), "c2")
e.git("", "clone", "-q", "file://"+pub, clone2)
if out := e.git(clone2, "branch", "-r"); strings.Contains(out, "wip") {
t.Errorf("clone sees private branch: %s", out)
}
}
func TestPubExportGrowsIncrementally(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.render()
pub := e.site.Lay.PubDir("proj")
export := func() []string {
t.Helper()
if err := e.site.ExportPub("proj", "main"); err != nil {
t.Fatal(err)
}
idx := packIndexes(t, pub)
list := mustReadFile(t, filepath.Join(pub, "objects", "info", "packs"))
if n := strings.Count(list, "P pack-"); n != len(idx) {
t.Fatalf("objects/info/packs lists %d packs, %d on disk:\n%s", n, len(idx), list)
}
return idx
}
inPub := func(sha string) bool {
for _, idx := range export() {
if strings.Contains(e.git("", "verify-pack", "-v", idx), sha) {
return true
}
}
return false
}
// Clones over dumb HTTP, as httpd serves it, and checks out file.
srv := httptest.NewServer(http.FileServer(http.Dir(pub)))
defer srv.Close()
cloneHas := func(file string) {
t.Helper()
dst := filepath.Join(t.TempDir(), "c")
e.git("", "clone", "-q", srv.URL, dst)
if _, err := os.Stat(filepath.Join(dst, filepath.FromSlash(file))); err != nil {
t.Errorf("dumb-HTTP clone is missing %s", file)
}
}
if n := len(export()); n != 1 {
t.Fatalf("first export: %d packs, want 1", n)
}
// Fast-forwards and a new tag add a pack each; the old ones stay.
e.write("one.txt", "one\n")
e.commit("one")
e.push(false)
if n := len(export()); n != 2 {
t.Fatalf("after a fast-forward: %d packs, want 2", n)
}
e.git(e.work, "tag", "-a", "-m", "v1", "v1")
e.git(e.work, "push", "-q", "origin", "v1")
if n := len(export()); n != 3 {
t.Fatalf("after a new tag: %d packs, want 3", n)
}
cloneHas("one.txt")
// A rewound branch rebuilds one pack, without the abandoned commit.
e.write("oops.txt", "oops\n")
e.commit("oops")
e.push(false)
oops := strings.TrimSpace(e.git(e.work, "rev-parse", "HEAD:oops.txt"))
if !inPub(oops) {
t.Fatal("pushed object not exported")
}
e.git(e.work, "reset", "-q", "--hard", "HEAD~")
e.push(true)
if n := len(export()); n != 1 {
t.Fatalf("after a force push: %d packs, want 1", n)
}
if inPub(oops) {
t.Error("export still holds an object only the rewound commit reached")
}
cloneHas("one.txt")
// So does a moved tag, and so does reaching maxPubPacks.
e.write("two.txt", "two\n")
e.commit("two")
e.push(false)
if n := len(export()); n != 2 {
t.Fatalf("after a fast-forward: %d packs, want 2", n)
}
e.git(e.work, "tag", "-f", "-a", "-m", "v1 again", "v1")
e.git(e.work, "push", "-q", "-f", "origin", "v1")
if n := len(export()); n != 1 {
t.Fatalf("after moving a tag: %d packs, want 1", n)
}
for i := 2; i <= render.MaxPubPacks; i++ {
e.write("n.txt", fmt.Sprintf("%d\n", i))
e.commit(fmt.Sprint(i))
e.push(false)
if n := len(export()); n != i {
t.Fatalf("after %d fast-forwards: %d packs, want %d", i-1, n, i)
}
}
e.write("n.txt", "last\n")
e.commit("last")
e.push(false)
if n := len(export()); n != 1 {
t.Fatalf("at MaxPubPacks: %d packs, want 1", n)
}
cloneHas("n.txt")
}
// TestCloneOverHTTP clones from a plain file server of htdocs: the export
// must live inside the served root, and git must find nothing to complain
// about.
func TestCloneOverHTTP(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.render()
if err := e.site.ExportPub("proj", "main"); err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(http.FileServer(http.Dir(e.site.Lay.Htdocs)))
defer srv.Close()
dst := filepath.Join(t.TempDir(), "c")
if out := e.git("", "clone", "-q", srv.URL+"/git/proj.git", dst); out != "" {
t.Errorf("clone over http printed:\n%s", out)
}
if b, err := os.ReadFile(filepath.Join(dst, "src", "main.c")); err != nil || !strings.Contains(string(b), "main") {
t.Errorf("clone over http: %v %q", err, b)
}
}
// packIndexes lists the export's pack indexes. It reads the directory
// rather than globbing its path, which holds the test's temporary
// directory and so whatever characters TMPDIR has.
func packIndexes(t *testing.T, pub string) []string {
t.Helper()
dir := filepath.Join(pub, "objects", "pack")
ents, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
var idx []string
for _, e := range ents {
if strings.HasSuffix(e.Name(), ".idx") {
idx = append(idx, filepath.Join(dir, e.Name()))
}
}
return idx
}
|