Artifact c1ef60f074eed0870da93985b8ae35d87e9961eb2af56838465d9d43ee4ff213:
- File
internal/sync/receipt.go
— part of check-in
[4add332299]
at
2026-10-18 20:30:31
on branch feat/submanifest-reader
— fix: record one rcvfrom row per receipt, as fossil does
Fossil records each arrival of content (a commit, a tag, a sync or clone, a request its server answers, an unversioned add) as one rcvfrom row, made by content_rcvid_init when the first content arrives, and points every blob and unversioned file it stores at that row. go-libfossil seeded a single row at creation and pointed every blob at rcvid 1.
db.Receipt is that row: it is made lazily on first use, with the time as a Julian day, and remade if the transaction that made it rolled back. A transaction records content under the receipt it is given, and repo.WithReceipt gives one to every transaction, bound ones included, of a commit, tag, sync, clone or served request. The client credits its local user and the peer's address, as fossil's client records g.zIpAddr; the server credits nobody until a login card names a user, then that user and the card's nonce, from the request's address. Phantoms keep rcvid 0, and an applied unversioned deletion keeps its row's rcvid, as in fossil.
A new repository no longer starts with a placeholder row; an existing one keeps it. (user: [email protected], size: 4212) [annotate] [blame] [check-ins using] [more...]
package sync import ( "context" "fmt" "net" "net/http/httptrace" gosync "sync" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/simio" ) // A sync, a clone and a request the server answers are each one receipt, as // each is one fossil process holding one g.rcvid (src/content.c, // content_rcvid_init): the first content one stores records an rcvfrom row, // and everything it stores names that row. This file holds what fills those // rows the way fossil fills them. // peerAddresser is a Transport that knows the address of the peer it last // exchanged with, as fossil's client knows g.zIpAddr: the numeric address of // the socket it connected (src/http_socket.c). A transport that does not // know leaves the client's receipt without one, as fossil's file transport // does. type peerAddresser interface { PeerAddr() string } // PeerAddr records the IP address of the peer an HTTP request connected to. // The zero value is ready to use and safe for concurrent use. type PeerAddr struct { mu gosync.Mutex ip string } // Trace returns ctx with an HTTP trace that records the address of the // connection a request made with it goes out on. func (p *PeerAddr) Trace(ctx context.Context) context.Context { if p == nil { panic("sync.PeerAddr.Trace: receiver must not be nil") } return httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ GotConn: func(info httptrace.GotConnInfo) { if info.Conn == nil { return } ip := hostOnly(info.Conn.RemoteAddr().String()) p.mu.Lock() p.ip = ip p.mu.Unlock() }, }) } // Get returns the address last recorded, "" before any. func (p *PeerAddr) Get() string { if p == nil { panic("sync.PeerAddr.Get: receiver must not be nil") } p.mu.Lock() defer p.mu.Unlock() return p.ip } // hostOnly drops the port from a host:port address, as REMOTE_ADDR and // fossil's getnameinfo carry the address alone. func hostOnly(addr string) string { host, _, err := net.SplitHostPort(addr) if err != nil { return addr } return host } // withClientReceipt returns r recording what a sync or clone receives under // one receipt, credited to the local user named login (fossil's user_select) // and stamped by clock. A Repo already carrying a receipt keeps it, so the // pull a clone runs for its phantoms is part of the clone's receipt. func withClientReceipt(r *repo.Repo, login string, clock simio.Clock) (*repo.Repo, error) { if r.Receipt() != nil { return r, nil } uid, err := r.LocalUID(login) if err != nil { return nil, fmt.Errorf("receipt: %w", err) } return r.WithReceipt(&db.Receipt{UID: uid, Clock: clock}), nil } // notePeer gives the client's receipt the address of the peer t last // exchanged with. It matters only until the receipt's row is made, which is // when fossil reads g.zIpAddr. func notePeer(r *repo.Repo, t Transport) { rc := r.Receipt() if rc == nil { panic("sync.notePeer: the session's repo has no receipt") } if p, ok := t.(peerAddresser); ok { rc.IPAddr = p.PeerAddr() } } // remoteAddrKey is the context key for the address a request came from. type remoteAddrKey struct{} // WithRemoteAddr returns ctx carrying addr, the address a sync request came // from, for the server's receipt (fossil's REMOTE_ADDR). A host:port address // is recorded as the host alone. func WithRemoteAddr(ctx context.Context, addr string) context.Context { return context.WithValue(ctx, remoteAddrKey{}, hostOnly(addr)) } // serverReceipt is the receipt of one request the server answers: credited to // nobody until a login card names a user (src/login.c sets g.userUid to // nobody's uid before page_xfer reads any card), from the request's address. func serverReceipt(ctx context.Context, r *repo.Repo, opts HandleOpts) (*db.Receipt, error) { var uid int64 err := r.DB().QueryRow("SELECT coalesce((SELECT uid FROM user WHERE login='nobody'), 0)"). Scan(&uid) if err != nil { return nil, fmt.Errorf("receipt: nobody's uid: %w", err) } addr := hostOnly(opts.RemoteAddr) if addr == "" { addr, _ = ctx.Value(remoteAddrKey{}).(string) } return &db.Receipt{UID: uid, IPAddr: addr, Clock: opts.Clock}, nil }