Detailed changes
@@ -9,8 +9,10 @@ import (
"sync"
"time"
+ gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/storage/memory"
@@ -25,22 +27,147 @@ type Repo struct {
Repository *git.Repository
Readme string
LastUpdated *time.Time
- commits CommitLog
+ refCommits map[plumbing.Hash]gitypes.Commits
+ ref *plumbing.Reference
}
-// RepoCommit contains metadata for a Git commit.
-type RepoCommit struct {
- Name string
- Commit *object.Commit
+// GetName returns the name of the repository.
+func (r *Repo) GetName() string {
+ return r.Name
}
-// CommitLog is a series of Git commits.
-type CommitLog []RepoCommit
+// GetReference returns the reference for a repository.
+func (r *Repo) GetReference() *plumbing.Reference {
+ return r.ref
+}
+
+// SetReference sets the repository head reference.
+func (r *Repo) SetReference(ref *plumbing.Reference) error {
+ r.ref = ref
+ return nil
+}
+
+// GetRepository returns the underlying go-git repository object.
+func (r *Repo) GetRepository() *git.Repository {
+ return r.Repository
+}
+
+// Tree returns the git tree for a given path.
+func (r *Repo) Tree(ref *plumbing.Reference, path string) (*object.Tree, error) {
+ path = filepath.Clean(path)
+ hash, err := r.targetHash(ref)
+ if err != nil {
+ return nil, err
+ }
+ c, err := r.Repository.CommitObject(hash)
+ if err != nil {
+ return nil, err
+ }
+ t, err := c.Tree()
+ if err != nil {
+ return nil, err
+ }
+ if path == "." {
+ return t, nil
+ }
+ return t.Tree(path)
+}
-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)
+// GetCommits returns the commits for a repository.
+func (r *Repo) GetCommits(ref *plumbing.Reference) (gitypes.Commits, error) {
+ hash, err := r.targetHash(ref)
+ if err != nil {
+ return nil, err
+ }
+ // return cached commits if available
+ commits, ok := r.refCommits[hash]
+ if ok {
+ return commits, nil
+ }
+ log.Printf("caching commits for %s/%s: %s", r.Name, ref.Name(), ref.Hash())
+ commits = gitypes.Commits{}
+ co, err := r.Repository.CommitObject(hash)
+ if err != nil {
+ return nil, err
+ }
+ // traverse the commit tree to get all commits
+ commits = append(commits, &gitypes.Commit{Commit: co})
+ for {
+ co, err = co.Parent(0)
+ if err != nil {
+ if err == object.ErrParentNotFound {
+ err = nil
+ }
+ break
+ }
+ commits = append(commits, &gitypes.Commit{Commit: co})
+ }
+ if err != nil {
+ return nil, err
+ }
+ sort.Sort(commits)
+ // cache the commits in the repo
+ r.refCommits[hash] = commits
+ return commits, nil
+}
+
+// targetHash returns the target hash for a given reference. If reference is an
+// annotated tag, find the target hash for that tag.
+func (r *Repo) targetHash(ref *plumbing.Reference) (plumbing.Hash, error) {
+ hash := ref.Hash()
+ if ref.Type() != plumbing.HashReference {
+ return plumbing.ZeroHash, plumbing.ErrInvalidType
+ }
+ if ref.Name().IsTag() {
+ to, err := r.Repository.TagObject(hash)
+ switch err {
+ case nil:
+ // annotated tag (object has a target hash)
+ hash = to.Target
+ case plumbing.ErrObjectNotFound:
+ // lightweight tag (hash points to a commit)
+ default:
+ return plumbing.ZeroHash, err
+ }
+ }
+ return hash, nil
+}
+
+// loadCommits loads the commits for a repository.
+func (r *Repo) loadCommits(ref *plumbing.Reference) (gitypes.Commits, error) {
+ commits := gitypes.Commits{}
+ hash, err := r.targetHash(ref)
+ if err != nil {
+ return nil, err
+ }
+ l, err := r.Repository.Log(&git.LogOptions{
+ Order: git.LogOrderCommitterTime,
+ From: hash,
+ })
+ if err != nil {
+ return nil, err
+ }
+ defer l.Close()
+ err = l.ForEach(func(c *object.Commit) error {
+ commits = append(commits, &gitypes.Commit{Commit: c})
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return commits, nil
+}
+
+// GetReadme returns the readme for a repository.
+func (r *Repo) GetReadme() string {
+ if r.Readme != "" {
+ return r.Readme
+ }
+ md, err := r.LatestFile("README.md")
+ if err != nil {
+ return ""
+ }
+ return md
}
// RepoSource is a reference to an on-disk repositories.
@@ -106,16 +233,6 @@ func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) {
return r, nil
}
-func (r *Repo) GetCommits(limit int) CommitLog {
- if limit <= 0 {
- return r.commits
- }
- if limit > len(r.commits) {
- limit = len(r.commits)
- }
- return r.commits[:limit]
-}
-
// LoadRepos opens Git repositories.
func (rs *RepoSource) LoadRepos() error {
rs.mtx.Lock()
@@ -141,37 +258,29 @@ func (rs *RepoSource) LoadRepos() error {
}
func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
- r := &Repo{Name: name}
- r.commits = make([]RepoCommit, 0)
- r.Repository = rg
- l, err := rg.Log(&git.LogOptions{All: true})
+ r := &Repo{
+ Name: name,
+ Repository: rg,
+ }
+ r.refCommits = make(map[plumbing.Hash]gitypes.Commits)
+ ref, err := rg.Head()
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 {
- r.Readme = rmd
- }
- }
- }
- r.commits = append(r.commits, RepoCommit{Name: name, Commit: c})
- return nil
- })
+ r.ref = ref
+ rm, err := r.LatestFile("README.md")
if err != nil {
return nil, err
}
- sort.Sort(r.commits)
+ r.Readme = rm
return r, nil
}
// LatestFile returns the latest file at the specified path in the repository.
func (r *Repo) LatestFile(path string) (string, error) {
- lg, err := r.Repository.Log(&git.LogOptions{})
+ lg, err := r.Repository.Log(&git.LogOptions{
+ From: r.GetReference().Hash(),
+ })
if err != nil {
return "", err
}
@@ -3,9 +3,11 @@ package about
import (
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
"github.com/charmbracelet/soft-serve/internal/tui/style"
+ "github.com/go-git/go-git/v5/plumbing"
)
type Bubble struct {
@@ -16,6 +18,7 @@ type Bubble struct {
heightMargin int
width int
widthMargin int
+ ref *plumbing.Reference
}
func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
@@ -27,6 +30,7 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int)
styles: styles,
widthMargin: wm,
heightMargin: hm,
+ ref: repo.GetReference(),
}
b.SetSize(width, height)
return b
@@ -53,6 +57,9 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "R":
b.GotoTop()
}
+ case refs.RefMsg:
+ b.ref = msg
+ return b, b.setupCmd
}
rv, cmd := b.readmeViewport.Update(msg)
b.readmeViewport = rv.(*vp.ViewportBubble)
@@ -87,7 +94,7 @@ func (b *Bubble) glamourize() (string, error) {
func (b *Bubble) setupCmd() tea.Msg {
md, err := b.glamourize()
if err != nil {
- return types.ErrMsg{err}
+ return types.ErrMsg{Err: err}
}
b.readmeViewport.Viewport.SetContent(md)
b.GotoTop()
@@ -34,6 +34,7 @@ type Bubble struct {
widthMargin int
style *style.Styles
boxes []tea.Model
+ ref *plumbing.Reference
}
func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
@@ -46,6 +47,7 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int)
heightMargin: hm,
style: styles,
boxes: make([]tea.Model, 4),
+ ref: repo.GetReference(),
}
heightMargin := hm + lipgloss.Height(b.headerView())
b.boxes[aboutPage] = about.NewBubble(repo, b.style, b.width, wm, b.height, heightMargin)
@@ -63,7 +65,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case tea.KeyMsg:
- if b.repo.Name() != "config" {
+ if b.repo.GetName() != "config" {
switch msg.String() {
case "R":
b.state = aboutPage
@@ -87,6 +89,14 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case refs.RefMsg:
b.state = treePage
+ b.ref = msg
+ for i, bx := range b.boxes {
+ m, cmd := bx.Update(msg)
+ b.boxes[i] = m
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
}
m, cmd := b.boxes[b.state].Update(msg)
b.boxes[b.state] = m
@@ -99,7 +109,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (b *Bubble) Help() []types.HelpEntry {
h := []types.HelpEntry{}
h = append(h, b.boxes[b.state].(types.BubbleHelper).Help()...)
- if b.repo.Name() != "config" {
+ if b.repo.GetName() != "config" {
h = append(h, types.HelpEntry{"R", "readme"})
h = append(h, types.HelpEntry{"F", "files"})
h = append(h, types.HelpEntry{"C", "commits"})
@@ -109,7 +119,7 @@ func (b *Bubble) Help() []types.HelpEntry {
}
func (b *Bubble) Reference() plumbing.ReferenceName {
- return b.repo.GetReference().Name()
+ return b.ref.Name()
}
func (b *Bubble) headerView() string {
@@ -12,10 +12,12 @@ import (
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
gansi "github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
"github.com/charmbracelet/soft-serve/internal/tui/style"
"github.com/dustin/go-humanize/english"
+ "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
@@ -91,6 +93,7 @@ type Bubble struct {
list list.Model
state sessionState
commitViewport *vp.ViewportBubble
+ ref *plumbing.Reference
style *style.Styles
width int
widthMargin int
@@ -122,16 +125,23 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
height: height,
heightMargin: heightMargin,
list: l,
+ ref: repo.GetReference(),
}
b.SetSize(width, height)
return b
}
+func (b *Bubble) reset() tea.Cmd {
+ b.state = logState
+ b.list.Select(0)
+ return b.updateItems()
+}
+
func (b *Bubble) updateItems() tea.Cmd {
items := make([]list.Item, 0)
- cc, err := b.repo.GetCommits(0)
+ cc, err := b.repo.GetCommits(b.ref)
if err != nil {
- return func() tea.Msg { return types.ErrMsg{err} }
+ return func() tea.Msg { return types.ErrMsg{Err: err} }
}
for _, c := range cc {
items = append(items, item{c})
@@ -148,7 +158,7 @@ func (b *Bubble) GotoTop() {
}
func (b *Bubble) Init() tea.Cmd {
- return b.updateItems()
+ return b.reset()
}
func (b *Bubble) SetSize(width, height int) {
@@ -168,9 +178,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
case "C":
- b.state = logState
- b.list.Select(0)
- cmds = append(cmds, b.updateItems())
+ return b, b.reset()
case "enter", "right", "l":
if b.state == logState {
cmds = append(cmds, b.loadCommit())
@@ -189,6 +197,8 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
b.state = commitState
b.commitViewport.Viewport.SetContent(content)
b.GotoTop()
+ case refs.RefMsg:
+ b.ref = msg
}
switch b.state {
@@ -73,6 +73,7 @@ type Bubble struct {
widthMargin int
height int
heightMargin int
+ ref *plumbing.Reference
}
func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
@@ -92,14 +93,15 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
widthMargin: widthMargin,
heightMargin: heightMargin,
list: l,
+ ref: repo.GetReference(),
}
b.SetSize(width, height)
return b
}
func (b *Bubble) SetBranch(ref *plumbing.Reference) (tea.Model, tea.Cmd) {
+ b.ref = ref
return b, func() tea.Msg {
- b.repo.SetReference(ref)
return RefMsg(ref)
}
}
@@ -121,7 +123,7 @@ func (b *Bubble) Help() []types.HelpEntry {
func (b *Bubble) updateItems() tea.Cmd {
its := make(items, 0)
tags := make(items, 0)
- ri, err := b.repo.Repository().References()
+ ri, err := b.repo.GetRepository().References()
if err != nil {
return nil
}
@@ -128,6 +128,7 @@ type Bubble struct {
error types.ErrMsg
fileViewport *vp.ViewportBubble
lastSelected []int
+ ref *plumbing.Reference
}
func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
@@ -153,6 +154,7 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
heightMargin: heightMargin,
list: l,
state: treeState,
+ ref: repo.GetReference(),
}
b.SetSize(width, height)
return b
@@ -183,7 +185,7 @@ func (b *Bubble) Help() []types.HelpEntry {
func (b *Bubble) updateItems() tea.Cmd {
its := make(items, 0)
- t, err := b.repo.Tree(b.path)
+ t, err := b.repo.Tree(b.ref, b.path)
if err != nil {
return func() tea.Msg { return types.ErrMsg{err} }
}
@@ -219,6 +221,14 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
b.SetSize(msg.Width, msg.Height)
case tea.KeyMsg:
+ if b.state == errorState {
+ ref := b.repo.GetReference()
+ b.ref = ref
+ return b, tea.Batch(b.reset(), func() tea.Msg {
+ return ref
+ })
+ }
+
switch msg.String() {
case "F":
return b, b.reset()
@@ -252,6 +262,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case refs.RefMsg:
+ b.ref = msg
return b, b.reset()
case types.ErrMsg:
@@ -7,13 +7,13 @@ import (
)
type Repo interface {
- Name() string
+ GetName() string
GetReference() *plumbing.Reference
SetReference(*plumbing.Reference) error
GetReadme() string
- GetCommits(limit int) (Commits, error)
- Repository() *git.Repository
- Tree(path string) (*object.Tree, error)
+ GetCommits(*plumbing.Reference) (Commits, error)
+ GetRepository() *git.Repository
+ Tree(*plumbing.Reference, string) (*object.Tree, error)
}
type Commit struct {
@@ -0,0 +1,7 @@
+package types
+
+import tea "github.com/charmbracelet/bubbletea"
+
+type BubbleReset interface {
+ Reset() tea.Msg
+}
@@ -96,9 +96,6 @@ func (b *Bubble) menuEntriesFromSource() ([]MenuEntry, error) {
}
func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
- gr := &Repo{
- name: rn,
- }
me := MenuEntry{Name: name, Repo: rn}
r, err := b.config.Source.GetRepo(rn)
if err != nil {
@@ -111,19 +108,13 @@ func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
}
r.Readme = md
}
- gr.repo = r.Repository
- gr.readme = r.Readme
- gr.ref, err = r.Repository.Head()
- if err != nil {
- return me, err
- }
boxLeftWidth := b.styles.Menu.GetWidth() + b.styles.Menu.GetHorizontalFrameSize()
// TODO: also send this along with a tea.WindowSizeMsg
var heightMargin = lipgloss.Height(b.headerView()) +
lipgloss.Height(b.footerView()) +
b.styles.RepoBody.GetVerticalFrameSize() +
b.styles.App.GetVerticalMargins()
- rb := repo.NewBubble(rn, b.config.Host, b.config.Port, gr, b.styles, b.width, boxLeftWidth, b.height, heightMargin)
+ rb := repo.NewBubble(rn, b.config.Host, b.config.Port, r, b.styles, b.width, boxLeftWidth, b.height, heightMargin)
initCmd := rb.Init()
msg := initCmd()
switch msg := msg.(type) {
@@ -1,105 +0,0 @@
-package tui
-
-import (
- "path/filepath"
-
- gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
-)
-
-type Repo struct {
- name string
- repo *git.Repository
- readme string
- ref *plumbing.Reference
-}
-
-func (r *Repo) Name() string {
- return r.name
-}
-
-func (r *Repo) GetReference() *plumbing.Reference {
- return r.ref
-}
-
-func (r *Repo) SetReference(ref *plumbing.Reference) error {
- r.ref = ref
- return nil
-}
-
-func (r *Repo) Repository() *git.Repository {
- return r.repo
-}
-
-func (r *Repo) Tree(path string) (*object.Tree, error) {
- path = filepath.Clean(path)
- c, err := r.repo.CommitObject(r.ref.Hash())
- if err != nil {
- return nil, err
- }
- t, err := c.Tree()
- if err != nil {
- return nil, err
- }
- if path == "." {
- return t, nil
- }
- return t.Tree(path)
-}
-
-func (r *Repo) GetCommits(limit int) (gitypes.Commits, error) {
- commits := gitypes.Commits{}
- l, err := r.repo.Log(&git.LogOptions{
- Order: git.LogOrderCommitterTime,
- From: r.ref.Hash(),
- })
- if err != nil {
- return nil, err
- }
- err = l.ForEach(func(c *object.Commit) error {
- commits = append(commits, &gitypes.Commit{c})
- return nil
- })
- if err != nil {
- return nil, err
- }
- if limit <= 0 || limit > len(commits) {
- limit = len(commits)
- }
- return commits[:limit], nil
-}
-
-func (r *Repo) GetReadme() string {
- if r.readme != "" {
- return r.readme
- }
- md, err := r.readFile("README.md")
- if err != nil {
- return ""
- }
- return md
-}
-
-func (r *Repo) readFile(path string) (string, error) {
- lg, err := r.repo.Log(&git.LogOptions{
- From: r.ref.Hash(),
- })
- if err != nil {
- return "", err
- }
- c, err := lg.Next()
- if err != nil {
- return "", err
- }
- f, err := c.File(path)
- if err != nil {
- return "", err
- }
- content, err := f.Contents()
- if err != nil {
- return "", err
- }
- return content, nil
-}