From 3308f43a6e40266947abfc54223603240d5ab26a Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 16 Feb 2022 16:02:37 -0500 Subject: [PATCH] fix(tui): cache ref commits and track annotated tags * Keep track of the selected ref in each bubble * Lazy cache commits of each ref on demand * Find the target hash of annotated tags --- internal/git/git.go | 191 ++++++++++++++++++----- internal/tui/bubbles/git/about/bubble.go | 9 +- internal/tui/bubbles/git/bubble.go | 16 +- internal/tui/bubbles/git/log/bubble.go | 22 ++- internal/tui/bubbles/git/refs/bubble.go | 6 +- internal/tui/bubbles/git/tree/bubble.go | 13 +- internal/tui/bubbles/git/types/git.go | 8 +- internal/tui/bubbles/git/types/reset.go | 7 + internal/tui/commands.go | 11 +- internal/tui/git.go | 105 ------------- 10 files changed, 215 insertions(+), 173 deletions(-) create mode 100644 internal/tui/bubbles/git/types/reset.go delete mode 100644 internal/tui/git.go diff --git a/internal/git/git.go b/internal/git/git.go index 16931421d..fbfa10250 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -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 } diff --git a/internal/tui/bubbles/git/about/bubble.go b/internal/tui/bubbles/git/about/bubble.go index 9b3e84f86..17ad0dbdc 100644 --- a/internal/tui/bubbles/git/about/bubble.go +++ b/internal/tui/bubbles/git/about/bubble.go @@ -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() diff --git a/internal/tui/bubbles/git/bubble.go b/internal/tui/bubbles/git/bubble.go index 9e26e4567..8bec715b8 100644 --- a/internal/tui/bubbles/git/bubble.go +++ b/internal/tui/bubbles/git/bubble.go @@ -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 { diff --git a/internal/tui/bubbles/git/log/bubble.go b/internal/tui/bubbles/git/log/bubble.go index 3299ca211..d6928889d 100644 --- a/internal/tui/bubbles/git/log/bubble.go +++ b/internal/tui/bubbles/git/log/bubble.go @@ -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 { diff --git a/internal/tui/bubbles/git/refs/bubble.go b/internal/tui/bubbles/git/refs/bubble.go index bd4b2f8e8..ac9cf285e 100644 --- a/internal/tui/bubbles/git/refs/bubble.go +++ b/internal/tui/bubbles/git/refs/bubble.go @@ -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 } diff --git a/internal/tui/bubbles/git/tree/bubble.go b/internal/tui/bubbles/git/tree/bubble.go index 926ff77cc..e8d4e0924 100644 --- a/internal/tui/bubbles/git/tree/bubble.go +++ b/internal/tui/bubbles/git/tree/bubble.go @@ -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: diff --git a/internal/tui/bubbles/git/types/git.go b/internal/tui/bubbles/git/types/git.go index 8e0213871..9cff00a5b 100644 --- a/internal/tui/bubbles/git/types/git.go +++ b/internal/tui/bubbles/git/types/git.go @@ -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 { diff --git a/internal/tui/bubbles/git/types/reset.go b/internal/tui/bubbles/git/types/reset.go new file mode 100644 index 000000000..919ab3d89 --- /dev/null +++ b/internal/tui/bubbles/git/types/reset.go @@ -0,0 +1,7 @@ +package types + +import tea "github.com/charmbracelet/bubbletea" + +type BubbleReset interface { + Reset() tea.Msg +} diff --git a/internal/tui/commands.go b/internal/tui/commands.go index d8879e087..4a6f34507 100644 --- a/internal/tui/commands.go +++ b/internal/tui/commands.go @@ -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) { diff --git a/internal/tui/git.go b/internal/tui/git.go deleted file mode 100644 index f8105a695..000000000 --- a/internal/tui/git.go +++ /dev/null @@ -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 -}