From 81420ce36204f5c4ff3764a330bfce1dddbe0634 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 7 Jun 2026 14:08:02 +0200 Subject: [PATCH] Reject pager entries that combine multiple diff mechanisms A pager (GIT_PAGER) formats the diff git produces, while externalDiffCommand and useExternalDiffGitConfig change how git produces the diff in the first place. They are different pipeline stages, not alternatives, so combining them on one entry just pipes one through the other and produces garbled output (e.g. delta trying to parse difftastic's side-by-side output as a unified diff). The two external mechanisms likewise conflict, with the explicit command silently shadowing the git config one. Treat all three as mutually exclusive and reject configs that set more than one on the same entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs-master/Config.md | 3 +++ docs-master/Custom_Pagers.md | 4 ++- pkg/config/user_config.go | 2 ++ pkg/config/user_config_validation.go | 27 ++++++++++++++++++++ pkg/config/user_config_validation_test.go | 31 +++++++++++++++++++++++ schema-master/config.json | 2 +- 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/docs-master/Config.md b/docs-master/Config.md index 9f7921821..6de2ad978 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -361,6 +361,9 @@ git: # # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver. # useExternalDiffGitConfig: false # + # 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' are mutually + # exclusive; set at most one per entry. + # # See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md # for more information. pagers: [] diff --git a/docs-master/Custom_Pagers.md b/docs-master/Custom_Pagers.md index 903928d46..0bfffe7dc 100644 --- a/docs-master/Custom_Pagers.md +++ b/docs-master/Custom_Pagers.md @@ -71,7 +71,7 @@ git: - externalDiffCommand: difft --color=always ``` -The `colorArg` and `pager` options are not used in this case. +The `colorArg` option is not used in this case. You can add whatever extra arguments you prefer for your difftool; for instance @@ -91,6 +91,8 @@ git: This can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver. +`pager`, `externalDiffCommand`, and `useExternalDiffGitConfig` are alternative ways of producing the diff, so a pager entry may use at most one of them. + ## Emulating custom pagers on Windows There is a trick to emulate custom pagers on Windows using a Powershell script configured as an external diff command. It's not perfect, but certainly better than nothing. To do this, save the following script as `lazygit-pager.ps1` at a convenient place on your disk: diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index cac87ec91..acadc8e80 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -275,6 +275,8 @@ type GitConfig struct { // # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver. // useExternalDiffGitConfig: false // + // 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' are mutually exclusive; set at most one per entry. + // // See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md for more information. Pagers []PagingConfig `yaml:"pagers"` // Config relating to committing diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 163fc61c4..109b3f1d0 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -46,6 +46,9 @@ func (config *UserConfig) Validate() error { []string{"always", "never", "when-maximised"}); err != nil { return err } + if err := validatePagers(config.Git.Pagers); err != nil { + return err + } if err := validateKeybindings(config.Keybinding); err != nil { return err } @@ -71,6 +74,30 @@ func validateSpinner(spinner SpinnerConfig) error { return nil } +// validatePagers rejects pager entries that combine more than one diff +// mechanism. A pager (GIT_PAGER) formats the diff that git produces, whereas +// externalDiffCommand and useExternalDiffGitConfig change how git produces the +// diff in the first place; piping one through the other almost always yields +// garbled output, so we treat the three as mutually exclusive. +func validatePagers(pagers []PagingConfig) error { + for i, pager := range pagers { + count := 0 + if pager.Pager != "" { + count++ + } + if pager.ExternalDiffCommand != "" { + count++ + } + if pager.UseExternalDiffGitConfig { + count++ + } + if count > 1 { + return fmt.Errorf("git.pagers[%d]: at most one of 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' may be set; they are mutually exclusive", i) + } + } + return nil +} + func validateEnum(name string, value string, allowedValues []string) error { if slices.Contains(allowedValues, value) { return nil diff --git a/pkg/config/user_config_validation_test.go b/pkg/config/user_config_validation_test.go index bb2d2580f..26c9b7145 100644 --- a/pkg/config/user_config_validation_test.go +++ b/pkg/config/user_config_validation_test.go @@ -323,3 +323,34 @@ func TestUserConfigValidate_spinnerFrames(t *testing.T) { }) } } + +func TestUserConfigValidate_pagers(t *testing.T) { + scenarios := []struct { + name string + pager PagingConfig + valid bool + }{ + {name: "empty", pager: PagingConfig{}, valid: true}, + {name: "pager only", pager: PagingConfig{Pager: "delta"}, valid: true}, + {name: "external diff command only", pager: PagingConfig{ExternalDiffCommand: "difft"}, valid: true}, + {name: "git config external diff only", pager: PagingConfig{UseExternalDiffGitConfig: true}, valid: true}, + {name: "pager and external diff command", pager: PagingConfig{Pager: "delta", ExternalDiffCommand: "difft"}, valid: false}, + {name: "pager and git config external diff", pager: PagingConfig{Pager: "delta", UseExternalDiffGitConfig: true}, valid: false}, + {name: "both external diff mechanisms", pager: PagingConfig{ExternalDiffCommand: "difft", UseExternalDiffGitConfig: true}, valid: false}, + {name: "all three", pager: PagingConfig{Pager: "delta", ExternalDiffCommand: "difft", UseExternalDiffGitConfig: true}, valid: false}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + config := GetDefaultConfig() + config.Git.Pagers = []PagingConfig{s.pager} + err := config.Validate() + + if s.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/schema-master/config.json b/schema-master/config.json index 2e968ba8f..9042986aa 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -321,7 +321,7 @@ "$ref": "#/$defs/PagingConfig" }, "type": "array", - "description": "Array of pagers. Each entry has the following format:\n\n # Value of the --color arg in the git diff command. Some pagers want\n # this to be set to 'always' and some want it set to 'never'\n colorArg: \"always\"\n\n # e.g.\n # diff-so-fancy\n # delta --dark --paging=never\n # ydiff -p cat -s --wrap --width={{columnWidth}}\n pager: \"\"\n\n # e.g. 'difft --color=always'\n externalDiffCommand: \"\"\n\n # If true, Lazygit will use git's `diff.external` config for paging.\n # The advantage over `externalDiffCommand` is that this can be\n # configured per file type in .gitattributes; see\n # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.\n useExternalDiffGitConfig: false\n\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md for more information." + "description": "Array of pagers. Each entry has the following format:\n\n # Value of the --color arg in the git diff command. Some pagers want\n # this to be set to 'always' and some want it set to 'never'\n colorArg: \"always\"\n\n # e.g.\n # diff-so-fancy\n # delta --dark --paging=never\n # ydiff -p cat -s --wrap --width={{columnWidth}}\n pager: \"\"\n\n # e.g. 'difft --color=always'\n externalDiffCommand: \"\"\n\n # If true, Lazygit will use git's `diff.external` config for paging.\n # The advantage over `externalDiffCommand` is that this can be\n # configured per file type in .gitattributes; see\n # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.\n useExternalDiffGitConfig: false\n\n'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' are mutually exclusive; set at most one per entry.\n\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md for more information." }, "commit": { "$ref": "#/$defs/CommitConfig",