Skip to content

Commit b75c177

Browse files
authored
Add default lazygit config generation in Config.md from JSON schema (#3565)
- **PR Description** This uses the JSON schema generated in #3039 to generate and replace the default lazygit config in Config.md when running `go generate ./...` Relevant issue: #3441 The generated config contains all the entries that have default values set in `user_config.go`
2 parents 6fcb7eb + 9b152d7 commit b75c177

File tree

11 files changed

+1602
-726
lines changed

11 files changed

+1602
-726
lines changed

docs/Config.md

Lines changed: 441 additions & 206 deletions
Large diffs are not rendered by default.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/gdamore/tcell/v2 v2.7.4
1212
github.com/go-errors/errors v1.5.1
1313
github.com/gookit/color v1.4.2
14+
github.com/iancoleman/orderedmap v0.3.0
1415
github.com/imdario/mergo v0.3.11
1516
github.com/integrii/flaggy v1.4.0
1617
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
171171
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
172172
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
173173
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
174+
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
175+
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
174176
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
175177
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
176178
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=

pkg/config/user_config.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ type UserConfig struct {
1919
ConfirmOnQuit bool `yaml:"confirmOnQuit"`
2020
// If true, exit Lazygit when the user presses escape in a context where there is nothing to cancel/close
2121
QuitOnTopLevelReturn bool `yaml:"quitOnTopLevelReturn"`
22-
// Keybindings
23-
Keybinding KeybindingConfig `yaml:"keybinding"`
2422
// Config relating to things outside of Lazygit like how files are opened, copying to clipboard, etc
2523
OS OSConfig `yaml:"os,omitempty"`
2624
// If true, don't display introductory popups upon opening Lazygit.
@@ -38,6 +36,8 @@ type UserConfig struct {
3836
NotARepository string `yaml:"notARepository" jsonschema:"enum=prompt,enum=create,enum=skip,enum=quit"`
3937
// If true, display a confirmation when subprocess terminates. This allows you to view the output of the subprocess before returning to Lazygit.
4038
PromptToReturnFromSubprocess bool `yaml:"promptToReturnFromSubprocess"`
39+
// Keybindings
40+
Keybinding KeybindingConfig `yaml:"keybinding"`
4141
}
4242

4343
type RefresherConfig struct {
@@ -252,7 +252,7 @@ type PagingConfig struct {
252252
// diff-so-fancy
253253
// delta --dark --paging=never
254254
// ydiff -p cat -s --wrap --width={{columnWidth}}
255-
Pager PagerType `yaml:"pager" jsonschema:"minLength=1"`
255+
Pager PagerType `yaml:"pager"`
256256
// If true, Lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
257257
UseConfig bool `yaml:"useConfig"`
258258
// e.g. 'difft --color=always'
@@ -294,9 +294,9 @@ type LogConfig struct {
294294

295295
type CommitPrefixConfig struct {
296296
// pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use "^\\w+\\/(\\w+-\\w+).*"
297-
Pattern string `yaml:"pattern" jsonschema:"example=^\\w+\\/(\\w+-\\w+).*,minLength=1"`
297+
Pattern string `yaml:"pattern" jsonschema:"example=^\\w+\\/(\\w+-\\w+).*"`
298298
// Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use "[$1] "
299-
Replace string `yaml:"replace" jsonschema:"example=[$1] ,minLength=1"`
299+
Replace string `yaml:"replace" jsonschema:"example=[$1]"`
300300
}
301301

302302
type UpdateConfig struct {
@@ -684,6 +684,7 @@ func GetDefaultConfig() *UserConfig {
684684
CommandLogSize: 8,
685685
SplitDiff: "auto",
686686
SkipRewordInEditorWarning: false,
687+
WindowSize: "normal",
687688
Border: "rounded",
688689
AnimateExplosion: true,
689690
PortraitMode: "auto",
@@ -735,8 +736,14 @@ func GetDefaultConfig() *UserConfig {
735736
Method: "prompt",
736737
Days: 14,
737738
},
738-
ConfirmOnQuit: false,
739-
QuitOnTopLevelReturn: false,
739+
ConfirmOnQuit: false,
740+
QuitOnTopLevelReturn: false,
741+
OS: OSConfig{},
742+
DisableStartupPopups: false,
743+
CustomCommands: []CustomCommand(nil),
744+
Services: map[string]string(nil),
745+
NotARepository: "prompt",
746+
PromptToReturnFromSubprocess: true,
740747
Keybinding: KeybindingConfig{
741748
Universal: KeybindingUniversalConfig{
742749
Quit: "q",
@@ -903,11 +910,5 @@ func GetDefaultConfig() *UserConfig {
903910
CommitMenu: "<c-o>",
904911
},
905912
},
906-
OS: OSConfig{},
907-
DisableStartupPopups: false,
908-
CustomCommands: []CustomCommand(nil),
909-
Services: map[string]string(nil),
910-
NotARepository: "prompt",
911-
PromptToReturnFromSubprocess: true,
912913
}
913914
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package jsonschema
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"github.com/iancoleman/orderedmap"
12+
"github.com/jesseduffield/lazycore/pkg/utils"
13+
"github.com/samber/lo"
14+
15+
"gopkg.in/yaml.v3"
16+
)
17+
18+
type Node struct {
19+
Name string
20+
Description string
21+
Default any
22+
Children []*Node
23+
}
24+
25+
const (
26+
IndentLevel = 2
27+
DocumentationCommentStart = "<!-- START CONFIG YAML: AUTOMATICALLY GENERATED with `go generate ./..., DO NOT UPDATE MANUALLY -->\n"
28+
DocumentationCommentEnd = "<!-- END CONFIG YAML -->"
29+
DocumentationCommentStartLen = len(DocumentationCommentStart)
30+
)
31+
32+
func insertBlankLines(buffer bytes.Buffer) bytes.Buffer {
33+
lines := strings.Split(strings.TrimRight(buffer.String(), "\n"), "\n")
34+
35+
var newBuffer bytes.Buffer
36+
37+
previousIndent := -1
38+
wasComment := false
39+
40+
for _, line := range lines {
41+
trimmedLine := strings.TrimLeft(line, " ")
42+
indent := len(line) - len(trimmedLine)
43+
isComment := strings.HasPrefix(trimmedLine, "#")
44+
if isComment && !wasComment && indent <= previousIndent {
45+
newBuffer.WriteString("\n")
46+
}
47+
newBuffer.WriteString(line)
48+
newBuffer.WriteString("\n")
49+
previousIndent = indent
50+
wasComment = isComment
51+
}
52+
53+
return newBuffer
54+
}
55+
56+
func prepareMarshalledConfig(buffer bytes.Buffer) []byte {
57+
buffer = insertBlankLines(buffer)
58+
59+
// Remove all `---` lines
60+
lines := strings.Split(strings.TrimRight(buffer.String(), "\n"), "\n")
61+
62+
var newBuffer bytes.Buffer
63+
64+
for _, line := range lines {
65+
if strings.TrimSpace(line) != "---" {
66+
newBuffer.WriteString(line)
67+
newBuffer.WriteString("\n")
68+
}
69+
}
70+
71+
config := newBuffer.Bytes()
72+
73+
// Add markdown yaml block tag
74+
config = append([]byte("```yaml\n"), config...)
75+
config = append(config, []byte("```\n")...)
76+
77+
return config
78+
}
79+
80+
func setComment(yamlNode *yaml.Node, description string) {
81+
// Workaround for the way yaml formats the HeadComment if it contains
82+
// blank lines: it renders these without a leading "#", but we want a
83+
// leading "#" even on blank lines. However, yaml respects it if the
84+
// HeadComment already contains a leading "#", so we prefix all lines
85+
// (including blank ones) with "#".
86+
yamlNode.HeadComment = strings.Join(
87+
lo.Map(strings.Split(description, "\n"), func(s string, _ int) string {
88+
if s == "" {
89+
return "#" // avoid trailing space on blank lines
90+
}
91+
return "# " + s
92+
}),
93+
"\n")
94+
}
95+
96+
func (n *Node) MarshalYAML() (interface{}, error) {
97+
node := yaml.Node{
98+
Kind: yaml.MappingNode,
99+
}
100+
101+
keyNode := yaml.Node{
102+
Kind: yaml.ScalarNode,
103+
Value: n.Name,
104+
}
105+
if n.Description != "" {
106+
setComment(&keyNode, n.Description)
107+
}
108+
109+
if n.Default != nil {
110+
valueNode := yaml.Node{
111+
Kind: yaml.ScalarNode,
112+
}
113+
err := valueNode.Encode(n.Default)
114+
if err != nil {
115+
return nil, err
116+
}
117+
node.Content = append(node.Content, &keyNode, &valueNode)
118+
} else if len(n.Children) > 0 {
119+
childrenNode := yaml.Node{
120+
Kind: yaml.MappingNode,
121+
}
122+
for _, child := range n.Children {
123+
childYaml, err := child.MarshalYAML()
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
childKey := yaml.Node{
129+
Kind: yaml.ScalarNode,
130+
Value: child.Name,
131+
}
132+
if child.Description != "" {
133+
setComment(&childKey, child.Description)
134+
}
135+
childYaml = childYaml.(*yaml.Node)
136+
childrenNode.Content = append(childrenNode.Content, childYaml.(*yaml.Node).Content...)
137+
}
138+
node.Content = append(node.Content, &keyNode, &childrenNode)
139+
}
140+
141+
return &node, nil
142+
}
143+
144+
func getDescription(v *orderedmap.OrderedMap) string {
145+
description, ok := v.Get("description")
146+
if !ok {
147+
description = ""
148+
}
149+
return description.(string)
150+
}
151+
152+
func getDefault(v *orderedmap.OrderedMap) (error, any) {
153+
defaultValue, ok := v.Get("default")
154+
if ok {
155+
return nil, defaultValue
156+
}
157+
158+
dataType, ok := v.Get("type")
159+
if ok {
160+
dataTypeString := dataType.(string)
161+
if dataTypeString == "string" {
162+
return nil, ""
163+
}
164+
}
165+
166+
return errors.New("Failed to get default value"), nil
167+
}
168+
169+
func parseNode(parent *Node, name string, value *orderedmap.OrderedMap) {
170+
description := getDescription(value)
171+
err, defaultValue := getDefault(value)
172+
if err == nil {
173+
leaf := &Node{Name: name, Description: description, Default: defaultValue}
174+
parent.Children = append(parent.Children, leaf)
175+
}
176+
177+
properties, ok := value.Get("properties")
178+
if !ok {
179+
return
180+
}
181+
182+
orderedProperties := properties.(orderedmap.OrderedMap)
183+
184+
node := &Node{Name: name, Description: description}
185+
parent.Children = append(parent.Children, node)
186+
187+
keys := orderedProperties.Keys()
188+
for _, name := range keys {
189+
value, _ := orderedProperties.Get(name)
190+
typedValue := value.(orderedmap.OrderedMap)
191+
parseNode(node, name, &typedValue)
192+
}
193+
}
194+
195+
func writeToConfigDocs(config []byte) error {
196+
configPath := utils.GetLazyRootDirectory() + "/docs/Config.md"
197+
markdown, err := os.ReadFile(configPath)
198+
if err != nil {
199+
return fmt.Errorf("Error reading Config.md file %w", err)
200+
}
201+
202+
startConfigSectionIndex := bytes.Index(markdown, []byte(DocumentationCommentStart))
203+
if startConfigSectionIndex == -1 {
204+
return errors.New("Default config starting comment not found")
205+
}
206+
207+
endConfigSectionIndex := bytes.Index(markdown[startConfigSectionIndex+DocumentationCommentStartLen:], []byte(DocumentationCommentEnd))
208+
if endConfigSectionIndex == -1 {
209+
return errors.New("Default config closing comment not found")
210+
}
211+
212+
endConfigSectionIndex = endConfigSectionIndex + startConfigSectionIndex + DocumentationCommentStartLen
213+
214+
newMarkdown := make([]byte, 0, len(markdown)-endConfigSectionIndex+startConfigSectionIndex+len(config))
215+
newMarkdown = append(newMarkdown, markdown[:startConfigSectionIndex+DocumentationCommentStartLen]...)
216+
newMarkdown = append(newMarkdown, config...)
217+
newMarkdown = append(newMarkdown, markdown[endConfigSectionIndex:]...)
218+
219+
if err := os.WriteFile(configPath, newMarkdown, 0o644); err != nil {
220+
return fmt.Errorf("Error writing to file %w", err)
221+
}
222+
return nil
223+
}
224+
225+
func GenerateConfigDocs() {
226+
content, err := os.ReadFile(GetSchemaDir() + "/config.json")
227+
if err != nil {
228+
panic("Error reading config.json")
229+
}
230+
231+
schema := orderedmap.New()
232+
233+
err = json.Unmarshal(content, &schema)
234+
if err != nil {
235+
panic("Failed to unmarshal config.json")
236+
}
237+
238+
root, ok := schema.Get("properties")
239+
if !ok {
240+
panic("properties key not found in schema")
241+
}
242+
orderedRoot := root.(orderedmap.OrderedMap)
243+
244+
rootNode := Node{}
245+
for _, name := range orderedRoot.Keys() {
246+
value, _ := orderedRoot.Get(name)
247+
typedValue := value.(orderedmap.OrderedMap)
248+
parseNode(&rootNode, name, &typedValue)
249+
}
250+
251+
var buffer bytes.Buffer
252+
encoder := yaml.NewEncoder(&buffer)
253+
encoder.SetIndent(IndentLevel)
254+
255+
for _, child := range rootNode.Children {
256+
err := encoder.Encode(child)
257+
if err != nil {
258+
panic("Failed to Marshal document")
259+
}
260+
}
261+
encoder.Close()
262+
263+
config := prepareMarshalledConfig(buffer)
264+
265+
err = writeToConfigDocs(config)
266+
if err != nil {
267+
panic(err)
268+
}
269+
}

pkg/jsonschema/generator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ import (
1111
func main() {
1212
fmt.Printf("Generating jsonschema in %s...\n", jsonschema.GetSchemaDir())
1313
jsonschema.GenerateSchema()
14+
jsonschema.GenerateConfigDocs()
1415
}

0 commit comments

Comments
 (0)