diff --git a/cmd/soft/migrate_config.go b/cmd/soft/migrate_config.go index 48edf556a010032b4b8a6b2827c8cb706e5b0ef4..a20cc9d41a51d71aaac9b4433150b52986892177 100644 --- a/cmd/soft/migrate_config.go +++ b/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) } } } diff --git a/git/utils.go b/git/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..2b3d287285f8abf0a80a2c3084f2e97a246ce6ab --- /dev/null +++ b/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 +} diff --git a/go.mod b/go.mod index e6c7c0fe02ae29e8dc6eb4c6d5da581ee5d5bea9..6475eae923607d11c45877380d387dab8cb0593f 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 1ea72fe0d34a0fc8147244823532d1399f086803..876c5c3c5fdd6a752393733fc96394cb4beaec34 100644 --- a/go.sum +++ b/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= diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go index bc69e2628cb5c90ade79b40a84e2a358d0a13bca..382c99ff3e32005802de520ab5b6dcd273f05303 100644 --- a/server/backend/sqlite/sqlite.go +++ b/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 diff --git a/server/backend/utils.go b/server/backend/utils.go index c6cdff4db8d7d4d36b90d75e53235a95a369731e..e2724402c1165ec073803d2a94e97c50d89a0b2d 100644 --- a/server/backend/utils.go +++ b/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. diff --git a/server/config/config.go b/server/config/config.go index 4c2373c52efc60a26f87c72cd8a92f57f8e4d739..7c598548430b8f846cea8d1e86bc75892ff44670 100644 --- a/server/config/config.go +++ b/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 diff --git a/server/config/config_test.go b/server/config/config_test.go index f4cc8f16942db8b1b50735f8fd91803ca6d3a8aa..3812e4f9e72b871f3611b53ee2a1db1a12c803cd 100644 --- a/server/config/config_test.go +++ b/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)