fix: migrate readme & fix permissions

Ayman Bagabas created

Change summary

cmd/soft/migrate_config.go      | 122 +++++++++++++++++++++++++++++-----
git/utils.go                    |  51 ++++++++++++++
go.mod                          |   1 
go.sum                          |   3 
server/backend/sqlite/sqlite.go |  12 +-
server/backend/utils.go         |  44 ------------
server/config/config.go         |   4 
server/config/config_test.go    |   4 
8 files changed, 167 insertions(+), 74 deletions(-)

Detailed changes

cmd/soft/migrate_config.go 🔗

@@ -7,6 +7,7 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+	"time"
 
 	"github.com/charmbracelet/log"
 	"github.com/charmbracelet/soft-serve/git"
@@ -14,6 +15,7 @@ import (
 	"github.com/charmbracelet/soft-serve/server/backend/sqlite"
 	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/charmbracelet/soft-serve/server/utils"
+	gitm "github.com/gogs/git-module"
 	"github.com/spf13/cobra"
 	"golang.org/x/crypto/ssh"
 	"gopkg.in/yaml.v3"
@@ -25,6 +27,9 @@ var (
 		Short:  "Migrate config to new format",
 		Hidden: true,
 		RunE: func(cmd *cobra.Command, _ []string) error {
+			// Disable logging timestamp
+			log.SetReportTimestamp(false)
+
 			keyPath := os.Getenv("SOFT_SERVE_KEY_PATH")
 			reposPath := os.Getenv("SOFT_SERVE_REPO_PATH")
 			bindAddr := os.Getenv("SOFT_SERVE_BIND_ADDRESS")
@@ -46,7 +51,7 @@ var (
 			// Copy SSH host key
 			log.Info("Copying SSH host key...")
 			if keyPath != "" {
-				if err := os.MkdirAll(filepath.Join(cfg.DataPath, "ssh"), 0700); err != nil {
+				if err := os.MkdirAll(filepath.Join(cfg.DataPath, "ssh"), os.ModePerm); err != nil {
 					return fmt.Errorf("failed to create ssh directory: %w", err)
 				}
 
@@ -104,6 +109,9 @@ var (
 				}
 			}
 
+			readme, readmePath, err := git.LatestFile(r, "README*")
+			hasReadme := err == nil
+
 			// Set server name
 			cfg.Name = ocfg.Name
 
@@ -125,13 +133,17 @@ var (
 			// Copy repos
 			if reposPath != "" {
 				log.Info("Copying repos...")
+				if err := os.MkdirAll(filepath.Join(cfg.DataPath, "repos"), os.ModePerm); err != nil {
+					return fmt.Errorf("failed to create repos directory: %w", err)
+				}
+
 				dirs, err := os.ReadDir(reposPath)
 				if err != nil {
 					return fmt.Errorf("failed to read repos directory: %w", err)
 				}
 
 				for _, dir := range dirs {
-					if !dir.IsDir() {
+					if !dir.IsDir() || dir.Name() == "config" {
 						continue
 					}
 
@@ -140,12 +152,12 @@ var (
 					}
 
 					log.Infof("  Copying repo %s", dir.Name())
-					if err := os.MkdirAll(filepath.Join(cfg.DataPath, "repos"), 0700); err != nil {
-						return fmt.Errorf("failed to create repos directory: %w", err)
+					src := filepath.Join(reposPath, utils.SanitizeRepo(dir.Name()))
+					dst := filepath.Join(cfg.DataPath, "repos", utils.SanitizeRepo(dir.Name())) + ".git"
+					if err := os.MkdirAll(dst, os.ModePerm); err != nil {
+						return fmt.Errorf("failed to create repo directory: %w", err)
 					}
 
-					src := utils.SanitizeRepo(filepath.Join(reposPath, dir.Name()))
-					dst := utils.SanitizeRepo(filepath.Join(cfg.DataPath, "repos", dir.Name())) + ".git"
 					if err := copyDir(src, dst); err != nil {
 						return fmt.Errorf("failed to copy repo: %w", err)
 					}
@@ -154,26 +166,100 @@ var (
 						fmt.Fprintf(os.Stderr, "failed to create repository: %s\n", err)
 					}
 				}
+
+				if hasReadme {
+					log.Infof("  Copying readme from \"config\" to \".soft-serve\"")
+
+					// Switch to main branch
+					bcmd := git.NewCommand("branch", "-M", "main")
+
+					rp := filepath.Join(cfg.DataPath, "repos", ".soft-serve.git")
+					nr, err := git.Init(rp, true)
+					if err != nil {
+						return fmt.Errorf("failed to init repo: %w", err)
+					}
+
+					if _, err := nr.SymbolicRef("HEAD", gitm.RefsHeads+"main"); err != nil {
+						return fmt.Errorf("failed to set HEAD: %w", err)
+					}
+
+					tmpDir, err := os.MkdirTemp("", "soft-serve")
+					if err != nil {
+						return fmt.Errorf("failed to create temp dir: %w", err)
+					}
+
+					r, err := git.Init(tmpDir, false)
+					if err != nil {
+						return fmt.Errorf("failed to clone repo: %w", err)
+					}
+
+					if _, err := bcmd.RunInDir(tmpDir); err != nil {
+						return fmt.Errorf("failed to create main branch: %w", err)
+					}
+
+					if err := os.WriteFile(filepath.Join(tmpDir, readmePath), []byte(readme), 0o644); err != nil {
+						return fmt.Errorf("failed to write readme: %w", err)
+					}
+
+					if err := r.Add(gitm.AddOptions{
+						All: true,
+					}); err != nil {
+						return fmt.Errorf("failed to add readme: %w", err)
+					}
+
+					if err := r.Commit(&gitm.Signature{
+						Name:  "Soft Serve",
+						Email: "vt100@charm.sh",
+						When:  time.Now(),
+					}, "Add readme"); err != nil {
+						return fmt.Errorf("failed to commit readme: %w", err)
+					}
+
+					if err := r.RemoteAdd("origin", "file://"+rp); err != nil {
+						return fmt.Errorf("failed to add remote: %w", err)
+					}
+
+					if err := r.Push("origin", "main"); err != nil {
+						return fmt.Errorf("failed to push readme: %w", err)
+					}
+
+					// Create `.soft-serve` repository and add readme
+					if _, err := sb.CreateRepository(".soft-serve", backend.RepositoryOptions{
+						ProjectName: "Home",
+						Description: "Soft Serve home repository",
+						Hidden:      true,
+						Private:     false,
+					}); err != nil {
+						fmt.Fprintf(os.Stderr, "failed to create repository: %s\n", err)
+					}
+				}
 			}
 
 			// Set repos metadata & collabs
 			log.Info("Setting repos metadata & collabs...")
-			for _, repo := range ocfg.Repos {
-				if err := sb.SetProjectName(repo.Repo, repo.Name); err != nil {
-					log.Errorf("failed to set repo name to %s: %s", repo.Repo, err)
+			for _, r := range ocfg.Repos {
+				repo, name := r.Repo, r.Name
+				// Special case for config repo
+				if repo == "config" {
+					repo = ".soft-serve"
+					r.Private = false
+				}
+
+				if err := sb.SetProjectName(repo, name); err != nil {
+					log.Errorf("failed to set repo name to %s: %s", repo, err)
 				}
 
-				if err := sb.SetDescription(repo.Repo, repo.Note); err != nil {
-					log.Errorf("failed to set repo description to %s: %s", repo.Repo, err)
+				if err := sb.SetDescription(repo, r.Note); err != nil {
+					log.Errorf("failed to set repo description to %s: %s", repo, err)
 				}
 
-				if err := sb.SetPrivate(repo.Repo, repo.Private); err != nil {
-					log.Errorf("failed to set repo private to %s: %s", repo.Repo, err)
+				if err := sb.SetPrivate(repo, r.Private); err != nil {
+					log.Errorf("failed to set repo private to %s: %s", repo, err)
 				}
 
-				for _, collab := range repo.Collabs {
-					if err := sb.AddCollaborator(repo.Repo, collab); err != nil {
-						log.Errorf("failed to add repo collab to %s: %s", repo.Repo, err)
+				for _, collab := range r.Collabs {
+					if err := sb.AddCollaborator(repo, collab); err != nil {
+						log.Errorf("failed to add repo collab to %s: %s", repo, err)
 					}
 				}
 			}
@@ -291,11 +377,11 @@ func copyDir(src string, dst string) error {
 
 		if fd.IsDir() {
 			if err = copyDir(srcfp, dstfp); err != nil {
-				fmt.Println(err)
+				log.Error("failed to copy directory", "err", err)
 			}
 		} else {
 			if err = copyFile(srcfp, dstfp); err != nil {
-				fmt.Println(err)
+				log.Error("failed to copy file", "err", err)
 			}
 		}
 	}

git/utils.go 🔗

@@ -0,0 +1,51 @@
+package git
+
+import (
+	"path/filepath"
+
+	"github.com/gobwas/glob"
+)
+
+// LatestFile returns the contents of the first file at the specified path pattern in the repository and its file path.
+func LatestFile(repo *Repository, pattern string) (string, string, error) {
+	g := glob.MustCompile(pattern)
+	dir := filepath.Dir(pattern)
+	head, err := repo.HEAD()
+	if err != nil {
+		return "", "", err
+	}
+	t, err := repo.TreePath(head, dir)
+	if err != nil {
+		return "", "", err
+	}
+	ents, err := t.Entries()
+	if err != nil {
+		return "", "", err
+	}
+	for _, e := range ents {
+		te := e
+		fp := filepath.Join(dir, te.Name())
+		if te.IsTree() {
+			continue
+		}
+		if g.Match(fp) {
+			if te.IsSymlink() {
+				bts, err := te.Contents()
+				if err != nil {
+					return "", "", err
+				}
+				fp = string(bts)
+				te, err = t.TreeEntry(fp)
+				if err != nil {
+					return "", "", err
+				}
+			}
+			bts, err := te.Contents()
+			if err != nil {
+				return "", "", err
+			}
+			return string(bts), fp, nil
+		}
+	}
+	return "", "", ErrFileNotFound
+}

go.mod 🔗

@@ -18,7 +18,6 @@ require (
 )
 
 require (
-	github.com/aymanbagabas/go-osc52 v1.2.2
 	github.com/caarlos0/env/v7 v7.1.0
 	github.com/charmbracelet/keygen v0.4.2
 	github.com/charmbracelet/log v0.2.1

go.sum 🔗

@@ -49,9 +49,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
+github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E=
 github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
-github.com/aymanbagabas/go-osc52 v1.2.2 h1:NT7wkhEhPTcKnBCdPi9djmyy9L3JOL4+3SsfJyqptCo=
-github.com/aymanbagabas/go-osc52 v1.2.2/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=

server/backend/sqlite/sqlite.go 🔗

@@ -40,7 +40,7 @@ func (d *SqliteBackend) reposPath() string {
 // NewSqliteBackend creates a new SqliteBackend.
 func NewSqliteBackend(ctx context.Context, cfg *config.Config) (*SqliteBackend, error) {
 	dataPath := cfg.DataPath
-	if err := os.MkdirAll(dataPath, 0755); err != nil {
+	if err := os.MkdirAll(dataPath, os.ModePerm); err != nil {
 		return nil, err
 	}
 
@@ -254,7 +254,7 @@ func (d *SqliteBackend) RenameRepository(oldName string, newName string) error {
 	}
 
 	// Make sure the new repository parent directory exists.
-	if err := os.MkdirAll(filepath.Dir(np), 0755); err != nil {
+	if err := os.MkdirAll(filepath.Dir(np), os.ModePerm); err != nil {
 		return err
 	}
 
@@ -639,7 +639,7 @@ func (d *SqliteBackend) InitializeHooks(repo string) error {
 
 	repo = utils.SanitizeRepo(repo) + ".git"
 	hooksPath := filepath.Join(d.reposPath(), repo, "hooks")
-	if err := os.MkdirAll(hooksPath, 0755); err != nil {
+	if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil {
 		return err
 	}
 
@@ -659,13 +659,13 @@ func (d *SqliteBackend) InitializeHooks(repo string) error {
 		var data bytes.Buffer
 		var args string
 		hp := filepath.Join(hooksPath, hook)
-		if err := os.WriteFile(hp, []byte(hookTpls[i]), 0755); err != nil {
+		if err := os.WriteFile(hp, []byte(hookTpls[i]), os.ModePerm); err != nil {
 			return err
 		}
 
 		// Create hook.d directory.
 		hp += ".d"
-		if err := os.MkdirAll(hp, 0755); err != nil {
+		if err := os.MkdirAll(hp, os.ModePerm); err != nil {
 			return err
 		}
 
@@ -694,7 +694,7 @@ func (d *SqliteBackend) InitializeHooks(repo string) error {
 		}
 
 		hp = filepath.Join(hp, "soft-serve")
-		err = os.WriteFile(hp, data.Bytes(), 0755) //nolint:gosec
+		err = os.WriteFile(hp, data.Bytes(), os.ModePerm) //nolint:gosec
 		if err != nil {
 			logger.Error("failed to write hook", "err", err)
 			continue

server/backend/utils.go 🔗

@@ -1,59 +1,17 @@
 package backend
 
 import (
-	"path/filepath"
-
 	"github.com/charmbracelet/soft-serve/git"
-	"github.com/gobwas/glob"
 )
 
 // LatestFile returns the contents of the latest file at the specified path in
 // the repository and its file path.
 func LatestFile(r Repository, pattern string) (string, string, error) {
-	g := glob.MustCompile(pattern)
-	dir := filepath.Dir(pattern)
 	repo, err := r.Open()
 	if err != nil {
 		return "", "", err
 	}
-	head, err := repo.HEAD()
-	if err != nil {
-		return "", "", err
-	}
-	t, err := repo.TreePath(head, dir)
-	if err != nil {
-		return "", "", err
-	}
-	ents, err := t.Entries()
-	if err != nil {
-		return "", "", err
-	}
-	for _, e := range ents {
-		te := e
-		fp := filepath.Join(dir, te.Name())
-		if te.IsTree() {
-			continue
-		}
-		if g.Match(fp) {
-			if te.IsSymlink() {
-				bts, err := te.Contents()
-				if err != nil {
-					return "", "", err
-				}
-				fp = string(bts)
-				te, err = t.TreeEntry(fp)
-				if err != nil {
-					return "", "", err
-				}
-			}
-			bts, err := te.Contents()
-			if err != nil {
-				return "", "", err
-			}
-			return string(bts), fp, nil
-		}
-	}
-	return "", "", git.ErrFileNotFound
+	return git.LatestFile(repo, pattern)
 }
 
 // Readme returns the repository's README.

server/config/config.go 🔗

@@ -203,10 +203,10 @@ func ParseConfig(path string) (*Config, error) {
 
 // WriteConfig writes the configuration to the given file.
 func WriteConfig(path string, cfg *Config) error {
-	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
+	if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
 		return err
 	}
-	return os.WriteFile(path, []byte(newConfigFile(cfg)), 0o600) // nolint: errcheck
+	return os.WriteFile(path, []byte(newConfigFile(cfg)), 0o644) // nolint: errcheck
 }
 
 // DefaultConfig returns a Config with the values populated with the defaults

server/config/config_test.go 🔗

@@ -34,7 +34,7 @@ func TestMergeInitAdminKeys(t *testing.T) {
 	})
 	is.NoErr(err)
 	fp := filepath.Join(t.TempDir(), "config.yaml")
-	err = os.WriteFile(fp, bts, 0644)
+	err = os.WriteFile(fp, bts, 0o644)
 	is.NoErr(err)
 	cfg, err := ParseConfig(fp)
 	is.NoErr(err)
@@ -55,7 +55,7 @@ func TestValidateInitAdminKeys(t *testing.T) {
 	})
 	is.NoErr(err)
 	fp := filepath.Join(t.TempDir(), "config.yaml")
-	err = os.WriteFile(fp, bts, 0644)
+	err = os.WriteFile(fp, bts, 0o644)
 	is.NoErr(err)
 	cfg, err := ParseConfig(fp)
 	is.NoErr(err)