Monorepo for Tangled tangled.org

knotserver: implement internal endpoint to record git push

records git operations in an "oplog" table. this table can be routinely
cleaned up as necessary.

Signed-off-by: oppiliappan <me@oppi.li>

authored by oppi.li and committed by Tangled 01cee5e8 1da790a0

Changed files
+135
knotserver
+9
knotserver/db/init.go
··· 43 43 id integer primary key autoincrement, 44 44 last_time_us integer not null 45 45 ); 46 + 47 + create table if not exists oplog ( 48 + tid text primary key, 49 + did text not null, 50 + repo text not null, 51 + old_sha text not null, 52 + new_sha text not null, 53 + ref text not null 54 + ); 46 55 `) 47 56 if err != nil { 48 57 return nil, err
+63
knotserver/db/oplog.go
··· 1 + package db 2 + 3 + import ( 4 + "fmt" 5 + ) 6 + 7 + type Op struct { 8 + Tid string // time based ID, easy to enumerate & monotonic 9 + Did string // did of pusher 10 + Repo string // <did/repo> fully qualified repo 11 + OldSha string // old sha of reference being updated 12 + NewSha string // new sha of reference being updated 13 + Ref string // the reference being updated 14 + } 15 + 16 + func (d *DB) InsertOp(op Op) error { 17 + _, err := d.db.Exec( 18 + `insert into oplog (tid, did, repo, old_sha, new_sha, ref) values (?, ?, ?, ?, ?, ?)`, 19 + op.Tid, 20 + op.Did, 21 + op.Repo, 22 + op.OldSha, 23 + op.NewSha, 24 + op.Ref, 25 + ) 26 + return err 27 + } 28 + 29 + func (d *DB) GetOps(cursor string) ([]Op, error) { 30 + whereClause := "" 31 + args := []any{} 32 + if cursor != "" { 33 + whereClause = "where tid > ?" 34 + args = append(args, cursor) 35 + } 36 + 37 + query := fmt.Sprintf(` 38 + select tid, did, repo, old_sha, new_sha, ref 39 + from oplog 40 + %s 41 + order by tid asc 42 + limit 100 43 + `, whereClause) 44 + 45 + rows, err := d.db.Query(query, args...) 46 + if err != nil { 47 + return nil, err 48 + } 49 + defer rows.Close() 50 + 51 + var ops []Op 52 + for rows.Next() { 53 + var op Op 54 + rows.Scan(&op.Tid, &op.Did, &op.Repo, &op.OldSha, &op.NewSha, &op.Ref) 55 + ops = append(ops, op) 56 + } 57 + 58 + if err := rows.Err(); err != nil { 59 + return nil, err 60 + } 61 + 62 + return ops, nil 63 + }
+56
knotserver/internal.go
··· 1 1 package knotserver 2 2 3 3 import ( 4 + "bufio" 4 5 "context" 5 6 "log/slog" 6 7 "net/http" 8 + "path/filepath" 9 + "strings" 7 10 8 11 "github.com/go-chi/chi/v5" 9 12 "github.com/go-chi/chi/v5/middleware" ··· 54 57 return 55 58 } 56 59 60 + func (h *InternalHandle) PostReceiveHook(w http.ResponseWriter, r *http.Request) { 61 + l := h.l.With("handler", "PostReceiveHook") 62 + 63 + gitAbsoluteDir := r.Header.Get("X-Git-Dir") 64 + gitRelativeDir, err := filepath.Rel(h.c.Repo.ScanPath, gitAbsoluteDir) 65 + if err != nil { 66 + l.Error("failed to calculate relative git dir", "scanPath", h.c.Repo.ScanPath, "gitAbsoluteDir", gitAbsoluteDir) 67 + return 68 + } 69 + gitUserDid := r.Header.Get("X-Git-User-Did") 70 + 71 + var ops []db.Op 72 + scanner := bufio.NewScanner(r.Body) 73 + for scanner.Scan() { 74 + line := scanner.Text() 75 + parts := strings.SplitN(line, " ", 3) 76 + if len(parts) != 3 { 77 + l.Error("invalid payload", "parts", parts) 78 + continue 79 + } 80 + 81 + tid := TID() 82 + oldSha := parts[0] 83 + newSha := parts[1] 84 + ref := parts[2] 85 + op := db.Op{ 86 + Tid: tid, 87 + Did: gitUserDid, 88 + Repo: gitRelativeDir, 89 + OldSha: oldSha, 90 + NewSha: newSha, 91 + Ref: ref, 92 + } 93 + ops = append(ops, op) 94 + } 95 + 96 + if err := scanner.Err(); err != nil { 97 + l.Error("failed to read payload", "err", err) 98 + return 99 + } 100 + 101 + for _, op := range ops { 102 + err := h.db.InsertOp(op) 103 + if err != nil { 104 + l.Error("failed to insert op", "err", err, "op", op) 105 + continue 106 + } 107 + } 108 + 109 + return 110 + } 111 + 57 112 func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger) http.Handler { 58 113 r := chi.NewRouter() 59 114 ··· 66 121 67 122 r.Get("/push-allowed", h.PushAllowed) 68 123 r.Get("/keys", h.InternalKeys) 124 + r.Post("/hooks/post-receive", h.PostReceiveHook) 69 125 r.Mount("/debug", middleware.Profiler()) 70 126 71 127 return r
+7
knotserver/util.go
··· 5 5 "os" 6 6 "path/filepath" 7 7 8 + "github.com/bluesky-social/indigo/atproto/syntax" 8 9 securejoin "github.com/cyphar/filepath-securejoin" 9 10 "github.com/go-chi/chi/v5" 10 11 "github.com/microcosm-cc/bluemonday" ··· 43 44 func setMIME(w http.ResponseWriter, mime string) { 44 45 w.Header().Add("Content-Type", mime) 45 46 } 47 + 48 + var TIDClock = syntax.NewTIDClock(0) 49 + 50 + func TID() string { 51 + return TIDClock.Next().String() 52 + }