@@ -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(),
)
@@ -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)
+ }
+}
@@ -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: