mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Running the tests in an exported source tarball fails with "must run in lazy project folder or child folder". GetLazyRootDirectory searches the working directory and its parents for a .git directory, and a tarball doesn't have one. This has always affected the integration tests; since34da956f5da unit test calls the function too, so now even `go test ./... -short` fails. Search for the go.mod file that declares lazygit's module instead. It ships in tarballs, and there is exactly one of it per source tree. Put the function in our own pkg/utils rather than change lazycore's; the criterion is specific to lazygit, and I don't feel like making a change to lazycore. Return an error rather than call log.Fatal, and report it from the two callers that run under `go test`. In a test binary, log.Fatal exits without attributing the failure to any test. That is the failure mode34da956f5dset out to remove. The remaining callers are development tools that have nothing useful to do without the root directory; they keep exiting, now through MustFindLazygitRootDirectory. Also stop the search at the root of the file system rather than at "/". On Windows the old loop walks up to "C:\" and then spins there forever.
262 lines
6.6 KiB
Go
262 lines
6.6 KiB
Go
package jsonschema
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/karimkhaleel/jsonschema"
|
|
"github.com/samber/lo"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Node struct {
|
|
Name string
|
|
Description string
|
|
Default any
|
|
Children []*Node
|
|
}
|
|
|
|
const (
|
|
IndentLevel = 2
|
|
DocumentationCommentStart = "<!-- START CONFIG YAML: AUTOMATICALLY GENERATED with `go generate ./..., DO NOT UPDATE MANUALLY -->\n"
|
|
DocumentationCommentEnd = "<!-- END CONFIG YAML -->"
|
|
DocumentationCommentStartLen = len(DocumentationCommentStart)
|
|
)
|
|
|
|
func insertBlankLines(buffer bytes.Buffer) bytes.Buffer {
|
|
lines := strings.Split(strings.TrimRight(buffer.String(), "\n"), "\n")
|
|
|
|
var newBuffer bytes.Buffer
|
|
|
|
previousIndent := -1
|
|
wasComment := false
|
|
|
|
for _, line := range lines {
|
|
trimmedLine := strings.TrimLeft(line, " ")
|
|
indent := len(line) - len(trimmedLine)
|
|
isComment := strings.HasPrefix(trimmedLine, "#")
|
|
if isComment && !wasComment && indent <= previousIndent {
|
|
newBuffer.WriteString("\n")
|
|
}
|
|
newBuffer.WriteString(line)
|
|
newBuffer.WriteString("\n")
|
|
previousIndent = indent
|
|
wasComment = isComment
|
|
}
|
|
|
|
return newBuffer
|
|
}
|
|
|
|
func prepareMarshalledConfig(buffer bytes.Buffer) []byte {
|
|
buffer = insertBlankLines(buffer)
|
|
|
|
// Remove all `---` lines
|
|
lines := strings.Split(strings.TrimRight(buffer.String(), "\n"), "\n")
|
|
|
|
var newBuffer bytes.Buffer
|
|
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) != "---" {
|
|
newBuffer.WriteString(line)
|
|
newBuffer.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
config := newBuffer.Bytes()
|
|
|
|
// Add markdown yaml block tag
|
|
config = append([]byte("```yaml\n"), config...)
|
|
config = append(config, []byte("```\n")...)
|
|
|
|
return config
|
|
}
|
|
|
|
func wrapLine(line string, maxLineLength int) []string {
|
|
result := []string{}
|
|
startOfLine := 0
|
|
lastSpaceIdx := -1
|
|
for i, r := range line {
|
|
// Don't break on "See https://..." lines
|
|
if r == ' ' && line[startOfLine:i] != "See" {
|
|
lastSpaceIdx = i + 1
|
|
} else if i-startOfLine >= maxLineLength && lastSpaceIdx != -1 {
|
|
result = append(result, line[startOfLine:lastSpaceIdx-1])
|
|
startOfLine = lastSpaceIdx
|
|
lastSpaceIdx = -1
|
|
}
|
|
}
|
|
result = append(result, line[startOfLine:])
|
|
return result
|
|
}
|
|
|
|
func setComment(yamlNode *yaml.Node, description string) {
|
|
lines := strings.Split(description, "\n")
|
|
wrappedLines := lo.Flatten(lo.Map(lines,
|
|
func(line string, _ int) []string { return wrapLine(line, 78) }))
|
|
|
|
// Workaround for the way yaml formats the HeadComment if it contains
|
|
// blank lines: it renders these without a leading "#", but we want a
|
|
// leading "#" even on blank lines. However, yaml respects it if the
|
|
// HeadComment already contains a leading "#", so we prefix all lines
|
|
// (including blank ones) with "#".
|
|
yamlNode.HeadComment = strings.Join(
|
|
lo.Map(wrappedLines, func(s string, _ int) string {
|
|
if s == "" {
|
|
return "#" // avoid trailing space on blank lines
|
|
}
|
|
return "# " + s
|
|
}),
|
|
"\n")
|
|
}
|
|
|
|
func (n *Node) MarshalYAML() (any, error) {
|
|
node := yaml.Node{
|
|
Kind: yaml.MappingNode,
|
|
}
|
|
|
|
keyNode := yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
Value: n.Name,
|
|
}
|
|
if n.Description != "" {
|
|
setComment(&keyNode, n.Description)
|
|
}
|
|
|
|
if len(n.Children) > 0 {
|
|
childrenNode := yaml.Node{
|
|
Kind: yaml.MappingNode,
|
|
}
|
|
for _, child := range n.Children {
|
|
childYaml, err := child.MarshalYAML()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
childKey := yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
Value: child.Name,
|
|
}
|
|
if child.Description != "" {
|
|
setComment(&childKey, child.Description)
|
|
}
|
|
childYaml = childYaml.(*yaml.Node)
|
|
childrenNode.Content = append(childrenNode.Content, childYaml.(*yaml.Node).Content...)
|
|
}
|
|
node.Content = append(node.Content, &keyNode, &childrenNode)
|
|
} else {
|
|
valueNode := yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
}
|
|
err := valueNode.Encode(n.Default)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
node.Content = append(node.Content, &keyNode, &valueNode)
|
|
}
|
|
|
|
return &node, nil
|
|
}
|
|
|
|
func writeToConfigDocs(config []byte) error {
|
|
configPath := utils.MustFindLazygitRootDirectory() + "/docs-master/Config.md"
|
|
markdown, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("Error reading Config.md file %w", err)
|
|
}
|
|
|
|
startConfigSectionIndex := bytes.Index(markdown, []byte(DocumentationCommentStart))
|
|
if startConfigSectionIndex == -1 {
|
|
return errors.New("Default config starting comment not found")
|
|
}
|
|
|
|
endConfigSectionIndex := bytes.Index(markdown[startConfigSectionIndex+DocumentationCommentStartLen:], []byte(DocumentationCommentEnd))
|
|
if endConfigSectionIndex == -1 {
|
|
return errors.New("Default config closing comment not found")
|
|
}
|
|
|
|
endConfigSectionIndex = endConfigSectionIndex + startConfigSectionIndex + DocumentationCommentStartLen
|
|
|
|
newMarkdown := make([]byte, 0, len(markdown)-endConfigSectionIndex+startConfigSectionIndex+len(config))
|
|
newMarkdown = append(newMarkdown, markdown[:startConfigSectionIndex+DocumentationCommentStartLen]...)
|
|
newMarkdown = append(newMarkdown, config...)
|
|
newMarkdown = append(newMarkdown, markdown[endConfigSectionIndex:]...)
|
|
|
|
if err := os.WriteFile(configPath, newMarkdown, 0o644); err != nil {
|
|
return fmt.Errorf("Error writing to file %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GenerateConfigDocs(schema *jsonschema.Schema) {
|
|
rootNode := &Node{
|
|
Children: make([]*Node, 0),
|
|
}
|
|
|
|
recurseOverSchema(schema, schema.Definitions["UserConfig"], rootNode)
|
|
|
|
var buffer bytes.Buffer
|
|
encoder := yaml.NewEncoder(&buffer)
|
|
encoder.SetIndent(IndentLevel)
|
|
|
|
for _, child := range rootNode.Children {
|
|
err := encoder.Encode(child)
|
|
if err != nil {
|
|
panic("Failed to Marshal document")
|
|
}
|
|
}
|
|
encoder.Close()
|
|
|
|
config := prepareMarshalledConfig(buffer)
|
|
|
|
err := writeToConfigDocs(config)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func recurseOverSchema(rootSchema, schema *jsonschema.Schema, parent *Node) {
|
|
if schema == nil || schema.Properties == nil || schema.Properties.Len() == 0 {
|
|
return
|
|
}
|
|
|
|
for pair := schema.Properties.Oldest(); pair != nil; pair = pair.Next() {
|
|
subSchema := getSubSchema(rootSchema, schema, pair.Key)
|
|
|
|
if strings.Contains(strings.ToLower(subSchema.Description), "deprecated") {
|
|
continue
|
|
}
|
|
|
|
node := Node{
|
|
Name: pair.Key,
|
|
Description: subSchema.Description,
|
|
Default: getZeroValue(subSchema.Default, subSchema.Type),
|
|
}
|
|
parent.Children = append(parent.Children, &node)
|
|
recurseOverSchema(rootSchema, subSchema, &node)
|
|
}
|
|
}
|
|
|
|
func getZeroValue(val any, t string) any {
|
|
if !isZeroValue(val) {
|
|
return val
|
|
}
|
|
|
|
switch t {
|
|
case "string":
|
|
return ""
|
|
case "boolean":
|
|
return false
|
|
case "object":
|
|
return map[string]any{}
|
|
case "array":
|
|
return []any{}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|