diff --git a/README.md b/README.md index 36c04f75..9b477f61 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,12 @@ pacman -S --needed git base-devel yay Make sure you have the `Color` option in your `/etc/pacman.conf` (see issue [#123](https://github.com/Jguer/yay/issues/123)). -- **Sometimes diffs are printed to the terminal, and other times they are paged via less. How do I fix this?** +- **How do I change the pager used for PKGBUILD diffs?** - yay uses `git diff` to display diffs, which by default tells less not to - page if the output can fit into one terminal length. This behavior can be - overridden by exporting your own flags (`export LESS=SRX`). + Diffs for all selected packages are collected and shown in a single pager + session. Set `pager` in `config.json` or `yay.opt.pager` in `init.lua`, or + set `PAGER` to override the default (`less`, or `cat` if less is unavailable). + Example: `yay.opt.pager = "delta"` or `export PAGER=less`. - **yay is not asking me to edit PKGBUILDS, and I don't like the diff menu! What can I do?** diff --git a/doc/init.lua b/doc/init.lua index 9057602e..1ff4f686 100644 --- a/doc/init.lua +++ b/doc/init.lua @@ -9,6 +9,7 @@ yay.opt.aurrpcurl = "" yay.opt.build_dir = os.getenv("HOME") .. "/.cache/yay" yay.opt.editor = os.getenv("EDITOR") or os.getenv("VISUAL") or "vi" yay.opt.editor_flags = "" +yay.opt.pager = os.getenv("PAGER") or "" yay.opt.makepkg_bin = "makepkg" yay.opt.makepkg_conf = "" yay.opt.pacman_bin = "pacman" diff --git a/doc/lua.md b/doc/lua.md index fc667fc7..632db98e 100644 --- a/doc/lua.md +++ b/doc/lua.md @@ -66,7 +66,7 @@ startup and reports the offending keys/values so misconfigurations fail fast. **Strings** -`aururl`, `aurrpcurl`, `build_dir`, `editor`, `editor_flags`, `makepkg_bin`, +`aururl`, `aurrpcurl`, `build_dir`, `editor`, `editor_flags`, `pager`, `makepkg_bin`, `makepkg_conf`, `pacman_bin`, `pacman_conf`, `redownload`, `rebuild`, `git_bin`, `gpg_bin`, `gpg_flags`, `mflags`, `sort_by`, `search_by`, `git_flags`, `remove_make`, `sudo_bin`, `sudo_flags` diff --git a/doc/yay.8 b/doc/yay.8 index ff470885..fc897bb9 100644 --- a/doc/yay.8 +++ b/doc/yay.8 @@ -307,9 +307,10 @@ using '--cleanmenu=false' on the command line Show the diff menu. This menu gives you the option to view diffs from build files before building. -Diffs are shown via \fBgit diff\fR which uses -less by default. This behaviour can be changed via git's config, the -\fB$GIT_PAGER\fR or \fB$PAGER\fR environment variables. +Diffs for all selected packages are collected and shown through a single +pager. The pager is taken from the \fBpager\fR config option (also settable +via Lua as \fByay.opt.pager\fR), then \fB$PAGER\fR, then \fBless\fR (or +\fBcat\fR if less is unavailable). .TP .B \-\-editmenu @@ -540,6 +541,11 @@ Overridden by \-\-builddir. When editor is not configured, use these variables to pick what editor to use when editing PKGBUILDS. +.TP +.B PAGER +When pager is not configured, use this variable when showing PKGBUILD diffs. +If unset, yay uses \fBless\fR when available, otherwise \fBcat\fR. + .SH FILES .TP .B CONFIG DIRECTORY diff --git a/pkg/menus/diff_menu.go b/pkg/menus/diff_menu.go index 37e46e19..02900560 100644 --- a/pkg/menus/diff_menu.go +++ b/pkg/menus/diff_menu.go @@ -23,9 +23,35 @@ const ( ) func showPkgbuildDiffs(ctx context.Context, cmdBuilder exe.ICmdBuilder, logger *text.Logger, - pkgbuildDirs map[string]string, bases []string, + pkgbuildDirs map[string]string, bases []string, pagerConfig string, ) error { - var errs []error + combined, errs := collectPkgbuildDiffs(ctx, cmdBuilder, logger, pkgbuildDirs, bases) + if combined == "" { + return errors.Join(errs...) + } + + if !isStdoutTerminal() { + logger.Print(combined) + + return errors.Join(errs...) + } + + if err := runPager(ctx, combined, pagerConfig); err != nil { + errs = append(errs, err) + } + + return errors.Join(errs...) +} + +// collectPkgbuildDiffs captures each selected package's git diff with --no-pager +// and joins them with package headers into one buffer for a single pager session. +func collectPkgbuildDiffs(ctx context.Context, cmdBuilder exe.ICmdBuilder, logger *text.Logger, + pkgbuildDirs map[string]string, bases []string, +) (string, []error) { + var ( + errs []error + buf strings.Builder + ) for _, pkg := range bases { dir := pkgbuildDirs[pkg] @@ -53,7 +79,7 @@ func showPkgbuildDiffs(ctx context.Context, cmdBuilder exe.ICmdBuilder, logger * } args := []string{ - "diff", + "--no-pager", "diff", start + "..HEAD@{upstream}", "--src-prefix", dir + "/", "--dst-prefix", dir + "/", "--", ".", ":(exclude).SRCINFO", } @@ -63,10 +89,24 @@ func showPkgbuildDiffs(ctx context.Context, cmdBuilder exe.ICmdBuilder, logger * args = append(args, "--color=never") } - _ = cmdBuilder.Show(cmdBuilder.BuildGitCmd(ctx, dir, args...)) + stdout, stderr, err := cmdBuilder.Capture(cmdBuilder.BuildGitCmd(ctx, dir, args...)) + if err != nil { + errs = append(errs, fmt.Errorf("%s%w", stderr, err)) + + continue + } + + if stdout == "" { + continue + } + + buf.WriteString(logger.SprintOperationInfo(gotext.Get("Showing diff for %s", text.Bold(pkg)))) + buf.WriteByte('\n') + buf.WriteString(stdout) + buf.WriteString("\n\n") } - return errors.Join(errs...) + return buf.String(), errs } // Check whether or not a diff exists between the last reviewed diff and @@ -163,7 +203,7 @@ func DiffFn(ctx context.Context, run *runtime.Runtime, w io.Writer, return errMenu } - if errD := showPkgbuildDiffs(ctx, run.CmdBuilder, run.Logger, pkgbuildDirsByBase, toDiff); errD != nil { + if errD := showPkgbuildDiffs(ctx, run.CmdBuilder, run.Logger, pkgbuildDirsByBase, toDiff, run.Cfg.Pager); errD != nil { return errD } diff --git a/pkg/menus/pager.go b/pkg/menus/pager.go new file mode 100644 index 00000000..b99526f6 --- /dev/null +++ b/pkg/menus/pager.go @@ -0,0 +1,54 @@ +package menus + +import ( + "context" + "os" + "os/exec" + "strings" + + "golang.org/x/term" +) + +// runPager pages content through a single pager process. Overridable in tests. +var runPager = pageThroughPager + +// isStdoutTerminal reports whether stdout is a terminal. Overridable in tests. +var isStdoutTerminal = func() bool { + return term.IsTerminal(int(os.Stdout.Fd())) +} + +// resolvePager returns the pager command string. +// Priority: config pager → PAGER → less → cat. +func resolvePager(pagerConfig string) string { + if pagerConfig != "" { + return pagerConfig + } + + if pager := os.Getenv("PAGER"); pager != "" { + return pager + } + + if _, err := exec.LookPath("less"); err == nil { + return "less" + } + + return "cat" +} + +// pageThroughPager runs the configured pager with content on stdin. +// The pager command is user-controlled (config / $PAGER), same model as $EDITOR. +func pageThroughPager(ctx context.Context, content, pagerConfig string) error { + pager := resolvePager(pagerConfig) + cmd := exec.CommandContext(ctx, "sh", "-c", pager) + cmd.Stdin = strings.NewReader(content) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + + if os.Getenv("LESS") == "" { + // S: chop long lines; R: raw ANSI; X: no termcap init; F: quit if one screen + cmd.Env = append(cmd.Env, "LESS=SRXF") + } + + return cmd.Run() +} diff --git a/pkg/menus/pager_test.go b/pkg/menus/pager_test.go new file mode 100644 index 00000000..134b566a --- /dev/null +++ b/pkg/menus/pager_test.go @@ -0,0 +1,127 @@ +//go:build !integration + +package menus + +import ( + "bytes" + "context" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Jguer/yay/v13/pkg/text" +) + +func TestResolvePager(t *testing.T) { + // Not parallel: t.Setenv cannot be used after t.Parallel. + tests := []struct { + name string + pagerConfig string + pager string + want string + }{ + {name: "config wins", pagerConfig: "delta", pager: "less", want: "delta"}, + {name: "PAGER when config empty", pagerConfig: "", pager: "most", want: "most"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("PAGER", tt.pager) + + assert.Equal(t, tt.want, resolvePager(tt.pagerConfig)) + }) + } +} + +func TestCollectPkgbuildDiffs(t *testing.T) { + t.Parallel() + + var warnOut bytes.Buffer + logger := text.NewLogger(&bytes.Buffer{}, &warnOut, strings.NewReader(""), false, "test") + + builder := &fakeMenusCmdBuilder{ + captureFn: func(cmd *exec.Cmd) (string, string, error) { + joined := strings.Join(cmd.Args, " ") + + switch { + case strings.Contains(joined, "--quiet --verify AUR_SEEN"): + return "", "", nil + case strings.Contains(joined, "rev-parse AUR_SEEN HEAD@{upstream}"): + return "aaa\nbbb\n", "", nil + case strings.Contains(joined, "rev-parse AUR_SEEN"): + return "aaa\n", "", nil + case strings.Contains(joined, "--no-pager diff"): + require.Contains(t, joined, "--no-pager") + pkg := "alpha" + if strings.Contains(joined, "beta/") { + pkg = "beta" + } + + return "diff for " + pkg, "", nil + default: + t.Fatalf("unexpected git args: %s", joined) + + return "", "", nil + } + }, + } + + dirs := map[string]string{ + "alpha": "/tmp/alpha", + "beta": "/tmp/beta", + } + + out, errs := collectPkgbuildDiffs(context.Background(), builder, logger, dirs, []string{"alpha", "beta"}) + require.Empty(t, errs) + require.Contains(t, out, "Showing diff for") + require.Contains(t, out, "diff for alpha") + require.Contains(t, out, "diff for beta") + // Headers for both packages appear before their diffs in one buffer. + alphaIdx := strings.Index(out, "diff for alpha") + betaIdx := strings.Index(out, "diff for beta") + require.Positive(t, alphaIdx) + require.Greater(t, betaIdx, alphaIdx) +} + +func TestShowPkgbuildDiffsPagesOnce(t *testing.T) { + t.Parallel() + + var paged strings.Builder + origRunPager := runPager + origIsTTY := isStdoutTerminal + t.Cleanup(func() { + runPager = origRunPager + isStdoutTerminal = origIsTTY + }) + + isStdoutTerminal = func() bool { return true } + runPager = func(_ context.Context, content, _ string) error { + paged.WriteString(content) + + return nil + } + + logger := text.NewLogger(&bytes.Buffer{}, &bytes.Buffer{}, strings.NewReader(""), false, "test") + builder := &fakeMenusCmdBuilder{ + captureFn: func(cmd *exec.Cmd) (string, string, error) { + joined := strings.Join(cmd.Args, " ") + switch { + case strings.Contains(joined, "--quiet --verify AUR_SEEN"): + return "", "", exec.ErrNotFound // no AUR_SEEN → empty tree, always show + case strings.Contains(joined, "--no-pager diff"): + return "+++ changed", "", nil + default: + return "", "", nil + } + }, + } + + err := showPkgbuildDiffs(context.Background(), builder, logger, + map[string]string{"pkg": "/tmp/pkg"}, []string{"pkg"}, "") + require.NoError(t, err) + require.Contains(t, paged.String(), "+++ changed") + require.Contains(t, paged.String(), "Showing diff for") +} diff --git a/pkg/settings/config.go b/pkg/settings/config.go index e10a69da..54946c6e 100644 --- a/pkg/settings/config.go +++ b/pkg/settings/config.go @@ -28,6 +28,7 @@ type Configuration struct { BuildDir string `json:"buildDir" lua:"build_dir"` Editor string `json:"editor" lua:"editor"` EditorFlags string `json:"editorflags" lua:"editor_flags"` + Pager string `json:"pager" lua:"pager"` MakepkgBin string `json:"makepkgbin" lua:"makepkg_bin"` MakepkgConf string `json:"makepkgconf" lua:"makepkg_conf"` PacmanBin string `json:"pacmanbin" lua:"pacman_bin"` @@ -115,6 +116,7 @@ func (c *Configuration) expandEnv() { c.BuildDir = expandEnvOrHome(c.BuildDir) c.Editor = expandEnvOrHome(c.Editor) c.EditorFlags = os.ExpandEnv(c.EditorFlags) + c.Pager = os.ExpandEnv(c.Pager) c.MakepkgBin = expandEnvOrHome(c.MakepkgBin) c.MakepkgConf = expandEnvOrHome(c.MakepkgConf) c.PacmanBin = expandEnvOrHome(c.PacmanBin) @@ -196,6 +198,7 @@ func DefaultConfig(version string) *Configuration { KeepSrc: false, Editor: "", EditorFlags: "", + Pager: "", Devel: false, MakepkgBin: "makepkg", MakepkgConf: "",