feat: store repo meta data in repo directory (#338)

Ayman Bagabas created

* feat: store repo meta data in repo directory

- Store auth'd user in context.
- Write description, owner, and git-daemon-export-ok files.

Fixes: https://github.com/charmbracelet/soft-serve/issues/255
Fixes: https://github.com/charmbracelet/soft-serve/issues/256

* fix: add tests

Change summary

server/backend/repo.go                | 51 ++++++++++++++++++++++++----
testscript/script_test.go             |  6 +++
testscript/testdata/mirror.txtar      |  6 +++
testscript/testdata/repo-create.txtar |  2 +
4 files changed, 57 insertions(+), 8 deletions(-)

Detailed changes

server/backend/repo.go 🔗

@@ -5,6 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"time"
@@ -53,6 +54,18 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto.
 			return err
 		}
 
+		if err := os.WriteFile(filepath.Join(rp, "description"), []byte(opts.Description), fs.ModePerm); err != nil {
+			d.logger.Error("failed to write description", "repo", name, "err", err)
+			return err
+		}
+
+		if !opts.Private {
+			if err := os.WriteFile(filepath.Join(rp, "git-daemon-export-ok"), []byte{}, fs.ModePerm); err != nil {
+				d.logger.Error("failed to write git-daemon-export-ok", "repo", name, "err", err)
+				return err
+			}
+		}
+
 		return hooks.GenerateHooks(ctx, d.cfg, repo)
 	}); err != nil {
 		d.logger.Debug("failed to create repository in database", "err", err)
@@ -341,29 +354,51 @@ func (d *Backend) SetHidden(ctx context.Context, name string, hidden bool) error
 // SetDescription sets the description of a repository.
 //
 // It implements backend.Backend.
-func (d *Backend) SetDescription(ctx context.Context, repo string, desc string) error {
-	repo = utils.SanitizeRepo(repo)
+func (d *Backend) SetDescription(ctx context.Context, name string, desc string) error {
+	name = utils.SanitizeRepo(name)
+	rp := filepath.Join(d.reposPath(), name+".git")
 
 	// Delete cache
-	d.cache.Delete(repo)
+	d.cache.Delete(name)
 
 	return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
-		return d.store.SetRepoDescriptionByName(ctx, tx, repo, desc)
+		if err := os.WriteFile(filepath.Join(rp, "description"), []byte(desc), fs.ModePerm); err != nil {
+			d.logger.Error("failed to write description", "repo", name, "err", err)
+			return err
+		}
+
+		return d.store.SetRepoDescriptionByName(ctx, tx, name, desc)
 	})
 }
 
 // SetPrivate sets the private flag of a repository.
 //
 // It implements backend.Backend.
-func (d *Backend) SetPrivate(ctx context.Context, repo string, private bool) error {
-	repo = utils.SanitizeRepo(repo)
+func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) error {
+	name = utils.SanitizeRepo(name)
+	rp := filepath.Join(d.reposPath(), name+".git")
 
 	// Delete cache
-	d.cache.Delete(repo)
+	d.cache.Delete(name)
 
 	return db.WrapError(
 		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
-			return d.store.SetRepoIsPrivateByName(ctx, tx, repo, private)
+			fp := filepath.Join(rp, "git-daemon-export-ok")
+			if !private {
+				if err := os.WriteFile(fp, []byte{}, fs.ModePerm); err != nil {
+					d.logger.Error("failed to write git-daemon-export-ok", "repo", name, "err", err)
+					return err
+				}
+			} else {
+				if _, err := os.Stat(fp); err == nil {
+					if err := os.Remove(fp); err != nil {
+						d.logger.Error("failed to remove git-daemon-export-ok", "repo", name, "err", err)
+						return err
+					}
+				}
+			}
+
+			return d.store.SetRepoIsPrivateByName(ctx, tx, name, private)
 		}),
 	)
 }

testscript/script_test.go 🔗

@@ -52,6 +52,7 @@ func TestScript(t *testing.T) {
 			"usoft":    cmdSoft(user1.Signer()),
 			"git":      cmdGit(key),
 			"mkfile":   cmdMkfile,
+			"readfile": cmdReadfile,
 			"dos2unix": cmdDos2Unix,
 		},
 		Setup: func(e *testscript.Env) error {
@@ -67,6 +68,7 @@ func TestScript(t *testing.T) {
 			statsListen := fmt.Sprintf("localhost:%d", statsPort)
 			serverName := "Test Soft Serve"
 
+			e.Setenv("DATA_PATH", data)
 			e.Setenv("SSH_PORT", fmt.Sprintf("%d", sshPort))
 			e.Setenv("ADMIN1_AUTHORIZED_KEY", admin1.AuthorizedKey())
 			e.Setenv("ADMIN2_AUTHORIZED_KEY", admin2.AuthorizedKey())
@@ -250,3 +252,7 @@ func check(ts *testscript.TestScript, err error, neg bool) {
 		ts.Check(err)
 	}
 }
+
+func cmdReadfile(ts *testscript.TestScript, neg bool, args []string) {
+	ts.Stdout().Write([]byte(ts.ReadFile(args[0])))
+}

testscript/testdata/mirror.txtar 🔗

@@ -6,6 +6,9 @@
 # import a repo
 soft repo import --mirror charmbracelet/catwalk https://github.com/charmbracelet/catwalk.git
 
+# check empty description file
+readfile $DATA_PATH/repos/charmbracelet/catwalk.git/description ''
+
 # check repo info
 soft repo info charmbracelet/catwalk
 cmp stdout info1.txt
@@ -32,6 +35,7 @@ soft repo description charmbracelet/catwalk
 soft repo description charmbracelet/catwalk "testing repo"
 soft repo description charmbracelet/catwalk
 stdout 'testing repo'
+readfile $DATA_PATH/repos/charmbracelet/catwalk.git/description 'testing repo'
 
 # rename
 soft repo rename charmbracelet/catwalk charmbracelet/test
@@ -41,11 +45,13 @@ stdout charmbracelet/test # TODO: shouldn't this still show the project-name?
 # check its not private
 soft repo private charmbracelet/test
 stdout false
+exists $DATA_PATH/repos/charmbracelet/test.git/git-daemon-export-ok
 
 # make it private
 soft repo private charmbracelet/test  true
 soft repo private charmbracelet/test
 stdout true
+! exists $DATA_PATH/repos/charmbracelet/test.git/git-daemon-export-ok
 
 # check its not hidden
 soft repo hidden charmbracelet/test

testscript/testdata/repo-create.txtar 🔗

@@ -9,8 +9,10 @@ soft repo hidden repo1
 stdout true
 soft repo private repo1
 stdout true
+! exists $DATA_PATH/repos/repo1.git/git-daemon-export-ok
 soft repo description repo1
 stdout 'description'
+readfile $DATA_PATH/repos/repo1.git/description 'description'
 soft repo project-name repo1
 stdout 'repo1'