Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
charm.land/lipgloss/v2 v2.0.3
github.com/adrg/xdg v0.5.3
github.com/atotto/clipboard v0.1.4
github.com/creack/pty v1.1.24
github.com/earthboundkid/versioninfo/v2 v2.24.1
github.com/go-playground/validator/v10 v10.30.3
github.com/joho/godotenv v1.5.1
Expand Down
74 changes: 55 additions & 19 deletions internal/git/client.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package git

import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"strings"

"github.com/cockroachdb/errors"
"github.com/creack/pty"
)

type Client struct {
Expand Down Expand Up @@ -59,38 +62,71 @@ func (c *Client) ModifiedFiles() ([]string, error) {
return strings.Split(trimmed, "\n"), nil
}

func (c *Client) Diff() (string, error) {
func (c *Client) Diff(color, exclude bool) (string, error) {
args := c.diffArgs(color, exclude)

cmd := exec.Command("git", args...)
if !color {
result, err := cmd.CombinedOutput()
if err != nil {
return "", errors.Wrap(err, "git diff failed")
}
return strings.ReplaceAll(string(result), "\t", " "), nil
}

ptmx, err := pty.Start(cmd)
if err != nil {
// fallback to plain pipe if pty fails
return c.Diff(false, exclude)
}
defer func() { _ = ptmx.Close() }()

var buf bytes.Buffer
if _, err := io.Copy(&buf, ptmx); err != nil {
return "", errors.Wrap(err, "reading git diff output failed")
}
Comment on lines +85 to +87
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

On Linux systems, reading from a PTY master file descriptor often returns a syscall.EIO error when the child process (in this case, git) terminates. This will cause io.Copy to return an error even if the command executed successfully and all output was captured. You should check for this specific error and treat it as an EOF if the command otherwise succeeded.


if err := cmd.Wait(); err != nil {
return "", errors.Wrap(err, "waiting for git diff command failed")
}

return strings.ReplaceAll(buf.String(), "\t", " "), nil
}

func (c *Client) diffArgs(color, exclude bool) []string {
args := []string{
"--no-pager",
"diff",
}
if color {
args = append(args, "--color=always")
}
args = append(args,
"--no-ext-diff",
"--no-textconv",
}
)

if c.addAll {
args = append(args, "HEAD")
} else {
args = append(args, "--staged")
}

args = append(args,
"--diff-filter=ACMRTUXBD",
"--", // separates options from pathspecs
".", // include everything under the repo root
":(exclude)*-lock.*", // package-lock.json, pnpm-lock.yaml, etc.
":(exclude)*.lock", // yarn.lock, poetry.lock, Cargo.lock, etc.
":(exclude)**/build/**",
":(exclude)**/dist/**",
":(exclude)**/target/**",
":(exclude)**/out/**",
":(exclude)go.sum",
)

result, err := exec.Command("git", args...).CombinedOutput()
if err != nil {
return "", errors.Wrap(err, "git diff failed")
if exclude {
args = append(args,
"--diff-filter=ACMRTUXBD",
"--", // separates options from pathspecs
".", // include everything under the repo root
":(exclude)*-lock.*", // package-lock.json, pnpm-lock.yaml, etc.
":(exclude)*.lock", // yarn.lock, poetry.lock, Cargo.lock, etc.
":(exclude)**/build/**",
":(exclude)**/dist/**",
":(exclude)**/target/**",
":(exclude)**/out/**",
":(exclude)go.sum",
)
}
return string(result), nil
return args
}

func (c *Client) prepareCommitMessage(message string, skipCI bool) string {
Expand Down
2 changes: 1 addition & 1 deletion internal/interfaces/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ var ErrAborted = errors.New("aborted")
type GitClient interface {
IsInWorkTree() error
ModifiedFiles() ([]string, error)
Diff() (string, error)
Diff(color, exclude bool) (string, error)
Commit(message string, skipCI bool) error
}
16 changes: 8 additions & 8 deletions internal/ui/commit_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ func initialCommitViewModel(message string, duration time.Duration) (*commitView
textStyles.Focused.CursorLine = lipgloss.NewStyle()
ta.SetStyles(textStyles)

vp := viewport.New(viewport.WithWidth(ta.Width()), viewport.WithHeight(ta.Height()))

customStyle := styles.DarkStyleConfig
customStyle.Document.Margin = uintPtr(0)
customStyle.H2.BlockSuffix = ""
Expand All @@ -76,7 +74,7 @@ func initialCommitViewModel(message string, duration time.Duration) (*commitView

return &commitViewModel{
textarea: ta,
viewport: vp,
viewport: viewport.New(viewport.WithWidth(ta.Width()), viewport.WithHeight(ta.Height())),
history: NewHistory(message),
boxStyle: lipgloss.NewStyle().
BorderForeground(lipgloss.Color("6")). // Cyan
Expand Down Expand Up @@ -114,6 +112,9 @@ func (m *commitViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.textarea.Blur()
return m, func() tea.Msg { return regenerateMsg{} }

case "ctrl+d":
return m, func() tea.Msg { return showDiffViewMsg{} }

case "esc":
if m.preview {
m.preview = false
Expand All @@ -139,7 +140,6 @@ func (m *commitViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.preview = !m.preview
return m, nil

}

if m.preview {
Expand Down Expand Up @@ -264,21 +264,21 @@ func (m *commitViewModel) helpTextView() string {
}

if m.preview {
return fmt.Sprintf("%s:commit %s:clear %s:undo %s:regen %s:editor %s:back",
return fmt.Sprintf("%s:commit %s:clear %s:regen %s:editor %s:diff %s:back",
BoldYellow.Render("CTRL+A"),
Strikethrough.Render("CTRL+K"),
Strikethrough.Render("CTRL+Z"),
BoldYellow.Render("CTRL+R"),
BoldYellow.Render("CTRL+P"),
BoldYellow.Render("CTRL+D"),
BoldYellow.Render("ESC"))
}

return fmt.Sprintf("%s:commit %s:clear %s:undo %s:regen %s:preview %s:abort",
return fmt.Sprintf("%s:commit %s:clear %s:regen %s:preview %s:diff %s:abort",
BoldYellow.Render("CTRL+A"),
BoldYellow.Render("CTRL+K"),
BoldYellow.Render("CTRL+Z"),
BoldYellow.Render("CTRL+R"),
BoldYellow.Render("CTRL+P"),
BoldYellow.Render("CTRL+D"),
BoldYellow.Render("ESC"))
Comment thread
rm-hull marked this conversation as resolved.
}

Expand Down
68 changes: 68 additions & 0 deletions internal/ui/diff_view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package ui

import (
"fmt"
"strings"

"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)

type diffViewModel struct {
viewport viewport.Model
boxStyle lipgloss.Style
}

func initialDiffViewModel(width, height int) *diffViewModel {
vp := viewport.New(viewport.WithWidth(width), viewport.WithHeight(height))
vp.SoftWrap = false

return &diffViewModel{
viewport: vp,
boxStyle: lipgloss.NewStyle().
BorderForeground(lipgloss.Color("6")). // Cyan
Padding(0, 1),
}
}

func (m *diffViewModel) Init() tea.Cmd {
return nil
}

func (m *diffViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.String() {
case "esc", "ctrl+d":
return m, func() tea.Msg { return cancelDiffViewMsg{} }
}
case diffColorMsg:
m.viewport.SetContent(string(msg))
return m, nil
}

var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}

func (m *diffViewModel) View() tea.View {
title := " Raw diff "
titleBorder := lipgloss.RoundedBorder()

repeatCount := max((m.viewport.Width()+2)-lipgloss.Width(title), 0)
titleBorder.Top = title + strings.Repeat("─", repeatCount)

helpText := fmt.Sprintf("%s:commit %s:clear %s:regen %s:editor %s:diff %s:back",
Strikethrough.Render("CTRL+A"),
Strikethrough.Render("CTRL+K"),
Strikethrough.Render("CTRL+R"),
Strikethrough.Render("CTRL+P"),
BoldYellow.Render("CTRL+D"),
BoldYellow.Render("ESC"))

return tea.NewView(m.boxStyle.
BorderStyle(titleBorder).
Render(m.viewport.View()) + "\n" + helpText)
}
52 changes: 40 additions & 12 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
showSpinner sessionState = iota
showCommitView
showRegeneratePrompt
showDiffView
)

type (
Expand All @@ -32,10 +33,13 @@ type (
duration time.Duration
}
commitMsg string
diffColorMsg string
errMsg struct{ err error }
abortMsg struct{}
regenerateMsg struct{}
cancelRegenPromptMsg struct{}
showDiffViewMsg struct{}
cancelDiffViewMsg struct{}
userResponseMsg string
)

Expand All @@ -60,6 +64,8 @@ type Model struct {
spinnerMessage string
latestVersion string
commitView tea.Model
diffView tea.Model
diffLoaded bool
commitMessage string
promptView tea.Model
action Action
Expand All @@ -86,6 +92,7 @@ func InitialModel(
hint: hint,
spinner: spinner.New(spinner.WithSpinner(spinner.MiniDot)),
spinnerMessage: Magenta.Render("Checking whether a newer version exists..."),
diffView: initialDiffViewModel(72+2, 20),
action: None,
yolo: yolo,
}
Expand All @@ -96,21 +103,21 @@ func (m *Model) Init() tea.Cmd {
}

func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd

switch msg := msg.(type) {
case tea.KeyPressMsg:
if msg.String() == "ctrl+c" {
if m.state == showSpinner {
m.action = Abort
return m, tea.Quit
}
if msg.String() == "ctrl+c" && m.state == showSpinner {
m.action = Abort
return m, tea.Quit
}
Comment thread
rm-hull marked this conversation as resolved.

case gitCheckMsg:
if len(msg) == 0 {
m.err = errors.New("no changes detected")
return m, tea.Quit
}
return m, m.getGitDiff
return m, m.getGitDiffForLLM

case gitDiffMsg:
m.spinnerMessage = fmt.Sprintf("%s%s%s",
Expand Down Expand Up @@ -179,10 +186,22 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.hint = string(msg)
return m, tea.Batch(m.spinner.Tick, m.generateSummary(m.diff))

case cancelRegenPromptMsg:
case cancelRegenPromptMsg, cancelDiffViewMsg:
m.state = showCommitView
return m, m.commitView.Init()

case showDiffViewMsg:
m.state = showDiffView
if !m.diffLoaded {
return m, m.getFullDiffWithColor
}
return m, m.diffView.Init()

case diffColorMsg:
m.diffLoaded = true
m.diffView, cmd = m.diffView.Update(msg)
return m, cmd

case errMsg:
m.err = msg.err
return m, tea.Quit
Expand All @@ -197,14 +216,15 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.checkGitStatus
}

var cmd tea.Cmd
switch m.state {
case showSpinner:
m.spinner, cmd = m.spinner.Update(msg)
case showCommitView:
m.commitView, cmd = m.commitView.Update(msg)
case showRegeneratePrompt:
m.promptView, cmd = m.promptView.Update(msg)
case showDiffView:
m.diffView, cmd = m.diffView.Update(msg)
}
return m, cmd
}
Expand All @@ -221,8 +241,6 @@ func (m *Model) LatestVersion() string {
return m.latestVersion
}

// LatestVersionMsg is handled above to chain into git checks

func (m *Model) View() tea.View {
switch m.state {
case showSpinner:
Expand All @@ -237,6 +255,8 @@ func (m *Model) View() tea.View {
return tea.NewView(m.spinner.View() + " " + m.spinnerMessage)
}
return tea.NewView(m.commitView.View().Content + m.promptView.View().Content)
case showDiffView:
return m.diffView.View()
default:
return tea.NewView("")
}
Expand All @@ -254,8 +274,16 @@ func (m *Model) checkGitStatus() tea.Msg {
return gitCheckMsg(modifiedFiles)
}

func (m *Model) getGitDiff() tea.Msg {
diff, err := m.gitClient.Diff()
func (m *Model) getFullDiffWithColor() tea.Msg {
diff, err := m.gitClient.Diff(true, false)
if err != nil {
return errMsg{err}
}
return diffColorMsg(diff)
}

func (m *Model) getGitDiffForLLM() tea.Msg {
diff, err := m.gitClient.Diff(false, true)
if err != nil {
return errMsg{err}
}
Expand Down
Loading