Create a config repo with defaults

Toby Padilla created

Change summary

git/git.go                      | 88 +++++++++++++++++++-----------
tui/bubble.go                   | 38 ++++++++----
tui/bubbles/selection/bubble.go |  9 ++-
tui/commands.go                 | 99 ++++++++++++++++++++++++++++++----
tui/defaults.go                 | 15 +++++
5 files changed, 186 insertions(+), 63 deletions(-)

Detailed changes

git/git.go 🔗

@@ -37,21 +37,19 @@ func (cl CommitLog) Less(i, j int) bool {
 }
 
 type RepoSource struct {
+	Path            string
 	mtx             sync.Mutex
-	path            string
 	repos           []*Repo
 	commits         CommitLog
 	readmeTransform ReadmeTransform
 }
 
-func NewRepoSource(repoPath string, poll time.Duration, rf ReadmeTransform) *RepoSource {
-	rs := &RepoSource{path: repoPath, readmeTransform: rf}
-	go func() {
-		for {
-			rs.loadRepos()
-			time.Sleep(poll)
-		}
-	}()
+func NewRepoSource(repoPath string, rf ReadmeTransform) *RepoSource {
+	err := os.MkdirAll(repoPath, os.ModeDir|os.FileMode(0700))
+	if err != nil {
+		log.Fatal(err)
+	}
+	rs := &RepoSource{Path: repoPath, readmeTransform: rf}
 	return rs
 }
 
@@ -72,6 +70,21 @@ func (rs *RepoSource) GetRepo(name string) (*Repo, error) {
 	return nil, ErrMissingRepo
 }
 
+func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) {
+	rs.mtx.Lock()
+	defer rs.mtx.Unlock()
+	rg, err := git.PlainInit(rs.Path+string(os.PathSeparator)+name, bare)
+	if err != nil {
+		return nil, err
+	}
+	r := &Repo{
+		Name:       name,
+		Repository: rg,
+	}
+	rs.repos = append(rs.repos, r)
+	return r, nil
+}
+
 func (rs *RepoSource) GetCommits(limit int) []RepoCommit {
 	rs.mtx.Lock()
 	defer rs.mtx.Unlock()
@@ -81,42 +94,51 @@ func (rs *RepoSource) GetCommits(limit int) []RepoCommit {
 	return rs.commits[:limit]
 }
 
-func (rs *RepoSource) loadRepos() {
+func (rs *RepoSource) LoadRepos() error {
 	rs.mtx.Lock()
 	defer rs.mtx.Unlock()
-	rd, err := os.ReadDir(rs.path)
+	rd, err := os.ReadDir(rs.Path)
 	if err != nil {
-		return
+		return err
 	}
 	rs.repos = make([]*Repo, 0)
 	rs.commits = make([]RepoCommit, 0)
 	for _, de := range rd {
 		rn := de.Name()
-		r := &Repo{Name: rn}
-		rg, err := git.PlainOpen(rs.path + string(os.PathSeparator) + rn)
+		rg, err := git.PlainOpen(rs.Path + string(os.PathSeparator) + rn)
 		if err != nil {
-			log.Fatal(err)
+			return err
 		}
-		r.Repository = rg
-		l, err := rg.Log(&git.LogOptions{All: true})
-		if err != nil {
-			log.Fatal(err)
-		}
-		l.ForEach(func(c *object.Commit) error {
-			if r.LastUpdated == nil {
-				r.LastUpdated = &c.Author.When
-				rf, err := c.File("README.md")
+		r, err := rs.loadRepo(rn, rg)
+		rs.repos = append(rs.repos, r)
+	}
+	return nil
+}
+
+func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
+	r := &Repo{Name: name}
+	r.Repository = rg
+	l, err := rg.Log(&git.LogOptions{All: true})
+	if err != nil {
+		return nil, err
+	}
+	err = l.ForEach(func(c *object.Commit) error {
+		if r.LastUpdated == nil {
+			r.LastUpdated = &c.Author.When
+			rf, err := c.File("README.md")
+			if err == nil {
+				rmd, err := rf.Contents()
 				if err == nil {
-					rmd, err := rf.Contents()
-					if err == nil {
-						r.Readme = rs.readmeTransform(rmd)
-					}
+					r.Readme = rs.readmeTransform(rmd)
 				}
 			}
-			rs.commits = append(rs.commits, RepoCommit{Name: rn, Commit: c})
-			return nil
-		})
-		sort.Sort(rs.commits)
-		rs.repos = append(rs.repos, r)
+		}
+		rs.commits = append(rs.commits, RepoCommit{Name: name, Commit: c})
+		return nil
+	})
+	if err != nil {
+		return nil, err
 	}
+	sort.Sort(rs.commits)
+	return r, nil
 }

tui/bubble.go 🔗

@@ -6,7 +6,6 @@ import (
 	"smoothie/git"
 	"smoothie/tui/bubbles/commits"
 	"smoothie/tui/bubbles/selection"
-	"time"
 
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
@@ -30,6 +29,7 @@ type Bubble struct {
 	error          string
 	width          int
 	height         int
+	session        ssh.Session
 	windowChanges  <-chan ssh.Window
 	repoSource     *git.RepoSource
 	repos          []*git.Repo
@@ -40,17 +40,25 @@ type Bubble struct {
 	readmeViewport *ViewportBubble
 }
 
-func NewBubble(width int, height int, windowChanges <-chan ssh.Window, repoSource *git.RepoSource) *Bubble {
+type Config struct {
+	Width         int
+	Height        int
+	Session       ssh.Session
+	WindowChanges <-chan ssh.Window
+	RepoSource    *git.RepoSource
+}
+
+func NewBubble(cfg Config) *Bubble {
 	b := &Bubble{
-		width:         width,
-		height:        height,
-		windowChanges: windowChanges,
-		repoSource:    repoSource,
+		width:         cfg.Width,
+		height:        cfg.Height,
+		windowChanges: cfg.WindowChanges,
+		repoSource:    cfg.RepoSource,
 		boxes:         make([]tea.Model, 2),
 		readmeViewport: &ViewportBubble{
 			Viewport: &viewport.Model{
 				Width:  boxRightWidth - horizontalPadding - 2,
-				Height: height - verticalPadding - viewportHeightConstant,
+				Height: cfg.Height - verticalPadding - viewportHeightConstant,
 			},
 		},
 	}
@@ -140,19 +148,21 @@ func glamourReadme(md string) string {
 }
 
 func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
-	rs := git.NewRepoSource(reposPath, time.Second*10, glamourReadme)
+	rs := git.NewRepoSource(reposPath, glamourReadme)
 	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
 		if len(s.Command()) == 0 {
 			pty, changes, active := s.Pty()
 			if !active {
 				return nil, nil
 			}
-			return NewBubble(
-					pty.Window.Width,
-					pty.Window.Height,
-					changes,
-					rs),
-				[]tea.ProgramOption{tea.WithAltScreen()}
+			cfg := Config{
+				Width:         pty.Window.Width,
+				Height:        pty.Window.Height,
+				WindowChanges: changes,
+				RepoSource:    rs,
+				Session:       s,
+			}
+			return NewBubble(cfg), []tea.ProgramOption{tea.WithAltScreen()}
 		}
 		return nil, nil
 	}

tui/bubbles/selection/bubble.go 🔗

@@ -63,8 +63,11 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 }
 
 func (b *Bubble) sendMessage() tea.Msg {
-	return SelectedMsg{
-		Name:  b.Items[b.selectedItem],
-		Index: b.selectedItem,
+	if b.selectedItem >= 0 && b.selectedItem < len(b.Items) {
+		return SelectedMsg{
+			Name:  b.Items[b.selectedItem],
+			Index: b.selectedItem,
+		}
 	}
+	return nil
 }

tui/commands.go 🔗

@@ -1,10 +1,15 @@
 package tui
 
 import (
+	"os"
+	"path/filepath"
+	"smoothie/git"
 	"smoothie/tui/bubbles/commits"
 	"smoothie/tui/bubbles/selection"
 
 	tea "github.com/charmbracelet/bubbletea"
+	gg "github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing/object"
 )
 
 type windowMsg struct{}
@@ -21,24 +26,76 @@ func (b *Bubble) windowChangesCmd() tea.Msg {
 	return windowMsg{}
 }
 
-func (b *Bubble) getRepoCmd(name string) tea.Cmd {
-	return func() tea.Msg {
-		r, err := b.repoSource.GetRepo(name)
+func (b *Bubble) loadGitCmd() tea.Msg {
+	cn := "config"
+	err := b.repoSource.LoadRepos()
+	cr, err := b.repoSource.GetRepo(cn)
+	if err == git.ErrMissingRepo {
+		cr, err = b.repoSource.InitRepo(cn, false)
 		if err != nil {
 			return errMsg{err}
 		}
-		b.readmeViewport.Viewport.GotoTop()
-		b.readmeViewport.Viewport.Height = b.height - verticalPadding - viewportHeightConstant
-		b.readmeViewport.Viewport.Width = boxLeftWidth - 2
-		b.readmeViewport.Viewport.SetContent(r.Readme)
-		b.boxes[1] = b.readmeViewport
-		b.activeBox = 1
-		return nil
-	}
-}
 
-func (b *Bubble) loadGitCmd() tea.Msg {
+		// Add default README and config
+		rp := filepath.Join(b.repoSource.Path, cn, "README.md")
+		rf, err := os.Create(rp)
+		if err != nil {
+			return errMsg{err}
+		}
+		defer rf.Close()
+		_, err = rf.WriteString(defaultReadme)
+		if err != nil {
+			return errMsg{err}
+		}
+		err = rf.Sync()
+		if err != nil {
+			return errMsg{err}
+		}
+		cp := filepath.Join(b.repoSource.Path, cn, "config.json")
+		cf, err := os.Create(cp)
+		if err != nil {
+			return errMsg{err}
+		}
+		defer cf.Close()
+		_, err = cf.WriteString(defaultConfig)
+		if err != nil {
+			return errMsg{err}
+		}
+		err = cf.Sync()
+		if err != nil {
+			return errMsg{err}
+		}
+		wt, err := cr.Repository.Worktree()
+		if err != nil {
+			return errMsg{err}
+		}
+		_, err = wt.Add("README.md")
+		if err != nil {
+			return errMsg{err}
+		}
+		_, err = wt.Add("config.json")
+		if err != nil {
+			return errMsg{err}
+		}
+		_, err = wt.Commit("Default init", &gg.CommitOptions{
+			All: true,
+			Author: &object.Signature{
+				Name:  "Smoothie Server",
+				Email: "vt100@charm.sh",
+			},
+		})
+		if err != nil {
+			return errMsg{err}
+		}
+		err = b.repoSource.LoadRepos()
+		if err != nil {
+			return errMsg{err}
+		}
+	} else if err != nil {
+		return errMsg{err}
+	}
 	b.repos = b.repoSource.AllRepos()
+
 	rs := make([]string, 0)
 	for _, r := range b.repos {
 		rs = append(rs, r.Name)
@@ -55,3 +112,19 @@ func (b *Bubble) loadGitCmd() tea.Msg {
 	b.state = loadedState
 	return nil
 }
+
+func (b *Bubble) getRepoCmd(name string) tea.Cmd {
+	return func() tea.Msg {
+		r, err := b.repoSource.GetRepo(name)
+		if err != nil {
+			return errMsg{err}
+		}
+		b.readmeViewport.Viewport.GotoTop()
+		b.readmeViewport.Viewport.Height = b.height - verticalPadding - viewportHeightConstant
+		b.readmeViewport.Viewport.Width = boxLeftWidth - 2
+		b.readmeViewport.Viewport.SetContent(r.Readme)
+		b.boxes[1] = b.readmeViewport
+		b.activeBox = 1
+		return nil
+	}
+}

tui/defaults.go 🔗

@@ -0,0 +1,15 @@
+package tui
+
+const defaultReadme = "# Smoothie\nWelcome to Smoothie. To setup your own configuration, please clone this repo."
+
+const defaultConfig = `{
+	"name": "Smoothie",
+	"show_all_repos": true,
+	"menu": [
+	  {
+			"name": "Home",
+			"repo": "config",
+			"note": ""
+		}
+	]
+}`