List latest commits in bubble

Toby Padilla created

Change summary

main.go         |  2 
tui/commands.go |  6 +++
tui/git.go      | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++
tui/model.go    | 36 +++++++++++++++++------
tui/style.go    |  3 -
5 files changed, 110 insertions(+), 14 deletions(-)

Detailed changes

main.go 🔗

@@ -27,7 +27,7 @@ func main() {
 	s, err := server.NewServer(
 		cfg.Port,
 		cfg.KeyPath,
-		bm.Middleware(tui.SessionHandler),
+		bm.Middleware(tui.SessionHandler(cfg.RepoPath)),
 		gm.Middleware(cfg.RepoPath, cfg.RepoAuthPath),
 		lm.Middleware(),
 	)

tui/commands.go 🔗

@@ -12,3 +12,9 @@ func (m *Model) windowChangesCmd() tea.Msg {
 	m.height = w.Height
 	return windowMsg{}
 }
+
+func (m *Model) getCommitsCmd() tea.Msg {
+	m.commits = m.repos.getCommits(20)
+	m.state = commitsLoadedState
+	return nil
+}

tui/git.go 🔗

@@ -0,0 +1,77 @@
+package tui
+
+import (
+	"log"
+	"os"
+	"sort"
+	"sync"
+	"time"
+
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing/object"
+)
+
+type commitLog []*object.Commit
+
+func (cl commitLog) Len() int           { return len(cl) }
+func (cl commitLog) Swap(i, j int)      { cl[i], cl[j] = cl[j], cl[i] }
+func (cl commitLog) Less(i, j int) bool { return cl[i].Author.When.After(cl[j].Author.When) }
+
+type repoSource struct {
+	mtx     sync.Mutex
+	path    string
+	repos   []*git.Repository
+	commits commitLog
+}
+
+func newRepoSource(repoPath string) *repoSource {
+	rs := &repoSource{path: repoPath}
+	go func() {
+		for {
+			rs.loadRepos()
+			time.Sleep(time.Second * 10)
+		}
+	}()
+	return rs
+}
+
+func (rs *repoSource) allRepos() []*git.Repository {
+	rs.mtx.Lock()
+	defer rs.mtx.Unlock()
+	return rs.repos
+}
+
+func (rs *repoSource) getCommits(limit int) []*object.Commit {
+	rs.mtx.Lock()
+	defer rs.mtx.Unlock()
+	if limit > len(rs.commits) {
+		limit = len(rs.commits)
+	}
+	return rs.commits[:limit]
+}
+
+func (rs *repoSource) loadRepos() {
+	rs.mtx.Lock()
+	defer rs.mtx.Unlock()
+	rd, err := os.ReadDir(rs.path)
+	if err != nil {
+		return
+	}
+	rs.repos = make([]*git.Repository, 0)
+	for _, rd := range rd {
+		r, err := git.PlainOpen(rs.path + string(os.PathSeparator) + rd.Name())
+		if err != nil {
+			log.Fatal(err)
+		}
+		l, err := r.Log(&git.LogOptions{All: true})
+		if err != nil {
+			log.Fatal(err)
+		}
+		l.ForEach(func(c *object.Commit) error {
+			rs.commits = append(rs.commits, c)
+			return nil
+		})
+		sort.Sort(rs.commits)
+		rs.repos = append(rs.repos, r)
+	}
+}

tui/model.go 🔗

@@ -5,6 +5,7 @@ import (
 
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/gliderlabs/ssh"
+	"github.com/go-git/go-git/v5/plumbing/object"
 )
 
 type sessionState int
@@ -12,6 +13,7 @@ type sessionState int
 const (
 	startState sessionState = iota
 	errorState
+	commitsLoadedState
 	quittingState
 	quitState
 )
@@ -24,15 +26,18 @@ func (e errMsg) Error() string {
 	return e.err.Error()
 }
 
-func SessionHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
-	if len(s.Command()) == 0 {
-		pty, changes, active := s.Pty()
-		if !active {
-			return nil, nil
+func SessionHandler(repoPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
+	rs := newRepoSource(repoPath)
+	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 NewModel(pty.Window.Width, pty.Window.Height, changes, rs), nil
 		}
-		return NewModel(pty.Window.Width, pty.Window.Height, changes), []tea.ProgramOption{tea.WithAltScreen()}
+		return nil, nil
 	}
-	return nil, nil
 }
 
 type Model struct {
@@ -42,20 +47,24 @@ type Model struct {
 	width         int
 	height        int
 	windowChanges <-chan ssh.Window
+	repos         *repoSource
+	commits       []*object.Commit
 }
 
-func NewModel(width int, height int, windowChanges <-chan ssh.Window) *Model {
+func NewModel(width int, height int, windowChanges <-chan ssh.Window, repos *repoSource) *Model {
 	m := &Model{
 		width:         width,
 		height:        height,
 		windowChanges: windowChanges,
+		repos:         repos,
+		commits:       make([]*object.Commit, 0),
 	}
 	m.state = startState
 	return m
 }
 
 func (m *Model) Init() tea.Cmd {
-	return tea.Batch(m.windowChangesCmd, tea.HideCursor)
+	return tea.Batch(m.windowChangesCmd, tea.HideCursor, m.getCommitsCmd)
 }
 
 func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -86,11 +95,18 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 func (m *Model) View() string {
 	pad := 6
-	h := headerStyle.Width(m.width - pad).Render("Smoothie")
+	h := headerStyle.Width(m.width - pad).Render("Charm Beta")
 	f := footerStyle.Render(m.info)
 	s := ""
 	content := ""
 	switch m.state {
+	case startState:
+		s += normalStyle.Render("Loading")
+	case commitsLoadedState:
+		for _, c := range m.commits {
+			msg := fmt.Sprintf("%s %s %s %s", c.Author.When, c.Author.Name, c.Author.Email, c.Message)
+			s += normalStyle.Render(msg) + "\n"
+		}
 	case errorState:
 		s += errorStyle.Render(fmt.Sprintf("Bummer: %s", m.error))
 	default:

tui/style.go 🔗

@@ -18,12 +18,9 @@ var normalStyle = lipgloss.NewStyle().
 	Foreground(lipgloss.Color("#FFFFFF"))
 
 var footerStyle = lipgloss.NewStyle().
-	BorderStyle(lipgloss.Border{Left: ">"}).
 	BorderForeground(lipgloss.Color("#6D6D6D")).
 	BorderLeft(true).
 	Foreground(lipgloss.Color("#373737")).
-	PaddingLeft(1).
-	MarginLeft(1).
 	Bold(true)
 
 var errorStyle = lipgloss.NewStyle().