From 3843eb55bacf238a627efe60a88763af293d2d7d Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sat, 15 Aug 2026 12:09:45 +0300 Subject: [PATCH] remoteapi: prove an empty store takes an unrelated first push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This file had not compiled since the logrus->slog migration removed the package-level `logger` its serve goroutines still called. The `integration` build tag keeps it out of every default build, test and vet run, so nothing ever said so — the suite simply never ran again. With it compiling, add the scenario the storage layer's contract is actually about: a database provisioned empty ahead of its first push (the web create form, or git.sr.ht's hook through /internal/repos) accepts a push from a local database with a root commit of its own, with no --force. Mutating the provisioning to InitStore fails it exactly as a user hits it today: "unknown push error; no common ancestor". --- remoteapi/integration_test.go | 64 +++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/remoteapi/integration_test.go b/remoteapi/integration_test.go index 947c8b40a81762e0680b93f25324f71700a068cb..1d270d22f607138dcd685536c3915588de365d04 100644 --- a/remoteapi/integration_test.go +++ b/remoteapi/integration_test.go @@ -11,7 +11,12 @@ // (5) keypair (Bearer EdDSA JWT) clone of a PRIVATE own db succeeds after the // key is registered (dolt creds flow, no --user); // (6) WhoAmI answers with the account identity for a keypair JWT; -// (7) an ACL RO grantee can clone but not push (private repo stays "found"). +// (7) an ACL RO grantee can clone but not push (private repo stays "found"); +// (8) push-to-create provisions a repo + empty store on the first push; +// (9) push-to-create is refused in another user's namespace; +// (10) a database provisioned empty ahead of time (the web form, or git.sr.ht's +// hook via /internal/repos) accepts a first push from an unrelated local +// history with NO --force. // // Run with: // @@ -29,6 +34,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "log/slog" "net" "os" "os/exec" @@ -154,9 +160,15 @@ func TestRemoteAPIIntegration(t *testing.T) { if err != nil { t.Fatalf("New: %v", err) } + // The serve loops outlive the test body (GracefulStop is deferred), so their + // errors go to slog rather than to t: a t.Errorf after the test returns + // panics the run. This used to be a package-level logrus `logger` that the + // slog migration removed, which left this file failing to COMPILE — and + // nothing noticed, because the integration tag keeps it out of every default + // build and vet run. go func() { if err := srv.Serve(); err != nil { - logger.Errorf("remotesapi serve: %v", err) + slog.Error("remotesapi serve", "err", err) } }() defer srv.GracefulStop() @@ -167,7 +179,7 @@ func TestRemoteAPIIntegration(t *testing.T) { } go func() { if err := csrv.Serve(); err != nil { - logger.Errorf("credentials serve: %v", err) + slog.Error("credentials serve", "err", err) } }() defer csrv.GracefulStop() @@ -395,6 +407,52 @@ func TestRemoteAPIIntegration(t *testing.T) { } t.Logf("foreign-namespace push-to-create correctly denied:\n%s", out) }) + + // (10) A database provisioned ahead of the first push — the web create form, + // or git.sr.ht's post-update hook through /internal/repos — is created with + // an EMPTY store, and that is what lets its owner push a database they + // already have without --force. The local side here is deliberately not a + // fresh clone: it is an independent `dolt init` with its own root commit, + // the exact shape of a beads tracker or any locally grown database. With an + // "Initialize data repository" commit on the remote this push is a + // non-fast-forward and dolt refuses it client-side, which is the regression + // this test exists to catch. + t.Run("provisioned_empty_accepts_unrelated_history", func(t *testing.T) { + emptyAbs := storage.RepoDiskPath(reposRoot, "alice", "provisioned") + if err := storage.InitEmptyStore(ctx, emptyAbs); err != nil { + t.Fatalf("provision empty store: %v", err) + } + if _, err := store.CreateRepo(ctx, &core.Repo{ + Name: "provisioned", OwnerID: 1, OwnerName: "alice", + Path: emptyAbs, Visibility: core.VisibilityPrivate, + }); err != nil { + t.Fatalf("create provisioned repo row: %v", err) + } + + provURL := fmt.Sprintf("http://%s/~alice/provisioned", addr) + env := append(append([]string{}, baseEnv...), "DOLT_REMOTE_PASSWORD="+pat) + + work := filepath.Join(t.TempDir(), "provisioned") + if err := os.MkdirAll(work, 0o755); err != nil { + t.Fatal(err) + } + runDolt(t, env, work, "init") + runDolt(t, env, work, "sql", "-q", "create table beads(id int primary key); insert into beads values (7);") + runDolt(t, env, work, "commit", "-Am", "local history that predates the remote") + runDolt(t, env, work, "remote", "add", "origin", provURL) + + // No --force: runDolt fails the test on a non-zero exit, so a rejected + // non-fast-forward is the failure. + runDolt(t, env, work, "push", "--user", "alice", "origin", "main") + + dir := t.TempDir() + runDolt(t, env, dir, "clone", "--user", "alice", provURL) + out := runDolt(t, env, filepath.Join(dir, "provisioned"), "sql", "-q", "select id from beads", "-r", "csv") + if !strings.Contains(out, "7") { + t.Fatalf("pushed row not present after clone; got:\n%s", out) + } + t.Log("pre-provisioned empty database accepted an unrelated history without --force") + }) } // --- helpers ---