3308f43a6e40266947abfc54223603240d5ab26a

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

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

Diff

This diff is truncated to protect this page.

  1diff --git a/internal/git/git.go b/internal/git/git.go
  2index 16931421dbddafa113dff031539d5b14fc901f1a..fbfa102503fbe978c07756f0bb9d7dd35d22ab1b 100644
  3--- a/internal/git/git.go
  4+++ b/internal/git/git.go
  5@@ -9,8 +9,10 @@ import (
  6 	"sync"
  7 	"time"
  8 
  9+	gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
 10 	"github.com/go-git/go-billy/v5/memfs"
 11 	"github.com/go-git/go-git/v5"
 12+	"github.com/go-git/go-git/v5/plumbing"
 13 	"github.com/go-git/go-git/v5/plumbing/object"
 14 	"github.com/go-git/go-git/v5/plumbing/transport"
 15 	"github.com/go-git/go-git/v5/storage/memory"
 16@@ -25,22 +27,147 @@ type Repo struct {
 17 	Repository  *git.Repository
 18 	Readme      string
 19 	LastUpdated *time.Time
 20-	commits     CommitLog
 21+	refCommits  map[plumbing.Hash]gitypes.Commits
 22+	ref         *plumbing.Reference
 23 }
 24 
 25-// RepoCommit contains metadata for a Git commit.
 26-type RepoCommit struct {
 27-	Name   string
 28-	Commit *object.Commit
 29+// GetName returns the name of the repository.
 30+func (r *Repo) GetName() string {
 31+	return r.Name
 32 }
 33 
 34-// CommitLog is a series of Git commits.
 35-type CommitLog []RepoCommit
 36+// GetReference returns the reference for a repository.
 37+func (r *Repo) GetReference() *plumbing.Reference {
 38+	return r.ref
 39+}
 40+
 41+// SetReference sets the repository head reference.
 42+func (r *Repo) SetReference(ref *plumbing.Reference) error {
 43+	r.ref = ref
 44+	return nil
 45+}
 46+
 47+// GetRepository returns the underlying go-git repository object.
 48+func (r *Repo) GetRepository() *git.Repository {
 49+	return r.Repository
 50+}
 51+
 52+// Tree returns the git tree for a given path.
 53+func (r *Repo) Tree(ref *plumbing.Reference, path string) (*object.Tree, error) {
 54+	path = filepath.Clean(path)
 55+	hash, err := r.targetHash(ref)
 56+	if err != nil {
 57+		return nil, err
 58+	}
 59+	c, err := r.Repository.CommitObject(hash)
 60+	if err != nil {
 61+		return nil, err
 62+	}
 63+	t, err := c.Tree()
 64+	if err != nil {
 65+		return nil, err
 66+	}
 67+	if path == "." {
 68+		return t, nil
 69+	}
 70+	return t.Tree(path)
 71+}
 72 
 73-func (cl CommitLog) Len() int      { return len(cl) }
 74-func (cl CommitLog) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
 75-func (cl CommitLog) Less(i, j int) bool {
 76-	return cl[i].Commit.Author.When.After(cl[j].Commit.Author.When)
 77+// GetCommits returns the commits for a repository.
 78+func (r *Repo) GetCommits(ref *plumbing.Reference) (gitypes.Commits, error) {
 79+	hash, err := r.targetHash(ref)
 80+	if err != nil {
 81+		return nil, err
 82+	}
 83+	// return cached commits if available
 84+	commits, ok := r.refCommits[hash]
 85+	if ok {
 86+		return commits, nil
 87+	}
 88+	log.Printf("caching commits for %s/%s: %s", r.Name, ref.Name(), ref.Hash())
 89+	commits = gitypes.Commits{}
 90+	co, err := r.Repository.CommitObject(hash)
 91+	if err != nil {
 92+		return nil, err
 93+	}
 94+	// traverse the commit tree to get all commits
 95+	commits = append(commits, &gitypes.Commit{Commit: co})
 96+	for {
 97+		co, err = co.Parent(0)
 98+		if err != nil {
 99+			if err == object.ErrParentNotFound {
100+				err = nil
101+			}
102+			break
103+		}
104+		commits = append(commits, &gitypes.Commit{Commit: co})
105diff --git a/internal/tui/bubbles/git/about/bubble.go b/internal/tui/bubbles/git/about/bubble.go
106index 9b3e84f86012b05acb569299a0665a0aeb4a9340..17ad0dbdc1903786faad95e8e6761b6d0d9a8798 100644
107--- a/internal/tui/bubbles/git/about/bubble.go
108+++ b/internal/tui/bubbles/git/about/bubble.go
109@@ -3,9 +3,11 @@ package about
110 import (
111 	"github.com/charmbracelet/bubbles/viewport"
112 	tea "github.com/charmbracelet/bubbletea"
113+	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
114 	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
115 	vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
116 	"github.com/charmbracelet/soft-serve/internal/tui/style"
117+	"github.com/go-git/go-git/v5/plumbing"
118 )
119 
120 type Bubble struct {
121@@ -16,6 +18,7 @@ type Bubble struct {
122 	heightMargin   int
123 	width          int
124 	widthMargin    int
125+	ref            *plumbing.Reference
126 }
127 
128 func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
129@@ -27,6 +30,7 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int)
130 		styles:       styles,
131 		widthMargin:  wm,
132 		heightMargin: hm,
133+		ref:          repo.GetReference(),
134 	}
135 	b.SetSize(width, height)
136 	return b
137@@ -53,6 +57,9 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
138 		case "R":
139 			b.GotoTop()
140 		}
141+	case refs.RefMsg:
142+		b.ref = msg
143+		return b, b.setupCmd
144 	}
145 	rv, cmd := b.readmeViewport.Update(msg)
146 	b.readmeViewport = rv.(*vp.ViewportBubble)
147@@ -87,7 +94,7 @@ func (b *Bubble) glamourize() (string, error) {
148 func (b *Bubble) setupCmd() tea.Msg {
149 	md, err := b.glamourize()
150 	if err != nil {
151-		return types.ErrMsg{err}
152+		return types.ErrMsg{Err: err}
153 	}
154 	b.readmeViewport.Viewport.SetContent(md)
155 	b.GotoTop()
156diff --git a/internal/tui/bubbles/git/bubble.go b/internal/tui/bubbles/git/bubble.go
157index 9e26e4567fcbe1782dae844ce4aabfd6184e6d2b..8bec715b8feb344db683de02d1c75494d8088cd0 100644
158--- a/internal/tui/bubbles/git/bubble.go
159+++ b/internal/tui/bubbles/git/bubble.go
160@@ -34,6 +34,7 @@ type Bubble struct {
161 	widthMargin  int
162 	style        *style.Styles
163 	boxes        []tea.Model
164+	ref          *plumbing.Reference
165 }
166 
167 func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
168@@ -46,6 +47,7 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int)
169 		heightMargin: hm,
170 		style:        styles,
171 		boxes:        make([]tea.Model, 4),
172+		ref:          repo.GetReference(),
173 	}
174 	heightMargin := hm + lipgloss.Height(b.headerView())
175 	b.boxes[aboutPage] = about.NewBubble(repo, b.style, b.width, wm, b.height, heightMargin)
176@@ -63,7 +65,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
177 	cmds := make([]tea.Cmd, 0)
178 	switch msg := msg.(type) {
179 	case tea.KeyMsg:
180-		if b.repo.Name() != "config" {
181+		if b.repo.GetName() != "config" {
182 			switch msg.String() {
183 			case "R":
184 				b.state = aboutPage
185@@ -87,6 +89,14 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
186 		}
187 	case refs.RefMsg:
188 		b.state = treePage
189+		b.ref = msg
190+		for i, bx := range b.boxes {
191+			m, cmd := bx.Update(msg)
192+			b.boxes[i] = m
193+			if cmd != nil {
194+				cmds = append(cmds, cmd)
195+			}
196+		}
197 	}
198 	m, cmd := b.boxes[b.state].Update(msg)
199 	b.boxes[b.state] = m
200@@ -99,7 +109,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
201 func (b *Bubble) Help() []types.HelpEntry {
202 	h := []types.HelpEntry{}
203 	h = append(h, b.boxes[b.state].(types.BubbleHelper).Help()...)
204-	if b.repo.Name() != "config" {
205+	if b.repo.GetName() != "config" {
206 		h = append(h, types.HelpEntry{"R", "readme"})
207 		h = append(h, types.HelpEntry{"F", "files"})
208 		h = append(h, types.HelpEntry{"C", "commits"})
209@@ -109,7 +119,7 @@ func (b *Bubble) Help() []types.HelpEntry {
210 }
211 
212 func (b *Bubble) Reference() plumbing.ReferenceName {
213-	return b.repo.GetReference().Name()
214+	return b.ref.Name()
215 }
216 
217 func (b *Bubble) headerView() string {
218diff --git a/internal/tui/bubbles/git/log/bubble.go b/internal/tui/bubbles/git/log/bubble.go
219index 3299ca211d9a0f226028f08895cbbeb91505cc9a..d6928889d76fcbeea298f066f285e3b8fcbdda21 100644
220--- a/internal/tui/bubbles/git/log/bubble.go
221+++ b/internal/tui/bubbles/git/log/bubble.go
222@@ -12,10 +12,12 @@ import (
223 	"github.com/charmbracelet/bubbles/viewport"
224 	tea "github.com/charmbracelet/bubbletea"
225 	gansi "github.com/charmbracelet/glamour/ansi"
226+	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
227 	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
228 	vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
229 	"github.com/charmbracelet/soft-serve/internal/tui/style"
230 	"github.com/dustin/go-humanize/english"
231+	"github.com/go-git/go-git/v5/plumbing"
232 	"github.com/go-git/go-git/v5/plumbing/object"
233 )
234 
235@@ -91,6 +93,7 @@ type Bubble struct {
236 	list           list.Model
237 	state          sessionState
238 	commitViewport *vp.ViewportBubble
239+	ref            *plumbing.Reference
240 	style          *style.Styles
241 	width          int
242 	widthMargin    int
243@@ -122,16 +125,23 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
244 		height:       height,
245 		heightMargin: heightMargin,
246 		list:         l,
247+		ref:          repo.GetReference(),
248 	}
249 	b.SetSize(width, height)
250 	return b
251 }
252 
253+func (b *Bubble) reset() tea.Cmd {
254+	b.state = logState
255+	b.list.Select(0)
256+	return b.updateItems()
257+}
258+
259 func (b *Bubble) updateItems() tea.Cmd {
260 	items := make([]list.Item, 0)
261-	cc, err := b.repo.GetCommits(0)
262+	cc, err := b.repo.GetCommits(b.ref)
263 	if err != nil {
264-		return func() tea.Msg { return types.ErrMsg{err} }
265+		return func() tea.Msg { return types.ErrMsg{Err: err} }
266 	}
267 	for _, c := range cc {
268 		items = append(items, item{c})
269@@ -148,7 +158,7 @@ func (b *Bubble) GotoTop() {
270 }
271 
272 func (b *Bubble) Init() tea.Cmd {
273-	return b.updateItems()
274+	return b.reset()
275 }
276 
277 func (b *Bubble) SetSize(width, height int) {
278@@ -168,9 +178,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
279 	case tea.KeyMsg:
280 		switch msg.String() {
281 		case "C":
282-			b.state = logState
283-			b.list.Select(0)
284-			cmds = append(cmds, b.updateItems())
285+			return b, b.reset()
286 		case "enter", "right", "l":
287 			if b.state == logState {
288 				cmds = append(cmds, b.loadCommit())
289@@ -189,6 +197,8 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
290 		b.state = commitState
291 		b.commitViewport.Viewport.SetContent(content)
292 		b.GotoTop()
293+	case refs.RefMsg:
294+		b.ref = msg
295 	}
296 
297 	switch b.state {
298diff --git a/internal/tui/bubbles/git/refs/bubble.go b/internal/tui/bubbles/git/refs/bubble.go
299index bd4b2f8e83fd713a8d2dae394b325ef547a8e249..ac9cf285ec351b2952809dd930c0af00351c446d 100644
300--- a/internal/tui/bubbles/git/refs/bubble.go
301+++ b/internal/tui/bubbles/git/refs/bubble.go
302@@ -73,6 +73,7 @@ type Bubble struct {
303 	widthMargin  int
304 	height       int
305 	heightMargin int
306+	ref          *plumbing.Reference
307 }
308 
309 func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
310@@ -92,14 +93,15 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
311 		widthMargin:  widthMargin,
312 		heightMargin: heightMargin,
313 		list:         l,
314+		ref:          repo.GetReference(),
315 	}
316 	b.SetSize(width, height)
317 	return b
318 }
319 
320 func (b *Bubble) SetBranch(ref *plumbing.Reference) (tea.Model, tea.Cmd) {
321+	b.ref = ref
322 	return b, func() tea.Msg {
323-		b.repo.SetReference(ref)
324 		return RefMsg(ref)
325 	}
326 }
327@@ -121,7 +123,7 @@ func (b *Bubble) Help() []types.HelpEntry {
328 func (b *Bubble) updateItems() tea.Cmd {
329 	its := make(items, 0)
330 	tags := make(items, 0)
331-	ri, err := b.repo.Repository().References()
332+	ri, err := b.repo.GetRepository().References()
333 	if err != nil {
334 		return nil
335 	}
336diff --git a/internal/tui/bubbles/git/tree/bubble.go b/internal/tui/bubbles/git/tree/bubble.go
337index 926ff77cc9c4057136fa42b7ec9908f6e7a4edd4..e8d4e0924eb8276c4046fb5ba3fa0c2a233fcef0 100644
338--- a/internal/tui/bubbles/git/tree/bubble.go
339+++ b/internal/tui/bubbles/git/tree/bubble.go
340@@ -128,6 +128,7 @@ type Bubble struct {
341 	error        types.ErrMsg
342 	fileViewport *vp.ViewportBubble
343 	lastSelected []int
344+	ref          *plumbing.Reference
345 }
346 
347 func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
348@@ -153,6 +154,7 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
349 		heightMargin: heightMargin,
350 		list:         l,
351 		state:        treeState,
352+		ref:          repo.GetReference(),
353 	}
354 	b.SetSize(width, height)
355 	return b
356@@ -183,7 +185,7 @@ func (b *Bubble) Help() []types.HelpEntry {
357 
358 func (b *Bubble) updateItems() tea.Cmd {
359 	its := make(items, 0)
360-	t, err := b.repo.Tree(b.path)
361+	t, err := b.repo.Tree(b.ref, b.path)
362 	if err != nil {
363 		return func() tea.Msg { return types.ErrMsg{err} }
364 	}
365@@ -219,6 +221,14 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
366 		b.SetSize(msg.Width, msg.Height)
367 
368 	case tea.KeyMsg:
369+		if b.state == errorState {
370+			ref := b.repo.GetReference()
371+			b.ref = ref
372+			return b, tea.Batch(b.reset(), func() tea.Msg {
373+				return ref
374+			})
375+		}
376+
377 		switch msg.String() {
378 		case "F":
379 			return b, b.reset()
380@@ -252,6 +262,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
381 		}
382 
383 	case refs.RefMsg:
384+		b.ref = msg
385 		return b, b.reset()
386 
387 	case types.ErrMsg:
388diff --git a/internal/tui/bubbles/git/types/git.go b/internal/tui/bubbles/git/types/git.go
389index 8e0213871b9bc7e868e15dfa9fd87b5a44fb88e7..9cff00a5b2ef8a00891b2cdd9126b53dd2f439f1 100644
390--- a/internal/tui/bubbles/git/types/git.go
391+++ b/internal/tui/bubbles/git/types/git.go
392@@ -7,13 +7,13 @@ import (
393 )
394 
395 type Repo interface {
396-	Name() string
397+	GetName() string
398 	GetReference() *plumbing.Reference
399 	SetReference(*plumbing.Reference) error
400 	GetReadme() string
401-	GetCommits(limit int) (Commits, error)
402-	Repository() *git.Repository
403-	Tree(path string) (*object.Tree, error)
404+	GetCommits(*plumbing.Reference) (Commits, error)
405+	GetRepository() *git.Repository
406+	Tree(*plumbing.Reference, string) (*object.Tree, error)
407 }
408 
409 type Commit struct {
410diff --git a/internal/tui/bubbles/git/types/reset.go b/internal/tui/bubbles/git/types/reset.go
411new file mode 100644
412index 0000000000000000000000000000000000000000..919ab3d896bcc1dfa7d677f7e5df94d9a6ae6a4e
413--- /dev/null
414+++ b/internal/tui/bubbles/git/types/reset.go
415@@ -0,0 +1,7 @@
416+package types
417+
418+import tea "github.com/charmbracelet/bubbletea"
419+
420+type BubbleReset interface {
421+	Reset() tea.Msg
422+}
423diff --git a/internal/tui/commands.go b/internal/tui/commands.go
424index d8879e0873903f25884d9d6a1f33a66b191deeeb..4a6f34507b22c3141a6035951e90f5c133cacb8c 100644
425--- a/internal/tui/commands.go
426+++ b/internal/tui/commands.go
427@@ -96,9 +96,6 @@ func (b *Bubble) menuEntriesFromSource() ([]MenuEntry, error) {
428 }
429 
430 func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
431-	gr := &Repo{
432-		name: rn,
433-	}
434 	me := MenuEntry{Name: name, Repo: rn}
435 	r, err := b.config.Source.GetRepo(rn)
436 	if err != nil {
437@@ -111,19 +108,13 @@ func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
438 		}
439 		r.Readme = md
440 	}
441-	gr.repo = r.Repository
442-	gr.readme = r.Readme
443-	gr.ref, err = r.Repository.Head()
444-	if err != nil {
445-		return me, err
446-	}
447 	boxLeftWidth := b.styles.Menu.GetWidth() + b.styles.Menu.GetHorizontalFrameSize()
448 	// TODO: also send this along with a tea.WindowSizeMsg
449 	var heightMargin = lipgloss.Height(b.headerView()) +
450 		lipgloss.Height(b.footerView()) +
451 		b.styles.RepoBody.GetVerticalFrameSize() +
452 		b.styles.App.GetVerticalMargins()
453-	rb := repo.NewBubble(rn, b.config.Host, b.config.Port, gr, b.styles, b.width, boxLeftWidth, b.height, heightMargin)
454+	rb := repo.NewBubble(rn, b.config.Host, b.config.Port, r, b.styles, b.width, boxLeftWidth, b.height, heightMargin)
455 	initCmd := rb.Init()
456 	msg := initCmd()
457 	switch msg := msg.(type) {
458diff --git a/internal/tui/git.go b/internal/tui/git.go
459deleted file mode 100644
460index f8105a695317d734ddb4502ee04cb34dd3253968..0000000000000000000000000000000000000000
461--- a/internal/tui/git.go
462+++ /dev/null
463@@ -1,105 +0,0 @@
464-package tui
465-
466-import (
467-	"path/filepath"
468-
469-	gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
470-	"github.com/go-git/go-git/v5"
471-	"github.com/go-git/go-git/v5/plumbing"
472-	"github.com/go-git/go-git/v5/plumbing/object"
473-)
474-
475-type Repo struct {
476-	name   string
477-	repo   *git.Repository
478-	readme string
479-	ref    *plumbing.Reference
480-}
481-
482-func (r *Repo) Name() string {
483-	return r.name
484-}
485-
486-func (r *Repo) GetReference() *plumbing.Reference {
487-	return r.ref
488-}
489-
490-func (r *Repo) SetReference(ref *plumbing.Reference) error {
491-	r.ref = ref
492-	return nil
493-}
494-
495-func (r *Repo) Repository() *git.Repository {
496-	return r.repo
497-}
498-
499-func (r *Repo) Tree(path string) (*object.Tree, error) {
500-	path = filepath.Clean(path)
501-	c, err := r.repo.CommitObject(r.ref.Hash())
502-	if err != nil {
503-		return nil, err
504-	}
505-	t, err := c.Tree()
506-	if err != nil {
507-		return nil, err
508-	}
509-	if path == "." {
510-		return t, nil
511-	}
512-	return t.Tree(path)
513-}
514-
515-func (r *Repo) GetCommits(limit int) (gitypes.Commits, error) {
516-	commits := gitypes.Commits{}
517-	l, err := r.repo.Log(&git.LogOptions{
518-		Order: git.LogOrderCommitterTime,
519-		From:  r.ref.Hash(),
520-	})
521-	if err != nil {
522-		return nil, err
523-	}
524-	err = l.ForEach(func(c *object.Commit) error {
525-		commits = append(commits, &gitypes.Commit{c})
526-		return nil
527-	})
528-	if err != nil {
529-		return nil, err
530-	}
531-	if limit <= 0 || limit > len(commits) {
532-		limit = len(commits)
533-	}
534-	return commits[:limit], nil
535-}
536-
537-func (r *Repo) GetReadme() string {
538-	if r.readme != "" {
539-		return r.readme
540-	}
541-	md, err := r.readFile("README.md")
542-	if err != nil {
543-		return ""
544-	}
545-	return md
546-}
547-
548-func (r *Repo) readFile(path string) (string, error) {
549-	lg, err := r.repo.Log(&git.LogOptions{
550-		From: r.ref.Hash(),
551-	})
552-	if err != nil {
553-		return "", err
554-	}
555-	c, err := lg.Next()
556-	if err != nil {
557-		return "", err
558-	}
559-	f, err := c.File(path)
560-	if err != nil {
561-		return "", err
562-	}