Skip to content

Commit

Permalink
Scrollable timeline for latest commits across all repos
Browse files Browse the repository at this point in the history
  • Loading branch information
Toby Padilla committed Aug 3, 2021
1 parent f2794d4 commit ae2a71a
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 40 deletions.
36 changes: 22 additions & 14 deletions tui/git.go → git/git.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tui
package git

import (
"log"
Expand All @@ -11,21 +11,29 @@ import (
"github.com/go-git/go-git/v5/plumbing/object"
)

type commitLog []*object.Commit
type RepoCommit struct {
Name string
Repo *git.Repository
Commit *object.Commit
}

type CommitLog []RepoCommit

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) }
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].Commit.Author.When.After(cl[j].Commit.Author.When)
}

type repoSource struct {
type RepoSource struct {
mtx sync.Mutex
path string
repos []*git.Repository
commits commitLog
commits CommitLog
}

func newRepoSource(repoPath string) *repoSource {
rs := &repoSource{path: repoPath}
func NewRepoSource(repoPath string) *RepoSource {
rs := &RepoSource{path: repoPath}
go func() {
for {
rs.loadRepos()
Expand All @@ -35,13 +43,13 @@ func newRepoSource(repoPath string) *repoSource {
return rs
}

func (rs *repoSource) allRepos() []*git.Repository {
func (rs *RepoSource) AllRepos() []*git.Repository {
rs.mtx.Lock()
defer rs.mtx.Unlock()
return rs.repos
}

func (rs *repoSource) getCommits(limit int) []*object.Commit {
func (rs *RepoSource) GetCommits(limit int) []RepoCommit {
rs.mtx.Lock()
defer rs.mtx.Unlock()
if limit > len(rs.commits) {
Expand All @@ -50,15 +58,15 @@ func (rs *repoSource) getCommits(limit int) []*object.Commit {
return rs.commits[:limit]
}

func (rs *repoSource) loadRepos() {
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)
rs.commits = make([]*object.Commit, 0)
rs.commits = make([]RepoCommit, 0)
for _, rd := range rd {
r, err := git.PlainOpen(rs.path + string(os.PathSeparator) + rd.Name())
if err != nil {
Expand All @@ -69,7 +77,7 @@ func (rs *repoSource) loadRepos() {
log.Fatal(err)
}
l.ForEach(func(c *object.Commit) error {
rs.commits = append(rs.commits, c)
rs.commits = append(rs.commits, RepoCommit{Name: rd.Name(), Repo: r, Commit: c})
return nil
})
sort.Sort(rs.commits)
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ replace github.com/charmbracelet/charm => ../charm
replace github.com/charmbracelet/bubbletea => ../bubbletea

require (
github.com/charmbracelet/bubbles v0.8.0
github.com/charmbracelet/bubbletea v0.14.0
github.com/charmbracelet/charm v0.8.6
github.com/charmbracelet/lipgloss v0.2.1
github.com/dustin/go-humanize v1.0.0
github.com/gliderlabs/ssh v0.3.3
github.com/go-git/go-git/v5 v5.4.2
github.com/meowgorithm/babyenv v1.3.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/charmbracelet/bubbles v0.8.0 h1:+l2op90Ag37Vn+30O1hbg/0wBl+e+sxHhgY1F/rvdHs=
github.com/charmbracelet/bubbles v0.8.0/go.mod h1:5WX1sSSjNCgCrzvRMN/z23HxvWaa+AI16Ch0KPZPeDs=
github.com/charmbracelet/bubbletea v0.13.1/go.mod h1:tp9tr9Dadh0PLhgiwchE5zZJXm5543JYjHG9oY+5qSg=
github.com/charmbracelet/bubbletea v0.14.0 h1:SBbtRPkZ/wGYyTAn2zrBERes/nwzxZR5QdJ5nvczmFw=
Expand Down Expand Up @@ -53,6 +54,7 @@ github.com/dgraph-io/ristretto v0.0.4-0.20210122082011-bb5d392ed82d/go.mod h1:tv
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
Expand Down
47 changes: 23 additions & 24 deletions tui/model.go → tui/bubble.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ package tui

import (
"fmt"
"smoothie/git"
"smoothie/tui/bubbles/commits"

tea "github.com/charmbracelet/bubbletea"
"github.com/gliderlabs/ssh"
"github.com/go-git/go-git/v5/plumbing/object"
)

type sessionState int

const (
startState sessionState = iota
errorState
commitsLoadedState
loadedState
quittingState
quitState
)
Expand All @@ -26,45 +27,44 @@ func (e errMsg) Error() string {
return e.err.Error()
}

func SessionHandler(repoPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
rs := newRepoSource(repoPath)
func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
rs := git.NewRepoSource(reposPath)
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, rs), []tea.ProgramOption{tea.WithAltScreen()}
}
return nil, nil
}
}

type Model struct {
state sessionState
error string
info string
width int
height int
windowChanges <-chan ssh.Window
repos *repoSource
commits []*object.Commit
state sessionState
error string
info string
width int
height int
windowChanges <-chan ssh.Window
repoSource *git.RepoSource
commitTimeline *commits.Bubble
}

func NewModel(width int, height int, windowChanges <-chan ssh.Window, repos *repoSource) *Model {
func NewModel(width int, height int, windowChanges <-chan ssh.Window, repoSource *git.RepoSource) *Model {
m := &Model{
width: width,
height: height,
windowChanges: windowChanges,
repos: repos,
commits: make([]*object.Commit, 0),
repoSource: repoSource,
}
m.state = startState
return m
}

func (m *Model) Init() tea.Cmd {
return tea.Batch(m.windowChangesCmd, tea.HideCursor, m.getCommitsCmd)
return tea.Batch(m.windowChangesCmd, m.getCommitsCmd)
}

func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Expand All @@ -77,6 +77,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "j", "k", "up", "down":
_, cmd := m.commitTimeline.Update(msg)
cmds = append(cmds, cmd)
}
case errMsg:
m.error = msg.Error()
Expand All @@ -89,6 +92,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.commitTimeline.Height = msg.Height
}
return m, tea.Batch(cmds...)
}
Expand All @@ -100,13 +104,8 @@ func (m *Model) View() string {
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 loadedState:
s += m.commitTimeline.View()
case errorState:
s += errorStyle.Render(fmt.Sprintf("Bummer: %s", m.error))
default:
Expand Down
70 changes: 70 additions & 0 deletions tui/bubbles/commits/bubble.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package commits

import (
"smoothie/git"
"strings"

"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/dustin/go-humanize"
)

type Bubble struct {
Commits []git.RepoCommit
Margin int
Width int
Height int
viewport viewport.Model
}

func NewBubble(height int, margin int, width int, rcs []git.RepoCommit) *Bubble {
b := &Bubble{
Commits: rcs,
viewport: viewport.Model{Height: height - margin, Width: width},
}
s := ""
for _, rc := range rcs {
s += b.renderCommit(rc) + "\n"
}
b.viewport.SetContent(s)
return b
}

func (b *Bubble) Init() tea.Cmd {
return nil
}

func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
b.viewport.Height = msg.Height - b.Margin
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
b.viewport.LineUp(1)
case "down", "j":
b.viewport.LineDown(1)
}
}
return b, tea.Batch(cmds...)
}

func (b *Bubble) renderCommit(rc git.RepoCommit) string {
s := ""
s += commitRepoNameStyle.Render(rc.Name)
s += " "
s += commitDateStyle.Render(humanize.Time(rc.Commit.Author.When))
s += "\n"
s += commitCommentStyle.Render(strings.TrimSpace(rc.Commit.Message))
s += "\n"
s += commitAuthorStyle.Render(rc.Commit.Author.Name)
s += " "
s += commitAuthorEmailStyle.Render(rc.Commit.Author.Email)
s += " "
return commitBoxStyle.Width(b.viewport.Height).Render(s)
}

func (b *Bubble) View() string {
return b.viewport.View()
}
26 changes: 26 additions & 0 deletions tui/bubbles/commits/style.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package commits

import (
"github.com/charmbracelet/lipgloss"
)

var commitBoxStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#670083")).
Padding(1)
var commitRepoNameStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#8922A5"))
var commitAuthorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#670083"))
var commitAuthorEmailStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#781194"))
var commitDateStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#781194"))
var commitCommentStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#A0A0A0")).
BorderStyle(lipgloss.Border{Left: ">"}).
BorderForeground(lipgloss.Color("#606060")).
PaddingLeft(1).
PaddingBottom(0).
Margin(0)
22 changes: 22 additions & 0 deletions tui/bubbles/repo/bubble.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package repo

import (
tea "github.com/charmbracelet/bubbletea"
"github.com/go-git/go-git/v5"
)

type Bubble struct {
repo *git.Repository
}

func (b *Bubble) Init() tea.Cmd {
return nil
}

func (b *Bubble) Update(tea.Msg) (tea.Model, tea.Cmd) {
return nil, nil
}

func (b *Bubble) View() string {
return "repo"
}
25 changes: 25 additions & 0 deletions tui/bubbles/repo/style.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package repo

import (
"github.com/charmbracelet/lipgloss"
)

var commitBoxStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#670083")).
Padding(1)
var commitRepoNameStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#8922A5"))
var commitAuthorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#670083"))
var commitAuthorEmailStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#781194"))
var commitDateStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#781194"))
var commitCommentStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#606060")).
BorderStyle(lipgloss.Border{Left: ">"}).
PaddingLeft(1).
PaddingBottom(0).
Margin(0)
6 changes: 4 additions & 2 deletions tui/commands.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package tui

import (
"smoothie/tui/bubbles/commits"

tea "github.com/charmbracelet/bubbletea"
)

Expand All @@ -14,7 +16,7 @@ func (m *Model) windowChangesCmd() tea.Msg {
}

func (m *Model) getCommitsCmd() tea.Msg {
m.commits = m.repos.getCommits(20)
m.state = commitsLoadedState
m.commitTimeline = commits.NewBubble(m.height, 2, 80, m.repoSource.GetCommits(200))
m.state = loadedState
return nil
}

0 comments on commit ae2a71a

Please sign in to comment.