Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Git TUI improvements #97

Merged
merged 5 commits into from Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -5,7 +5,7 @@ go 1.17
require (
github.com/alecthomas/chroma v0.10.0
github.com/caarlos0/env/v6 v6.9.1
github.com/charmbracelet/bubbles v0.10.3-0.20220208194203-1d489252fe50
github.com/charmbracelet/bubbles v0.10.4-0.20220302223835-88562515cf7b
github.com/charmbracelet/bubbletea v0.19.4-0.20220208181305-42cd4c31919c
github.com/charmbracelet/glamour v0.4.0
github.com/charmbracelet/lipgloss v0.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Expand Up @@ -21,8 +21,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k=
github.com/caarlos0/env/v6 v6.9.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
github.com/charmbracelet/bubbles v0.10.3-0.20220208194203-1d489252fe50 h1:hAsXGdqKHVoEbBlvReSfz8X605xddHMBFSxSrCaSSO4=
github.com/charmbracelet/bubbles v0.10.3-0.20220208194203-1d489252fe50/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
github.com/charmbracelet/bubbles v0.10.4-0.20220302223835-88562515cf7b h1:o+LFpRn1fXtu1hDJLtBFjp7tMZ8AqwSpl84w1TnUj0Y=
github.com/charmbracelet/bubbles v0.10.4-0.20220302223835-88562515cf7b/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
github.com/charmbracelet/bubbletea v0.19.4-0.20220208181305-42cd4c31919c h1:hcS4xdVQwblKo8xuA5gRO/jql+yCVfnBlOwWcZrxOmA=
github.com/charmbracelet/bubbletea v0.19.4-0.20220208181305-42cd4c31919c/go.mod h1:5nPeULOIxbAMykb3ggwhw1kruS7nP+Y4Za9yEH4J27U=
Expand Down
37 changes: 37 additions & 0 deletions internal/git/git.go
@@ -1,6 +1,7 @@
package git

import (
"context"
"errors"
"log"
"os"
Expand Down Expand Up @@ -30,6 +31,7 @@ type Repo struct {
refs []*plumbing.Reference
trees map[plumbing.Hash]*object.Tree
commits map[plumbing.Hash]*object.Commit
patch map[plumbing.Hash]*object.Patch
}

// GetName returns the name of the repository.
Expand Down Expand Up @@ -105,6 +107,40 @@ func (r *Repo) commitForHash(hash plumbing.Hash) (*object.Commit, error) {
return co, nil
}

func (r *Repo) PatchCtx(ctx context.Context, commit *object.Commit) (*object.Patch, error) {
hash := commit.Hash
p, ok := r.patch[hash]
if !ok {
c, err := r.commitForHash(hash)
if err != nil {
return nil, err
}
// Using commit trees fixes the issue when generating diff for the first commit
// https://github.com/go-git/go-git/issues/281
tree, err := r.treeForHash(c.TreeHash)
if err != nil {
return nil, err
}
var parent *object.Commit
parentTree := &object.Tree{}
if c.NumParents() > 0 {
parent, err = r.commitForHash(c.ParentHashes[0])
if err != nil {
return nil, err
}
parentTree, err = r.treeForHash(parent.TreeHash)
if err != nil {
return nil, err
}
}
p, err = parentTree.PatchContext(ctx, tree)
if err != nil {
return nil, err
}
}
return p, nil
}

// GetCommits returns the commits for a repository.
func (r *Repo) GetCommits(ref *plumbing.Reference) (gitypes.Commits, error) {
hash, err := r.targetHash(ref)
Expand Down Expand Up @@ -264,6 +300,7 @@ func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
r := &Repo{
name: name,
repository: rg,
patch: make(map[plumbing.Hash]*object.Patch),
}
r.commits = make(map[plumbing.Hash]*object.Commit)
r.trees = make(map[plumbing.Hash]*object.Tree)
Expand Down
6 changes: 5 additions & 1 deletion internal/tui/bubbles/git/about/bubble.go
Expand Up @@ -88,7 +88,11 @@ func (b *Bubble) Help() []types.HelpEntry {

func (b *Bubble) glamourize() (string, error) {
w := b.width - b.widthMargin - b.styles.RepoBody.GetHorizontalFrameSize()
return types.Glamourize(w, b.repo.GetReadme())
rm := b.repo.GetReadme()
if rm == "" {
return b.styles.AboutNoReadme.Render("No readme found."), nil
}
return types.Glamourize(w, rm)
}

func (b *Bubble) setupCmd() tea.Msg {
Expand Down
7 changes: 7 additions & 0 deletions internal/tui/bubbles/git/bubble.go
Expand Up @@ -80,6 +80,13 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
b.width = msg.Width
b.height = msg.Height
for i, bx := range b.boxes {
m, cmd := bx.Update(msg)
b.boxes[i] = m
if cmd != nil {
cmds = append(cmds, cmd)
}
}
case refs.RefMsg:
b.state = treeState
b.ref = msg
Expand Down
171 changes: 96 additions & 75 deletions internal/tui/bubbles/git/log/bubble.go
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
gansi "github.com/charmbracelet/glamour/ansi"
Expand All @@ -26,21 +27,17 @@ var (
Code: "",
Language: "diff",
}
waitBeforeLoading = time.Millisecond * 300
)

type commitMsg struct {
commit *object.Commit
parent *object.Commit
tree *object.Tree
parentTree *object.Tree
patch *object.Patch
}
type commitMsg *object.Commit

type sessionState int

const (
logState sessionState = iota
commitState
loadingState
errorState
)

Expand Down Expand Up @@ -100,19 +97,23 @@ type Bubble struct {
height int
heightMargin int
error types.ErrMsg
spinner spinner.Model
}

func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
l := list.New([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
l.SetShowFilter(false)
l.SetShowHelp(false)
l.SetShowPagination(false)
l.SetShowPagination(true)
l.SetShowStatusBar(false)
l.SetShowTitle(false)
l.SetFilteringEnabled(false)
l.DisableQuitKeybindings()
l.KeyMap.NextPage = types.NextPage
l.KeyMap.PrevPage = types.PrevPage
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = styles.Spinner
b := &Bubble{
commitViewport: &vp.ViewportBubble{
Viewport: &viewport.Model{},
Expand All @@ -126,6 +127,7 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
heightMargin: heightMargin,
list: l,
ref: repo.GetHEAD(),
spinner: s,
}
b.SetSize(width, height)
return b
Expand All @@ -134,7 +136,9 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
func (b *Bubble) reset() tea.Cmd {
b.state = logState
b.list.Select(0)
return b.updateItems()
cmd := b.updateItems()
b.SetSize(b.width, b.height)
return cmd
}

func (b *Bubble) updateItems() tea.Cmd {
Expand Down Expand Up @@ -167,6 +171,7 @@ func (b *Bubble) SetSize(width, height int) {
b.commitViewport.Viewport.Width = width - b.widthMargin
b.commitViewport.Viewport.Height = height - b.heightMargin
b.list.SetSize(width-b.widthMargin, height-b.heightMargin)
b.list.Styles.PaginationStyle = b.style.LogPaginator.Copy().Width(width - b.widthMargin)
}

func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Expand All @@ -193,12 +198,19 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
b.state = errorState
return b, nil
case commitMsg:
content := b.renderCommit(msg)
b.state = commitState
b.commitViewport.Viewport.SetContent(content)
b.GotoTop()
if b.state == loadingState {
cmds = append(cmds, b.spinner.Tick)
}
case refs.RefMsg:
b.ref = msg
case spinner.TickMsg:
if b.state == loadingState {
s, cmd := b.spinner.Update(msg)
if cmd != nil {
cmds = append(cmds, cmd)
}
b.spinner = s
}
}

switch b.state {
Expand All @@ -215,83 +227,77 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return b, tea.Batch(cmds...)
}

func (b *Bubble) loadPatch(c *object.Commit) error {
var patch strings.Builder
style := b.style.LogCommit.Copy().Width(b.width - b.widthMargin - b.style.LogCommit.GetHorizontalFrameSize())
ctx, cancel := context.WithTimeout(context.TODO(), types.MaxPatchWait)
defer cancel()
p, err := b.repo.PatchCtx(ctx, c)
if err != nil {
return err
}
patch.WriteString(b.renderCommit(c))
fpl := len(p.FilePatches())
if fpl > types.MaxDiffFiles {
patch.WriteString("\n" + types.ErrDiffFilesTooLong.Error())
} else {
patch.WriteString("\n" + b.renderStats(p.Stats()))
}
if fpl <= types.MaxDiffFiles {
ps := p.String()
if len(strings.Split(ps, "\n")) > types.MaxDiffLines {
patch.WriteString("\n" + types.ErrDiffTooLong.Error())
} else {
patch.WriteString("\n" + b.renderDiff(ps))
}
}
content := style.Render(patch.String())
b.commitViewport.Viewport.SetContent(content)
b.GotoTop()
return nil
}

func (b *Bubble) loadCommit() tea.Cmd {
var err error
done := make(chan struct{}, 1)
i := b.list.SelectedItem()
if i == nil {
return nil
}
c, ok := i.(item)
if !ok {
return nil
}
go func() {
err = b.loadPatch(c.Commit)
done <- struct{}{}
b.state = commitState
}()
return func() tea.Msg {
i := b.list.SelectedItem()
if i == nil {
return nil
}
c, ok := i.(item)
if !ok {
return nil
select {
case <-done:
case <-time.After(waitBeforeLoading):
b.state = loadingState
}
// Using commit trees fixes the issue when generating diff for the first commit
// https://github.com/go-git/go-git/issues/281
tree, err := c.Tree()
if err != nil {
return types.ErrMsg{Err: err}
}
var parent *object.Commit
parentTree := &object.Tree{}
if c.NumParents() > 0 {
parent, err = c.Parents().Next()
if err != nil {
return types.ErrMsg{Err: err}
}
parentTree, err = parent.Tree()
if err != nil {
return types.ErrMsg{Err: err}
}
}
ctx, cancel := context.WithTimeout(context.TODO(), types.MaxPatchWait)
defer cancel()
patch, err := parentTree.PatchContext(ctx, tree)
if err != nil {
return types.ErrMsg{Err: err}
}
return commitMsg{
commit: c.Commit,
tree: tree,
parent: parent,
parentTree: parentTree,
patch: patch,
}
return commitMsg(c.Commit)
}
}

func (b *Bubble) renderCommit(m commitMsg) string {
func (b *Bubble) renderCommit(c *object.Commit) string {
s := strings.Builder{}
st := b.style
c := m.commit
// FIXME: lipgloss prints empty lines when CRLF is used
// sanitize commit message from CRLF
msg := strings.ReplaceAll(c.Message, "\r\n", "\n")
s.WriteString(fmt.Sprintf("%s\n%s\n%s\n%s\n",
st.LogCommitHash.Render("commit "+c.Hash.String()),
st.LogCommitAuthor.Render("Author: "+c.Author.String()),
st.LogCommitDate.Render("Date: "+c.Committer.When.Format(time.UnixDate)),
st.LogCommitBody.Render(msg),
b.style.LogCommitHash.Render("commit "+c.Hash.String()),
b.style.LogCommitAuthor.Render("Author: "+c.Author.String()),
b.style.LogCommitDate.Render("Date: "+c.Committer.When.Format(time.UnixDate)),
b.style.LogCommitBody.Render(msg),
))
stats := m.patch.Stats()
if len(stats) > types.MaxDiffFiles {
s.WriteString("\n" + types.ErrDiffFilesTooLong.Error())
} else {
s.WriteString("\n" + b.renderStats(stats))
}
ps := m.patch.String()
if len(strings.Split(ps, "\n")) > types.MaxDiffLines {
s.WriteString("\n" + types.ErrDiffTooLong.Error())
} else {
p := strings.Builder{}
diffChroma.Code = ps
err := diffChroma.Render(&p, types.RenderCtx)
if err != nil {
s.WriteString(fmt.Sprintf("\n%s", err.Error()))
} else {
s.WriteString(fmt.Sprintf("\n%s", p.String()))
}
}
return st.LogCommit.Copy().Width(b.width - b.widthMargin - st.LogCommit.GetHorizontalFrameSize()).Render(s.String())
return s.String()
}

func (b *Bubble) renderStats(fileStats object.FileStats) string {
Expand Down Expand Up @@ -381,10 +387,25 @@ func (b *Bubble) renderStats(fileStats object.FileStats) string {
return output.String()
}

func (b *Bubble) renderDiff(diff string) string {
var s strings.Builder
pr := strings.Builder{}
diffChroma.Code = diff
err := diffChroma.Render(&pr, types.RenderCtx)
if err != nil {
s.WriteString(fmt.Sprintf("\n%s", err.Error()))
} else {
s.WriteString(fmt.Sprintf("\n%s", pr.String()))
}
return s.String()
}

func (b *Bubble) View() string {
switch b.state {
case logState:
return b.list.View()
case loadingState:
return fmt.Sprintf("%s loading commit", b.spinner.View())
case errorState:
return b.error.ViewWithPrefix(b.style, "Error")
case commitState:
Expand Down