1package backend
2
3import (
4 "context"
5 "io"
6 "os"
7 "sync"
8
9 "github.com/charmbracelet/soft-serve/git"
10 "github.com/charmbracelet/soft-serve/pkg/hooks"
11 "github.com/charmbracelet/soft-serve/pkg/proto"
12 "github.com/charmbracelet/soft-serve/pkg/sshutils"
13 "github.com/charmbracelet/soft-serve/pkg/webhook"
14)
15
16var _ hooks.Hooks = (*Backend)(nil)
17
18// PostReceive is called by the git post-receive hook.
19//
20// It implements Hooks.
21func (d *Backend) PostReceive(_ context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
22 d.logger.Debug("post-receive hook called", "repo", repo, "args", args)
23}
24
25// PreReceive is called by the git pre-receive hook.
26//
27// It implements Hooks.
28func (d *Backend) PreReceive(_ context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
29 d.logger.Debug("pre-receive hook called", "repo", repo, "args", args)
30}
31
32// Update is called by the git update hook.
33//
34// It implements Hooks.
35func (d *Backend) Update(ctx context.Context, _ io.Writer, _ io.Writer, repo string, arg hooks.HookArg) {
36 d.logger.Debug("update hook called", "repo", repo, "arg", arg)
37
38 // Find user
39 var user proto.User
40 if pubkey := os.Getenv("SOFT_SERVE_PUBLIC_KEY"); pubkey != "" { //nolint:nestif
41 pk, _, err := sshutils.ParseAuthorizedKey(pubkey)
42 if err != nil {
43 d.logger.Error("error parsing public key", "err", err)
44 return
45 }
46
47 user, err = d.UserByPublicKey(ctx, pk)
48 if err != nil {
49 d.logger.Error("error finding user from public key", "key", pubkey, "err", err)
50 return
51 }
52 } else if username := os.Getenv("SOFT_SERVE_USERNAME"); username != "" {
53 var err error
54 user, err = d.User(ctx, username)
55 if err != nil {
56 d.logger.Error("error finding user from username", "username", username, "err", err)
57 return
58 }
59 } else {
60 d.logger.Error("error finding user")
61 return
62 }
63
64 // Get repo
65 r, err := d.Repository(ctx, repo)
66 if err != nil {
67 d.logger.Error("error finding repository", "repo", repo, "err", err)
68 return
69 }
70
71 // TODO: run this async
72 // This would probably need something like an RPC server to communicate with the hook process.
73 if git.IsZeroHash(arg.OldSha) || git.IsZeroHash(arg.NewSha) {
74 wh, err := webhook.NewBranchTagEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha)
75 if err != nil {
76 d.logger.Error("error creating branch_tag webhook", "err", err)
77 } else if err := webhook.SendEvent(ctx, wh); err != nil {
78 d.logger.Error("error sending branch_tag webhook", "err", err)
79 }
80 }
81 wh, err := webhook.NewPushEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha)
82 if err != nil {
83 d.logger.Error("error creating push webhook", "err", err)
84 } else if err := webhook.SendEvent(ctx, wh); err != nil {
85 d.logger.Error("error sending push webhook", "err", err)
86 }
87}
88
89// PostUpdate is called by the git post-update hook.
90//
91// It implements Hooks.
92func (d *Backend) PostUpdate(ctx context.Context, _ io.Writer, _ io.Writer, repo string, args ...string) {
93 d.logger.Debug("post-update hook called", "repo", repo, "args", args)
94
95 var wg sync.WaitGroup
96
97 // Populate last-modified file.
98 wg.Add(1)
99 go func() {
100 defer wg.Done()
101 if err := populateLastModified(ctx, d, repo); err != nil {
102 d.logger.Error("error populating last-modified", "repo", repo, "err", err)
103 return
104 }
105 }()
106
107 wg.Wait()
108}
109
110func populateLastModified(ctx context.Context, d *Backend, name string) error {
111 var rr *repo
112 _rr, err := d.Repository(ctx, name)
113 if err != nil {
114 return err
115 }
116
117 if r, ok := _rr.(*repo); ok {
118 rr = r
119 } else {
120 return proto.ErrRepoNotFound
121 }
122
123 r, err := rr.Open()
124 if err != nil {
125 return err
126 }
127
128 c, err := r.LatestCommitTime()
129 if err != nil {
130 return err //nolint:wrapcheck
131 }
132
133 return rr.writeLastModified(c)
134}