Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7eaf175

Browse files
committedAug 24, 2023
feat: implement server webhooks
- branch/tag event - collaborators event - push event - repository event Fixes: #148 Fixes: #56 Fixes: #49
1 parent 8241f61 commit 7eaf175

28 files changed

+1304
-15
lines changed
 

‎git/commit.go

+8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package git
22

33
import (
4+
"regexp"
5+
46
"github.com/gogs/git-module"
57
)
68

79
// ZeroHash is the zero hash.
810
var ZeroHash Hash = git.EmptyID
911

12+
// IsZeroHash returns whether the hash is a zero hash.
13+
func IsZeroHash(h Hash) bool {
14+
pattern := regexp.MustCompile(`^[0]+$`)
15+
return pattern.MatchString(h.String())
16+
}
17+
1018
// Hash represents a git hash.
1119
type Hash string
1220

‎go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ require (
2929
github.com/gobwas/glob v0.2.3
3030
github.com/gogs/git-module v1.8.3
3131
github.com/golang-jwt/jwt/v5 v5.0.0
32+
github.com/google/go-querystring v1.1.0
33+
github.com/google/uuid v1.3.0
3234
github.com/gorilla/handlers v1.5.1
3335
github.com/gorilla/mux v1.8.0
3436
github.com/hashicorp/golang-lru/v2 v2.0.5
@@ -64,7 +66,6 @@ require (
6466
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
6567
github.com/go-logfmt/logfmt v0.6.0 // indirect
6668
github.com/golang/protobuf v1.5.3 // indirect
67-
github.com/google/uuid v1.3.0 // indirect
6869
github.com/gorilla/css v1.0.0 // indirect
6970
github.com/inconshreveable/mousetrap v1.1.0 // indirect
7071
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect

‎go.sum

+3
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,11 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
7575
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
7676
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
7777
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
78+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7879
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7980
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
81+
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
82+
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
8083
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
8184
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
8285
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

‎server/backend/collab.go

+24-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"github.com/charmbracelet/soft-serve/server/access"
88
"github.com/charmbracelet/soft-serve/server/db"
99
"github.com/charmbracelet/soft-serve/server/db/models"
10+
"github.com/charmbracelet/soft-serve/server/proto"
1011
"github.com/charmbracelet/soft-serve/server/utils"
12+
"github.com/charmbracelet/soft-serve/server/webhook"
1113
)
1214

1315
// AddCollaborator adds a collaborator to a repository.
@@ -20,11 +22,20 @@ func (d *Backend) AddCollaborator(ctx context.Context, repo string, username str
2022
}
2123

2224
repo = utils.SanitizeRepo(repo)
23-
return db.WrapError(
25+
r, err := d.Repository(ctx, repo)
26+
if err != nil {
27+
return err
28+
}
29+
30+
if err := db.WrapError(
2431
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
2532
return d.store.AddCollabByUsernameAndRepo(ctx, tx, username, repo, level)
2633
}),
27-
)
34+
); err != nil {
35+
return err
36+
}
37+
38+
return webhook.SendCollaboratorEvent(ctx, proto.UserFromContext(ctx), r, username, webhook.CollaboratorEventAdded)
2839
}
2940

3041
// Collaborators returns a list of collaborators for a repository.
@@ -75,9 +86,18 @@ func (d *Backend) IsCollaborator(ctx context.Context, repo string, username stri
7586
// It implements backend.Backend.
7687
func (d *Backend) RemoveCollaborator(ctx context.Context, repo string, username string) error {
7788
repo = utils.SanitizeRepo(repo)
78-
return db.WrapError(
89+
r, err := d.Repository(ctx, repo)
90+
if err != nil {
91+
return err
92+
}
93+
94+
if err := db.WrapError(
7995
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
8096
return d.store.RemoveCollabByUsernameAndRepo(ctx, tx, username, repo)
8197
}),
82-
)
98+
); err != nil {
99+
return err
100+
}
101+
102+
return webhook.SendCollaboratorEvent(ctx, proto.UserFromContext(ctx), r, username, webhook.CollaboratorEventRemoved)
83103
}

‎server/backend/hooks.go

+49-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package backend
33
import (
44
"context"
55
"io"
6+
"os"
67
"sync"
78

9+
"github.com/charmbracelet/soft-serve/git"
810
"github.com/charmbracelet/soft-serve/server/hooks"
911
"github.com/charmbracelet/soft-serve/server/proto"
12+
"github.com/charmbracelet/soft-serve/server/sshutils"
13+
"github.com/charmbracelet/soft-serve/server/webhook"
1014
)
1115

