{"contents":"package githttp\n\nimport (\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-git/go-git/v5/plumbing/protocol/packp\"\n\t\"github.com/go-git/go-git/v5/plumbing/server\"\n\t\"github.com/go-git/go-git/v5/plumbing/storer\"\n\t\"github.com/go-git/go-git/v5/plumbing/transport\"\n\n\t\"tangled.org/http-knot/config\"\n\t\"tangled.org/http-knot/db\"\n\ts3store \"tangled.org/http-knot/storage/s3\"\n\n\t\"tangled.org/core/notifier\"\n\t\"tangled.org/core/rbac\"\n)\n\n// Handler provides git smart HTTP protocol endpoints.\ntype Handler struct {\n\ts3cfg config.S3\n\tcfg *config.Config\n\tdb *db.DB\n\tenforcer *rbac.Enforcer\n\tnotifier *notifier.Notifier\n\tlogger *slog.Logger\n}\n\n// NewHandler creates a new git HTTP handler.\nfunc NewHandler(cfg *config.Config, db *db.DB, enforcer *rbac.Enforcer, n *notifier.Notifier, logger *slog.Logger) *Handler {\n\treturn \u0026Handler{\n\t\ts3cfg: cfg.S3,\n\t\tcfg: cfg,\n\t\tdb: db,\n\t\tenforcer: enforcer,\n\t\tnotifier: n,\n\t\tlogger: logger,\n\t}\n}\n\n// InfoRefs handles GET /{did}/{name}/info/refs?service=git-upload-pack|git-receive-pack\nfunc (h *Handler) InfoRefs(w http.ResponseWriter, r *http.Request) {\n\tdid := chi.URLParam(r, \"did\")\n\tname := chi.URLParam(r, \"name\")\n\tsvc := r.URL.Query().Get(\"service\")\n\n\tif svc != \"git-upload-pack\" \u0026\u0026 svc != \"git-receive-pack\" {\n\t\thttp.Error(w, \"invalid service\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tst, err := s3store.NewStorage(h.s3cfg, did, name)\n\tif err != nil {\n\t\th.logger.Error(\"failed to create storage\", \"err\", err)\n\t\thttp.Error(w, \"storage error\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tep, _ := transport.NewEndpoint(\"/\")\n\tsrv := server.NewServer(\u0026singleLoader{s: st})\n\n\tw.Header().Set(\"Content-Type\", fmt.Sprintf(\"application/x-%s-advertisement\", svc))\n\tw.Header().Set(\"Cache-Control\", \"no-cache, max-age=0, must-revalidate\")\n\n\t// Write pktline service announcement\n\tpktLine := fmt.Sprintf(\"# service=%s\\n\", svc)\n\tfmt.Fprintf(w, \"%04x%s0000\", len(pktLine)+4, pktLine)\n\n\tswitch svc {\n\tcase \"git-upload-pack\":\n\t\tsess, err := srv.NewUploadPackSession(ep, nil)\n\t\tif err != nil {\n\t\t\th.logger.Error(\"upload-pack session\", \"err\", err)\n\t\t\treturn\n\t\t}\n\t\tdefer sess.Close()\n\t\tar, err := sess.AdvertisedReferences()\n\t\tif err != nil {\n\t\t\th.logger.Error(\"advertised references\", \"err\", err)\n\t\t\treturn\n\t\t}\n\t\tar.Encode(w)\n\n\tcase \"git-receive-pack\":\n\t\tsess, err := srv.NewReceivePackSession(ep, nil)\n\t\tif err != nil {\n\t\t\th.logger.Error(\"receive-pack session\", \"err\", err)\n\t\t\treturn\n\t\t}\n\t\tdefer sess.Close()\n\t\tar, err := sess.AdvertisedReferences()\n\t\tif err != nil {\n\t\t\th.logger.Error(\"advertised references\", \"err\", err)\n\t\t\treturn\n\t\t}\n\t\tar.Encode(w)\n\t}\n}\n\n// UploadPack handles POST /{did}/{name}/git-upload-pack (clone/fetch)\nfunc (h *Handler) UploadPack(w http.ResponseWriter, r *http.Request) {\n\tdid := chi.URLParam(r, \"did\")\n\tname := chi.URLParam(r, \"name\")\n\n\tst, err := s3store.NewStorage(h.s3cfg, did, name)\n\tif err != nil {\n\t\thttp.Error(w, \"storage error\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbody := r.Body\n\tif r.Header.Get(\"Content-Encoding\") == \"gzip\" {\n\t\tgz, err := gzip.NewReader(body)\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"gzip decode error\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tdefer gz.Close()\n\t\tbody = gz\n\t}\n\n\tep, _ := transport.NewEndpoint(\"/\")\n\tsrv := server.NewServer(\u0026singleLoader{s: st})\n\n\tsess, err := srv.NewUploadPackSession(ep, nil)\n\tif err != nil {\n\t\th.logger.Error(\"upload-pack session\", \"did\", did, \"repo\", name, \"err\", err)\n\t\thttp.Error(w, \"session error\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer sess.Close()\n\n\tw.Header().Set(\"Content-Type\", \"application/x-git-upload-pack-result\")\n\n\t// Initialize session state (AdvRefs already sent via InfoRefs GET)\n\tif _, err := sess.AdvertisedReferences(); err != nil {\n\t\th.logger.Error(\"advertised references\", \"err\", err)\n\t\thttp.Error(w, \"internal error\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// Decode upload-pack request from POST body\n\treq := packp.NewUploadPackRequest()\n\tif err := req.Decode(body); err != nil {\n\t\th.logger.Error(\"decode upload-pack request\", \"did\", did, \"repo\", name, \"err\", err)\n\t\treturn\n\t}\n\n\tresp, err := sess.UploadPack(r.Context(), req)\n\tif err != nil {\n\t\th.logger.Error(\"upload-pack\", \"did\", did, \"repo\", name, \"err\", err)\n\t\treturn\n\t}\n\n\tif err := resp.Encode(w); err != nil {\n\t\th.logger.Error(\"encode upload-pack response\", \"did\", did, \"repo\", name, \"err\", err)\n\t}\n}\n\n// ReceivePack handles POST /{did}/{name}/git-receive-pack (push)\nfunc (h *Handler) ReceivePack(w http.ResponseWriter, r *http.Request) {\n\tdid := chi.URLParam(r, \"did\")\n\tname := chi.URLParam(r, \"name\")\n\n\tst, err := s3store.NewStorage(h.s3cfg, did, name)\n\tif err != nil {\n\t\thttp.Error(w, \"storage error\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbody := r.Body\n\tif r.Header.Get(\"Content-Encoding\") == \"gzip\" {\n\t\tgz, err := gzip.NewReader(body)\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"gzip decode error\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tdefer gz.Close()\n\t\tbody = gz\n\t}\n\n\tep, _ := transport.NewEndpoint(\"/\")\n\tsrv := server.NewServer(\u0026singleLoader{s: st})\n\n\tsess, err := srv.NewReceivePackSession(ep, nil)\n\tif err != nil {\n\t\th.logger.Error(\"receive-pack session\", \"did\", did, \"repo\", name, \"err\", err)\n\t\thttp.Error(w, \"session error\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer sess.Close()\n\n\tw.Header().Set(\"Content-Type\", \"application/x-git-receive-pack-result\")\n\n\t// Initialize session state (AdvRefs already sent via InfoRefs GET)\n\tif _, err := sess.AdvertisedReferences(); err != nil {\n\t\th.logger.Error(\"advertised references\", \"err\", err)\n\t\thttp.Error(w, \"internal error\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// Decode the reference update request\n\treq := packp.NewReferenceUpdateRequest()\n\tif err := req.Decode(body); err != nil {\n\t\th.logger.Error(\"decode receive-pack request\", \"err\", err)\n\t\treturn\n\t}\n\n\t// Execute the receive-pack\n\trs, err := sess.ReceivePack(r.Context(), req)\n\tif rs != nil {\n\t\tif encErr := rs.Encode(w); encErr != nil {\n\t\t\th.logger.Error(\"encode report status\", \"err\", encErr)\n\t\t}\n\t}\n\tif err != nil {\n\t\th.logger.Error(\"receive-pack\", \"did\", did, \"repo\", name, \"err\", err)\n\t\treturn\n\t}\n\n\t// Post-receive: process ref updates\n\tactorDid, _ := ActorDidFromRequest(r)\n\th.postReceive(r.Context(), st, did, name, actorDid, req)\n}\n\n// singleLoader maps any endpoint to a single storer.\ntype singleLoader struct {\n\ts storer.Storer\n}\n\nfunc (l *singleLoader) Load(*transport.Endpoint) (storer.Storer, error) {\n\tif l.s == nil {\n\t\treturn nil, transport.ErrRepositoryNotFound\n\t}\n\treturn l.s, nil\n}\n\n// writeCloser wraps http.ResponseWriter to satisfy io.WriteCloser.\ntype writeCloser struct {\n\thttp.ResponseWriter\n}\n\nfunc newWriteCloser(w http.ResponseWriter) io.WriteCloser {\n\treturn \u0026writeCloser{w}\n}\n\nfunc (wc *writeCloser) Close() error {\n\tif f, ok := wc.ResponseWriter.(http.Flusher); ok {\n\t\tf.Flush()\n\t}\n\treturn nil\n}\n\n// rejectPush writes a git-compatible rejection message.\nfunc rejectPush(w http.ResponseWriter, msg string) {\n\tw.Header().Set(\"Content-Type\", \"application/x-git-receive-pack-result\")\n\tline := \"\\x03\" + msg + \"\\n\"\n\tfmt.Fprintf(w, \"%04x%s0000\", len(line)+4, line)\n}\n\n// ActorDidFromRequest extracts the actor DID from the request context.\nfunc ActorDidFromRequest(r *http.Request) (string, bool) {\n\tv := r.Context().Value(actorDidKey)\n\tif v == nil {\n\t\treturn \"\", false\n\t}\n\tdid, ok := v.(string)\n\treturn did, ok\n}\n","path":"githttp/handler.go","ref":"main"}