feat: display repo commits

Ayman Bagabas created

Change summary

server/session.go                  |   2 
ui/common/common.go                |   2 
ui/components/selector/selector.go |  75 ++++++++++-----
ui/pages/repo/log.go               | 154 ++++++++++++++++++++++---------
ui/pages/repo/repo.go              |  33 +++++-
ui/pages/selection/selection.go    |   9 +
ui/ui.go                           |  29 ++++-
7 files changed, 215 insertions(+), 89 deletions(-)

Detailed changes

server/session.go 🔗

@@ -57,7 +57,7 @@ func SessionHandler(ac *appCfg.Config) bm.ProgramHandler {
 		}
 		c := common.Common{
 			Styles: styles.DefaultStyles(),
-			Keymap: keymap.DefaultKeyMap(),
+			KeyMap: keymap.DefaultKeyMap(),
 			Width:  pty.Window.Width,
 			Height: pty.Window.Height,
 		}

ui/common/common.go 🔗

@@ -8,7 +8,7 @@ import (
 // Common is a struct all components should embed.
 type Common struct {
 	Styles *styles.Styles
-	Keymap *keymap.KeyMap
+	KeyMap *keymap.KeyMap
 	Width  int
 	Height int
 }

ui/components/selector/selector.go 🔗

@@ -9,8 +9,7 @@ import (
 
 // Selector is a list of items that can be selected.
 type Selector struct {
-	KeyMap      list.KeyMap
-	list        list.Model
+	list.Model
 	common      common.Common
 	active      int
 	filterState list.FilterState
@@ -36,63 +35,87 @@ func New(common common.Common, items []IdentifiableItem, delegate list.ItemDeleg
 	}
 	l := list.New(itms, delegate, common.Width, common.Height)
 	s := &Selector{
-		list:   l,
+		Model:  l,
 		common: common,
 	}
-	s.KeyMap = list.DefaultKeyMap()
 	s.SetSize(common.Width, common.Height)
 	return s
 }
 
+// PerPage returns the number of items per page.
+func (s *Selector) PerPage() int {
+	return s.Model.Paginator.PerPage
+}
+
+// SetPage sets the current page.
+func (s *Selector) SetPage(page int) {
+	s.Model.Paginator.Page = page
+}
+
+// Page returns the current page.
+func (s *Selector) Page() int {
+	return s.Model.Paginator.Page
+}
+
+// TotalPages returns the total number of pages.
+func (s *Selector) TotalPages() int {
+	return s.Model.Paginator.TotalPages
+}
+
+// Select selects the item at the given index.
+func (s *Selector) Select(index int) {
+	s.Model.Select(index)
+}
+
 // SetShowTitle sets the show title flag.
 func (s *Selector) SetShowTitle(show bool) {
-	s.list.SetShowTitle(show)
+	s.Model.SetShowTitle(show)
 }
 
 // SetShowHelp sets the show help flag.
 func (s *Selector) SetShowHelp(show bool) {
-	s.list.SetShowHelp(show)
+	s.Model.SetShowHelp(show)
 }
 
 // SetShowStatusBar sets the show status bar flag.
 func (s *Selector) SetShowStatusBar(show bool) {
-	s.list.SetShowStatusBar(show)
+	s.Model.SetShowStatusBar(show)
 }
 
 // DisableQuitKeybindings disables the quit keybindings.
 func (s *Selector) DisableQuitKeybindings() {
-	s.list.DisableQuitKeybindings()
+	s.Model.DisableQuitKeybindings()
 }
 
 // SetShowFilter sets the show filter flag.
 func (s *Selector) SetShowFilter(show bool) {
-	s.list.SetShowFilter(show)
+	s.Model.SetShowFilter(show)
 }
 
 // SetShowPagination sets the show pagination flag.
 func (s *Selector) SetShowPagination(show bool) {
-	s.list.SetShowPagination(show)
+	s.Model.SetShowPagination(show)
 }
 
 // SetFilteringEnabled sets the filtering enabled flag.
 func (s *Selector) SetFilteringEnabled(enabled bool) {
-	s.list.SetFilteringEnabled(enabled)
+	s.Model.SetFilteringEnabled(enabled)
 }
 
 // SetSize implements common.Component.
 func (s *Selector) SetSize(width, height int) {
 	s.common.SetSize(width, height)
-	s.list.SetSize(width, height)
+	s.Model.SetSize(width, height)
 }
 
 // SetItems sets the items in the selector.
 func (s *Selector) SetItems(items []list.Item) tea.Cmd {
-	return s.list.SetItems(items)
+	return s.Model.SetItems(items)
 }
 
 // Index returns the index of the selected item.
 func (s *Selector) Index() int {
-	return s.list.Index()
+	return s.Model.Index()
 }
 
 // Init implements tea.Model.
@@ -107,44 +130,44 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tea.MouseMsg:
 		switch msg.Type {
 		case tea.MouseWheelUp:
-			s.list.CursorUp()
+			s.Model.CursorUp()
 		case tea.MouseWheelDown:
-			s.list.CursorDown()
+			s.Model.CursorDown()
 		}
 	case tea.KeyMsg:
 		switch {
-		case key.Matches(msg, s.common.Keymap.Select):
+		case key.Matches(msg, s.common.KeyMap.Select):
 			cmds = append(cmds, s.selectCmd)
 		}
 	case list.FilterMatchesMsg:
 		cmds = append(cmds, s.activeFilterCmd)
 	}
-	m, cmd := s.list.Update(msg)
-	s.list = m
+	m, cmd := s.Model.Update(msg)
+	s.Model = m
 	if cmd != nil {
 		cmds = append(cmds, cmd)
 	}
 	// Track filter state and update active item when filter state changes.
-	filterState := s.list.FilterState()
+	filterState := s.Model.FilterState()
 	if s.filterState != filterState {
 		cmds = append(cmds, s.activeFilterCmd)
 	}
 	s.filterState = filterState
 	// Send ActiveMsg when index change.
-	if s.active != s.list.Index() {
+	if s.active != s.Model.Index() {
 		cmds = append(cmds, s.activeCmd)
 	}
-	s.active = s.list.Index()
+	s.active = s.Model.Index()
 	return s, tea.Batch(cmds...)
 }
 
 // View implements tea.Model.
 func (s *Selector) View() string {
-	return s.list.View()
+	return s.Model.View()
 }
 
 func (s *Selector) selectCmd() tea.Msg {
-	item := s.list.SelectedItem()
+	item := s.Model.SelectedItem()
 	i, ok := item.(IdentifiableItem)
 	if !ok {
 		return SelectMsg("")
@@ -153,7 +176,7 @@ func (s *Selector) selectCmd() tea.Msg {
 }
 
 func (s *Selector) activeCmd() tea.Msg {
-	item := s.list.SelectedItem()
+	item := s.Model.SelectedItem()
 	i, ok := item.(IdentifiableItem)
 	if !ok {
 		return ActiveMsg("")
@@ -165,7 +188,7 @@ func (s *Selector) activeFilterCmd() tea.Msg {
 	// Here we use VisibleItems because when list.FilterMatchesMsg is sent,
 	// VisibleItems is the only way to get the list of filtered items. The list
 	// bubble should export something like list.FilterMatchesMsg.Items().
-	items := s.list.VisibleItems()
+	items := s.Model.VisibleItems()
 	if len(items) == 0 {
 		return nil
 	}

ui/pages/repo/log.go 🔗

@@ -1,10 +1,15 @@
 package repo
 
 import (
+	"fmt"
+
+	"github.com/charmbracelet/bubbles/list"
 	tea "github.com/charmbracelet/bubbletea"
+	ggit "github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/ui/common"
 	"github.com/charmbracelet/soft-serve/ui/components/selector"
 	"github.com/charmbracelet/soft-serve/ui/components/viewport"
+	"github.com/charmbracelet/soft-serve/ui/git"
 )
 
 type view int
@@ -14,11 +19,19 @@ const (
 	commitView
 )
 
+type LogCountMsg int64
+
+type LogItemsMsg []list.Item
+
 type Log struct {
 	common     common.Common
 	selector   *selector.Selector
 	vp         *viewport.Viewport
 	activeView view
+	repo       git.GitRepo
+	ref        *ggit.Reference
+	count      int64
+	nextPage   int
 }
 
 func NewLog(common common.Common) *Log {
@@ -30,13 +43,13 @@ func NewLog(common common.Common) *Log {
 	selector := selector.New(common, []selector.IdentifiableItem{}, LogItemDelegate{common.Styles})
 	selector.SetShowFilter(false)
 	selector.SetShowHelp(false)
-	selector.SetShowPagination(true)
+	selector.SetShowPagination(false)
 	selector.SetShowStatusBar(false)
 	selector.SetShowTitle(false)
 	selector.SetFilteringEnabled(false)
 	selector.DisableQuitKeybindings()
-	selector.KeyMap.NextPage = common.Keymap.NextPage
-	selector.KeyMap.PrevPage = common.Keymap.PrevPage
+	selector.KeyMap.NextPage = common.KeyMap.NextPage
+	selector.KeyMap.PrevPage = common.KeyMap.PrevPage
 	l.selector = selector
 	return l
 }
@@ -47,53 +60,54 @@ func (l *Log) SetSize(width, height int) {
 	l.vp.SetSize(width, height)
 }
 
-// func (b *Bubble) countCommits() tea.Msg {
-// 	if b.ref == nil {
-// 		ref, err := b.repo.HEAD()
-// 		if err != nil {
-// 			return common.ErrMsg{Err: err}
-// 		}
-// 		b.ref = ref
-// 	}
-// 	count, err := b.repo.CountCommits(b.ref)
-// 	if err != nil {
-// 		return common.ErrMsg{Err: err}
-// 	}
-// 	return countMsg(count)
-// }
-
-// func (b *Bubble) updateItems() tea.Msg {
-// 	if b.count == 0 {
-// 		b.count = int64(b.countCommits().(countMsg))
-// 	}
-// 	count := b.count
-// 	items := make([]list.Item, count)
-// 	page := b.nextPage
-// 	limit := b.list.Paginator.PerPage
-// 	skip := page * limit
-// 	// CommitsByPage pages start at 1
-// 	cc, err := b.repo.CommitsByPage(b.ref, page+1, limit)
-// 	if err != nil {
-// 		return common.ErrMsg{Err: err}
-// 	}
-// 	for i, c := range cc {
-// 		idx := i + skip
-// 		if int64(idx) >= count {
-// 			break
-// 		}
-// 		items[idx] = item{c}
-// 	}
-// 	b.list.SetItems(items)
-// 	b.SetSize(b.width, b.height)
-// 	return itemsMsg{}
-// }
-
 func (l *Log) Init() tea.Cmd {
-	return nil
+	cmds := make([]tea.Cmd, 0)
+	cmds = append(cmds, l.updateCommitsCmd)
+	return tea.Batch(cmds...)
 }
 
 func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	return l, nil
+	cmds := make([]tea.Cmd, 0)
+	switch msg := msg.(type) {
+	case RepoMsg:
+		l.count = 0
+		l.selector.Select(0)
+		l.nextPage = 0
+		l.repo = git.GitRepo(msg)
+	case RefMsg:
+		l.ref = msg
+		l.count = 0
+		cmds = append(cmds, l.countCommitsCmd)
+	case LogCountMsg:
+		l.count = int64(msg)
+	case LogItemsMsg:
+		cmds = append(cmds, l.selector.SetItems(msg))
+		l.selector.SetPage(l.nextPage)
+		l.SetSize(l.common.Width, l.common.Height)
+	case tea.KeyMsg, tea.MouseMsg:
+		// This is a hack for loading commits on demand based on list.Pagination.
+		if l.activeView == logView {
+			curPage := l.selector.Page()
+			s, cmd := l.selector.Update(msg)
+			m := s.(*selector.Selector)
+			l.selector = m
+			if m.Page() != curPage {
+				l.nextPage = m.Page()
+				l.selector.SetPage(curPage)
+				cmds = append(cmds, l.updateCommitsCmd)
+			}
+			cmds = append(cmds, cmd)
+		}
+	}
+	switch l.activeView {
+	case commitView:
+		vp, cmd := l.vp.Update(msg)
+		l.vp = vp.(*viewport.Viewport)
+		if cmd != nil {
+			cmds = append(cmds, cmd)
+		}
+	}
+	return l, tea.Batch(cmds...)
 }
 
 func (l *Log) View() string {
@@ -106,3 +120,51 @@ func (l *Log) View() string {
 		return ""
 	}
 }
+
+func (l *Log) StatusBarInfo() string {
+	switch l.activeView {
+	case logView:
+		// We're using l.nextPage instead of l.selector.Paginator.Page because
+		// of the paginator hack above.
+		return fmt.Sprintf("%d/%d", l.nextPage+1, l.selector.TotalPages())
+	default:
+		return ""
+	}
+}
+
+func (l *Log) countCommitsCmd() tea.Msg {
+	count, err := l.repo.CountCommits(l.ref)
+	if err != nil {
+		return common.ErrorMsg(err)
+	}
+	return LogCountMsg(count)
+}
+
+func (l *Log) updateCommitsCmd() tea.Msg {
+	count := l.count
+	if l.count == 0 {
+		switch msg := l.countCommitsCmd().(type) {
+		case common.ErrorMsg:
+			return msg
+		case LogCountMsg:
+			count = int64(msg)
+		}
+	}
+	items := make([]list.Item, count)
+	page := l.nextPage
+	limit := l.selector.PerPage()
+	skip := page * limit
+	// CommitsByPage pages start at 1
+	cc, err := l.repo.CommitsByPage(l.ref, page+1, limit)
+	if err != nil {
+		return common.ErrorMsg(err)
+	}
+	for i, c := range cc {
+		idx := i + skip
+		if int64(idx) >= count {
+			break
+		}
+		items[idx] = LogItem{c}
+	}
+	return LogItemsMsg(items)
+}

ui/pages/repo/repo.go 🔗

@@ -9,7 +9,6 @@ import (
 	ggit "github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/ui/common"
 	"github.com/charmbracelet/soft-serve/ui/components/code"
-	"github.com/charmbracelet/soft-serve/ui/components/selector"
 	"github.com/charmbracelet/soft-serve/ui/components/statusbar"
 	"github.com/charmbracelet/soft-serve/ui/components/tabs"
 	"github.com/charmbracelet/soft-serve/ui/git"
@@ -79,9 +78,9 @@ func (r *Repo) SetSize(width, height int) {
 // ShortHelp implements help.KeyMap.
 func (r *Repo) ShortHelp() []key.Binding {
 	b := make([]key.Binding, 0)
-	tab := r.common.Keymap.Section
+	tab := r.common.KeyMap.Section
 	tab.SetHelp("tab", "switch tab")
-	b = append(b, r.common.Keymap.Back)
+	b = append(b, r.common.KeyMap.Back)
 	b = append(b, tab)
 	return b
 }
@@ -101,28 +100,48 @@ func (r *Repo) Init() tea.Cmd {
 func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	cmds := make([]tea.Cmd, 0)
 	switch msg := msg.(type) {
-	case selector.SelectMsg:
-		r.activeTab = 0
-		cmds = append(cmds, r.tabs.Init(), r.setRepoCmd(string(msg)))
 	case RepoMsg:
+		r.activeTab = 0
 		r.selectedRepo = git.GitRepo(msg)
 		r.readme.GotoTop()
 		cmds = append(cmds,
+			r.tabs.Init(),
 			r.updateReadmeCmd,
 			r.updateRefCmd,
 		)
+		// Pass msg to log.
+		l, cmd := r.log.Update(msg)
+		r.log = l.(*Log)
+		if cmd != nil {
+			cmds = append(cmds, cmd)
+		}
 	case RefMsg:
 		r.ref = msg
 		cmds = append(cmds,
 			r.updateStatusBarCmd,
 			r.log.Init(),
 		)
+		// Pass msg to log.
+		l, cmd := r.log.Update(msg)
+		r.log = l.(*Log)
+		if cmd != nil {
+			cmds = append(cmds, cmd)
+		}
 	case tabs.ActiveTabMsg:
 		r.activeTab = tab(msg)
+		if r.selectedRepo != nil {
+			cmds = append(cmds, r.updateStatusBarCmd)
+		}
 	case tea.KeyMsg, tea.MouseMsg:
 		if r.selectedRepo != nil {
 			cmds = append(cmds, r.updateStatusBarCmd)
 		}
+	case LogCountMsg, LogItemsMsg:
+		l, cmd := r.log.Update(msg)
+		r.log = l.(*Log)
+		if cmd != nil {
+			cmds = append(cmds, cmd)
+		}
 	}
 	t, cmd := r.tabs.Update(msg)
 	r.tabs = t.(*tabs.Tabs)
@@ -213,6 +232,8 @@ func (r *Repo) updateStatusBarCmd() tea.Msg {
 	switch r.activeTab {
 	case readmeTab:
 		info = fmt.Sprintf("%.f%%", r.readme.ScrollPercent()*100)
+	case commitsTab:
+		info = r.log.StatusBarInfo()
 	}
 	return statusbar.StatusBarMsg{
 		Key:    r.selectedRepo.Name(),

ui/pages/selection/selection.go 🔗

@@ -74,12 +74,12 @@ func (s *Selection) ShortHelp() []key.Binding {
 	k := s.selector.KeyMap
 	kb := make([]key.Binding, 0)
 	kb = append(kb,
-		s.common.Keymap.UpDown,
-		s.common.Keymap.Section,
+		s.common.KeyMap.UpDown,
+		s.common.KeyMap.Section,
 	)
 	if s.activeBox == selectorBox {
 		kb = append(kb,
-			s.common.Keymap.Select,
+			s.common.KeyMap.Select,
 			k.Filter,
 			k.ClearFilter,
 		)
@@ -205,9 +205,10 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		cmds = append(cmds, s.changeActive(msg))
 		// reset readme position when active item change
 		s.readme.GotoTop()
+	case selector.SelectMsg:
 	case tea.KeyMsg:
 		switch {
-		case key.Matches(msg, s.common.Keymap.Section):
+		case key.Matches(msg, s.common.KeyMap.Section):
 			s.activeBox = (s.activeBox + 1) % 2
 		}
 	}

ui/ui.go 🔗

@@ -11,6 +11,7 @@ import (
 	"github.com/charmbracelet/soft-serve/ui/components/footer"
 	"github.com/charmbracelet/soft-serve/ui/components/header"
 	"github.com/charmbracelet/soft-serve/ui/components/selector"
+	"github.com/charmbracelet/soft-serve/ui/git"
 	"github.com/charmbracelet/soft-serve/ui/pages/repo"
 	"github.com/charmbracelet/soft-serve/ui/pages/selection"
 	"github.com/charmbracelet/soft-serve/ui/session"
@@ -64,7 +65,7 @@ func (ui *UI) getMargins() (wm, hm int) {
 func (ui *UI) ShortHelp() []key.Binding {
 	b := make([]key.Binding, 0)
 	b = append(b, ui.pages[ui.activePage].ShortHelp()...)
-	b = append(b, ui.common.Keymap.Quit)
+	b = append(b, ui.common.KeyMap.Quit)
 	return b
 }
 
@@ -72,7 +73,7 @@ func (ui *UI) ShortHelp() []key.Binding {
 func (ui *UI) FullHelp() [][]key.Binding {
 	b := make([][]key.Binding, 0)
 	b = append(b, ui.pages[ui.activePage].FullHelp()...)
-	b = append(b, []key.Binding{ui.common.Keymap.Quit})
+	b = append(b, []key.Binding{ui.common.KeyMap.Quit})
 	return b
 }
 
@@ -129,9 +130,12 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		ui.SetSize(msg.Width, msg.Height)
 	case tea.KeyMsg:
 		switch {
-		case key.Matches(msg, ui.common.Keymap.Quit):
+		case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
+			ui.error = nil
+			ui.state = loadedState
+		case key.Matches(msg, ui.common.KeyMap.Quit):
 			return ui, tea.Quit
-		case ui.activePage == 1 && key.Matches(msg, ui.common.Keymap.Back):
+		case ui.activePage == 1 && key.Matches(msg, ui.common.KeyMap.Back):
 			ui.activePage = 0
 		}
 	case common.ErrorMsg:
@@ -139,7 +143,10 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		ui.state = errorState
 		return ui, nil
 	case selector.SelectMsg:
-		ui.activePage = (ui.activePage + 1) % 2
+		if ui.activePage == 0 {
+			ui.activePage = (ui.activePage + 1) % 2
+			cmds = append(cmds, ui.setRepoCmd(string(msg)))
+		}
 	}
 	m, cmd := ui.pages[ui.activePage].Update(msg)
 	ui.pages[ui.activePage] = m.(common.Page)
@@ -171,3 +178,15 @@ func (ui *UI) View() string {
 	}
 	return ui.common.Styles.App.Render(s.String())
 }
+
+func (ui *UI) setRepoCmd(rn string) tea.Cmd {
+	rs := ui.s.Config().Source
+	return func() tea.Msg {
+		for _, r := range rs.AllRepos() {
+			if r.Name() == rn {
+				return repo.RepoMsg(r)
+			}
+		}
+		return common.ErrorMsg(git.ErrMissingRepo)
+	}
+}