fix(web/bugs): improve cache management

Amolith and Crush created

Implements: bug-3823b04
Co-authored-by: Crush <crush@charm.land>

Change summary

pkg/backend/hooks.go  | 91 ++++++++++++++++++++++++++++++++++++++++++++
pkg/web/webui_bugs.go | 12 ++++
2 files changed, 100 insertions(+), 3 deletions(-)

Detailed changes

pkg/backend/hooks.go 🔗

@@ -2,15 +2,22 @@ package backend
 
 import (
 	"context"
+	"fmt"
 	"io"
 	"os"
+	"path/filepath"
+	"strings"
 	"sync"
 
+	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/git"
+	"github.com/charmbracelet/soft-serve/pkg/config"
 	"github.com/charmbracelet/soft-serve/pkg/hooks"
 	"github.com/charmbracelet/soft-serve/pkg/proto"
 	"github.com/charmbracelet/soft-serve/pkg/sshutils"
 	"github.com/charmbracelet/soft-serve/pkg/webhook"
+	bugCache "github.com/git-bug/git-bug/cache"
+	"github.com/git-bug/git-bug/repository"
 )
 
 var _ hooks.Hooks = (*Backend)(nil)
@@ -18,8 +25,24 @@ var _ hooks.Hooks = (*Backend)(nil)
 // PostReceive is called by the git post-receive hook.
 //
 // It implements Hooks.
-func (d *Backend) PostReceive(_ context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
+func (d *Backend) PostReceive(ctx context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
 	d.logger.Debug("post-receive hook called", "repo", repo, "args", args)
+
+	hasGitBugRefs := false
+	for _, arg := range args {
+		if isGitBugRef(arg.RefName) {
+			hasGitBugRefs = true
+			d.logger.Info("git-bug ref detected, rebuilding cache", "repo", repo, "ref", arg.RefName)
+			if err := rebuildGitBugCache(d.cfg, repo, d.logger); err != nil {
+				d.logger.Error("failed to rebuild git-bug cache", "repo", repo, "err", err)
+			}
+			break
+		}
+	}
+	
+	if !hasGitBugRefs {
+		d.logger.Debug("no git-bug refs detected in push", "repo", repo)
+	}
 }
 
 // PreReceive is called by the git pre-receive hook.
@@ -132,3 +155,69 @@ func populateLastModified(ctx context.Context, d *Backend, name string) error {
 
 	return rr.writeLastModified(c)
 }
+
+func isGitBugRef(refName string) bool {
+	return strings.HasPrefix(refName, "refs/bugs/") || strings.HasPrefix(refName, "refs/identities/")
+}
+
+func rebuildGitBugCache(cfg *config.Config, repoName string, logger *log.Logger) error {
+	logger.Info("starting git-bug cache rebuild", "repo", repoName)
+	repoPath := filepath.Join(cfg.DataPath, "repos", repoName+".git")
+
+	goGitRepo, err := repository.OpenGoGitRepo(repoPath, "git-bug", nil)
+	if err != nil {
+		return fmt.Errorf("open go-git repo: %w", err)
+	}
+	defer goGitRepo.Close()
+
+	rc, err := bugCache.NewRepoCacheNoEvents(goGitRepo)
+	if err != nil {
+		return fmt.Errorf("create repo cache: %w", err)
+	}
+	defer func() {
+		if closeErr := rc.Close(); closeErr != nil {
+			logger.Error("failed to close git-bug cache", "repo", repoName, "err", closeErr)
+		}
+	}()
+
+	var identityTotal int64
+	logger.Debug("rebuilding identities cache", "repo", repoName)
+	for ev := range rc.Identities().Build() {
+		if ev.Err != nil {
+			return fmt.Errorf("rebuild identities cache: %w", ev.Err)
+		}
+		logger.Debug("identity build event",
+			"repo", repoName,
+			"event", ev.Event,
+			"typename", ev.Typename,
+			"total", ev.Total,
+			"progress", ev.Progress)
+		
+		if ev.Total > 0 {
+			identityTotal = ev.Total
+		}
+	}
+	logger.Info("identities cache rebuilt", "repo", repoName, "count", identityTotal)
+
+	var bugTotal int64
+	logger.Debug("rebuilding bugs cache", "repo", repoName)
+	for ev := range rc.Bugs().Build() {
+		if ev.Err != nil {
+			return fmt.Errorf("rebuild bugs cache: %w", ev.Err)
+		}
+		logger.Debug("bug build event",
+			"repo", repoName,
+			"event", ev.Event,
+			"typename", ev.Typename,
+			"total", ev.Total,
+			"progress", ev.Progress)
+		
+		if ev.Total > 0 {
+			bugTotal = ev.Total
+		}
+	}
+	logger.Info("bugs cache rebuilt", "repo", repoName, "count", bugTotal)
+
+	logger.Info("git-bug cache rebuild complete", "repo", repoName, "identities", identityTotal, "bugs", bugTotal)
+	return nil
+}

pkg/web/webui_bugs.go 🔗

@@ -316,7 +316,11 @@ func repoBugs(w http.ResponseWriter, r *http.Request) {
 		renderNotFound(w, r)
 		return
 	}
-	defer rc.Close()
+	defer func() {
+		if closeErr := rc.Close(); closeErr != nil {
+			logger.Debug("failed to close bug cache", "repo", repo.Name(), "err", closeErr)
+		}
+	}()
 
 	status := r.URL.Query().Get("status")
 	if status == "" {
@@ -404,7 +408,11 @@ func repoBug(w http.ResponseWriter, r *http.Request) {
 		renderNotFound(w, r)
 		return
 	}
-	defer rc.Close()
+	defer func() {
+		if closeErr := rc.Close(); closeErr != nil {
+			logger.Debug("failed to close bug cache", "repo", repo.Name(), "err", closeErr)
+		}
+	}()
 
 	bugCache, err := findBugByHash(rc, hash)
 	if err != nil {