fix(ui): UI regressions (#279)

Ayman Bagabas created

* fix(backend): wrap i/o operations in transactions

when i/o errors, transaction rolls back

* fix(lint): inefficient assignment

* fix(ui): respect log settings

* fix(ui): truncate command string after yanking

* fix(git): make sure to use system git config in diff

* fix(ui): move support goto top/bottom in viewport

Change summary

cmd/soft/root.go                          | 28 +----------------
git/repo.go                               |  6 +++
internal/log/log.go                       | 39 +++++++++++++++++++++++++
server/git/git.go                         |  1 
server/ssh/session.go                     |  6 +++
server/ui/components/code/code.go         | 11 ------
server/ui/components/viewport/viewport.go | 10 ++++++
server/ui/pages/repo/log.go               |  4 ++
server/ui/pages/selection/item.go         | 13 +++++---
9 files changed, 76 insertions(+), 42 deletions(-)

Detailed changes

cmd/soft/root.go 🔗

@@ -4,11 +4,9 @@ import (
 	"context"
 	"os"
 	"runtime/debug"
-	"strconv"
-	"strings"
-	"time"
 
 	"github.com/charmbracelet/log"
+	. "github.com/charmbracelet/soft-serve/internal/log"
 	"github.com/spf13/cobra"
 )
 
@@ -53,28 +51,8 @@ func init() {
 }
 
 func main() {
-	ctx := context.Background()
-	logger := log.NewWithOptions(os.Stderr, log.Options{
-		ReportTimestamp: true,
-		TimeFormat:      time.DateOnly,
-	})
-	if debug, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_DEBUG")); debug {
-		logger.SetLevel(log.DebugLevel)
-	}
-	if tsfmt := os.Getenv("SOFT_SERVE_LOG_TIME_FORMAT"); tsfmt != "" {
-		logger.SetTimeFormat(tsfmt)
-	}
-
-	switch strings.ToLower(os.Getenv("SOFT_SERVE_LOG_FORMAT")) {
-	case "json":
-		logger.SetFormatter(log.JSONFormatter)
-	case "logfmt":
-		logger.SetFormatter(log.LogfmtFormatter)
-	case "text":
-		logger.SetFormatter(log.TextFormatter)
-	}
-
-	ctx = log.WithContext(ctx, logger)
+	logger := NewDefaultLogger()
+	ctx := log.WithContext(context.Background(), logger)
 	if err := rootCmd.ExecuteContext(ctx); err != nil {
 		os.Exit(1)
 	}

git/repo.go 🔗

@@ -147,7 +147,11 @@ func (r *Repository) TreePath(ref *Reference, path string) (*Tree, error) {
 
 // Diff returns the diff for the given commit.
 func (r *Repository) Diff(commit *Commit) (*Diff, error) {
-	ddiff, err := r.Repository.Diff(commit.Hash.String(), DiffMaxFiles, DiffMaxFileLines, DiffMaxLineChars)
+	ddiff, err := r.Repository.Diff(commit.Hash.String(), DiffMaxFiles, DiffMaxFileLines, DiffMaxLineChars, git.DiffOptions{
+		CommandOptions: git.CommandOptions{
+			Envs: []string{"GIT_CONFIG_GLOBAL=/dev/null"},
+		},
+	})
 	if err != nil {
 		return nil, err
 	}

internal/log/log.go 🔗

@@ -0,0 +1,39 @@
+package log
+
+import (
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/charmbracelet/log"
+)
+
+var contextKey = &struct{ string }{"logger"}
+
+// NewDefaultLogger returns a new logger with default settings.
+func NewDefaultLogger() *log.Logger {
+	logger := log.NewWithOptions(os.Stderr, log.Options{
+		ReportTimestamp: true,
+		TimeFormat:      time.DateOnly,
+	})
+
+	if debug, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_DEBUG")); debug {
+		logger.SetLevel(log.DebugLevel)
+	}
+
+	if tsfmt := os.Getenv("SOFT_SERVE_LOG_TIME_FORMAT"); tsfmt != "" {
+		logger.SetTimeFormat(tsfmt)
+	}
+
+	switch strings.ToLower(os.Getenv("SOFT_SERVE_LOG_FORMAT")) {
+	case "json":
+		logger.SetFormatter(log.JSONFormatter)
+	case "logfmt":
+		logger.SetFormatter(log.LogfmtFormatter)
+	case "text":
+		logger.SetFormatter(log.TextFormatter)
+	}
+
+	return logger
+}

server/git/git.go 🔗

@@ -88,6 +88,7 @@ func RunGit(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, dir
 	c.Env = append(c.Env, "SOFT_SERVE_DEBUG="+os.Getenv("SOFT_SERVE_DEBUG"))
 	if cfg != nil {
 		c.Env = append(c.Env, "SOFT_SERVE_LOG_FORMAT="+cfg.LogFormat)
+		c.Env = append(c.Env, "SOFT_SERVE_LOG_TIME_FORMAT="+cfg.LogTimeFormat)
 	}
 
 	stdin, err := c.StdinPipe()

server/ssh/session.go 🔗

@@ -4,6 +4,8 @@ import (
 	"strings"
 
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/log"
+	. "github.com/charmbracelet/soft-serve/internal/log"
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/charmbracelet/soft-serve/server/config"
 	"github.com/charmbracelet/soft-serve/server/errors"
@@ -48,7 +50,9 @@ func SessionHandler(cfg *config.Config) bm.ProgramHandler {
 
 		envs := &sessionEnv{s}
 		output := termenv.NewOutput(s, termenv.WithColorCache(true), termenv.WithEnvironment(envs))
-		c := common.NewCommon(s.Context(), output, pty.Window.Width, pty.Window.Height)
+		logger := NewDefaultLogger()
+		ctx := log.WithContext(s.Context(), logger)
+		c := common.NewCommon(ctx, output, pty.Window.Width, pty.Window.Height)
 		c.SetValue(common.ConfigKey, cfg)
 		m := ui.New(c, initialRepo)
 		p := tea.NewProgram(m,

server/ui/components/code/code.go 🔗

@@ -6,7 +6,6 @@ import (
 	"sync"
 
 	"github.com/alecthomas/chroma/lexers"
-	"github.com/charmbracelet/bubbles/key"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/glamour"
 	gansi "github.com/charmbracelet/glamour/ansi"
@@ -99,18 +98,10 @@ func (r *Code) Init() tea.Cmd {
 // Update implements tea.Model.
 func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	cmds := make([]tea.Cmd, 0)
-	switch msg := msg.(type) {
+	switch msg.(type) {
 	case tea.WindowSizeMsg:
 		// Recalculate content width and line wrap.
 		cmds = append(cmds, r.Init())
-	case tea.KeyMsg:
-		// Viewport doesn't handle these keys, so we do it here.
-		switch {
-		case key.Matches(msg, r.common.KeyMap.GotoTop):
-			r.GotoTop()
-		case key.Matches(msg, r.common.KeyMap.GotoBottom):
-			r.GotoBottom()
-		}
 	}
 	v, cmd := r.Viewport.Update(msg)
 	r.Viewport = v.(*vp.Viewport)

server/ui/components/viewport/viewport.go 🔗

@@ -1,6 +1,7 @@
 package viewport
 
 import (
+	"github.com/charmbracelet/bubbles/key"
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/soft-serve/server/ui/common"
@@ -36,6 +37,15 @@ func (v *Viewport) Init() tea.Cmd {
 
 // Update implements tea.Model.
 func (v *Viewport) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	switch msg := msg.(type) {
+	case tea.KeyMsg:
+		switch {
+		case key.Matches(msg, v.common.KeyMap.GotoTop):
+			v.GotoTop()
+		case key.Matches(msg, v.common.KeyMap.GotoBottom):
+			v.GotoBottom()
+		}
+	}
 	vp, cmd := v.Model.Update(msg)
 	v.Model = &vp
 	return v, cmd

server/ui/pages/repo/log.go 🔗

@@ -105,6 +105,8 @@ func (l *Log) ShortHelp() []key.Binding {
 		return []key.Binding{
 			l.common.KeyMap.UpDown,
 			l.common.KeyMap.BackItem,
+			l.common.KeyMap.GotoTop,
+			l.common.KeyMap.GotoBottom,
 		}
 	default:
 		return []key.Binding{}
@@ -151,6 +153,8 @@ func (l *Log) FullHelp() [][]key.Binding {
 			{
 				k.Down,
 				k.Up,
+				l.common.KeyMap.GotoTop,
+				l.common.KeyMap.GotoBottom,
 			},
 		}...)
 	}

server/ui/pages/selection/item.go 🔗

@@ -83,7 +83,7 @@ func (i Item) Title() string {
 }
 
 // Description returns the item description. Implements list.DefaultItem.
-func (i Item) Description() string { return i.repo.Description() }
+func (i Item) Description() string { return strings.TrimSpace(i.repo.Description()) }
 
 // FilterValue implements list.Item.
 func (i Item) FilterValue() string { return i.Title() }
@@ -199,13 +199,16 @@ func (d *ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem lis
 	s.WriteRune('\n')
 	s.WriteString(desc)
 	s.WriteRune('\n')
-	cmd := common.TruncateString(i.Command(), m.Width()-styles.Base.GetHorizontalFrameSize())
-	cmd = styles.Command.Render(cmd)
+
+	cmd := i.Command()
+	cmdStyler := styles.Command.Render
 	if d.copiedIdx == index {
-		cmd += " " + styles.Desc.Render("(copied to clipboard)")
+		cmd = "(copied to clipboard)"
+		cmdStyler = styles.Desc.Render
 		d.copiedIdx = -1
 	}
-	s.WriteString(cmd)
+	cmd = common.TruncateString(cmd, m.Width()-styles.Base.GetHorizontalFrameSize())
+	s.WriteString(cmdStyler(cmd))
 	fmt.Fprint(w,
 		d.common.Zone.Mark(i.ID(),
 			styles.Base.Render(s.String()),