completed phase 2b

This commit is contained in:
2026-05-11 20:10:45 +02:00
parent 83d96d0a1e
commit 4002a3b84d
20 changed files with 1566 additions and 50 deletions
+50 -20
View File
@@ -11,22 +11,32 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"golang.org/x/crypto/bcrypt"
"xorm.io/xorm"
"github.com/forgeo/forgebucket/internal/config"
"github.com/forgeo/forgebucket/internal/events"
"github.com/forgeo/forgebucket/internal/models"
)
type GitHTTPHandler struct {
db *xorm.Engine
cfg *config.Config
bus events.EventBus
}
func NewGitHTTPHandler(db *xorm.Engine, cfg *config.Config) *GitHTTPHandler {
return &GitHTTPHandler{db: db, cfg: cfg}
func NewGitHTTPHandler(db *xorm.Engine, cfg *config.Config, bus events.EventBus) *GitHTTPHandler {
return &GitHTTPHandler{db: db, cfg: cfg, bus: bus}
}
// refUpdate captures one ref-update line from a git-receive-pack request.
type refUpdate struct {
OldRev string
NewRev string
Ref string
}
// ServeGit is the entry point for all git smart-HTTP requests.
@@ -107,13 +117,15 @@ func (h *GitHTTPHandler) ServeGit(w http.ResponseWriter, r *http.Request) {
// Branch protection check: parse pkt-lines from the receive-pack body,
// check each ref against stored protection rules, then restore the body.
var pushedRefs []refUpdate
if service == "git-receive-pack" {
if reason, newBody := checkProtectionsFromBody(h.db, repo.ID, authedUser, r.Body); reason != "" {
reason, refs, newBody := parseAndCheckBody(h.db, repo.ID, authedUser, r.Body)
if reason != "" {
http.Error(w, reason, http.StatusForbidden)
return
} else {
r.Body = io.NopCloser(newBody)
}
pushedRefs = refs
r.Body = io.NopCloser(newBody)
}
// Build PATH_INFO: /{reponame}.git/{suffix}
@@ -157,6 +169,27 @@ func (h *GitHTTPHandler) ServeGit(w http.ResponseWriter, r *http.Request) {
if err := runGitBackend(r.Context(), w, r.Body, gitExec, env); err != nil {
http.Error(w, fmt.Sprintf("git http-backend: %v", err), http.StatusInternalServerError)
return
}
// Publish push.received for each ref pushed so the CI orchestrator can react.
if service == "git-receive-pack" {
zeroOID := strings.Repeat("0", 40)
for _, ref := range pushedRefs {
if ref.NewRev == zeroOID {
continue // branch deletion — skip CI trigger
}
go h.bus.Publish(events.SubjectPushReceived, events.PushEvent{ //nolint:errcheck
RepoID: repo.ID,
RepoName: repoName,
OwnerName: owner,
Ref: ref.Ref,
Before: ref.OldRev,
After: ref.NewRev,
Pusher: authedUser,
At: time.Now().UTC(),
})
}
}
}
@@ -239,15 +272,14 @@ func runGitBackend(ctx context.Context, w http.ResponseWriter, body io.Reader, g
return waitErr
}
// checkProtectionsFromBody parses git pkt-line ref updates from a receive-pack body,
// checks each ref against stored branch protection rules, and returns a denial reason
// (or "") plus a restored reader so the body can still be passed to http-backend.
func checkProtectionsFromBody(db *xorm.Engine, repoID int64, pusher string, body io.Reader) (reason string, restored io.Reader) {
// parseAndCheckBody parses git pkt-line ref updates from a receive-pack body,
// checks each ref against stored branch protection rules, and returns a denial
// reason (or ""), the list of parsed ref updates, and a restored reader.
func parseAndCheckBody(db *xorm.Engine, repoID int64, pusher string, body io.Reader) (reason string, refs []refUpdate, restored io.Reader) {
var buf bytes.Buffer
zeroOID := strings.Repeat("0", 40)
for {
// Every pkt-line starts with a 4-hex-digit length that includes itself.
lenBuf := make([]byte, 4)
if _, err := io.ReadFull(body, lenBuf); err != nil {
break
@@ -259,8 +291,7 @@ func checkProtectionsFromBody(db *xorm.Engine, repoID int64, pusher string, body
break
}
if pktLen64 == 0 {
// Flush packet — end of ref-update list.
break
break // flush packet — end of ref-update list
}
dataLen := int(pktLen64) - 4
if dataLen <= 0 {
@@ -280,16 +311,15 @@ func checkProtectionsFromBody(db *xorm.Engine, repoID int64, pusher string, body
}
oldRev, newRev, refname := parts[0], parts[1], parts[2]
// New branches (oldRev all zeros) are not subject to protection.
if oldRev == zeroOID {
continue
}
// Detect force push: if newRev is all zeros it's a branch deletion.
isForcePush := newRev == zeroOID
refs = append(refs, refUpdate{OldRev: oldRev, NewRev: newRev, Ref: refname})
if oldRev == zeroOID {
continue // new branch — not subject to protection
}
isForcePush := newRev == zeroOID
if msg := CheckBranchProtection(db, repoID, pusher, refname, isForcePush); msg != "" {
return msg, io.MultiReader(&buf, body)
return msg, refs, io.MultiReader(&buf, body)
}
}
return "", io.MultiReader(&buf, body)
return "", refs, io.MultiReader(&buf, body)
}