1216
var _ hooks.Hooks = (*Backend)(nil)
@@ -28,8 +32,52 @@ func (d *Backend) PreReceive(_ context.Context, _ io.Writer, _ io.Writer, repo s
2832
// Update is called by the git update hook.
2933
//
3034
// It implements Hooks.
31-
func (d *Backend) Update(_ context.Context, _ io.Writer, _ io.Writer, repo string, arg hooks.HookArg) {
35+
func (d *Backend) Update(ctx context.Context, _ io.Writer, _ io.Writer, repo string, arg hooks.HookArg) {
3236
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 != "" {
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(git.Hash(arg.OldSha)) || git.IsZeroHash(git.Hash(arg.NewSha)) {
74+
if err := webhook.SendBranchTagEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha); err != nil {
75+
d.logger.Error("error sending branch_tag webhook", "err", err)
76+
}
77+
}
78+
if err := webhook.SendPushEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha); err != nil {
79+
d.logger.Error("error sending push webhook", "err", err)
80+
}
3381
}
3482

3583
// PostUpdate is called by the git post-update hook.

‎server/backend/repo.go

+63-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/charmbracelet/soft-serve/server/storage"
2222
"github.com/charmbracelet/soft-serve/server/task"
2323
"github.com/charmbracelet/soft-serve/server/utils"
24+
"github.com/charmbracelet/soft-serve/server/webhook"
2425
)
2526

2627
func (d *Backend) reposPath() string {
@@ -88,7 +89,16 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.
8889
return nil, err
8990
}
9091

91-
return d.Repository(ctx, name)
92+
r, err := d.Repository(ctx, name)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
if err := webhook.SendRepositoryEvent(ctx, user, r, webhook.RepositoryEventCreated); err != nil {
98+
return r, err
99+
}
100+
101+
return r, nil
92102
}
93103

94104
// ImportRepository imports a repository from remote.
@@ -197,7 +207,7 @@ func (d *Backend) ImportRepository(_ context.Context, name string, user proto.Us
197207
return err
198208
}
199209

200-
return nil
210+
return webhook.SendRepositoryEvent(ctx, user, r, webhook.RepositoryEventImported)
201211
})
202212

203213
go func() {
@@ -262,12 +272,18 @@ func (d *Backend) DeleteRepository(ctx context.Context, name string) error {
262272
return proto.ErrRepoNotFound
263273
}
264274

265-
return err
275+
user := proto.UserFromContext(ctx)
276+
r, err := d.Repository(ctx, name)
277+
if err != nil {
278+
return err
279+
}
280+
281+
return webhook.SendRepositoryEvent(ctx, user, r, webhook.RepositoryEventDeleted)
266282
}
267283

268284
// DeleteUserRepositories deletes all user repositories.
269285
func (d *Backend) DeleteUserRepositories(ctx context.Context, username string) error {
270-
return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
286+
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
271287
user, err := d.store.FindUserByUsername(ctx, tx, username)
272288
if err != nil {
273289
return err
@@ -285,7 +301,11 @@ func (d *Backend) DeleteUserRepositories(ctx context.Context, username string) e
285301
}
286302

287303
return nil
288-
})
304+
}); err != nil {
305+
return db.WrapError(err)
306+
}
307+
308+
return nil
289309
}
290310

291311
// RenameRepository renames a repository.
@@ -301,6 +321,11 @@ func (d *Backend) RenameRepository(ctx context.Context, oldName string, newName
301321
if err := utils.ValidateRepo(newName); err != nil {
302322
return err
303323
}
324+
325+
if oldName == newName {
326+
return nil
327+
}
328+
304329
oldRepo := oldName + ".git"
305330
newRepo := newName + ".git"
306331
op := filepath.Join(d.reposPath(), oldRepo)
@@ -331,6 +356,16 @@ func (d *Backend) RenameRepository(ctx context.Context, oldName string, newName
331356
return db.WrapError(err)
332357
}
333358

359+
user := proto.UserFromContext(ctx)
360+
repo, err := d.Repository(ctx, newName)
361+
if err != nil {
362+
return err
363+
}
364+
365+
if err := webhook.SendRepositoryEvent(ctx, user, repo, webhook.RepositoryEventRenamed); err != nil {
366+
return err
367+
}
368+
334369
return nil
335370
}
336371

@@ -537,7 +572,7 @@ func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) err
537572
// Delete cache
538573
d.cache.Delete(name)
539574

