Skip to content

Commit

Permalink
fix(tui): cache ref commits and track annotated tags
Browse files Browse the repository at this point in the history
* Keep track of the selected ref in each bubble
* Lazy cache commits of each ref on demand
* Find the target hash of annotated tags
  • Loading branch information
aymanbagabas committed Feb 17, 2022
1 parent 73fd14e commit 3308f43
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 173 deletions.
191 changes: 150 additions & 41 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
Expand Down
9 changes: 8 additions & 1 deletion internal/tui/bubbles/git/about/bubble.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
16 changes: 13 additions & 3 deletions internal/tui/bubbles/git/bubble.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"})
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 3308f43

Please sign in to comment.