mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-13 00:56:25 -04:00
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) <noreply@anthropic.com>
218 lines
6.6 KiB
Go
218 lines
6.6 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/constants"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
func (config *UserConfig) Validate() error {
|
|
if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView,
|
|
[]string{"dashboard", "allBranchesLog"}); err != nil {
|
|
return err
|
|
}
|
|
if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch,
|
|
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
|
|
return err
|
|
}
|
|
if err := validateEnum("gui.fileTreeSortOrder", config.Gui.FileTreeSortOrder,
|
|
[]string{"mixed", "filesFirst", "foldersFirst"}); err != nil {
|
|
return err
|
|
}
|
|
if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches,
|
|
[]string{"none", "onlyMainBranches", "allBranches"}); err != nil {
|
|
return err
|
|
}
|
|
if err := validateEnum("git.localBranchSortOrder", config.Git.LocalBranchSortOrder,
|
|
[]string{"date", "recency", "alphabetical"}); err != nil {
|
|
return err
|
|
}
|
|
if err := validateEnum("git.remoteBranchSortOrder", config.Git.RemoteBranchSortOrder,
|
|
[]string{"date", "alphabetical"}); err != nil {
|
|
return err
|
|
}
|
|
if err := validateEnum("git.log.order", config.Git.Log.Order,
|
|
[]string{"date-order", "author-date-order", "topo-order", "default"}); err != nil {
|
|
return err
|
|
}
|
|
if err := validateEnum("git.log.showGraph", config.Git.Log.ShowGraph,
|
|
[]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
|
|
}
|
|
if err := validateCustomCommands(config.CustomCommands); err != nil {
|
|
return err
|
|
}
|
|
if err := validateSpinner(config.Gui.Spinner); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSpinner(spinner SpinnerConfig) error {
|
|
if len(spinner.Frames) == 0 {
|
|
return errors.New("gui.spinner.frames must not be empty.")
|
|
}
|
|
firstWidth := utils.StringWidth(spinner.Frames[0])
|
|
if lo.SomeBy(spinner.Frames, func(frame string) bool {
|
|
return utils.StringWidth(frame) != firstWidth
|
|
}) {
|
|
return errors.New("All gui.spinner.frames entries must have the same width.")
|
|
}
|
|
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
|
|
}
|
|
allowedValuesStr := strings.Join(allowedValues, ", ")
|
|
return fmt.Errorf("Unexpected value '%s' for '%s'. Allowed values: %s", value, name, allowedValuesStr)
|
|
}
|
|
|
|
func validateKeybindingsRecurse(path string, node any) error {
|
|
value := reflect.ValueOf(node)
|
|
if value.Kind() == reflect.Struct {
|
|
for _, field := range reflect.VisibleFields(reflect.TypeOf(node)) {
|
|
var newPath string
|
|
if len(path) == 0 {
|
|
newPath = field.Name
|
|
} else {
|
|
newPath = fmt.Sprintf("%s.%s", path, field.Name)
|
|
}
|
|
if err := validateKeybindingsRecurse(newPath,
|
|
value.FieldByName(field.Name).Interface()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if value.Kind() == reflect.Slice {
|
|
for i := range value.Len() {
|
|
if err := validateKeybindingsRecurse(
|
|
fmt.Sprintf("%s[%d]", path, i), value.Index(i).Interface()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if value.Kind() == reflect.String {
|
|
key := node.(string)
|
|
if !isValidKeybindingKey(key) {
|
|
return fmt.Errorf("Unrecognized key '%s' for keybinding '%s'. For permitted values see %s",
|
|
key, path, constants.Links.Docs.CustomKeybindings)
|
|
}
|
|
} else {
|
|
log.Fatalf("Unexpected type for property '%s': %s", path, value.Kind())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateKeybindings(keybindingConfig KeybindingConfig) error {
|
|
if err := validateKeybindingsRecurse("", keybindingConfig); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(keybindingConfig.Universal.JumpToBlock) != 5 {
|
|
return fmt.Errorf("keybinding.universal.jumpToBlock must have 5 elements; found %d.",
|
|
len(keybindingConfig.Universal.JumpToBlock))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateCustomCommandKey(key Keybinding) error {
|
|
for _, k := range key {
|
|
if !isValidKeybindingKey(k) {
|
|
return fmt.Errorf("Unrecognized key '%s' for custom command. For permitted values see %s",
|
|
k, constants.Links.Docs.CustomKeybindings)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateCustomCommands(customCommands []CustomCommand) error {
|
|
for _, customCommand := range customCommands {
|
|
if err := validateCustomCommandKey(customCommand.Key); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(customCommand.CommandMenu) > 0 {
|
|
if len(customCommand.Context) > 0 ||
|
|
len(customCommand.Command) > 0 ||
|
|
len(customCommand.Prompts) > 0 ||
|
|
len(customCommand.LoadingText) > 0 ||
|
|
len(customCommand.Output) > 0 ||
|
|
len(customCommand.OutputTitle) > 0 ||
|
|
customCommand.After != nil {
|
|
commandRef := ""
|
|
if len(customCommand.Key) > 0 {
|
|
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key.String())
|
|
}
|
|
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
|
|
}
|
|
|
|
if err := validateCustomCommands(customCommand.CommandMenu); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
for _, prompt := range customCommand.Prompts {
|
|
if err := validateCustomCommandPrompt(prompt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := validateEnum("customCommand.output", customCommand.Output,
|
|
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateCustomCommandPrompt(prompt CustomCommandPrompt) error {
|
|
for _, option := range prompt.Options {
|
|
for _, k := range option.Key {
|
|
if !isValidKeybindingKey(k) {
|
|
return fmt.Errorf("Unrecognized key '%s' for custom command prompt option. For permitted values see %s",
|
|
k, constants.Links.Docs.CustomKeybindings)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|