540-
return db.WrapError(
575+
if err := db.WrapError(
541576
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
542577
fp := filepath.Join(rp, "git-daemon-export-ok")
543578
if !private {
@@ -556,7 +591,23 @@ func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) err
556591

557592
return d.store.SetRepoIsPrivateByName(ctx, tx, name, private)
558593
}),
559-
)
594+
); err != nil {
595+
return err
596+
}
597+
598+
user := proto.UserFromContext(ctx)
599+
repo, err := d.Repository(ctx, name)
600+
if err != nil {
601+
return err
602+
}
603+
604+
if repo.IsPrivate() != !private {
605+
if err := webhook.SendRepositoryEvent(ctx, user, repo, webhook.RepositoryEventVisibilityChanged); err != nil {
606+
return err
607+
}
608+
}
609+
610+
return nil
560611
}
561612

562613
// SetProjectName sets the project name of a repository.
@@ -651,6 +702,11 @@ func (r *repo) IsHidden() bool {
651702
return r.repo.Hidden
652703
}
653704

705+
// CreatedAt returns the repository's creation time.
706+
func (r *repo) CreatedAt() time.Time {
707+
return r.repo.CreatedAt
708+
}
709+
654710
// UpdatedAt returns the repository's last update time.
655711
func (r *repo) UpdatedAt() time.Time {
656712
// Try to read the last modified time from the info directory.

‎server/config/config.go

+5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type GitConfig struct {
4040
// ListenAddr is the address on which the Git daemon will listen.
4141
ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
4242

43+
// PublicURL is the public URL of the Git daemon server.
44+
PublicURL string `env:"PUBLIC_URL" yaml:"public_url"`
45+
4346
// MaxTimeout is the maximum number of seconds a connection can take.
4447
MaxTimeout int `env:"MAX_TIMEOUT" yaml:"max_timeout"`
4548

@@ -157,6 +160,7 @@ func (c *Config) Environ() []string {
157160
fmt.Sprintf("SOFT_SERVE_SSH_MAX_TIMEOUT=%d", c.SSH.MaxTimeout),
158161
fmt.Sprintf("SOFT_SERVE_SSH_IDLE_TIMEOUT=%d", c.SSH.IdleTimeout),
159162
fmt.Sprintf("SOFT_SERVE_GIT_LISTEN_ADDR=%s", c.Git.ListenAddr),
163+
fmt.Sprintf("SOFT_SERVE_GIT_PUBLIC_URL=%s", c.Git.PublicURL),
160164
fmt.Sprintf("SOFT_SERVE_GIT_MAX_TIMEOUT=%d", c.Git.MaxTimeout),
161165
fmt.Sprintf("SOFT_SERVE_GIT_IDLE_TIMEOUT=%d", c.Git.IdleTimeout),
162166
fmt.Sprintf("SOFT_SERVE_GIT_MAX_CONNECTIONS=%d", c.Git.MaxConnections),
@@ -304,6 +308,7 @@ func DefaultConfig() *Config {
304308
},
305309
Git: GitConfig{
306310
ListenAddr: ":9418",
311+
PublicURL: "git://localhost",
307312
MaxTimeout: 0,
308313
IdleTimeout: 3,
309314
MaxConnections: 32,

‎server/config/file.go

+4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ git:
5050
# The address on which the Git daemon will listen.
5151
listen_addr: "{{ .Git.ListenAddr }}"
5252
53+
# The public URL of the Git daemon server.
54+
# This is the address that will be used to clone repositories.
55+
public_url: "{{ .Git.PublicURL }}"
56+
5357
# The maximum number of seconds a connection can take.
5458
# A value of 0 means no timeout.
5559
max_timeout: {{ .Git.MaxTimeout }}

‎server/db/migrate/0002_webhooks.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package migrate
2+
3+
import (
4+
"context"
5+
6+
"github.com/charmbracelet/soft-serve/server/db"
7+
)
8+
9+
const (
10+
webhooksName = "webhooks"
11+
webhooksVersion = 2
12+
)
13+
14+
var webhooks = Migration{
15+
Name: webhooksName,
16+
Version: webhooksVersion,
17+
Migrate: func(ctx context.Context, tx *db.Tx) error {
18+
return migrateUp(ctx, tx, webhooksVersion, webhooksName)
19+
},
20+
Rollback: func(ctx context.Context, tx *db.Tx) error {
21+
return migrateDown(ctx, tx, webhooksVersion, webhooksName)
22+
},
23+
}

‎server/db/migrate/0002_webhooks_postgres.down.sql

Whitespace-only changes.

0 commit comments

Comments
 (0)
Please sign in to comment.