Something went wrong. Try again.
Git backed by object storage because you can't stop me
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227package main
import ( "log/slog" "net/http/httptest" "os/exec" "path/filepath" "strings" "testing" "time"
"github.com/go-git/go-billy/v6" "github.com/go-git/go-billy/v6/memfs" "github.com/go-git/go-git/v6/plumbing/transport" "tangled.org/xeiaso.net/objgit/internal/auth")
// TestSmartHTTP drives a real git client against the smart-HTTP handler over an// in-memory filesystem, covering push (create-on-demand), the write-gate// enforced by the authorizer, and clone round-trips.func TestSmartHTTP(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") }
for _, tt := range []struct { name string allowPush bool doPush bool wantPushErr bool wantCloneErr bool }{ { name: "push creates repo and clone round-trips", allowPush: true, doPush: true, }, { name: "push rejected when disabled", allowPush: false, doPush: true, wantPushErr: true, wantCloneErr: true, }, { name: "clone of missing repo fails", allowPush: true, doPush: false, wantCloneErr: true, }, } { t.Run(tt.name, func(t *testing.T) { ts, fs := newHTTPServer(t, tt.allowPush) remote := ts.URL + "/test.git"
var srcHead string if tt.doPush { work := seedRepo(t) srcHead = strings.TrimSpace(runGit(t, work, "rev-parse", "HEAD"))
out, err := tryGit(work, "push", remote, "main") if tt.wantPushErr { if err == nil { t.Fatalf("expected push to be rejected, got success:\n%s", out) } } else if err != nil { t.Fatalf("push failed: %v\n%s", err, out) } }
// The bare repo must exist on disk iff a push was expected to land. _, statErr := fs.Stat("/test.git/config") pushLanded := tt.doPush && !tt.wantPushErr if pushLanded && statErr != nil { t.Fatalf("expected repo to be created on push, but config missing: %v", statErr) } if !pushLanded && statErr == nil { t.Fatal("repository must not exist when push did not land") }
dst := t.TempDir() out, err := tryGit(dst, "clone", remote, "cloned") if tt.wantCloneErr { if err == nil { t.Fatalf("expected clone to fail, got success:\n%s", out) } return } if err != nil { t.Fatalf("clone failed: %v\n%s", err, out) }
gotHead := strings.TrimSpace(runGit(t, filepath.Join(dst, "cloned"), "rev-parse", "HEAD")) if gotHead != srcHead { t.Logf("want: %s", srcHead) t.Logf("got: %s", gotHead) t.Error("cloned HEAD does not match pushed HEAD") } }) }}
// newHTTPServer starts an httptest server backed by a fresh in-memory filesystem// and returns it alongside that filesystem for state assertions.func newHTTPServer(t *testing.T, allowPush bool) (*httptest.Server, billy.Filesystem) { t.Helper() fs := memfs.New() d := &daemon{ fs: fs, loader: transport.NewFilesystemLoader(fs, false), authz: auth.AllowAnonymous{AllowWrite: allowPush}, } ts := httptest.NewServer(d) t.Cleanup(ts.Close) return ts, fs}
// TestSmartHTTPAnonymousReadWhilePushDisabled verifies that with push disabled,// anonymous clone of an existing repo still succeeds — reads are always allowed// by the default authorizer, only writes are gated.func TestSmartHTTPAnonymousReadWhilePushDisabled(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") }
// Seed a repo via a push-enabled server over a shared filesystem. fs := memfs.New() seed := httptest.NewServer(&daemon{ fs: fs, loader: transport.NewFilesystemLoader(fs, false), authz: auth.AllowAnonymous{AllowWrite: true}, }) t.Cleanup(seed.Close)
work := seedRepo(t) srcHead := strings.TrimSpace(runGit(t, work, "rev-parse", "HEAD")) if out, err := tryGit(work, "push", seed.URL+"/test.git", "main"); err != nil { t.Fatalf("seed push failed: %v\n%s", err, out) }
// Serve the same filesystem with push disabled and clone from it. ro := httptest.NewServer(&daemon{ fs: fs, loader: transport.NewFilesystemLoader(fs, false), authz: auth.AllowAnonymous{AllowWrite: false}, }) t.Cleanup(ro.Close)
dst := t.TempDir() if out, err := tryGit(dst, "clone", ro.URL+"/test.git", "cloned"); err != nil { t.Fatalf("anonymous clone should succeed with push disabled: %v\n%s", err, out) } gotHead := strings.TrimSpace(runGit(t, filepath.Join(dst, "cloned"), "rev-parse", "HEAD")) if gotHead != srcHead { t.Errorf("cloned HEAD %q != seeded HEAD %q", gotHead, srcHead) }}
// TestSmartHTTPHookStreams pushes a repo carrying .objgit/hooks/receive-pack// over smart-HTTP and asserts the hook runs synchronously and streams its// output to the client over the sideband (rendered as "remote:" lines). This// exercises the flush-on-write wrapper that delivers band-2 progress live// instead of letting net/http buffer it.func TestSmartHTTPHookStreams(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() ts := httptest.NewServer(&daemon{ fs: fs, loader: transport.NewFilesystemLoader(fs, false), authz: auth.AllowAnonymous{AllowWrite: true}, allowHooks: true, hookTimeout: 30 * time.Second, }) t.Cleanup(ts.Close)
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")
hook := strings.Join([]string{ "cat README.md", "echo hook_ran", }, "\n") + "\n" writeFile(t, filepath.Join(work, "README.md"), "hello from http repo\n") writeFile(t, filepath.Join(work, ".objgit", "hooks", "receive-pack"), hook) runGit(t, work, "add", ".") runGit(t, work, "commit", "-m", "with hook")
out, err := tryGit(work, "push", ts.URL+"/hooked.git", "main") if err != nil { t.Fatalf("push failed: %v\n%s", err, out) }
logs := logBuf.String() if !strings.Contains(logs, "hook: running") { t.Fatalf("hook did not run; logs:\n%s", logs) } for _, want := range []string{"hello from http repo", "hook_ran"} { if !strings.Contains(out, want) { t.Errorf("push output missing streamed hook output %q; output:\n%s", want, out) } } if !strings.Contains(out, "remote:") { t.Errorf("hook output not streamed as remote progress; output:\n%s", out) }}
// seedRepo creates a local git repository with one commit and returns its path.func seedRepo(t *testing.T) string { t.Helper() 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", "initial") return work}