Something went wrong. Try again.
Git backed by object storage because you can't stop me
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244package main
import ( "bytes" "context" "log/slog" "net" "os" "os/exec" "path/filepath" "strings" "sync" "testing" "time"
"github.com/go-git/go-billy/v6/memfs" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/transport" "tangled.org/xeiaso.net/objgit/internal/auth")
func TestDiffRefs(t *testing.T) { main := plumbing.NewBranchReferenceName("main") dev := plumbing.NewBranchReferenceName("dev") h1 := plumbing.NewHash("1111111111111111111111111111111111111111") h2 := plumbing.NewHash("2222222222222222222222222222222222222222")
tests := []struct { name string before map[plumbing.ReferenceName]plumbing.Hash after map[plumbing.ReferenceName]plumbing.Hash want []refUpdate }{ { name: "created", before: map[plumbing.ReferenceName]plumbing.Hash{}, after: map[plumbing.ReferenceName]plumbing.Hash{main: h1}, want: []refUpdate{{Name: main, Old: plumbing.ZeroHash, New: h1}}, }, { name: "updated", before: map[plumbing.ReferenceName]plumbing.Hash{main: h1}, after: map[plumbing.ReferenceName]plumbing.Hash{main: h2}, want: []refUpdate{{Name: main, Old: h1, New: h2}}, }, { name: "deleted", before: map[plumbing.ReferenceName]plumbing.Hash{main: h1, dev: h2}, after: map[plumbing.ReferenceName]plumbing.Hash{main: h1}, want: []refUpdate{{Name: dev, Old: h2, New: plumbing.ZeroHash}}, }, { name: "unchanged", before: map[plumbing.ReferenceName]plumbing.Hash{main: h1}, after: map[plumbing.ReferenceName]plumbing.Hash{main: h1}, want: nil, }, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := diffRefs(tt.before, tt.after) if len(got) != len(tt.want) { t.Fatalf("diffRefs = %v, want %v", got, tt.want) } for i, u := range got { if u != tt.want[i] { t.Errorf("update[%d] = %+v, want %+v", i, u, tt.want[i]) } } }) }}
// syncBuffer is a goroutine-safe buffer for capturing slog output while the// server and an async hook write concurrently.type syncBuffer struct { mu sync.Mutex buf bytes.Buffer}
func (b *syncBuffer) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() return b.buf.Write(p)}
func (b *syncBuffer) String() string { b.mu.Lock() defer b.mu.Unlock() return b.buf.String()}
// TestReceivePackHook pushes a repo carrying .objgit/hooks/receive-pack and// asserts the hook runs in the sandbox: it reads /src, writes scratch to /tmp,// and cannot write to the read-only /src.func TestReceivePackHook(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") }
var logBuf syncBuffer prev := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelDebug}))) defer slog.SetDefault(prev)
fs := memfs.New() d := &daemon{ fs: fs, loader: transport.NewFilesystemLoader(fs, false), authz: auth.AllowAnonymous{AllowWrite: true}, allowHooks: true, hookTimeout: 30 * time.Second, }
ctx, cancel := context.WithCancel(context.Background()) defer cancel()
ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } go func() { _ = d.Serve(ctx, ln) }()
remote := "git://" + ln.Addr().String() + "/hooked.git"
work := t.TempDir() runGit(t, work, "init", "-b", "main") runGit(t, work, "config", "user.email", "test@example.com") runGit(t, work, "config", "user.name", "Test")
// The hook reads /src (cwd), writes scratch to /tmp, then attempts a write // into the read-only /src. The final write aborts the shell with a // read-only error, so WROTE_SRC must never print. hook := strings.Join([]string{ "cat README.md", "echo built > /tmp/out", "cat /tmp/out", "echo nope > /src/nope.txt", "echo WROTE_SRC", }, "\n") + "\n" writeFile(t, filepath.Join(work, "README.md"), "hello from repo\n") writeFile(t, filepath.Join(work, ".objgit", "hooks", "receive-pack"), hook) runGit(t, work, "add", ".") runGit(t, work, "commit", "-m", "with hook")
pushOut := runGit(t, work, "push", remote, "main")
// Hooks now run synchronously and stream stdout/stderr to the client over // the sideband, so by the time push returns the hook has finished and its // output rides along in the push output as "remote:" lines. logs := logBuf.String() if !strings.Contains(logs, "hook: running") { t.Fatalf("hook did not run; logs:\n%s", logs) } // /src is readable and /tmp is writable; their output streamed to the client. for _, want := range []string{"hello from repo", "built"} { if !strings.Contains(pushOut, want) { t.Errorf("push output missing streamed hook output %q; output:\n%s", want, pushOut) } } // Output reaches the client over the sideband, rendered with a "remote:" prefix. if !strings.Contains(pushOut, "remote:") { t.Errorf("hook output not streamed as remote progress; output:\n%s", pushOut) } // Writing to /src is rejected and aborts the script (logged, not streamed). if !strings.Contains(logs, "read-only filesystem") { t.Errorf("expected read-only error when writing /src; logs:\n%s", logs) } if strings.Contains(pushOut, "WROTE_SRC") { t.Errorf("hook was able to write to read-only /src; output:\n%s", pushOut) }}
// TestReceivePackHookAbsent confirms a push with no hook file is a no-op (push// still succeeds, nothing logged as a hook run).func TestReceivePackHookAbsent(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") }
var logBuf syncBuffer prev := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelDebug}))) defer slog.SetDefault(prev)
fs := memfs.New() d := &daemon{ fs: fs, loader: transport.NewFilesystemLoader(fs, false), authz: auth.AllowAnonymous{AllowWrite: true}, allowHooks: true, hookTimeout: 30 * time.Second, }
ctx, cancel := context.WithCancel(context.Background()) defer cancel()
ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } go func() { _ = d.Serve(ctx, ln) }()
remote := "git://" + ln.Addr().String() + "/plain.git" work := t.TempDir() runGit(t, work, "init", "-b", "main") runGit(t, work, "config", "user.email", "test@example.com") runGit(t, work, "config", "user.name", "Test") runGit(t, work, "commit", "--allow-empty", "-m", "no hook") runGit(t, work, "push", remote, "main")
// runHook logs this at debug level once it sees there is no hook file. waitForLog(t, &logBuf, "no hook file", 10*time.Second)
if strings.Contains(logBuf.String(), "hook: running") { t.Errorf("hook ran for a repo with no hook file; logs:\n%s", logBuf.String()) }}
func writeFile(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("mkdir %q: %v", path, err) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("write %q: %v", path, err) }}
// waitForLog blocks until the captured log output contains substr, or fails the// test after timeout.func waitForLog(t *testing.T, buf *syncBuffer, substr string, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if strings.Contains(buf.String(), substr) { return } time.Sleep(10 * time.Millisecond) } t.Fatalf("timed out waiting for log %q; logs:\n%s", substr, buf.String())}