37950203a7110c0061aed869deaec1cf3b486c2f

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

Message

feat: show spinner when loading a large commit diff

Diff

This diff is truncated to protect this page.

  1diff --git a/internal/git/git.go b/internal/git/git.go
  2index cf2b09d37bfdbb3bcea2ae0ec3daab12e26aa222..7cbd469849e185d641c700fae01a0a74a72c8e2e 100644
  3--- a/internal/git/git.go
  4+++ b/internal/git/git.go
  5@@ -1,6 +1,7 @@
  6 package git
  7 
  8 import (
  9+	"context"
 10 	"errors"
 11 	"log"
 12 	"os"
 13@@ -30,6 +31,7 @@ type Repo struct {
 14 	refs       []*plumbing.Reference
 15 	trees      map[plumbing.Hash]*object.Tree
 16 	commits    map[plumbing.Hash]*object.Commit
 17+	patch      map[plumbing.Hash]*object.Patch
 18 }
 19 
 20 // GetName returns the name of the repository.
 21@@ -105,6 +107,40 @@ func (r *Repo) commitForHash(hash plumbing.Hash) (*object.Commit, error) {
 22 	return co, nil
 23 }
 24 
 25+func (r *Repo) PatchCtx(ctx context.Context, commit *object.Commit) (*object.Patch, error) {
 26+	hash := commit.Hash
 27+	p, ok := r.patch[hash]
 28+	if !ok {
 29+		c, err := r.commitForHash(hash)
 30+		if err != nil {
 31+			return nil, err
 32+		}
 33+		// Using commit trees fixes the issue when generating diff for the first commit
 34+		// https://github.com/go-git/go-git/issues/281
 35+		tree, err := r.treeForHash(c.TreeHash)
 36+		if err != nil {
 37+			return nil, err
 38+		}
 39+		var parent *object.Commit
 40+		parentTree := &object.Tree{}
 41+		if c.NumParents() > 0 {
 42+			parent, err = r.commitForHash(c.ParentHashes[0])
 43+			if err != nil {
 44+				return nil, err
 45+			}
 46+			parentTree, err = r.treeForHash(parent.TreeHash)
 47+			if err != nil {
 48+				return nil, err
 49+			}
 50+		}
 51+		p, err = parentTree.PatchContext(ctx, tree)
 52+		if err != nil {
 53+			return nil, err
 54+		}
 55+	}
 56+	return p, nil
 57+}
 58+
 59 // GetCommits returns the commits for a repository.
 60 func (r *Repo) GetCommits(ref *plumbing.Reference) (gitypes.Commits, error) {
 61 	hash, err := r.targetHash(ref)
 62@@ -264,6 +300,7 @@ func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
 63 	r := &Repo{
 64 		name:       name,
 65 		repository: rg,
 66+		patch:      make(map[plumbing.Hash]*object.Patch),
 67 	}
 68 	r.commits = make(map[plumbing.Hash]*object.Commit)
 69 	r.trees = make(map[plumbing.Hash]*object.Tree)
 70diff --git a/internal/tui/bubbles/git/log/bubble.go b/internal/tui/bubbles/git/log/bubble.go
 71index 600011791a4424921cd0a12d8f4daf6ef0e3bf0c..95e6b7b5598a937b3a7c1b762be43f0b8150d1fa 100644
 72--- a/internal/tui/bubbles/git/log/bubble.go
 73+++ b/internal/tui/bubbles/git/log/bubble.go
 74@@ -9,6 +9,7 @@ import (
 75 	"time"
 76 
 77 	"github.com/charmbracelet/bubbles/list"
 78+	"github.com/charmbracelet/bubbles/spinner"
 79 	"github.com/charmbracelet/bubbles/viewport"
 80 	tea "github.com/charmbracelet/bubbletea"
 81 	gansi "github.com/charmbracelet/glamour/ansi"
 82@@ -26,21 +27,17 @@ var (
 83 		Code:     "",
 84 		Language: "diff",
 85 	}
 86+	waitBeforeLoading = time.Millisecond * 300
 87 )
 88 
 89-type commitMsg struct {
 90-	commit     *object.Commit
 91-	parent     *object.Commit
 92-	tree       *object.Tree
 93-	parentTree *object.Tree
 94-	patch      *object.Patch
 95-}
 96+type commitMsg *object.Commit
 97 
 98 type sessionState int
 99 
100 const (
101 	logState sessionState = iota
102 	commitState
103+	loadingState
104 	errorState
105 )
106 
107@@ -100,6 +97,7 @@ type Bubble struct {
108 	height         int
109 	heightMargin   int
110 	error          types.ErrMsg
111+	spinner        spinner.Model
112 }
113 
114 func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
115@@ -113,6 +111,9 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
116 	l.DisableQuitKeybindings()
117 	l.KeyMap.NextPage = types.NextPage
118 	l.KeyMap.PrevPage = types.PrevPage
119+	s := spinner.New()
120+	s.Spinner = spinner.Dot
121+	s.Style = styles.Spinner
122 	b := &Bubble{
123 		commitViewport: &vp.ViewportBubble{
124 			Viewport: &viewport.Model{},
125@@ -126,6 +127,7 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
126 		heightMargin: heightMargin,
127 		list:         l,
128 		ref:          repo.GetHEAD(),
129+		spinner:      s,
130 	}
131 	b.SetSize(width, height)
132 	return b
133@@ -193,12 +195,19 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
134 		b.state = errorState
135 		return b, nil
136 	case commitMsg:
137-		content := b.renderCommit(msg)
138-		b.state = commitState
139-		b.commitViewport.Viewport.SetContent(content)
140-		b.GotoTop()
141+		if b.state == loadingState {
142+			cmds = append(cmds, b.spinner.Tick)
143+		}
144 	case refs.RefMsg:
145 		b.ref = msg
146+	case spinner.TickMsg:
147+		if b.state == loadingState {
148+			s, cmd := b.spinner.Update(msg)
149+			if cmd != nil {
150+				cmds = append(cmds, cmd)
151+			}
152+			b.spinner = s
153+		}
154 	}
155 
156 	switch b.state {
157@@ -215,83 +224,77 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
158 	return b, tea.Batch(cmds...)
159 }
160 
161+func (b *Bubble) loadPatch(c *object.Commit) error {
162+	var patch strings.Builder
163+	style := b.style.LogCommit.Copy().Width(b.width - b.widthMargin - b.style.LogCommit.GetHorizontalFrameSize())
164+	ctx, cancel := context.WithTimeout(context.TODO(), types.MaxPatchWait)
165+	defer cancel()
166+	p, err := b.repo.PatchCtx(ctx, c)
167+	if err != nil {
168+		return err
169+	}
170+	patch.WriteString(b.renderCommit(c))
171+	fpl := len(p.FilePatches())
172+	if fpl > types.MaxDiffFiles {
173+		patch.WriteString("\n" + types.ErrDiffFilesTooLong.Error())
174diff --git a/internal/tui/bubbles/git/types/git.go b/internal/tui/bubbles/git/types/git.go
175index 758f9d126eb39b2018ba03c4f70e869df4b555b8..05a1c26114c178ee6e08286c3687c7a187c6b95c 100644
176--- a/internal/tui/bubbles/git/types/git.go
177+++ b/internal/tui/bubbles/git/types/git.go
178@@ -1,6 +1,8 @@
179 package types
180 
181 import (
182+	"context"
183+
184 	"github.com/go-git/go-git/v5"
185 	"github.com/go-git/go-git/v5/plumbing"
186 	"github.com/go-git/go-git/v5/plumbing/object"
187@@ -15,6 +17,7 @@ type Repo interface {
188 	GetCommits(*plumbing.Reference) (Commits, error)
189 	Repository() *git.Repository
190 	Tree(*plumbing.Reference, string) (*object.Tree, error)
191+	PatchCtx(context.Context, *object.Commit) (*object.Patch, error)
192 }
193 
194 type Commits []*object.Commit
195diff --git a/internal/tui/style/style.go b/internal/tui/style/style.go
196index 1f57cb5b763cd83ea39c5add97521545ab2ac6eb..bc997ed31d03c6d46ad65931995f307bdca62393 100644
197--- a/internal/tui/style/style.go
198+++ b/internal/tui/style/style.go
199@@ -65,6 +65,8 @@ type Styles struct {
200 	TreeFileMode     lipgloss.Style
201 	TreeFileSize     lipgloss.Style
202 	TreeFileContent  lipgloss.Style
203+
204+	Spinner lipgloss.Style
205 }
206 
207 // DefaultStyles returns default styles for the TUI.
208@@ -252,5 +254,9 @@ func DefaultStyles() *Styles {
209 
210 	s.TreeFileContent = lipgloss.NewStyle()
211 
212+	s.Spinner = lipgloss.NewStyle().
213+		MarginLeft(1).
214+		Foreground(lipgloss.Color("205"))
215+
216 	return s
217 }