hooks.go

  1package backend
  2
  3import (
  4	"context"
  5	"fmt"
  6	"io"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10	"sync"
 11
 12	"github.com/charmbracelet/log/v2"
 13	"github.com/charmbracelet/soft-serve/git"
 14	"github.com/charmbracelet/soft-serve/pkg/config"
 15	"github.com/charmbracelet/soft-serve/pkg/hooks"
 16	"github.com/charmbracelet/soft-serve/pkg/proto"
 17	"github.com/charmbracelet/soft-serve/pkg/sshutils"
 18	"github.com/charmbracelet/soft-serve/pkg/webhook"
 19	bugCache "github.com/git-bug/git-bug/cache"
 20	"github.com/git-bug/git-bug/repository"
 21)
 22
 23var _ hooks.Hooks = (*Backend)(nil)
 24
 25// PostReceive is called by the git post-receive hook.
 26//
 27// It implements Hooks.
 28func (d *Backend) PostReceive(ctx context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
 29	d.logger.Debug("post-receive hook called", "repo", repo, "args", args)
 30
 31	hasGitBugRefs := false
 32	for _, arg := range args {
 33		if isGitBugRef(arg.RefName) {
 34			hasGitBugRefs = true
 35			d.logger.Info("git-bug ref detected, rebuilding cache", "repo", repo, "ref", arg.RefName)
 36			if err := rebuildGitBugCache(d.cfg, repo, d.logger); err != nil {
 37				d.logger.Error("failed to rebuild git-bug cache", "repo", repo, "err", err)
 38			}
 39			break
 40		}
 41	}
 42	
 43	if !hasGitBugRefs {
 44		d.logger.Debug("no git-bug refs detected in push", "repo", repo)
 45	}
 46}
 47
 48// PreReceive is called by the git pre-receive hook.
 49//
 50// It implements Hooks.
 51func (d *Backend) PreReceive(_ context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
 52	d.logger.Debug("pre-receive hook called", "repo", repo, "args", args)
 53}
 54
 55// Update is called by the git update hook.
 56//
 57// It implements Hooks.
 58func (d *Backend) Update(ctx context.Context, _ io.Writer, _ io.Writer, repo string, arg hooks.HookArg) {
 59	d.logger.Debug("update hook called", "repo", repo, "arg", arg)
 60
 61	// Find user
 62	var user proto.User
 63	if pubkey := os.Getenv("SOFT_SERVE_PUBLIC_KEY"); pubkey != "" {
 64		pk, _, err := sshutils.ParseAuthorizedKey(pubkey)
 65		if err != nil {
 66			d.logger.Error("error parsing public key", "err", err)
 67			return
 68		}
 69
 70		user, err = d.UserByPublicKey(ctx, pk)
 71		if err != nil {
 72			d.logger.Error("error finding user from public key", "key", pubkey, "err", err)
 73			return
 74		}
 75	} else if username := os.Getenv("SOFT_SERVE_USERNAME"); username != "" {
 76		var err error
 77		user, err = d.User(ctx, username)
 78		if err != nil {
 79			d.logger.Error("error finding user from username", "username", username, "err", err)
 80			return
 81		}
 82	} else {
 83		d.logger.Error("error finding user")
 84		return
 85	}
 86
 87	// Get repo
 88	r, err := d.Repository(ctx, repo)
 89	if err != nil {
 90		d.logger.Error("error finding repository", "repo", repo, "err", err)
 91		return
 92	}
 93
 94	// TODO: run this async
 95	// This would probably need something like an RPC server to communicate with the hook process.
 96	if git.IsZeroHash(arg.OldSha) || git.IsZeroHash(arg.NewSha) {
 97		wh, err := webhook.NewBranchTagEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha)
 98		if err != nil {
 99			d.logger.Error("error creating branch_tag webhook", "err", err)
100		} else if err := webhook.SendEvent(ctx, wh); err != nil {
101			d.logger.Error("error sending branch_tag webhook", "err", err)
102		}
103	}
104	wh, err := webhook.NewPushEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha)
105	if err != nil {
106		d.logger.Error("error creating push webhook", "err", err)
107	} else if err := webhook.SendEvent(ctx, wh); err != nil {
108		d.logger.Error("error sending push webhook", "err", err)
109	}
110}
111
112// PostUpdate is called by the git post-update hook.
113//
114// It implements Hooks.
115func (d *Backend) PostUpdate(ctx context.Context, _ io.Writer, _ io.Writer, repo string, args ...string) {
116	d.logger.Debug("post-update hook called", "repo", repo, "args", args)
117
118	var wg sync.WaitGroup
119
120	// Populate last-modified file.
121	wg.Add(1)
122	go func() {
123		defer wg.Done()
124		if err := populateLastModified(ctx, d, repo); err != nil {
125			d.logger.Error("error populating last-modified", "repo", repo, "err", err)
126			return
127		}
128	}()
129
130	wg.Wait()
131}
132
133func populateLastModified(ctx context.Context, d *Backend, name string) error {
134	var rr *repo
135	_rr, err := d.Repository(ctx, name)
136	if err != nil {
137		return err
138	}
139
140	if r, ok := _rr.(*repo); ok {
141		rr = r
142	} else {
143		return proto.ErrRepoNotFound
144	}
145
146	r, err := rr.Open()
147	if err != nil {
148		return err
149	}
150
151	c, err := r.LatestCommitTime()
152	if err != nil {
153		return err
154	}
155
156	return rr.writeLastModified(c)
157}
158
159func isGitBugRef(refName string) bool {
160	return strings.HasPrefix(refName, "refs/bugs/") || strings.HasPrefix(refName, "refs/identities/")
161}
162
163func rebuildGitBugCache(cfg *config.Config, repoName string, logger *log.Logger) error {
164	logger.Info("starting git-bug cache rebuild", "repo", repoName)
165	repoPath := filepath.Join(cfg.DataPath, "repos", repoName+".git")
166
167	goGitRepo, err := repository.OpenGoGitRepo(repoPath, "git-bug", nil)
168	if err != nil {
169		return fmt.Errorf("open go-git repo: %w", err)
170	}
171	defer goGitRepo.Close()
172
173	rc, err := bugCache.NewRepoCacheNoEvents(goGitRepo)
174	if err != nil {
175		return fmt.Errorf("create repo cache: %w", err)
176	}
177	defer func() {
178		if closeErr := rc.Close(); closeErr != nil {
179			logger.Error("failed to close git-bug cache", "repo", repoName, "err", closeErr)
180		}
181	}()
182
183	var identityTotal int64
184	logger.Debug("rebuilding identities cache", "repo", repoName)
185	for ev := range rc.Identities().Build() {
186		if ev.Err != nil {
187			return fmt.Errorf("rebuild identities cache: %w", ev.Err)
188		}
189		logger.Debug("identity build event",
190			"repo", repoName,
191			"event", ev.Event,
192			"typename", ev.Typename,
193			"total", ev.Total,
194			"progress", ev.Progress)
195		
196		if ev.Total > 0 {
197			identityTotal = ev.Total
198		}
199	}
200	logger.Info("identities cache rebuilt", "repo", repoName, "count", identityTotal)
201
202	var bugTotal int64
203	logger.Debug("rebuilding bugs cache", "repo", repoName)
204	for ev := range rc.Bugs().Build() {
205		if ev.Err != nil {
206			return fmt.Errorf("rebuild bugs cache: %w", ev.Err)
207		}
208		logger.Debug("bug build event",
209			"repo", repoName,
210			"event", ev.Event,
211			"typename", ev.Typename,
212			"total", ev.Total,
213			"progress", ev.Progress)
214		
215		if ev.Total > 0 {
216			bugTotal = ev.Total
217		}
218	}
219	logger.Info("bugs cache rebuilt", "repo", repoName, "count", bugTotal)
220
221	logger.Info("git-bug cache rebuild complete", "repo", repoName, "identities", identityTotal, "bugs", bugTotal)
222	return nil
223}