227f178ad2ca13ae909065cce734d77172664930
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/cmd/soft/hook.go b/cmd/soft/hook.go
2new file mode 100644
3index 0000000000000000000000000000000000000000..ec51aac5010744696e3c03a9c10bd0f9ee763a14
4--- /dev/null
5+++ b/cmd/soft/hook.go
6@@ -0,0 +1,215 @@
7+package main
8+
9+import (
10+ "bufio"
11+ "fmt"
12+ "os"
13+ "path/filepath"
14+ "strings"
15+
16+ "github.com/charmbracelet/keygen"
17+ "github.com/charmbracelet/soft-serve/server/config"
18+ "github.com/spf13/cobra"
19+ gossh "golang.org/x/crypto/ssh"
20+)
21+
22+var (
23+ configPath string
24+
25+ hookCmd = &cobra.Command{
26+ Use: "hook",
27+ Short: "Run git server hooks",
28+ Long: "Handles git server hooks. This includes pre-receive, update, and post-receive.",
29+ Hidden: true,
30+ }
31+
32+ preReceiveCmd = &cobra.Command{
33+ Use: "pre-receive",
34+ Short: "Run git pre-receive hook",
35+ RunE: func(cmd *cobra.Command, args []string) error {
36+ c, s, err := commonInit()
37+ if err != nil {
38+ return err
39+ }
40+ defer c.Close() //nolint:errcheck
41+ defer s.Close() //nolint:errcheck
42+ in, err := s.StdinPipe()
43+ if err != nil {
44+ return err
45+ }
46+ scanner := bufio.NewScanner(os.Stdin)
47+ for scanner.Scan() {
48+ in.Write([]byte(scanner.Text()))
49+ in.Write([]byte("\n"))
50+ }
51+ in.Close() //nolint:errcheck
52+ b, err := s.Output("hook pre-receive")
53+ if err != nil {
54+ return err
55+ }
56+ cmd.Print(string(b))
57+ return nil
58+ },
59+ }
60+
61+ updateCmd = &cobra.Command{
62+ Use: "update",
63+ Short: "Run git update hook",
64+ Args: cobra.ExactArgs(3),
65+ RunE: func(cmd *cobra.Command, args []string) error {
66+ refName := args[0]
67+ oldSha := args[1]
68+ newSha := args[2]
69+ c, s, err := commonInit()
70+ if err != nil {
71+ return err
72+ }
73+ defer c.Close() //nolint:errcheck
74+ defer s.Close() //nolint:errcheck
75+ b, err := s.Output(fmt.Sprintf("hook update %s %s %s", refName, oldSha, newSha))
76+ if err != nil {
77+ return err
78+ }
79+ cmd.Print(string(b))
80+ return nil
81+ },
82+ }
83+
84+ postReceiveCmd = &cobra.Command{
85+ Use: "post-receive",
86+ Short: "Run git post-receive hook",
87+ RunE: func(cmd *cobra.Command, args []string) error {
88+ c, s, err := commonInit()
89+ if err != nil {
90+ return err
91+ }
92+ defer c.Close() //nolint:errcheck
93+ defer s.Close() //nolint:errcheck
94+ in, err := s.StdinPipe()
95+ if err != nil {
96+ return err
97+ }
98+ scanner := bufio.NewScanner(os.Stdin)
99+ for scanner.Scan() {
100+ in.Write([]byte(scanner.Text()))
101+ in.Write([]byte("\n"))
102+ }
103+ in.Close() //nolint:errcheck
104+ b, err := s.Output("hook post-receive")
105+ if err != nil {
106diff --git a/cmd/soft/root.go b/cmd/soft/root.go
107index 1f4cbeacdff6e3eb2c343f4048b79f1358eb8983..fe7cb6540a8028bda78d65df29b1a1bf46b02071 100644
108--- a/cmd/soft/root.go
109+++ b/cmd/soft/root.go
110@@ -31,6 +31,7 @@ func init() {
111 rootCmd.AddCommand(
112 serveCmd,
113 manCmd,
114+ hookCmd,
115 )
116 rootCmd.CompletionOptions.HiddenDefaultCmd = true
117
118diff --git a/go.mod b/go.mod
119index f2088542e92c527d1979083e70d2c4994907d09f..ea287b8a72ecb35b94c9858d32a5504ade2c8c1e 100644
120--- a/go.mod
121+++ b/go.mod
122@@ -32,6 +32,7 @@ require (
123 goji.io v2.0.2+incompatible
124 golang.org/x/crypto v0.7.0
125 golang.org/x/sync v0.1.0
126+ gopkg.in/yaml.v3 v3.0.1
127 )
128
129 require (
130diff --git a/server/backend/file/file.go b/server/backend/file/file.go
131index aa0465b3f44b64c8d7f04f63ccab5ab8cfcf1e55..9f855db570573db36b0b313e8ce6addf49d877a6 100644
132--- a/server/backend/file/file.go
133+++ b/server/backend/file/file.go
134@@ -20,8 +20,10 @@ package file
135
136 import (
137 "bufio"
138+ "bytes"
139 "errors"
140 "fmt"
141+ "html/template"
142 "io"
143 "io/fs"
144 "os"
145@@ -584,18 +586,31 @@ func (fb *FileBackend) CreateRepository(repo string, private bool) (backend.Repo
146 return nil, os.ErrExist
147 }
148
149- if _, err := git.Init(rp, true); err != nil {
150+ rr, err := git.Init(rp, true)
151+ if err != nil {
152 logger.Debug("failed to create repository", "err", err)
153 return nil, err
154 }
155
156- fb.SetPrivate(repo, private)
157- fb.SetDescription(repo, "")
158+ if err := rr.UpdateServerInfo(); err != nil {
159+ logger.Debug("failed to update server info", "err", err)
160+ return nil, err
161+ }
162+
163+ if err := fb.SetPrivate(repo, private); err != nil {
164+ logger.Debug("failed to set private status", "err", err)
165+ return nil, err
166+ }
167+
168+ if err := fb.SetDescription(repo, ""); err != nil {
169+ logger.Debug("failed to set description", "err", err)
170+ return nil, err
171+ }
172
173 r := &Repo{path: rp, root: fb.reposPath()}
174 // Add to cache.
175 fb.repos[name] = r
176- return r, nil
177+ return r, fb.InitializeHooks(name)
178 }
179
180 // DeleteRepository deletes the given repository.
181@@ -687,6 +702,9 @@ func (fb *FileBackend) initRepos() error {
182 r := &Repo{path: path, root: fb.reposPath()}
183 fb.repos[r.Name()] = r
184 repos = append(repos, r)
185+ if err := fb.InitializeHooks(r.Name()); err != nil {
186+ logger.Warn("failed to initialize hooks", "err", err, "repo", r.Name())
187+ }
188 }
189
190 return nil
191@@ -709,3 +727,156 @@ func (fb *FileBackend) Repositories() ([]backend.Repository, error) {
192
193 return repos, nil
194 }
195+
196+var (
197+ hookNames = []string{"pre-receive", "update", "post-update", "post-receive"}
198+ hookTpls = []string{
199+ // for pre-receive
200+ `#!/usr/bin/env bash
201+# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
202+data=$(cat)
203+exitcodes=""
204+hookname=$(basename $0)
205+GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
206+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
207+ test -x "${hook}" && test -f "${hook}" || continue
208+ echo "${data}" | "${hook}"
209+ exitcodes="${exitcodes} $?"
210+done
211+for i in ${exitcodes}; do
212+ [ ${i} -eq 0 ] || exit ${i}
213+done
214+`,
215+
216+ // for update
217+ `#!/usr/bin/env bash
218+# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
219+exitcodes=""
220+hookname=$(basename $0)
221+GIT_DIR=${GIT_DIR:-$(dirname $0/..)}
222+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
223+ test -x "${hook}" && test -f "${hook}" || continue
224+ "${hook}" $1 $2 $3
225+ exitcodes="${exitcodes} $?"
226+done
227+for i in ${exitcodes}; do
228+ [ ${i} -eq 0 ] || exit ${i}
229+done
230+`,
231+
232+ // for post-update
233+ `#!/usr/bin/env bash
234diff --git a/server/cmd/blob.go b/server/cmd/blob.go
235index 58e2361f9342c32d1c482b8b2f5ad2031104c00f..187ee07b21d60a804ebde4c19af408e4b3ee30b9 100644
236--- a/server/cmd/blob.go
237+++ b/server/cmd/blob.go
238@@ -30,7 +30,7 @@ func blobCommand() *cobra.Command {
239 cmd := &cobra.Command{
240 Use: "blob REPOSITORY [REFERENCE] [PATH]",
241 Aliases: []string{"cat", "show"},
242- Short: "Print out the contents of file at path.",
243+ Short: "Print out the contents of file at path",
244 Args: cobra.RangeArgs(1, 3),
245 PersistentPreRunE: checkIfReadable,
246 RunE: func(cmd *cobra.Command, args []string) error {
247diff --git a/server/cmd/cmd.go b/server/cmd/cmd.go
248index a23ee8afdb5c752b710b576b579aeec1fdbb5b18..461a9041f126e3526dfb0fed2f5f7bce7cc0d914 100644
249--- a/server/cmd/cmd.go
250+++ b/server/cmd/cmd.go
251@@ -4,8 +4,10 @@ import (
252 "context"
253 "fmt"
254
255+ "github.com/charmbracelet/log"
256 "github.com/charmbracelet/soft-serve/server/backend"
257 "github.com/charmbracelet/soft-serve/server/config"
258+ "github.com/charmbracelet/soft-serve/server/hooks"
259 "github.com/charmbracelet/soft-serve/server/utils"
260 "github.com/charmbracelet/ssh"
261 "github.com/charmbracelet/wish"
262@@ -25,6 +27,8 @@ var (
263 ConfigCtxKey = ContextKey("config")
264 // SessionCtxKey is the key for the session in the context.
265 SessionCtxKey = ContextKey("session")
266+ // HooksCtxKey is the key for the git hooks in the context.
267+ HooksCtxKey = ContextKey("hooks")
268 )
269
270 var (
271@@ -36,6 +40,10 @@ var (
272 ErrFileNotFound = fmt.Errorf("File not found")
273 )
274
275+var (
276+ logger = log.WithPrefix("server.cmd")
277+)
278+
279 // rootCommand is the root command for the server.
280 func rootCommand() *cobra.Command {
281 rootCmd := &cobra.Command{
282@@ -47,15 +55,17 @@ func rootCommand() *cobra.Command {
283 rootCmd.CompletionOptions.DisableDefaultCmd = true
284 rootCmd.AddCommand(
285 adminCommand(),
286+ blobCommand(),
287 branchCommand(),
288 collabCommand(),
289 createCommand(),
290 deleteCommand(),
291 descriptionCommand(),
292+ hookCommand(),
293 listCommand(),
294 privateCommand(),
295 renameCommand(),
296- blobCommand(),
297+ settingCommand(),
298 tagCommand(),
299 treeCommand(),
300 )
301@@ -107,7 +117,7 @@ func checkIfCollab(cmd *cobra.Command, args []string) error {
302 }
303
304 // Middleware is the Soft Serve middleware that handles SSH commands.
305-func Middleware(cfg *config.Config) wish.Middleware {
306+func Middleware(cfg *config.Config, hooks hooks.Hooks) wish.Middleware {
307 return func(sh ssh.Handler) ssh.Handler {
308 return func(s ssh.Session) {
309 func() {
310@@ -128,6 +138,7 @@ func Middleware(cfg *config.Config) wish.Middleware {
311
312 ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
313 ctx = context.WithValue(ctx, SessionCtxKey, s)
314+ ctx = context.WithValue(ctx, HooksCtxKey, hooks)
315
316 rootCmd := rootCommand()
317 rootCmd.SetArgs(args)
318diff --git a/server/cmd/create.go b/server/cmd/create.go
319index 89c261d12bfa8ef3cb6b8bc652652a3158391586..d7cfdb04da9c1e5371bea6207c14308b40cacea2 100644
320--- a/server/cmd/create.go
321+++ b/server/cmd/create.go
322@@ -8,7 +8,7 @@ func createCommand() *cobra.Command {
323 var description string
324 cmd := &cobra.Command{
325 Use: "create REPOSITORY",
326- Short: "Create a new repository.",
327+ Short: "Create a new repository",
328 Args: cobra.ExactArgs(1),
329 PersistentPreRunE: checkIfAdmin,
330 RunE: func(cmd *cobra.Command, args []string) error {
331diff --git a/server/cmd/delete.go b/server/cmd/delete.go
332index 8dd94a64552f34620d801fe04a303aa88e24505b..7c335ac6c403e6ec46eedbb9aa6d8177692a6be3 100644
333--- a/server/cmd/delete.go
334+++ b/server/cmd/delete.go
335@@ -6,7 +6,7 @@ func deleteCommand() *cobra.Command {
336 cmd := &cobra.Command{
337 Use: "delete REPOSITORY",
338 Aliases: []string{"del", "remove", "rm"},
339- Short: "Delete a repository.",
340+ Short: "Delete a repository",
341 Args: cobra.ExactArgs(1),
342 PersistentPreRunE: checkIfAdmin,
343 RunE: func(cmd *cobra.Command, args []string) error {
344diff --git a/server/cmd/description.go b/server/cmd/description.go
345index 2eb59c5ebafa4628063374547390d352c7a704ea..bafabbfe8574904da2560442e541d80208550829 100644
346--- a/server/cmd/description.go
347+++ b/server/cmd/description.go
348@@ -10,7 +10,7 @@ func descriptionCommand() *cobra.Command {
349 cmd := &cobra.Command{
350 Use: "description REPOSITORY [DESCRIPTION]",
351 Aliases: []string{"desc"},
352- Short: "Set or get the description for a repository.",
353+ Short: "Set or get the description for a repository",
354 Args: cobra.MinimumNArgs(1),
355 RunE: func(cmd *cobra.Command, args []string) error {
356 cfg, _ := fromContext(cmd)
357diff --git a/server/cmd/hook.go b/server/cmd/hook.go
358new file mode 100644
359index 0000000000000000000000000000000000000000..afd4ae2f19d5c2ae8f5387a631e51ac4608edb92
360--- /dev/null
361+++ b/server/cmd/hook.go
362@@ -0,0 +1,145 @@
363+package cmd
364+
365+import (
366+ "bufio"
367+ "fmt"
368+ "path/filepath"
369+ "strings"
370+
371+ "github.com/charmbracelet/keygen"
372+ "github.com/charmbracelet/soft-serve/server/hooks"
373+ "github.com/charmbracelet/ssh"
374+ "github.com/spf13/cobra"
375+ gossh "golang.org/x/crypto/ssh"
376+)
377+
378+// hookCommand handles Soft Serve internal API git hook requests.
379+func hookCommand() *cobra.Command {
380+ preReceiveCmd := &cobra.Command{
381+ Use: "pre-receive",
382+ Short: "Run git pre-receive hook",
383+ PersistentPreRunE: checkIfInternal,
384+ RunE: func(cmd *cobra.Command, args []string) error {
385+ _, s := fromContext(cmd)
386+ hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
387+ repoName := getRepoName(s)
388+ opts := make([]hooks.HookArg, 0)
389+ scanner := bufio.NewScanner(s)
390+ for scanner.Scan() {
391+ fields := strings.Fields(scanner.Text())
392+ if len(fields) != 3 {
393+ return fmt.Errorf("invalid pre-receive hook input: %s", scanner.Text())
394+ }
395+ opts = append(opts, hooks.HookArg{
396+ OldSha: fields[0],
397+ NewSha: fields[1],
398+ RefName: fields[2],
399+ })
400+ }
401+ hks.PreReceive(s, s.Stderr(), repoName, opts)
402+ return nil
403+ },
404+ }
405+
406+ updateCmd := &cobra.Command{
407+ Use: "update",
408+ Short: "Run git update hook",
409+ Args: cobra.ExactArgs(3),
410+ PersistentPreRunE: checkIfInternal,
411+ RunE: func(cmd *cobra.Command, args []string) error {
412+ _, s := fromContext(cmd)
413+ hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
414+ repoName := getRepoName(s)
415+ hks.Update(s, s.Stderr(), repoName, hooks.HookArg{
416+ RefName: args[0],
417+ OldSha: args[1],
418+ NewSha: args[2],
419+ })
420+ return nil
421+ },
422+ }
423+
424+ postReceiveCmd := &cobra.Command{
425+ Use: "post-receive",
426+ Short: "Run git post-receive hook",
427+ PersistentPreRunE: checkIfInternal,
428+ RunE: func(cmd *cobra.Command, _ []string) error {
429+ _, s := fromContext(cmd)
430+ hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
431+ repoName := getRepoName(s)
432+ opts := make([]hooks.HookArg, 0)
433+ scanner := bufio.NewScanner(s)
434+ for scanner.Scan() {
435+ fields := strings.Fields(scanner.Text())
436+ if len(fields) != 3 {
437+ return fmt.Errorf("invalid post-receive hook input: %s", scanner.Text())
438+ }
439+ opts = append(opts, hooks.HookArg{
440+ OldSha: fields[0],
441+ NewSha: fields[1],
442+ RefName: fields[2],
443+ })
444+ }
445+ hks.PostReceive(s, s.Stderr(), repoName, opts)
446+ return nil
447+ },
448+ }
449+
450+ postUpdateCmd := &cobra.Command{
451+ Use: "post-update",
452+ Short: "Run git post-update hook",
453+ PersistentPreRunE: checkIfInternal,
454+ RunE: func(cmd *cobra.Command, args []string) error {
455+ _, s := fromContext(cmd)
456+ hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
457+ repoName := getRepoName(s)
458+ hks.PostUpdate(s, s.Stderr(), repoName, args...)
459+ return nil
460+ },
461+ }
462diff --git a/server/cmd/list.go b/server/cmd/list.go
463index 8ed558a15cdfc55d8c9cd19889b03a258c8988d4..7b6626c56ea0b7d1a472f944318620089d50610f 100644
464--- a/server/cmd/list.go
465+++ b/server/cmd/list.go
466@@ -10,7 +10,7 @@ func listCommand() *cobra.Command {
467 listCmd := &cobra.Command{
468 Use: "list",
469 Aliases: []string{"ls"},
470- Short: "List repositories.",
471+ Short: "List repositories",
472 Args: cobra.NoArgs,
473 RunE: func(cmd *cobra.Command, args []string) error {
474 cfg, s := fromContext(cmd)
475diff --git a/server/cmd/private.go b/server/cmd/private.go
476index 593845dfb81b7787047eef73b6a1053021c2750b..83e8e77ca72cfc022ad74c49d58b1268fc930e63 100644
477--- a/server/cmd/private.go
478+++ b/server/cmd/private.go
479@@ -10,7 +10,7 @@ import (
480 func privateCommand() *cobra.Command {
481 cmd := &cobra.Command{
482 Use: "private REPOSITORY [true|false]",
483- Short: "Set or get a repository private property.",
484+ Short: "Set or get a repository private property",
485 Args: cobra.RangeArgs(1, 2),
486 RunE: func(cmd *cobra.Command, args []string) error {
487 cfg, _ := fromContext(cmd)
488diff --git a/server/cmd/rename.go b/server/cmd/rename.go
489index a5b88d83ac7f654ab3cab13fb90571ca1fd39cbe..d3ab7b0ac8c52379f886b79a3826146550beba82 100644
490--- a/server/cmd/rename.go
491+++ b/server/cmd/rename.go
492@@ -5,7 +5,8 @@ import "github.com/spf13/cobra"
493 func renameCommand() *cobra.Command {
494 cmd := &cobra.Command{
495 Use: "rename REPOSITORY NEW_NAME",
496- Short: "Rename an existing repository.",
497+ Aliases: []string{"mv", "move"},
498+ Short: "Rename an existing repository",
499 Args: cobra.ExactArgs(2),
500 PersistentPreRunE: checkIfCollab,
501 RunE: func(cmd *cobra.Command, args []string) error {
502diff --git a/server/cmd/setting.go b/server/cmd/setting.go
503index 8faea89c5c2d7a001b5c59d28791f226c66bcab6..eaa55942ef0884abef3cd0b24a96f02b53cd1a7c 100644
504--- a/server/cmd/setting.go
505+++ b/server/cmd/setting.go
506@@ -11,7 +11,7 @@ import (
507 func settingCommand() *cobra.Command {
508 cmd := &cobra.Command{
509 Use: "setting",
510- Short: "Manage settings",
511+ Short: "Manage server settings",
512 }
513
514 cmd.AddCommand(
515@@ -37,12 +37,13 @@ func settingCommand() *cobra.Command {
516 },
517 )
518
519+ als := []string{backend.NoAccess.String(), backend.ReadOnlyAccess.String(), backend.ReadWriteAccess.String(), backend.AdminAccess.String()}
520 cmd.AddCommand(
521 &cobra.Command{
522 Use: "anon-access [ACCESS_LEVEL]",
523 Short: "Set or get the default access level for anonymous users",
524 Args: cobra.RangeArgs(0, 1),
525- ValidArgs: []string{backend.NoAccess.String(), backend.ReadOnlyAccess.String(), backend.ReadWriteAccess.String(), backend.AdminAccess.String()},
526+ ValidArgs: als,
527 PersistentPreRunE: checkIfAdmin,
528 RunE: func(cmd *cobra.Command, args []string) error {
529 cfg, _ := fromContext(cmd)
530@@ -52,7 +53,7 @@ func settingCommand() *cobra.Command {
531 case 1:
532 al := backend.ParseAccessLevel(args[0])
533 if al < 0 {
534- return fmt.Errorf("invalid access level: %s", args[0])
535+ return fmt.Errorf("invalid access level: %s. Please choose one of the following: %s", args[0], als)
536 }
537 if err := cfg.Backend.SetAnonAccess(al); err != nil {
538 return err
539diff --git a/server/cmd/tree.go b/server/cmd/tree.go
540index 43be304c6a1eb640ed7748d1408ad6b1fdb5b128..0ca08a922d90d3a2dd88631454d15411d841a80a 100644
541--- a/server/cmd/tree.go
542+++ b/server/cmd/tree.go
543@@ -12,7 +12,7 @@ import (
544 func treeCommand() *cobra.Command {
545 cmd := &cobra.Command{
546 Use: "tree REPOSITORY [REFERENCE] [PATH]",
547- Short: "Print repository tree at path.",
548+ Short: "Print repository tree at path",
549 Args: cobra.RangeArgs(1, 3),
550 PersistentPreRunE: checkIfReadable,
551 RunE: func(cmd *cobra.Command, args []string) error {
552diff --git a/server/daemon.go b/server/daemon.go
553index 86ad93e4167f908198a177fd176b9c79079db47d..6424988d75d361ab9b715526942face540ce00d5 100644
554--- a/server/daemon.go
555+++ b/server/daemon.go
556@@ -4,7 +4,6 @@ import (
557 "bytes"
558 "context"
559 "fmt"
560- "io"
561 "net"
562 "path/filepath"
563 "sync"
564@@ -129,7 +128,7 @@ func (d *GitDaemon) Start() error {
565 }
566
567 func fatal(c net.Conn, err error) {
568- WritePktline(c, err)
569+ writePktline(c, err)
570 if err := c.Close(); err != nil {
571 logger.Debugf("git: error closing connection: %v", err)
572 }
573@@ -184,13 +183,13 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
574 return
575 }
576
577- var gitPack func(io.Reader, io.Writer, io.Writer, string) error
578+ gitPack := uploadPack
579 cmd := string(split[0])
580 switch cmd {
581- case UploadPackBin:
582- gitPack = UploadPack
583- case UploadArchiveBin:
584- gitPack = UploadArchive
585+ case uploadPackBin:
586+ gitPack = uploadPack
587+ case uploadArchiveBin:
588+ gitPack = uploadArchive
589 default:
590 fatal(c, ErrInvalidRequest)
591 return
592diff --git a/server/daemon_test.go b/server/daemon_test.go
593index 220624000d8cd733a1686f0bcb38214376059db3..d7816537297e1c516dc3e95e3ab2bbbc034a56ac 100644
594--- a/server/daemon_test.go
595+++ b/server/daemon_test.go
596@@ -35,7 +35,7 @@ func TestMain(m *testing.M) {
597 if err != nil {
598 log.Fatal(err)
599 }
600- cfg := config.DefaultConfig().WithBackend(fb).WithAccessMethod(fb)
601+ cfg := config.DefaultConfig().WithBackend(fb)
602 d, err := NewGitDaemon(cfg)
603 if err != nil {
604 log.Fatal(err)
605diff --git a/server/git.go b/server/git.go
606index 1748f1ec4d7a1fcc6579a1b0ab7a1b6f4b726d2e..41a1609f1710051b7b1f46ebe0a789386c59a54d 100644
607--- a/server/git.go
608+++ b/server/git.go
609@@ -36,13 +36,13 @@ var (
610
611 // Git protocol commands.
612 const (
613- ReceivePackBin = "git-receive-pack"
614- UploadPackBin = "git-upload-pack"
615- UploadArchiveBin = "git-upload-archive"
616+ receivePackBin = "git-receive-pack"
617+ uploadPackBin = "git-upload-pack"
618+ uploadArchiveBin = "git-upload-archive"
619 )
620
621-// UploadPack runs the git upload-pack protocol against the provided repo.
622-func UploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
623+// uploadPack runs the git upload-pack protocol against the provided repo.
624+func uploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
625 exists, err := fileExists(repoDir)
626 if !exists {
627 return ErrInvalidRepo
628@@ -50,11 +50,11 @@ func UploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error
629 if err != nil {
630 return err
631 }
632- return RunGit(in, out, er, "", UploadPackBin[4:], repoDir)
633+ return runGit(in, out, er, "", uploadPackBin[4:], repoDir)
634 }
635
636-// UploadArchive runs the git upload-archive protocol against the provided repo.
637-func UploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
638+// uploadArchive runs the git upload-archive protocol against the provided repo.
639+func uploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
640 exists, err := fileExists(repoDir)
641 if !exists {
642 return ErrInvalidRepo
643@@ -62,22 +62,19 @@ func UploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) er
644 if err != nil {
645 return err
646 }
647- return RunGit(in, out, er, "", UploadArchiveBin[4:], repoDir)
648+ return runGit(in, out, er, "", uploadArchiveBin[4:], repoDir)
649 }
650
651-// ReceivePack runs the git receive-pack protocol against the provided repo.
652-func ReceivePack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
653- if err := ensureRepo(repoDir, ""); err != nil {
654- return err
655- }
656- if err := RunGit(in, out, er, "", ReceivePackBin[4:], repoDir); err != nil {
657+// receivePack runs the git receive-pack protocol against the provided repo.
658+func receivePack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
659+ if err := runGit(in, out, er, "", receivePackBin[4:], repoDir); err != nil {
660 return err
661 }
662 return ensureDefaultBranch(in, out, er, repoDir)
663 }
664
665-// RunGit runs a git command in the given repo.
666-func RunGit(in io.Reader, out io.Writer, err io.Writer, dir string, args ...string) error {
667+// runGit runs a git command in the given repo.
668+func runGit(in io.Reader, out io.Writer, err io.Writer, dir string, args ...string) error {
669 c := git.NewCommand(args...)
670 return c.RunInDirWithOptions(dir, git.RunInDirOptions{
671 Stdin: in,
672@@ -86,8 +83,8 @@ func RunGit(in io.Reader, out io.Writer, err io.Writer, dir string, args ...stri
673 })
674 }
675
676-// WritePktline encodes and writes a pktline to the given writer.
677-func WritePktline(w io.Writer, v ...interface{}) {
678+// writePktline encodes and writes a pktline to the given writer.
679+func writePktline(w io.Writer, v ...interface{}) {
680 msg := fmt.Sprintln(v...)
681 pkt := pktline.NewEncoder(w)
682 if err := pkt.EncodeString(msg); err != nil {
683@@ -132,32 +129,6 @@ func fileExists(path string) (bool, error) {
684 return true, err
685 }
686
687-func ensureRepo(dir string, repo string) error {
688- exists, err := fileExists(dir)
689- if err != nil {
690- return err
691- }
692- if !exists {
693- err = os.MkdirAll(dir, os.ModeDir|os.FileMode(0700))
694- if err != nil {
695- return err
696- }
697- }
698- rp := filepath.Join(dir, repo)
699- exists, err = fileExists(rp)
700- if err != nil {
701- return err
702- }
703- // FIXME: use backend.CreateRepository
704- if !exists {
705- _, err := git.Init(rp, true)
706- if err != nil {
707- return err
708- }
709diff --git a/server/hooks.go b/server/hooks.go
710new file mode 100644
711index 0000000000000000000000000000000000000000..a4f9ed5683f2c728cdf38854dd53f256ad780c22
712--- /dev/null
713+++ b/server/hooks.go
714@@ -0,0 +1,52 @@
715+package server
716+
717+import (
718+ "io"
719+
720+ "github.com/charmbracelet/soft-serve/server/hooks"
721+)
722+
723+var _ hooks.Hooks = (*Server)(nil)
724+
725+// PostReceive is called by the git post-receive hook.
726+//
727+// It implements Hooks.
728+func (*Server) PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) {
729+ logger.Debug("post-receive hook called", "repo", repo, "args", args)
730+}
731+
732+// PreReceive is called by the git pre-receive hook.
733+//
734+// It implements Hooks.
735+func (*Server) PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) {
736+ logger.Debug("pre-receive hook called", "repo", repo, "args", args)
737+}
738+
739+// Update is called by the git update hook.
740+//
741+// It implements Hooks.
742+func (*Server) Update(stdout io.Writer, stderr io.Writer, repo string, arg hooks.HookArg) {
743+ logger.Debug("update hook called", "repo", repo, "arg", arg)
744+}
745+
746+// PostUpdate is called by the git post-update hook.
747+//
748+// It implements Hooks.
749+func (s *Server) PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string) {
750+ rr, err := s.Config.Backend.Repository(repo)
751+ if err != nil {
752+ logger.WithPrefix("server.hooks.post-update").Error("error getting repository", "repo", repo, "err", err)
753+ return
754+ }
755+
756+ r, err := rr.Open()
757+ if err != nil {
758+ logger.WithPrefix("server.hooks.post-update").Error("error opening repository", "repo", repo, "err", err)
759+ return
760+ }
761+
762+ if err := r.UpdateServerInfo(); err != nil {
763+ logger.WithPrefix("server.hooks.post-update").Error("error updating server info", "repo", repo, "err", err)
764+ return
765+ }
766+}
767diff --git a/server/hooks/hooks.go b/server/hooks/hooks.go
768new file mode 100644
769index 0000000000000000000000000000000000000000..fb47b729b9a99a7df02edcf3cfe3fa955ca73ac7
770--- /dev/null
771+++ b/server/hooks/hooks.go
772@@ -0,0 +1,18 @@
773+package hooks
774+
775+import "io"
776+
777+// HookArg is an argument to a git hook.
778+type HookArg struct {
779+ OldSha string
780+ NewSha string
781+ RefName string
782+}
783+
784+// Hooks provides an interface for git server-side hooks.
785+type Hooks interface {
786+ PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
787+ Update(stdout io.Writer, stderr io.Writer, repo string, arg HookArg)
788+ PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
789+ PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string)
790+}
791diff --git a/server/server.go b/server/server.go
792index e3f66d3c5a6e49bc2ed4b9ecf6db364768647dd9..eb62692f1f77d465987f613a75cd9422e668d6d3 100644
793--- a/server/server.go
794+++ b/server/server.go
795@@ -3,7 +3,9 @@ package server
796 import (
797 "context"
798 "net/http"
799+ "path/filepath"
800
801+ "github.com/charmbracelet/keygen"
802 "github.com/charmbracelet/log"
803
804 "github.com/charmbracelet/soft-serve/server/backend"
805@@ -57,7 +59,7 @@ func NewServer(cfg *config.Config) (*Server, error) {
806 Config: cfg,
807 Backend: cfg.Backend,
808 }
809- srv.SSHServer, err = NewSSHServer(cfg)
810+ srv.SSHServer, err = NewSSHServer(cfg, srv)
811 if err != nil {
812 return nil, err
813 }
814diff --git a/server/server_test.go b/server/server_test.go
815index b3bdb9ebbafb89ac7987fb2744c01e3f4c41896b..5505bec565280f8ed92c0487eed52c31c26c9910 100644
816--- a/server/server_test.go
817+++ b/server/server_test.go
818@@ -8,7 +8,6 @@ import (
819 "testing"
820
821 "github.com/charmbracelet/keygen"
822- "github.com/charmbracelet/soft-serve/server/backend/noop"
823 "github.com/charmbracelet/soft-serve/server/config"
824 "github.com/charmbracelet/ssh"
825 "github.com/matryer/is"
826@@ -32,9 +31,7 @@ func setupServer(tb testing.TB) (*Server, *config.Config, string) {
827 tb.Setenv("SOFT_SERVE_SSH_LISTEN_ADDR", sshPort)
828 tb.Setenv("SOFT_SERVE_GIT_LISTEN_ADDR", fmt.Sprintf(":%d", randomPort()))
829 cfg := config.DefaultConfig()
830- nop := &noop.Noop{Port: sshPort[1:]}
831 tb.Log("configuring server")
832- cfg = cfg.WithBackend(nop).WithAccessMethod(nop)
833 s, err := NewServer(cfg)
834 if err != nil {
835 tb.Fatal(err)
836diff --git a/server/session_test.go b/server/session_test.go
837index a120321a266005fdf6d98f6504dde1523e5ffc4b..542d312c6a5c7778dda1a45c4cea12fe42871091 100644
838--- a/server/session_test.go
839+++ b/server/session_test.go
840@@ -56,7 +56,7 @@ func setup(tb testing.TB) *gossh.Session {
841 if err != nil {
842 log.Fatal(err)
843 }
844- cfg := config.DefaultConfig().WithBackend(fb).WithAccessMethod(fb)
845+ cfg := config.DefaultConfig().WithBackend(fb)
846 return testsession.New(tb, &ssh.Server{
847 Handler: bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256)(func(s ssh.Session) {
848 _, _, active := s.Pty()
849diff --git a/server/ssh.go b/server/ssh.go
850index 61e8f7f54c7072866f7927b583375b6cfb11802b..6a73e935ac43b0a25872cfb9831e61c311e98112 100644
851--- a/server/ssh.go
852+++ b/server/ssh.go
853@@ -12,6 +12,7 @@ import (
854 "github.com/charmbracelet/soft-serve/server/backend"
855 cm "github.com/charmbracelet/soft-serve/server/cmd"
856 "github.com/charmbracelet/soft-serve/server/config"
857+ "github.com/charmbracelet/soft-serve/server/hooks"
858 "github.com/charmbracelet/soft-serve/server/utils"
859 "github.com/charmbracelet/ssh"
860 "github.com/charmbracelet/wish"
861@@ -29,7 +30,7 @@ type SSHServer struct {
862 }
863
864 // NewSSHServer returns a new SSHServer.
865-func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
866+func NewSSHServer(cfg *config.Config, hooks hooks.Hooks) (*SSHServer, error) {
867 var err error
868 s := &SSHServer{cfg: cfg}
869 logger := logger.StandardLog(log.StandardLogOptions{ForceLevel: log.DebugLevel})
870@@ -39,7 +40,7 @@ func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
871 // BubbleTea middleware.
872 bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256),
873 // CLI middleware.
874- cm.Middleware(cfg),
875+ cm.Middleware(cfg, hooks),
876 // Git middleware.
877 s.Middleware(cfg),
878 // Logging middleware.
879@@ -50,7 +51,7 @@ func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
880 ssh.PublicKeyAuth(s.PublicKeyHandler),
881 ssh.KeyboardInteractiveAuth(s.KeyboardInteractiveHandler),
882 wish.WithAddress(cfg.SSH.ListenAddr),
883- wish.WithHostKeyPath(cfg.SSH.KeyPath),
884+ wish.WithHostKeyPath(filepath.Join(cfg.DataPath, cfg.SSH.KeyPath)),
885 wish.WithMiddleware(mw...),
886 )
887 if err != nil {
888@@ -89,7 +90,7 @@ func (s *SSHServer) Shutdown(ctx context.Context) error {
889
890 // PublicKeyAuthHandler handles public key authentication.
891 func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
892- return s.cfg.Backend.AccessLevel("", pk) > backend.NoAccess
893+ return s.cfg.Backend.AccessLevel("", pk) >= backend.ReadOnlyAccess
894 }
895
896 // KeyboardInteractiveHandler handles keyboard interactive authentication.
897@@ -116,7 +117,6 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
898 // git bare repositories should end in ".git"
899 // https://git-scm.com/docs/gitrepository-layout
900 repo := name + ".git"
901-
902 reposDir := filepath.Join(cfg.DataPath, "repos")
903 if err := ensureWithin(reposDir, repo); err != nil {
904 sshFatal(s, err)
905@@ -125,30 +125,30 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
906
907 repoDir := filepath.Join(reposDir, repo)
908 switch gc {
909- case ReceivePackBin:
910+ case receivePackBin:
911 if access < backend.ReadWriteAccess {
912 sshFatal(s, ErrNotAuthed)
913 return
914 }
915 if _, err := cfg.Backend.Repository(name); err != nil {
916 if _, err := cfg.Backend.CreateRepository(name, false); err != nil {
917- log.Printf("failed to create repo: %s", err)
918+ log.Errorf("failed to create repo: %s", err)
919 sshFatal(s, err)
920 return
921 }
922 }
923- if err := ReceivePack(s, s, s.Stderr(), repoDir); err != nil {
924+ if err := receivePack(s, s, s.Stderr(), repoDir); err != nil {
925 sshFatal(s, ErrSystemMalfunction)
926 }
927 return
928- case UploadPackBin, UploadArchiveBin:
929+ case uploadPackBin, uploadArchiveBin:
930 if access < backend.ReadOnlyAccess {
931 sshFatal(s, ErrNotAuthed)
932 return
933 }
934- gitPack := UploadPack
935- if gc == UploadArchiveBin {
936- gitPack = UploadArchive
937+ gitPack := uploadPack
938+ if gc == uploadArchiveBin {
939+ gitPack = uploadArchive
940 }
941 err := gitPack(s, s, s.Stderr(), repoDir)
942 if errors.Is(err, ErrInvalidRepo) {
943@@ -166,6 +166,6 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
944
945 // sshFatal prints to the session's STDOUT as a git response and exit 1.
946 func sshFatal(s ssh.Session, v ...interface{}) {
947- WritePktline(s, v...)
948+ writePktline(s, v...)
949 s.Exit(1) // nolint: errcheck
950 }